- name
- git
- description
- Use this skill for complex git operations including rebases, merge conflict resolution, cherry-picking, branch management, PR branch upkeep, or repository archaeology. Activates on mentions of git rebase, merge conflict, cherry-pick, git history, branch cleanup, git bisect, worktree, force push, force-with-lease, PR branch, stacked PR, squash-merge, lease, shared repo, or complex git operations.
# 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:
```bash
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:
```bash
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 main` updates `origin/main` under the usual configured refspec; use `git fetch origin refs/heads/main:refs/remotes/origin/main` when 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 view` base/head oids against local `rev-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:
```bash
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.
```bash
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:
```bash
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. |
View on GitHub