Keeping Track of Personal Linux Customization with Git

May 08, 2018

When you heavily use Linux on desktop, it is not easy to keep track of all the customization you make. Here is my solution.

I often, probably daily, modify some configuration in my desktop computer running Ubuntu Linux, and it is very easy to lose track of what I have modified after some time. For personal computers, I think a popular solution is to use dotfiles, and if it is a server you have Ansible, Chef etc. However, I am not happy with neither approaches on a desktop. Mainly for two reasons; 1) I have to test the modifications also on the automation scripts (to be sure scripts are correct you need to at least re-run them), 2) I do not want to explicitly spend effort every time I change something, i.e. if I add a new apt source config, that should be reflected automatically without requiring me to modify dotfiles/Ansible/Chef.

The approach I describe below can also be used for simple/prototyping projects that you normally do not start using Git from the beginning or if you do not need the full power of Git of individual commits you do not need to manually maintain the repo.

First we need a systemd unit to run everytime we shutdown/reboot the computer. This unit basically runs just before a shutdown/reboot/halt is initiated, and executes /home/mete/.commit-configs.sh and wait for it to finish. systemd is available on Linux, but every OS has some way of doing this.

$ cat .config/systemd/user/commit-configs.service

[Unit]
Description=Commit Configs
DefaultDependencies=no
Before=shutdown.target reboot.target halt.target
[Service]
Type=oneshot
ExecStart=/home/mete/.commit-configs.sh
[Install]
WantedBy=halt.target reboot.target shutdown.target

.commit-configs.sh is very simple, just commits anything modified or deleted but does not automatically add new files (!). The reason I have git diff-index is that git commit exists with status code=1 when there is nothing to commit and this causes systemd to think the commit-configs unit failed. You can read more about using git diff-index (and a few others) which helps scripting with git in this post.

$ cat .commit-configs.sh
#!/bin/sh

cd /home/mete/configs
/usr/bin/git diff-index - quiet HEAD - || /usr/bin/git commit -a -m "update"

Do not forget to make this executable, so:

chmod +x .commit-configs.sh

We need a git repo to store all changes. I am using /home/mete/configs for that.

mkdir configs
cd configs
git init

As you guess, we cannot normally add files out of this directory to the git repository. However, we can modify the worktree to achieve this. I use “/” since I store anything on the file system in this repo. This is I think the most important part of this setup, since you can achieve the same in many different ways but being able to map whole or part of filesystem to somewhere else (git repo here) makes this setup very easy to use.

git config --local core.worktree "/"

Now, we can add files to the repo, for example:

git add ~/.vimrc
git add /etc/apt

Of course, you need to be very careful (!) what you add to this repo, as some of the system files may contain private information, credentials, passwords etc. You can use .gitignore to have a fine control. For example, I would like to keep my .vim folder like this but I do not want to keep the external bundles:

$ cat ~/.vim/bundle/.gitignore
*/

$ git add ~/.vim

*/ is because I do not want the folders in .vim/bundle which are the external vim plugins / bundles that I can clone from their repo when needed.

Let’s try systemd script now, first reload the daemon to read changes.

systemctl --user daemon-reload

You can see the unit we created with this:

systemctl --user list-unit-files

You can manually start the unit like this:

systemctl --user start commit-configs

The output of the script can be seen with:

journalctl -xe

Finally, we enable the unit to run it as we planned, before shutdown/halt and reboot.

systemctl --user enable commit-configs

Now we can add anything we want to track to the repo, and the changes are stored between reboots. There are many ways you can customize this:

  • You can have more than one repo for different purposes. Just modify the .commit-configs.sh to commit changes to each repo.
  • You can add systemd start or .commit-configs.sh to crontab to commit changes more often e.g. every hour.

As the repo keeps the latest working state, it also functions as a backup. You can keep the repo in a cloud storage service like Dropbox.

I also do like to keep all the changes I made to configuration files that comes with standard packages. The simplest way to do this is to install debsums package in Ubuntu. I use it as:

debsums -ce --ignore-permissions --ignore-obsolete

which shows all configurations that are different than the ones installed with the packages, ignoring permission errors and obsolete packages. Then, I add this to crontab with | xargs git add and all the changed configuration files are added automatically to my configuration repo as well. The crontab entry is:

0 * * * * cd ~/configs && list-modified-files-of-packages | xargs git add

The solution I described above is simple but not perfect. There are two major problems. You may miss the changes between commits since it checks for the changes and creates the commits periodically. Also, you have to add the files to the repo manually, although this can be improved a bit by using a combination of .gitignore files and adding all of the new files in a specific directory (e.g. /etc/apt) or with a specific suffix (e.g. .conf) to the commit (probably in .commit-configs.sh). In order to solve the first issue and have a more advanced solution for the second, we need a way to monitor file system changes. There are a few ways to this in Linux. systemd provides a path based activation method where systemd monitors the paths you specify and activates a unit when a change is detected. However, you need to implement at least some utility scripts/tools to manage this. A much more advanced solution would be to use inotify and fanotify APIs where you can monitor the file system changes and then create a Git commit also programmatically. However, not surprisingly, this requires a significant effort before being usable.