Keeping ‘live‘ dotfiles in a Git repo
Dotfiles are hidden files that usually live in a user's home directory (or subdirectories of the home directory) to store per-user settings.
Some people keep their dotfiles in a repository which, in theory, makes it easy to share the same set of dotfiles between multiple machines. Doing this usually involves some way of syncing (specific files in) a home directory with a repository or symlinking from a home directory to a repository.
For a long time I used rsync
to keep the dotfiles in my home directory in sync
with a Git repository. Somehow I always ended up with some Franken-version of my
(old and somewhat outdated) dotfiles because I forgot to either sync or
push from one of my machines.
The obvious solution might be to turn the home directory itself into a repository. Unfortunately this has has some annoying side effects.
Git does all of its housekeeping in a (hidden) directory called .git/
in the
root of a repository. Running git status
in the root or any of the
subdirectories of the repository will find this directory and return the status
of the working tree.
For this reason, having a .git/
directory in your home directory, turns it
into a repository root. This is not in and of itself annoying, unless, like
me[1], you[2] use some magic to show some relevant Git information in your
prompt. In that case, every subdirectory of your home directory would report for
example the current branch your home directory was on.
A ‘live’ solution
There is a way to turn a home directory into a ‘live‘ repository. Git can be configured to do its housekeeping in a directory that you can name yourself.
Let's use .dotfiles/
.
~ $ git --git-dir="$HOME/.dotfiles" --work-tree="$HOME" init
This will create a (git-dir) directory .dotfiles/
in the home directory where
Git will do its housekeeping instead of .git/
. It wil also configure the
repository and set the working tree to be the home directory itself.
Because not all files in a home directory need to be tracked, also add the following.
~ $ git --git-dir "$HOME/.dotfiles" config status.showUntrackedFiles no
With this setting, Git will only show files that are being tracked. This is a
good setting to have for a repository in which we will only be tracking a
limited number of files. Otherwise commands such as git status
would be
extremely noisy.
The repository is now ready and can be used almost like any other repository. You can add a remote, add files to track, commit and push to a remote. There is a drawback however (there always is). For Git to be able to work with a non-standard git-dir you need to tell Git where to find it for every command.
For example, instead of the following command (that will not work in your home directory—try it).
~ $ git status
fatal: not a git repository (or any of the parent directories): .git
You need to explicitly tell Git about the git-dir.
~ $ git --git-dir="$HOME/.dotfiles" status
However, adding --git-dir "$HOME/.dotfiles"
to every Git command is a lot of
typing, so let's have a look at two ways around this.
1. Run all Git commands from inside the git-dir
The easiest (and possibly safest) way is to run all Git commands from within the
git-dir (~/.dotfiles/
) itself.
~ $ cd "$HOME/.dotfiles"
.dotfiles [main] $ git status
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)
Notice how the command prompt in the example above picks up that we are on the main branch.
2. Setting a GIT_DIR
environment variable
Another way is to (temporarily) export a GIT_DIR
environment variable. This
makes the home directory behave like any other working tree.
~ $ export GIT_DIR="$HOME/.dotfiles"
~ [main] $ git status
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)
Again, notice how the command prompt in the example picks up that we are on the main branch.
It is important to unset the GIT_DIR
again when you're done. If you do not,
all repositories that live in subdirectories of the home directory will be
unusable because Git will use ~/.dotfiles/
as the git-dir. A GIT_DIR
environment variable beats the presence of a .git/
directory.
~ [main] $ unset GIT_DIR
~ $
Everything is back to normal now.
Setting things up on a new computer
Cloning a repository into a directory that already contains files is not possible. So ‘cloning‘ into a home directory that undoubtedly already contains files needs to be done slightly differently.
On a new machine, start by repeating the steps for setting up the repository.
~ $ git --git-dir="$HOME/.dotfiles" --work-tree="$HOME" init
~ $ export GIT_DIR="$HOME/.dotfiles"
~ [main] $ git config status.showUntrackedFiles no
Then add your remote and fetch to download objects and refs from the remote.
~ [main] $ git remote add origin <url>
~ [main] $ git fetch
# Git fetching from origin
In an empty working tree, pulling or merging would work fine. But since this is
a home directory, chances are there are already a lot of files like .zshrc
or
.bashrc
that would be overwritten by a checkout. Trying this will make Git
abort and complain.
What does work however is to reset the state of our local repository to the
state of origin/main
.
Warning: The next command will overwrite local files with files from the remote.
~ [main] $ git reset --hard origin/main
And that is all there is to it. From now on it is possible to add or remove files like with any other repository. Logging out and back in again should have you end up in a familiar environment.
Final remarks
To make sure that some sensitive directories never end up on a remote, I have added a few of them to my global Git ignore file[3].
Git does not allow adding .git/
directories and will silently ignore you when
you try to add it anyway or loudly complain if you try to add a file inside it.
I added dotfiles/
to have a similar effect.
I also added .ssh/
. There are only things in there that I would never want to
share across machines.
And finally .bash_history
just to make sure that is never accidentally added.
I use Git's own
git-prompt.sh
↩︎Git's default:
~/.config/git/ignore
↩︎