dotfiles/docs/config/git.md
Felipe M. 54965e17aa
Add Laugh-Tale and fix three faults that blocked any macOS host
Registering the first Mac surfaced problems that would have hit every
future one, not just this machine:

metapac keys off a raw gethostname(), which on macOS carries the mDNS
".local" suffix, while the registry is keyed by chezmoi's
.chezmoi.hostname, which stops at the first dot. A bare key never
matched and metapac refused to run at all. config.toml.tmpl now emits
each host twice, bare and ".local", so nothing in the registry has to
declare which machines are Macs; the spare key is inert elsewhere.

chezmoi was rewriting ~/Library from 0700 to 0755 on every apply,
loosening a directory macOS deliberately closes. Both levels now carry
the private_ prefix.

dot_gitconfig.tmpl would have destroyed real config on adoption: the
git-lfs filters, seven aliases, diff.noprefix, rerere and the gh
credential helper. All folded in. The lfs block and the GitHub helper
are lookPath-gated like the signing block -- lfs filters run on every
checkout and abort wholesale when the binary is missing, so asserting
them unconditionally breaks a minimal box rather than degrading. gh's
path comes from lookPath, not a hardcoded /opt/homebrew/bin/gh.

Laugh-Tale takes common + a new darwin group, the macOS counterpart to
archlinux.toml, holding only the tooling a backend needs. Its baseline
captures 325 packages, 24 taps and 21 App Store apps -- without it
`metapac clean` would offer to remove every one of them.

Verified on Laugh-Tale: chezmoi status clean after apply, ~/Library
still 0700, `metapac unmanaged` reports nothing, `metapac sync` is a
no-op, and the status line renders correctly under macOS bash 3.2.

Shared groups are still arch-only, so this host draws nothing from
common yet; that gap is documented, not closed.

Claude-Session: https://claude.ai/code/session_01BvpSVPsLrUK4N53LVTofR8
2026-08-25 23:08:01 +02:00

97 lines
3.7 KiB
Markdown

# Git
## Signing uses SSH, not GPG
1Password has no GPG agent and ships only `op-ssh-sign`, so SSH signing is the
only thing it can actually do. Git has supported it since 2.34, and
`git.nakama.town` (Forgejo 14) verifies it, as do GitHub, GitLab and Bitbucket.
The private key never leaves 1Password.
Storing a GPG key in 1Password as a Document is possible, but 1Password cannot
*serve* it to `gpg` — the key would have to be imported into `~/.gnupg` on every
machine, which is exactly what this avoids.
Only the public half is committed, in `.chezmoi.toml.tmpl` under `[data.user]`.
It feeds both the gitconfig and `allowed_signers`, so rotating the key is a
one-line change.
### The signing block is capability-gated
`dot_gitconfig.tmpl` emits it only where the signer binary exists —
`/opt/1Password/op-ssh-sign` on Linux, `/Applications/1Password.app/Contents/
MacOS/op-ssh-sign` on macOS — checked with `stat` at apply time. A machine with
only the 1Password CLI gets a working git with signing off, rather than one that
fails on every commit. No per-host declaration to keep in sync.
### allowed_signers
`~/.config/git/allowed_signers` is managed so `git log --show-signature` can
verify our own commits locally. Without it signing works but every signature
reads as an unknown key.
## Registering the key with a forge
Adding the key to Forgejo or GitHub requires signing a challenge token with it.
The obvious command from their UI assumes a private key on disk, which does not
exist here; the key has to come from the agent instead. In fish:
```fish
set -l sock $HOME/.1password/agent.sock
env SSH_AUTH_SOCK=$sock ssh-add -L | grep 'Git Signing Key' > /tmp/sig.pub
echo -n 'TOKEN' | env SSH_AUTH_SOCK=$sock ssh-keygen -Y sign -n git.nakama.town -f /tmp/sig.pub
```
`env` is required because fish has no `VAR=value cmd` form, and `SSH_AUTH_SOCK`
is not set anywhere. The `grep` matters — the agent holds more than one key, and
an auth key will fail to sign. Change `-n` to match the host asking.
The key must be added as a **signing** key, separately from any auth key.
## Credentials
`git-credential-op` answers credential queries from 1Password at auth time, so no
token is ever written to disk. It is scoped per host via
`credential.<url>.helper`, and reads whatever `GIT_CREDENTIAL_OP_REF` points at —
the script itself never sees a URL.
## Other capability gates
Two more blocks follow the same rule as the signing block — emitted only where
the binary exists, checked with `lookPath` at apply time.
`filter "lfs"` is the dangerous one. Those filters are declared per-repository
work that git runs on *every* checkout, so asserting them on a machine without
`git-lfs` breaks checkouts wholesale rather than degrading. Gating it means the
same gitconfig is safe on a minimal remote box.
The GitHub credential helper is emitted with `gh`'s absolute path from
`lookPath`, not a hardcoded `/opt/homebrew/bin/gh`, so the same template serves
Homebrew on Apple silicon, Homebrew on Intel and a distro package. The empty
`helper =` line before it is deliberate: it resets any helper inherited from the
system config, so the stack cannot silently fall back to a stale keychain entry.
## Aliases
| | |
|---|---|
| `br` | `branch` |
| `cane` | `commit --amend --no-edit` |
| `co` | `checkout` |
| `d` | `diff` |
| `fp` | fetch all remotes with prune, then `pull --ff-only` |
| `last` | `log -1 HEAD` |
| `lo` | `log --oneline -n 10` |
| `pr` | `pull --rebase` |
| `s` | `status` |
`fp` refuses to merge or rebase a diverged branch, matching `dotfiles update`.
## Work identity
When the work laptop arrives, its identity belongs in git's own conditional
include, not a new registry field:
```gitconfig
[includeIf "gitdir:~/work/"]
path = ~/.config/git/config.work
```