This is a reference for me, to carry the config to new machines. Three values are machine-local, everything else is portable as-is.

The file

[user]
    email = <your-email>
    name = Andreas Fertsch-Röver
    signingkey = <gpg-key-id>          # used by commit.gpgsign below
[core]
    preloadindex = true                # parallel stat() on the index, faster status on big repos
    pager = less
    sshCommand = /usr/bin/ssh          # pin the ssh that holds my keys
[fetch]
    prune = true                       # drop local refs for branches deleted upstream
[push]
    default = upstream                 # push to the tracked branch, not to a same-named one
[merge]
    summary = true                     # list the merged commits in the merge message
    verbosity = 1                      # report conflicts, stay quiet otherwise
    ff = false                         # always create a merge commit, so branch history stays visible
[commit]
    gpgsign = true                     # sign every commit
[init]
    defaultBranch = main               # no hint banner on git init
[alias]
    # staging and committing
    all = add . --all
    co = commit
    coa = commit --amend
    coall = !git all && git co         # stage everything, then commit
    wip = !git all && git commit -m 'WIP'   # park the current state to switch context
    undo = reset --soft HEAD~          # drop the last commit, keep its changes staged
    pushf = push --force-with-lease
    cp = cherry-pick

    # moving around
    ch = checkout
    sw = switch
    fsw = !git fetch && git switch     # fetch first, so a fresh remote branch is found
    main = switch main
    omain = !git fetch && git switch --detach origin/main   # throwaway checkout of upstream main
    master = checkout master
    omaster = !git fetch && git switch --detach origin/master   # throwaway checkout of upstream master
    uprebase = pull --rebase --autostash
    upmerge = pull --no-rebase
    up = uprebase
    rom = !git fetch && git rebase origin/main --autostash   # rebase onto current upstream main
    fp = !git fetch --prune --verbose origin '+refs/tags/*:refs/tags/*'   # fetch and refresh all tags
    fa = !git fetch --all --verbose

    # looking
    st = status
    l = log
    last = log -1 HEAD
    lme = "!git log --author=\"$(git config user.name)\""   # only my commits
    ls = log --stat --no-abbrev-commit
    lsm = !git fetch && git log --stat --no-abbrev-commit origin/main...HEAD   # my commits since main
    dch = diff --stat origin/main...HEAD    # the size of the PR I am about to open
    g = grep --break --heading --line-number
    dif = diff --color --color-words --abbrev  # word-level diff, unstaged
    difc = diff --color --color-words --abbrev --cached   # word-level diff, staged
    diffc = diff --color --abbrev --cached     # line-level diff, staged
    glog = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset'   # compact one-line graph
    graph = log --graph --all --decorate --stat --date=iso   # full graph across all branches

    # cleanup
    smb = !git branch --merged | grep -i -v -E \"hotfix|release|main|master\"   # merged branches, minus protected ones
    delm = !git smb | xargs git branch -d   # delete exactly those
    ignore = !git update-index --assume-unchanged     # stop tracking local edits to a tracked file
    unignore = !git update-index --no-assume-unchanged
    ignored = !git ls-files -v | grep ^[a-z]          # what is currently assume-unchanged

    # stashing
    pop = stash pop
    slist = stash list
    stash-unstaged = "!f() { git stash push --staged -m staged; git stash push -u -m rest; git stash pop stash@{1}; }; f"   # stash only the unstaged part

    # worktrees
    wt = worktree
    wtl = worktree list
    wtr = worktree remove
    wta = "!f() { git fetch && git worktree add \"../$1\" \"$1\"; }; f"    # add worktree ../<branch> for <branch>
    wtd = "!f() { git worktree remove \"../$1\" && git branch -d \"$1\"; }; f"   # remove that worktree and the branch
[format]
    # date, full hash, refs; subject and author on the next line
    pretty = format:%C(blue)%ad%Creset %C(yellow)%H%C(green)%d%n%Creset %C(blue)%s %C(magenta) [%an]%Creset%n
[color "branch"]
    current = yellow reverse
    local = yellow
    remote = green
[color "diff"]
    meta = yellow bold
    frag = magenta bold
    old = red
    new = green
[mergetool]
    prompt = false                     # do not ask before launching the merge tool
    keepBackup = false                 # no .orig files left behind

Machine-local, fill on arrival

Three things change per machine. Set them once and never touch the rest:

git config --global user.email you@example.com
git config --global user.signingkey <gpg-key-id>
git config --global core.sshCommand /usr/bin/ssh

core.sshCommand matters when a package manager drops a second ssh binary earlier in PATH than the one holding your keys. Pinning the absolute path removes the guessing. A credential helper is deliberately not in the file: the right value differs on every OS.

Two settings are per-environment rather than per-machine, which is why they stay out of the global file. If a remote serves a self-signed certificate, point git at its CA bundle instead of switching verification off everywhere:

git config --local http.sslCAInfo /path/to/ca-bundle.crt
git config --local http.sslVerify false   # last resort, and only in that one repo

http.sslVerify = false in the global file disables certificate verification for every HTTPS remote on the machine, including the ones you did not think about. Scoped to a single repository it is a known trade-off; scoped globally it is a hole you forget about.

Defaults worth changing

Four settings do more for me than all the aliases combined.

fetch.prune = true

Deletes local remote-tracking refs for branches that are gone upstream. Without it, git branch -r accumulates months of merged feature branches and tab completion turns into archaeology.

push.default = upstream

Pushes to the branch you are actually tracking, not to a same-named branch on the remote. On a detached or renamed local branch this is the difference between a push and an incident.

merge.ff = false

Forces a merge commit even when a fast-forward is possible. The topology then still shows which commits arrived together as one unit, which is what you want when reading history six months later.

init.defaultBranch = main

Stops the hint banner and matches every remote I work with.

Aliases worth a note

Most of them are typing shortcuts and need no explanation. These carry a reason.

wip, undo

A pair. wip commits whatever state you are in so you can switch context; undo is reset --soft HEAD~, which removes that commit again and leaves its changes staged, as if you had never left.

pushf

--force-with-lease, not --force. After a rebase the push aborts if the remote moved in the meantime, instead of overwriting a colleague’s commits.

up, rom

Both carry --autostash, so a dirty working tree no longer blocks a rebase: git stashes it, rebases, and puts it back.

omain

Detaches at origin/main instead of switching to the local branch. For a quick build, bisect, or "does it also break on main", with no local branch to clean up afterwards.

dch

Triple-dot, so it diffs against the merge base. It reports what my PR adds and not what main moved on to.

stash-unstaged

Splits the working tree: it parks the staged part and the rest in two separate stashes, then restores the staged part. For handing over a clean, reviewable change while the half-finished rest waits in stash@{0}.

smb, delm

smb lists merged branches minus main, master, release* and hotfix*; delm deletes exactly that list. Add your own protected names to the grep before running it anywhere that matters.

Warning

ignore is assume-unchanged, which is not gitignore. It tells git to skip stat checks on a tracked file, and it silently loses your local edits on a checkout that touches that file. ignored lists every file currently marked that way, and unignore <file> puts one back under normal tracking.

Worktrees

wta and wtd create and tear down a worktree in a sibling directory named after the branch. They carry the whole parallel-branch workflow I described in My LLM Git Workflow, so I will not repeat it here.

None of this changes what git does. It changes how fast I can read what git says, and that is why the file travels with me.