Git Config: The Settings Worth Stealing
Git’s defaults are not chosen for you. They are chosen for everybody who has ever used git,
including people whose scripts from 2009 would break if anything changed, and the result is a
tool that ships with master, refuses to pull until you have stated a philosophy of history,
greets every new branch with a lecture about upstreams, and draws merge conflicts with half
the information left out.
None of that needs fixing on the command line every day. It needs fixing once, in
~/.gitconfig, and then committing to your dotfiles so it never
needs fixing again. Below is the file, followed by what each part does and the handful of
places where the advice everyone copies is wrong.
The short version
Section titled “The short version”Paste this into ~/.gitconfig, change the name and email, and read on only for the parts you
want to argue with:
[user] name = Your Name
[init] defaultBranch = main
[pull] rebase = true
[push] autoSetupRemote = true
[fetch] prune = true
[rebase] autoStash = true autoSquash = true updateRefs = true
[merge] conflictStyle = zdiff3
[rerere] enabled = true autoUpdate = true
[diff] algorithm = histogram colorMoved = plain mnemonicPrefix = true
[commit] verbose = true
[branch] sort = -committerdate
[tag] sort = version:refname
[column] ui = auto
[help] autocorrect = prompt
[alias] st = status -sb lg = log --graph --format='%C(auto)%h%d %s %C(dim)%an, %ar' last = log -1 --stat amend = commit --amend --no-edit undo = reset --soft HEAD~1 unstage = restore --staged wip = !git add -A && git commit -m wip --no-verify root = rev-parse --show-toplevel
# Keep this last: later values win, so the work email must come after [user][includeIf "gitdir:~/work/"] path = ~/.gitconfig-workThe final block reads a second file for repositories under ~/work/. A work email that sets
itself explains it; delete it if you have one identity.
Where .gitconfig lives
Section titled “Where .gitconfig lives”Git reads your personal config from two places: ~/.config/git/config, then ~/.gitconfig.
Both are loaded, and where they disagree the last one read wins, so ~/.gitconfig beats
the XDG file. Pick one. Having both is how a setting you can see in a file you’re looking at
quietly fails to apply.
You rarely need to edit it by hand, and git 2.46 gave the git config command subcommands that
read like English rather than a set of flags:
git config set --global push.autoSetupRemote truegit config get push.autoSetupRemotegit config list --show-origin --show-scope # every setting, and which file it came fromgit config edit --global # open the file in $EDITORThe old spellings — git config --global push.autoSetupRemote true, --get, --list — still
work, and are what everything written before mid-2024 uses. --show-origin is the one to
remember either way. When git is behaving as though a setting doesn’t exist, it will tell you
which file set it, and more usefully, which other file set it again afterwards.
Pull and push: stop negotiating
Section titled “Pull and push: stop negotiating”Current git will not pull a branch that has diverged from its upstream until you tell it how you’d like history handled. With an empty config it says so at some length, and then refuses:
hint: You have divergent branches and need to specify how to reconcile them.hint: You can do so by running one of the following commands sometime beforehint: your next pull:hint:hint: git config pull.rebase false # mergehint: git config pull.rebase true # rebasehint: git config pull.ff only # fast-forward only...fatal: Need to specify how to reconcile divergent branches.Answer the question once. pull.rebase = true replays your local commits on top of what you
fetched, so a pull on a feature branch doesn’t leave a “Merge branch ‘main’ of github.com:…”
commit behind to decorate the log. rebase.autoStash = true stashes uncommitted changes
before the rebase and puts them back afterwards, which is what you were going to do by hand.
If rebasing on pull makes you nervous, pull.ff = only is the conservative answer: pull
succeeds when it can fast-forward, and fails loudly when it can’t, leaving the decision to you
each time. Either is better than the default, which is to make you read that hint again.
Pushing has the same problem on every new branch:
fatal: The current branch feat has no upstream branch.To push the current branch and set the remote as upstream, use
git push --set-upstream origin featpush.autoSetupRemote = true (git 2.37 and later) does exactly what that message tells you to
do, silently, on the first push of any branch without an upstream. It is the single most
satisfying line in the file.
fetch.prune = true deletes your remote-tracking branches when the branch is deleted on the
server, so git branch -a stops listing everything anyone merged in the last three years. It
never touches your local branches.
Merge conflicts you can actually read
Section titled “Merge conflicts you can actually read”Here is a conflict in git’s default style. Both sides changed a greeting:
def greet(name):<<<<<<< HEAD prefix = "Howdy"======= prefix = "Hi">>>>>>> left return f"{prefix}, {name}!"And here is the same merge with merge.conflictStyle = zdiff3 (git 2.35 and later):
def greet(name):<<<<<<< HEAD prefix = "Howdy"||||||| fdfa849 prefix = "Hello" return f"{prefix}, {name}"======= prefix = "Hi">>>>>>> left return f"{prefix}, {name}!"The section between ||||||| and ======= is the original, before either side touched it. It
tells you two things the default style hid: that it said "Hello" before anyone changed it,
and that both branches added the ! — which the default style had quietly resolved and
moved out of the conflict, so you could never have known. Resolving a conflict without the
base is guessing at intent from two answers without the question. zdiff3 is diff3 with the
lines both sides agree on trimmed off the edges, which keeps the markers tight.
rerere — reuse recorded resolution — is for the conflict you’ve resolved before. Rebase a
long-lived branch a few times, or merge and then abandon the merge, and you meet the same
conflicts again. With rerere.enabled = true, git records how you resolved each one and
replays it next time:
CONFLICT (content): Merge conflict in f.pyResolved 'f.py' using previous resolution.Automatic merge failed; fix conflicts and then commit the result.Note the last line. rerere fixes the file but still stops the merge, so you look before
committing. rerere.autoUpdate = true goes one step further and stages the resolved file —
Staged 'f.py' using previous resolution — which saves a git add without taking away the
review. It never commits for you. git rerere forget path/to/file removes a resolution you’ve
come to regret.
Rebasing without the busywork
Section titled “Rebasing without the busywork”rebase.updateRefs = true (git 2.38 and later) is for anyone who stacks branches: part2
built on part1, built on main. Rebase part2 onto a newer main without it, and part1
is left pointing at the old commits, and the log afterwards has two copies of p1:
* p1 (part1)| * p2 (HEAD -> part2)| * p1| * m3 (main)|/* m2With it, git moves every branch that points into the rebased range, and says so:
Successfully rebased and updated refs/heads/part2.Updated the following refs with --update-refs: refs/heads/part1rebase.autoSquash = true makes git rebase -i automatically reorder commits made with
git commit --fixup=<commit> next to the commit they fix, marked for squashing. It applies to
interactive rebases only. Together they make “fix the typo in the third commit of this stack”
one commit and one rebase rather than an afternoon.
Diffs worth reading
Section titled “Diffs worth reading”diff.algorithm = histogram swaps git’s default Myers algorithm for one that tends to line
diffs up on the lines that actually changed, rather than on whatever blank lines and closing
braces happen to match. The difference shows most when a function is moved or rewritten — the
diff stops interleaving the old and the new like a card shuffle.
diff.colorMoved = plain colours lines that were moved rather than changed in a different
colour from genuine additions and deletions. Reviewing a refactor becomes a matter of reading
the lines that aren’t in the moved colour.
diff.mnemonicPrefix = true replaces the meaningless a/ and b/ in diff headers with
letters saying what’s being compared:
diff --git i/f w/f # git diff: index vs working treediff --git c/f i/f # git diff --cached: commit vs indexThat tells you at a glance whether you’re looking at what’s staged or what isn’t, which is
most of the confusion git diff causes.
commit.verbose = true puts the full diff of what you’re committing underneath the message
in your editor, below a >8 scissors line that git strips before saving. You write the message
looking at the change, which produces better messages and catches the debug print you meant to
leave out.
For diffs with syntax highlighting and line numbers, the next step is a pager rather than a setting. delta is four lines in this same file.
Small things that add up
Section titled “Small things that add up”branch.sort = -committerdate lists branches most recently committed-to first, so the branch
you were on yesterday is at the top rather than wherever the alphabet put it.
column.ui = auto lays git branch, git tag and git status untracked lists out in columns
when printing to a terminal, and leaves pipes alone.
tag.sort = version:refname sorts tags as versions rather than strings. The default:
v1.10.0v1.2.0v1.9.0With the setting:
v1.2.0v1.9.0v1.10.0help.autocorrect deserves more care than it gets, because the value many copied configs use
means something different now. Until git 2.49, autocorrect = 1 meant wait a tenth of a
second, then run the corrected command. Since 2.49 it means run it immediately, and plenty
of guides still describe the old behaviour. Values above 1 are still a delay in tenths of a
second. Use prompt, which asks first:
[help] autocorrect = promptAutocorrecting git stauts to git status is harmless; autocorrecting a typo into a command
you didn’t mean, without asking, is the kind of thing you want a question about.
Git aliases
Section titled “Git aliases”The ones in the short version, and what they’re for:
| Alias | Runs | For |
|---|---|---|
git st |
status -sb |
Status in five lines instead of thirty |
git lg |
log --graph --format=… |
One line per commit, with branches and age |
git last |
log -1 --stat |
What did I just commit? |
git amend |
commit --amend --no-edit |
Add staged changes to the last commit |
git undo |
reset --soft HEAD~1 |
Uncommit, keeping the changes staged |
git unstage |
restore --staged |
The opposite of git add |
git wip |
add -A && commit -m wip --no-verify |
Save everything before a risky operation |
git root |
rev-parse --show-toplevel |
Path to the top of the repository |
amend and undo rewrite the last commit, so use them before you push rather than after.
wip skips your hooks on purpose — it’s a save point, not a commit anybody will review —
and git undo takes it back out again.
Git aliases or shell aliases? Both. Shell aliases
like gst save keystrokes; git aliases live in the file that goes wherever git goes, work in
every shell, and complete properly after git . The two don’t conflict.
Four things about aliases that are not in the brochure:
You can’t override a built-in command. git config set --global alias.status 'log -1' is
accepted without complaint and then ignored forever; git status runs the real thing. No
error, no warning. If an alias isn’t working, check its name isn’t already a git command.
A leading ! runs a shell command rather than a git subcommand, which is how wip chains
two commands with &&.
Shell aliases run from the root of the repository, not the directory you’re in. Git
changes directory first. Where you actually were is in $GIT_PREFIX, relative to the root —
sub/, or empty at the top.
Arguments go on the end, which is fine for simple aliases and useless when you need one in the middle. The workaround is a shell function defined and called in the same line:
[alias] fixup = "!f() { git commit --fixup=\"$1\" && git rebase -i --autosquash \"$1~\"; }; f"git fixup abc123 then commits the staged changes as a fixup for abc123 and opens the
interactive rebase with it already in place. Anything more involved than that belongs in a
script called git-something on your PATH, which git will run as git something.
A work email that sets itself
Section titled “A work email that sets itself”The traditional way to use a different email for work repositories is to remember to set it
in each one, then forget, then find your personal Gmail address in the company’s history for
the rest of time. includeIf loads another file only when a condition matches:
# ~/.gitconfig — at the bottom[includeIf "gitdir:~/work/"] path = ~/.gitconfig-work[user]Every repository under ~/work/ now commits with the work address, and everything else with
the personal one. It pairs naturally with the two GitHub keys in your SSH
config, which handles which account pushes,
where this handles who wrote it. The work file can also carry a different
user.signingKey, or anything else.
Two easy mistakes will stop it working, and git warns you about neither:
Put it at the bottom. Git reads the included file at the point where the includeIf
appears, and the last value wins. Put the block above [user] and the personal email is read
second and wins, in exactly the repositories you set this up for.
Keep the trailing slash. gitdir:~/work/ matches everything beneath ~/work.
gitdir:~/work, without the slash, matches nothing you want, and says nothing about it.
If your work repositories don’t all live in one directory, match on their remote URL instead,
with hasconfig:remote.*.url: (git 2.36 and later):
[includeIf "hasconfig:remote.*.url:[email protected]:acme/**"] path = ~/.gitconfig-workTo check it’s working, run this inside a work repository — it should list both files, with the work one last:
git config list --show-origin | grep user.emailSettings you don’t need
Section titled “Settings you don’t need”Advice about .gitconfig is copied from post to post, and some of it stopped being necessary
a decade ago. None of these do any harm; they just don’t do anything:
| Setting | Why it’s redundant |
|---|---|
push.default = simple |
The default since git 2.0 |
color.ui = auto |
The default since git 1.8.4 |
diff.renames = true |
Already the default |
core.excludesFile |
~/.config/git/ignore is read without it |
The one to be careful with is core.autocrlf. On macOS and Linux, leave it unset. Line-ending
policy belongs in a .gitattributes file in the repository, where it applies to everyone who
clones it rather than to whoever remembered to configure their machine.
Where next
Section titled “Where next”The file is only worth the effort if it survives a new laptop, so put it in your dotfiles
repo — which also covers keeping a ~/.gitconfig.local for anything
you’d rather not publish. delta is the pager that makes
all these diffs pleasant to read, fzf’s branch
switcher makes that branch.sort ordering
something you choose from rather than scroll through, and a Starship
prompt keeps the branch and its state in view
before you type anything at all.