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.

Blue Sky design by Jonas John.