Tomas Krizek's blog

A collection of madman's technical rambling

View on GitHub

.dotfiles: avoid accidentally deleting your home directory

Last updated: 2020-12-14

TL;DR If you’re using git to track your dotfiles, make sure to separate the git directory using worktrees. Being able to call git directly in your home directory can have unexpected and potentially destructive consequences.

git responsibly.

A few years ago, I’ve started tracking my configuration files in my home directory with git. I was happy with my solution, since it was simple and convenient. By doing so, I’ve also unwittingly planted a nasty landmine to step onto later: an equivalent of rm -rf ~/. Today, I’ve became the victim of muscle memory and my lousy dotfiles setup. I’ll explain how it happened and how you can avoid it.

The Lousy Dotfiles Setup

My dotfiles setup was very basic. I treated ~/ as any other git repository. When on a new machine, I’d initialize it as a git repo, add my remote, fetch and checkout and I’d be done. I used the same setup as described here.

This is what my file structure looked like and if yours looks the same, you should definitely read on to save yourself some trouble!

/home/tkrizek
├── .bashrc
├── .config
├── .git        <-- DON'T track your dotfiles like this!
└── .vim

The Landmine

I regularly deal with many different projects tracked in git. There are countless way the git directory can get polluted by different build systems, caches, runtime environments and whatnot. Some of it you’ll see in git status, some of it you won’t, because it’s excluded in .gitignore.

These various artifacts can sometime cause unexpected failures or behaviour. That’s why I’ve learned to love a very powerful command: git clean -dfx. It recursively removes any untracked files, including those that are ignored with .gitignore to truly extinguish all those pesky files that might have sneaked in e.g. during a build.

I find the command so ubiquitously useful, I probably use it at least once a day. It’s encoded in my muscle memory as the go-to command when something seems off or I just want a fresh state of the repository. I typically never think twice about using it, since it only acts in the scope of the git repository. Unlike the rm -rf command, which I tend to double or triple check to avoid disastrous typos.

The Explosion

It was one of these days when I juggle three different tasks I want to work on, but all of a sudden, I just need to make a quick commit in the repository. I knew I didn’t have any ongoing work in that project and I just wanted to make sure the repository is in a pristine state and doesn’t contain any left-over files. My muscle memory fired up and I issued the fatal git clean -dfx command. The last thing I can remember is being annoyed that it’s taking more time than usual…

/home/tkrizek           <-- my *actual* working directory
├── .git
└── projects
    └── knot-resolver   <-- what I *think* is my working directory
        └── .git

$ git clean -dfx  # don't try this at $HOME

By the time the command started logging, it only took me a split-second to suddenly understand why the command is taking such a long time. The command started to recursively delete all files in my home directory that weren’t tracked in my dotfiles repository, which is, you know, ALMOST EVERYTHING!

Even though I’ve immediately canceled the execution, the damage was already done. I’ve lost my private keys, certificates and other important files. It also removed some seemingly inconsequential files like ~/.Xauthority, which suddenly rendered my system unable to launch new windows in my current X session. Luckily, I’ve had a few terminals open, so I still had a semi-working system I could try to fix.

The Aftermath

Disaster recovery is one of the few events that push me to “test” my backups. I use duplicity and systemd timers for a fully automated backup with no user interaction. The downside is that sometimes weeks pass by before I notice it’s broken for some reason. The upside is that I usually get regular backups I don’t have to pay any attention to.

This time, my backup just a few hours old. Great! Now, I just need my private keys and certificates to connect to the VPN and then SSH to the server that stores my backups… I suppose this is the reason you should really test your backups in a “clean” environment before you really need them. I had these credentials backed up locally on a different medium. After a bit of fiddling with duplicity, I was able to recover everything.

In hindsight, I could’ve expected something like this to happen eventually. It wasn’t the first time I was accidentally issuing git commands in $HOME. There were at least a few times when I was surprised why the project repository seems totally alien, until I finally looked into git log and realized I’m in my dotfiles repository. Most of them were benign like git status or git commit and I’ve never realized the dangers until today.

The Solution: Git Worktree

Git has a feature called worktrees. It allows you to check out the code in a separate directory. They seemed useful, but I just never found a place for them in my development workflow. However, my colleague suggested to use them as a solution to this problem. Specifically, you can separate the checked-out state, such the dotfiles (which can remain directly in $HOME/) and the .git directory which contains the entire repository and place it somewhere else.

When running the git command, it looks for $PWD/.git and if it doesn’t find it, it’ll refuse to operate. Let’s suppose I’d store the bare git repository in $HOME/.config/conf.git instead of $HOME/.git. The git command wouldn’t be usable in $HOME and nothing would have happened. Instead of deleting my precious files, I would’ve ended up with this error message:

$ git clean -dfx
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).

When you want to actually interact with the dotfiles repository, you instruct git which repository and worktree you’re using. This can be done with --git-dir and --work-tree options. Instead of calling git in $HOME, you have to use git --git-dir $HOME/.config/conf.git --work-tree $HOME instead. Everything else remains the same as you’re used to. Using this command is a bit unwieldy, but a simple alias can fix that.

The Proper Dotfiles Setup

The goal is make the regular git command benign in your home directory and use a special command to interact with your dotfiles instead. In order to do that, you must move the bare git repository from $HOME/.git somewhere else, e.g. $HOME/.config/conf.git:

/home/tkrizek       <-- worktree for ~/.config/conf.git
├── .bashrc
├── .config
│   └── conf.git    <-- bare git repository with dotfiles
└── .vim

If you already have the lousy dotfiles setup, you can simply move the directory. If you’re starting out from scratch, you can clone your dotfiles repository with --bare option. (If you have any issues, you may need to use a temporary directory like this instead.)

# existing installations
$ mv $HOME/.git $HOME/.config/conf.git

# setup on new machines
$ git clone --bare https://example.com/user/dotfiles "$HOME/.config/conf.git"
$ git --git-dir "$HOME/.config/conf.git" --work-tree "$HOME" checkout master

You double-check that you’re not able to sucessfully use the simple git command from $HOME. Afterwards, you may want to create an alias you can use for interacting with the dotfiles repository (unless you enjoy typing really long and tedious commands). I’ve put this in my ~/.bashrc:

# ~/.bashrc
alias confgit="git --git-dir $HOME/.config/conf.git --work-tree $HOME"

Then you can work with your dotfiles as usual and simply use a dedicated confgit command:

$ confgit status

That’s it! Now you can spend your time on something better than recovering files from backup (you have backups, right?) when you inevitably make a mistake.

What about submodules?

I use submodules to pull in different repositories, that contain e.g. vim plugins. The good news is that git supports that. I’ve had to remove the existing modules from the worktree, re-initialize them and then they worked as expected.

$ rm path/to/module
$ confgit submodule update --init

The bad news is that in order to use worktrees along with submodules, you need sufficiently modern git. The feature was introduced sometime in 2020. git 2.29.2 works fine. I use Arch btw.


Tags

Back to Index Feedback