From 9aa0916d9599acec261bc21db9f4f07885be023e Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Sat, 22 Aug 2026 01:06:49 +0900 Subject: [PATCH] fix: correct conflict-marker direction and other review findings The rebase conflict-marker explanation had HEAD and the incoming side backwards, verified by reproducing all three merge/rebase scenarios in a throwaway repo. Rewrote that passage around the actual behavior instead of a merge-vs-rebase binary, since the old "merge = HEAD is trunk" claim was also only true for one of the two merge scenarios the doc itself describes. Also fixes, from the same review pass: - push-rejection handling in Branching split into its two real causes (protection/permission vs. plain non-fast-forward) - the commit-body trigger reworded to match how it's actually used ("why worth keeping", not just subject length) - a mislabeled checkout equivalent for unstaging (that was always git reset's job, not checkout's) - the vendor-sync example no longer assumes upstream's trunk is named master Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011EBCcjJvEvhpToahqdsLLA --- git-operations.md | 74 +++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/git-operations.md b/git-operations.md index e49ba60..8702894 100644 --- a/git-operations.md +++ b/git-operations.md @@ -46,12 +46,20 @@ bumps), it's `chore`. **When a branch is not warranted.** Trivial, low-risk changes — a typo fix, a corrected newline, a single-line doc or config edit — can be committed straight to trunk: `git commit`, then `git push`. -Don't check branch-protection status first; just attempt the push. A rejection (protected branch, or -no direct push permission at all — increasingly the default for an agent identity) *is* the signal: -fall back to branching normally (`git switch -c fix/...` — `git checkout -b` is equivalent — then -push, open a PR) and carry on as if a -branch had been used from the start. Anything past that bar — any actual logic or behavior change, -however small — branches from the outset; don't wait for a push rejection to decide. +Don't check branch-protection status first; just attempt the push. A rejection has two different +causes, and they call for different responses: + +- **Protected branch, or no direct push permission at all** (increasingly the default for an agent + identity) — this *is* the signal to fall back to branching normally (`git switch -c fix/...` — + `git checkout -b` is equivalent — then push, open a PR) and carry on as if a branch had been used + from the start. +- **A plain non-fast-forward rejection** — trunk moved since your last fetch, nothing to do with + permission. `git fetch && git rebase origin/master`, then push again, is enough on its own; + branching is still a safe fallback if a second collision seems likely, just not the only correct + response to this one. + +Anything past the trivial bar — any actual logic or behavior change, however small — branches from +the outset regardless of which rejection would apply; don't wait for a push rejection to decide. **Branch lifetime.** No fixed day limit — the right threshold varies too much by project pace to be universal — but watch for the smell: a branch that's drifted far enough from trunk that merging it @@ -86,10 +94,11 @@ subject with no prefix is better than a mislabeled one. Subject line in imperati expired tokens", not "Rejected expired tokens" or "Rejects expired tokens" — as if finishing the sentence "If applied, this commit will...". -**Body: add one when the subject line alone would run too long to stay readable**, not as a -mandatory second paragraph on every commit. When a body is warranted, its job is to carry what the -diff itself can't — motivation, the root cause behind a fix, why an approach was chosen over an -obvious alternative — not to re-narrate the diff line by line. +**Body: add one whenever there's a "why" worth keeping**, not as a mandatory second paragraph on +every commit. A subject that would have to run long to say what's needed is one trigger; just as +common is a subject that's already a clean, short label but leaves the motivation unstated — the +root cause behind a fix, why an approach was chosen over an obvious alternative. Either case gets a +body; its job is to carry what the diff itself can't, not to re-narrate the diff line by line. ## Merging vs. rebasing @@ -138,13 +147,27 @@ commit is what ties them back to a single piece of work. >>>>>>> branch-name ``` -**`HEAD` and "incoming" swap meaning between merge and rebase.** During a `merge`, `HEAD` is the -branch you're on (trunk) and the lower half is the branch being merged in. During a `rebase` it -inverts: `HEAD` is the commit being replayed from the branch, and the lower half is trunk — because a -rebase replays each commit as if it were a fresh change against the new base. Getting this backwards -means resolving every conflict exactly wrong. Run `git status` first to confirm which operation is -actually in progress, and resolve by reading what each side really contains, never by reflexively -picking one side. If a resolution goes wrong partway through, `git merge --abort` or +**`HEAD` always means whatever's currently checked out — never assume, check.** `<<<<<<< HEAD` is the +state you have checked out right now; the bottom half (`>>>>>>> ...`) is whatever you told git to +bring in. That produces three different results across the operations this handbook actually uses, +verified directly rather than assumed: + +- **Staying current** (`git rebase origin/master` on your feature branch): rebase rebuilds trunk + forward and replays your commits onto it one at a time, so `HEAD` during a conflict is *trunk's* + content — even though you're conceptually "on" your feature branch — and the bottom half is your + original commit being replayed. +- **Catching up a shared branch** (`git merge origin/master` while on the feature branch): `HEAD` is + your feature branch's content, and the bottom half is trunk's incoming change — the opposite + polarity from the rebase case above, despite both being "sync with trunk." +- **Landing on trunk** (`git switch master && git merge --no-ff branch-name`): `HEAD` is trunk again, + and the bottom half is the branch being merged in. + +There's no single "merge vs. rebase" rule that covers all three — rebase always puts the target +you're rebasing *onto* in `HEAD`; merge puts whatever you had checked out *before* running the +command, which is trunk in the landing case and the feature branch in the catch-up case. Run +`git status` before resolving — it names both sides explicitly ("rebasing branch X on Y", or "On +branch X ... You are merging Y") — and resolve by reading what each side actually contains, never by +reflexively picking one. If a resolution goes wrong partway through, `git merge --abort` or `git rebase --abort` returns to the pre-conflict state cleanly — cheaper than untangling a bad resolution by hand. @@ -176,15 +199,16 @@ already shared is the same mistake as rebasing shared history — don't. **`git switch` / `git restore` — day-to-day, not history-rewriting.** `switch` changes which branch `HEAD` points to; `restore` discards or unstages changes to specific files. Neither touches commit -history at all — the older, overloaded `git checkout` still does both jobs and remains valid on any -git a project might be pinned to; the equivalents below are for reference, not a second command set -to also learn. +history at all. Three of these four map onto the older, overloaded `git checkout`, which remains +valid on any git a project might be pinned to — but unstaging was never one of `checkout`'s jobs; +that was always `git reset`'s, from well before `restore` existed. The equivalents below are for +reference, not a second command set to also learn. ``` -git switch -c fix/typo-readme # create + switch to a new branch (checkout: git checkout -b fix/typo-readme) -git switch master # switch back (checkout: git checkout master) -git restore --staged file.txt # unstage a file (checkout: git reset file.txt) -git restore file.txt # discard working-tree changes (checkout: git checkout -- file.txt) +git switch -c fix/typo-readme # create + switch to a new branch (checkout: git checkout -b fix/typo-readme) +git switch master # switch back (checkout: git checkout master) +git restore --staged file.txt # unstage a file (pre-restore: git reset file.txt) +git restore file.txt # discard working-tree changes (checkout: git checkout -- file.txt) ``` **Recovering from a bad rebase or reset: the reflog.** Every move of `HEAD` — including ones a @@ -255,7 +279,7 @@ not the merge conflict itself (Merging vs. rebasing already covers that mechanic ``` git switch vendor git fetch upstream - git merge --ff-only upstream/master + git merge --ff-only upstream/master # or whatever upstream calls its own trunk — not yours to rename ``` If the fast-forward fails, upstream's history moved in a way it shouldn't have (rare, but possible on a repo you don't control) — stop and look, don't force it. `vendor` is the answer to "what did