A minimal POSIX shell tool for managing multiple Git identities via git include — no config file format changes, no wrappers around git commit.
gitid stores identity snippets as <name>.gitconfig files in GITID_DIR (default: ~/.config/git/). Each file contains a [user] block:
# ~/.config/git/work.gitconfig
[user]
name = Thomas Hughes
email = thomas@work.example.comApplying an identity writes an include.path entry into the repo's local .git/config, so Git resolves the identity from the snippet file. Global [includeIf] rules (keyed on remote URL patterns) can activate identities automatically without any per-repo gitid <name> call.
Clone directly:
git clone https://github.com/hgto/gitid.git ~/tools/gitid
ln -s ~/tools/gitid/gitid /usr/local/bin/gitidAs a git submodule (pinned to a commit, shared across your dotfiles):
# inside your dotfiles repo
git submodule add https://github.com/hgto/gitid.git tools/gitid
ln -s "$PWD/tools/gitid/gitid" /usr/local/bin/gitidSet GITID_DIR to the directory holding your identity snippet files (default: ${XDG_CONFIG_HOME:-$HOME/.config}/git).
Each identity is a file named <name>.gitconfig containing at minimum a [user] block:
[user]
name = Jane Dev
email = jane@example.com
signingkey = ABC123 # optionalYou can also add [github] or other sections — gitid includes the whole file.
gitid work
# gitid: work appliedWrites include.path = ~/.config/git/work.gitconfig into .git/config. Running it again on the same identity is idempotent; switching identities replaces the previous include.
gitid show
# name: Thomas Hughes
# email: thomas@work.example.com
# active: workReports the resolved user.name, user.email, and which identity file is active in the local config. Warns if an inline [user] block is shadowing the include.
gitid rules
# ▸ *github.com[:/]work/** -> work
# *github.com[:/]oss/** -> ossReads [includeIf] blocks from your global config and prints each rule with its mapped identity. A ▸ prefix marks rules that match the current repo's remote URLs.
gitid check
# gitid: ok (thomas@work.example.com)Compares the effective identity against what the matching [includeIf] rule expects. Exits non-zero on mismatch and suggests the fix:
gitid: identity mismatch: effective=personal@example.com expected=thomas@work.example.com
fix: gitid work
By default, when no identity is set and no [includeIf] rule matches, Git silently invents
you@hostname and commits it. gitid guard install prevents that:
gitid guard install
# gitid: guard installed (useConfigOnly=true, hooksPath=~/.config/git/hooks)It does two things:
- Sets
user.useConfigOnly=trueglobally — Git then refuses to auto-detect an identity and aborts any commit whereuser.name/user.emailcan't be resolved from config. This floor holds even undergit commit --no-verify. - Installs a dispatcher (via
core.hooksPath) covering the standard client-side hooks; onpre-commitit also runs the enforcement check below. The dispatcher chains to any previous global hooks dir and each repo's own.git/hooks, so nothing you already rely on is shadowed.
gitid guard status # show useConfigOnly, hooksPath ownership, and the chain target
gitid guard uninstall # restore the previous hooksPath; add --all to also drop useConfigOnlyThe floor guarantees some real identity; enforcement guarantees the right one. Mark an identity enforced, and any commit whose remote matches a rule mapping to it must use it — a stale or wrong identity is rejected:
gitid enforce work # writes gitid.enforce = true into work.gitconfig
gitid unenforce workWith guard install active, committing in a repo whose remote matches an enforced rule while the
effective identity differs fails:
gitid: identity mismatch (enforced): effective=you@laptop.local expected=jane@work.example.com
fix: gitid work
Enforced rules are flagged in gitid rules and gitid show.
Notes:
git commit --no-verifybypasses enforcement (the floor still applies), and a repo that sets its own localcore.hooksPathopts out of the global hook.- The dispatcher chains the standard client-side hooks; the specialized
fsmonitor-watchmanand Perforce (p4-*) hooks are not wrapped, so a prior global one of those keeps running only if you invoke it yourself. - Because the dispatcher chains each repo's own
.git/hooks, installing the guard re-enables repo-local hooks. If you had set a globalcore.hooksPathspecifically to disable untrusted repo-local hooks, note that gitid's chaining runs them again.
Dry-run (default — no writes):
gitid migrate ~/src
# would-clean ~/src/project-a ( user.email user.name )
# clean ~/src/project-bApply:
gitid migrate --apply ~/src
# cleaned ~/src/project-a ( user.email user.name )Recursively finds .git directories under the given path (. if omitted) and removes inline user.* keys from each repo's local config. Use this to clean up repos that had identities set directly rather than via includes.
Dry-run:
gitid migrate-global
# would-clean /home/jane/.config/git/config ( user.email user.name )Apply:
gitid migrate-global --apply
# cleaned /home/jane/.config/git/config ( user.email user.name )Same as migrate but targets the global config file (and any files referenced by unconditional include.path entries in it). [includeIf] entries and referenced snippet files are left untouched.
# Bash — add to ~/.bashrc:
eval "$(gitid completion bash)"
# Zsh — add to ~/.zshrc:
eval "$(gitid completion zsh)"Identity is per-repo and shared across all of a repo's worktrees. This is intentional: gitid <name> writes include.path via git config --local, which edits the repo's shared .git/config (not the worktree-local config file). Every worktree of the same repo sees the same identity.
MIT — see LICENSE.