About 4 years ago, I started tracking my configuration files with git. The advantages of storing configuration files in some repository are numerous:
- You can destroy your configuration/computer and easily revert to a known good state.
- You can easily distribute and update the same set of configfiles across multiple machines (especially virtual machines or other test setups).
- You can refer other people to your configfiles if you store them in a public repository.
This article describes the way I use git to track my configfiles. My solution is simple and should be easy to understand even if you have not used git extensively before.
Please note that I am not trying to convince you to use my script(s), or my configfiles. Also, I am not going to support this — if you have any troubles with it, you have to debug it on your own. I am merely describing what works fine for me, in the hope that it will be useful to somebody else.
initialize.sh
Obviously, the first step to get the configfiles repository on a “fresh”
computer is to clone the git repository: git clone
git://code.stapelberg.de/configfiles
Afterwards, run the script initialize.sh: cd configfiles &&
./initialize.sh
. This script will create a corresponding dotfile for
each file in your configfiles repository, e.g. when you have
“configfiles/foorc”, it will create “~/.foorc” as a symbolic link:
$ ls -l .zshrc lrwxrwxrwx 1 michael staff 32 2010-09-30 07:57 .zshrc -> /home/michael/configfiles/zshrc
Every already existing file, e.g. your current ~/.zshrc, will be preserved in ~/.configfiles.bak.
Files located in subdirectories
While this approach works well for a large number of configfiles, there are
exceptions. MPlayer for example stores its configfile in ~/.mplayer/config.
Since folders are reserved for a different purpose in my way of doing things,
the solution for this problem is a file called MAPPING
. It
contains a simple lookup table of (filename → path) pairs, e.g. mplayer.config
~/.mplayer/config
. initialize.sh takes care of creating the destination
parent folders if necessary.
Furthermore, you can use that mechanism to group files together by giving them a filename with common prefix. As an example, all my emacs-specific configfilse start with “emacs-”, e.g. “emacs-init.el” and “emacs-zkj-notmuch.el”. I find this much clearer than just naming the file configfiles/init.el.
Host-specific files
If you have files which are specific to a certain host, you can store them in a folder named like your host (use “hostname” to see how the folder needs to be called precisely). To have a standard ~/.Xmodmap, but overwrite it on a single machine called midna (with a weird keyboard), use configfiles/Xmodmap and configfiles/midna/Xmodmap.
I have read about other people using branches to have different configfiles for different hosts. While that may be a more complete solution for some extreme cases, I find it way too hard to manage.
update.sh
In order to actually update the configfiles repository, one would normally run
git pull
. To make this happen automatically, I call update.sh from
my zshrc:
cfgfiles=$(dirname $(readlink ~/.zshrc)) # If the configfiles are in a git repository, update if it’s older than one hour find $cfgfiles -maxdepth 1 -name .git -mmin +60 -execdir ./update.sh \;
Whenever I log into a system which is automatically managed, it will update the configuration files. On the other hand, if I don’t use a system, it will not spend any bandwith/cpu time on updating.
The script update.sh itself is a wrapper around git pull
, but has
a few nice properties:
- Only one instance of the script will run, so that you can open multiple shells/terminal emulators quickly and not have any race conditions.
-
The script uses
git stash
to preserve your local, uncomitted changes. - Instead of operating inplace (and breaking programs that run precisely when git replaces a file, e.g. your shell), the script works in a temporary directory.
- It runs the update in the background so that you don’t have to wait for a terminal emulator window to open. This implies that the shell process which triggered the upgrade will not pick up changes to zshrc automatically.
Detecting and handling errors
Because update.sh runs its upgrade in the background, it will not post any messages about success or failures to stdout/stderr — that might interrupt what you are currently doing in foreground. Instead, it is entirely quiet in case everything is okay.
When something fails (e.g. the server which hosts your git repository is down), update.sh will create a file called ERROR in your configfiles directory. My zshrc is configured to pick this up in its prompt and display it prominently in red:
setup_prompt() { local _cfg_nag if [ -f "$(dirname $(readlink ~/.zshrc))/ERROR" ]; then _cfg_nag="%F{red}cfg-git-error%f " else _cfg_nag="" fi PROMPT="%K{cyan}%F{black}%m%k%f ${fg_green}%~${fg_no_colour} \$(get_git_prompt_info)$_cfg_nag$ " } setup_prompt
So whenever something is wrong, you will end up with a prompt like this:
In order to figure out what went wrong, view the file last-update.log in the configfiles folder. Afterwards, just delete the file ERROR.
Committing changes
There is nothing special to watch out for when committing your changes. That
is, you could edit ~/.zshrc, then go to the configfiles directory and commit
your changes with git commit -a
. Don’t forget to git
push
your changes afterwards. Also, git add -p
might come
in handy to only add specific parts of a file.
Pitfalls
In order to help with trouble-shooting, here are a few mistakes I have done in the past:
- I thought it’d be a clever idea to check out the configfiles repository once in /etc/configfiles and run initialize.sh for each user. The obvious problem is that as a user, you don’t have permission to write to /etc, thus you cannot upgrade. So this idea only works out when you log in as root by default, which you should avoid.
-
You should not use
git stash
on your own in the configfiles repository. update.sh tries to stash any changes and will then just unconditionally try to pop the latest stash entry after pulling.
I run a blog since 2005, spreading knowledge and experience for almost 20 years! :)
If you want to support my work, you can buy me a coffee.
Thank you for your support! ❤️