Git Operations
Advanced git workflows: rebase surgery, conflict resolution, and coexistence in shared multi-agent repos.
PR Branch Upkeep
The most-run workflow: keeping your own PR branch current against a moving main.
Capture the target base and remote branch tip, preserve a backup, rebase when required, and run checks on the resulting content. Recheck remote state before pushing. A base that keeps moving is not a reason for an endless rebase loop: use the captured base and the repository's merge policy. When current-main ancestry is required, check it explicitly:
git merge-base --is-ancestor origin/main HEAD && echo "based on current main"
Review etiquette: while a reviewer (bot, human, or agent) is actively reading, hold pushes. Batch fixes, then one rebase+push when the review lands. Freshness loops run at push boundaries.
Pushing rewritten history
Pin the lease to the remote tip whose work you inspected before rewriting. Background fetches can refresh a tracking ref and weaken an implicit lease. Recheck with ls-remote before pushing; if the tip changed, inspect and reconcile that work instead of copying its SHA into a fresh lease:
git ls-remote origin refs/heads/<branch> # compare with the previously captured and inspected remote tip
git push --force-with-lease=refs/heads/<branch>:<expected-sha> origin HEAD:<branch>
On a "stale info" rejection, diagnose with ls-remote before any retry. Observed causes: auto-delete-on-merge removed the branch, a typo'd lease SHA, or the remote legitimately moved. Bare --force is not an escalation path.
After the base squash-merges
When a parent PR squash-merges, its commits vanish from main's ancestry. A plain rebase replays them as ghosts. Detect the merge type first: squash, rebase-merge, and merge-commit each leave different ancestry.
| Situation | Move |
|---|---|
| Parent PR squash-merged | git rebase --onto <new-base> <old-base-sha>: only your own commits replay |
| Replay keeps conflicting / branch polluted | Rebuild as main + delta: one git diff --binary patch from the backup, applied onto fresh main; prove with range-diff |
| Pushing fixes to an old branch | Verify PR open-state first. A squash-merged PR's branch is dead |
| Stacked chain | Cascade bottom-up with --onto, per-level backup, push bottom-first (validators diff against origin/<base>) |
Pre-Surgery Ref Reality
Local refs and the forge's view routinely disagree. Before any history surgery:
- Inspect the fetch mapping when tracking refs matter.
git fetch origin mainupdatesorigin/mainunder the usual configured refspec; usegit fetch origin refs/heads/main:refs/remotes/origin/mainwhen an explicit mapping is needed git rev-parse --is-shallow-repository: shallow history can hide the real merge base; deepen when ancestry is incomplete- Cross-check
gh pr viewbase/head oids against localrev-parse/ls-remote - Probe conflict shape for free:
git merge-tree --write-tree origin/main <branch>. A clean merge predicts textual compatibility, not semantic correctness or whether branch policy requires a restack - Pin every operation to a captured SHA, never a moving ref
Conflict Resolution
Conflicts are intent-merges, not side-picks. Read all three index stages (git show :1:<file> :2:<file> :3:<file>) plus the pre-rebase tip before resolving, and ask: did upstream obsolete this branch's mechanism? When main replaced it with a newer abstraction, plug your feature into main's shape instead of resurrecting the old one. Preserve invariant-explaining comments. Pace by risk class (slower on security-sensitive files). Close with a mechanical conflict-marker scan; survivors are real.
| Situation | Strategy |
|---|---|
| Encoded artifact (lockfile, SOPS, generated schema) | Never text-merge the encoding. Merge the meaning, re-encode with the canonical tool, roundtrip-verify. |
| Simple content conflict | Resolve as a union of both sides' intent; prefer the smallest diff. |
| Large structural conflict | Consider --ours/--theirs + manual reapply of the smaller side. |
Lock files
Resolve package manifests first. Inspect both lockfile sides and choose the intended baseline deliberately, then regenerate with the pinned package manager. During rebase, ours is the rebased upstream and theirs is the replayed commit; those labels do not mean mine and upstream. Example after choosing the upstream baseline:
git restore --ours --worktree pnpm-lock.yaml
pnpm install --lockfile-only
git add pnpm-lock.yaml
Same shape for any generated lockfile. Fold the regenerated lockfile back into the commit that carried it.
Rebase vs Merge
Ownership and review state decide, not pushed-ness:
| Situation | Use |
|---|---|
| Your own PR branch behind main (pushed or not) | Rebase + pinned-lease push. Hold pushes while a review is actively reading. |
| Branch checked out in another worktree | Work there; don't steal the checkout. |
| Genuinely shared branch (others based work on it) | Never rebase. Merge, or git revert for published mistakes. |
| Cleaning up messy commits before PR | git rebase -i with squash/fixup |
Ceremony scales with collaborator count (a solo repo can live on main), but the push-boundary rules hold regardless.
Undo Operations
| What happened | Fix |
|---|---|
| Uncommit / squash (keep changes staged) | git reset --soft <captured-sha>: never a moving ref. reset --soft origin/main mid-squash silently staged reverts of newly-landed main when the ref moved. Re-check base movement before amending. |
| Need to recover something lost | Inspect reflog, status, and log, then preserve the candidate with git branch recovery/<name> <sha>. Inspect that ref before switching or restoring anything |
Verify Before You Trust
Regenerating or rebasing is not the same as verifying the result. In a concurrent monorepo, prove it.
Lockfile check: after a rebase touches a lockfile, verify with the gate's exact command in a throwaway worktree. pnpm install --lockfile-only is vacuous: it never materializes snapshots, so it reports "up to date" while a full install fails.
git worktree list
# Choose an unused path under the repository's worktree convention.
check_tree="$HOME/dev/worktrees/<project>/nova/lockcheck-<unique>"
git worktree add --detach "$check_tree" HEAD
(cd "$check_tree" && pnpm install --frozen-lockfile)
# Inspect the result and status before removing the worktree you created.
git worktree remove "$check_tree"
A passing check that disagrees with an observed failure is itself a finding. Diagnose why the check is vacuous, upgrade the standard.
Bracket every rewrite: backup ref before, range-diff proof after. Persist the proof inputs so the receipt can be reconstructed exactly:
backup=backup/pre-rebase-$(date +%Y%m%d-%H%M%S)
git branch "$backup" && old_base=$(git merge-base HEAD origin/main) # persist both to a scratch file
git rebase origin/main
git range-diff "$old_base".."$backup" origin/main..HEAD # explicit ranges — the three-dot shorthand can include main's new commits
Clean ≠ correct. Zero conflicts prove nothing about semantics. After any rewrite, run the semantic drift audit: full gates on the rebased SHA, range-diff read as a bug detector (it catches resolutions rolling back newer main), symbol greps across HEAD vs origin/main vs the backup ref, syntax checks on every resolved file. Files new on your branch merge "cleanly" while still importing what upstream deleted. Typecheck catches it, the merge doesn't.
Proofs
Match the proof to the claim:
| Claim to prove | Proof |
|---|---|
| Replay preserved per-commit intent | git range-diff <old-base>..<old-tip> <new-base>..<new-tip> (explicit ranges) |
| Squash/reshuffle left the tree identical | git rev-parse HEAD^{tree} equality vs the backup ref (sharper than range-diff for N→1 squashes) |
| Cherry-pick / second PR carries same change | git patch-id --stable on both |
| Nothing stranded before deletion | Ancestry checks plus tree/patch comparison for squash merges; inspect dirty worktrees before deletion |
| Merge captured everything | Content-parity diff after the merge event |
History Serves Its Readers
Atomic while working; collapse only when the history itself stops serving the reviewer.
| Concern | Move |
|---|---|
| Squashing a reviewed branch | The PR body inherits the narrative. Enumerate the logical commits the squash removed. Human-authored PR titles, bodies, and drafts are read-only absent explicit instruction. |
| Post-review fixes | git commit --fixup=<logical-parent> + autosquash, not a "review fix" blob. Fix at the introducing commit when CI reads history (diffs HEAD~1) rather than the tree. |
| PR ancestry poisoned (wrong-base merge, CODEOWNERS dragnet) | The forge computes review surface from ancestry. Merge gymnastics to dodge a force-push is worse than the force-push. Recover: push the clean replacement first, close the old PR with a pointer comment naming the replacement and why, then reopen. |
| Stale failed check inherited from a closed PR | Inspect the failure and rerun the appropriate check on the intended artifact. Do not rewrite history merely to change a status badge. |
Commit bodies
Compose multi-line bodies via git commit -F - with a single-quoted heredoc (<<'EOF') or a message file. Stacked -m flags keep each paragraph as one unwrapped line and burn amend cycles. The quoting is the protection: an unquoted heredoc executes backticks and $() inside the message before Git sees it. Wrap at 76 characters; length checks are backstops, not the mechanism. Verify the recorded message after any shell-composed body (git log -1 --format=%B).
Shared-Repo Coexistence
Multiple agents (and humans) work the same repo concurrently. Causation decides ownership.
| Signal | Move |
|---|---|
| Ambiguous churn in shared files | Restore churn your own commands generated; leave others' work untouched, even in the same file |
| Co-edited file, mixed hunks | Stage only your hunks (git add -p, or a hand-built patch via git apply --cached --unidiff-zero), then commit the index: git commit <file>/--only commits the worktree copy and swallows unstaged sibling hunks |
index.lock |
Triage before removing: owning operation and process (lsof/ps). Git lockfiles can be empty while active; no visible open handle alone does not establish staleness. Remove only after confirming the owning operation ended |
| Another agent's rebase in progress | Hold your verified commit, but a blocker must reproduce before you report it, and after repeated blocked turns escalate with pid + age as a question, not a fact |
| Branch checked out in another worktree | Work there; don't steal the checkout |
| Multi-worktree edits | Edit tools root at the original cwd. Identity-check (git branch --show-current + pwd) before editing; status-check every involved worktree after |
Hooks
Hooks are receipts, not friction. Wait them out and cite their output. A new hook failure gets fixed, never bypassed (pre-warm the build cache, fix the type error). The bypass window is narrow: the failure is known, named, pre-existing, and unrelated to your diff (or the hook physically can't run here) plus equivalent gates ran green, and the bypass is disclosed in the wrap-up. Some hosts mechanically block --no-verify; respect it. Auto-fixing hooks can rewrite unrelated files: diff after every commit, and before restoring a hook-touched file confirm it carried no one else's edits pre-hook (git restore discards the whole worktree copy, not just the hook's hunks).
Non-Interactive Surgery
Agent hosts have no tty:
GIT_SEQUENCE_EDITOR=true GIT_EDITOR=truepre-armors rebases; a stuck editor gets its pid killed, never the rebase- No parallel git commands during surgery:
index.lockcollisions are self-inflicted - When the sequencer wedges ("patch staged, commit not recorded"), read
.git/rebase-mergestate files and resume withgit commit -C <sha>instead of firing recovery commands speculatively
Safety Rules
- Never rebase genuinely shared branches: your own PR branch is yours to rebase
- Pin the lease:
--force-with-lease=refs/heads/<branch>:<sha>, confirmed vials-remoteimmediately before the push; never bare--force - Regenerate encoded artifacts: never text-merge them
- Backup ref before destructive ops:
git branch backup/pre-op-$(date +%Y%m%d-%H%M%S); recovery infrastructure, not ceremony, so persist the name and old base to a scratch file - Prove history rewrites: backup the pre-rewrite head, then explicit-range
range-diffto confirm every changed commit is intentional - Identity-check before edits in multi-worktree repos:
git branch --show-current+pwd
Anti-Patterns
| Anti-Pattern | Fix |
|---|---|
| Manually merging generated lockfiles | Take one side, regenerate with the package tool |
| Trusting a lockfile check that never installs | Verify with the gate's exact command (pnpm install --frozen-lockfile) in a throwaway worktree |
| Plain rebase over a squash-merged base | git rebase --onto <new-base> <old-base-sha>: only your commits replay |
| Merge gymnastics to dodge a force-push on a PR branch | Proper rebase + pinned-lease push. Ancestry poisoning triggers review dragnets |
| Rebasing a genuinely shared branch | Merge, or create a new branch |
Using --force |
Pinned --force-with-lease only when approved |
Stacked -m flags for multi-line commit bodies |
git commit -F - heredoc or a message file |
| Running recovery commands by habit | Inspect status, log, and reflog first |
| Staging unrelated work | Stage owned paths or hunks, inspect the complete index, then commit without path arguments; coordinate if someone else owns staged work |
git checkout <commit> -- <paths> then committing |
It stages silently; check git diff --cached --name-only before each commit or it swallows unintended files |
| Assuming a rebase kept your commits | Prove it: explicit-range range-diff, tree-hash, or patch-id per the proofs table |
Sources and Applicability
Checked on 2026-09-04 against the official push, fetch, commit, and range-diff manuals. Explicit leases protect an inspected remote expectation. Range-diff helps compare patch series; it is not a semantic-equivalence proof or a reliable replacement for tree comparison after squashing. Local branch ownership, worktree layout, and publishing authorization remain repository policy.
What This Skill is NOT
- Not for routine
git statusor simple commits. - Not permission to rewrite genuinely shared history.
- Not a replacement for understanding the diff before committing.