For a while now I've been using subversion for my personal projects.
More and more I keep hearing great things about Git and Mercurial, and DVCS in general.
I'd like to give the whole DVCS thing a whirl, but I'm not too familiar with either option.
What are some of the differences between Mercurial and Git?
Note: I'm not trying to find out which one is "best" or even which one I should start with. I'm mainly looking for key areas where they are similar, and where they are different, because I am interested to know how they differ in terms of implementation and philosophy.
Disclaimer: I use Git, follow Git development on git mailing list, and even contribute a bit to Git (gitweb mainly). I know Mercurial from documentation and some from discussion on #revctrl IRC channel on FreeNode.
Thanks to all people on on #mercurial IRC channel who provided help about Mercurial for this writeup
Here it would be nice to have some syntax for table, something like in PHPMarkdown / MultiMarkdown / Maruku extension of Markdown
.hgtags
file with special rules for per-repository tags, and has also support for local tags in .hg/localtags
; in Git tags are refs residing in refs/tags/
namespace, and by default are autofollowed on fetching and require explicit pushing.There are a few things that differ Mercurial from Git, but there are other things that make them similar. Both projects borrow ideas from each other. For example hg bisect
command in Mercurial (formerly bisect extension) was inspired by git bisect
command in Git, while idea of git bundle
was inspired by hg bundle
.
In Git there are four types of objects in its object database: blob objects which contain contents of a file, hierarchical tree objects which store directory structure, including file names and relevant parts of file permissions (executable permission for files, being a symbolic link), commit object which contain authorship info, pointer to snapshot of state of repository at revision represented by a commit (via a tree object of top directory of project) and references to zero or more parent commits, and tag objects which reference other objects and can be signed using PGP / GPG.
Git uses two ways of storing objects: loose format, where each object is stored in a separate file (those files are written once, and never modified), and packed format where many objects are stored delta-compressed in a single file. Atomicity of operations is provided by the fact, that reference to a new object is written (atomically, using create + rename trick) after writing an object.
Git repositories require periodic maintenance using git gc
(to reduce disk space and improve performance), although nowadays Git does that automatically. (This method provides better compression of repositories.)
Mercurial (as far as I understand it) stores history of a file in a filelog (together, I think, with extra metadata like rename tracking, and some helper information); it uses flat structure called manifest to store directory structure, and structure called changelog which store information about changesets (revisions), including commit message and zero, one or two parents.
Mercurial uses transaction journal to provide atomicity of operations, and relies on truncating files to clean-up after failed or interrupted operation. Revlogs are append-only.
Looking at repository structure in Git versus in Mercurial, one can see that Git is more like object database (or a content-addressed filesystem), and Mercurial more like traditional fixed-field relational database.
Differences:
In Git the tree objects form a hierarchical structure; in Mercurial manifest file is flat structure. In Git blob object store one version of a contents of a file; in Mercurial filelog stores whole history of a single file (if we do not take into account here any complications with renames). This means that there are different areas of operations where Git would be faster than Mercurial, all other things considered equal (like merges, or showing history of a project), and areas where Mercurial would be faster than Git (like applying patches, or showing history of a single file). This issue might be not important for end user.
Because of the fixed-record structure of Mercurial's changelog structure, commits in Mercurial can have only up to two parents; commits in Git can have more than two parents (so called "octopus merge"). While you can (in theory) replace octopus merge by a series of two-parent merges, this might cause complications when converting between Mercurial and Git repositories.
As far as I know Mercurial doesn't have equivalent of annotated tags (tag objects) from Git. A special case of annotated tags are signed tags (with PGP / GPG signature); equivalent in Mercurial can be done using GpgExtension, which extension is being distributed along with Mercurial. You can't tag non-commit object in Mercurial like you can in Git, but that is not very important, I think (some git repositories use tagged blob to distribute public PGP key to use to verify signed tags).
In Git references (branches, remote-tracking branches and tags) reside outside DAG of commits (as they should). References in refs/heads/
namespace (local branches) point to commits, and are usually updated by "git commit"; they point to the tip (head) of branch, that's why such name. References in refs/remotes/<remotename>/
namespace (remote-tracking branches) point to commit, follow branches in remote repository <remotename>
, and are updated by "git fetch" or equivalent. References in refs/tags/
namespace (tags) point usually to commits (lightweight tags) or tag objects (annotated and signed tags), and are not meant to change.
In Mercurial you can give persistent name to revision using tag; tags are stored similarly to the ignore patterns. It means that globally visible tags are stored in revision-controlled .hgtags
file in your repository. That has two consequences: first, Mercurial has to use special rules for this file to get current list of all tags and to update such file (e.g. it reads the most recently committed revision of the file, not currently checked out version); second, you have to commit changes to this file to have new tag visible to other users / other repositories (as far as I understand it).
Mercurial also supports local tags, stored in hg/localtags
, which are not visible to others (and of course are not transferable)
In Git tags are fixed (constant) named references to other objects (usually tag objects, which in turn point to commits) stored in refs/tags/
namespace. By default when fetching or pushing a set of revision, git automatically fetches or pushes tags which point to revisions being fetched or pushed. Nevertheless you can control to some extent which tags are fetched or pushed.
Git treats lightweight tags (pointing directly to commits) and annotated tags (pointing to tag objects, which contain tag message which optionally includes PGP signature, which in turn point to commit) slightly differently, for example by default it considers only annotated tags when describing commits using "git describe".
Git doesn't have a strict equivalent of local tags in Mercurial. Nevertheless git best practices recommend to setup separate public bare repository, into which you push ready changes, and from which others clone and fetch. This means that tags (and branches) that you don't push, are private to your repository. On the other hand you can also use namespace other than heads
, remotes
or tags
, for example local-tags
for local tags.
Personal opinion: In my opinion tags should reside outside revision graph, as they are external to it (they are pointers into graph of revisions). Tags should be non-versioned, but transferable. Mercurial's choice of using a mechanism similar to the one for ignoring files, means that it either has to treat .hgtags
specially (file in-tree is transferable, but ordinary it is versioned), or have tags which are local only (.hg/localtags
is non-versioned, but untransferable).
In Git local branch (branch tip, or branch head) is a named reference to a commit, where one can grow new commits. Branch can also mean active line of development, i.e. all commits reachable from branch tip. Local branches reside in refs/heads/
namespace, so e.g. fully qualified name of 'master' branch is 'refs/heads/master'.
Current branch in Git (meaning checked out branch, and branch where new commit will go) is the branch which is referenced by the HEAD ref. One can have HEAD pointing directly to a commit, rather than being symbolic reference; this situation of being on an anonymous unnamed branch is called detached HEAD ("git branch" shows that you are on '(no branch)').
In Mercurial there are anonymous branches (branch heads), and one can use bookmarks (via bookmark extension). Such bookmark branches are purely local, and those names were (up to version 1.6) not transferable using Mercurial. You can use rsync or scp to copy the .hg/bookmarks
file to a remote repository. You can also use hg id -r <bookmark> <url>
to get the revision id of a current tip of a bookmark.
Since 1.6 bookmarks can be pushed/pulled. The BookmarksExtension page has a section on Working With Remote Repositories. There is a difference in that in Mercurial bookmark names are global, while definition of 'remote' in Git describes also mapping of branch names from the names in remote repository to the names of local remote-tracking branches; for example refs/heads/*:refs/remotes/origin/*
mapping means that one can find state of 'master' branch ('refs/heads/master') in the remote repository in the 'origin/master' remote-tracking branch ('refs/remotes/origin/master').
Mercurial has also so called named branches, where the branch name is embedded in a commit (in a changeset). Such name is global (transferred on fetch). Those branch names are permanently recorded as part of the changeset\u2019s metadata. With modern Mercurial you can close "named branch" and stop recording branch name. In this mechanism tips of branches are calculated on the fly.
Mercurial's "named branches" should in my opinion be called commit labels instead, because it is what they are. There are situations where "named branch" can have multiple tips (multiple childless commits), and can also consist of several disjoint parts of graph of revisions.
There is no equivalent of those Mercurial "embedded branches" in Git; moreover Git's philosophy is that while one can say that branch includes some commit, it doesn't mean that a commit belongs to some branch.
Note that Mercurial documentation still proposes to use separate clones (separate repositories) at least for long-lived branches (single branch per repository workflow), aka branching by cloning.
Mercurial by default pushes all heads. If you want to push a single branch (single head), you have to specify tip revision of the branch you want to push. You can specify branch tip by its revision number (local to repository), by revision identifier, by bookmark name (local to repository, doesn't get transferred), or by embedded branch name (named branch).
As far as I understand it, if you push a range of revisions that contain commits marked as being on some "named branch" in Mercurial parlance, you will have this "named branch" in the repository you push to. This means that names of such embedded branches ("named branches") are global (with respect to clones of given repository / project).
By default (subject to push.default
configuration variable) "git push" or "git push <remote>" Git would push matching branches, i.e. only those local branches that have their equivalent already present in remote repository you push into. You can use --all
option to git-push ("git push --all") to push all branches, you can use "git push <remote> <branch>" to push a given single branch, and you can use "git push <remote> HEAD" to push current branch.
All of the above assumes that Git isn't configured which branches to push via remote.<remotename>.push
configuration variables.
Note: here I use Git terminology where "fetch" means downloading changes from remote repository without integrating those changes with local work. This is what "git fetch
" and "hg pull
" does.
If I understand it correctly, by default Mercurial fetches all heads from remote repository, but you can specify branch to fetch via "hg pull --rev <rev> <url>
" or "hg pull <url>#<rev>
" to get single branch. You can specify <rev> using revision identifier, "named branch" name (branch embedded in changelog), or bookmark name. Bookmark name however (at least currently) doesn't get transferred. All "named branches" revisions you get belong to get transferred. "hg pull" stores tips of branches it fetched as anonymous, unnamed heads.
In Git by default (for 'origin' remote created by "git clone", and for remotes created using "git remote add") "git fetch
" (or "git fetch <remote>
") gets all branches from remote repository (from refs/heads/
namespace), and stores them in refs/remotes/
namespace. This means for example that branch named 'master' (full name: 'refs/heads/master') in remote 'origin' would get stored (saved) as 'origin/master' remote-tracking branch (full name: 'refs/remotes/origin/master').
You can fetch single branch in Git by using git fetch <remote> <branch>
- Git would store requested branch(es) in FETCH_HEAD, which is something similar to Mercurial unnamed heads.
Those are but examples of default cases of powerful refspec Git syntax: with refspecs you can specify and/or configure which branches one want to fetch, and where to store them. For example default "fetch all branches" case is represented by '+refs/heads/*:refs/remotes/origin/*' wildcard refspec, and "fetch single branch" is shorthand for 'refs/heads/<branch>:'. Refspecs are used to map names of branches (refs) in remote repository to local refs names. But you don't need to know (much) about refspecs to be able to work effectively with Git (thanks mainly to "git remote" command).
Personal opinion: I personally think that "named branches" (with branch names embedded in changeset metadata) in Mercurial are misguided design with its global namespace, especially for a distributed version control system. For example let's take case where both Alice and Bob have "named branch" named 'for-joe' in their repositories, branches which have nothing in common. In Joe's repository however those two branches would be mistreated as a single branch. So you have somehow come up with convention protecting against branch name clashes. This is not problem with Git, where in Joe's repository 'for-joe' branch from Alice would be 'alice/for-joe', and from Bob it would be 'bob/for-joe'. See also Separating branch name from branch identity issue raised on Mercurial wiki.
Mercurial's "bookmark branches" currently lack in-core distribution mechanism.