One git identity per directory, declared as a table

  ·  3 min read

My main development machine is a Mac driven by a single nix-darwin flake, and git’s global config is generated by Home Manager1, which writes it to ~/.config/git/config. What I wanted from it was simple to state. Repositories for an employer or client should commit with that organisation’s email and everything else with my default, with the choice made by where the repository lives on disk and nothing asked of me at clone time, which rules out any step I’d have to remember.

Git has supported the mechanism for years through conditional includes, and what I wanted on top of it was for the mapping to live in one place, as data, alongside the rest of the machine’s identity.

The identity table #

The machine identity file, nix/local.nix, holds the default email and a list of contexts:

email = "[EMAIL]";
gitIdentities = [
  {
    condition = "gitdir:~/repositories/acme/";
    name = "Richard Claydon";
    email = "[EMAIL]";
  }
];

With the trailing slash, gitdir:~/repositories/acme/ matches any repository at any depth below that directory, so ~/repositories/acme/actions and anything nested further down both qualify.

In nix/modules/home.nix, Home Manager turns each row into a conditional include:

includes = map
  (id: {
    condition = id.condition;
    contents = {
      user = {
        inherit (id) name email;
      };
    };
  })
  (user.gitIdentities or [ ]);

Home Manager writes each contents block into an immutable file in the Nix store and emits an [includeIf …] section in the generated config that points at it, which after a rebuild looks like this:

$ git config --file ~/.config/git/config --list | grep -E '^user\.|^includeif'
user.email=[EMAIL]
user.name=Richard Claydon
includeif.gitdir:~/repositories/acme/.path=/nix/store/vsaj4aggjb741jggrwa8pqh76s6rxjrm-hm_gitconfig
$ cat /nix/store/vsaj4agg…-hm_gitconfig
[user]
	email = "[EMAIL]"
	name = "Richard Claydon"

The default sits in the generated file, and the context lives in a store path that nobody can edit by hand (including me, which is most of the appeal).

The ordering does the rest. Home Manager appends the includeIf section after the default, and later assignments win within a file, so a repository below a declared context gets that identity and everything else gets the default. Repository-local config still beats all of it, because git reads the repository’s .git/config last. A repository in context resolves like this:

$ git -C ~/repositories/acme/actions config --show-origin --get user.email
file:/nix/store/vsaj4agg…-hm_gitconfig    [EMAIL]

What a new context costs #

Adding another employer or client is one entry in gitIdentities (a gitdir: condition, a name, and an email) followed by one rebuild, after which Home Manager writes the store file and the include and the ordering does the rest. The whole setup lives as a few reviewable lines in local.nix, and git picks the right name without being asked.


  1. Home Manager, a Nix tool for managing a user environment declaratively, including dotfiles like git’s config. ↩︎