Dotfiles Part 3: A Modular Approach Using Multiple Git Repositories

Jonathan Bowman Created: February 04, 2021 Updated: July 10, 2023 [Dev] #dotfiles #commandline #git ~/.*

In this article, I offer an approach for managing dotfiles in a modular way. I find a modular approach important because only some config files are useful in all contexts, while others are unique to a specific environment. For instance, my text editor configuration (.vimrc, in my case) is used on my Windows laptop, Linux laptop, FreeBSD server, and even my phone. On the other hand, files for configuring a Linux graphical environment, a developerโ€™s Macbook, Windows Subsystem for Linux (WSL), or Windows Powershell, may not make sense to clutter or confuse environments to which they do not apply.

It would be nice to use multiple Git repositories, or multiple branches of one Git repository, in order to customize various environments.

In a previous article, I outlined a simple approach to storing dotfiles that makes the entire home directory a git repository.

Letโ€™s build on that approach, exploring the possibility of using multiple repositories in a modular fashion.

Feel free to browse the whole series if interested in exploring a variety of approaches.

๐Ÿ”—Summary steps

Feel free to read the full article below for detailed explanation and options. As a quick summary, the following steps should get you started with three โ€œmodulesโ€: base, personal, and work:

  1. Set up the base repo
  2. Create two additional directories, such as ~/.config/custom/personal and ~/.config/custom/work and initialize a git repo in each, similar to the base instructions but using those directories instead of home.
  3. Modularize your config files (see below) so that the main config includes related files in subdirectories of ~/.config/custom
  4. Manage the files: cd into each module directory and add, commit, push and pull as necessary

๐Ÿ”—Repository setup

You can continue to use the convenience functions from the first article (dtfnew and dtfrestore), just make sure you position yourself in the appropriate directory first, and specify the correct repo for each module. For example:

cd ~
dtfrestore $BASEREPO
mkdir ~/.config/custom
git clone $WORKREPO ~/.config/custom/work
git clone $LAPTOPREPO ~/.config/custom/laptop

In the above, we first create a base repository in the home directory. This is the repo in which are stored the main files like .bashrc, .profile, etc. As noted below, these files should be configured to load other files in other directories.

Then we clone the remote repositories to the given directories after creating them.

You can browse the companion Github repo for the basic functions used above, in both a Unix shell version and a Powershell version

๐Ÿ”—One directory per โ€œmoduleโ€ with a common parent directory

Choose a parent directory in which you will place each module directory. I use the term โ€œmoduleโ€ here to refer to a repository that adds additional config files. I do not mean to refer to git submodules. Although that introduces possibilities worth exploring another dayโ€ฆ

I use ~/.config/custom as the parent directory, but you can use any location that serves you well.

Underneath that parent directory, create a directory for each โ€œmodule.โ€ The end result may look something like this:

~/.config/custom/
โ”œโ”€โ”€ base
โ”œโ”€โ”€ macbook
โ”œโ”€โ”€ personal
โ”œโ”€โ”€ server
โ”œโ”€โ”€ work
โ””โ”€โ”€ wsl

Perhaps you donโ€™t need that many, but you get the idea.

Within each directory, you can place relevant config files. I suggest some advance planning to determine naming scheme, as follows.

๐Ÿ”—Modularize your config files as needed

The first step to a layered or modular approach is to think in modules. Rather than imagining a single config file that changes per system, use that file to load other config files if they are present.

Here are some ideas, specific to various tools:

๐Ÿ”—Unix shell configs

Shell configurations like .bashrc or .zsheenv or .profile can source files from other directories.

For instance, in .bashrc we might place something like the following:

for file in "$(find $HOME/.config/dotfiles -name 'bashrc')"; do 
    . $file
done

This would load any or all of the following files if they exist:

~/.config/dotfiles/bashrc
~/.config/dotfiles/work/bashrc
~/.config/dotfiles/personal/bashrc
~/.config/dotfiles/wsl/bashrc
~/.config/dotfiles/linuxui/bashrc

๐Ÿ”—SSH configs

In .ssh/config, the Include keyword can be used like so

Include *.ssh/config

For example, this will include any of the following files, if they are available:

~/.ssh/personal.ssh/config
~/.ssh/work.ssh/config
~/.ssh/datacenter.ssh/config

Or a different Include line to find files in other directories:

Include ~/.config/dotfiles/*/*.ssh

This will include any files ending in .ssh (such as config.ssh) in any subdirectory of ~/.config/dotfiles.

And so on. Name the directories in ways that suit you.

๐Ÿ”—Powershell

In Windows, the Powershell profile in ~\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 is automatically loaded. Within that file, you could loop through files in a directory of your choice and load them:

Get-ChildItem -Recurse "$HOME\.config\dotfiles\*.ps1" | ForEach-Object {& $_.FullName}

For example, this will include the following files, if they are available:

~\.config\dotfiles\dc1\profile.ps1
~\.config\dotfiles\work\profile.ps1
~\.config\dotfiles\personal\extra.ps1

๐Ÿ”—Vim

With Vim or Neovim, you can place any vim script ending with .vim in ~/.vim/plugin of subdirectory thereof, and it will autoload. In Vim 8 and above, a more modern location is something like ~/.vim/pack/work/start or ~/.vim/pack/personal/start and so on.

You can also load multiple files from a directory of your choice, using the runtime command. For instance, add something like the following to ~/.vimrc:

runtime! ../.config/dotfiles/**/*.vim

This will load any and all files ending with .vim in ~/.config/dotfiles or any subdirectory thereof.

๐Ÿ”—Maintaining config files in module directories

The above are examples to demonstrate a theme: create base config files that simply load an arbitrary number of other files, in a directory of your choosing. Once this is done, individual files as well as directories of files can be added to Git repo(s).

As noted, this requires thinking through directory and file structure carefully, because files are tracked in entirely separate git directories. But that careful organization pays off with simplicity: cd into the module directory, then use git as you like, no extra command line options needed. For instance, imagine that we have two modules: base and personal, with base being our main repo in $HOME and personal having an additional person Vim config in ~/.config/custom/personal/personal.vim. Initiating the tracking could look something like this:

cd ~
git add ~/.vimrc
git commit -m "New Vim config"
git push
cd ~/.config/custom/personal
git add personal.vim
git commit -m "Addition Vim config for personal laptop"
git push

๐Ÿ”—Other hints and recipes?

I hope this offers you some ideas and inspiration for your own configurations. Please feel free to send me feedback with suggestions and experiences.

View my entire dotfiles series for more exploration of the topic.

Back to top