Dotfiles Part 2: A Bare Repo Approach to storing Home Directory Config Files in GitJonathan Bowman Created: January 27, 2021 Updated: July 10, 2023 [Dev] #dotfiles #commandline #git
We can make life easier by using Git to store and version configuration files that reside in a system’s home directory (aka “dotfiles”). But how do we do so selectively and non-invasively, so that only the desired files are committed to version control? This article explores one such method: using a “bare” git repo to track the files.
If your needs are straightforward, I highly favor the method decribed in the previous article in this series. There I describe the simple approach of making the entire home directory a local git repo, and disabling tracking of (or just ignoring) all files by default. Then, one selectively adds the desired files, such as
That article has significant overlap with this one. You may welcome reviewing some of the introductory information there.
Just need a quick up-and-running approach? You might appreciate the easy article. Also feel free to browse the whole series.
There is an oddity to these approaches that do not use a bare repo: before you initialize a Git repo in a subdirectory, that subdirectory is considered part of the home repo. Try
mkdir newfolder and then
git status newfolder. Didn’t get the expected “fatal: not a git repository”? Yeah, exactly. While weird, for the most part I don’t find it too troubling. Once you
git init or
git clone in a subdirectory, it becomes a new repo in its own right.
But if this bothers you, or if you are considering a layered approach with multiple repos or branches, then the bare repo method detailed here may appeal.
🔗Advantages of the bare repo method
The bare repo approach is a little more complex than the aforementioned strategy. In a nutshell, the symptom of that complexity surfaces in the need to append
--git-dir=$HOME/.dotfiles --work-tree=$HOME to every git command. We’ll get to options for easing that pain, though, in a moment.
In spite of the complexity, I see two advantages to the bare repo:
- Logical separation. Git won’t think your entire home directory is a repo unless you explicitly tell it to. That is what
--git-dir=$HOME/.dotfiles --work-tree=$HOMEis for.
- That separation eases a layered approach. Let’s say you want to have a base layer for all your machines, then another layer (from another repo or branch) just for Linux machines with a window manager, then another layer for Windows machines, and another for Mac… You get the idea. This article will not dive into using layers or modules, but understanding the bare repo concept may help later.
Feel free to read the full article for detailed explanation and options. As a quick summary, the following commands offer an introduction. (The url for your Git repo should be assigned to or substituted for the
# If non-default branch in repo, or naming the initial branch before first push:
# If first-time push to empty repo, add and commit some files, then push
# Just adding ".profile" in the following example
# If instead pulling an already populated repo, simply:
# Deal with conflicting files, or run again with -f flag if you are OK with overwriting
As can be seen in the command summary above, there is a whole lot of
--git-dir=$HOME/.dotfiles --work-tree=$HOME going on. Let’s start by making a convenience function.
For Bash/Zsh/Ash, add the following to
~/.profile, depending on your shell:
And the same in Powershell (on Windows, add this to
You don’t have to call the function
dtf, of course. It could be
homedirectoryconfigurationsforversioncontrol as long as you are consistent. It simply adds those two commandline options to the git command and passes through any other commands and options.
Now, when managing configuration files, instead of using
git you would use
dtf (or whatever you opted to call it).
I should note a cross-platform git-centered approach, using git aliases. On any platform (Windows, Mac, or Linux or BSD) you could do something like:
Then, instead of using
dtf you would use
git dtf (or whatever you opted to call it). Choices! You decide.
🔗Clone the Git repository
The first step, whether restoring an existing set of files or populating a new repo, is to clone the remote Git repository:
🔗Choose the Git branch
For most people the default Git branch is called
master. For configuration files, I like to instead use the name
base for the files I share across all systems. This allows for additional (orphan) branches later such as
linux/wsl, for example.
To explicitly select or create the branch
The discerning eye will have noticed we could skip this step for existing, populated repos that already have the branch
base, by specifying the branch when cloning, using the
🔗Exclude untracked files from
git status for readability
I periodically like to see what files I have changed by using
dtf status but this will, by default, show every file in the home directory that is not tracked. This will be a mess in the terminal, and make it difficult to discern changes.
So, let’s tell Git not to share the status of untracked files. In other words, only files that have been explicitly added with
dtf add $FILENAME will be shown with
dtf status. To do so:
🔗Populating an empty repository
(If you are downloading files from an already-populated repository, skip this step and proceed to “Working with existing dotfiles”, below.)
If this is the first time you are pushing files into an empty repository, then you will want to add and commit some files now. For instance, if you want to add your VSCode configuration on your Macbook, then try something like:
Once at least one file has been added and committed to version control, the repo is ready to be pushed and have the upstream set:
From now on, with the upstream configured on this machine, all you need is
dtf push after committing new changes.
🔗Working with existing dotfiles
If you are setting up a new machine from an existing Git repo that already has your dotfiles, then all you need is:
This will place your tracked files in your home directory from the bare
.dotfiles repo. In some cases, you may have conflicts. For instance, if there is already a
.bashrc in your home directory, and the remote repo also has this file, then you should see something like this:
error: The following untracked working tree files would be overwritten by checkout:
Please move or remove them before you switch branches.
If you don’t see that error, then you are done with setup! If you do see it, then deal with the files (backing up, erasing, etc.) or run
dtf checkout -f to force overwriting.
🔗Setup and restore functions
So far, we have the one
dtf function, for convenience. I suggest making sure you have available two additional convenience functions to wrap up the above steps in a repeatable way:
dtfrestore (or whatever you would like to call them).
Here is a working example, for Bash, Zsh, or Ash:
The equivalent, in Powershell form:
$DOTFILES = "$HOME\.dotfiles"
dtfnew $REPO will set up a new repo ready to be populated and pushed to an empty remote repository.
dtfrestore $REPO will accept an already-populated remote repository URL, and pull the files into your home directory.
I suggest customizing the above functions as you like, then placing them in a Git repo or Github Gist or Gitlab Snippet. Assign $URL appropriately, then use something like:
The above works on Bash/Ash/Zsh, and even on Busybox-based distros. Feel free to try
For a Powershell example, try something like:
Set-ExecutionPolicy RemoteSigned -scope CurrentUser
iwr -useb $URL | iex
For Powershell, feel free to try
$URL = "https://raw.githubusercontent.com/bowmanjd/dotfile-scripts/main/bare.ps1"
Feel free to investigate my dotfile-scripts repository on Github. The above files are available there, as well as files related to other articles in this series.
🔗A flexible approach
While the first strategy favors simplicity, this bare repo approach offers flexibility. We will explore this flexibility in a future article that engages a modular approach to dotfiles. Meanwhile, I hope the methods discussed here help you compose tools that work well for you.
Back to top