Skip to content
Journey to Code

Advanced Git

Make Git Yours.

Config Scopes, Safe Defaults, and Aliases Worth Having

· By · 7 min read

Cover reading "Make Git Yours." a terminal window running git config --list --show-origin with the file each setting comes from

You run git pull and Git answers like this:

$ git pull
fatal: Need to specify how to reconcile divergent branches.

It also prints a dozen lines of hints. They tell you to set a config value, and they do not say where that value is kept or whose settings you are about to change.

In this article, we are going to read Git's settings, work out which file each one comes from, and set a short list of defaults, each one tied to a message like the one above. By the end, one command will tell you where every setting lives:

git config --list --show-origin

We will also write a few aliases and decide which ones are safe to keep.

Where a setting lives#

Git reads settings from several files and layers them. The ones you will touch are these:

ScopeFlagFileApplies to
system--systemetc/gitconfig inside the Git installevery user on the machine
global--global.gitconfig in your home folderevery repository you use
local--local (the default when you write).git/config in the repositorythat repository only

When the same setting appears in more than one file, the narrower scope wins.

To see that without touching your real settings, point Git at a throwaway global file. In Git Bash:

export GIT_CONFIG_GLOBAL="$PWD/.gitconfig"
export GIT_CONFIG_NOSYSTEM=1

GIT_CONFIG_GLOBAL replaces your global file for this terminal window only. GIT_CONFIG_NOSYSTEM=1 ignores the system file. Close the window and your own settings are back.

Now set an email twice, once globally and once in a repository:

git config --global user.name "Ada Example"
git config --global user.email "ada@home.example"
git init -q demo && cd demo
git config user.email "ada@work.example"

Ask Git for everything it knows, and where it learned it. I left out the core.* lines that git init writes into .git/config:

$ git config --list --show-origin
file:C:/.../.gitconfig	user.name=Ada Example
file:C:/.../.gitconfig	user.email=ada@home.example
file:.git/config	user.email=ada@work.example

The left column is the file and the right column is the setting. The email appears twice, once from each file.

Ask for the one that counts:

$ git config user.email
ada@work.example

$ git config --show-scope --show-origin user.email
local	file:.git/config	ada@work.example

The local file won. --show-scope adds the scope name, so you do not have to guess from the path.

If a setting behaves strangely, this is the first command to run. A value you forgot you set three years ago shows up here with its file name next to it.

Two more commands finish the picture. git config --global --edit opens the global file in your editor, and git config --global --unset user.email removes one setting.

Six defaults that each stop one annoyance#

Every default below has a message it answers. We ran each one against a pretend remote, origin.git, with two clones standing in for two people.

pull.ff: stop guessing on a divergent pull#

Alice pushes a commit. Bob commits locally without pulling first, then runs git pull. Here is Bob's screen, with the hints cut out:

$ git pull
From .../origin
   9afce9f..d953b3d  main       -> origin/main
...
fatal: Need to specify how to reconcile divergent branches.

Git will not pick between a merge and a rebase for you. The hint lists three ways to answer. Two of them are useful defaults:

$ git -c pull.ff=only pull
fatal: Not possible to fast-forward, aborting.

$ git -c pull.rebase=true pull
Successfully rebased and updated refs/heads/main.

-c name=value applies a setting to one command and nothing else, which is a safe way to try a default before keeping it.

pull.ff only pulls only when the pull is a straight line, and stops otherwise. You then choose to merge or rebase yourself. pull.rebase true always replays your local commits on top of what arrived, which gave Bob this history:

* 751c288 bob change
* d953b3d alice change
* 9afce9f start

Either one removes the surprise. Pick one, and set it with --global:

git config --global pull.ff only

push.autoSetupRemote: stop typing --set-upstream#

A new branch has no upstream yet, so the first plain git push fails:

$ git push
fatal: The current branch feature/x has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin feature/x

Turn on the setting, and the same command works:

$ git config --global push.autoSetupRemote true
$ git push
branch 'feature/x' set up to track 'origin/feature/x'.
 * [new branch]      feature/x -> feature/x

The branch is now tracking origin/feature/x, so git status -sb starts with ## feature/x...origin/feature/x. This setting needs Git 2.37 or newer. It assumes the branch keeps the same name on the remote, which is the usual case on GitHub.

fetch.prune: stop seeing branches that are gone#

When a branch is deleted on GitHub, your copy of its pointer, origin/feature/x, stays behind. Alice's list after Bob deleted feature/x on the remote:

$ git fetch
$ git branch -r
  origin/feature/x
  origin/main

A plain fetch does not remove it. With --prune, it does:

$ git fetch --prune
 - [deleted]         (none)     -> origin/feature/x
$ git branch -r
  origin/main

fetch.prune true makes every fetch behave that way. It only removes those remote pointers. A local branch of the same name stays, and Git marks it:

$ git branch -vv
* main d953b3d [origin/main] alice change
  y    ea11253 [origin/y: gone] y

gone is your cue that the branch was merged or deleted upstream.

rebase.autoSquash: fixups land where they belong#

git commit --fixup <commit> creates a commit titled fixup! <that commit's title>. Without help, an interactive rebase leaves it at the end:

$ git rebase -i main
$ git log --oneline
3bb5134 fixup! Add b
8ae88bd Add c
d42c68b Add b
c5773f4 Add a

With rebase.autoSquash on, the same command folds it into its target:

$ git config --global rebase.autoSquash true
$ git rebase -i main
$ git log --oneline
4b423fe Add c
883a054 Add b
c5773f4 Add a

The Git manual says this setting applies to interactive mode. The full workflow is in Interactive Rebase, Done Properly.

rebase.updateRefs: stacked branches move together#

Say step2 is built on step1, and main moves. Rebase step2 onto main and step1 is left behind:

$ git rebase main
$ git log --oneline --graph --all --decorate
* c628fd7 (HEAD -> step2) step 2
* 2477d66 step 1
* 2f96efb (main) main moves
| * 545b7ce (step1) step 1
|/
* 2502ebe Add a

With rebase.updateRefs true, the same rebase also moves step1:

Successfully rebased and updated refs/heads/step2.
Updated the following refs with --update-refs:
	refs/heads/step1

This needs Git 2.38 or newer. On 2.45.2 it worked for a plain git rebase main, not only with -i.

merge.conflictStyle: a conflict that shows more#

git config --global merge.conflictStyle zdiff3

This adds the original text to conflict markers and trims lines both sides share. It needs Git 2.35 or newer.

Aliases#

An alias is a short name for a longer command, stored in config. They live in the same files, so --show-origin lists them too.

git config --global alias.st "status -sb"
git config --global alias.lg "log --oneline --graph --decorate"
git config --global alias.cm "commit -m"
git config --global alias.unstage "restore --staged"

They behave like built-in commands:

$ git st
## main
M  a.txt

$ git lg
* 80e12e5 (HEAD -> main) two
* 69ce328 one

$ git config --get-regexp '^alias\.'
alias.st status -sb
alias.lg log --oneline --graph --decorate
alias.cm commit -m
alias.unstage restore --staged

What an alias cannot do#

Git does not let an alias replace a command it already has. We set one anyway:

$ git config --global alias.status "log --oneline"
$ git status
On branch main
nothing to commit, working tree clean

The built-in git status still ran. So an alias cannot hide a real command, and a new name is always safe to try.

A shell alias#

A leading ! runs the rest as a shell command:

$ git config --global alias.who '!git config user.name'
$ git who
Ada Example

On Windows that shell is the one bundled with Git. Anything you put after ! is a script you maintain, so keep those few.

Aliases to think twice about#

The danger in an alias is not what it does. It is that it hides what it does, at the exact moment you stop reading.

$ git config --global alias.undo "reset --hard HEAD~1"
$ git undo
HEAD is now at 69ce328 one

That discarded the commit named two. It was recoverable only because the reflog remembers it:

$ git reflog
69ce328 HEAD@{0}: reset: moving to HEAD~1
80e12e5 HEAD@{1}: commit: two

git reset --hard 80e12e5 printed HEAD is now at 80e12e5 two and the commit was back. The lesson is about hiding --hard behind a friendly word.

The same goes for alias.pushf "push --force". It works, and git help pushf politely says 'pushf' is aliased to 'push --force'. A name that sounds routine makes a force-push routine.

If you want one, alias --force-with-lease instead. We amended a commit in one clone while someone else pushed to the same branch from another. The lease noticed:

$ git push --force-with-lease origin main
 ! [rejected]        main -> main (stale info)

A plain git push --force in the same spot went through (+ c3c5e08...2606a78 main -> main (forced update)) and replaced the other person's commit.

A fair rule: alias commands that read (status, log, diff) freely, and think twice before aliasing anything that deletes or overwrites.

On Windows: line endings and sign-in#

Two settings already sit in the system file. We can ask Git where they came from. These are from the Git for Windows install on this machine:

$ git config --system --show-origin --list
file:.../Git/etc/gitconfig	core.autocrlf=true
file:.../Git/etc/gitconfig	core.symlinks=true
file:.../Git/etc/gitconfig	credential.helper=manager

I filtered that to three lines.

core.autocrlf=true converts line endings in both directions: files get Windows endings (CRLF) in your folder and Unix endings (LF) in the repository. You see it as a warning:

$ git -c core.autocrlf=true add notes.txt
warning: in the working copy of 'notes.txt', LF will be replaced by CRLF the next time Git touches it

With core.autocrlf=input or false, the same git add prints nothing.

To see what is really stored, git ls-files --eol lists the line endings in the index (i/) and in your folder (w/). For a Windows-style file in a repository that has a .gitattributes file containing * text=auto:

$ git ls-files --eol win.txt
i/lf    w/crlf  attr/text=auto        	win.txt

The index holds LF, the folder holds CRLF. A .gitattributes file is committed with the repository, so every clone sees the same rule. A config setting exists only on the machine that has it.

credential.helper=manager is Git Credential Manager, the helper that Git for Windows sets up, and it is what remembers your sign-in for HTTPS remotes. On this machine git credential-manager --version printed 2.5.0+d34930736e131ad80e5690e5634ced1808aff3e2. If a push asks for a password every time, check this setting first, with --show-origin so you know which file overrides it.

Try it#

Here is the tour in seven commands. Type them one at a time.

Practice terminal · config and aliases

Type these commands in order, in a practice folder:

  1. List every setting and the file it comes from. Try git config --list --show-origin. The left column is the file; where a name repeats, the narrower scope wins.
  2. Find out where the user.email in effect comes from. Try git config --show-origin user.email. It prints the local file, .git/config.
  3. Make the first push of a new branch set its upstream. Try git config --global push.autoSetupRemote true.
  4. Remove remote branches that no longer exist on every fetch. Try git config --global fetch.prune true.
  5. Make a divergent pull stop instead of guessing. Try git config --global pull.ff only; git config --global pull.rebase true and git config --global pull.rebase false (merge) also answer Git's question.
  6. Add an alias named lg for log --oneline --graph --decorate. Try git config --global alias.lg "log --oneline --graph --decorate".
  7. Remove that alias again. Try git config --global --unset alias.lg.

Two things worth trying: ask for a hint, and answer step 5 with the rebase setting.

user.email is set in your global file and again in a repository's .git/config. In that repository, which value does git config user.email print?
You run git config --global alias.status "log --oneline" and then git status. What happens?
What does fetch.prune true remove?

Challenge: find where it lives

Inside any repository, set pull.rebase for that repository only, then find out which file holds it.

Expected result:

$ git config --show-origin pull.rebase
file:.git/config	true

If it printed a path outside .git, you set it with --global.

What we learned#

  • Settings live in system, global and local files. The narrower scope wins.
  • git config --list --show-origin shows every setting and the file behind it. --show-scope names the scope.
  • -c name=value tries a setting for one command only.
  • pull.ff only or pull.rebase true ends the divergent pull error. push.autoSetupRemote (2.37) sets upstream on the first push. fetch.prune removes pointers to deleted remote branches.
  • rebase.autoSquash places fixups. rebase.updateRefs (2.38) moves stacked branches. merge.conflictStyle zdiff3 (2.35) shows the original text inside a conflict.
  • An alias can add a name but cannot replace a command. Be careful with aliases that hide --hard or --force.
  • On Windows, core.autocrlf and credential.helper come from the install. .gitattributes is the team's rule.

git config --list --show-origin now tells us where every setting lives, and which one wins.

Comments require JavaScript.