Dealing with multiple version control systems
I love git and I use it all the time. I use it for my personal projects, my dotfiles, my documents and even my home directory. But for work, I need to use mercurial as well as git (mercurial for the mozilla-central mono-repo and git for other repositories, mostly hosted separately on GitHub). I like mercurial too. But there’s a problem: whenever I switch between those version control systems, I always forget which one I’m using right now. For example, when I switch from a git repo to a mercurial repo, I use the git command instead of hg. Then I change my command back and run the correct command after getting an error that says this is not a git repository… That was driving me crazy for a while. Then I came up with a bash function that saves me from rewriting the whole command and acts like the version control system of that repository.1 It’s not perfect and doesn’t solve everything. But it solves most of my issues, so I wanted to write about it in case someone else needs it.
By the way, for background, I’m using the g alias for the git command and the h alias for the hg command.
Proxy function that acts like it’s git or hg depending on your repository
Let’s see the whole function first:
f() {
if [ -d ".git" ]; then
git "$@"
elif [ -d ".hg" ]; then
hg "$@"
else
if git rev-parse --git-dir > /dev/null 2>&1; then
git "$@"
elif hg root > /dev/null 2>&1; then
hg "$@"
else
echo "Not a git or hg repository!"
fi
fi
}
alias g="f"
alias h="f"
As I told you before, I’m using g and h, that’s why I aliased them to this f function at the end. So now, whenever you type a g or h command, that function will act as a proxy and dispatch it to the corresponding version control system. I was using g and h commands before this function and that’s why I wanted to keep them and alias them to this function. But if you want to, you can remove the aliases and just use the f function itself (or you can rename it and use it however you like).
Now if you run g status in an hg repository, the function will automatically convert it to hg status and you won’t get any error that says “not a git repository bla bla…”.
Now let’s go through the function and see what’s going on there.
if [ -d ".git" ]; then
git "$@"
elif [ -d ".hg" ]; then
hg "$@"
This is the first part of our function. It will look at your current directory and see if there is a .git or .hg directory inside. If there is one of them, that means we found our version control system since these are the directories that contain all the information about the repository. But that part will only work if you are in the root directory of your repository. But you may be a few levels deep inside that root directory. So if we fail to find .git or .hg, we go into the else branch:
if git rev-parse --git-dir > /dev/null 2>&1; then
git "$@"
elif hg root > /dev/null 2>&1; then
hg "$@"
else
echo "Not a git or hg repository!"
fi
In the first if condition, we have git rev-parse --git-dir > /dev/null 2>&1, that command walks up from your current directory to the root directory and tries to find a .git directory. If it finds one, it returns it. If it doesn’t find any, it says “not a git repository” and fails with an error return code. Since we have > /dev/null 2>&1 at the end, we won’t see any output during execution; it will pass or fail silently. So with that command we will be able to see if we are inside a git repository.
The elif branch works very similarly: it tries to find the root hg directory of the current directory. It returns that directory if it finds it and fails if it doesn’t.
That way we manage to find the repository of the current directory. You may be wondering why we have the outer if, since we can also use the inner if as a more general solution and get rid of the outer one. These commands are pretty fast, but checking whether a directory is present is much faster. So I wanted to keep this outer if as a fast path, since we mostly run our git/hg commands in the root directory.
But the commands are not the same!
There is another thing. Let’s say that you wrote g show in an hg repository. That will automatically turn it into hg show. But the problem is, there is no show command in mercurial :(. So hg will try to run the closest command to it, which is showconfig. So it will print the whole config, but we didn’t want that. We wanted to see the latest revision. At this point, aliases come to the rescue! You can create a show alias in mercurial to act like hg log -pr if you want. The simplest alias would be like this in your .hgrc file:
[alias]
show = log -pr
That’s the simplest solution, but it will fail if we enter h show because we didn’t put any revision number after it. We can make it a little bit more sophisticated like this:
[alias]
show = !sh -c 'REV=$1; hg log -pr ${REV:-tip};' -
In this alias, we are running a shell command directly with !sh -c command. Then, we are doing argument defaulting in ${REV:-tip}. If nothing is passed to the command, it will run hg log -pr tip. If a revision number is passed, it will run hg log -pr <revision>.
So yeah, I hope you got the idea. My example was about using the git command in hg. But the opposite is also possible by adding an alias to your .gitconfig file. That way you can create aliases in git or hg for all the commands you use frequently, so you won’t have to rewrite everything. Of course, sometimes it’s not possible to create an alias for that specific command you use. But hey, I didn’t say that my solution solves everything :). But it does solve the majority of my problems, and I hope it solves yours as well!
-
You may ask about using git-cinnabar for mercurial repositories. I know that the git-cinnabar project is pretty solid and has a great maintainer, but I personally don’t like using third-party tools to interact with my repositories. ↩︎