Recording /etc/ config in git is a recommended way to track history and revert breaking changes. In many cases, content needs to be imported and transformed from other repositories before being deployed as /etc/ config on a VM . Config changes to /etc are often made in haste during development & in emergencies. While git is helpful in recording those changes locally, often content needs to be managed on one machine and pushed to another. Or content can be in an outside repo with a different directory schema.

Git Recombine is a decentralized strategy to allow content to flow in any direction and manage merge conflicts when they occur. With Git Recombine, which relies on git clone, git subtree & git push — content can be edited anywhere — either in-place or on a separate machine — and deployed to /etc.

Git Recombine is not a replacement for ansible, terraform, chef or other VM management tools—it’s a content-first approach that compliments more formal VM management. Being content-based, it’s faster & more dynamic than writing templates and transforms. It catches merge conflicts early and immediately when the config is applied, avoiding clobbered changes. It tracks all changes and requires minimal work to record history.

Repository Layout

Git Recombine relies on pushing commits around multiple clones (local or remote).

  • the primary repo is in /etc (/etc/.git)
  • $HOME/clones/etc-staging is a clone of /etc/ for illustration. It could be any remote.
  • app-specific clones like $HOME/clones/smokeping , $HOME/clones/lighttpd are external repositories with a different directory structure

Alt text

Let’s go through an example setting up Smokeping & Lighttpd on alpine with Git Recombining 2 third-party repos, smokeping & lighttpd, and one etc-staging repo

Creating the Git Repo

Create the git repo with git init , adding receive.denyCurrentBranch=updateInstead to support deploy-on-push, and status.showUntrackedFiles=no to reduce clutter

cd /etc
sudo git init . 
sudo chown -R $USER .git
# optional, reduces clutter 
git config --add status.showUntrackedFiles no
# checkout (deploy) files to /etc upon push
git config receive.denyCurrentBranch updateInstead 
# add your first directory as usal
git add smokeping

Cloning /etc and Pushing Around Content

Git Recombine relies on multiple clones. This way content can be managed within /etc/ directly or edited in the etc-staging clone on a remote machine. git fetch and git push will deploy config to /etc

cd $HOME/clones
$ git clone /etc ./etc-staging
$ cd /etc
# make changes to any file e.g. /etc/lighttpd/lightttpd.conf
$ git commit -m "add alias to lighttpd.conf"
$ cd $HOME/etc-staging
# fetch all commits and branches from primary repo /etc/
$ git fetch origin
# reconcile changes
$ git rebase origin/master

Pushing from etc-staging to /etc

And you can deploy to /etc by pushing from $HOME/clones/etc-staging. git will block this push as usual if there are conflicts. use git hooks to further restrict this as needed.

$ cd $HOME/clones/etc-staging
# edit ./lighttpd/lighttpd.conf
$ git commit -m "added hostname param"
# this will deploy to /etc/ because receive.denyCurrentBranch=updateInstead
$ git push origin HEAD 

Recomposing config from an outside Repository

git subtree is an underappreciated command that works to transform and integrate content from outside repositories into a target repository by using a merge commit. It’s similar to submodule, except that all content and commits are stored within the target repository. Because merge commits are used, subtree pull can reconcile future commits into an existing history.

Structure Before

clones
├──  smokeping
|      └── Config
├── etc-clone
├   └──  smokeping
         └── config.d

Let’s Recombine smokeping/Configetc-staging/smokeping/config.d

Transform the config

git subtree split transforms trees . In this case we are moving /Config/* to the top of the tree so we can merge it into our ~/clones/etc-staging repo

$ cd ~/clones/smokeping
$ git subtree split --prefix=config \
  --annotate='(smokeping-config)' \
  --squash --rejoin \
  --branch smokeping-config

Now content has been moved to the top of the tree

$ git checkout smokeping-config
git ls-tree HEAD|head -n 2
100644 blob cbcfc41a361bb548cd68445bee8c613c0b3f8936    Alerts
100644 blob 0ca91f6f49bc89acc1c8b96a2a4f170bb8533d6c    Database

Recombine into etc-staging using subtree

git subtree add will merge the config into a subtree (subdirectory) . In this case ~/clones/smokeping:/~/clones/etc-staging:smokeping/config.d

# merge content from ~/clones/smokeping into here as smokeping/config.d
# --squash is optional -- omit it if you prefer keeping the full source repo history
git subtree add --prefix=smokeping/config.d ~/clones/smokeping smokeping-config --squash

Review the merge log to see the squashed commit.

$ git log -1 

23696e5 - Merge commit '920056da1b35aafe5e50280008d20c0c5b6fd20b' as 'smokeping/config.d' (8 hours ago) <Anthony Metzidis>

Conclusions and Next Steps

The goal of Git Recombine is to allow for content to flow in any direction— among /etc & a development workspace — among outside source repos, while allowing for transforms and ad-hoc commits. Since changes to /etc are often haphazard and urgent, having tools to manage many various sources of content — from within and outside /etc itself — will help improve change management and merge conflicts.

Most often, only git push or git subtree add/merge will be necessary, and overhead will be similar to your familiar git add-commit-push flow. With practice you’ll find it more manageable to push content around via git than to rsync or copy/paste.

More Tools & Resources

  • etckeeper is a great tool to automate saving to etc nightly & during package installation
  • git docs on subtree undersell subtree as an “advanced merge” feature. In fact — it’s better than submodule and enables many more flows when a transform is needed.