Skip to content

jmarette/git-id

Repository files navigation

git-id

Manage Git identities and route them to directories — using Git's own conditional includes, nothing else.

If you juggle several Git accounts (personal, work, clients…) on one machine, git-id lets you keep no global identity at all and never run git config user.email after a clone again: every repository picks the right user.name / user.email (and signing settings) from the directory it lives in.

$ git id create work --name "Jane Doe" --email jane@work.example
$ git id use work ~/dev/work
$ git clone git@github.com:your-org/some-repo ~/dev/work/some-repo
$ cd ~/dev/work/some-repo && git commit ...   # commits as jane@work.example

The binary is named git-id, so Git picks it up as a native subcommand: git id use workgit-id use work.

How it works

git-id never rewrites your ~/.gitconfig. It owns exactly two things, both under ${XDG_CONFIG_HOME:-~/.config}/git-id/:

~/.gitconfig                        ← yours; gets ONE include line, once (git-id init)
└─ include.path = ~/.config/git-id/routes.gitconfig

~/.config/git-id/routes.gitconfig   ← owned & generated by git-id
└─ [includeIf "gitdir:/Users/you/dev/work/"]
       path = "/Users/you/.config/git-id/identities/work.gitconfig"

~/.config/git-id/identities/work.gitconfig   ← one fragment per identity
└─ [user] name / email / signingkey, [gpg] format,
   [commit] gpgsign, [core] sshCommand

A route on a directory applies to it and every repository below it. Routes are stored canonicalized (symlinks resolved) with a trailing slash — the exact form Git matches against — and sorted parent-before-child, so the deepest route always wins (Git gives precedence to the last value found).

Operations are atomic (temp file + rename), idempotent (re-running use on a routed directory replaces the route, never duplicates it), and the global config is backed up to a timestamped .bak-* file before any modification.

Install

Requires git ≥ 2.13. Works on macOS, Linux and Windows — paths are stored in the canonical, forward-slash form Git matches gitdir: patterns against on each platform, and every release is tested on all three.

With Homebrew (prebuilt binaries):

$ brew install jmarette/tap/git-id

With the standalone installer (no toolchain needed):

$ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/jmarette/git-id/releases/latest/download/git-id-installer.sh | sh

On Windows (PowerShell):

> irm https://github.com/jmarette/git-id/releases/latest/download/git-id-installer.ps1 | iex

Or from source:

$ cargo install --path .

Make sure ~/.cargo/bin is on your PATH (it usually is), then:

$ git id init

init is one-time and idempotent: it creates the config directory, links the routes file into your global git config, and offers to set user.useConfigOnly=true — a recommended guard that makes Git refuse to commit in any directory where no identity applies, instead of silently guessing one from your hostname.

Commands

Command What it does
git id init One-time setup (idempotent, backs up your gitconfig first).
git id create <name> Create an identity (--name, --email, --signing-key, --sign, --format, --ssh-key/--ssh-command; prompts for the basics if omitted).
git id list Identities and the directories routed to them (--paths for the dir → identity map).
git id show <name> Full detail of one identity, including the raw fragment.
git id edit <name> Update via flags, or open the fragment in $EDITOR when no flags are given. Hand-added keys survive.
git id delete <name> Delete the identity and purge its routes (asks for confirmation; --force to skip).
git id use <name> [dir] Route dir (default: current directory) and everything below it to an identity. Replaces any existing route for that directory.
git id unset [dir] Remove the route of a directory.
git id which [dir] Which identity applies here? Shows the matched route and what git actually resolves, and flags local overrides. Alias: current.
git id doctor Sanity-check the whole setup (include line, fragments, stale routes, …).
git id uninstall Undo git-id's setup (include line, useConfigOnly, config dir) before removing the binary.
git id completions [shell] Print shell completions; completions install writes them into place for every shell on PATH (or just [shell]). Supports bash, zsh, fish, nushell, elvish, powershell.
git id man Print the man page (roff). git id init already installs it, so man git-id and git id --help just work.

Every command has a detailed --help.

Signing and SSH keys

An identity can carry its signing and transport settings, so they follow the directory like the name and email do:

$ git id create work --name "Jane Doe" --email jane@work.example \
    --signing-key ABCDEF12 --sign --format ssh \
    --ssh-key ~/.ssh/id_work
  • --format <openpgp|ssh|x509> sets gpg.format (SSH signing needs git ≥ 2.34).
  • --ssh-key <path> is shorthand: git-id writes core.sshCommand = ssh -i <path> -o IdentitiesOnly=yes, so that exact key is used and an agent key can't shadow it. Use --ssh-command "<cmd>" instead to store a full command verbatim (custom port, proxy, …).

On edit, --no-format and --no-ssh remove those settings; --signing-key "" removes the key. Anything you add to a fragment by hand is preserved.

core.sshCommand only drives Git's own transport (clone, fetch, push). A bare ssh -T git@gitlab.com ignores Git config and always uses your default key, so to check a specific identity's key, point ssh at it explicitly:

$ ssh -i ~/.ssh/id_work -o IdentitiesOnly=yes -T git@gitlab.com

That per-identity key is what lets two accounts on the same host coexist. A given SSH key can only belong to one account (GitHub and GitLab reject a key already registered elsewhere), so give each account its own key and route them by directory:

$ git id create work  --email you@work.example  --ssh-key ~/.ssh/id_work
$ git id create perso --email you@perso.example --ssh-key ~/.ssh/id_perso
$ git id use work  ~/dev/work
$ git id use perso ~/dev/perso

A plain git push then uses the right account from wherever the repo lives.

Scripting

list, show and which take --json:

$ git id which --json
{
  "identity": "work",
  "gitdir": "/Users/you/dev/work/some-repo/",
  "route": "/Users/you/dev/work/",
  "name": "Jane Doe",
  "email": "jane@work.example",
  "in_repo": true,
  "effective": { "name": "Jane Doe", "email": "jane@work.example", "origin": "…/identities/work.gitconfig" },
  "mismatch": false
}

which exits non-zero when no identity applies, so it works as a guard in scripts and hooks.

Shell completions

git id init already sets these up for every shell it finds on your PATH, so usually there is nothing to do. To (re)install them yourself:

$ git-id completions install            # every shell found on PATH
$ git-id completions install zsh        # …or just one, named explicitly
$ git-id completions install --current  # …or just the shell you're in
$ git-id completions install --activate # …and wire it into your startup file

It writes each completion script where the shell autoloads it (skipping any already up to date), so usually you just restart your shell. When a shell has no writable autoload location — a system zsh whose $fpath is read-only, an older nushell, or elvish/powershell — it falls back to a private location and prints the one line to add to your startup file. Pass --activate to have git-id add that line for you (idempotent, and removed by git id uninstall); without it, git-id never edits your rc files. Supported shells: bash, zsh, fish, nushell, elvish, powershell. git id doctor reports, per shell, whether its completion file is installed.

Note (nushell): nushell completes the exact command name, so use git-id <Tab>. git-id also ships git id <Tab> (the spaced form) externs for nushell; in zsh, git id <Tab> works through git's own completion dispatch.

Manual. git-id completions <shell> prints the script to stdout, so you can place it yourself:

# bash
$ git-id completions bash > ~/.local/share/bash-completion/completions/git-id

# zsh — then add `fpath+=(~/.zfunc)` before compinit in ~/.zshrc
$ git-id completions zsh > ~/.zfunc/_git-id

# fish
$ git-id completions fish > ~/.config/fish/completions/git-id.fish

# nushell — then add `source ~/.config/nushell/completions/git-id.nu` to config.nu
$ git-id completions nushell | save -f ~/.config/nushell/completions/git-id.nu

Caveats worth knowing

  • Linked worktrees and submodules: Git matches gitdir: against the real .git directory, which lives under the main repository. A worktree placed under a routed directory whose main repo lives elsewhere keeps the main repo's identity. git id which detects and explains this.
  • Bare repositories route like any other: because routes are stored with a trailing slash, a route matches a bare repo whose git directory is the routed directory itself, as well as every repository below it.
  • Symlinked paths are stored resolved (e.g. /tmp/... becomes /private/tmp/... on macOS) because that is what Git matches against.
  • git id --help opens the git-id man page; git id init installs it (and git-id man prints it). Windows has no man pages — use git-id --help or git id <command> --help there.
  • Completions cover git-id … invocations; completing through git id … would need a git-specific completion script (future work).

Uninstall

First let git-id undo everything it set up — the include.path line in your global git config, the user.useConfigOnly guard, and the config directory:

$ git id uninstall

Then remove the binary, depending on how you installed it:

  • Homebrew: brew uninstall git-id
  • cargo: cargo uninstall git-id
  • standalone installer: delete git-id from ~/.cargo/bin (git-id.exe on Windows)

Development

$ cargo test          # unit + end-to-end tests (run in throwaway HOMEs)
$ cargo clippy --all-targets -- -D warnings
$ cargo fmt --check

The integration tests never touch your real configuration: each scenario runs the binary and a real git inside its own temporary HOME.

Notable changes are tracked in CHANGELOG.md.

Future work: routing by remote URL (hasconfig:remote.*.url), doctor --fix.

Contributing

Contributions are welcome.

  1. For anything beyond a small fix, open an issue first so we can agree on the approach before you spend time on it. Small fixes can go straight to a pull request.
  2. Fork, branch off master, and open your PR against master. CI must be green — it runs rustfmt, clippy (warnings denied) and the full test suite on every pull request.
  3. New features should come with tests and an entry under ## [Unreleased] in CHANGELOG.md.

Commit messages follow Conventional Commits (feat:, fix:, docs:, refactor:, test:, chore:). Pull requests are merged with a merge commit (not squashed), so every commit is preserved in master — keep each commit focused and give it a Conventional Commits message.

Contributions are dual-licensed under MIT OR Apache-2.0; see License.

Releasing

Releases are automated by cargo-dist: pushing a v* tag builds the binaries, publishes the GitHub Release, and updates the Homebrew formula in the tap. To cut version X.Y.Z:

  1. Bump version in Cargo.toml and run cargo build so Cargo.lock matches — the tag and the package version must be equal, or the release fails.

  2. Rename the in-progress CHANGELOG.md section to ## [X.Y.Z] (no date — it becomes the Release title), add its compare link, and open a fresh section for the next cycle.

  3. Commit, then tag and push:

    $ git tag vX.Y.Z
    $ git push origin master vX.Y.Z

Never move or delete a published tag — the release assets and the Homebrew formula point at it, so re-tagging breaks existing installs; ship a new version instead. (The Release title can be changed afterwards without touching the tag.) Release targets, installers and the tap live in dist-workspace.toml; re-run dist init after changing it, and dist plan to preview a release.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages