The slacker’s guide to managing dotfiles

In the past I was using Git for managing dotfiles, and it worked pretty well. Main advantages of this solution were a possibility of syncing settings between devices and an ability to easily experiment with different configurations in separate branches (let’s say switch your code editor to emacs and switch back by just changing a branch if emacs didn’t appeal to you).

But at the beginning didn’t look for any tips and recommendations and went with the very first thing that came to my mind:

  • I created the Git repo in my $HOME.
  • As there were too many files displayed I simply put “*” to .gitignore file.
  • I added required files by “git add -f” and commited them.
  • I created a bare repo on my server,
  • I’ve set the correct remote on my local repo and pushed my changes to server,
  • For my laptop with Wayland (PC has nVidia GPU so…) I was using a separate branch.
  • When I changed something on the PC I firstly rebased and force pushed my Wayland branch but later started merging branches instad of rebasing them.
  • And this setup worked but I my opinion that was not the optimal solution.

    The first problem I noticed was: I put repo’s worktree in my $HOME and not in $HOME/.dotfiles/. Cloning this repo to the new device which already has some configs is problematic, but doable. Managing dotfiles in a separate directory and symlinking it to correct places with help of GNU stow¹ is much easier in my opinion. Generally this could work like this as the only problem I noticed is a little more steps when cloning the repo to the new device, but still I recommend putting the repository in separate folder, I’ll elaborate on it a little later.

    The second problem was: I made the .gitignore file with just “*” as the content and I was using “git add -f ” to add files to the Git. This worked too, but some programs tried to be smart *AKHEM* ripgrep *AKHEM* and by default hide files not managed by Git if the folder is in a repository. So I’ve migrated to using “status.showUntrackedFiles no” and deleted .gitignore file. And again this setting managed to survive for quite a long time.

    The third problem was: I put configurations for different devices in different branches, this seemed like a good idea but in the practice just adds a lot of merging and resolving conflicts which is tiresome… GNU stow and little scripting magic where possible to change some settings based on the hostname helps a lot. Short lived branches for experiments are generally Ok but for slackers’ I would recommend against making the separate branch for each of devices. Too much merging, too much conflicts…

    I recommend using GNU stow instead of branches. It’s quite nice helper for managing many symlinks. It’s Perl script that doesn’t add any extra layer of abstraction, it’s just managing symlinks, allowing to group related configuration in “packages”.

    To illustrate this let’s say that I have 2 “packages” in my .dotfiles folder: fish-completions and fish-config which I want to install in my $HOME directory.

    “Packages” are just directories which mirror final structure of files I want to install:

    └── .dotfiles
        ├── fish-completions
        │   └── .config
        │       └── fish
        │           └── completions
        └── fish-config
            └── .config
                └── fish
                    └── functions

    And now when I’m in my “.dotfiles” directory and type following command:

    $ stow fish-config

    My $HOME will look like this:

    ├── .config → .dotfiles/fish-config/.config
    └── .dotfiles
    ✂ SNIP ✂ SNIP ✂ SNIP

    And now when I’ll execute next command:

    $ stow fish-completions

    My $HOME will look like this:

    ├── .config
    │   └── fish
    │       ├── completions → ../../.dotfiles/fish-completions/.config/fish/completions
    │       ├── → ../../.dotfiles/fish-config/.config/fish/
    │       └── functions → ../../.dotfiles/fish-config/.config/fish/functions
    └── .dotfiles
    ✂ SNIP ✂ SNIP ✂ SNIP

    So, GNU stow merged my “packages” in the smart way so all symlinks are in correct places and points to files inside my “packages”.

    And like a proper package manager GNU stow allow to delete a specified package:

    $ stow -D fish-completions

    And my $HOME again looks like this:

    ├── .config → .dotfiles/fish-config/.config
    └── .dotfiles
    ✂ SNIP ✂ SNIP ✂ SNIP

    (Ab)using those functionalities in a smart way can simulate branches by making some packages containing just different files, and allow experimenting in safe way in a separate package. So it is really helpful. I’m totally recommending using it instead of branches for each of devices.

    This and little ifs in scripts where possible, eliminated need for branching completely for my needs.

    And, forth final problem: Some programs do not play nice with putting it’s files to VCS. Sometimes they are storing the last window size and position together with configs. Sometimes they are using files in “$HOME/.config” to store an ever changing program state. Sometimes they are storing important settings in binary files. All of them makes using Git to store dotfiles quite problematic.

    I’m currently rarely experimenting with new things that require separate branches. And I’m just using my computer without messing with dotfiles. And so lately 90% of my commits had the same message: “qutebrowser: Update bookmarks”.

    And syncing bookmarks between devices started be a real drag:


    Review, state and commit changes.

    git push

    And on the second device:

    git pull
    git merge main

    And then sometimes resolve conficts, if I’ve changed something problematic.

    Have I written something about forgetting to commit or push last changes when I wanted to quickly switch from PC to laptop? Yeah, that happened too.

    Way too much work for me. At this point I’m not even using Git as VCS but just as way to sync my dotfiles between different devices.

    Some time ago I’ve made small experiment with using Fossil SCM² for all my repos (the dotfiles’ one too) and noticed that its autosync feature makes managing dotfiles much easier, but still at that point I felt that using VCS just to sync dotfiles between computers was cargo culting.

    But then my experiment with Fossil SCM failed (this is material for other post) and I returned to sweet home – Git. And so I’ve had an opportunity to rethink my way of managing dotfiles.

    I want an effortless synchronization of dotfiles between my devices. Seriously, stage – commit – push – pull – merge dance every time I’ve changed my bookmarks was no fun.

    I no longer need an ability to quickly revert some changes or switch branches. I’ve used this at the beginning when I was experimenting a lot but now Ctrl+C and Ctrl+V folder with settings and using GNU stow to make separate “package” with modified files to simulate branching is completely sufficient for me. So, I’ve decided to sacrifice powers of VSC for convenience.

    I put all my dotfiles in $HOME/.dotfiles/ symlinked to required places with help of glorious GNU stow, modified configs where possible to use hostname to add changes for different devices and put this in Syncthing³ instead of Git or another VCS. I’m using Syncthing to sync my photos and other files between my devices, so I already had everything installed and configured. And last step: enable simple file versioning on all devices for my dotfiles folder — this only saves few last versions of file when it was changed on other device, it’s not storing local changes. But it helps to manage such case when the configuration change works on other device but is causing problems on current one.

    And this was a hit. Managing my dotfiles become effortless at this point. But I partially sacrificed ability to do quick and safe experiments and revert changes, so this is something to consider.


    1. GNU stow
    2. Fossil SCM
    3. Syncthing


    ↩ Back to home


    This is only a mirror of my Gemini capsule available here: