Merge feature/gitconfig-autopush into develop

This commit is contained in:
bchanot
2026-10-07 18:26:08 +02:00
10 changed files with 490 additions and 20 deletions
+3
View File
@@ -120,3 +120,6 @@ Done: `bin/repo-sync` (gitlab/github/gitea/bitbucket cloud via curl+jq, daily ca
## 2026-10-07 (2) — macOS zsh default, remote-install prompts ## 2026-10-07 (2) — macOS zsh default, remote-install prompts
Done: choose_macos_shell default bash → zsh (Enter, no tty, unknown value). Found: `curl | bash` path never asked USER/EMAIL (stdin = pipe → `[ -t 0 ]` false → login name + empty email, bash, no offers); remote-install.sh now runs install.sh `</dev/tty` when openable. Found: BRANCH default `master`, origin has only `main` (raw URL 404) → `main`. Lint OK, chooser tested 4 values. Branch feature/macos-zsh-default, not merged. Other session: feature/repo-sync open (CLAUDE.md layout already lists repo-sync). Done: choose_macos_shell default bash → zsh (Enter, no tty, unknown value). Found: `curl | bash` path never asked USER/EMAIL (stdin = pipe → `[ -t 0 ]` false → login name + empty email, bash, no offers); remote-install.sh now runs install.sh `</dev/tty` when openable. Found: BRANCH default `master`, origin has only `main` (raw URL 404) → `main`. Lint OK, chooser tested 4 values. Branch feature/macos-zsh-default, not merged. Other session: feature/repo-sync open (CLAUDE.md layout already lists repo-sync).
## 2026-10-07 (3) — gitflow.autopush asked at install, hooksPath fixed, identity bug
Done: `gitconfig` template `[gitflow] autopush = @AUTOPUSH@` + fixed `core.hooksPath = ~/.claude/githooks`; install.sh `resolve_autopush` (existing ~/.gitconfig value via sed → `DOTFILES_GITFLOW_AUTOPUSH` strict, bad value aborts → prompt re-asks exact true/false, Enter = true → true), `render_gitconfig` fail-closed (non-boolean / leaked placeholder = nothing written). Found by challenge: install.sh:688 passed the repo bashrc TEMPLATE to deploy_gitconfig → every install wrote `name = @USER@`; fixed, identity passed directly. `shopt -u patsub_replacement` (bash ≥ 5.2 `&` in names). Oracle harness `.claude/tasks/contracts/check-autopush-render.sh` (12 cases, scratch HOMEs, zero `git config`: that command family is denied to the session). 3 challengers + 1 confirm, verifier ECARTS(1) → CONFORME, security PASS (MEDIUM noted: newline in IDENTITY_* env injects gitconfig lines, pre-existing for rc too). Code commit 9901ec5 on feature/gitconfig-autopush; merge on user signal.
+4
View File
@@ -135,3 +135,7 @@ Port of the Alphalink dotfiles `repo` / `repo-reset` zsh functions, bash + zsh,
- [x] README.md (table + CLI section) + CLAUDE.md layout - [x] README.md (table + CLI section) + CLAUDE.md layout
- [x] shellcheck + bash -n; stub-curl harness per forge (fixtures), live GitLab run - [x] shellcheck + bash -n; stub-curl harness per forge (fixtures), live GitLab run
- [ ] commit on feature branch; registries. No finish without explicit signal. - [ ] commit on feature branch; registries. No finish without explicit signal.
## Feature — gitflow.autopush asked at install, written to ~/.gitconfig (2026-10-07)
Branch: feature/gitconfig-autopush (off develop). Plan: .claude/tasks/plans/2026-10-07-gitconfig-autopush-1734.md
- [ ] /feat run: gitconfig `[gitflow] autopush = @AUTOPUSH@` + fixed `core.hooksPath`; install.sh `resolve_autopush` (exact true/false: existing ~/.gitconfig value via sed → DOTFILES_GITFLOW_AUTOPUSH (bad value aborts) → prompt re-asks → true), deploy_gitconfig takes name/email/autopush (fixes `name = @USER@` deployed from the template path) + fail-closed leak check; README/CLAUDE.md; oracle harness .claude/tasks/contracts/check-autopush-render.sh
@@ -0,0 +1,48 @@
# CONTRACT — gitconfig-autopush
- date: 2026-10-07 | flow: feat | branch: develop → feature/gitconfig-autopush
- status: active
## REQUEST (verbatim — IMMUTABLE)
est-ce qu'on deploi un gitconfig ? Oui peut-on faire en sorte de demander a l'installation si on ajoute git config --global gitflow.autopush false ou git config --global gitflow.autopush true . Au choix a l'installation et mis dnas le gitconfig. d'ailleurs on a bien le user et le mail dnas le gitconfig aui sont bien ceux qu'on configure a l'installation ?
## CLARIFICATIONS
Q: Default when Enter is pressed or no terminal is attached? / A: `true` [gated 2026-10-07] — confirmed by the user's correction after the second message ("non pardon je voulais dire defaut true").
Q: Env preset name? / A: `GITFLOW_AUTOPUSH` [gated 2026-10-07] — SUPERSEDED by the user spec: `DOTFILES_GITFLOW_AUTOPUSH`.
Q: install.sh:688 passes the repo template to deploy_gitconfig, so ~/.gitconfig gets `name = @USER@`; fix in this run? / A: yes, deploy_gitconfig takes the resolved name/email/autopush [gated 2026-10-07]
Q: redeploy drops `core.hooksPath` (set by `make link`)? / A: warn + note [gated 2026-10-07] — SUPERSEDED by the user spec: fixed template line `hooksPath = ~/.claude/githooks` in `[core]`, git expands `~`, the installer neither creates nor checks the directory.
Q: how is an existing value read, given `git config … gitflow.*` is denied to this session? / A: sed over ~/.gitconfig, exact true/false reused silently; no git config access in installer or oracle [gated 2026-10-07]
USER SPEC (second message, 2026-10-07, verbatim constraints) [gated 2026-10-07]:
- Question at install: « Push automatique des commits par les hooks gitflow sur cette machine ? [true/false] » default true (corrected from the spec's "défaut : false" by the user's next message). Rendered in English like the other install prompts; wording open to change at the diff review.
- Only the exact values `true` and `false` are accepted; anything else re-asks; empty → `true`.
- The render fails explicitly if `@AUTOPUSH@` remains in the final content (grep after render); nothing written.
- Non-interactive: `DOTFILES_GITFLOW_AUTOPUSH=true|false`, default true. (Orchestrator choice, flagged for the diff review: any other preset value aborts the install at the identity step, before any file is touched.)
- Keep the existing backup `~/.gitconfig.backup-<timestamp>`; no other template section touched.
- No test suite / no --dry-run exists: the oracle harness carries the two requested cases (rendered @AUTOPUSH@, leaked placeholder = failure).
- The user sees the full diff before anything is committed.
## ACCEPTANCE CRITERIA
1. `gitconfig` template: `[gitflow]` section with `autopush = @AUTOPUSH@`, and `hooksPath = ~/.claude/githooks` inside `[core]`; other sections unchanged except the header comment.
CHECK: grep -q '^\[gitflow\]' gitconfig && grep -q '^ autopush = @AUTOPUSH@' gitconfig && grep -q '^ hooksPath = ~/.claude/githooks' gitconfig && echo GITCONFIG_TEMPLATE_OK
EXPECT: GITCONFIG_TEMPLATE_OK
EVIDENCE: MET exit=0 marker-found :: GITCONFIG_TEMPLATE_OK
2. `install.sh` resolves the push mode: an exact `true`/`false` already in ~/.gitconfig wins silently; else `DOTFILES_GITFLOW_AUTOPUSH` (exact true/false; any other value aborts with a message); else a tty prompt accepting only `true`/`false`, re-asking otherwise, Enter = `true`; else `true`.
3. Oracle harness: rendered file carries the resolved autopush, name, email, hooksPath and no placeholder; a leaked `@AUTOPUSH@` makes deploy_gitconfig fail without writing; existing value beats the preset; strict preset; prompt loop; empty email skip; idempotent re-run.
CHECK: bash .claude/tasks/contracts/check-autopush-render.sh
EXPECT: AUTOPUSH_RENDER_OK
EVIDENCE: MET exit=0 marker-found :: AUTOPUSH_RENDER_OK
4. install.sh stays syntactically valid and shellcheck clean.
CHECK: bash -n install.sh && shellcheck install.sh && echo LINT_OK
EXPECT: LINT_OK
EVIDENCE: MET exit=0 marker-found :: LINT_OK
5. README.md (gitconfig row, remote-install question list, install step 6) and CLAUDE.md layout line document the question, the strict values, the env preset, the reuse of an existing value and the fixed hooksPath.
6. Call site passes the resolved identity, never the repo template path.
CHECK: grep -q 'deploy_gitconfig "\$identity_name" "\$identity_email" "\$autopush"' install.sh && ! grep -q 'deploy_gitconfig "\$SCRIPT_DIR' install.sh && echo CALLSITE_OK
EXPECT: CALLSITE_OK
EVIDENCE: MET exit=0 marker-found :: CALLSITE_OK
7. `.githooks/post-commit` and `.githooks/post-merge` are not part of the change (pre-existing session-hook refresh).
CHECK: git diff --cached --name-only | grep -q '^\.githooks/' && exit 1; echo HOOKS_UNTOUCHED
EXPECT: HOOKS_UNTOUCHED
EVIDENCE: MET exit=0 marker-found :: HOOKS_UNTOUCHED
## FILE SCOPE
gitconfig, install.sh, README.md, CLAUDE.md, .claude/tasks/contracts/check-autopush-render.sh (oracle harness, LRN-011 pattern)
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# Oracle for the contract: exercises install.sh functions on scratch HOMEs under
# mktemp; never reads or writes the real ~/.gitconfig and never calls git config.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
SCRIPT_DIR="$PWD"
fail() { echo "FAIL: $*" >&2; exit 1; }
fresh_home() { HOME="$(mktemp -d)"; export HOME; }
guard_home() {
case "$HOME" in /tmp/*|/private/*|/var/*) ;; *) fail "refusing HOME=$HOME" ;; esac
}
has() { grep -qF -- "$2" "$1" || fail "$1 lacks: $2"; }
funcs="$(mktemp)"
for fn in ask_autopush resolve_autopush render_identity_template \
render_gitconfig deploy_gitconfig; do
sed -n "/^$fn() {/,/^}/p" install.sh >> "$funcs"
done
source "$funcs"
shopt -u patsub_replacement 2>/dev/null || true
unset DOTFILES_GITFLOW_AUTOPUSH
# Case A: render
fresh_home; guard_home
[ "$(DOTFILES_GITFLOW_AUTOPUSH=false resolve_autopush </dev/null)" = false ] \
|| fail "A: preset false"
deploy_gitconfig x x@y false >/dev/null
for l in 'autopush = false' 'name = x' 'email = x@y' 'hooksPath = ~/.claude/githooks'; do
has "$HOME/.gitconfig" "$l"
done
! grep -qE '@(USER|EMAIL|AUTOPUSH)@' "$HOME/.gitconfig" || fail "A: placeholder left"
# Case I: idempotent re-run in A's HOME
out="$(deploy_gitconfig x x@y false)"
case "$out" in *"already up to date"*) ;; *) fail "I: not skipped: $out" ;; esac
ls "$HOME"/.gitconfig.backup-* >/dev/null 2>&1 && fail "I: backup created"
# Case A2: ampersand
fresh_home; guard_home
deploy_gitconfig 'a & b' x@y true >/dev/null
has "$HOME/.gitconfig" 'name = a & b'
# Case B: leaked placeholder
fresh_home; guard_home
set +e; deploy_gitconfig x x@y '@AUTOPUSH@' 2>"$HOME/err"; rc=$?; set -e
[ "$rc" -ne 0 ] || fail "B: rc 0"
has "$HOME/err" '@AUTOPUSH@'
[ ! -e "$HOME/.gitconfig" ] || fail "B: file written"
# Case C: existing wins over preset
fresh_home; guard_home
printf '[gitflow]\n autopush = true\n' > "$HOME/.gitconfig"
[ "$(DOTFILES_GITFLOW_AUTOPUSH=false resolve_autopush </dev/null)" = true ] || fail "C"
# Case D: defaults
fresh_home; guard_home
[ "$(resolve_autopush </dev/null)" = true ] || fail "D"
# Case E: strict preset
set +e; DOTFILES_GITFLOW_AUTOPUSH=yes resolve_autopush 2>"$HOME/err" </dev/null; rc=$?; set -e
[ "$rc" -ne 0 ] || fail "E: rc 0"
has "$HOME/err" exactly
# Case F: prompt loop
[ "$(printf 'yes\ntrue\n' | ask_autopush 2>"$HOME/err")" = true ] || fail "F1"
has "$HOME/err" exactly
[ "$(printf '\n' | ask_autopush)" = true ] || fail "F2"
# Case G: empty email
fresh_home; guard_home
deploy_gitconfig x "" true 2>"$HOME/err"
[ ! -e "$HOME/.gitconfig" ] || fail "G: file written"
has "$HOME/err" "not written"
# Case H: call site
grep -q 'deploy_gitconfig "\$identity_name" "\$identity_email" "\$autopush"' install.sh \
|| fail "H: call site"
! grep -q 'deploy_gitconfig "\$SCRIPT_DIR' install.sh || fail "H: template path"
# Case J: non-exact existing value
fresh_home; guard_home
printf '[gitflow]\n autopush = off\n' > "$HOME/.gitconfig"
[ "$(resolve_autopush 2>"$HOME/err" </dev/null)" = true ] || fail "J: value"
has "$HOME/err" off
# Case K: refused value
fresh_home; guard_home
set +e; deploy_gitconfig x x@y yes 2>"$HOME/err"; rc=$?; set -e
[ "$rc" -ne 0 ] || fail "K: rc 0"
has "$HOME/err" "not exactly"
[ ! -e "$HOME/.gitconfig" ] || fail "K: file written"
echo AUTOPUSH_RENDER_OK
@@ -0,0 +1,247 @@
# PLAN — gitconfig-autopush (feat) — REVISED v4 (round 1 + user spec + confirmation pass, default true)
Contract: .claude/tasks/contracts/2026-10-07-gitconfig-autopush-1734.md
Branch: feature/gitconfig-autopush (off develop)
## Goal (user spec, verbatim constraints in the contract)
1. `gitconfig` template, section `[core]`: fixed line `hooksPath = ~/.claude/githooks`
(git expands `~` itself for core.hooksPath; no absolute path, no expansion by the
script; the installer neither creates nor checks that directory).
2. install.sh asks at install: automatic push by the gitflow hooks on this machine?
`[true/false]` (default: true — user correction). Answer rendered into a new section
`[gitflow]` / `autopush = @AUTOPUSH@`.
- Only the exact strings `true` and `false` are accepted. Anything else re-asks;
an empty answer gives `true`.
- The render FAILS explicitly (grep after render) if `@AUTOPUSH@` is still in the
final content. A non-boolean value blocks every push on the machine (fail-closed),
so a leaked placeholder would be a silent outage.
- Non-interactive install: env `DOTFILES_GITFLOW_AUTOPUSH=true|false`, default true.
3. Keep the existing backup (`~/.gitconfig.backup-<timestamp>`). Do not touch the other
template sections.
4. Human-gated 2026-10-07 (kept from round 1): fix the identity call site so the deployed
file carries the resolved name/email, not the literal `@USER@`/`@EMAIL@`.
5. The user sees the full diff BEFORE anything is committed (working tree only until then).
## Context (verified)
- `gitconfig` is a template; `render_identity_template` (install.sh:497) replaces
@USER@/@EMAIL@ by bash substitution over every line (comments included).
- `deploy_gitconfig` (install.sh:532) takes an rc PATH and reads USER/EMAIL from it.
BUG: the call site install.sh:688 passes `"$SCRIPT_DIR/$bashrc"` = the repo TEMPLATE
whose exports are `export USER="@USER@"` (bash/bashrc-linux:33-34). The placeholders
are non-empty, the guard passes, the deployed file reads `name = @USER@`.
- Identity resolved at install.sh:584-585 into `identity_name` / `identity_email`, before
package installs (prompts first). EMAIL default "" when no tty and no env.
- Readers (~/.claude/lib/gitflow.sh gitflow_push_mode, post-commit/post-merge hooks):
`git config --bool gitflow.autopush` → false = manual, true/unset = auto, other =
invalid → nothing pushed (fail closed). Only exact `true`/`false` are ever written.
- `core.hooksPath` is a pathname-typed key: git tilde-expands `~/.claude/githooks`.
Today `make link` writes it with `git config --global`; the redeploy dropped it. With
the fixed template line the redeploy keeps it and the file matches the render again.
- This session's permission layer denies every `git config … gitflow.*` command, so the
installer and the oracle must NOT read or write the key through git: an existing value
is read from ~/.gitconfig with sed (same shape as rc_export_value). Trade-off: a value
set in XDG/system/included config is not seen; ~/.gitconfig is the only file written.
- install.sh runs under macOS /bin/bash 3.2 before brew bash exists (LRN-014): plain
string compares only, no `${var,,}`.
- The repo has no test suite and install.sh has no --dry-run: the oracle harness below is
the test, run by the contract gate.
## Checklist
[ ] gitconfig — header comment (lines 1-4) reworded WITHOUT literal placeholder tokens
(today it renders as "bchanot and x@y are replaced at install time"): "Template for
the user-scope ~/.gitconfig, rendered by install.sh. Git never expands $VARS, so the
identity and the gitflow push mode are placeholders filled at install time with the
installer's answers. A repo .git/config still overrides these values for that repo."
In `[core]`, after `excludesfile`, add the fixed line:
hooksPath = ~/.claude/githooks
with a one-line comment above it: "# gitflow hooks (created by `make link` in
claude-config); git expands ~ itself." After `[user]`, add:
[gitflow]
# Push mode of the gitflow hooks: false = manual, you run `git push`;
# true = every commit and merge is pushed. Exact true/false only (fail-closed).
autopush = @AUTOPUSH@
4-space indent like the other keys. No other section touched.
[ ] install.sh — two new functions right after `resolve_identity` (~line 525), tabs:
# Ask the push question on the terminal until the answer is exactly true or false;
# Enter (or EOF) = true. Prints the value.
ask_autopush() {
local answer=""
while :; do
read -rp "Automatic push of commits by the gitflow hooks on this machine? [true/false] (default: true) " answer || true
case "${answer:-true}" in
true|false) printf '%s\n' "${answer:-true}"; return 0 ;;
*) echo "Answer exactly true or false." >&2 ;;
esac
done
}
# Push mode of the gitflow hooks (gitflow.autopush), exact true/false only: the
# readers fail closed on anything else. Never asked twice: a true/false already in
# ~/.gitconfig wins silently (read with sed, like rc_export_value, so a hand-set
# value survives the redeploy; a non-exact spelling is named and re-asked), else
# DOTFILES_GITFLOW_AUTOPUSH (anything but true/false aborts the install here,
# before any file is touched, whatever ~/.gitconfig holds), else the prompt
# on a terminal, else true (today's unset = auto). Prints the value.
resolve_autopush() {
local value="" preset="${DOTFILES_GITFLOW_AUTOPUSH:-}"
case "$preset" in
true|false|"") ;;
*) echo "DOTFILES_GITFLOW_AUTOPUSH='$preset' — must be exactly true or false" >&2; return 1 ;;
esac
if [ -f "$HOME/.gitconfig" ]; then
value="$(sed -n 's/^[[:space:]]*autopush[[:space:]]*=[[:space:]]*//p' "$HOME/.gitconfig" | tail -n 1)"
case "$value" in
true|false) printf '%s\n' "$value"; return 0 ;;
"") ;;
*) echo "gitflow.autopush='$value' in ~/.gitconfig is not exactly true/false — asking again" >&2 ;;
esac
fi
if [ -n "$preset" ]; then printf '%s\n' "$preset"; return 0; fi
if [ -t 0 ]; then ask_autopush; else echo true; fi
}
Order: preset syntax is validated FIRST (a bad preset always aborts, even on a
re-run), then an existing exact value wins, then a valid preset, prompt, default.
Each ≤ 25 logic lines. `read … || true` on EOF → empty → true → loop ends.
[ ] install.sh — after `set -euo pipefail` (line 4) add, with a one-line comment
("bash ≥ 5.2 expands `&` in ${var//pat/rep} replacements: an `&` in a name would
corrupt the rendered identity; no-op on bash 3.2"):
shopt -u patsub_replacement 2>/dev/null || true
[ ] install.sh — main sequence, right after identity_email (line 585):
autopush="$(resolve_autopush)"
(set -e: a `return 1` from an invalid preset aborts the install right here.)
[ ] install.sh — `deploy_gitconfig` becomes `deploy_gitconfig <name> <email> <autopush>`
(no rc path, no rc_export_value call):
local name="$1" email="$2" autopush="$3" rendered backup
if [ -z "$name" ] || [ -z "$email" ]; then
echo "Name or email empty — skipping ~/.gitconfig (push mode $autopush not written)" >&2
return 0
fi
rendered="$(render_identity_template "$SCRIPT_DIR/gitconfig" "$name" "$email")"
case "$autopush" in true|false) ;; *)
echo "gitconfig render refused: push mode '$autopush' is not exactly true/false — nothing written" >&2
return 1 ;;
esac
rendered="${rendered//@AUTOPUSH@/$autopush}"
case "$rendered" in *@AUTOPUSH@*)
echo "gitconfig render failed: @AUTOPUSH@ left in the output — nothing written" >&2
return 1 ;;
esac
(no pipe: `printf | grep -q` under pipefail can read SIGPIPE as "no leak")
… cmp/skip, backup, write unchanged …
echo "Deploying gitconfig to ~/.gitconfig ($name <$email>, autopush=$autopush)"
Header comment rewritten: $1 $2 = the identity rendered into the rc, $3 = push mode;
one line on the two fail-closed checks. If > 25 logic lines, extract
`render_gitconfig <name> <email> <autopush>` (render + substitution + grep).
[ ] install.sh:688 call site → `deploy_gitconfig "$identity_name" "$identity_email" "$autopush"`
comment: "User-scope git config, same identity as the rc just rendered."
[ ] README.md:31 gitconfig row — "`@USER@`, `@EMAIL@` and `@AUTOPUSH@` (gitflow push
mode) are filled at install with the installer's answers; `core.hooksPath` points at
the gitflow hooks `make link` creates."
[ ] README.md:50 — "(identity, push mode, macOS shell, offers)".
[ ] README.md:70 step 6 — replace "with the same `USER` / `EMAIL`" by "with the same name
and email", then add: the installer asks once whether the gitflow hooks push every
commit and merge (`true`/`false` exactly, Enter = `true`); a `true`/`false` already
in `~/.gitconfig` is reused without asking and survives the redeploy;
`DOTFILES_GITFLOW_AUTOPUSH=true|false` presets it for a non-interactive install (no
terminal and no preset → `true`; any other preset aborts the install); the render
refuses to write a file where `@AUTOPUSH@` leaked, since a non-boolean value blocks
every push. To switch later: `git config --global gitflow.autopush true|false`.
[ ] CLAUDE.md:25 — `gitconfig user-scope ~/.gitconfig template, @USER@/@EMAIL@/@AUTOPUSH@
(gitflow push mode, exact true/false) filled at install; core.hooksPath fixed`
[ ] .claude/tasks/contracts/check-autopush-render.sh — oracle harness (LRN-011 pattern):
`set -euo pipefail`; `cd "$(git rev-parse --show-toplevel)"` (the only git call, no
config access); `SCRIPT_DIR="$PWD"`. Extract ask_autopush, resolve_autopush,
render_identity_template, deploy_gitconfig (and render_gitconfig if extracted) from
install.sh via `sed -n '/^fn() {/,/^}/p'` into `$(mktemp)` and source it.
`fresh_home() { HOME="$(mktemp -d)"; export HOME; }` before every case; guard
`case "$HOME" in /tmp/*|/private/*|/var/*) ;; *) fail "refusing HOME=$HOME";; esac`
before any deploy call. No rm of temp dirs (never rm -rf through a variable).
`fail() { echo "FAIL: $*" >&2; exit 1; }`; every assertion goes through it.
stderr captures go to `"$HOME/err"` (the scratch HOME), never a relative path in
the repo. Header comment: "Oracle for the contract: exercises install.sh functions
on scratch HOMEs under mktemp; never reads or writes the real ~/.gitconfig and never
calls git config." If the permission layer refuses a step of this harness, the
refusal is reported with its rule and the harness is handed to the user to run; it
is never reworked to get around the rule.
Case A (render): fresh_home; DOTFILES_GITFLOW_AUTOPUSH=false </dev/null →
resolve_autopush = false; deploy_gitconfig x x@y false → file has
`autopush = false`, `name = x`, `email = x@y`, `hooksPath = ~/.claude/githooks`,
and `! grep -qE '@(USER|EMAIL|AUTOPUSH)@'`.
Case A2 (ampersand): fresh_home; deploy_gitconfig 'a & b' x@y true → file has
`name = a & b` (patsub_replacement off; bash 3.2 passes trivially).
Case B (leaked placeholder = failure): fresh_home; `set +e; deploy_gitconfig x x@y
'@AUTOPUSH@' 2>err; rc=$?; set -e` → rc != 0, err contains "@AUTOPUSH@", no
~/.gitconfig written. (Passing the placeholder as the value is the only way to
make the substitution a no-op; it stands in for a broken template.)
Case C (existing wins): fresh_home; printf '[gitflow]\n autopush = true\n' >
"$HOME/.gitconfig"; DOTFILES_GITFLOW_AUTOPUSH=false </dev/null → true.
Case D (defaults): fresh_home; no file, env unset, </dev/null → true.
Case E (strict preset): DOTFILES_GITFLOW_AUTOPUSH=yes </dev/null → `set +e;
resolve_autopush 2>err; rc=$?; set -e` → rc != 0, err contains "exactly".
Case F (prompt loop): `printf 'yes\ntrue\n' | ask_autopush 2>err` → true, err
contains "exactly"; `printf '\n' | ask_autopush` → true.
Case G (empty email): deploy_gitconfig x "" true 2>err → no file, err has "not written".
Case H (call site): `grep -q 'deploy_gitconfig "\$identity_name" "\$identity_email" "\$autopush"' install.sh`
and `! grep -q 'deploy_gitconfig "\$SCRIPT_DIR' install.sh`.
Case I (idempotent): run right after Case A in Case A's HOME (no fresh_home between
them): deploy_gitconfig x x@y false again → stdout contains "already up to date",
no `.gitconfig.backup-*` created.
Case J (non-exact existing): fresh_home; printf '[gitflow]\n autopush = off\n' >
"$HOME/.gitconfig"; env unset, </dev/null → prints true and stderr names "off".
Case K (refused value): `set +e; deploy_gitconfig x x@y yes 2>"$HOME/err"; rc=$?; set -e`
→ rc != 0, err contains "not exactly", no file.
Print AUTOPUSH_RENDER_OK only at the end.
## Edge cases
- Non-exact existing value: `off`/`no`/`0` are VALID false for the readers (`--bool`),
`maybe` is invalid (nothing pushed). Neither is reused (strict grammar): the value is
named on stderr, then preset/prompt/default decide. With no tty and no preset a
hand-set `off` becomes `true`: accepted consequence of the strict grammar + default
true, documented in the README sentence on switching the mode.
- Re-run: value found in ~/.gitconfig → no prompt → identical render → "already up to
date — skipping" (now holds with hooksPath in the template).
- No tty (curl | bash without /dev/tty) and no preset → true: same as today's unset = auto.
- Invalid preset aborts before Oldconfig/rm or any write (resolution happens at the
identity step).
- Empty email → file skipped, dropped push mode named in the warning.
- `.githooks/post-commit` / `.githooks/post-merge` are dirty from the session hook
refresh: do not touch, do not stage.
- Known, not handled: a system-scope `/etc/gitconfig` value is shadowed by the written
global one (blockers entry at CAPITALIZE).
## Tests
- `bash -n install.sh`, `shellcheck install.sh`.
- `bash .claude/tasks/contracts/check-autopush-render.sh` → AUTOPUSH_RENDER_OK.
## Disposition (memory read-before)
- honors BDR-001 (bash) — bash substitutions, `read -rp`.
- honors BDR-002 — template read via `$SCRIPT_DIR/gitconfig`.
- BDR-012 pattern (installer prompts, re-ask on bad input, keep-existing, no TTY →
default) honored on the interactive path; DEVIATION flagged: an invalid
DOTFILES_GITFLOW_AUTOPUSH preset aborts the install (user's fail-closed requirement),
before any file is touched.
- honors LRN-001 idempotency — unchanged ~/.gitconfig skipped; existing value reused.
- honors LRN-011 — oracle is a stub harness on extracted functions, no live install, no git config.
- honors LRN-014 — bash 3.2 safe.
- Non-binding: BDR-016 (backup naming, kept as is).
## CHALLENGE SUMMARY (round 1 → v2)
- BLOCKER (correctness+robustness): template path at the call site → placeholders deployed
→ closed: deploy_gitconfig takes name/email/autopush; call site passes the resolved
identity; Case A asserts no placeholder; Case H pins the call site. [gated]
- BLOCKER (simplicity+robustness): `git config … gitflow.*` denied to this session →
closed: sed read of ~/.gitconfig, harness writes files directly, zero config access.
- MAJOR: loose boolean grammar / fail-open normaliser → superseded by the user spec:
exact true/false only, re-ask, default true (user correction), invalid preset aborts.
- MAJOR: empty email drops the answer → closed: named in the skip warning.
- MAJOR: hooksPath wiped on redeploy → superseded by the user spec: fixed template line.
- MAJOR: shared scratch HOME → closed: fresh_home per case + guard.
- MINORs: header comment reworded, `@(USER|EMAIL|AUTOPUSH)@` assertion, README:50, README
wording (precedence + how to switch), call-site grep. Deferred: system-scope shadowing.
- Added by the user spec: fail-closed grep after render (Case B), DOTFILES_ prefix.
## CHALLENGE SUMMARY (confirmation pass, v3 → v4): SOLID, 8 MINOR
- Accepted: preset validated first; non-exact existing value named; positive true/false
guard + no-pipe leak check; patsub_replacement off + Case A2; err files in scratch HOME;
Case I in Case A's HOME; harness header + refusal rule; TODO line rewritten.
- Edge-case text corrected (off/no/0 are valid false for the readers).
- BDR-012 deviation (preset abort) flagged in the Disposition.
+8
View File
@@ -5,6 +5,14 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: se
## [Unreleased] ## [Unreleased]
### Added
- `install.sh` asks the gitflow push mode (`gitflow.autopush`, exactly `true` or `false`, Enter = `true`) and writes it to `~/.gitconfig`; an existing value is reused, `DOTFILES_GITFLOW_AUTOPUSH` presets it, any other preset aborts the install.
- `gitconfig`: fixed `core.hooksPath = ~/.claude/githooks`.
### Fixed
- `~/.gitconfig` was deployed with the literal `@USER@` / `@EMAIL@` placeholders: the installer read the identity from the repo bashrc template instead of the answers.
- An `&` in the name no longer corrupts the rendered identity on bash 5.2 and later (`patsub_replacement` turned off).
## [1.0.0] — 2026-10-06 ## [1.0.0] — 2026-10-06
### Added ### Added
+2 -1
View File
@@ -22,7 +22,8 @@ vim/autoload/ pathogen loader (committed)
vim/colors/ molokai colorscheme (committed) vim/colors/ molokai colorscheme (committed)
bash/bashrc-{linux,osx} OS-detected bashrc; USER/EMAIL identity = @USER@/@EMAIL@ placeholders, bash/bashrc-{linux,osx} OS-detected bashrc; USER/EMAIL identity = @USER@/@EMAIL@ placeholders,
asked at install (reused from an existing rc), never tracked asked at install (reused from an existing rc), never tracked
gitconfig user-scope ~/.gitconfig template, @USER@/@EMAIL@ filled at install gitconfig user-scope ~/.gitconfig template, @USER@/@EMAIL@/@AUTOPUSH@
(gitflow push mode, exact true/false) filled at install; core.hooksPath fixed
tmux.conf tmux config (vi keys, tpm plugins) → ~/.config/tmux/tmux.conf, both OSes (tmux ≥ 3.1) tmux.conf tmux config (vi keys, tpm plugins) → ~/.config/tmux/tmux.conf, both OSes (tmux ≥ 3.1)
zsh/{zshrc-osx,bchanot.zsh-theme} macOS zsh option: oh-my-zsh zshrc + theme porting the bash prompt zsh/{zshrc-osx,bchanot.zsh-theme} macOS zsh option: oh-my-zsh zshrc + theme porting the bash prompt
bin/{dt,dtach-router,claude-provider} CLI scripts deployed to ~/.local/bin bin/{dt,dtach-router,claude-provider} CLI scripts deployed to ~/.local/bin
+4 -4
View File
@@ -28,7 +28,7 @@ curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/main/remote-install.
| `vim/vimrc` | Vim config: pathogen, molokai, syntastic (C with `-Wall -Werror -Wextra`), NERDTree, 42-style canonical class generators (`:ClassH`, `:ClassC`). | | `vim/vimrc` | Vim config: pathogen, molokai, syntastic (C with `-Wall -Werror -Wextra`), NERDTree, 42-style canonical class generators (`:ClassH`, `:ClassC`). |
| `vim/autoload/` | `pathogen.vim` plugin loader (committed). | | `vim/autoload/` | `pathogen.vim` plugin loader (committed). |
| `vim/colors/` | `molokai.vim` colorscheme (committed). | | `vim/colors/` | `molokai.vim` colorscheme (committed). |
| `gitconfig` | Template of the user-scope `~/.gitconfig`. `@USER@` and `@EMAIL@` are filled at install with the `USER` and `EMAIL` exported by the bashrc (git never expands `$VARS` itself). | | `gitconfig` | Template of the user-scope `~/.gitconfig`. `@USER@`, `@EMAIL@` and `@AUTOPUSH@` (gitflow push mode) are filled at install with the installer's answers (git never expands `$VARS` itself); `core.hooksPath` points at the gitflow hooks `make link` creates in the claude-config repo. |
| `bash/bashrc-linux` | bashrc for desktop Linux (git-aware prompt + command timer). | | `bash/bashrc-linux` | bashrc for desktop Linux (git-aware prompt + command timer). |
| `bash/bashrc-osx` | bashrc for macOS: `bashrc-linux` adapted (Homebrew on `PATH`, BSD `ls -G`, bash 5 clock for the timer, `cc` without `systemd-run`). | | `bash/bashrc-osx` | bashrc for macOS: `bashrc-linux` adapted (Homebrew on `PATH`, BSD `ls -G`, bash 5 clock for the timer, `cc` without `systemd-run`). |
| `zsh/zshrc-osx` | zshrc for macOS when zsh is chosen: oh-my-zsh + the same env, aliases and dtach menu as `bashrc-osx`. Loads `~/.zshrc.local` for machine-specific lines. | | `zsh/zshrc-osx` | zshrc for macOS when zsh is chosen: oh-my-zsh + the same env, aliases and dtach menu as `bashrc-osx`. Loads `~/.zshrc.local` for machine-specific lines. |
@@ -47,7 +47,7 @@ curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/main/remote-install.
curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/main/remote-install.sh | bash curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/main/remote-install.sh | bash
``` ```
`remote-install.sh` ensures `git` is present, clones the repo to `~/config` (or pulls if already there), then runs `install.sh` with the terminal as its stdin, so the questions below (identity, macOS shell, offers) are asked even though the script arrives through a pipe. Override with env vars: `REPO_URL=... CLONE_DIR=... BRANCH=... curl ... | bash`. `remote-install.sh` ensures `git` is present, clones the repo to `~/config` (or pulls if already there), then runs `install.sh` with the terminal as its stdin, so the questions below (identity, push mode, macOS shell, offers) are asked even though the script arrives through a pipe. Override with env vars: `REPO_URL=... CLONE_DIR=... BRANCH=... curl ... | bash`.
> Piping a remote script into `bash` runs unreviewed code over the network. Read [`remote-install.sh`](remote-install.sh) first, or use the manual clone below. > Piping a remote script into `bash` runs unreviewed code over the network. Read [`remote-install.sh`](remote-install.sh) first, or use the manual clone below.
@@ -67,7 +67,7 @@ What it does:
3. Moves any existing `~/.vim`, `~/.vimrc`, `~/.bashrc`, `~/.Sublivim` to `~/Oldconfig`. 3. Moves any existing `~/.vim`, `~/.vimrc`, `~/.bashrc`, `~/.Sublivim` to `~/Oldconfig`.
4. Clones the `syntastic` and `nerdtree` vim plugins into `~/.vim/bundle/`. 4. Clones the `syntastic` and `nerdtree` vim plugins into `~/.vim/bundle/`.
5. Copies the tracked vim files into `~/.vim` and symlinks `~/.vimrc`. 5. Copies the tracked vim files into `~/.vim` and symlinks `~/.vimrc`.
6. Picks the bashrc by OS: macOS → `bashrc-osx` (falls back to `bashrc-linux` if missing), everything else → `bashrc-linux`. Renders it into `~/.bashrc` with the identity asked at the very start: a name and an email for git commits and vim headers. An `export USER=` / `export EMAIL=` already present in `~/.bashrc` or `~/.zshrc` is reused without asking, so a re-run never prompts twice; `IDENTITY_USER` / `IDENTITY_EMAIL` preset them; with no terminal attached it falls back to the login name and an empty email. The values live only in the deployed files, never in the repo. Then renders `gitconfig` into `~/.gitconfig` with the same `USER` / `EMAIL`. A different existing `~/.gitconfig` is saved as `~/.gitconfig.backup-<date>`; an identical one is left alone. It is the global level only: a repo's own `.git/config` still overrides it. `core.excludesfile` points at `~/.gitignore`, ignored by git when the file does not exist. Then deploys `tmux.conf` to `~/.config/tmux/tmux.conf` (both OSes, tmux ≥ 3.1: Ubuntu 22.04+, brew), clones [tpm](https://github.com/tmux-plugins/tpm) and fetches the listed plugins headlessly; details under [macOS](#macos) step 6, the step is the same. On Linux, `y` copies into tmux's buffer and the terminal clipboard through OSC 52; macOS uses `pbcopy`/`pbpaste`. 6. Picks the bashrc by OS: macOS → `bashrc-osx` (falls back to `bashrc-linux` if missing), everything else → `bashrc-linux`. Renders it into `~/.bashrc` with the identity asked at the very start: a name and an email for git commits and vim headers. An `export USER=` / `export EMAIL=` already present in `~/.bashrc` or `~/.zshrc` is reused without asking, so a re-run never prompts twice; `IDENTITY_USER` / `IDENTITY_EMAIL` preset them; with no terminal attached it falls back to the login name and an empty email. The values live only in the deployed files, never in the repo. Then renders `gitconfig` into `~/.gitconfig` with the same name and email (skipped, push mode included, when the name or email is empty, e.g. a first install with no terminal and no `IDENTITY_EMAIL`). The installer asks once whether the gitflow hooks push every commit and merge (`true` or `false` exactly, Enter = `true`); a `true`/`false` already in `~/.gitconfig` is reused without asking and survives the redeploy; `DOTFILES_GITFLOW_AUTOPUSH=true|false` presets it for a non-interactive install, an existing `~/.gitconfig` value still winning (no terminal and no preset gives `true`; any other preset aborts the install); the render refuses to write a file where `@AUTOPUSH@` leaked, since a non-boolean value blocks every push. To switch later: `git config --global gitflow.autopush true|false`. A different existing `~/.gitconfig` is saved as `~/.gitconfig.backup-<date>`; an identical one is left alone. It is the global level only: a repo's own `.git/config` still overrides it. `core.excludesfile` points at `~/.gitignore`, ignored by git when the file does not exist. `core.hooksPath` is fixed to `~/.claude/githooks`, the gitflow hooks created by `make link` in the claude-config repo; the installer neither creates nor checks that directory. Being global, it makes git ignore each repo's `.git/hooks/` unless that repo sets its own `core.hooksPath`. Then deploys `tmux.conf` to `~/.config/tmux/tmux.conf` (both OSes, tmux ≥ 3.1: Ubuntu 22.04+, brew), clones [tpm](https://github.com/tmux-plugins/tpm) and fetches the listed plugins headlessly; details under [macOS](#macos) step 6, the step is the same. On Linux, `y` copies into tmux's buffer and the terminal clipboard through OSC 52; macOS uses `pbcopy`/`pbpaste`.
7. Installs Python CLIs via `pipx` (`PyMuPDF` → `pymupdf`, `Markdown` → `markdown_py`) — skipped if `pipx` is absent. 7. Installs Python CLIs via `pipx` (`PyMuPDF` → `pymupdf`, `Markdown` → `markdown_py`) — skipped if `pipx` is absent.
8. Copies the `bin/` scripts (`dt`, `dtach-router`, `claude-provider`) into `~/.local/bin`. The dtach session-resume menu ships in the deployed bashrc (both OSes), so every interactive shell offers it — including VS Code Remote-SSH terminals, which are non-login and never read `~/.profile`. The installer also strips any older dtach block left in `~/.profile` so a plain SSH login doesn't prompt twice. 8. Copies the `bin/` scripts (`dt`, `dtach-router`, `claude-provider`) into `~/.local/bin`. The dtach session-resume menu ships in the deployed bashrc (both OSes), so every interactive shell offers it — including VS Code Remote-SSH terminals, which are non-login and never read `~/.profile`. The installer also strips any older dtach block left in `~/.profile` so a plain SSH login doesn't prompt twice.
9. On Linux, installs `etc/profile.d/disk-usage-warning.sh` to `/etc/profile.d/` (needs `sudo`) so each login warns when `/` or `/home` cross 85% usage. 9. On Linux, installs `etc/profile.d/disk-usage-warning.sh` to `/etc/profile.d/` (needs `sudo`) so each login warns when `/` or `/home` cross 85% usage.
@@ -99,7 +99,7 @@ The script is re-runnable: each run re-backs up to `~/Oldconfig` (overwriting th
### macOS ### macOS
The same `./install.sh` detects macOS and replaces `apt-get` with Homebrew. It first asks which login shell you want, **zsh** or **bash** (`[zsh]` by default; answer in advance with `MACOS_SHELL=bash ./install.sh`, and with no terminal attached it picks zsh): The same `./install.sh` detects macOS and replaces `apt-get` with Homebrew. After the identity and push-mode questions, it asks which login shell you want, **zsh** or **bash** (`[zsh]` by default; answer in advance with `MACOS_SHELL=bash ./install.sh`, and with no terminal attached it picks zsh):
1. Installs Homebrew with its official script when `brew` is missing (this also pulls the Xcode Command Line Tools: clang, make, git), then `brew update` + `brew upgrade`. 1. Installs Homebrew with its official script when `brew` is missing (this also pulls the Xcode Command Line Tools: clang, make, git), then `brew update` + `brew upgrade`.
2. Installs the apt list mapped to formulae: `vim git git-lfs git-filter-repo gitleaks pkgconf shellcheck gh git-delta curl gnupg lftp inetutils unzip tree tmux fzf dtach node python pipx php mariadb imagemagick ffmpeg weasyprint poppler qpdf webp libavif bash`. Brew's `php` already ships gd, mbstring, xml, intl, curl and mysql. 2. Installs the apt list mapped to formulae: `vim git git-lfs git-filter-repo gitleaks pkgconf shellcheck gh git-delta curl gnupg lftp inetutils unzip tree tmux fzf dtach node python pipx php mariadb imagemagick ffmpeg weasyprint poppler qpdf webp libavif bash`. Brew's `php` already ships gd, mbstring, xml, intl, curl and mysql.
+10 -4
View File
@@ -1,10 +1,14 @@
# Template for the user-scope ~/.gitconfig, rendered by install.sh. # Template for the user-scope ~/.gitconfig, rendered by install.sh. Git never
# Git never expands $VARS: @USER@ and @EMAIL@ are replaced at install # expands $VARS, so the identity and the gitflow push mode are placeholders
# time with the USER and EMAIL exported by the deployed bashrc. # filled at install time with the installer's answers. A repo .git/config
# A repo .git/config still overrides these values for that repo. # still overrides these values for that repo.
[user] [user]
name = @USER@ name = @USER@
email = @EMAIL@ email = @EMAIL@
[gitflow]
# Push mode of the gitflow hooks: false = manual, you run `git push`;
# true = every commit and merge is pushed. Exact true/false only (fail-closed).
autopush = @AUTOPUSH@
[push] [push]
default = current default = current
[color] [color]
@@ -15,6 +19,8 @@
editor = vim editor = vim
pager = delta pager = delta
excludesfile = ~/.gitignore excludesfile = ~/.gitignore
# gitflow hooks (created by `make link` in claude-config); git expands ~ itself.
hooksPath = ~/.claude/githooks
[advice] [advice]
detachedHead = false detachedHead = false
[merge] [merge]
+70 -11
View File
@@ -2,6 +2,9 @@
# install.sh — deploy the vim + bash dotfiles. OS is auto-detected. # install.sh — deploy the vim + bash dotfiles. OS is auto-detected.
# Usage: ./install.sh # Usage: ./install.sh
set -euo pipefail set -euo pipefail
# bash >= 5.2 expands `&` in ${var//pat/rep} replacements: an `&` in a name would
# corrupt the rendered identity. No-op on bash 3.2.
shopt -u patsub_replacement 2>/dev/null || true
# Resolve the repo root so the script works from any working directory. # Resolve the repo root so the script works from any working directory.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -524,20 +527,75 @@ resolve_identity() {
printf '%s\n' "${value:-$default}" printf '%s\n' "${value:-$default}"
} }
# Ask the push question on the terminal until the answer is exactly true or false;
# Enter (or EOF) = true. Prints the value.
ask_autopush() {
local answer=""
while :; do
read -rp "Automatic push of commits by the gitflow hooks on this machine? [true/false] (default: true) " answer || true
case "${answer:-true}" in
true|false) printf '%s\n' "${answer:-true}"; return 0 ;;
*) echo "Answer exactly true or false." >&2 ;;
esac
done
}
# Push mode of the gitflow hooks (gitflow.autopush), exact true/false only: the
# readers fail closed on anything else. Never asked twice: a true/false already in
# ~/.gitconfig wins silently (read with sed, like rc_export_value, so a hand-set
# value survives the redeploy; a non-exact spelling is named and re-asked), else
# DOTFILES_GITFLOW_AUTOPUSH (anything but true/false aborts the install here,
# before any file is touched, whatever ~/.gitconfig holds), else the prompt
# on a terminal, else true (today's unset = auto). Prints the value.
resolve_autopush() {
local value="" preset="${DOTFILES_GITFLOW_AUTOPUSH:-}"
case "$preset" in
true|false|"") ;;
*) echo "DOTFILES_GITFLOW_AUTOPUSH='$preset' — must be exactly true or false" >&2; return 1 ;;
esac
if [ -f "$HOME/.gitconfig" ]; then
value="$(sed -n 's/^[[:space:]]*autopush[[:space:]]*=[[:space:]]*//p' "$HOME/.gitconfig" | tail -n 1)"
case "$value" in
true|false) printf '%s\n' "$value"; return 0 ;;
"") ;;
*) echo "gitflow.autopush='$value' in ~/.gitconfig is not exactly true/false — asking again" >&2 ;;
esac
fi
if [ -n "$preset" ]; then printf '%s\n' "$preset"; return 0; fi
if [ -t 0 ]; then ask_autopush; else echo true; fi
}
# Print the gitconfig template with the identity ($1 name, $2 email) and the push
# mode ($3) filled in. Fails, printing nothing, when the mode is not exactly
# true/false or when @AUTOPUSH@ survives the render: a non-boolean
# gitflow.autopush blocks every push, so a leaked placeholder is an outage.
render_gitconfig() {
local name="$1" email="$2" autopush="$3" rendered
case "$autopush" in
true|false) ;;
*) echo "gitconfig render refused: push mode '$autopush' is not exactly true/false — nothing written" >&2; return 1 ;;
esac
rendered="$(render_identity_template "$SCRIPT_DIR/gitconfig" "$name" "$email")"
rendered="${rendered//@AUTOPUSH@/$autopush}"
case "$rendered" in
*@AUTOPUSH@*) echo "gitconfig render failed: @AUTOPUSH@ left in the output — nothing written" >&2; return 1 ;;
esac
printf '%s\n' "$rendered"
}
# Install the user-scope ~/.gitconfig (a repo's own .git/config still wins). # Install the user-scope ~/.gitconfig (a repo's own .git/config still wins).
# The identity is read from the USER/EMAIL exports of the deployed rc ($1), so # $1 $2 = the identity rendered into the rc, so git and the shell agree; $3 = the
# git and the shell agree. A ~/.gitconfig that differs is kept as # gitflow push mode (true/false). A ~/.gitconfig that differs is kept as
# ~/.gitconfig.backup-<date>, outside ~/Oldconfig which every run wipes. # ~/.gitconfig.backup-<date>, outside ~/Oldconfig which every run wipes.
# Idempotent: an identical ~/.gitconfig is left alone. # Idempotent: an identical ~/.gitconfig is left alone. Two fail-closed checks
# live in render_gitconfig: exact push mode, no leaked placeholder.
deploy_gitconfig() { deploy_gitconfig() {
local rc="$1" name email rendered backup local name="$1" email="$2" autopush="$3" rendered backup
name="$(rc_export_value USER "$rc")"
email="$(rc_export_value EMAIL "$rc")"
if [ -z "$name" ] || [ -z "$email" ]; then if [ -z "$name" ] || [ -z "$email" ]; then
echo "USER/EMAIL not exported by $rc — skipping ~/.gitconfig" >&2 echo "Name or email empty — skipping ~/.gitconfig (push mode $autopush not written)" >&2
return 0 return 0
fi fi
rendered="$(render_identity_template "$SCRIPT_DIR/gitconfig" "$name" "$email")" rendered="$(render_gitconfig "$name" "$email" "$autopush")" || return 1
if printf '%s\n' "$rendered" | cmp -s - "$HOME/.gitconfig"; then if printf '%s\n' "$rendered" | cmp -s - "$HOME/.gitconfig"; then
echo "$HOME/.gitconfig already up to date — skipping" echo "$HOME/.gitconfig already up to date — skipping"
return 0 return 0
@@ -547,7 +605,7 @@ deploy_gitconfig() {
echo "Saving the current ~/.gitconfig to $backup" echo "Saving the current ~/.gitconfig to $backup"
mv "$HOME/.gitconfig" "$backup" mv "$HOME/.gitconfig" "$backup"
fi fi
echo "Deploying gitconfig to ~/.gitconfig ($name <$email>)" echo "Deploying gitconfig to ~/.gitconfig ($name <$email>, autopush=$autopush)"
printf '%s\n' "$rendered" > "$HOME/.gitconfig" printf '%s\n' "$rendered" > "$HOME/.gitconfig"
} }
@@ -584,6 +642,7 @@ EOF
identity_name="$(resolve_identity USER "Name for git commits and vim headers" "$(id -un)")" identity_name="$(resolve_identity USER "Name for git commits and vim headers" "$(id -un)")"
identity_email="$(resolve_identity EMAIL "Email for git commits and vim headers" "")" identity_email="$(resolve_identity EMAIL "Email for git commits and vim headers" "")"
echo "Identity: $identity_name <${identity_email:-no email}>" echo "Identity: $identity_name <${identity_email:-no email}>"
autopush="$(resolve_autopush)"
# System packages: apt-get on Debian/Ubuntu, Homebrew on macOS. # System packages: apt-get on Debian/Ubuntu, Homebrew on macOS.
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
@@ -684,8 +743,8 @@ fi
echo "Deploying $bashrc ($identity_name <$identity_email>)" echo "Deploying $bashrc ($identity_name <$identity_email>)"
render_identity_template "$SCRIPT_DIR/$bashrc" "$identity_name" "$identity_email" > "$HOME/.bashrc" render_identity_template "$SCRIPT_DIR/$bashrc" "$identity_name" "$identity_email" > "$HOME/.bashrc"
# User-scope git config, identity taken from the bashrc just deployed. # User-scope git config, same identity as the rc just rendered.
deploy_gitconfig "$SCRIPT_DIR/$bashrc" deploy_gitconfig "$identity_name" "$identity_email" "$autopush"
# tmux config + plugins (tmux comes from the apt or brew list above). # tmux config + plugins (tmux comes from the apt or brew list above).
if command -v tmux >/dev/null 2>&1; then if command -v tmux >/dev/null 2>&1; then