Skip to content

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.

Paste this into ~/.gitconfig, change the name and email, and read on only for the parts you want to argue with:

~/.gitconfig
[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-work

The 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.

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:

Terminal window
git config set --global push.autoSetupRemote true
git config get push.autoSetupRemote
git config list --show-origin --show-scope # every setting, and which file it came from
git config edit --global # open the file in $EDITOR

The 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.

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 before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: 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 feat

push.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.

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.py
Resolved '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.

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)
|/
* m2

With 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/part1

rebase.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.

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 tree
diff --git c/f i/f # git diff --cached: commit vs index

That 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.

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.0
v1.2.0
v1.9.0

With the setting:

v1.2.0
v1.9.0
v1.10.0

help.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 = prompt

Autocorrecting 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.

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.

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
~/.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-work

To check it’s working, run this inside a work repository — it should list both files, with the work one last:

Terminal window
git config list --show-origin | grep user.email

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.

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.