gitignore and symlinks: a lightweight alternative to git submodules


Ok so I don’t like using git submodules. The workflow for that is:

and all is dandy for a while. But then you change the submodule and commit, and you get:

has-submodules $ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   submodule (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

Ok, nothing too scary here, you can, as they say, add the submodule and commit the new commits.

But this is generally a huge distraction, when working on the top level project.

It’s usually a better idea to have the submodule repository completely outside, and link it via the means of the language you are working in: for example with python you can install an “editable” directory with pip install -e.

But sometimes you need a repository to be in a subdirectory of another. Perhaps it has to be at that absolute path on your filesystem, and a parent directory is also tracked in git.

My lightweight solution is first to add the nested repository to .gitignore. The you can clone the nested repository somewhere else, and symlink it in.

Case study: splitting fish functions from fish abbrs

The motivation for this came from my attempt to clean up my fish config. I used to have all my config in https://git.sr.ht/~razzi/fish-functions, however since my abbrs change much more frequently than my functions, they were creating a lot of noise in that repository.

I considered using submodules or subtrees or other built-in git tools, I even considered moving the git root (.git directory) to the functions/ path. However I liked having the README outside of the functions directory, and some of my functions have accompanying completions/ scripts.

But I prefer the simple solution I came up with.

First, I decided that the functions would stay, and the abbrs would be moved out. So I moved the conf.d directory with the abbrs to another repository, in this case my .dotfiles:

$ cd ~/.config/fish
fish $ mv conf.d ~/.dotfiles/fish_conf.d
fish $ cd ~/.dotfiles
.dotfiles $ git add fish_conf.d
.dotfiles $ git commit  # etc

Then I added the path they were at, conf.d, to my .gitignore:

fish $ cat .gitignore

And symlinked them back in:

.dotfiles $ symlink fish_config.d ~/.config/fish/conf.d

Now I can update the abbrs in my .dotfiles and they will be immediately available in my fish config, no build step.

The only trickiness is remembering to symlink them in on a fresh clone, which I document in my dotfiles README.

Closing thoughts

Submodules have the advantage that the path to the submodule would be tracked in .gitmodules, so you don’t have to remember where to find the subdirectory to link in, but working with submodules requires git submodule commands like git submodule update --init and gives the noise of untracked commits in submodules.

I’m happy with my symlink and gitignore solution in this case, especially since it allows me to just use a part of my .dotfiles as a part of my fish config. For a submodule I’d have to make my fish abbrs into a separate repository.

But perhaps submodules could be used to get the initial repositories set up, then I could update the git index to assume they are unchanged. I might try doing this to track my vim plugins, which are themselves git repositories, and live within my ~/.vim repository.