Using Mercurial locally with CVS

Note: that this is only useful for code that's still exclusively in CVS.

This page is an introduction on how to use Mercurial locally with CVS. That is, code is still checked out/committed to the Mozilla repository using CVS, but all local work is done using Mercurial.

You will manually pull updates from CVS, and you won't get any changeset information (that is, everyone's commits since the last time you updated from CVS will come in as one big blob), but your own commits will be kept as separate changesets.

Note: the term "CVS repository" here refers to a mercurial repository that you keep your CVS checkout in; it never refers to an actual CVS repository, like the one that lives on cvs.mozilla.org.

Contents

Prerequisites

  • Mercurial 0.9.4 (or newer -- though release versions are preferred)

Goal

At the end of this document, you'll end up with two hg repositories:

  • a CVS repository, which is where the hg <-> cvs interaction takes place
  • a clone of the above repository, which is where all your work takes place (you can have more than one of these; but you can only have one CVS repository)

Each chunk of commands will assume that you're starting in some toplevel directory; the CVS repository will end up in cvssrc/mozilla; feel free to replace cvssrc with something else.

Note that there are other ways of setting this up, such as using named branches, or doing all your work in one repository, but this is the way that works for me, and I think is also the easiest way to avoid getting very confused.

Initializing the CVS repository

First, pull a mozilla checkout into a clean dir.

% mkdir cvssrc
% cd cvssrc
% cvs -d :ext:yourcvsusername@cvs.mozilla.org:/cvsroot co mozilla/client.mk
% cd mozilla
% MOZ_CO_PROJECT=browser make -f client.mk pull_all

Now, check that tree into a mercurial repository.

% cd cvssrc/mozilla
% hg init
% vi .hgignore     see below
% hg addremove
% hg commit -m "cvs sync"

Here are the contents of my .hgignore file; they can probably be optimized, but this works for me (please edit this if you end up with a better one!)..

.*~$
^CVS
.*/CVS
.*/CVS/.*
.*/NONE
\.\#.*$
^\.cvsignore$
^\.DS_Store$
^Makefile$
^config\.log$
^config\.cache$
^config\.status$
^\.client-defs\.mk$
^config-defs\.h$
^\.mozconfig$
^\.mozconfig\.mk$
^\.mozconfig\.out$
^a\.out$
^unallmakefiles$
^nss$
^mozilla-config\.h$
\.flc$
\.orig$
\.pyc$

hg addremove marks any files in the working dir that aren't present in the repository for addition, and marks any missing files as missing. The hg commit will create your initial commit.

Cloning the CVS repository

Do not do any work inside the CVS repository! Instead, create a clone:

% hg clone cvssrc/mozilla mozilla

And do your work inside the mozilla repository.

Doing work

Perform any changes inside the cloned mozilla repository. You may use any combination of hg extensions, including mq, transplant, etc. to manage your changes in this repository.

Once you are ready to commit your work back to CVS, you'll need to make sure that a hg changeset exists for your patch(es). In the simplest way, this will just mean running hg commit.

Committing your work to CVS

Let's say you just made a change:

% cd mozilla
% echo "Hello World" >> README.txt
% hg commit -m "Add mystery message to README.txt"
% hg log -l 2
256[tip]   603e507ddf25   2007-07-31 13:41 -0700   vladimir
  Add mystery message to README.txt

255   a4a7678829ff   2007-07-31 13:31 -0700   vladimir
  cvs sync

Now you want to get it into CVS:

% cd cvssrc/mozilla
% hg pull ../../mozilla    this is the cloned mozilla repo
pulling from ../../mozilla
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
(run 'hg update' to get a working copy)

At this point, your repository contains the changesets from ../../mozilla. However, your working dir does not:

% hg parent
255   a4a7678829ff   2007-07-31 13:31 -0700   vladimir
  cvs sync
% hg tip
256[tip]   603e507ddf25   2007-07-31 13:41 -0700   vladimir
  Add mystery message to README.txt

Note that the parent of your working dir is rev 255, whereas the tip of the repository is 256. Because CVS has no knowledge about what's in the hg repository, we need to get our working dir up to date:

% hg update
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

Now, you can do the cvs commit:

% cvs commit -m "b=123456, Add mystery message to README.txt, r=sparky" README.txt

Updating your tree(s) from CVS

Because other people also work on Mozilla, you'll want to update from CVS often. First, we need to pull from CVS and add a new commit to the CVS repository:

% cd cvssrc/mozilla
% MOZ_CO_PROJECT=browser make -f client.mk pull_all
% hg addremove
% hg commit -m "cvs sync"

(Tip from my Sylvain on my blog: you can combine the last two lines into just hg commit -A -m "cvs sync"; once in a while I end up with leftover CVS .foo.12.3 files that I don't want committed though, so it's nice to be able to verify before committing.) Then, you need to update your local clone(s):

% cd mozilla
% hg pull ../cvssrc/mozilla

Just like committing to CVS, a pull does not update your working directory. If you haven't made any changesets since the last time you did a pull, you can bring yourself up to date by just:

% hg update

However, if hg pull said that it added 1 (or more) heads, then you need to work out a merge strategy. See below for strategies.

Note that if you're using mq to manage all your patches in your cloned repo, you can make sure that you always have the simplest situation by removing all patches from the stack using hg qpop -a before pulling from the CVS repo.

Dealing with conflicts

Conflicts when committing to CVS

The safest way to ensure that there are no conflicts before committing to CVS is to update your CVS trees immediately before, and do any merging necessary. However, sometimes someone else gets a commit in before you. So, we have:

% cvs commit -m "..." foo
cvs server: Up-to-date check failed for `foo'!

You have two options at this point:

  • go back and do a CVS update first:
hg update 255    the rev num before the current one; there's no shorthand yet

Then follow the instructions for updating from CVS, and try committing again.

  • manually cvs update foo and hope for the best:
% cvs update foo
M foo
% cvs commit -m "new foo" foo
% hg commit -m "cvs sync" foo

Note the final hg commit -- you made a change to the working dir that hg doesn't know about yet, so you need to make sure that it's recorded. At this point you should do a full CVS update as well.

Conflicts when updating from CVS

Ideally you'll never run into this if you're using MQ to manage your patches, because you will always have removed all the patches from your queue before pulling from the CVS repository into your cloned working repository.

However, sometimes you will end up with a situation where you have multiple heads in your working repo, and you need to do something about it. There are a few options; I won't go into too much detail, but here they are:

  • hg merge, clean up conflicts, hg commit and carry on. This works, but the downside is that this potentially leaves your un-CVS-committed patch (your changeset) in a somewhat messy state that will be hard to submit for reviews (because it may not be against the latest CVS code) and will be impossible to modify (because it's already in the revision stream)
  • hg update tip to jump to the latest tip that you just pulled from the CVS repo, and then use the transplant extension to move your patch(es) over from previous change sets. e.g. hg transplant 255 to take changeset 255 and apply it to the current working dir. Transplanting in this way is very similar to hg export 255 > foo.patch followed by a patch -p1 < foo.patch, hg commit -m "orig commit message", except that it will stop and ask you to fix up conflicts.

Managing patches

This is a brief introduction to MQ, the Mercurial Queues extension. You must explicitly enable it by adding

hgext.mq =

to the [extensions] section of ~/.hgrc or Mercurial.ini. You'll know whether it's enabled if hg qapplied doesn't return an error about an unknown command.

A much more comprehensive overview is available in the MQ chapter of the hg book.

Creating a new patch

% cd mozilla
% hg qnew 369012-fix-bad-crash
% [hack hack hack]
% hg qrefresh

The above initializes a new patch named "369012-fix-bad-crash" (you can use any valid filename that you wish, though I tend to prefix things with the bug number... I use xxxxxx if no bug has been filed yet). After you make your changes, hg qrefresh updates the topmost patch with the current diff from the working directory.

You can also create a patch after the fact, incorporating any outstanding changes in the working directory into it. This is the way that I tend to work:

% cd mozilla
% [hack hack hack]
% hg qnew -f 369012-fix-bad-crash

The -f informs qnew to take the outstanding changes and make them part of the patch; otherwise, qnew will complain about outstanding changes and refuse to make a new patch.

After either of these steps:

% hg qapplied
369012-fix-bad-crash
% hg qtop
369012-fix-bad-crash

qtop will show you the current topmost patch, and qapplied will show you the list of applied patches. Let's say you make another patch:

% ... make changes ...
% hg qnew -f 301123-new-feature
% hg qapplied
369012-fix-bad-crash
301123-new-feature
% hg qtop
301123-new-feature

The 301123-new-feature patch is created relative to the previous patch for 369012. This is important in case you ever want to reorder patches, or if you need to submit patches for review that have dependencies on other patches.

You can pop the topmost patch from your working directory:

% hg qpop
Now at: 369012-fix-bad-crash

Note that these patches live as normal patch files in the .hg/patches directory; this is what I use for submitting patches for review. The order in which they're applied is just a text file in .hg/patches/series -- but don't edit the order of currently applied patches, because bad things will happen.

Note that while patches are applied, they show up as real changesets in the repository:

% hg qapplied
one
two
% hg log -l 3
3[qtip,tip,two]   c1e6c90c12ad   2007-07-31 15:20 -0700   vladimir
  imported patch two

2[qbase,one]   2ba59c979503   2007-07-31 15:18 -0700   vladimir
  [mq]: one

1[qparent]   587167de96c1   2007-07-31 14:42 -0700   vladimir
  Add mystery message to README.txt

This is why it's important to qpop -a to remove all patches from the stack before doing any pushing or pulling from your working repository. Otherwise, the remote doesn't have any idea of your patches, and these will just show up as changesets.

When you're ready to commit a patch, the easiest thing is:

% hg qpush
Now at: one
% hg qapplied
one
% hg qrm -r one

qrm -r patchname tells mq to stop managing the patch named patchname which must a) be applied; and b) be the bottommost patch on the stack. At that point, that patch is just a normal changeset, and you can proceed with the "committing to CVS instructions".

Some other useful commands: hg qdiff will show you the diff between your working dir and the previous patch; this is different from hg diff, which will show you the diff between your working dir and the last time you refreshed the topmost patch. Both are useful. You can rename patches using hg qrename, e.g. to give a patch a bug number. You can also give a patch a commit message, either at qnew time using -m, or at qrefresh time, also using -m.