# Git Configuration
> [!warning] Raw notes
> These are living, personal config notes — raw, frequently out-of-date, and sometimes half-baked. They reflect my setup at some point in time, not a maintained tutorial. Verify against upstream docs before copying. Reach out if something looks broken.
Personal Git setup on macOS. [[delta]] for diffs, [[diffnav]] for paging, [[gitsign]] for Sigstore-based commit signing on the work profile. Config is split across a shared base and per-environment layers.
Config files:
- `~/.config/git/config.base` — shared settings (aliases, delta, diff, merge, gc…)
- `~/.gitconfig` — compatibility shim that includes the base + `~/.gitconfig.local`
- `~/.gitconfig.local` — machine-local overrides (credential helpers, etc.)
## Layered Config Architecture
Git doesn't have first-class profile support, so the setup uses a three-layer include chain:
```
~/.gitconfig
└─ [include] ~/.config/git/config.base (shared defaults)
└─ [include] ~/.gitconfig.local (machine-local overrides)
```
Per-environment gitconfigs (personal vs Datadog) each include the same `config.base` and add their own `[user]` block. The active environment's gitconfig is symlinked to `~/.gitconfig`. `~/.gitconfig.local` is never committed — it holds credential helpers and any per-machine tweaks.
## Identity
Personal:
```gitconfig
[user]
name = Kemal Akkoyun
email =
[email protected]
```
Datadog (work profile):
```gitconfig
[user]
name = Kemal Akkoyun
email =
[email protected]
[commit]
gpgsign = true
```
The Datadog profile adds `gpgsign = true` and includes `~/.config/gitsign/gitconfig` for [[gitsign]] (Sigstore keyless signing — no GPG key needed). Credential helpers are configured in `~/.gitconfig.local`, not committed.
## Core Settings
```gitconfig
[core]
excludesFile = ~/.gitignore_global
pager = delta
[pull]
rebase = true
[init]
defaultBranch = main
[commit]
verbose = true
[push]
autoSetupRemote = true
[rebase]
autoStash = true
[merge]
conflictStyle = zdiff3
[rerere]
enabled = true
autoupdate = true
```
Why these settings:
- `pager = delta` — replaces the built-in less pager with delta's syntax-highlighted diff output
- `pull.rebase = true` — avoids merge commits on pull; keeps history linear
- `commit.verbose = true` — shows the full diff below the commit message editor so you see what you're committing
- `push.autoSetupRemote = true` — sets `--set-upstream` automatically on first push; stops `git push` from complaining on new branches
- `rebase.autoStash = true` — stashes dirty working tree before rebase, unstashes after; less friction on mid-work rebases
- `merge.conflictStyle = zdiff3` — shows the common ancestor base in conflict markers (three-way diff); makes conflicts much easier to resolve
- `rerere.enabled = true` — records how you resolved each conflict and replays the same resolution next time; `autoupdate` stages the replayed fixes automatically
## Diff Settings
```gitconfig
[diff]
algorithm = histogram
colorMoved = plain
mnemonicPrefix = true
[interactive]
diffFilter = delta --color-only --diff-so-fancy
[delta]
navigate = true
side-by-side = true
line-numbers = true
[pager]
diff = diffnav
show = diffnav
```
- `diff.algorithm = histogram` — better at detecting moved blocks than the default Myers algorithm; fewer spurious conflicts
- `diff.colorMoved = plain` — highlights moved lines in a different color (not just added/removed) so you can spot refactors
- `diff.mnemonicPrefix = true` — changes `a/` / `b/` prefixes to `i/` (index) / `w/` (worktree) / `c/` (commit) — actually meaningful
- `delta` — syntax-highlighted side-by-side diffs. `navigate = true` enables `n`/`N` to jump between hunks
- `pager.diff = diffnav` — [[diffnav]] wraps delta output in a navigable TUI; `show` uses it for `git show` too
## Branch and Tag Sorting
```gitconfig
[branch]
sort = -committerdate
[tag]
sort = -version:refname
[column]
ui = auto
```
- `branch.sort = -committerdate` — `git branch` lists most recently active branches first instead of alphabetically
- `tag.sort = -version:refname` — `git tag` sorts semantically (v1.10 after v1.9, not before) and newest first
- `column.ui = auto` — displays output in columns when stdout is a terminal (e.g. `git branch` looks like `ls`)
## Fetch and GC
```gitconfig
[fetch]
prune = true
pruneTags = true
[gc]
auto = 256
pruneExpire = 30.days.ago
reflogExpire = 30.days.ago
reflogExpireUnreachable = 7.days.ago
```
- `fetch.prune = true` — removes remote-tracking branches that no longer exist on the remote after every fetch
- `fetch.pruneTags = true` — same for tags; keeps local tag list in sync with remote
- `gc.auto = 256` — triggers auto-GC every 256 loose objects instead of the default 6700; more frequent, smaller GC runs
## Conventional Commit Aliases
The `[alias]` block has two kinds of entries:
**Simple shorthands:**
```gitconfig
[alias]
co = checkout
br = branch
st = status
```
**Conventional commit type aliases** — each is a shell function that builds a conventional commit message with optional `--scope` / `--attention` flags:
```gitconfig
feat = "!a() { ... git commit -m \"feat${_scope:+(${_scope})}${_attention}:${_message}\"; }; a"
fix = "!a() { ... git commit -m \"fix${_scope:+(${_scope})}${_attention}:${_message}\"; }; a"
# also: build, chore, ci, docs, perf, refactor, rev, style, test, wip
```
Usage:
```bash
git feat "add histogram diff algorithm"
git fix -s core "stop delta pager eating first line"
git chore -s deps -a "drop unmaintained rerere plugin"
# -s sets scope → fix(core): ...
# -a adds ! → fix(core)!: ... (breaking change)
```
**Remote URL helpers:**
```gitconfig
url = ! bash -c 'git config --get remote.origin.url | sed ...'
upstream-url = ! bash -c 'git config --get remote.upstream.url | sed ...'
```
These print the HTTPS browser URL for `origin` / `upstream` — handy for copying into a browser or a PR description.
**Tag management:**
```gitconfig
list-local-tags = !git tag -l | grep -v "$(git ls-remote --tags upstream ...)"
remove-local-tags = !git tag -l | grep -v "$(git ls-remote --tags upstream ...)" | xargs git tag -d
```
Useful when your local tag list drifts from upstream (common when working on forks).
## Links
- [git-delta](https://dandavison.github.io/delta/) — syntax-highlighting pager
- [diffnav](https://github.com/dlvhdr/diffnav) — TUI wrapper for delta
- [gitsign](https://github.com/sigstore/gitsign) — Sigstore keyless commit signing
- [git rerere docs](https://git-scm.com/docs/git-rerere)
- [git diff algorithms](https://luppeng.wordpress.com/2020/10/10/when-to-use-each-of-the-git-diff-algorithms/) — histogram vs Myers vs patience
## Field notes from agent sessions (2026-11-10)
These notes come from agent sessions that debugged git configuration problems. They add facts about the config chain, global config leftovers, environment overrides, and per-remote identity that the sections above do not state.
- An explicit `core.excludesFile` makes the conventional `~/.gitignore_global` dead in every repository on the machine. Git reads only the configured path. The symptom looks specific to one repository, but the cause is global config. `git config --get core.excludesFile` shows the effective file before you blame a repository `.gitignore`.
- Git reads the full config chain at startup. A sandbox that blocks `open(2)` on any file in that chain makes git exit 128 before any operation runs.
- A global `core.hooksPath` leftover can point every repository commit hook at a different project worktree. Commit hooks can run silently from another project.
- In sandboxed test scripts, override a blocked global config with a wrapper on `PATH` that sets `GIT_CONFIG_NOSYSTEM=1` and an empty `HOME`.
- `GIT_CONFIG_COUNT` with `GIT_CONFIG_KEY_n` and `GIT_CONFIG_VALUE_n` injects config such as `safe.directory` and identity into git without changes to the environment config files.
- `GIT_CONFIG_GLOBAL=/dev/null git push <https-url>` with the gh credential helper bypasses a global `insteadOf` rewrite when the SSH signing agent is locked. `--force-with-lease` still applies.
- A work-managed global include applies the work identity and signing key to every repository, personal repositories included. `includeIf hasconfig:remote.*.url` blocks placed after the dominant include give personal repositories their own identity and allowed-signers file without wrapper scripts.
### Observations
- [git] An explicit `core.excludesFile` makes `~/.gitignore_global` dead in every repository. `git config --get core.excludesFile` shows the effective file before you blame a repository `.gitignore`. #config (source: agent sessions, 2026-07)
- [git] A sandbox that blocks `open(2)` on any file in the config chain makes git exit 128 before any operation runs. #sandbox (source: agent sessions, 2026-05)
- [git] A global `core.hooksPath` leftover can point commit hooks at a different project worktree. #config (source: agent sessions, 2026-11)
- [git] A wrapper on `PATH` that sets `GIT_CONFIG_NOSYSTEM=1` and an empty `HOME` overrides a blocked global config in sandboxed test scripts. #sandbox (source: agent sessions, 2026-04)
- [git] `GIT_CONFIG_COUNT` with `GIT_CONFIG_KEY_n` and `GIT_CONFIG_VALUE_n` injects `safe.directory` and identity without changes to config files. #testing (source: agent sessions, 2026-10)
- [git] `GIT_CONFIG_GLOBAL=/dev/null git push <https-url>` with the gh credential helper bypasses a global `insteadOf` rewrite. `--force-with-lease` still applies. #workflow (source: agent sessions, 2026-10)
- [git] `includeIf hasconfig:remote.*.url` blocks placed after the dominant include give personal repositories their own identity and allowed-signers file without wrapper scripts. #identity (source: agent sessions, 2026-10)