World Playground Deceit.net

Dotfiles management


Last week I wrote a final (and very NIH) solution to my dotfiles management problem, so here's a small post about it. What motivated me is hearing from a colleague who started using chezmoi and my general distaste for these dedicated kitchen sink solutions when a simple script is perfectly suited for such a task.

My ideal design (and what I initially tried, like a lot of people) would have been for git to allow following symlinks and for filters to also handle path rewriting. Sadly, this isn't the reality we live in and would have been only half of the solution (save/backup, not restore/deploy) anyway.

Another interesting solution I've recently read about is a bare git repo inside $HOME configured to hide untracked files together with a git --git-dir=/path/to/repo --work-tree=$HOME alias. Pretty elegant, but there's a lot of stuff in /etc I have read access to I also want to manage.

So here it is, at the root of my dotfiles repo: do (so-called because it'll usually be called as .../dotfiles/do sync|restore).

It's a pretty large POSIX sh script with a section at the end reserved for the user to declare his backup strategy. I'll let the explanatory banner and my example speak for themselves:

################################################################################
#                          User syncing rules section                          #
################################################################################
# update    update existing files in store dir $1                              #
# mirror    destructively mirror store dir $1                                  #
# generate  save/restore file $1 ("!" basename prefix added) using cmd $2/$3   #
#                                                                              #
# The "store" is the parent dir of this script, a store path is always         #
# written relatively to the store's root, the corresponding system path used   #
# as reference for syncing/restoring is that store path prefixed with "/".     #
#                                                                              #
# The -m option can be used to alter that mapping for all operations. For      #
# example, `mirror home` would use "/home" as system reference, but specifying #
# `-m home /home/user` changes it to "/home/user".                             #
################################################################################

update etc/
mirror etc/conf.d/
mirror etc/portage/
update home/
mirror home/.wallpapers/
generate crontab 'crontab -l' 'crontab'

And a small session to show it off:

$ ~/.dotfiles/do sync -m home "$HOME"
$ alias dfgit='git -C ~/.dotfiles'
$ dfgit status
...
$ dfgit add . && dfgit commit -m sync && dfgit push origin HEAD

Then on another machine, for example at work:

$ git -C ~/.dotfiles pull
$ DIFF=colordiff PAGER='less -F' ~/.dotfiles/do restore -m home "$HOME" home/.zshrc
Restoring 'home/.zshrc' would overwrite already existing file '/home/user/.zshrc'
overwrite (y), overwrite all (a), skip (n), quit (q) or show diff and ask again (d)?
--- /home/user/.zshrc        2025-01-04 18:42:18.427402971 +0100
+++ home/.zshrc 2025-01-04 18:36:58.070489857 +0100
@@ -35,6 +35,7 @@
 alias rsync='rsync --info=progress2 --sparse --progress --human-readable'
 alias rsync_vfat='rsync --no-owner --no-group --no-perms --modify-window=2'
 alias rsync_exfat='rsync --no-owner --no-group --no-perms'
+alias startx='startx -- -nolisten tcp -nolisten local vt1'
 alias tmux='tmux -u'
 alias sbcl='sbcl --noinform'
 alias ccl='ccl -b'
overwrite (y), overwrite all (a), skip (n), quit (q) or show diff and ask again (d)?

Except for generate not yet having that cool git-style interactive menu, I don't see what else I could ask from this script. Anything more complicated like filtering/preprocessing can be handled by that generate catch-all.

It only needs some better documentation in -h, a more useful -r (dry run) output and maybe a merge tool option for interactive restore.

I'd love to see other people write about their own way of doing it!