# 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)