# Make Git Yours

Canonical: <https://mnixblog.netlify.app/make-git-yours-config-and-aliases/>

Config Scopes, Safe Defaults, and Aliases Worth Having

You run `git pull` and Git answers like this:

```console
$ 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:

```bash
git config --list --show-origin
```


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

> **Note:**
> Everything here ran on Git 2.45.2 on Windows 11, in Git Bash. Where a setting needs a newer Git, the text says so. In the output I replaced long scratch folder names with `...`.

## Where a setting lives

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

| Scope | Flag | File | Applies to |
| --- | --- | --- | --- |
| system | `--system` | `etc/gitconfig` inside the Git install | every user on the machine |
| global | `--global` | `.gitconfig` in your home folder | every repository you use |
| local | `--local` (the default when you write) | `.git/config` in the repository | that 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:

```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:

```bash
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`:

```console
$ 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:

```console
$ 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:

```console
$ 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:

```console
$ 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:

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


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

```bash
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:

```console
$ 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:

```console
$ 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:

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


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

```console
$ 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:

```console
$ 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:

```console
$ 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:

```console
$ 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](https://mnixblog.netlify.app/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:

```console
$ 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`:

```text
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

```bash
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.

```bash
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:

```console
$ 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:

```console
$ 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:

```console
$ 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.

```console
$ 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:

```console
$ 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:

```console
$ 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:

```console
$ 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:

```console
$ 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`:

```console
$ 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`.

[Try the interactive Git practice terminal on the canonical article](https://mnixblog.netlify.app/make-git-yours-config-and-aliases/).


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

**Question:** `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?

1. The one from `.git/config`
2. The one from the global file
3. Both, one per line

**Answer:** 1. The one from `.git/config`

> **Explanation:** The narrower scope wins. `git config --show-scope --show-origin user.email` prints `local file:.git/config` and the value.


**Question:** You run `git config --global alias.status "log --oneline"` and then `git status`. What happens?

1. Git runs the built-in `status` and ignores the alias
2. Git runs `git log --oneline`
3. Git refuses with an error

**Answer:** 1. Git runs the built-in `status` and ignores the alias

> **Explanation:** Git does not let an alias replace an existing command. In our run, `git status` printed `On branch main` as usual.


**Question:** What does `fetch.prune true` remove?

1. Remote-tracking pointers such as `origin/feature/x`, when the branch is gone from the remote
2. Local branches whose remote branch was deleted
3. Commits that nobody references

**Answer:** 1. Remote-tracking pointers such as `origin/feature/x`, when the branch is gone from the remote

> **Explanation:** Only the `origin/...` pointers go. A local branch stays, and `git branch -vv` marks it `[origin/y: gone]`.


**Challenge: find where it lives**

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

**Expected result:**

```console
$ 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.
> **Portable fallback:** The authored description of the demo is preserved; the live demonstration remains on the canonical article. Review: Review the authored description of the demo and keep the canonical link for the live version.

> **Portable fallback:** The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.

> **Portable fallback:** The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.

> **Portable fallback:** The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.

[Join the discussion on the canonical article](https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#ob-discussion)
