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, 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 first 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 .bashrc
, .zshenv
, .vimrc
, etc.
That article has significant overlap with this one. You may welcome reviewing some of the introductory information there.
There is an oddity to that approach: 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 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=$HOME
is 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.
Summary commands
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 $REPO
variable.)
git clone --bare $REPO $HOME/.dotfiles
git --git-dir=$HOME/.dotfiles/ config --local status.showUntrackedFiles no
# If non-default branch in repo, or naming the initial branch before first push:
git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME switch -c base
# If first-time push to empty repo, add and commit some files, then push
# Just adding ".profile" in the following example
git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME add .profile
git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME commit -m "initial commit"
git --git-dir=$HOME/.dotfiles/ push -u origin base
# If instead pulling an already populated repo, simply:
dtf checkout
# Deal with conflicting files, or run again with -f flag if you are OK with overwriting
Convenience functions
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 ~/.bashrc
or ~/.zshrc
or ~/.profile
, depending on your shell:
dtf () {
git --git-dir="$HOME/.dotfiles" --work-tree="$HOME" "$@"
}
And the same in Powershell (on Windows, add this to Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
):
function dtf {
git --git-dir="$HOME\.dotfiles" --work-tree="$HOME" @Args
}
You don't have to call the function dtf
, of course. It could be dtfgit
or dotfiles
or 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:
git config --global alias.dtf '!git --git-dir=$HOME/.dotfiles --work-tree=$HOME'
Then, instead of using git
or 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:
git clone --bare $REPO $HOME/.dotfiles
Choose the Git branch
For most people the default Git branch is called main
or 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 windows
and linux/ui
and linux/wsl
, for example.
To explicitly select or create the branch base
:
dtf switch -c base
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 -b
option:
git clone -b base --bare $REPO $HOME/.dotfiles
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:
dtf config --local status.showUntrackedFiles no
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:
dtf add ~/Library/Application Support/Code/User/settings
dtf commit -m "Initial commit of VSCode config"
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:
dtf push -u origin base
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:
dtf checkout
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:
.bashrc
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: dtfnew
and dtfrestore
(or whatever you would like to call them).
Here is a working example, for Bash, Zsh, or Ash:
DOTFILES="$HOME/.dotfiles"
dtf () {
git --git-dir="$DOTFILES" --work-tree="$HOME" "$@"
}
dtfnew () {
git clone --bare $1 $DOTFILES
dtf config --local status.showUntrackedFiles no
dtf switch -c base
echo "Please add and commit additional files"
echo "using 'dtf add' and 'dtf commit', then run"
echo "dtf push -u origin base"
}
dtfrestore () {
git clone -b base --bare $1 $DOTFILES
dtf config --local status.showUntrackedFiles no
dtf checkout || echo -e 'Deal with conflicting files, then run (possibly with -f flag if you are OK with overwriting)\ndtf checkout'
}
The equivalent, in Powershell form:
$DOTFILES = "$HOME\.dotfiles"
function dtf {
git --git-dir="$DOTFILES" --work-tree="$HOME" @Args
}
function dtfnew {
Param ([string]$repo)
git clone --bare $repo $DOTFILES
dtf config --local status.showUntrackedFiles no
dtf switch -c base
echo "Please add and commit additional files"
echo "using 'dtf add' and 'dtf commit', then run"
echo "dtf push -u origin base"
}
function dtfrestore {
Param ([string]$repo)
git clone -b base --bare $repo $DOTFILES
dtf config --local status.showUntrackedFiles no
dtf checkout
if ($LASTEXITCODE) {
echo "Deal with conflicting files, then run (possibly with -f flag if you are OK with overwriting)"
echo "dtf checkout"
}
}
The function dtfnew $REPO
will set up a new repo ready to be populated and pushed to an empty remote repository.
The function 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:
OUT="$(mktemp)"; wget -q -O - $URL > $OUT; . $OUT
The above works on Bash/Ash/Zsh, and even on Busybox-based distros. Feel free to try URL="https://raw.githubusercontent.com/bowmanjd/dotfile-scripts/main/bare.sh"
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.
Top comments (2)
Hi Jonathan, great post.
A simpler approach I use is
This allows things to be a bit more readable and has the benefit of allowing other programs to get the same context if needed ( vscode etc. )
Don't we have to do this?