This is where I put my non-benchmark related notes about the VCSes I'm studying. This includes introductory notes on DVCS, summary of use cases I consider important, links to important pages, and so on.
Introduction to DVCS
Most software developers are familiar with Subversion to such an extent that their use of it is automatic. Distributed version control seems like a huge shift in mindset, and to some extent there are differences in the two technologies, but in actual fact getting up to speed with distributed version control is not difficult.
First: what does distributed mean?
Distributed means that instead of one central repository, like Subversion has, each developer has his own repository. Imagine that somehow you have your repository that you're working on, while I have my repository that I'm working on, and that revisions from the repositories will migrate back and forth without a problem. This is the essence of distributed version control, but in practice there are a few gotchas that you aren't used to from Subversion.
Why are there all these SHA1s floating all over the place?
To identify files and revisions unambiguously.
Consider Subversion. Specifically, consider its implementation. Imagine that you're tracking a repository with two files in it, files A and B.
Revision 1: A "a", B "b" Revision 2: A "aha", B "b" Revision 3: A "aaa", B "baa" Revision 4: A "aah", B "baa"
How does Subversion keep track of all this data? Well, one approach might be to store each revision as a separate directory, with each file as of this revision stored in the directory. So you might have:
- 1
- 1/A
- 1/B
- 1/meta: "initial import"
- 2
- 2/A
- 2/B
- 2/meta: "changed A"
This is a little wasteful, though, because 2/B and 1/B are the same file. So instead we might store all the versions of a file, and revisions 1 and 2 could just be files that store which versions of each file are used.
- A.allversions
- A:1
- A:2
- A:3
- A:4
- B.allversions
- B:1
- B:2
- 1: "A:1, B:1, logmessage:initial import"
- 2: "A:2, B:1, logmessage:changed A"
- 3: "A:3, B:2, logmessage:changed both"
This is all great, but it relies on a certain property of SVN that we can't always rely on: all versions come into one server and can therefore be ordered. In the world of distributed version control, this is no longer true. Perhaps you committed revision 2 in your repository long after I committed revision 3 in my repository. Somehow we have to communicate those revisions to one another, but your file A:2 is very different from my version A:2, and it's too late to splice it in. We'd somehow have to keep track of each repository's version of each file, and map from one to the other -- your A:2 might be my A:49, so your version "A:2, B:1, logmessage:changed A" would be my "A:49, B:1, logmessage:changed A". Similarly, while it would be your revision 2, it might be my revision 48. Communication about versions, files, and so on would get to be a nightmare fast.
To avoid this, some distributed version control systems record the contents of files using a system that uniquely identifies a file by its contents. Specifically, they compute the SHA1 hash of a file. Now your copy A:2 is instead A:3240884debaec25b7229bd9e9554dc7c08836b2f. In the same way, your revision 2 is a SHA1 computed from the file describing it. This way, the revisions can be discussed unambiguously among the various repositories. hg assigns revision numbers like Subversion's, but these are strictly repository-local.
For more information about how SHA1s come into play in Monotone, see the Monotone manual: Versions of Files. This discussion is also accurate for git and hg. The Monotone FAQ also has some discussion of this under "version numbers".
bzr actually does something different here: versions of files are identified with UUIDs (i.e. like SHA1, except pseudorandom), and revisions are identified with a naming scheme based on branches. (The children of revision 2 might be revision 3 and revision 2.1.) In practice it comes down to keeping track of which revision comes from which repository. I think darcs works this way too, except that it doesn't care about which repository anything comes from. [XXX: FIXME.]
Where are my repository and working copy? What's all this nonsense about cloning and branching?
Many distributed VCSes combine the concepts of "repository" and "working copy" into one thing, called a "branch" or "clone".
In Subversion, to get any work done, you create a working copy that is "bound" to the repository. You change things in the working copy (over which you have absolute control), and then try to "commit" these changes to the repository (over which you may not have any control). Above, we define distributed as meaning "each developer has his own repository", but many systems (git, darcs, bzr, hg) implement this as "each working copy has its own repository". This has the advantage that you can branch off easily by simply creating a new working copy, and start working right away. However, in these systems, the concepts of working copy and repository get kind of melded together, and the command to create a new one then becomes something like "branch" or "clone".
Note that Monotone and SVK maintain the separation between repository and working copy. In order to use these systems, you generally "sync" your repository with someone else's, and then "checkout" a working copy from your repository. (In Monotone, a "clone" is more like a Subversion working copy; new revisions are applied to a remote repository.)
What's with all these new commands like push, pull, send, apply, sync, etc.?
These are commands to control the flow of revisions between repositories.
In Subversion, there's obviously no need to worry about what happens to a revision, once committed. It's in the repository now. But in a distributed system, the flow of revisions between repositories is a big deal. Each system has its own set of commands for doing this.
SVK has a particularly knotty set of commands: http://svk.bestpractical.com/view/SVKQuestions (search for "Clarification"), but to some extent all systems suffer from this to some extent. Generally, pull means to bring revisions over from a repository to yours, where push is the opposite. After the revisions are brought over, some systems, like darcs, try to bring your working copy up-to-date immediately, whereas others (I think bzr, hg) wait for an "update" command.
"darcs send" is a feature darcs has which sends an emailed patch bundle by email in the event that you don't have permissions on the remote repository. "darcs apply" is how you bring patches in from such an email.
"sync" is used in Monotone to mean two-way flow of revisions, but in SVK it has another meaning, beyond the scope of this document.
The flow of revisions between distributed systems is the flip side of the coin where each developer has a repository. Some of the use cases (below) focus solely on the flow of revisions.
Use Cases
This section is meant as a simple phrasebook, exploring the common tasks performed with distributed version control and how each system gets the job done.
"Oops": Rollbacks
I think darcs was the first open source DVCS to introduce this feature. As described in this page on sourcefrog.net, sometimes you commit something you didn't want; the log message might be wrong, or the commit included the wrong files. Many distributed version control systems allow you to "uncommit" or roll back the most recent commits and "do them over". Subversion doesn't allow you to do this, but there's no technical reason -- merely the sociological reason that once you commit a revision, it stays forever. With distributed version control, any repository can be "temporary"; until and unless you push a change, it hasn't gone anywhere.
So imagine this scenario: you have a project with two files. You want to commit two changes -- one to each of the files. You change both and commit by mistake. You'd like to undo that commit and commit each file separately.
darcs
First, we create the repository:
ethan@sundance:~/tests/vcsplay$ cd darcs-unrecord/ ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs init ethan@sundance:~/tests/vcsplay/darcs-unrecord$ echo "initial 1" > file1 ethan@sundance:~/tests/vcsplay/darcs-unrecord$ echo "initial 2" > file2 ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs add file1 file2 ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs record -m "initial import" Darcs needs to know what name (conventionally an email address) to use as the patch author, e.g. 'Fred Bloggs <fred@bloggs.invalid>'. If you provide one now it will be stored in the file '_darcs/prefs/author' and used as a default in the future. To change your preferred author address, simply delete or edit this file. What is your email address? ethan@localhost addfile ./file1 Shall I record this change? (1/?) [ynWsfqadjkc], or ? for help: y hunk ./file1 1 +initial 1 Shall I record this change? (2/?) [ynWsfqadjkc], or ? for help: Invalid response, try again! Shall I record this change? (2/?) [ynWsfqadjkc], or ? for help: y addfile ./file2 Shall I record this change? (3/?) [ynWsfqadjkc], or ? for help: y hunk ./file2 1 +initial 2 Shall I record this change? (4/?) [ynWsfqadjkc], or ? for help: y Finished recording patch 'initial import'
Jeez, that interactive recording is a real pain! Next time I'll just use the -a option and record everything.
ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs changes Thu Jan 3 16:00:02 EST 2008 ethan@localhost * initial import ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs changes -v Thu Jan 3 16:00:02 EST 2008 ethan@localhost * initial import addfile ./file1 hunk ./file1 1 +initial 1 addfile ./file2 hunk ./file2 1 +initial 2
However, you can see that the files were imported successfully.
ethan@sundance:~/tests/vcsplay/darcs-unrecord$ echo "changed 1" > file1 ethan@sundance:~/tests/vcsplay/darcs-unrecord$ echo "changed 2" > file2 ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs record -a -m "Changed file" Finished recording patch 'Changed file' ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs changes -v Thu Jan 3 16:00:56 EST 2008 ethan@localhost * Changed file hunk ./file1 1 -initial 1 +changed 1 hunk ./file2 1 -initial 2 +changed 2 Thu Jan 3 16:00:02 EST 2008 ethan@localhost * initial import addfile ./file1 hunk ./file1 1 +initial 1 addfile ./file2 hunk ./file2 1 +initial 2
Oh no! I shouldn't have used -a. You can see that the "Changed file" patch has two changes now -- one in each file. So we unrecord:
ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs unrecord Thu Jan 3 16:00:56 EST 2008 ethan@localhost * Changed file Shall I unrecord this patch? (1/2) [ynWvpxqadjk], or ? for help: y Thu Jan 3 16:00:02 EST 2008 ethan@localhost * initial import Shall I unrecord this patch? (2/2) [ynWvpxqadjk], or ? for help: n Finished unrecording. ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs record -a file1 -m "Changed file1" Recording changes in "file1": Finished recording patch 'Changed file1' ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs record -a file2 -m "Changed file2" Recording changes in "file2": Finished recording patch 'Changed file2' ethan@sundance:~/tests/vcsplay/darcs-unrecord$ darcs changes -v Thu Jan 3 16:01:48 EST 2008 ethan@localhost * Changed file2 hunk ./file2 1 -initial 2 +changed 2 Thu Jan 3 16:01:42 EST 2008 ethan@localhost * Changed file1 hunk ./file1 1 -initial 1 +changed 1 Thu Jan 3 16:00:02 EST 2008 ethan@localhost * initial import addfile ./file1 hunk ./file1 1 +initial 1 addfile ./file2 hunk ./file2 1 +initial 2 ethan@sundance:~/tests/vcsplay/darcs-unrecord$
darcs unrecord leaves the working copy alone, so it's a simple matter to re-commit the files correctly. (But using -a this time.)
bzr
ethan@sundance:~/tests/vcsplay$ mkdir bzr-unrecord ethan@sundance:~/tests/vcsplay$ cd bzr-unrecord/ ethan@sundance:~/tests/vcsplay/bzr-unrecord$ mkd ethan@sundance:~/tests/vcsplay/bzr-unrecord$ bzr init ethan@sundance:~/tests/vcsplay/bzr-unrecord$ echo "initial 1" > file1 ethan@sundance:~/tests/vcsplay/bzr-unrecord$ echo "initial 2" > file2 ethan@sundance:~/tests/vcsplay/bzr-unrecord$ bzr add file1 file2 added file1 added file2 ethan@sundance:~/tests/vcsplay/bzr-unrecord$ bzr commit -m "Initial import." Committing to: /home/ethan/tests/vcsplay/bzr-unrecord/ added file1 added file2 Committed revision 1. ethan@sundance:~/tests/vcsplay/bzr-unrecord$ bzr log -v ------------------------------------------------------------ revno: 1 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-unrecord timestamp: Thu 2008-01-03 17:23:54 -0500 message: Initial import. added: file1 file2 ethan@sundance:~/tests/vcsplay/bzr-unrecord$ echo "changed 1" > file1 ethan@sundance:~/tests/vcsplay/bzr-unrecord$ echo "changed 2" > file2 ethan@sundance:~/tests/vcsplay/bzr-unrecord$ bzr commit -m "Changed file." Committing to: /home/ethan/tests/vcsplay/bzr-unrecord/ modified file1 modified file2 Committed revision 2. ethan@sundance:~/tests/vcsplay/bzr-unrecord$ bzr log -v ------------------------------------------------------------ revno: 2 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-unrecord timestamp: Thu 2008-01-03 17:24:13 -0500 message: Changed file. modified: file1 file2 ------------------------------------------------------------ revno: 1 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-unrecord timestamp: Thu 2008-01-03 17:23:54 -0500 message: Initial import. added: file1 file2 ethan@sundance:~/tests/vcsplay/bzr-unrecord$ bzr uncommit 2 Ethan Glasser-Camp 2008-01-03 Changed file. The above revision(s) will be removed. Are you sure [y/N]? y ethan@sundance:~/tests/vcsplay/bzr-unrecord$ bzr commit file1 -m "Changed file1." Committing to: /home/ethan/tests/vcsplay/bzr-unrecord/ modified file1 Committed revision 2. ethan@sundance:~/tests/vcsplay/bzr-unrecord$ bzr commit file2 -m "Changed file2." Committing to: /home/ethan/tests/vcsplay/bzr-unrecord/ modified file2 Committed revision 3. ethan@sundance:~/tests/vcsplay/bzr-unrecord$ bzr log -v ------------------------------------------------------------ revno: 3 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-unrecord timestamp: Thu 2008-01-03 17:26:11 -0500 message: Changed file2. modified: file2 ------------------------------------------------------------ revno: 2 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-unrecord timestamp: Thu 2008-01-03 17:26:05 -0500 message: Changed file1. modified: file1 ------------------------------------------------------------ revno: 1 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-unrecord timestamp: Thu 2008-01-03 17:23:54 -0500 message: Initial import. added: file1 file2
This is very much like the darcs case.
hg
Mercurial has two options for uncommitting: hg rollback, and hg backout. hg rollback corresponds roughly to bzr uncommit. The Mercurial and bzr developers seem to have a disagreement about this feature (see http://bazaar-vcs.org/BzrVsHg and the response at http://www.selenic.com/mercurial/wiki/index.cgi/BzrVsHg). The Mercurial people assert that the bzr command "changes history", although as far as I can tell the Mercurial command does the same; the bzr people assert that "Even if you uncommit a revision from your branch, it is still present in your repository, and you can use it later if you like", although I cannot figure out a way to do this.
Mercurial also provides hg backout, which creates a patch which "undoes" revisions and commits it immediately. Since the point of uncommit is to "rewrite history", backing out isn't what we want.
You can only hg rollback one revision, but bzr uncommit as many as you like. There is also a warning that Mercurial will "restore the dirstate at the time of the last transaction", so you may need to re-add or move files.
ethan@sundance:~/tests/vcsplay$ mkdir hg-unrecord ethan@sundance:~/tests/vcsplay$ cd hg-unrecord/ ethan@sundance:~/tests/vcsplay/hg-unrecord$ hg init ethan@sundance:~/tests/vcsplay/hg-unrecord$ echo "initial 1" > file1 ethan@sundance:~/tests/vcsplay/hg-unrecord$ echo "initial 2" > file2 ethan@sundance:~/tests/vcsplay/hg-unrecord$ hg add file1 file2 ethan@sundance:~/tests/vcsplay/hg-unrecord$ hg commit -m "Initial import." No username found, using 'ethan@sundance' instead ethan@sundance:~/tests/vcsplay/hg-unrecord$ echo "changed 1" > file1 ethan@sundance:~/tests/vcsplay/hg-unrecord$ echo "changed 2" > file2 ethan@sundance:~/tests/vcsplay/hg-unrecord$ hg commit -m "Change file." No username found, using 'ethan@sundance' instead ethan@sundance:~/tests/vcsplay/hg-unrecord$ hg log -v changeset: 1:1b3f51fbe15c tag: tip user: ethan@sundance date: Thu Jan 03 18:39:44 2008 -0500 files: file1 file2 description: Change file. changeset: 0:f492b9d31f8c user: ethan@sundance date: Thu Jan 03 18:39:29 2008 -0500 files: file1 file2 description: Initial import. ethan@sundance:~/tests/vcsplay/hg-unrecord$ hg rollback rolling back last transaction ethan@sundance:~/tests/vcsplay/hg-unrecord$ hg commit file1 -m "Change file1." -m: No such file or directory Change file1.: No such file or directory abort: file /home/ethan/tests/vcsplay/hg-unrecord/-m not found! ethan@sundance:~/tests/vcsplay/hg-unrecord$ hg commit -m "Change file1." file1 No username found, using 'ethan@sundance' instead ethan@sundance:~/tests/vcsplay/hg-unrecord$ hg commit -m "Change file2." file2 No username found, using 'ethan@sundance' instead
Note that options to the commit (-m) must come before the files to be committed.
ethan@sundance:~/tests/vcsplay/hg-unrecord$ hg log -v changeset: 2:902c83785ddf tag: tip user: ethan@sundance date: Thu Jan 03 18:40:14 2008 -0500 files: file2 description: Change file2. changeset: 1:fcb89316912e user: ethan@sundance date: Thu Jan 03 18:40:10 2008 -0500 files: file1 description: Change file1. changeset: 0:f492b9d31f8c user: ethan@sundance date: Thu Jan 03 18:39:29 2008 -0500 files: file1 file2 description: Initial import.
git
In contrast to the Mercurial approach of refusing to change history, the git people change history all the time, and love it. Your basic options are git reset, to set HEAD to a new revision, or git commit --amend, to replace the current revision. There is also a git-revert, much like hg backout.
I couldn't figure out how to use commit --amend to remove a file from the commit that had already happened, but here's how to do it using git reset.
ethan@sundance:~/tests/vcsplay$ mkdir git-unrecord ethan@sundance:~/tests/vcsplay$ cd git-unrecord/ ethan@sundance:~/tests/vcsplay/git-unrecord$ git init Initialized empty Git repository in .git/ ethan@sundance:~/tests/vcsplay/git-unrecord$ echo "initial 1" > file1 ethan@sundance:~/tests/vcsplay/git-unrecord$ echo "initial 2" > file2 ethan@sundance:~/tests/vcsplay/git-unrecord$ git add file1 file2 ethan@sundance:~/tests/vcsplay/git-unrecord$ git commit -m "Initial import." Created initial commit 9b345dc: Initial import. 2 files changed, 2 insertions(+), 0 deletions(-) create mode 100644 file1 create mode 100644 file2 ethan@sundance:~/tests/vcsplay/git-unrecord$ echo "changed 1" > file1 ethan@sundance:~/tests/vcsplay/git-unrecord$ echo "changed 2" > file2 ethan@sundance:~/tests/vcsplay/git-unrecord$ git add file1 file2 ethan@sundance:~/tests/vcsplay/git-unrecord$ git commit -m "Changed files." Created commit 670a6e8: Changed files. 2 files changed, 2 insertions(+), 2 deletions(-)
Remember that you have to re-add files to tell git that they go into the next commit. I could have also done a git commit -a, but I think of this as more git-y. Now, to reset:
ethan@sundance:~/tests/vcsplay/git-unrecord$ git reset HEAD^ file1: needs update file2: needs update ethan@sundance:~/tests/vcsplay/git-unrecord$ git diff diff --git a/file1 b/file1 index 483e423..ff6c1c4 100644 --- a/file1 +++ b/file1 @@ -1 +1 @@ -initial 1 +changed 1 diff --git a/file2 b/file2 index 4440c4d..8687e5c 100644 --- a/file2 +++ b/file2 @@ -1 +1 @@ -initial 2 +changed 2 ethan@sundance:~/tests/vcsplay/git-unrecord$ git diff --cached ethan@sundance:~/tests/vcsplay/git-unrecord$ git add file1 ethan@sundance:~/tests/vcsplay/git-unrecord$ git commit -m "Changed file1." Created commit f5c1e23: Changed file1. 1 files changed, 1 insertions(+), 1 deletions(-) ethan@sundance:~/tests/vcsplay/git-unrecord$ git add file2 ethan@sundance:~/tests/vcsplay/git-unrecord$ git commit -m "Changed file2." Created commit 71d599f: Changed file2. 1 files changed, 1 insertions(+), 1 deletions(-)
mtn
mtn has a mtn db kill_rev_locally command which you can use to accomplish the same goal. It's not as convenient as the other commands, but it's serviceable.
First, create a key for mtn, and add it to ssh-agent using ssh-add. This isn't strictly necessary, but it's good practice.
ethan@sundance:~/tests/vcsplay$ mtn genkey ethan@sundance mtn: generating key-pair 'ethan@sundance' enter passphrase for key ID [ethan@sundance]: confirm passphrase for key ID [ethan@sundance]: mtn: passphrases do not match, try again enter passphrase for key ID [ethan@sundance]: confirm passphrase for key ID [ethan@sundance]: mtn: storing key-pair 'ethan@sundance' in /home/ethan/.monotone/keys/ ethan@sundance:~/tests/vcsplay$ mtn ssh_agent_export monotone.ssh enter passphrase for key ID [ethan@sundance]: enter new passphrase for key ID [ethan@sundance]: confirm passphrase for key ID [ethan@sundance]: ethan@sundance:~/tests/vcsplay$ ssh-add monotone.ssh @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: UNPROTECTED PRIVATE KEY FILE! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Permissions 0644 for 'monotone.ssh' are too open. It is recommended that your private key files are NOT accessible by others. This private key will be ignored. ethan@sundance:~/tests/vcsplay$ chmod 0600 monotone.ssh ethan@sundance:~/tests/vcsplay$ ssh-add monotone.ssh Enter passphrase for monotone.ssh: Identity added: monotone.ssh (monotone.ssh)
Now, create a db, and from that db, create a workspace:
ethan@sundance:~/tests/vcsplay$ mtn db init --db mtn-unrecord.db ethan@sundance:~/tests/vcsplay$ mtn --db mtn-unrecord.db --branch="com.example.unrecord" setup mtn-unrecord ethan@sundance:~/tests/vcsplay$ cd mtn-unrecord/ ethan@sundance:~/tests/vcsplay/mtn-unrecord$ echo "initial 1" > file1 ethan@sundance:~/tests/vcsplay/mtn-unrecord$ echo "initial 2" > file2 ethan@sundance:~/tests/vcsplay/mtn-unrecord$ mtn add file1 file2 mtn: adding file1 to workspace manifest mtn: adding file2 to workspace manifest ethan@sundance:~/tests/vcsplay/mtn-unrecord$ mtn commit -m "Initial import." mtn: beginning commit on branch 'com.example.unrecord' mtn: committed revision 3b74d77ee3f80b527b3126907656503a0b2e7a3e ethan@sundance:~/tests/vcsplay/mtn-unrecord$ echo "changed 1" > file1 ethan@sundance:~/tests/vcsplay/mtn-unrecord$ echo "changed 2" > file2 ethan@sundance:~/tests/vcsplay/mtn-unrecord$ mtn commit -m "Changed file." mtn: beginning commit on branch 'com.example.unrecord' mtn: committed revision 6e4dbad188d1fe62bb5d136be80cfe3bc58264af ethan@sundance:~/tests/vcsplay/mtn-unrecord$ mtn log --diffs o ----------------------------------------------------------------- | Revision: 6e4dbad188d1fe62bb5d136be80cfe3bc58264af | Ancestor: 3b74d77ee3f80b527b3126907656503a0b2e7a3e | Author: ethan@sundance | Date: 2008-01-04T01:08:49 | Branch: com.example.unrecord | | Modified files: | file1 file2 | | ChangeLog: | | Changed file. | | ============================================================ | --- file1 29155935bce6147b2e7d79de3ade493a98bc173b | +++ file1 826cf4a8818dd4e98c9426416abb052d23b74394 | @@ -1 +1 @@ | -initial 1 | +changed 1 | ============================================================ | --- file2 e635921a56b7132d22dea54b2bfeaeb08417bf92 | +++ file2 0db4598beb9e60cc6a44ef6f20e86f5b756953bc | @@ -1 +1 @@ | -initial 2 | +changed 2 o ----------------------------------------------------------------- Revision: 3b74d77ee3f80b527b3126907656503a0b2e7a3e Ancestor: Author: ethan@sundance Date: 2008-01-04T01:02:37 Branch: com.example.unrecord Added files: file1 file2 Added directories: ChangeLog: Initial import. ============================================================ --- file1 29155935bce6147b2e7d79de3ade493a98bc173b +++ file1 29155935bce6147b2e7d79de3ade493a98bc173b @@ -0,0 +1 @@ +initial 1 ============================================================ --- file2 e635921a56b7132d22dea54b2bfeaeb08417bf92 +++ file2 e635921a56b7132d22dea54b2bfeaeb08417bf92 @@ -0,0 +1 @@ +initial 2
That's a lot of diff, but it confirms that it committed both files.
ethan@sundance:~/tests/vcsplay/mtn-unrecord$ mtn db kill_rev_locally 6e4dbad188d1fe62bb5d136be80cfe3bc58264af mtn: applying changes from 6e4dbad188d1fe62bb5d136be80cfe3bc58264af on the current workspace
Note that I copied the whole 40-character SHA1, but I could have just said mtn db kill_rev_locally h:.
ethan@sundance:~/tests/vcsplay/mtn-unrecord$ mtn diff # # old_revision [3b74d77ee3f80b527b3126907656503a0b2e7a3e] # # patch "file1" # from [29155935bce6147b2e7d79de3ade493a98bc173b] # to [826cf4a8818dd4e98c9426416abb052d23b74394] # # patch "file2" # from [e635921a56b7132d22dea54b2bfeaeb08417bf92] # to [0db4598beb9e60cc6a44ef6f20e86f5b756953bc] # ============================================================ --- file1 29155935bce6147b2e7d79de3ade493a98bc173b +++ file1 826cf4a8818dd4e98c9426416abb052d23b74394 @@ -1 +1 @@ -initial 1 +changed 1 ============================================================ --- file2 e635921a56b7132d22dea54b2bfeaeb08417bf92 +++ file2 0db4598beb9e60cc6a44ef6f20e86f5b756953bc @@ -1 +1 @@ -initial 2 +changed 2 ethan@sundance:~/tests/vcsplay/mtn-unrecord$ mtn commit file1 -m "Changed file1." mtn: beginning commit on branch 'com.example.unrecord' mtn: committed revision 7083ae8f41739a7f846ec6c18c6d13dd8ddc5fb5 ethan@sundance:~/tests/vcsplay/mtn-unrecord$ mtn commit file2 -m "Changed file2." mtn: beginning commit on branch 'com.example.unrecord' mtn: committed revision fe714c150f8cb3007c76f5eaf93fe06a20582c0a ethan@sundance:~/tests/vcsplay/mtn-unrecord$ mtn log --diffs o ----------------------------------------------------------------- | Revision: fe714c150f8cb3007c76f5eaf93fe06a20582c0a | Ancestor: 7083ae8f41739a7f846ec6c18c6d13dd8ddc5fb5 | Author: ethan@sundance | Date: 2008-01-04T01:10:12 | Branch: com.example.unrecord | | Modified files: | file2 | | ChangeLog: | | Changed file2. | | ============================================================ | --- file2 e635921a56b7132d22dea54b2bfeaeb08417bf92 | +++ file2 0db4598beb9e60cc6a44ef6f20e86f5b756953bc | @@ -1 +1 @@ | -initial 2 | +changed 2 o ----------------------------------------------------------------- | Revision: 7083ae8f41739a7f846ec6c18c6d13dd8ddc5fb5 | Ancestor: 3b74d77ee3f80b527b3126907656503a0b2e7a3e | Author: ethan@sundance | Date: 2008-01-04T01:10:07 | Branch: com.example.unrecord | | Modified files: | file1 | | ChangeLog: | | Changed file1. | | ============================================================ | --- file1 29155935bce6147b2e7d79de3ade493a98bc173b | +++ file1 826cf4a8818dd4e98c9426416abb052d23b74394 | @@ -1 +1 @@ | -initial 1 | +changed 1 o ----------------------------------------------------------------- Revision: 3b74d77ee3f80b527b3126907656503a0b2e7a3e Ancestor: Author: ethan@sundance Date: 2008-01-04T01:02:37 Branch: com.example.unrecord Added files: file1 file2 Added directories: ChangeLog: Initial import. ============================================================ --- file1 29155935bce6147b2e7d79de3ade493a98bc173b +++ file1 29155935bce6147b2e7d79de3ade493a98bc173b @@ -0,0 +1 @@ +initial 1 ============================================================ --- file2 e635921a56b7132d22dea54b2bfeaeb08417bf92 +++ file2 e635921a56b7132d22dea54b2bfeaeb08417bf92 @@ -0,0 +1 @@ +initial 2
Maintenance Releases and Backporting Fixes
One common use-case for version control is to keep track of more than one branch of a system. Distributed version control generally addresses this by supporting branch-driven development. We now turn to long-lived branches. Let's say you have a project with two branches, a stable branch and a development branch. The stable branch has a bug, and the development branch has other changes. You want both branches to share the fix, but the stable branch must not get any changes from the development branch.
To do this right needs knowledge of each DVCS's preferred branching style as well as what options exist for transferring revisions and fixes between them. In a pinch, one can always do a diff, translate this into a patch, and apply the patch, but generally a DVCS has support for tracking the history of a patch, merging it with other revisions, etc.
darcs makes this very easy by defining a repository as "a set of patches". If you commit the fix to the unstable branch, you can just push one patch back to the stable branch, and it will be applied automatically. Other DVCSes which are snapshot-based may have more trouble here -- generally, you can't just ask to merge a single revision. Instead, most of the time, you'll commit the fix directly to the stable branch, and pull it to the unstable one. This pattern is described in the Monotone wiki as daggy fixes; it amounts to forward-porting the fix rather than backporting it.
To simplify this example, I created some simple files, representing the state of the two files through various revisions.
ethan@sundance:~/tests/vcsplay$ cat file1-initial initial 1 bug 1 more text ethan@sundance:~/tests/vcsplay$ cat file2-initial initial 2
The initial states of the two files. You can see the bug in file1, plain as day. To fix it requires only to change the line "bug 1" to "no bug 1", but at the same time, development continues. Viewed linearly, the development line might look like this:
ethan@sundance:~/tests/vcsplay$ cat file1-add-bug initial 1 bug 1 more text addition
We make an addition to file1, but the bug is still there.
ethan@sundance:~/tests/vcsplay$ cat file2-add1 initial 2 appending
We append something to file2.
ethan@sundance:~/tests/vcsplay$ cat file2-add2 initial 2 depending
We make a cosmetic change to file2.
If, however, we fix the bug in the stable verion, file1 will look like this:
ethan@sundance:~/tests/vcsplay$ cat file1-noadd-nobug initial 1 no bug 1 more text
And finally, the development branch should get a file1 that looks like:
ethan@sundance:~/tests/vcsplay$ cat file1-add-nobug initial 1 no bug 1 more text addition
Pulling the patch from the middle of a revision history without pulling anything else through is called "cherry-picking", and darcs is very good at it, but doing things the "daggy" way demonstrates a bit of how merging and branching work in a given distributed version control system, so let's do it that way first.
bzr
bzr uses the simplest branching model: each repository/working copy is its own branch. To create a new branch from an existing branch, you use darcs branch.
First, create a stable branch with the initial state:
ethan@sundance:~/tests/vcsplay$ mkdir bzr-stable ethan@sundance:~/tests/vcsplay$ cd bzr-stable/ ethan@sundance:~/tests/vcsplay/bzr-stable$ bzr init ethan@sundance:~/tests/vcsplay/bzr-stable$ cp ../file1-initial file1 ethan@sundance:~/tests/vcsplay/bzr-stable$ cp ../file2-initial file2 ethan@sundance:~/tests/vcsplay/bzr-stable$ bzr add file1 file2 added file1 added file2 ethan@sundance:~/tests/vcsplay/bzr-stable$ bzr commit -m "Initial import." Committing to: /home/ethan/tests/vcsplay/bzr-stable/ added file1 added file2 Committed revision 1.
Next, branch the stable version into an unstable version, and resume development:
ethan@sundance:~/tests/vcsplay/bzr-stable$ cd .. ethan@sundance:~/tests/vcsplay$ bzr branch bzr-stable bzr-unstable Branched 1 revision(s). ethan@sundance:~/tests/vcsplay$ cd bzr-unstable ethan@sundance:~/tests/vcsplay/bzr-unstable$ cp ../file1-add-bug file1 ethan@sundance:~/tests/vcsplay/bzr-unstable$ bzr commit -m "Additional file1 information." Committing to: /home/ethan/tests/vcsplay/bzr-unstable/ modified file1 Committed revision 2. ethan@sundance:~/tests/vcsplay/bzr-unstable$ cp ../file2-add1 file2 ethan@sundance:~/tests/vcsplay/bzr-unstable$ bzr commit -m "More file2 stuff." Committing to: /home/ethan/tests/vcsplay/bzr-unstable/ modified file2 Committed revision 3. ethan@sundance:~/tests/vcsplay/bzr-unstable$ cp ../file2-add2 file2 ethan@sundance:~/tests/vcsplay/bzr-unstable$ bzr commit -m "Change in file2." Committing to: /home/ethan/tests/vcsplay/bzr-unstable/ modified file2 Committed revision 4.
Next, put the bug fix in file1:
ethan@sundance:~/tests/vcsplay/bzr-unstable$ cd .. ethan@sundance:~/tests/vcsplay$ cd bzr-stable ethan@sundance:~/tests/vcsplay/bzr-stable$ cp ../file1-noadd-nobug file1 ethan@sundance:~/tests/vcsplay/bzr-stable$ bzr diff === modified file 'file1' --- file1 2008-01-05 04:24:05 +0000 +++ file1 2008-01-05 04:28:16 +0000 @@ -1,3 +1,3 @@ initial 1 -bug 1 +no bug 1 more text ethan@sundance:~/tests/vcsplay/bzr-stable$ bzr commit -m "Bug fix in file1." Committing to: /home/ethan/tests/vcsplay/bzr-stable/ modified file1 Committed revision 2.
Then, pull this change into the unstable version:
ethan@sundance:~/tests/vcsplay/bzr-stable$ cd ../bzr-unstable/ ethan@sundance:~/tests/vcsplay/bzr-unstable$ bzr pull Using saved location: /home/ethan/tests/vcsplay/bzr-stable/ bzr: ERROR: These branches have diverged. Use the merge command to reconcile them. ethan@sundance:~/tests/vcsplay/bzr-unstable$ bzr merge Merging from remembered location /home/ethan/tests/vcsplay/bzr-stable/ M file1 All changes applied successfully. ethan@sundance:~/tests/vcsplay/bzr-unstable$ bzr diff === modified file 'file1' --- file1 2008-01-05 04:24:58 +0000 +++ file1 2008-01-05 04:28:37 +0000 @@ -1,4 +1,4 @@ initial 1 -bug 1 +no bug 1 more text addition ethan@sundance:~/tests/vcsplay/bzr-unstable$ bzr commit -m "Pulled bug fix from stable." Committing to: /home/ethan/tests/vcsplay/bzr-unstable/ modified file1 Committed revision 5. ethan@sundance:~/tests/vcsplay/bzr-unstable$ bzr log ------------------------------------------------------------ revno: 5 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-unstable timestamp: Fri 2008-01-04 23:29:13 -0500 message: Pulled bug fix from stable. ------------------------------------------------------------ revno: 1.1.1 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-stable timestamp: Fri 2008-01-04 23:28:24 -0500 message: Bug fix in file1. ------------------------------------------------------------ revno: 4 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-unstable timestamp: Fri 2008-01-04 23:25:38 -0500 message: Change in file2. ------------------------------------------------------------ revno: 3 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-unstable timestamp: Fri 2008-01-04 23:25:14 -0500 message: More file2 stuff. ------------------------------------------------------------ revno: 2 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-unstable timestamp: Fri 2008-01-04 23:24:58 -0500 message: Additional file1 information. ------------------------------------------------------------ revno: 1 committer: Ethan Glasser-Camp <ethan@sundance> branch nick: bzr-stable timestamp: Fri 2008-01-04 23:24:05 -0500 message: Initial import. ethan@sundance:~/tests/vcsplay/bzr-unstable$
hg
The pattern is very similar in hg:
ethan@sundance:~/tests/vcsplay$ mkdir hg-stable ethan@sundance:~/tests/vcsplay$ cd hg-stable/ ethan@sundance:~/tests/vcsplay/hg-stable$ hg init ethan@sundance:~/tests/vcsplay/hg-stable$ cp ../file1-initial file1 ethan@sundance:~/tests/vcsplay/hg-stable$ cp ../file2-initial file2 ethan@sundance:~/tests/vcsplay/hg-stable$ hg add file1 file2 ethan@sundance:~/tests/vcsplay/hg-stable$ hg commit -m "Initial import." No username found, using 'ethan@sundance' instead
I'm just going to delete these "no username" messages, but there's one after every commit.
ethan@sundance:~/tests/vcsplay/hg-stable$ cd .. ethan@sundance:~/tests/vcsplay$ hg clone hg-stable hg-unstable 2 files updated, 0 files merged, 0 files removed, 0 files unresolved ethan@sundance:~/tests/vcsplay$ cd hg-unstable ethan@sundance:~/tests/vcsplay/hg-unstable$ cp ../file1-add-bug file1 ethan@sundance:~/tests/vcsplay/hg-unstable$ hg commit -m "Additional file1 information." ethan@sundance:~/tests/vcsplay/hg-unstable$ cp ../file2-add1 file2 ethan@sundance:~/tests/vcsplay/hg-unstable$ hg commit -m "More file2 stuff." ethan@sundance:~/tests/vcsplay/hg-unstable$ cp ../file2-add2 file2 ethan@sundance:~/tests/vcsplay/hg-unstable$ hg commit -m "Change in file2."
Commit the fix to the stable branch:
ethan@sundance:~/tests/vcsplay/hg-unstable$ cd ../hg-stable ethan@sundance:~/tests/vcsplay/hg-stable$ cp ../file1-noadd-nobug file1 ethan@sundance:~/tests/vcsplay/hg-stable$ hg diff diff -r d68883cfd972 file1 --- a/file1 Mon Jan 07 22:25:43 2008 -0500 +++ b/file1 Mon Jan 07 22:36:02 2008 -0500 @@ -1,3 +1,3 @@ initial 1 initial 1 -bug 1 +no bug 1 more text ethan@sundance:~/tests/vcsplay/hg-stable$ hg commit -m "Bug fix in file1."
And now pull the change to the unstable branch:
ethan@sundance:~/tests/vcsplay/hg-stable$ cd ../hg-unstable/ ethan@sundance:~/tests/vcsplay/hg-unstable$ hg pull pulling from /home/ethan/tests/vcsplay/hg-stable searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) ethan@sundance:~/tests/vcsplay/hg-unstable$ hg heads changeset: 4:e35ab24f4e8d tag: tip parent: 0:d68883cfd972 user: ethan@sundance date: Mon Jan 07 22:36:11 2008 -0500 summary: Bug fix in file1. changeset: 3:4919ea49b330 user: ethan@sundance date: Mon Jan 07 22:35:45 2008 -0500 summary: Change in file2. ethan@sundance:~/tests/vcsplay/hg-unstable$ hg merge merging file1 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) ethan@sundance:~/tests/vcsplay/hg-unstable$ cat file1 initial 1 no bug 1 more text addition ethan@sundance:~/tests/vcsplay/hg-unstable$ hg commit -m "Merge." ethan@sundance:~/tests/vcsplay/hg-unstable$ hg view ethan@sundance:~/tests/vcsplay/hg-unstable$ hg log changeset: 5:28f11e9e992e tag: tip parent: 3:4919ea49b330 parent: 4:e35ab24f4e8d user: ethan@sundance date: Mon Jan 07 22:36:27 2008 -0500 summary: Merge. changeset: 4:e35ab24f4e8d parent: 0:d68883cfd972 user: ethan@sundance date: Mon Jan 07 22:36:11 2008 -0500 summary: Bug fix in file1. changeset: 3:4919ea49b330 user: ethan@sundance date: Mon Jan 07 22:35:45 2008 -0500 summary: Change in file2. changeset: 2:10e9dc8930ac user: ethan@sundance date: Mon Jan 07 22:35:33 2008 -0500 summary: More file2 stuff. changeset: 1:087931ab3964 user: ethan@sundance date: Mon Jan 07 22:35:13 2008 -0500 summary: Additional file1 information. changeset: 0:d68883cfd972 user: ethan@sundance date: Mon Jan 07 22:25:43 2008 -0500 summary: Initial import.
There is another technique for branching in hg (see the Mercurial manual), called "named branches", but the manual suggests that it's for "power users", so for the time being I'll skip it.
git
In git, however, multiple in-repository branches are the standard practice. This is the approach I take here.
ethan@sundance:~/tests/vcsplay$ mkdir git-bothbranches ethan@sundance:~/tests/vcsplay$ cd git-bothbranches/ ethan@sundance:~/tests/vcsplay/git-bothbranches$ git init Initialized empty Git repository in .git/ ethan@sundance:~/tests/vcsplay/git-bothbranches$ cp ../file1-initial file1 ethan@sundance:~/tests/vcsplay/git-bothbranches$ cp ../file2-initial file2 ethan@sundance:~/tests/vcsplay/git-bothbranches$ git add file1 file2 ethan@sundance:~/tests/vcsplay/git-bothbranches$ git commit -m "Initial import." Created initial commit b1ed6be: Initial import. 2 files changed, 4 insertions(+), 0 deletions(-) create mode 100644 file1 create mode 100644 file2
Here we create a new branch, "unstable", based on the "master" branch:
ethan@sundance:~/tests/vcsplay/git-bothbranches$ git branch unstable master ethan@sundance:~/tests/vcsplay/git-bothbranches$ git checkout unstable Switched to branch "unstable" ethan@sundance:~/tests/vcsplay/git-bothbranches$ cp ../file1-add-bug file1 ethan@sundance:~/tests/vcsplay/git-bothbranches$ git add file1 ethan@sundance:~/tests/vcsplay/git-bothbranches$ git commit -m "Additional file1 information." Created commit 678cf5b: Additional file1 information. 1 files changed, 1 insertions(+), 0 deletions(-) ethan@sundance:~/tests/vcsplay/git-bothbranches$ cp ../file2-add1 file2 ethan@sundance:~/tests/vcsplay/git-bothbranches$ git add file2 ethan@sundance:~/tests/vcsplay/git-bothbranches$ git commit -m "More file2 stuff." Created commit 9731ca1: More file2 stuff. 1 files changed, 1 insertions(+), 0 deletions(-) ethan@sundance:~/tests/vcsplay/git-bothbranches$ cp ../file2-add2 file2 git ethan@sundance:~/tests/vcsplay/git-bothbranches$ git add file2 ethan@sundance:~/tests/vcsplay/git-bothbranches$ git commit -m "Change in file2." Created commit a2e015f: Change in file2. 1 files changed, 1 insertions(+), 1 deletions(-)
Switch back to "master", which is the "stable" branch, and commit the bugfix:
ethan@sundance:~/tests/vcsplay/git-bothbranches$ git checkout master Switched to branch "master" ethan@sundance:~/tests/vcsplay/git-bothbranches$ cp ../file1-noadd-nobug file1 ethan@sundance:~/tests/vcsplay/git-bothbranches$ git diff diff --git a/file1 b/file1 index 0d91920..611cc66 100644 --- a/file1 +++ b/file1 @@ -1,3 +1,3 @@ initial 1 -bug 1 +no bug 1 more text ethan@sundance:~/tests/vcsplay/git-bothbranches$ git add file1 ethan@sundance:~/tests/vcsplay/git-bothbranches$ git commit -m "Bug fix in file1." Created commit f413bab: Bug fix in file1. 1 files changed, 1 insertions(+), 1 deletions(-)
This next part was tricky for me to do. We can't just do a pull, because there's no repository on the other end. Instead we have to merge from the master branch.
ethan@sundance:~/tests/vcsplay/git-bothbranches$ git checkout unstable Switched to branch "unstable" ethan@sundance:~/tests/vcsplay/git-bothbranches$ git merge master Auto-merged file1 Merge made by recursive. file1 | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) ethan@sundance:~/tests/vcsplay/git-bothbranches$ git diff ethan@sundance:~/tests/vcsplay/git-bothbranches$ cat file1 initial 1 no bug 1 more text addition ethan@sundance:~/tests/vcsplay/git-bothbranches$ git log commit e16a09baee1e6d03c2e9322ca140e5cfe595c9af Merge: a2e015f... f413bab... Author: Ethan Glasser-Camp <ethan@sundance.nakedbeekey.com> Date: Tue Jan 8 00:09:47 2008 -0500 Merge branch 'master' into unstable commit f413bab71e1fca06a5122928d92a4a15c4a83ff3 Author: Ethan Glasser-Camp <ethan@sundance.nakedbeekey.com> Date: Tue Jan 8 00:06:44 2008 -0500 Bug fix in file1. commit a2e015fce68efd062ee903c2f653b027e712a404 Author: Ethan Glasser-Camp <ethan@sundance.nakedbeekey.com> Date: Tue Jan 8 00:06:00 2008 -0500 Change in file2. commit 9731ca1001bef2051a56979fabddcdbfb72180ff Author: Ethan Glasser-Camp <ethan@sundance.nakedbeekey.com> Date: Tue Jan 8 00:05:48 2008 -0500 More file2 stuff. commit 678cf5b31cd00ab3e668ceae40275afbd4029d53 Author: Ethan Glasser-Camp <ethan@sundance.nakedbeekey.com> Date: Tue Jan 8 00:05:24 2008 -0500 Additional file1 information. commit b1ed6be9b647a4c3b0de26806a53a630737e28f9 Author: Ethan Glasser-Camp <ethan@sundance.nakedbeekey.com> Date: Tue Jan 8 00:03:18 2008 -0500 Initial import.
You can't see it from the log messages, but in fact commit f413.., merged from the master branch, is marked as a merge internally. (You can verify this with gitk.)
mtn
mtn supports a few other commands to control the flow of patches from branch to branch. One, mtn approve, is used to mark a revision as applying to another branch. This creates a new head, which is merged as normal. Alternately, we can use mtn propagate, which does both of these things. I use the approve method here because it's more interesting to me.
ethan@sundance:~/tests/vcsplay$ mtn db init --db=mtn-twobranches.db mtn: misuse: branch 'com.example.project' is empty ethan@sundance:~/tests/vcsplay$ mtn --db=mtn-twobranches.db --branch=com.example.project setup mtn-bothbranches ethan@sundance:~/tests/vcsplay$ cd mtn-bothbranches/ ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ cp ../file1-initial file1 ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ cp ../file2-initial file2 ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn add file1 file2 mtn: adding file1 to workspace manifest mtn: adding file2 to workspace manifest ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn commit -m "Initial import." mtn: beginning commit on branch 'com.example.project' mtn: committed revision 41f51c70d9154c007592505d2ced877239208e68
To create a new branch, the easiest way seems to be to make a new commit with a new branch name, as follows:
ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ cp ../file1-add-bug file1 ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn commit -m "Additional file1 information." -b "com.example.project.devel" mtn: beginning commit on branch 'com.example.project.devel' mtn: committed revision 7680e73fc89ab36633ee8b22fa3bb10602703ade
This switches the branch of the workspace. Further commits will inherit the branch:
ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ cp ../file2-add1 file2 ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn commit -m "More file2 stuf." mtn: beginning commit on branch 'com.example.project.devel' mtn: committed revision f3fa80b40a69b250e4ac34f1951a4b52d39dece9 ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ cp ../file2-add2 file2 ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn commit -m "Change in file2." mtn: beginning commit on branch 'com.example.project.devel' mtn: committed revision eb4fb9d10917bb51e1b95936bccbcc645461da4a
To switch back to the other branch, we update to the head of the other branch:
ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn update --revision=h:com.example.project mtn: expanding selection 'h:com.example.project' mtn: expanded to '41f51c70d9154c007592505d2ced877239208e68' mtn: selected update target 41f51c70d9154c007592505d2ced877239208e68 mtn: target revision is not in current branch mtn: switching to branch com.example.project mtn: modifying file1 mtn: modifying file2 mtn: switched branch; next commit will use branch com.example.project mtn: updated to base revision 41f51c70d9154c007592505d2ced877239208e68
Commit the bug fix:
ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ cp ../file1-noadd-nobug file1 ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn diff # # old_revision [41f51c70d9154c007592505d2ced877239208e68] # # patch "file1" # from [d987f1ccd07df58545c54b44eb12e842a8f26287] # to [41f02bb3ea0fb4edb8922a84b814f4354aeae004] # ============================================================ --- file1 d987f1ccd07df58545c54b44eb12e842a8f26287 +++ file1 41f02bb3ea0fb4edb8922a84b814f4354aeae004 @@ -1,3 +1,3 @@ initial 1 initial 1 -bug 1 +no bug 1 more text ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn commit -m "Bug fix in file1." mtn: beginning commit on branch 'com.example.project' mtn: committed revision 54ccc6e8eb8f779323d21e95d107e85a156c1057
Now, we mark the revision as applying to the development branch:
ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn approve h:com.example.project --branch com.example.project.devel mtn: expanding selection 'h:com.example.project' mtn: expanded to '54ccc6e8eb8f779323d21e95d107e85a156c1057' ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn head --branch com.example.project.devel mtn: branch 'com.example.project.devel' is currently unmerged: 54ccc6e8eb8f779323d21e95d107e85a156c1057 ethan@sundance 2008-01-08T06:23:22 eb4fb9d10917bb51e1b95936bccbcc645461da4a ethan@sundance 2008-01-08T06:19:02
And merge:
ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn merge -b com.example.project.devel mtn: 2 heads on branch 'com.example.project.devel' mtn: [left] 54ccc6e8eb8f779323d21e95d107e85a156c1057 mtn: [right] eb4fb9d10917bb51e1b95936bccbcc645461da4a mtn: [merged] 9f9dd14b67fb0e767ac85d8a8e97807faf2683c4 mtn: note: your workspaces have not been updated ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn update -r h:com.example.project.devel mtn: expanding selection 'h:com.example.project.devel' mtn: expanded to '9f9dd14b67fb0e767ac85d8a8e97807faf2683c4' mtn: selected update target 9f9dd14b67fb0e767ac85d8a8e97807faf2683c4 mtn: target revision is not in current branch mtn: switching to branch com.example.project.devel mtn: modifying file1 mtn: modifying file2 mtn: switched branch; next commit will use branch com.example.project.devel mtn: updated to base revision 9f9dd14b67fb0e767ac85d8a8e97807faf2683c4 ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn diff # # no changes # ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ mtn log o ----------------------------------------------------------------- |\ Revision: 9f9dd14b67fb0e767ac85d8a8e97807faf2683c4 | | Ancestor: 54ccc6e8eb8f779323d21e95d107e85a156c1057 | | Ancestor: eb4fb9d10917bb51e1b95936bccbcc645461da4a | | Author: ethan@sundance | | Date: 2008-01-08T06:27:14 | | Branch: com.example.project.devel | | | | Modified files: | | file1 file2 | | | | ChangeLog: | | | | merge of '54ccc6e8eb8f779323d21e95d107e85a156c1057' | | and 'eb4fb9d10917bb51e1b95936bccbcc645461da4a' | o ----------------------------------------------------------------- | | Revision: eb4fb9d10917bb51e1b95936bccbcc645461da4a | | Ancestor: f3fa80b40a69b250e4ac34f1951a4b52d39dece9 | | Author: ethan@sundance | | Date: 2008-01-08T06:19:02 | | Branch: com.example.project.devel | | | | Modified files: | | file2 | | | | ChangeLog: | | | | Change in file2. | o ----------------------------------------------------------------- | | Revision: f3fa80b40a69b250e4ac34f1951a4b52d39dece9 | | Ancestor: 7680e73fc89ab36633ee8b22fa3bb10602703ade | | Author: ethan@sundance | | Date: 2008-01-08T06:18:54 | | Branch: com.example.project.devel | | | | Modified files: | | file2 | | | | ChangeLog: | | | | More file2 stuff. | o ----------------------------------------------------------------- | | Revision: 7680e73fc89ab36633ee8b22fa3bb10602703ade | | Ancestor: 41f51c70d9154c007592505d2ced877239208e68 | | Author: ethan@sundance | | Date: 2008-01-08T06:16:30 | | Branch: com.example.project.devel | | | | Modified files: | | file1 | | | | ChangeLog: | | | | Additional file1information. o | ----------------------------------------------------------------- |/ Revision: 54ccc6e8eb8f779323d21e95d107e85a156c1057 | Ancestor: 41f51c70d9154c007592505d2ced877239208e68 | Author: ethan@sundance | Date: 2008-01-08T06:23:22 | Branch: com.example.project | Branch: com.example.project.devel | | Modified files: | file1 | | ChangeLog: | | Bug fix in file1. o ----------------------------------------------------------------- Revision: 41f51c70d9154c007592505d2ced877239208e68 Ancestor: Author: ethan@sundance Date: 2008-01-08T06:11:39 Branch: com.example.project Added files: file1 file2 Added directories: ChangeLog: Initial import. ethan@sundance:~/tests/vcsplay/mtn-bothbranches$ cat file1 initial 1 no bug 1 more text addition
darcs
The above version control systems, like Subversion before them, are snapshot-based. This doesn't mean every repository is stored as a whole-tree, only that the conceptual "revision" is a snapshot. By contrast, in darcs, each "revision" is a patch. This leads to the darcs "algebra of patches", and certain nice features like easy cherry-picking. So where in the other systems, we commit the change to the stable branch and pull it to the unstable branch, in darcs we can do it the other way around, commiting it as part of the normal development branch, and pushing it to the stable branch.
First, we create the stable branch, with only the initial files:
ethan@sundance:~/tests/vcsplay$ mkdir darcs-stable ethan@sundance:~/tests/vcsplay$ cd darcs-stable/ ethan@sundance:~/tests/vcsplay/darcs-stable$ cp ../file1-initial file1; cp ../file2-initial file2 ethan@sundance:~/tests/vcsplay/darcs-stable$ darcs init ethan@sundance:~/tests/vcsplay/darcs-stable$ darcs add file1 file2 ethan@sundance:~/tests/vcsplay/darcs-stable$ darcs record -am "Initial import." Darcs needs to know what name (conventionally an email address) to use as the patch author, e.g. 'Fred Bloggs <fred@bloggs.invalid>'. If you provide one now it will be stored in the file '_darcs/prefs/author' and used as a default in the future. To change your preferred author address, simply delete or edit this file. What is your email address? ethan@localhost Finished recording patch 'Initial import.'
Then, we create the development branch:
ethan@sundance:~/tests/vcsplay/darcs-stable$ cd .. ethan@sundance:~/tests/vcsplay$ darcs get darcs-stable darcs-devel Copying patch 1 of 1... done! Finished getting. ethan@sundance:~/tests/vcsplay$ cd darcs-devel
Now, let's create new revisions:
ethan@sundance:~/tests/vcsplay/darcs-devel$ cp ../file1-rev2bug file1 ethan@sundance:~/tests/vcsplay/darcs-devel$ darcs record -am "Additional file1 information." Darcs needs to know what name (conventionally an email address) to use as the patch author, e.g. 'Fred Bloggs <fred@bloggs.invalid>'. If you provide one now it will be stored in the file '_darcs/prefs/author' and used as a default in the future. To change your preferred author address, simply delete or edit this file. What is your email address? ethan@localhost Finished recording patch 'Additional file1 information.' ethan@sundance:~/tests/vcsplay/darcs-devel$ cp ../file2-rev3 file2 ethan@sundance:~/tests/vcsplay/darcs-devel$ darcs record -am "More file2 stuff." Finished recording patch 'More file2 stuff.' ethan@sundance:~/tests/vcsplay/darcs-devel$ cp ../file1-rev4nob file1 ethan@sundance:~/tests/vcsplay/darcs-devel$ darcs record -am "Bugfix in file1." Finished recording patch 'Bugfix in file1.' ethan@sundance:~/tests/vcsplay/darcs-devel$ cp ../file2-rev5 file2 ethan@sundance:~/tests/vcsplay/darcs-devel$ darcs record -am "Change in file2." Finished recording patch 'Change in file2.' ethan@sundance:~/tests/vcsplay/darcs-devel$ darcs changes Thu Jan 3 21:56:40 EST 2008 ethan@localhost * Change in file2. Thu Jan 3 21:55:33 EST 2008 ethan@localhost * Bugfix in file1. Thu Jan 3 21:55:17 EST 2008 ethan@localhost * More file2 stuff. Thu Jan 3 21:54:53 EST 2008 ethan@localhost * Additional file1 information. Thu Jan 3 21:52:23 EST 2008 ethan@localhost * Initial import.
Looks like your typical software development group. Now, the only patch we want to send is the "Bugfix" patch. darcs refers to patches using their commit messages, so:
ethan@sundance:~/tests/vcsplay/darcs-devel$ darcs push --patch "Bugfix" Pushing to "/home/ethan/tests/vcsplay/darcs-stable"... Thu Jan 3 21:55:33 EST 2008 ethan@localhost * Bugfix in file1. Shall I push this patch? (1/?) [ynWvpxqadjkc], or ? for help: y Finished applying...
Now, let's look at what happened in the stable branch:
ethan@sundance:~/tests/vcsplay/darcs-devel$ cd ../darcs-stable ethan@sundance:~/tests/vcsplay/darcs-stable$ cat file1 initial 1 no bug 1 more text ethan@sundance:~/tests/vcsplay/darcs-stable$ cat file2 initial 2
The bug fix was imported, without the rest of the text in file1 or file2. And if we look at the history:
ethan@sundance:~/tests/vcsplay/darcs-stable$ darcs changes Thu Jan 3 21:55:33 EST 2008 ethan@localhost * Bugfix in file1. Thu Jan 3 21:52:23 EST 2008 ethan@localhost * Initial import. ethan@sundance:~/tests/vcsplay/darcs-stable$
This is one of darcs's "killer features".
Pulling from Upstream and Maintaining a Feature Branch
Although we explored pulling and branching in the previous use case, sometimes the manipulation of revisions needs to be a little more complicated. This use case, as set out by Bart's Blog, is this:
So say you're working on a large project -- in [terms] of the number of developers -- to which you don't have commit privileges to. Usually you would submit patches via email and hope they get [accepted]... because if they do you will not have to maintain them out of tree.
Say after the first submission you are told to fix a few things and try again. A new upstream comes out and since you're not really interested in doing development on a patch for an older kernel -- because that will never get accepted.
So now, you need to move your development onto a new branch. With [some] SCMs you would do a merge of the new release into your working branch. And then as you do more development on that branch you end up having a mix of three kinds of changesets: a) upstream changes, b) your changes, and c) merges of your changes with the upstream. It becomes harder and harder to determine what is your new code.
To address this problem, there is a command called git-rebase. bzr has a rebase command, but hg, mtn and darcs don't appear to. Veterans of those systems are encouraged to send me an email..
Go back in time
This is a simple use case, similar to svn update -r. The intention is to see what a source tree looked like at a certain point in time.
darcs makes this the most difficult. The easiest way to do this in darcs is using a get command, i.e. clone the repository.
darcs
ethan@sundance:~/tests/vcsplay$ mkdir darcs-history ethan@sundance:~/tests/vcsplay$ cd darcs-history/ ethan@sundance:~/tests/vcsplay/darcs-history$ darcs init ethan@sundance:~/tests/vcsplay/darcs-history$ echo "file1 init" > file1 ethan@sundance:~/tests/vcsplay/darcs-history$ darcs add file1 ethan@sundance:~/tests/vcsplay/darcs-history$ darcs record -m "Initial import." Darcs needs to know what name (conventionally an email address) to use as the patch author, e.g. 'Fred Bloggs <fred@bloggs.invalid>'. If you provide one now it will be stored in the file '_darcs/prefs/author' and used as a default in the future. To change your preferred author address, simply delete or edit this file. What is your email address? ethan@sundance addfile ./file1 Shall I record this change? (1/?) [ynWsfqadjkc], or ? for help: y hunk ./file1 1 +file1 init Shall I record this change? (2/?) [ynWsfqadjkc], or ? for help: y Finished recording patch 'Initial import.' ethan@sundance:~/tests/vcsplay/darcs-history$ echo "new version" > file1 ethan@sundance:~/tests/vcsplay/darcs-history$ darcs record -am "Version 2." Finished recording patch 'Version 2.' ethan@sundance:~/tests/vcsplay/darcs-history$ echo "whee versions" > file1 ethan@sundance:~/tests/vcsplay/darcs-history$ darcs record -am "Third version." Finished recording patch 'Third version.' ethan@sundance:~/tests/vcsplay/darcs-history$ cd .. ethan@sundance:~/tests/vcsplay$ darcs get darcs-history --to-patch "Version 2." Directory '/home/ethan/tests/vcsplay/darcs-history' already exists, creating repository as '/home/ethan/tests/vcsplay/darcs-history_0' Copying patch 3 of 3... done! Unapplying 1 patch. Finished getting. ethan@sundance:~/tests/vcsplay$ cd darcs-history_0/ ethan@sundance:~/tests/vcsplay/darcs-history_0$ cat file1 new version
bzr
bzr has two options: Create a new repository, as in darcs, or use bzr revert, which modifies the working tree to look like it did in a given revision (but leaves the working tree as modified). I used the revert method here.
ethan@sundance:~/tests/vcsplay$ mkdir bzr-history ethan@sundance:~/tests/vcsplay$ cd bzr-history ethan@sundance:~/tests/vcsplay/bzr-history$ bzr init ethan@sundance:~/tests/vcsplay/bzr-history$ echo "file1 init" > file1 ethan@sundance:~/tests/vcsplay/bzr-history$ bzr add file1 added file1 ethan@sundance:~/tests/vcsplay/bzr-history$ bzr commit -m "Initial import." Committing to: /home/ethan/tests/vcsplay/bzr-history/ added file1 Committed revision 1. ethan@sundance:~/tests/vcsplay/bzr-history$ echo "new version" > file1 ethan@sundance:~/tests/vcsplay/bzr-history$ bzr commit -m "Version 2." Committing to: /home/ethan/tests/vcsplay/bzr-history/ modified file1 Committed revision 2. ethan@sundance:~/tests/vcsplay/bzr-history$ echo "whee versions" > file1 ethan@sundance:~/tests/vcsplay/bzr-history$ bzr commit -m "Third version." Committing to: /home/ethan/tests/vcsplay/bzr-history/ modified file1 Committed revision 3. ethan@sundance:~/tests/vcsplay/bzr-history$ bzr revert -r 2 M file1 ethan@sundance:~/tests/vcsplay/bzr-history$ ls file1 ethan@sundance:~/tests/vcsplay/bzr-history$ cat file1 new version ethan@sundance:~/tests/vcsplay/bzr-history$ bzr status modified: file1
hg
ethan@sundance:~/tests/vcsplay$ mkdir hg-history ethan@sundance:~/tests/vcsplay$ cd hg-history ethan@sundance:~/tests/vcsplay/hg-history$ hg init ethan@sundance:~/tests/vcsplay/hg-history$ echo "file1 init" > file1 ethan@sundance:~/tests/vcsplay/hg-history$ hg add file1 ethan@sundance:~/tests/vcsplay/hg-history$ hg commit -m "Initial import." No username found, using 'ethan@sundance' instead ethan@sundance:~/tests/vcsplay/hg-history$ echo "new version" > file1 ethan@sundance:~/tests/vcsplay/hg-history$ hg commit -m "Version 2." No username found, using 'ethan@sundance' instead ethan@sundance:~/tests/vcsplay/hg-history$ echo "whee versions" > file1 ethan@sundance:~/tests/vcsplay/hg-history$ hg commit -m "Third version." No username found, using 'ethan@sundance' instead ethan@sundance:~/tests/vcsplay/hg-history$ hg log changeset: 2:3e603f2becf7 tag: tip user: ethan@sundance date: Thu Jan 10 15:39:36 2008 -0500 summary: Third version. changeset: 1:b09384762b18 user: ethan@sundance date: Thu Jan 10 15:37:58 2008 -0500 summary: Version 2. changeset: 0:d9b6de4f1081 user: ethan@sundance date: Thu Jan 10 15:37:34 2008 -0500 summary: Initial import. ethan@sundance:~/tests/vcsplay/hg-history$ hg update -r 1 1 files updated, 0 files merged, 0 files removed, 0 files unresolved ethan@sundance:~/tests/vcsplay/hg-history$ cat file1 new version
mtn
ethan@sundance:~/tests/vcsplay$ mtn db init --db mtn-history.db ethan@sundance:~/tests/vcsplay$ mtn setup --db mtn-history.db --branch com.example.history mtn-history ethan@sundance:~/tests/vcsplay$ cd mtn-history/ ethan@sundance:~/tests/vcsplay/mtn-history$ ls _MTN ethan@sundance:~/tests/vcsplay/mtn-history$ echo "file init" > file1 ethan@sundance:~/tests/vcsplay/mtn-history$ mtn add file1 mtn: adding file1 to workspace manifest ethan@sundance:~/tests/vcsplay/mtn-history$ mtn commit -m "Initial import." mtn: beginning commit on branch 'com.example.history' mtn: committed revision d45dc9a897bd213b5583a49e30dad61bc475cdfe ethan@sundance:~/tests/vcsplay/mtn-history$ echo "new version" > file1 ethan@sundance:~/tests/vcsplay/mtn-history$ mtn commit -m "Version 2." mtn: beginning commit on branch 'com.example.history' mtn: committed revision 410c4be9847f750890b402fa7da898feb150cc9e ethan@sundance:~/tests/vcsplay/mtn-history$ echo "whee versions" > file1 ethan@sundance:~/tests/vcsplay/mtn-history$ mtn commit -m "Third version." mtn: beginning commit on branch 'com.example.history' mtn: committed revision b2751a78fdbb6f90e01d256653cbb3fbeeeb80fe ethan@sundance:~/tests/vcsplay/mtn-history$ mtn update -r 410c4 mtn: expanded selector '410c4' -> 'i:410c4' mtn: expanding selection '410c4' mtn: expanded to '410c4be9847f750890b402fa7da898feb150cc9e' mtn: selected update target 410c4be9847f750890b402fa7da898feb150cc9e mtn: modifying file1 mtn: updated to base revision 410c4be9847f750890b402fa7da898feb150cc9e ethan@sundance:~/tests/vcsplay/mtn-history$ cat file1 new version
git
The command you probably want is git reset --hard.
ethan@sundance:~/tests/vcsplay$ mkdir git-history ethan@sundance:~/tests/vcsplay$ cd git-history ethan@sundance:~/tests/vcsplay/git-history$ ls ethan@sundance:~/tests/vcsplay/git-history$ git init Initialized empty Git repository in .git/ ethan@sundance:~/tests/vcsplay/git-history$ echo "file init" > file1 ethan@sundance:~/tests/vcsplay/git-history$ git add file1 ethan@sundance:~/tests/vcsplay/git-history$ git commit -m "Initial import." Created initial commit 0ba134d: Initial import. 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 file1 ethan@sundance:~/tests/vcsplay/git-history$ echo "new version" > file1 ethan@sundance:~/tests/vcsplay/git-history$ git add file1 ethan@sundance:~/tests/vcsplay/git-history$ git commit -m "New version." Created commit 923989c: New version. 1 files changed, 1 insertions(+), 1 deletions(-) ethan@sundance:~/tests/vcsplay/git-history$ echo "whee versions" > file1 ethan@sundance:~/tests/vcsplay/git-history$ git add file1 ethan@sundance:~/tests/vcsplay/git-history$ git commit -m "Third version." Created commit 7f19cd0: Third version. 1 files changed, 1 insertions(+), 1 deletions(-) ethan@sundance:~/tests/vcsplay/git-history$ git reset --hard 923989c HEAD is now at 923989c... New version. ethan@sundance:~/tests/vcsplay/git-history$ cat file1 new version ethan@sundance:~/tests/vcsplay/git-history$ git status # On branch master nothing to commit (working directory clean)
SVN Phrasebook
I found this list on the Ruby-core discussion about revision control:
Command | Meaning | darcs | hg | git | mtn | bzr |
---|---|---|---|---|---|---|
svn blame | Who changed each line in this file last? | darcs annotate | hg annotate | git blame | mtn annotate | bzr blame |
svn log -v | What files were changed in each commit? | darcs changes -s | hg log -v | [1] | mtn log | bzr log -v |
svn diff -r V1:V2 | What changes were between versions V1 and V2? | darcs diff --from-patch V1 --to-patch V2 | hg diff -r V1 -r V2 | git diff -r V1 -r V2 | mtn diff -r V1 -r V2 | bzr diff -r V1 -r V2 |
svn cp [2] | Tag this revision. | darcs tag | hg tag | git tag | mtn tag | bzr tag |
svn cp [3] | Branch this revision. | darcs get | hg clone/hg branch [4] | git branch | mtn commit -b NAME [5] | bzr branch |
[6] | Shelve this change for later. |
[1] | git doesn't have an easy way to show only this information. git log -p shows patches for each commit, but not summaries of what happened to each file. |
[2] | In SVN culture, tagging a revision is done by copying it to a special "tags" directory. All of the distributed version control systems I've covered here support "first-class tags", which are special objects in a special namespace which refer to commits. This is considered "better" in many ways; see the Git crash course section on tags for more on this. |
[3] | In SVN culture, branching is dony by copying a base revision to a special "branches" directory. A DVCS can support branching using either a within-repository or separate-repository model. Those systems that use a "get" or "clone" or "pull" command in this row are using a separate-repository model. Any distributed system, by definition, can use a separate-repository model for branching, but some people feel it is more convenient to have all the branches in one repository. |
[4] | Mercurial's manual favors the separate-repository branch model, though there is support for within-repository branches too -- they are called "named branches". |
[5] | See the "Maintenance Release" use case, above, for more details. |
[6] | The original use case involves branching, switching branches, committing, and immediately switching back. It's not possible to fit the sequence of commands in this table, so I'm leaving it out for now. FIXME. |
Use cases conclusion
While each system has its quirks, they are largely the same and offer very similar functionality.
How to choose a DVCS
With so many options, it can be hard to make a decision for what version control system to use. Here are my recommendations, as well as a summary of salient differences I have found among today's version control systems.
svn
I hoped at the start of this project that I could recommend Subversion for some types of use, but I have only found that svn is largely slower and uses more disk space than other systems. If you want a "better SVN", the way svn is a "better CVS", I'd recommend bzr -- the user interface is very similar, the performance is better, and it's distributed if you need it to be.
cdv
Despite some (in my opinion unmerited flamebait) posts by Bram Cohen on his Livejournal, Codeville does not appear to have much in the way of vibrant user community, rapid development, pleasant user interface, or anything else. I would not recommend cdv for any use.
SVK
I found SVK brittle, slow, too complicated and generally annoying to use, especially by comparison with the other version control systems. It does enable backwards compatibility with svn, but if you need this, you'd probably do better to use bzr-svn, git-svn, or hgsvn.
This leaves the following systems: mtn, hg, bzr, darcs, and git.
Cross-platform compatibility
The Mozilla project ruled out mtn and git right off the bat because of poor Win32 support. git has poor Win32 support because some of its functionality is implemented with bash scripts. mtn does not have this problem, but the Mozilla people describe it as having "similar Win32 performance issues". As far as I can tell, Monotone has a native Win32 port, though I cannot comment on its quality.
hg has Win32 support, including integration with Tortoise Hg. darcs is allegedly cross-platform; see this post by a gentleman named Dave Roberts. bzr is written in Python and has a Win32 port; see this point in the BzrVsGit page.
Performance
The whole point of the VCS Shootout is to try to assess concerns of performance and see which are valid for a given project. Generally, git and hg have reputations for good performance, with bzr following and mtn trailing. darcs does have pretty good performance for the most part in my tests, but also see this post.
Note that the VCS Shootout as it stands now does not test network performance or anything representing actual distributed usage. So, for example, I cannot address the theory that Mercurial's on-disk layout is more optimal than git's.
Usability
For most developers, using a VCS is a matter of knowing about two dozen commands. git is considered to be more difficult to learn, and bzr easier. darcs has a simple view of the world in many ways, which advocates claim makes it easier to use, but I found it uncomfortable for a few reasons (unusual command names, such as "record" for "commit"; the conceptual gap between "set of patches" and "series of snapshots", as in the "history" use case; the use of commit messages to refer to patches, rather than some unique identifier like a number or SHA1). mtn suffers from some additional complexity due to its separation between database and working copy.
In terms of usability, I would say: bzr, darcs, hg, mtn, git.
Merging
When Linus Torvalds says merging sucks by definition, I have to agree. No version control system today (or in the near, i.e. 5-10 year future) has the smarts to say "Oh, in this branch someone referred to footnote 12, but in this other branch someone deleted footnote 12." If you care about merging and which algorithms are used by which systems, you should check the Revctrl Wiki.
Directories/containers
Some version control tools track directories explicitly: bzr, darcs, and mtn. git and hg do not track directories explicitly; they only track directories which contain files.
Some version control tools track files and assign stable identities to them, so that merging can be more intelligent if files are renamed or reorganized. That git does not do this is a "design feature"; the intent is that the contents of the files can be tracked, even if those contents are split into new files.
This is a philosophical argument to some extent.
Metadata: permissions, symlinks, etc.
For some applications, file permissions for given files are important. Being able to track symlinks is also important, although some view them as a security flaw.
FIXME: not sure about bzr, hg, git, mtn. I'm just making up rows for now:
System | Directories | Permissions | Symlinks |
---|---|---|---|
darcs | yes | no | no |
bzr | yes | yes | yes |
hg | no | yes | yes |
git | no | yes | yes |
mtn | yes | no | yes |