In watching Linus Torvalds' 2007 talk on Git at Google, I had a realization: Mercurial actually fails to match the basic functionality of Git. It's not just a different flavor of distributed source code management, it's lacking. Before I get into the details, I should explain the workflows I have used as they inform my perspective and conclusions. I have used Git and Mercurial at my day jobs with small teams using a centralized workflow. The way it works is there is a central repository which each engineer clones and pushes to. I have also done less centralized development in my free time using github, mostly. There, I clone a repo, push some changes and send a pull request. Those are my workflows.
Cheap Branching: One thing that Torvalds mentions is that people using Git like to create many branches on their own repository and those branches are easy and cheap and don't bother anyone else. When I started using Git at work, this is what I did. I made local branches on my own repository clone and was able to work on various features and switch between them quickly and painlessly. When you push to a central repository with Git you must specify a branch (unless there is a default set). Only that branch you specify gets pushed. This is an important feature which Mercurial lacks. First of all, let's see this in action. I've created a simple clone of a centralized repository. There is only a single branch, master.
$ git branch # show me local branches * master $ git remote # show me remote repositories origin $ git branch -r # show me all remote branches origin/master
OK, so there is a single local branch called master and the same branch exists on the remote repository origin, which is the central repo. Simple enough. Let's start working on a feature...
$ cat foo.py print("hello") $ git branch my_feature # create a local branch $ git branch * master my_feature $ git checkout my_feature # switch to local branch Switched to branch 'my_feature' $ git branch master * my_feature $ vim foo.py $ cat foo.py print("Hello, world") $ git commit -am "improved message" [my_feature c10518b] improved message 1 files changed, 1 insertions(+), 1 deletions(-)
Let's say that feature is not yet complete, but someone quickly needs a new file added and pushed to the central repo. Here's how you handle that in Git:
$ git checkout master # switch back to master branch Switched to branch 'master' $ cat foo.py # verify that foo.py does not have changes from my_feature branch print("hello") $ vim bar.py # add a file $ cat bar.py print("this is bar") $ git status # show what's different # On branch master # Untracked files: # (use "git add <file>..." to include in what will be committed) # # bar.py nothing added to commit but untracked files present (use "git add" to track) $ git add bar.py # stage changes $ git status # here's what it looks like staged # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file: bar.py # $ git commit -m "quickly add bar.py" [master b856f5b] quickly add bar.py 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 bar.py $ git push origin master # push master branch to central repo (origin) Counting objects: 4, done. Delta compression using up to 4 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 302 bytes, done. Total 3 (delta 0), reused 0 (delta 0) Unpacking objects: 100% (3/3), done. To /home/steve/gittest/central efdbc6d..b856f5b master -> master $ git branch * master my_feature $ git branch -r # show remote branches origin/master
As you can see, I was able to switch back to the master branch, make a change and push that to the central repository without pushing the my_feature branch. In practice, it is common to be working on many different feature simultaneously on different local branches. These local branches don't pollute the branch list on the main repository, because they never get pushed, unless I want to push them.
Mercurial does not have the concept of local branches that do not get pushed to the main repository. Instead, you do not specify which branch to push because all branches get pushed. Branches you've created locally get automatically created on whatever repository you push to. It is a fundamental flaw in Mercurial. There are a variety of plugins, some even that ship by default with recent versions of Mercurial, which work around this issue. Each one of these plugins works around the issue by allowing code changes to be put aside somewhere (other than a branch on your repository) where they will not be automatically pushed. So, instead of naturally and easily creating branches locally for work that you do not want to push (yet), you are forced to do something else to temporarily store your incomplete changesets. Some of these options are difficult to understand as they use fundamentally different paradigms than simple branches. Some very closely emulate the natural local branching semantics of Git. The closest to being useful is Local Branches which does not currently ship with Mercurial. Unfortunately, since it is somewhat of a hacky workaround, it is not as fast and simple as making branches with Git. What it does is make another clone of your repository to hold these extra branches. What it boils down to is this: no matter how you deal with this shortcoming in Mercurial's design, it requires extra mental effort to use, mental effort that could be better put to writing your code, not figuring out how to make your DVCS work properly.
Staging Changes: In some ways, Mercurial is stuck in the past. It tends to see the world in terms of files, not changesets. For example, in Mercurial, if you edit the lines of a file that is already under version control, and also add a new file and then perform a commit, the changes to the existing file will be part of the commit, but the new file will not. Why? I don't know. Git sees both of these changes as equally important. If you make some changes to a file already under version control and add a new file, neither changes will be added if you perform a commit. Instead, you need to "stage" your changes with Git. In this instance, you can stage either the new file, the changes to the existing file, or both and then commit only those changes which you have staged. In other words, in Mercurial, you have the option to only commit the changes to the existing file ignoring the new file, but you are not given the opportunity to only commit the new file and not the changes to the existing file. Git allows you to do either.
This added flexibility of Git is similar, in a way, to the flexibility of Git's branching. Let's say you are working on file X on the master branch and someone says, "quick, make a change to file Y and push it!". In Git, you could make a separate branch and put your change to file X there, and then change file Y on the master branch and push that. But you have an even simpler option. Instead, just make the required changes to file Y, stage file Y, commit only the changes to file Y and then push that to the central repo. No branching, no nothing, just simple flexibility. This is how easy it is:
$ git status # On branch master nothing to commit (working directory clean) $ ls bar.py foo.py $ vim foo.py # making changes to foo.py $ git status # see that there are unstaged changes in foo.py now # On branch master # Changed but not updated: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: foo.py # no changes added to commit (use "git add" and/or "git commit -a") $ vim bar.py # make quick changes to bar.py that need to be pushed $ git status # On branch master # Changed but not updated: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: bar.py # modified: foo.py # no changes added to commit (use "git add" and/or "git commit -a") $ git add bar.py # stage only changes to bar.py $ git commit -m "emergency change to bar.py" [master 92890b5] emergency change to bar.py 1 files changed, 1 insertions(+), 0 deletions(-) $ git push origin master Counting objects: 5, done. Delta compression using up to 4 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 324 bytes, done. Total 3 (delta 0), reused 0 (delta 0) Unpacking objects: 100% (3/3), done. To /home/steve/gittest/central b856f5b..92890b5 master -> master $ git status # see that indeed our changes to foo.py are still unstaged # On branch master # Changed but not updated: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: foo.py # no changes added to commit (use "git add" and/or "git commit -a")
Simplicity: For my workflow, Git is simpler to use than Mercurial. I developed this workflow very quickly when I started using Git. I've been using Mercurial where I work for several months now, and I haven't been able to find a workflow as simple and powerful. In the examples in this article, I've only used the following git commands:
$ git status # show the status of current branch $ git commit -m "comment" # commit currently staged changes $ git commit -am "comment" # commit, auto-staging changes to files under version control $ git branch # show local branches $ git branch -r # show remote branches $ git branch <newbranch> # create a new local branch $ git checkout <branchname> # switch to different local branch $ git push <remote> <branchname> # push a particular branch to a particular remote repo $ git add <file> # stage the changes to a particular file