Dotfiles, that is, files whose names begin with a period, are used to store settings on all Unix-like operating systems.
This blog post shows you how to manage such files on Linux using Git. Of course, you can also adapt these instructions for macOS, Free BSD, and more with little effort.
Dotfiles
Unix has been a multiuser operating system from the beginning. Consequently, configuration files have always been created so that they could be saved for individual users. The concept of a home directory has thus existed from the start. What could be more obvious than to store your configuration files there?
Unwanted Concept: According to Rob Pike, one of the driving forces behind Unix in the 1970s and 1980s, dotfiles actually came about because of a programming error and sloppiness. Previously, no defaults for saving program settings existed at that time. The original post is unfortunately no longer available (since it was written on the discontinued Google Plus network), but a copy is available on Reddit that we have linked for you: https://links.git-compendium.info/rob-pike-on-dotfiles
Nevertheless, the concept spread quickly, and filenames became established with a convention composed of the program name and an appended suffix (rc, for “resource configuration"). Most Linux users have probably edited their .bashrc file at some point to adjust Bash.
In contrast to the binary Windows registry, configuration files on Linux are always in text format. Since you don’t need any special software to view and edit the files, such files can also be managed perfectly in Git.
On Windows, the concept of dotfiles doesn’t actually exist. However, more and more such files are present on Windows as well, for instance, when you use programs that have been ported from the Unix/Linux world for Windows. Nevertheless, we’ll explicitly refer to Linux in the following sections.
Dotfiles under Git Control
The longer you work on Linux, the larger the number of dotfiles with beloved shell aliases, editor settings, and more you’ll have, as experience shows. The obvious approach is to manage these files in a Git repository; then, a remote repository can serve as a backup and at the same time help quickly transfer files to a new Linux installation.
No sooner said than done! However, we’ll quickly run into a problem in a concrete implementation: Do we really want to create a Git repository in the home directory where 200 GB of data might also reside? Not really. We need some Git tricks to separate the dotfiles from the rest of the home directory.
An easy way would be to simply store the dotfiles in a subdirectory $HOME/dotfiles. Symbolic links then point from the home directory to the corresponding files in dotfiles/. Unfortunately, this approach doesn’t work well at all: If new dotfiles are added, they’ll need to be moved and linked manually to dotfiles/.
git-dir and work-tree
Fortunately, Git provides a much more elegant way: the --git-dir and --work-tree options:
- --git-dir specifies the folder where Git manages all internal information (i.e., the Git database), usually the .git However, in the following sections, we’ll use a different name, namely, .cfg. Using a different name from .git has an advantage in that git then won’t assume, for each command, that the command refers to a repository for the entire home directory. Otherwise, you would run the risk that a git command accidentally executed in the wrong directory would automatically refer to the dotfiles repository.
- --work-tree specifies the location of the files to be managed. Without this option, Git searches from the current folder up the file system hierarchy for a .git directory to determine the worktree.
The two options can be passed to all git commands. As a result, the command in question uses the default Git and worktree directories, regardless of which directory is currently active.
We’ll start the configuration process by creating the .cfg folder:
git init --bare $HOME/.cfg
Initialized empty Git repository in /home/ubuntu/.cfg/
We’ve now created a bare repository, which is a Git directory without the option to check out files. By specifying the two parameters, we can look at the status of our home directory:
git --git-dir=$HOME/.cfg/ --work-tree=$HOME status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
.bashrc
.cfg/
.profile
Documents/
...
nothing added to commit but untracked files present
(use "git add" to track)
Notice that Git has marked all files and folders in the home directory as untracked files, including the Git directory .cfg. For our first experiments, let’s now add the two dotfiles (.bashrc and .profile) to the Git index and modify a local setting so that unversioned files are no longer shown in the status output:
git --git-dir=$HOME/.cfg/ --work-tree=$HOME add .bashrc .profile
git --git-dir=$HOME/.cfg/ --work-tree=$HOME config \
--local status.showUntrackedFiles no
git --git-dir=$HOME/.cfg/ --work-tree=$HOME status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: .bashrc
new file: .profile
Untracked files not listed
(use -u option to show untracked files)
Since typing out these parameters is somewhat tedious and error-prone, we’ll add an alias entry to the shell configuration with the following line:
alias config='git --git-dir=$HOME/.cfg/ --work-tree=$HOME'
Now, you can use the new config command like the git command, with the difference that config always applies to the repository of dotfiles.
Since including the .cfg folder itself in the repository never makes sense, create a .gitignore file and add that folder with the following command:
echo ".cfg" >> ~/.gitignore
Even if you were to mistakenly run config add . in the home directory, the ~/.cfg folder wouldn’t be included in the staging area.
After the first commits, set up a remote repository (e.g., in GitLab) and store the changes there, with the following commands:
config remote add origin \
https://gitlab.com/git-compendium/dotfiles.git
config push
Beware of Git Error Messages: After a short while, the new config command feels natural, and we use it to add configuration files to our dotfile repository, commit there, and push-sync them to the remote repository. If an error occurs in the process, for example a merge conflict, Git will help as always with practical tips like the following:
use "git merge --abort" to abort the merge.
However, remember that you must not copy this command as usual. Rather, you must replace git with config. (We’ve had some painful experiences here!)
Setting Up Dotfiles on a New Computer
A dotfile repository is particularly useful for quickly applying personal settings to a newly installed computer. In addition to the editor settings mentioned earlier, this approach also applies to SSH configuration with settings for various server access. Settings for your email program or other software products can also be helpful.
In our experience, a useful step is to additionally include a README.md file in the repository. In this file, you can store the necessary installation steps and other comments. Now, when you start up a new Linux system, you can visit the web page of your Git repository, copy the relevant commands from there to clone the repository, and run the commands. After a few minutes, the Linux system is configured in the way you’ve specified.
For initialization, only the following commands, presented earlier, are required:
- alias config ... sets up the config command with the --git-dir and --work-tree options.
- echo provides a minimal .gitignore
- git clone downloads the repository. The --bare option ensures that the $HOME/.cfg directory will be used.
- config checkout copies the files from the bare repository to the home directory.
However, the config checkout command will fail dotfiles with the same names already exist in the home directory. You can try to solve this problem with a trick, but the solution isn’t very elegant:
config checkout
if [ "$?" -gt 0 ]
then
mkdir $HOME/.dotfiles.bup
config checkout 2>&1 | grep "^[[:space:]]\+" \
| xargs -I{} mv -v {} $HOME/.dotfiles.bup/{}
fi
The command evaluates the output of config checkout and responds to a possible error. Only in case of an error should $? (the return value of the last command) be greater than 0 in shell scripts. If so, the backup directory $HOME/.dotfiles.bup will be created, and the output of config checkout will be rewritten into a command.
Shell Commands
Now, let’s look at how the following command works:
config checkout 2>&1 | grep "^[[:space:]]\+" \
| xargs -I{} mv -v {} $HOME/.dotfiles.bup/{}
(If you aren’t interested in shell scripting, you can safely skip this section.)
If config checkout can’t be executed successfully (for example, because existing dotfiles are present), the following output occurs:
config checkout
error: The following untracked working tree files would be o...
.bashrc
Please move or remove them before you switch branches.
Aborting
We intentionally didn’t wrap the second line in the output; instead, we truncated it because the correct output of the characters will become important later. The first problem with the config checkout output on an error is that the error output doesn’t occur on the same channel as the normal output. Errors are output in the shell on the second channel, and we redirect the error to the first channel with the addition 2>&1. This step is necessary to feed the following grep command in the pipeline with the correct data. (Grep searches for regular expressions in a string.)
In our example, however, we only want to search for the tab character at the beginning of the line. This search can be defined by using the string ^[[:space:]], which in plain language means: Search for the beginning of the line (^) followed by a character of the whitespace class (which includes the tab). The output of grep is passed to the next command in the Unix pipeline (not to be confused with continuous integration [CI] pipelines). xargs takes the output of the previous program in a pipeline and executes new commands with it. In our case, the output is .bashrc, and in the replacement of xargs, you would use the following command:
mv -v .bashrc $HOME/.dotfiles.bup/.bashrc
This step will move existing dotfiles that are also in the repository into the backup folder. Then, config checkout is called again to copy the moved files from the repository, but now without errors.
Vim Configuration
With our dotfiles, one more step involves Git: For the editor Vim, possibly the best editor on the planet, you’ll need to install some extensions. The original setup originates from Jess Frazelle’s impressive GitHub repository, with the dotfiles repository in particular worth looking at:
The Vim extensions mentioned earlier are loaded as Git submodules into the .vim/bundle subdirectory. To load the submodules after the checkout, you’ll need to call the submodule command:
config submodule update --init
This step will clone all repositories listed in the .gitmodules file and store them in the correct subdirectory. To update the plugins, a shell script in the .vim folder can be used that starts the git pull call for each submodule with a one-liner:
git --git-dir=$HOME/.cfg/ --work-tree=$HOME submodule \
foreach git pull --recurse-submodules origin main
Note the foreach statement in the git submodule command. To be on the safe side, if the plugins themselves still use Git submodules, call git pull with the --recurse-submodules option.
Miscellaneous
The other instructions in the README.md file refer to installing packages or other configuration settings that don’t have anything to do with Git.
We hope that this fairly simple setup whets your appetite for setting up your own dotfiles repository. If you search the internet for “dotfiles repositories,” you’ll come across many more cool tricks.
Editor’s note: This post has been adapted from a section of the book Git: Project Management for Developers and DevOps Teams by Bernd Öggl and Michael Kofler. Bernd is an experienced system administrator and web developer. Since 2001 he has been creating websites for customers, implementing individual development projects, and passing on his knowledge at conferences and in publications. Michael studied telematics at Graz University of Technology and is one of the most successful German-language IT specialist authors. In addition to Linux, his areas of expertise include IT security, Python, Swift, Java, and the Raspberry Pi. He is a developer, advises companies, and works as a lecturer.
This blog post was originally published 3/2025.
Comments