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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011EBCcjJvEvhpToahqdsLLA
This commit is contained in:
+46
-22
@@ -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,14 +199,15 @@ 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 --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)
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user