Skip to content

Instantly share code, notes, and snippets.

@bgauduch
Last active September 9, 2026 16:19
Show Gist options
  • Select an option

  • Save bgauduch/06a8c4ec2fec8fef6354afe94358c89e to your computer and use it in GitHub Desktop.

Select an option

Save bgauduch/06a8c4ec2fec8fef6354afe94358c89e to your computer and use it in GitHub Desktop.
Multiple git identities on one machine: config per folder and per provider (personal vs work). Every command verified.

Multiple git identities on one machine

One machine, several git identities (personal vs work, GitHub vs GitLab, two accounts on one provider). Each repository picks its own name, email and credentials. In a hurry: Appendix B, once per rule.

A wrong identity does not fail: git commits and pushes under the wrong author. Part 1 ends with the check that catches it.

Requirements

Feature Minimum git
user.useConfigOnly 2.8
includeIf "gitdir:" / gitdir/i: (identity per folder) 2.13
includeIf "onbranch:" 2.23
SSH commit signing (gpg.format = ssh) 2.34
includeIf "hasconfig:remote.*.url:" (identity per remote) 2.36

Every command and pattern below was executed in 2026-09 on git 2.55 (macOS) and git 2.47 (Debian 13, container). Conditions and their minimum versions: conditional includes.

Contents


Part 1. Setup

Five steps, all through git config.

ℹ️ Do not edit the config files by hand. A syntax error in ~/.gitconfig stops every git command (fatal: bad config line N); a wrong value matches nothing and says nothing. git config validates the syntax, Step 5 checks the values. The files, for reading: Appendix A.

Step 1. Pick the rule: folder or remote

gitdir: (per folder) hasconfig:remote.*.url: (per remote)
The identity follows where the repo is cloned who hosts the repo
Does not match a clone outside the folder (/tmp) a repo without remote (git init)

Both can be combined (2.4).

Step 2. Authenticate: provider CLI, or one ssh key per identity

Authentication (which account accepts the push) and authorship (which name goes in the commit) are two independent layers (2.5). Default: 2a. 2b for servers, CI runners, hosts without a CLI, and commit signing.

2a. https, provider CLI

Login: gh auth login, glab auth login. The CLI then acts as git credential helper (gh auth setup-git, glab auth git-credential). Providers' overview: GitHub.

gh auth login
gh auth setup-git             # helper for github.com

glab auth login
git config --global "credential.https://gitlab.com.helper" '!glab auth git-credential'
glab auth status

gh auth setup-git writes an empty helper = before its own: an empty value resets the helper list for that host (gitcredentials).

Token lifetime, refresh and revocation: GitHub, GitLab. Scripts and CI: a GitHub fine-grained personal access token or a GitLab personal access token, through GH_TOKEN / GITLAB_TOKEN. Other forges: tea (Gitea), git-credential-oauth (Forgejo, Codeberg, self-hosted; Forgejo OAuth2).

2b. ssh, one key per identity

ssh-keygen -t ed25519 -C "you@personal.example"   -f ~/.ssh/github_personal
ssh-keygen -t ed25519 -C "you@work.example"       -f ~/.ssh/gitlab_work

Register each *.pub on the matching account, then one Host per key in ~/.ssh/config:

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_personal
    IdentitiesOnly yes
    AddKeysToAgent yes

Host gitlab.com
    HostName gitlab.com
    User git
    IdentityFile ~/.ssh/gitlab_work
    IdentitiesOnly yes
    AddKeysToAgent yes

IdentitiesOnly yes: ssh offers this key only, never the other known keys. macOS: UseKeychain yes.

Two accounts on the same provider: 2.5.

Step 3. One file per identity

git config --file="$HOME/.gitconfig-personal" --replace-all user.name  "personal_username"
git config --file="$HOME/.gitconfig-personal" --replace-all user.email "personal@users.noreply.github.com"

git config --file="$HOME/.gitconfig-work" --replace-all user.name  "work_username"
git config --file="$HOME/.gitconfig-work" --replace-all user.email "you@company.example"

--replace-all overwrites; --add appends one line per run.

Private commit address, one per provider:

Step 4. Rules in ~/.gitconfig

Per remote, one rule per URL shape (2.3). CLI setup of step 2 clones over https: the two https rules are enough.

git config --global --replace-all "includeif.hasconfig:remote.*.url:https://github.com/**.path" "~/.gitconfig-personal"
git config --global --replace-all "includeif.hasconfig:remote.*.url:git@github.com:*/**.path"   "~/.gitconfig-personal"
git config --global --replace-all "includeif.hasconfig:remote.*.url:https://gitlab.com/**.path" "~/.gitconfig-work"
git config --global --replace-all "includeif.hasconfig:remote.*.url:git@gitlab.com:*/**.path"   "~/.gitconfig-work"

Per folder, after the remote rules: the rule read last wins (2.4).

git config --global --replace-all "includeif.gitdir:~/git/personal/.path" "~/.gitconfig-personal"
git config --global --replace-all "includeif.gitdir:~/git/work/.path"     "~/.gitconfig-work"

Key shape: includeif . <condition> . path. The condition keeps its colons and slashes, hence the quotes.

gitdir:~/git/personal/ matches every repository below the folder. gitdir:~/git/personal (no trailing slash) matches nothing (2.3).

No global identity, no guessing:

git config --global --unset-all user.email    # exit 5 = already absent
git config --global --unset-all user.name
git config --global user.useConfigOnly true

Without user.useConfigOnly, git builds an identity from the OS user name and the host name (root@laptop.local) and commits with it. With it, a commit in a repo that matches no rule stops:

fatal: no email was given and auto-detection is disabled

Fix: move the repo under the right folder, add the remote, or git config --local user.email <EMAIL>.

Step 5. Check, from inside a repository

cd ~/git/personal/some-repo                     # a repository, not its parent folder
git config --show-origin --get user.email
git config --show-origin --get user.name
git var GIT_AUTHOR_IDENT                        # identity of the next commit
ssh -T git@github.com                           # account that authenticates (same for git@gitlab.com)
git config --list --show-origin --show-scope    # the whole configuration, one file per value

Expected for user.email:

file:/Users/you/.gitconfig-personal    personal@users.noreply.github.com

Empty output: you are outside a repository (2.2). Wrong file: 2.7. Docs: git var, testing your SSH connection.

Optional: sign your commits

SSH key, no GPG:

git config --file="$HOME/.gitconfig-personal" --replace-all gpg.format ssh
git config --file="$HOME/.gitconfig-personal" --replace-all user.signingkey "~/.ssh/github_personal.pub"
git config --file="$HOME/.gitconfig-personal" --replace-all commit.gpgsign true

Register the same public key on the provider as a signing key: GitHub, GitLab. GPG and local verification: 2.6.

Result

~/.gitconfig              includeIf rules + user.useConfigOnly, no identity
~/.gitconfig-personal     name, email, keys of the personal identity
~/.gitconfig-work         name, email, keys of the work identity
~/.ssh/config             one Host block per key (ssh only)
~/.ssh/github_personal    + .pub, one key pair per identity (ssh only)

Part 2. How it works

2.1 Config files and precedence

Files are read in order; for a plain key, the last value read wins (git-config, FILES).

Scope File Flag
system /etc/gitconfig --system
global ~/.gitconfig or ~/.config/git/config --global
local <repo>/.git/config --local
worktree <repo>/.git/config.worktree --worktree
  • An included file is read at the includeIf line: it overrides what the global file set before it.
  • A local value beats an included one. A user.email in .git/config outranks the rules; --show-origin exposes it.
  • git config --list --show-scope labels included values as global. --show-origin gives the file.

2.2 How includeIf is evaluated

Rules (git dir, resolved paths, symlinks): conditional includes. Outside a repository there is no git dir: no gitdir: rule matches, git config --get user.email prints nothing.

Measured, not in the doc: a linked worktree has its git dir under the main repository (<main>/.git/worktrees/<name>), a submodule under <superproject>/.git/modules/<name>: the rule follows the main repository. A worktree created outside ~/git/personal/ from a repo inside it matches gitdir:~/git/personal/; the reverse does not (its hasconfig: rule does, the remote is shared).

2.3 Pattern grammar

Grammar: conditional includes. Measured against a repo in ~/git/perso/deep/nested/r1:

Pattern Matches Rule
gitdir:~/git/perso/ βœ… trailing / appends **: everything below
gitdir:~/git/perso ❌ no trailing slash: that exact path only
gitdir:perso/ βœ… no leading ~/, ./, / or **/: **/ is prepended
gitdir:~/git/perso/**/.git βœ… explicit glob, not required
gitdir:~/GIT/perso/ ❌ case-sensitive
gitdir/i:~/GIT/perso/ βœ… /i: case-insensitive (Windows, macOS)
  • onbranch:<glob>: config per branch name.
  • hasconfig:remote.*.url:<glob>: matches if any remote URL matches. ** is special only next to a slash (**/, /**); elsewhere it degrades to *, and * never crosses /. One rule per URL shape: https://github.com/** (https), git@github.com:*/** (ssh).

2.4 Combining rules

When a repository matches both, the rule listed last wins (2.1). hasconfig: rules for the providers, then one gitdir: rule for the work tree: the folder wins where both apply.

2.5 Two layers: git config and ssh

  • ssh (or the credential helper) decides which account authenticates. Wrong key: permission denied, or a push on the wrong account.
  • git config decides which name is written in the commit. Wrong identity: the push succeeds with the wrong author.

Set independently, they can disagree: commit as personal, push as work.

Same provider, two accounts

The host name is the same, both layers look at the path.

Authorship, one rule per account prefix:

git config --global --replace-all "includeif.hasconfig:remote.*.url:https://github.com/my-org/**.path" "~/.gitconfig-work"
git config --global --replace-all "includeif.hasconfig:remote.*.url:https://github.com/me/**.path"     "~/.gitconfig-personal"

Authentication over https: the helper receives the path and a user name per prefix (gitcredentials); a helper storing one secret per user and path (OS keychain, Git Credential Manager) keeps the accounts apart. gh and glab serve the account they are logged in as: gh auth switch before pushing to the other one. Helpers: Git Credential Manager.

git config --global credential.useHttpPath true
git config --global "credential.https://github.com/my-org.username" "work_username"

Authentication over ssh: one Host alias per account.

Host github-personal
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_personal
    IdentitiesOnly yes

Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_work
    IdentitiesOnly yes

Clone with git clone git@github-personal:me/repo.git. Alternative with real URLs: bind the key to the identity file with core.sshCommand, the key then follows the same rule as the name.

git config --file="$HOME/.gitconfig-work" --replace-all \
  core.sshCommand "ssh -i ~/.ssh/github_work -o IdentitiesOnly=yes"

Rewriting URLs with url.<base>.insteadOf is a third option.

2.6 Signing: GPG or SSH

SSH signing, local verification:

echo "personal@users.noreply.github.com $(cat ~/.ssh/github_personal.pub)" >> ~/.ssh/allowed_signers
git config --global gpg.ssh.allowedSignersFile "~/.ssh/allowed_signers"
git log --show-signature -1

GPG signing, per identity:

git config --file="$HOME/.gitconfig-personal" --replace-all user.signingkey "<GPG_KEY_ID>"
git config --file="$HOME/.gitconfig-personal" --replace-all commit.gpgsign true
git config --global --replace-all gpg.program "/path/to/gpg"

Keys: gpg.program, gpg.ssh.allowedSignersFile.

Signing key and commit email move together: the provider verifies a signature against the emails of the account that owns the key (GitHub, GitLab).

2.7 Troubleshooting

git rev-parse --git-dir (which git dir, if any), then the Step 5 check.

Symptom Cause Fix
git config --get user.email prints nothing 2.2 cd into a repo
Commits carry you@your-machine.local no rule matched, user.useConfigOnly unset Step 4, then fix the rule
Not applied in a repo under the folder no trailing / in gitdir: 2.3
Not applied, path looks right case or symlink gitdir/i:, or the resolved path, 2.2
Applied, wrong value user.email in .git/config git config --local --unset user.email, 2.1
Three emails in an identity file --add --replace-all
Right author, wrong account on push ssh key or helper, not git config ssh -T git@host, IdentitiesOnly yes, 2.5
hasconfig: never matches no remote yet (Step 1) add the remote, or a gitdir: rule
hasconfig: matches ssh clones but not https, or the reverse one glob per URL shape 2.3
Commit shown as unverified signing key β‰  commit email on the account 2.6

2.8 Alternatives

  • Per-project include. A .gitconfig in the project, pulled in with a global alias: git config --global alias.set-config '!git config --local include.path "$(git rev-parse --show-toplevel)/.gitconfig"', then git set-config once per clone.
  • Sandboxes, agents, CI. GIT_CONFIG_GLOBAL=/path/to/file replaces ~/.gitconfig (test a setup without touching yours). GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL / GIT_COMMITTER_* in the environment beat every config file (environment variables). An agent cloning into /tmp or a linked worktree is a clone outside the tree: hasconfig: rules and user.useConfigOnly apply.
  • Audit every clone; a repository matching no rule prints Author identity unknown:
    for d in ~/git/*/*; do printf '%s\t' "$d"; git -C "$d" var GIT_AUTHOR_IDENT 2>&1 | head -1; done

Try it in a container

lab.sh and lab.Dockerfile replay Part 1 on a bare Debian (git, gh, glab, no config, no key) against two empty public repositories of yours, one per provider.

docker build -f lab.Dockerfile -t git-identity-lab .
docker run --rm -it --hostname laptop.local --env-file tokens.env git-identity-lab
./lab.sh          # 8 steps, pause between each; ./lab.sh 4 plays one step

Every command is printed before it runs. Steps 4 and 7 end with the Step 5 check, ssh -T included (denied: no key in the container, the push goes through the CLI).

tokens.env: GH_TOKEN, GITLAB_TOKEN, LAB_GITHUB_REPO, LAB_GITLAB_REPO (header of lab.sh). Short-lived tokens limited to those two repositories, revoked afterwards. Steps 1 to 5 and 7 run offline. --hostname with a domain: without one, git refuses to guess an e-mail (fatal: unable to auto-detect email address) and step 1 shows no root@laptop.local. Replay: delete main on both repositories (steps 6 and 8 push -u origin main), or point the variables at fresh repositories.


Appendix A. The resulting files

What Part 1 writes, for reading.

~/.ssh/config (2b only):

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_personal
    IdentitiesOnly yes
    AddKeysToAgent yes

~/.gitconfig (Step 4):

[user]
    useConfigOnly = true
[includeIf "hasconfig:remote.*.url:https://github.com/**"]
    path = ~/.gitconfig-personal
[includeIf "hasconfig:remote.*.url:git@github.com:*/**"]
    path = ~/.gitconfig-personal
[includeIf "hasconfig:remote.*.url:https://gitlab.com/**"]
    path = ~/.gitconfig-work
[includeIf "hasconfig:remote.*.url:git@gitlab.com:*/**"]
    path = ~/.gitconfig-work
[includeIf "gitdir:~/git/personal/"]
    path = ~/.gitconfig-personal
[includeIf "gitdir:~/git/work/"]
    path = ~/.gitconfig-work

~/.gitconfig-personal (Step 3, signing):

[user]
    name = personal_username
    email = personal@users.noreply.github.com
    signingkey = ~/.ssh/github_personal.pub
[gpg]
    format = ssh
[commit]
    gpgsign = true

~/.gitconfig-work: same shape, work values.

Appendix B. Idempotent bootstrap script

One call per rule. Re-runs overwrite, never append. Authentication (step 2) and signing stay CLI actions.

#!/usr/bin/env bash
set -euo pipefail
# usage: ./git-identity.sh <profile> <name> <email> <rule>
#   rule ending with /  β†’ folder   (~/git/work/)
#   anything else       β†’ remote   (https://github.com/**, git@github.com:*/**)
profile="$1"; name="$2"; email="$3"; rule="$4"
file="$HOME/.gitconfig-$profile"

git config --file="$file" --replace-all user.name  "$name"
git config --file="$file" --replace-all user.email "$email"
case "$rule" in
  */) git config --global --replace-all "includeif.gitdir:$rule.path" "$file" ;;
  *)  git config --global --replace-all "includeif.hasconfig:remote.*.url:$rule.path" "$file" ;;
esac
git config --global user.useConfigOnly true
git config --global --unset-all user.name  || [ $? -eq 5 ]   # 5 = already absent
git config --global --unset-all user.email || [ $? -eq 5 ]
./git-identity.sh personal "personal_username" "personal@users.noreply.github.com" "https://github.com/**"
./git-identity.sh personal "personal_username" "personal@users.noreply.github.com" "git@github.com:*/**"
./git-identity.sh work     "work_username"     "you@company.example"               "https://gitlab.com/**"
./git-identity.sh work     "work_username"     "you@company.example"               ~/git/work/

Appendix C. Credits

Revisions:

  • 2018-10 first version
  • 2026-08 rewrite, every command validated
  • 2026-09 authentication through the provider CLI, container lab, references.

Contributors, from the comments:

# Bare workstation for the multiple-git-identities demo: Debian, git, gh, glab.
# No git config, no ssh key, no token. Build once, run `docker run --rm -it`.
# openssh-client + known_hosts only so that `ssh -T git@<host>` (guide, Step 5) can be played: it must fail with `Permission denied (publickey)`.
FROM debian:trixie-slim
ARG GLAB_VERSION=1.116.0
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates curl gnupg less openssh-client \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update && apt-get install -y --no-install-recommends gh \
&& arch="$(dpkg --print-architecture)" \
&& curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/v${GLAB_VERSION}/downloads/glab_${GLAB_VERSION}_linux_${arch}.deb" \
-o /tmp/glab.deb \
&& dpkg -i /tmp/glab.deb && rm /tmp/glab.deb \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -m 700 /root/.ssh \
&& ssh-keyscan -t ed25519 github.com gitlab.com > /root/.ssh/known_hosts 2>/dev/null
WORKDIR /root
CMD ["bash"]
#!/usr/bin/env bash
# Multiple git identities, from a bare machine to two providers.
# Version: 4.2
# Changelog:
# 2026-09-03 - v4.2 repositories under ~/git (was ~/code); check() ends with the whole config, --show-origin --show-scope.
# 2026-09-02 - v4.1 titles for a first-time viewer: step N/8, each title states what the output shows; the verification is `check`, no cross-reference.
# 2026-09-02 - v3.1 blank line before the verify block.
# 2026-09-02 - v3.0 every command on screen: mkdir, cd, rm, the README write and git add go through run()/seed(). No silent step.
# 2026-09-02 - v2.1 two blank lines before each step title.
# 2026-09-02 - v2.0 verify(): the guide's Step 5 block played as is (show-origin email+name, git var, ssh -T) after each remote-driven identity (steps 4 and 7).
# 2026-09-02 - v1.3 step 1 title: git guesses the author, it does not invent one (advice.implicitIdentity).
# 2026-09-02 - v1.2 fix: trimmed glab status killed the script (SIGPIPE 141 under pipefail).
# 2026-09-02 - v1.1 trim glab auth status output (env-token notice).
# 2026-09-02 - v1.0 initial: 8 steps, GitHub + GitLab over https, no ssh key required.
#
# Run inside the companion container (see lab.Dockerfile), never on your workstation:
# docker build -f lab.Dockerfile -t git-identity-lab .
# docker run --rm -it --hostname laptop.local --env-file tokens.env git-identity-lab
# ./lab.sh # all 8 steps, pause between each
# ./lab.sh --auto # all 8 steps, no pause
# ./lab.sh 4 # one step
#
# Environment (tokens.env, chmod 600, never committed):
# GH_TOKEN fine-grained PAT, Contents: read/write on LAB_GITHUB_REPO only
# GITLAB_TOKEN personal access token, scopes read_user + write_repository
# LAB_GITHUB_REPO owner/name of an EMPTY public repository on github.com
# LAB_GITLAB_REPO namespace/name of an EMPTY public project on gitlab.com
# LAB_PERSONAL_NAME / LAB_PERSONAL_EMAIL identity for GitHub (defaults below)
# LAB_WORK_NAME / LAB_WORK_EMAIL identity for GitLab (defaults below)
# Steps 6 and 8 push; every other step works offline.
set -euo pipefail
: "${LAB_GITHUB_REPO:=you/git-identity-lab}"
: "${LAB_GITLAB_REPO:=you/git-identity-lab}"
: "${LAB_PERSONAL_NAME:=personal_username}"
: "${LAB_PERSONAL_EMAIL:=personal@users.noreply.github.com}"
: "${LAB_WORK_NAME:=work_username}"
: "${LAB_WORK_EMAIL:=you@company.example}"
ROOT="$HOME/git"
bold=$'\e[1m'; dim=$'\e[2m'; off=$'\e[0m'
title() { printf '\n\n%s== step %s/8 Β· %s ==%s\n' "$bold" "$1" "$2" "$off"; }
run() { printf '%s$ %s%s\n' "$dim" "$*" "$off"; "$@" || true; } # builtins too: no subshell, so `run cd` moves the script
seed() { printf '%s$ echo "# %s" > README.md && git add README.md%s\n' "$dim" "$1" "$off"; echo "# $1" > README.md && git add README.md; }
ident() { printf '%s$ git var GIT_AUTHOR_IDENT%s\n' "$dim" "$off"; { git var GIT_AUTHOR_IDENT 2>&1 || true; } | head -1; }
check() { # the guide's verification block (Part 1, Step 5), verbatim; $1 = host. ssh -T must be denied: no key here, the push goes through the CLI
printf '\n%s-- check: identity, source file, ssh --%s\n' "$bold" "$off"
run git config --show-origin --get user.email
run git config --show-origin --get user.name
ident
run ssh -T "git@$1"
run git config --list --show-origin --show-scope
}
step1() { title 1 "bare machine: git signs with the OS user and the hostname"
run git --version
run ls -la "$HOME/.gitconfig"
run mkdir -p "$ROOT/scratch"
run cd "$ROOT/scratch"
run git init -q .
run git commit -q --allow-empty -m "first commit"
run git log -1 --format='%an <%ae>'
ident
}
step2() { title 2 "one file per identity"
run git config --file="$HOME/.gitconfig-personal" --replace-all user.name "$LAB_PERSONAL_NAME"
run git config --file="$HOME/.gitconfig-personal" --replace-all user.email "$LAB_PERSONAL_EMAIL"
run git config --file="$HOME/.gitconfig-work" --replace-all user.name "$LAB_WORK_NAME"
run git config --file="$HOME/.gitconfig-work" --replace-all user.email "$LAB_WORK_EMAIL"
run cat "$HOME/.gitconfig-personal"
run cat "$HOME/.gitconfig-work"
}
step3() { title 3 "~/.gitconfig: rules only, no identity, no guessing"
run git config --global --replace-all "includeif.hasconfig:remote.*.url:https://github.com/**.path" "~/.gitconfig-personal"
run git config --global --replace-all "includeif.hasconfig:remote.*.url:https://gitlab.com/**.path" "~/.gitconfig-work"
run git config --global user.useConfigOnly true
run cat "$HOME/.gitconfig"
}
step4() { title 4 "a repository takes its identity from its remote"
run mkdir -p "$ROOT/github"
run cd "$ROOT/github"
run rm -rf lab
run git init -q -b main lab
run cd lab
ident
run git commit -q --allow-empty -m "who am I"
run git remote add origin "https://github.com/$LAB_GITHUB_REPO.git"
ident
run git config --show-origin --get user.email
seed "$LAB_GITHUB_REPO"
run git commit -q -m "personal identity, picked by the remote"
run git log -1 --format='%an <%ae>'
check github.com
}
step5() { title 5 "identity by folder: the trailing slash"
run git config --global --replace-all "includeif.gitdir:~/git/github.path" "~/.gitconfig-personal"
run cd "$ROOT/github/lab"
run git remote remove origin
ident
run git config --global --unset "includeif.gitdir:~/git/github.path"
run git config --global --replace-all "includeif.gitdir:~/git/github/.path" "~/.gitconfig-personal"
ident
run git config --show-origin --get user.email
run git remote add origin "https://github.com/$LAB_GITHUB_REPO.git"
}
step6() { title 6 "push to GitHub with the gh token, no ssh key"
run gh auth status
run gh auth setup-git
run git config --global --get-regexp '^credential'
run cd "$ROOT/github/lab"
run git push -u origin main
echo "open https://github.com/$LAB_GITHUB_REPO/commits/main"
}
step7() { title 7 "same on GitLab, the other identity"
run mkdir -p "$ROOT/gitlab"
run cd "$ROOT/gitlab"
run rm -rf lab
run git init -q -b main lab
run cd lab
run git remote add origin "https://gitlab.com/$LAB_GITLAB_REPO.git"
ident
seed "$LAB_GITLAB_REPO"
run git commit -q -m "work identity, picked by the remote"
run git log -1 --format='%an <%ae>'
check gitlab.com
}
step8() { title 8 "push to GitLab with the glab token, then the ssh pattern"
{ glab auth status 2>&1 || true; } | head -3 # env-token notice trimmed; the group absorbs SIGPIPE under pipefail
run git config --global --replace-all "credential.https://gitlab.com.helper" '!glab auth git-credential'
run cd "$ROOT/gitlab/lab"
run git push -u origin main
echo "open https://gitlab.com/$LAB_GITLAB_REPO/-/commits/main"
run git remote set-url origin "git@gitlab.com:$LAB_GITLAB_REPO.git"
ident
run git config --global --replace-all "includeif.hasconfig:remote.*.url:git@gitlab.com:**.path" "~/.gitconfig-work"
ident
run git config --global --unset "includeif.hasconfig:remote.*.url:git@gitlab.com:**.path"
run git config --global --replace-all "includeif.hasconfig:remote.*.url:git@gitlab.com:*/**.path" "~/.gitconfig-work"
ident
run git remote set-url origin "https://gitlab.com/$LAB_GITLAB_REPO.git"
}
pause() { [ "${AUTO:-0}" = 1 ] || { printf '%s[enter for next step]%s' "$dim" "$off"; read -r; }; }
AUTO=0
case "${1:-}" in
--auto) AUTO=1; set -- ;;
esac
if [ $# -ge 1 ]; then "step$1"; exit; fi
for n in 1 2 3 4 5 6 7 8; do "step$n"; [ "$n" = 8 ] || pause; done
@slmg

slmg commented May 20, 2020

Copy link
Copy Markdown

Thanks for the gist. To partially address your roadmap, here's how to deal with the includeIf section only using git config commands:

git config --file=.gitconfig-personal --add user.name personal_username
git config --file=.gitconfig-personal --add user.email user.personal@users.noreply.github.com

git config --global --add includeif.gitdir:~/code/personal/.path .gitconfig-personal

@bgauduch

bgauduch commented May 26, 2020

Copy link
Copy Markdown
Author

Thanks for the tip @slmg !

ghost commented Oct 8, 2020

Copy link
Copy Markdown

Thanks for the gist. To partially address your roadmap, here's how to deal with the includeIf section only using git config commands:

git config --file=.gitconfig-personal --add user.name personal_username
git config --file=.gitconfig-personal --add user.email user.personal@users.noreply.github.com

git config --global --add includeif.gitdir:~/code/personal/.path .gitconfig-personal

I dont see includeif as a option under git config --gloabl --add , am i missing something? running git 2.28.0

@slmg

slmg commented Oct 8, 2020

Copy link
Copy Markdown

It is shown under git config.

> git --version
git version 2.25.1

> git config --help | grep includeIf
       The include and includeIf sections allow you to include config directives from another source. These sections behave identically to each other with the exception
       that includeIf sections may be ignored if their condition does not evaluate to true; see "Conditional includes" below.
       You can include a config file from another by setting the special include.path (or includeIf.*.path) variable to the name of the file to be included. The
       You can include a config file from another conditionally by setting a includeIf.<condition>.path variable to the name of the file to be included.
           [includeIf "gitdir:/path/to/foo/.git"]
           [includeIf "gitdir:/path/to/group/"]
           [includeIf "gitdir:~/to/group/"]
           [includeIf "gitdir:/path/to/group/"]
           [includeIf "onbranch:foo-branch"]

@slmingol

slmingol commented Dec 6, 2020

Copy link
Copy Markdown

Keep in mind that every time you run git config --file=.gitconfig-personal --add user.name or whatever git config cmd it'll keep adding entries to the specified file. It's likely better to use --replace-all.

@offwork

offwork commented Dec 6, 2020

Copy link
Copy Markdown

I repeated the steps over and over but the name and mail are not recognized by git. I had already tried something similar to this before but failed. I can try if I see something different.

@igorbrites

Copy link
Copy Markdown

I repeated the steps over and over but the name and mail are not recognized by git. I had already tried something similar to this before but failed. I can try if I see something different.

I was having the same problem, but I realised that the path without the trailing / does not work. The commands from @slmg saved the day! Thanks guys!

@YaoC

YaoC commented Jul 9, 2021

Copy link
Copy Markdown

I repeated the steps over and over but the name and mail are not recognized by git. I had already tried something similar to this before but failed. I can try if I see something different.

Are you trying it in a git repo, it doesn't work if the working directory is not a git repo. See this.

@ThierryBerger

Copy link
Copy Markdown

On windows I had to include the case insensitive postfix /i [includeIf "gitdir/i:~/Documents/work/"]

Or maybe I should have put every letters in small letters despite windows showing capitals..? either way it's working now, thanks !

@shelllee

Copy link
Copy Markdown

Is there a way we could set .gitconfig for per domain such as github.com and gitlab.com?

@bgauduch

Copy link
Copy Markdown
Author

@shelllee not that I'm aware of !

Not sure what you are trying to do here, but it seem's to be the use case of this gist : separate ssh config depending on the host as described here

Be aware that Git config is independent from your ssh config, which git uses to connect to the Git hosts !

@shelllee

Copy link
Copy Markdown

@shelllee not that I'm aware of !

Not sure what you are trying to do here, but it seem's to be the use case of this gist : separate ssh config depending on the host as described here

Be aware that Git config is independent from your ssh config, which git uses to connect to the Git hosts !

I found this: https://github.com/DrVanScott/git-clone-init, which automatic setup of user identity on git clone by post-checkout hook.

@bgauduch

Copy link
Copy Markdown
Author

@shelllee not that I'm aware of !
Not sure what you are trying to do here, but it seem's to be the use case of this gist : separate ssh config depending on the host as described here
Be aware that Git config is independent from your ssh config, which git uses to connect to the Git hosts !

I found this: https://github.com/DrVanScott/git-clone-init, which automatic setup of user identity on git clone by post-checkout hook.

Well okay, I think I initially misunderstood πŸ˜…

but I really don't see the point adding another external tool since you can configure the exact same behavior with git includeif instruction as described here.

Up to you πŸ˜‰

@tw-yshuang

tw-yshuang commented Dec 14, 2021

Copy link
Copy Markdown

I create a CLI command to handle this!
Checkout my repo~~
This repo uses ssh-agent to switch your ssh account.

Git_SSH-Account_Switch

A CLI tool can switch an ssh account to your current shell. You will easily switch to your git account & ssh key when using the server, and using your account to manipulate the project on the server.

Installation

$ bash ./setup.sh

it will add some code in your profile & $logout_profile, and setup git-acc & .gitacc on the $HOME.
file:

git-acc.sh -> $HOME/.git-acc, git-acc function.
.gitacc -> $HOME/.gitacc, save info. that regist on git-acc.

Control

        +---------------+
        |    git-acc    |
        +---------------+

SYNOPSIS

  git-acc [account]|[option]

OPTIONS

  [account]               use which accounts on this shell, type the account name that you register.
  -h, --help              print help information.
  -add, --add_account     build git_account info. & ssh-key.
      -t, --type          ssh-key types, follow `ssh-keygen` rule, 
                          types: dsa | ecdsa | ecdsa-sk | ed25519 | ed25519-sk | rsa(default)
  -rm, --remove_account   remove git_account info. & ssh-key from this device
  -out, --logout          logout your current ssh-acc.


EXAMPLES

  $ git-acc tw-yshuang

@cbbdev

cbbdev commented Mar 9, 2022

Copy link
Copy Markdown

Hello, sorry to be a little late to the party but after running into a similar issue and finding this solution, it inspired a more dynamic way to include the custom config files. When having multiple projects at the same time, the "IncludeIf..." can became too verbose and may also lead to confusion if some of those configs contain similar settings (or names). In order to alleviate this, we still placed the custom ".gitconfig" file inside each project, but in the global ".gitconfig" (in windows should be under C:\Users$user) and add an alias there like this:
[alias] set-config = !git config --global include.path $(git rev-parse --show-toplevel)/.gitconfig
We named it "set-config" but of course you can change that to your liking. after that, just do:

  • git init (to reload)
  • git set-config
    This will automatically set the path to the current project being used, meaning that it can be used without limitations or having to manually add multiple "if" statements.
    Thanks to @bgauduch for this post and hope this helps!

@Xat59

Xat59 commented Jun 22, 2022

Copy link
Copy Markdown

For your information you must now specify the .git folder in the gitdir such as :

[includeIf "gitdir:~/code/personal/repo1/.git"]

Another useful tip, you can use globbing on parent directory to detect new repos without editing your git-config file :

[includeIf "gitdir:~/code/personal/**/.git"]

@825i

825i commented Nov 22, 2023

Copy link
Copy Markdown

For your information you must now specify the .git folder in the gitdir such as :

[includeIf "gitdir:~/code/personal/repo1/.git"]

Another useful tip, you can use globbing on parent directory to detect new repos without editing your git-config file :

[includeIf "gitdir:~/code/personal/**/.git"]

Thanks! I will pulling my hair out wondering why it didn't work. Also thanks for the globbing advice because that would have been my next question!

@MGREMY

MGREMY commented Apr 24, 2024

Copy link
Copy Markdown

Thanks man ! Note that git config --get xxxx.xxxx works only when you are inside a repository, otherwise it doesn't show anything πŸ‘

@offwork

offwork commented Apr 25, 2024

Copy link
Copy Markdown

Hi there!

Simple solution for Mac and fish-shell users like me:
After the ssh keys are created, run the agent command for fish:

eval $(ssh-agent -c)

and then install the ssh keys on the mac keychain:
ssh-add --apple-load-keychain -A ~/.ssh/github_personal
ssh-add --apple-load-keychain -A ~/.ssh/bitbucket_work

and then install the ssh keys on the mac keychain.

Screenshot 2024-04-25 at 15 42 29

@shellheim

Copy link
Copy Markdown

I originally had a problem with using two hosts and when I signed my commits, the signature would be invalid on the web UI because my global git email was set to github only. What I wanted to do was figure out a way to automatically change the user.email variable to the respective noreply addresses.

That can be done using the IncludeIf directive, just have to use the right globbing pattern.
My config is like this :

[includeIf "hasconfig:remote.*.url:**github.com:*/*.git"]
	path = github_config 

[includeIf "hasconfig:remote.*.url:**codeberg.org:*/*.git"]
	path = codeberg_config

Where github_config and codeberg_config are files with their respective emails. The globbing pattern is just :

**example.com:*/*.git

for ssh remote urls.

@amaury-d

amaury-d commented Sep 2, 2024

Copy link
Copy Markdown

@shellheim nice tip πŸ‘

@bgauduch

bgauduch commented Aug 23, 2026

Copy link
Copy Markdown
Author

πŸ“£ 2026 rewrite is live!

This gist dated back to 2018. Git moved on, and your comments piled up 8 years of wisdom, so the whole thing has been rewritten:

  • Quick path through git config commands only, no manual file editing (the long-standing roadmap item, finally done)
  • Identity per provider with includeIf "hasconfig:remote.*.url:" (git 2.36+), next to the per-folder setup
  • Fixed GPG section: the key is gpg.program; the old [program] pgp block was silently ignored by git
  • New safety net: user.useConfigOnly true, so an unmatched repo fails loudly instead of committing with a guessed identity
  • Pattern grammar table (trailing slash, ** gotchas), troubleshooting table, SSH commit signing
  • Every command and pattern executed against git 2.55 before publication

⚠️ Two fixes suggested in this thread turned out to be off the root cause, and the rewrite debunks them with measurements (section 2.3):

  • The /.git suffix (or **/.git globbing) is not required. The real cause is the missing trailing slash: gitdir:~/code/personal/ matches every repo below it on its own.
  • The **example.com:*/*.git pattern only matches simple ssh clones. It misses https clones, nested groups, and URLs without .git: in this glob language ** is only special next to a slash. The guide now uses one pattern per URL shape.

πŸ™ Many thanks to everyone who commented over the years, this rewrite is built on your feedback:

Fresh eyes welcome: if something reads wrong or fails on your setup, comment away.

Take care πŸ™Œ

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment