Tuesday, March 8, 2011

Managing Configuration in Git

A common pattern I run into when using Git (my favorite version control software, by far) is wanting to manage local project configuration that does not get committed to a central repository. This might be something like a local email address for testing, or local database settings. The cleanest option is to set up these configuration options outside of your project or within a directory or file that is not tracked by version control. If you do want to track local configuration in your local version control, you have two options:

  1. Use 'git stash' to store away your local changes whenever you need a clean working set.
  2. Track local changes as a commit or set of commits.
The drawback for solution 1 is that it is easy to accidentally commit your local configuration changes. It's also easy to lose your local configuration if you are frequently stashing other changes too.

The drawback for solution 2 is that you can also accidentally commit local configuration changes. You do, however, have a reference to your configuration changes that you are unlikely to lose. What I'll be describing below is my shortcut for handling solution 2.

For this pattern, I use three branches.
The 'head' branch points to the latest commit from the central repository.
The 'config' branch points to your configuration commits that you don't intend to commit to the central repository.
The 'dev' branch points to your new commits that you will likely commit to a central repository.


head -> .. -> config -> .. -> dev

To start out, create the head branch to track the latest commit from the central repository.


git checkout -b head

Then create the config branch to track configuration changes.


git checkout -b config
git add foo.config bar.config
git commit -m "Local configuration"

Finally, create the dev branch for your current development efforts.


git checkout -b dev
git commit -a -m "Added feature X"

If you perform a git fetch or git pull, we'll want to update the head branch to include the upstream changes. We will periodically 'rebase' the config branch on top of our head branch. This will put the configuration commits after the head branch and allow you to work with the configuration changes applied.
The instructions to follow make judicious use of the 'git rebase' command, which is a command that rewrites history. Please read the man page for this command and never use it on published commits.


git checkout head
git pull (or fetch, or pull --rebase)
git checkout config
git rebase head
git checkout dev
git rebase config

When you are ready to commit your changes back to the repository, you'll need to pull out your local configuration changes.


(published commits) -> head -> (configuration commits) -> config -> (unpublished commits) -> dev

The following command applies only the changes after the config branch to the head branch.


git rebase --onto head config dev

The result is a version history that looks like the following. As you can see, the local configuration changes have been removed.


(published commits) -> head -> (unpublished commits) -> dev

At this point, you can perform a 'git push', or if you are using git-svn, perform 'git svn dcommit'.


git push origin
(or)
git svn dcommit

Now to include configuration changes in your development branch again:


# Stick our config changes back on top of the latest revision
git checkout config
git rebase head
# Move development branch on top of configuration changes
git checkout dev
git rebase config

Complicated, right? I found myself doing this sequence again and again and finally came up with some aliases that cut down on the number of keystrokes. These aliases do everything above in two simple commands.
Add the following to your ~/.gitconfig:


[alias]
        # Useful for omitting and restoring configuration commits
        configdrop = "!sh -c 'git rebase --onto $1 $2 $3 && git checkout $1 && git rebase $3' -"
        configrestore = "!sh -c 'git checkout $2 && git rebase $1 && git checkout $3 && git rebase $2' -"

So instead of doing everything above you can simply do:


// Drop config changes
git configdrop head config dev
// Push your changes to the central repository (make sure everything looks good in gitk first!)
git push / git svn dcommit
// Put your config changes back in place
git configrestore head config dev

You could merge those three commands into one big alias but then you wouldn't have the opportunity to review your change before pushing to the upstream repository.

These aliases use the 'git rebase' command, which is a command that rewrites history. Please read the man page for this command and never use this alias on published commits.

No comments:

Post a Comment