G4 · Part 1 · Foundations
Setup and configuration
Ten minutes of setup per machine: who you are, how you log in, and the defaults Git should have shipped with.
09On this page
Git works straight after install, but several of its defaults
exist only for backwards compatibility. A few minutes of setup
saves you from commits under the wrong email, a password prompt
on every push, CRLF noise in every diff, and a
git pull that makes merge commits you never asked
for. Do it once per machine, keep it in your dotfiles, and forget
about it.
Where settings live
Every setting is a section.key = value pair, and the
same key can be set in several files. Git reads them in a fixed
order, and the last value it reads wins. That is
how a repository can override your personal defaults, and how a
single command can override everything.
extensions.worktreeConfig, and matters when you use
git worktree.
The system file's path depends on how Git was installed: a
Homebrew Git on Apple silicon reads
/opt/homebrew/etc/gitconfig, and Git for Windows keeps
its own under the install folder. Global has two possible
homes. If both exist, Git reads ~/.config/git/config
first and then ~/.gitconfig. When a setting behaves
oddly, stop guessing and ask Git where each value came from:
$ git config --list --show-origin --show-scope
system file:/opt/homebrew/etc/gitconfig credential.helper=osxkeychain
global file:/Users/aisha/.gitconfig user.name=Aisha Khan
global file:/Users/aisha/.gitconfig user.email=aisha@hey.com
global file:/Users/aisha/.gitconfig-work user.email=aisha@corp.dev
local file:.git/config core.bare=false
local file:.git/config remote.origin.url=git@github.com:acme/api.git
$ git config --show-origin --get user.email
file:/Users/aisha/.gitconfig-work aisha@corp.dev
The output is tab-separated; it is lined up here for reading.
Notice user.email appears twice. The second value
wins, and --show-origin shows it came from an
included file, covered below. Git 2.46 added verb-style
subcommands: git config list,
git config get user.email and
git config set user.email you@example.com do the same
as the older flag forms, which still work. Keys are
case-insensitive (Git prints them in lower case), and a value can
be removed with git config unset --global key (or the
older git config --global --unset key). Examples on
this site use the older forms because they work on every version
you are likely to meet.
The first five minutes
git config --global user.name "Aisha Khan"
git config --global user.email "aisha@hey.com"
git config --global init.defaultBranch main
git config --global core.editor "code --wait"
Identity. Name and email are copied into every
commit you make. They are not a login; Git does not check them at
all. GitHub and GitLab link a commit to your account by matching
the email against the addresses on your profile, so use one you
have added there (or the host's private noreply
address if you do not want your real email public). Commits made
with the wrong email stay that way. You can fix the last one with
git commit --amend --reset-author --no-edit, but older
ones need a history rewrite. Setting
user.useConfigOnly true stops Git from guessing an
identity from your machine's host name, so a missing email becomes
an error instead of aisha@aishas-macbook.local.
Default branch. init.defaultBranch
(Git 2.28 and later) names the first branch in every new
repository. Without it, Git still creates master,
and current versions print a long hint on every
git init warning that the default will change to
main in Git 3.0. Setting it once silences the hint
and makes every machine agree.
Editor. Git opens an editor for commit messages,
interactive rebases and some merges. A GUI editor must be told to
wait until you close the file, or Git sees it exit at
once and aborts with an empty message: code --wait,
subl -w, zed --wait. Terminal editors
need no flag: vim, nano. The
GIT_EDITOR environment variable beats the setting,
which beats VISUAL and then EDITOR.
Settings that should have been defaults
git config --global pull.rebase true
git config --global push.autoSetupRemote true
git config --global rerere.enabled true
git config --global fetch.prune true
git config --global merge.conflictStyle zdiff3
git config --global diff.algorithm histogram
git config --global rebase.autoStash true
git config --global rebase.updateRefs true
git config --global commit.verbose true
git config --global branch.sort -committerdate
git config --global help.autocorrect prompt
| Setting | What changes |
|---|---|
pull.rebase true | git pull rebases your local commits onto the fetched ones instead of creating a "Merge branch 'main' of …" commit. Teams that prefer to decide each time use pull.ff only, which refuses anything but a fast-forward. |
push.autoSetupRemote true | The first git push on a new branch creates the remote branch and sets the upstream. No more --set-upstream origin error. Git 2.37 and later. |
rerere.enabled true | "Reuse recorded resolution". Git remembers how you resolved a conflict and replays it when the same conflict appears again, for example on every rebase of a long-lived branch. |
fetch.prune true | Deletes origin/* branches that were deleted on the server, so your branch list stops growing forever. |
merge.conflictStyle zdiff3 | Conflict markers also show the merge base, the original text both sides started from. This makes most conflicts obvious. Git 2.35 and later; use diff3 on older versions. |
diff.algorithm histogram | Usually more readable diffs when code is moved or blocks repeat. |
rebase.autoStash true | Stashes uncommitted changes before a rebase and restores them after, instead of refusing to start. |
rebase.updateRefs true | When you rebase a stack of branches, the branches in the middle of the stack move too. Git 2.38 and later. |
commit.verbose true | Shows the staged diff under the message in the editor. |
branch.sort -committerdate | git branch lists the most recently used branches first. |
help.autocorrect prompt | git stauts asks "Run 'status' instead?" rather than just failing. |
Aliases
An alias is a new Git subcommand made of an existing one plus arguments. Start with a handful you will really type:
git config --global alias.st "status -sb"
git config --global alias.lg "log --graph --format='%C(auto)%h%d %s %C(dim)%an, %ar'"
git config --global alias.last "log -1 HEAD --stat"
git config --global alias.unstage "restore --staged"
git config --global alias.amend "commit --amend --no-edit"
git config --global alias.gone "!git branch -vv | grep ': gone]' | awk '{print \$1}'"
An alias starting with ! runs as a shell command from
the top of the repository, which is how gone above
can use a pipe to list local branches whose remote branch has
been deleted. Aliases cannot replace built-in commands:
alias.status is silently ignored. Run
git config --get-regexp '^alias\.' to list the ones
you have.
Work and personal identities with includeIf
Many people have one laptop and two identities: a work email for
company repositories and a personal one for everything else.
Setting the email per repository works until you forget once.
includeIf makes it automatic, based on where the
repository lives on disk.
$ cat ~/.gitconfig
[user]
name = Aisha Khan
email = aisha@hey.com
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
$ cat ~/.gitconfig-work
[user]
email = aisha@corp.dev
[core]
sshCommand = ssh -i ~/.ssh/id_ed25519_work
$ cd ~/work/api && git config user.email
aisha@corp.dev
$ cd ~/code/blog && git config user.email
aisha@hey.com
[user], its email is read later and
wins. Put includeIf blocks at the end of the file.
The rules that catch people out:
-
The trailing slash matters.
gitdir:~/work/matches every repository at any depth under~/work. Without the slash it matches only a repository whose.gitis exactly that path. -
Order matters. An include placed before your
default
[user]block is overridden by it. -
It only applies inside a repository. Outside
one, such as when you run
git config user.emailin your home folder, you see the default. -
Other conditions exist.
gitdir/i:is case-insensitive,onbranch:release/**matches the current branch, andhasconfig:remote.*.url:git@github.com:acme/**(Git 2.36 and later) matches on the remote URL, which works even when work repositories are not in one folder.
Authentication: SSH or HTTPS
The remote URL decides how you log in.
git@github.com:acme/api.git uses SSH;
https://github.com/acme/api.git uses HTTPS. Either is
fine, and you can switch at any time with
git remote set-url origin <url>. Note that
GitHub has not accepted account passwords for Git over HTTPS since
2021. The "password" is a personal access token or an OAuth
login.
ssh and its agent; HTTPS hands it to a credential
helper. That is why fixing a login problem is usually outside
Git's own settings.
SSH: one key per machine
$ ssh-keygen -t ed25519 -C "aisha@hey.com"
$ ssh-add ~/.ssh/id_ed25519
$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG4r... aisha@hey.com
$ ssh -T git@github.com
Hi aisha! You've successfully authenticated, but GitHub does not provide shell access.
Paste the .pub line into your account's SSH keys
page; the private file never leaves your machine. Protect it with
a passphrase and let ssh-agent (or the macOS keychain,
via UseKeychain yes in ~/.ssh/config)
remember it. If you need two GitHub accounts on one machine, give
each its own key. Either use core.sshCommand in the
work include file, as shown earlier, or define a host alias in
~/.ssh/config such as Host github-work
with its own IdentityFile, and use
git@github-work:acme/api.git as the remote.
HTTPS: let a credential helper hold the token
| Helper | Where | Stores the secret in |
|---|---|---|
osxkeychain | macOS | The login keychain. Often preset in the system config. |
manager | Windows, also macOS and Linux | Git Credential Manager: OS secure storage, with browser-based OAuth sign-in and 2FA support. |
libsecret | Linux desktops | GNOME Keyring or KWallet. |
cache --timeout=3600 | Anywhere | Memory only, forgotten after the timeout. |
store | Anywhere | ~/.git-credentials as plain text. Avoid it. |
Set one with git config --global credential.helper
osxkeychain. If you use the GitHub CLI,
gh auth login followed by gh auth setup-git
configures a helper that reuses the CLI's login. When a token
expires or is revoked, the helper keeps offering the stale one;
remove it from the keychain (or run
git credential reject) and the next push will ask
again.
Since Git 2.34 you can sign commits with an SSH key instead of
GPG: set gpg.format ssh,
user.signingKey ~/.ssh/id_ed25519.pub and
commit.gpgSign true. Add the same public key to your
host as a signing key and commits show as verified.
Signing proves who made a commit; the email field alone proves
nothing.
A repository's own .git/config can run programs:
core.fsmonitor, core.sshCommand, hooks
and filters are all commands. Since Git 2.35.2, Git refuses to
read a repository owned by another user, with
fatal: detected dubious ownership in repository,
so a shared folder or a mounted volume cannot quietly run
someone else's settings as you. In a CI container where the
checkout belongs to a different user, trust exactly that path
with git config --global --add safe.directory /work/app
rather than '*'. Also note what config never
travels: a clone copies no .git/config and no
hooks, which is why team rules belong in committed files such
as .gitattributes and on the server, not in
anyone's local config. On shared or build machines,
transfer.fsckObjects true makes fetch and receive
check every incoming object and reject corrupt or malformed
ones.
.gitattributes: per-path rules that travel with the repo
Your config is personal. .gitattributes is committed,
so every clone gets the same behaviour. Each line is a path
pattern followed by attributes.
* text=auto
*.sh text eol=lf
*.bat text eol=crlf
*.png binary
*.pdf binary
*.jpg diff=exif
*.go diff=golang
*.md diff=markdown
package-lock.json -diff linguist-generated=true
dist/** linguist-generated=true
vendor/** linguist-vendored
docs/** linguist-documentation
CHANGELOG.md merge=union
/.github export-ignore
/tests export-ignore
.gitattributes export-ignore
Line endings. * text=auto tells Git
to detect text files and store them with LF in the
repository, converting on checkout if needed. eol=lf
forces LF in the working directory too, which shell
scripts need even on Windows, and eol=crlf does the
reverse for batch files. This beats the per-user
core.autocrlf setting because it does not depend on
every contributor configuring their machine. After adding these
rules to an existing project, run
git add --renormalize . and commit, so files already
stored with CRLF are fixed once, in one commit.
Binary and diff. binary is shorthand
for -diff -merge -text: no line-ending conversion, no
text diff, no attempt to merge. -diff alone keeps a
huge generated file like a lock file out of your diffs ("Binary
files differ") while still treating it as text. A
diff= driver changes how diffs are shown. The built-in
ones (golang, python,
java, markdown and others) give better
hunk headers, so a diff says which function it is in. A custom
driver can convert a binary to text first:
git config --global diff.exif.textconv exiftool makes
photo diffs show changed metadata.
Linguist. The linguist-* attributes
are read by GitHub, not Git. linguist-generated
collapses the file in pull request diffs and leaves it out of the
language statistics; linguist-vendored and
linguist-documentation also drop paths from the
statistics.
Merge and export. merge=union keeps
lines from both sides instead of reporting a conflict. That suits
an append-only file like a changelog, and is wrong for code.
export-ignore leaves paths out of
git archive, and so out of the source tarballs GitHub
builds for releases.
$ git check-attr -a deploy.sh logo.png package-lock.json
deploy.sh: text: set
deploy.sh: eol: lf
logo.png: binary: set
logo.png: diff: unset
logo.png: merge: unset
logo.png: text: unset
package-lock.json: diff: unset
package-lock.json: text: auto
package-lock.json: linguist-generated: true
A global gitignore for your machine's junk
A project's .gitignore should list what the
project produces: node_modules/,
dist/, .env. Files produced by
your operating system and editor belong in your own
global ignore file, so nobody has to add .DS_Store
to every repository on your behalf.
$ mkdir -p ~/.config/git
$ printf '.DS_Store\nThumbs.db\n.idea/\n*.swp\n.vscode/\n' > ~/.config/git/ignore
$ git check-ignore -v .DS_Store
/Users/aisha/.config/git/ignore:1:.DS_Store .DS_Store
~/.config/git/ignore is read automatically. To use
another file, point core.excludesFile at it. For
patterns that apply to one clone only, such as a scratch folder
you keep inside a project, use .git/info/exclude; it
works like .gitignore but is never committed.
Verify your setup
Run these once on a new machine. Each one answers a question you would otherwise discover the hard way.
$ git --version
git version 2.53.0
$ git config user.name && git config user.email
Aisha Khan
aisha@hey.com
$ git config --show-origin --get init.defaultBranch
file:/Users/aisha/.gitconfig main
$ git var GIT_EDITOR
code --wait
$ ssh -T git@github.com
Hi aisha! You've successfully authenticated, but GitHub does not provide shell access.
$ git ls-remote origin HEAD
1b06427a8c7d8fe438acd53c39008641a3cff92e HEAD
Check the email inside a work folder and a personal folder if you
use includeIf. git ls-remote is the
cheapest proof that authentication works: it lists the remote's
refs without changing anything. When something still fails,
GIT_TRACE=1 git fetch shows what Git runs, and
GIT_SSH_COMMAND="ssh -v" git fetch shows which SSH
key is offered and why it is refused.
Put ~/.gitconfig, your include files and
~/.config/git/ignore in a dotfiles repository. A new
laptop then gets your identity, aliases and defaults in one
clone. Keep secrets out of it: tokens belong in the credential
helper and keys in ~/.ssh, never in config.
Finished reading?
0 of 18 chapters read in this series