Shipping a team gitconfig with includeIf Jump to heading

Documentation that says โ€œplease set these seven valuesโ€ produces a team where some people have set seven, some four and one has set none. Gitโ€™s include mechanism turns that list into a file under version control: the settings are reviewed like code, changing them is a pull request, and a new machine picks them up with one command instead of seven. This recipe sets that up, including the conditional includes that keep work and personal identities apart, within Git configuration management at scale.

When to use this approach Jump to heading

  • Different machines produce different results from the same repository.
  • Your contributing documentation contains a list of git config commands.
  • Identity mistakes โ€” personal email on work commits โ€” happen occasionally.
  • You want configuration changes to go through review.
  • If your team is two people and everything already works, a short setup script is enough.

Step 1 โ€” Write the committed file, with reasons Jump to heading

Every line should have a justification, because the file will otherwise accumulate settings nobody can defend.

# .gitconfig-team โ€” settings that change what a commit or checkout contains.
# Personal preferences (aliases, pager, colours) deliberately stay out.

[core]
    # Line endings are decided by .gitattributes, not by each machine.
    autocrlf = false
    # Hooks live in the repository so they are reviewed like code.
    hooksPath = .githooks

[merge]
    # Shows the common ancestor in a conflict: resolution becomes a decision.
    conflictStyle = zdiff3

[diff]
    # Produces noticeably more readable diffs on refactors.
    algorithm = histogram
    colorMoved = default

[pull]
    # Avoids accidental merge commits from a routine pull.
    rebase = true

[fetch]
    # Removes remote-tracking refs for branches that are gone.
    prune = true

[rebase]
    # Keeps stacked branches intact when the base moves.
    updateRefs = true
    autosquash = true
git add .gitconfig-team && git commit -m 'chore: add shared Git configuration'
# Verification: the file is committed and syntactically valid
git config --file .gitconfig-team --list
A documented list against a committed fileA list of commands in a contributing guide is applied inconsistently, cannot be reviewed, and drifts silently. A committed file is version controlled, changes through review, and applies everywhere the include has been added.Commands in the docsCommitted config fileapplied consistentlyrarelyafter one includechanges reviewednolike any codedrift detectablenoone checksetup effortseven commandsonethe include command is the only thing that still has to happen per clone

Step 2 โ€” Include it from each clone Jump to heading

Git will not read a repositoryโ€™s configuration automatically, and that restriction is deliberate.

git config --local include.path ../.gitconfig-team
# Verification: the values now resolve, and their origin is the committed file
git config --show-origin --get merge.conflictStyle
git config --show-origin --get core.hooksPath

The path is relative to the repositoryโ€™s .git directory, which is why it starts with ../. Getting that wrong fails silently โ€” the include is recorded, no file is found, and nothing applies, which is exactly why the verification above checks the origin rather than just the value.

SAFETY WARNING โ€” Git deliberately refuses to read configuration out of a cloned repository, because configuration can specify commands: pagers, diff drivers, credential helpers and hooks. Running the include command is the consent step that makes it safe. Never add it automatically from a script that runs on clone of an untrusted repository, and be sure you trust the repository whose configuration you are including.

Step 3 โ€” Use conditional includes for identity Jump to heading

Identity is personal, so it stays out of the repository โ€” but it can still be automatic.

# ~/.gitconfig
[user]
    name = Ada Lovelace
    email = [email protected]

[includeIf "gitdir:~/work/"]
    path = ~/.gitconfig-work

[includeIf "gitdir:~/oss/"]
    path = ~/.gitconfig-oss
# ~/.gitconfig-work
[user]
    email = [email protected]
[commit]
    gpgsign = true
[gpg]
    format = ssh
[user]
    signingkey = ~/.ssh/id_ed25519_signing.pub
# Verification: identity differs by directory without any per-clone step
git -C ~/work/app config --get user.email
git -C ~/personal/notes config --get user.email
# Conditional on the remote URL instead, for directories that are not tidy
# [includeIf "hasconfig:remote.*.url:[email protected]:acme/**"]
#     path = ~/.gitconfig-work
git config --get-urlmatch user.email "$(git remote get-url origin)"
Choosing the condition for an includeA directory condition is simplest and works when work repositories live under one path. A remote-URL condition works regardless of layout and is the right choice when repositories are scattered. An unconditional include belongs only in the repository-local config.How do you tell work repositories from personal ones?they live under one pathgitdir conditionsimplestscattered directorieshasconfig remote URLmatches the remoteper repositoryinclude.path, localthe team filepick one and be consistent โ€” mixing conditions makes the effective identity hard to predict

Step 4 โ€” Make the include part of setup Jump to heading

#!/usr/bin/env sh
# scripts/setup โ€” idempotent; safe to run on every clone, repeatedly.
set -eu
root=$(git rev-parse --show-toplevel)

git -C "$root" config --local include.path ../.gitconfig-team

# Fail loudly if the include did not take effect.
test "$(git -C "$root" config --get merge.conflictStyle)" = "zdiff3" \
  || { echo "setup: include did not apply โ€” check the path" >&2; exit 1; }

echo "configured: $(git -C "$root" config --get user.email) / hooks: $(git -C "$root" config --get core.hooksPath)"
# Verification: from a completely fresh clone
git clone "$(git remote get-url origin)" /tmp/fresh && (cd /tmp/fresh && ./scripts/setup)

Step 5 โ€” Check it in CI, not just locally Jump to heading

# The settings that affect committed content should be verified on the server too
      - name: Attributes must be present
        run: git check-attr -a -- . >/dev/null && test -f .gitattributes
      - name: Hooks directory must exist and be executable
        run: test -d .githooks && test -x .githooks/pre-commit
# And a local drift check people can run themselves
for k in core.hooksPath merge.conflictStyle pull.rebase fetch.prune; do
  printf '%-24s %s\n' "$k" "$(git config --get "$k" || echo UNSET)"
done
From a committed file to a correctly configured cloneThe team file is reviewed and committed. The setup script adds one include and verifies it applied. Conditional includes in the personal configuration supply identity. A drift check confirms the result rather than assuming it.Committed filereviewed like codeinclude.pathone commandper cloneincludeIfidentity by directoryVerifyorigin, not just valuethe last box catches the silent failure of a wrong include path

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

What happens if someone does not run the include? Jump to heading

They keep whatever their machine had, which is the situation you are trying to leave. That is why the audit matters: the include cannot be forced, so the only reliable approach is to make it easy, check it, and enforce anything critical on the server where configuration does not apply at all.

Can the team file override a personal setting? Jump to heading

It can, and that is worth being careful about. An include read later in the resolution order wins, so a repository-local include beats a global setting. Keep the team file to settings where overriding is the intent, and never put identity in it.

Does this work with worktrees? Jump to heading

Yes, and repository-level configuration is shared across worktrees of the same repository, which is usually what you want. Where a worktree genuinely needs a different value, enable extensions.worktreeConfig and set it with --worktree โ€” covered in Git worktrees and parallel development.