Git is the source code version control system that is rapidly becoming the standard for open source projects. It has a powerful distributed model which allows advanced users to do tricky things with branches, and rewriting history. What a pity that itâs so hard to learn, has such an unpleasant command line interface, and treats its users with such utter contempt.
1. Complex information model
The information model is complicated â and you need to know all of it. As a point of reference, consider Subversion: you have files, a working directory, a repository, versions, branches, and tags. Thatâs pretty much everything you need to know. In fact, branches are tags, and files you already know about, so you really need to learn three new things. Versions are linear, with the odd merge. Now Git: you have files, a working tree, an index, a local repository, a remote repository, remotes (pointers to remote repositories), commits, treeishes (pointers to commits), branches, a stash⊠and you need to know all of it.
2. Crazy command line syntax
The command line syntax is completely arbitrary and inconsistent. Some âshortcutsâ are graced with top level commands: âgit pullâ is exactly equivalent to âgit fetchâ followed by âgit mergeâ. But the shortcut for âgit branchâ combined with âgit checkoutâ? âgit checkout -bâ. Specifying filenames completely changes the semantics of some commands (âgit commitâ ignores local, unstaged changes in foo.txt; âgit commit foo.txtâ doesnât). The various options of âgit resetâ do completely different things.
The most spectacular example of this is the command âgit amâ, which as far as I can tell, is something Linus hacked up and forced into the main codebase to solve a problem he was having one night. It combines email reading with patch applying, and thus uses a different patch syntax (specifically, one with email headers at the top).
3. Crappy documentation
The man pages are one almighty âfuck youâ. They describe the commands from the perspective of a computer scientist, not a user. Case in point:
git-push â Update remote refs along with associated objects
Hereâs a description for humans: git-push â Upload changes from your local repository into a remote repository
Update, another example: (thanks cgd)
git-rebase â Forward-port local commits to the updated upstream head
Translation: git-rebase â Sequentially regenerate a series of commits so they can be applied directly to the head node
4. Information model sprawl
Remember the complicated information model in step 1? It keeps growing, like a cancer. Keep using Git, and more concepts will occasionally drop out of the sky: refs, tags, the reflog, fast-forward commits, detached head state (!), remote branches, tracking, namespaces
5. Leaky abstraction
Git doesnât so much have a leaky abstraction as no abstraction. There is essentially no distinction between implementation detail and user interface. Itâs understandable that an advanced user might need to know a little about how features are implemented, to grasp subtleties about various commands. But even beginners are quickly confronted with hideous internal details. In theory, there is the âplumbingâ and âthe porcelainâ â but youâd better be a plumber to know how to work the porcelain.
A common response I get to complaints about Gitâs command line complexity is that âyou donât need to use all those commands, you can use it like Subversion if thatâs what you really wantâ. Rubbish. Thatâs like telling an old granny that the freeway isnât scary, she can drive at 20kph in the left lane if she wants. Git doesnât provide any useful subsets â every command soon requires another; even simple actions often require complex actions to undo or refine.
Here was the (well-intentioned!) advice from a GitHub maintainer of a project Iâm working on (with apologies!):
- Find the merge base between your branch and master: âgit merge-base master yourbranchâ
- Assuming youâve already committed your changes, rebased your commit onto the merge base, then create a new branch:
- git rebase âonto <basecommit> HEAD~1 HEAD
- git checkout -b my-new-branch
- Checkout your ruggedisation branch, and remove the commit you just rebased: âgit reset âhard HEAD~1â
- Merge your new branch back into ruggedisation: âgit merge my-new-branchâ
- Checkout master (âgit checkout masterâ), merge your new branch in (âgit merge my-new-branchâ), and check it works when merged, then remove the merge (âgit reset âhard HEAD~1â).
- Push your new branch (âgit push origin my-new-branchâ) and log a pull request.
Translation: âItâs easy, Granny. Just rev to 6000, dump the clutch, and use wheel spin to get round the first corner. Up to third, then trail brake onto the freeway, late apexing but watch the marbles on the inside. Hard up to fifth, then handbrake turn to make the exit.â
6. Power for the maintainer, at the expense of the contributor
Most of the power of Git is aimed squarely at maintainers of codebases: people who have to merge contributions from a wide number of different sources, or who have to ensure a number of parallel development efforts result in a single, coherent, stable release. This is good. But the majority of Git users are not in this situation: they simply write code, often on a single branch for months at a time. Git is a 4 handle, dual boiler espresso machine â when all they need is instant.
Interestingly, I donât think this trade-off is inherent in Gitâs  design. Itâs simply the result of ignoring the needs of normal users, and confusing architecture with interface. âGit is goodâ is true if speaking of architecture â but false of user interface. Someone could quite conceivably write an improved interface (easygit is a start) that hides unhelpful complexity such as the index and the local repository.
7. Unsafe version control
The fundamental promise of any version control system is this: âOnce you put your precious source code in here, itâs safe. You can make any changes you like, and you can always get it backâ. Git breaks this promise. Several ways a committer can irrevocably destroy the contents of a repository:
- git add . / ⊠/ git push -f origin master
- git push origin +master
- git rebase -i <some commit that has already been pushed and worked from> / git push
8. Burden of VCS maintainance pushed to contributors
In the traditional open source project, only one person had to deal with the complexities of branches and merges: the maintainer. Everyone else only had to update, commit, update, commit, update, commit⊠Git dumps the burden of  understanding complex version control on everyone â while making the maintainerâs job easier. Why would you do this to new contributors â those with nothing invested in the project, and every incentive to throw their hands up and leave?
9. Git history is a bunch of lies
The primary output of development work should be source code. Is a well-maintained history really such an important by-product? Most of the arguments for rebase, in particular, rely on aesthetic judgments about âmessy mergesâ in the history, or âunreadable logsâ. So rebase encourages you to lie in order to provide other developers with a âcleanâ, âunclutteredâ history. Surely the correct solution is a better log output that can filter out these unwanted merges.
10. Simple tasks need so many commands
The point of working on an open source project is to make some changes, then share them with the world. In Subversion, this looks like:
- Make some changes
- svn commit
If your changes involve creating new files, thereâs a tricky extra step:
- Make some changes
- svn add
- svn commit
For a Github-hosted project, the following is basically the bare minimum:
- Make some changes
- git add [not to be confused with svn add]
- git commit
- git push
- Your changes are still only halfway there. Now login to Github, find your commit, and issue a âpull requestâ so that someone downstream can merge it.
In reality though, the maintainer of that Github-hosted project will probably prefer your changes to be on feature branches. Theyâll ask you to work like this:
- git checkout master [to make sure each new feature starts from the baseline]
- git checkout -b newfeature
- Make some changes
- git add [not to be confused with svn add]
- git commit
- git push
- Now login to Github, switch to your newfeature branch, and issue a âpull requestâ so that the maintainer can merge it.
So, to move your changes from your local directory to the actual project repository will be: add, commit, push, âclick pull requestâ, pull, merge, push. (I think)
As an added bonus, hereâs a diagram illustrating the commands a typical developer on a traditional Subversion project needed to know about to get their work done. This is the bread and butter of VCS: checking out a repository, committing changes, and getting updates.
âBread and butterâ commands and concepts needed to work with a remote Subversion repository.
And now hereâs what you need to deal with for a typical Github-hosted project:
The âbread and butterâ commands and concepts needed to work with a Github-hosted project.
If the power of Git is sophisticated branching and merging, then its weakness is the complexity of simple tasks.
Update (August 3, 2012)
This post has obviously struck a nerve, and gets a lot of traffic. Thought Iâd address some of the most frequent comments.
- The comparison between a Subversion repository with commit access and a Git repository without it isnât fair True. But thatâs been my experience: most SVN repositories Iâve seen have many committers â it works better that way. Git (or at least Github) repositories tend not to: youâre expected to submit pull requests, even after you reach the âtrustedâ stage. Perhaps someone else would like to do a fairer apples-to-apples comparison.
- Youâre just used to SVN Thereâs some truth to this, even though I havenât done a huge amount of coding in SVN-based projects. Gitâs commands and information model are still inherently difficult to learn, and the situation is not helped by using Subversion command names with different meanings (eg, âsvn addâ vs âgit addâ).
- But my life is so much better with Git, why are you against it? Iâm not â I actually quite like the architecture and what it lets you do. You can be against a UI without being against the product.
- But you only need a few basic commands to get by. That hasnât been my experience at all. You can just barely survive for a while with clone, add, commit, and checkout. But very soon you need rebase, push, pull, fetch , merge, status, log, and the annoyingly-commandless âpull requestâ. And before long, cherry-pick, reflog, etc etcâŠ
- Use Mercurial instead! Sure, if youâre the lucky person who gets to choose the VCS used by your project.
- Subversion has even worse problems! Probably. This post is about Gitâs deficiencies. Subversionâs own crappiness is no excuse.
- As a programmer, itâs worth investing time learning your tools. True, but beside the point. The point is, the tool is hard to learn and should be improved.
- If you canât understand it, you must be dumb. True to an extent, but see the previous point.
- Thereâs a flaw in point X. Youâre right. As of writing, over 80,000 people have viewed this post. Probably over 1000 have commented on it, on Reddit (530 comments), on Hacker News (250 comments), here (100 comments). All the many flaws, inaccuracies, mischaracterisations, generalisations and biases have been brought to light. If Iâd known it would be so popular, I would have tried harder. Overall, the level of debate has actually been pretty good, so thank you all.
A few bonus command inconsistencies:
Reset/checkout
To reset one file in your working directory to its committed state:
git checkout file.txt
To reset every file in your working directory to its committed state:
git reset --hard
Remotes and branches
git checkout remotename/branchname
git pull remotename branchname
Thereâs another command where the separator is remotename:branchname, but I donât recall right now.
Command options that are practically mandatory
And finally, a list of commands Iâve noticed which are almost useless without additional options.
| Base command |
Useless functionality |
Useful command |
Useful functionality |
| git branch foo |
Creates a branch but does nothing with it |
git checkout -b foo |
Creates branch and switches to it |
| git remote |
Shows names of remotes |
git remote -v |
Shows names and URLs of remotes |
| git stash |
Stores modifications to tracked files, then rolls them back |
git stash -u |
Also does the same to untracked files |
| git branch |
Lists names of local branches |
git branch -rv |
Lists local and remote tracking branches; shows latest commit message |
| git rebase |
Destroy history blindfolded |
git rebase -i |
Lets you rewrite the upstream history of a branch, choosing which commits to keep, squash, or ditch. |
| git reset foo |
Unstages files |
git reset âhard git reset âsoft |
Discards local modifications Returns to another commit, but doesnât touch working directory. |
| git add |
Nothing â prints warning |
git add . git add -A |
Stages all local modifications/additions Stages all local modifications/additions/deletions |
Update 2 (September 3, 2012)
A few interesting links: