Git Workflow
Overview
Git operations with an eye to a clean, bisectable history and safe collaboration. Canonical home for commit hygiene and conventional commits (referenced by /loom-code-review and /loom-refactoring).
The golden rule
Never rewrite history that others have based work on. rebase, commit --amend, reset --hard, and push --force on a shared/public branch orphan collaborators' commits and force painful recovery. Rewrite freely on your own un-pushed / unshared branch; treat anything pushed to a shared branch as immutable.
- Force-push only your own feature branch, and prefer
--force-with-lease(aborts if someone else pushed since your last fetch) over--force. - Never force-push
main/master/release/*— protect them server-side.
Commit hygiene (conventional commits)
Format: type(scope): description — feat, fix, docs, refactor, test, chore (+ perf, build, ci, style). ! or a BREAKING CHANGE: footer marks an incompatible change.
- Atomic: one logical change per commit. It should build and pass tests on its own — a green history is a bisectable history.
- Separate refactor from behavior change (see
/loom-refactoring): arefactor:commit is a runtime no-op; afix:commit changes behavior. Mixing them hides the fix from reviewers and poisonsgit bisect. - Message = what + why, not how. The diff shows how. Body explains motivation, tradeoffs, and links issues.
- Group by concern: 5 files across 3 concerns → 3 commits (module / its tests / wiring), not 1 monolith or 5 fragments.
git commit -m "fix(cart): resolve race in quantity update
Rapid add/remove let the count go negative via unsynchronized state.
Guard with an atomic compare-and-swap.
Fixes #234"
⚠ Loom stages: never git add -A / git add . — that stages the .work/ symlink. Stage specific files only.
Rebase vs. merge — pick deliberately
| Situation | Use | Why |
| -------------------------------------------------- | -------------------------------- | ---------------------------------------------------------- |
| Update your local feature branch with main | rebase | Linear history; your commits replay on top, no merge noise |
| Integrate a reviewed feature into main | merge (often --no-ff) | Preserves the branch as a reviewable unit; never rewrites shared history |
| Branch already pushed & others pulled it | merge (NOT rebase) | Golden rule — rebasing rewrites shared commits |
| Clean up messy local WIP commits before a PR | rebase -i (before pushing) | Squash/reorder/reword while still private |
| Long-lived branch, want to record integration points | merge | Merge commits document when integration happened |
Rule of thumb: rebase to keep your private branch current; merge to share. Rebasing public branches is the most common way teams lose work.
Interactive rebase hygiene
git rebase -i origin/main — pick/reword/squash/fixup/edit/drop. Squash WIP noise, keep meaningful commits distinct (don't collapse a real feature into one opaque blob — you lose bisect granularity). git commit --fixup=<sha> + git rebase -i --autosquash targets fixups automatically. Abort anytime with git rebase --abort.
Conflict resolution
git merge feature/new-api # CONFLICT in src/api.rs
git status # list conflicted files
git diff src/api.rs # see both sides in context (<<<<<<< ======= >>>>>>>)
# resolve by understanding BOTH intents — not blindly picking a side:
git checkout --ours path # keep current-branch version wholesale
git checkout --theirs path # take incoming version wholesale
git add src/api.rs # mark resolved
cargo test # ALWAYS re-test: a clean textual merge can be a semantic conflict
git merge --continue
git merge --abort # bail out if it's the wrong approach
- ⚠ A conflict-free merge is not a correct merge. Two branches editing different lines can still break each other's assumptions. Run tests after every resolution.
- Document non-obvious resolutions in the merge commit body.
- ⚠
--ours/--theirsmean opposite things underrebasevsmerge(during rebase, "ours" is the branch you're rebasing onto). Check which operation you're in.
git rerere — resolve once, reuse forever
git config --global rerere.enabled true. Git records how you resolved a conflict and replays the same resolution automatically next time the identical conflict appears — invaluable for long-lived branches repeatedly rebased/merged, and for loom's progressive merges where the same worktree conflict can recur. Combine with periodic git rerere diff to sanity-check recorded resolutions.
Worktrees
Multiple working directories sharing one .git — parallel branches without stashing/switching. This is the mechanism loom uses for parallel stages.
git worktree list
git worktree add ../feat-auth feature/user-auth # new dir on a branch
git worktree add --detach ../test-v1.2 v1.2.0 # detached, for testing a tag
git worktree remove ../feat-auth # clean removal (must be clean, on another branch)
git worktree prune # after a dir was deleted by hand
# Loom: each stage runs in .worktrees/<stage-id>/ on branch loom/<stage-id>
ls .worktrees/
⚠ You cannot check out the same branch in two worktrees. ⚠ A worktree remove on a dirty tree fails — commit or discard first.
Recovery (nothing is truly lost for ~90 days)
git reflog is the undo history for HEAD movements — the escape hatch for "I reset --hard'd / rebased / deleted a branch and lost commits."
git reflog # find the sha you were at before the mistake
git reset --hard HEAD@{2} # jump back to that state
git branch recovered <sha> # or resurrect a lost branch/commit
git cherry-pick <sha> # pull back a single lost commit
reset variants: --soft (keep changes staged) · --mixed/default (keep unstaged) · --hard (discard — the only destructive one). git revert <sha> undoes a commit with a new commit — the safe choice on shared history (no rewrite).
Bisect (find the commit that introduced a bug)
git bisect start && git bisect bad && git bisect good v1.2.0
# git checks out the midpoint; test it, then:
git bisect good # or: git bisect bad
git bisect run ./test.sh # fully automated: script exits 0=good, non-0=bad
git bisect reset
Bisect is only reliable if history is bisectable — every commit builds/tests. See /loom-debugging for using bisect within a root-cause investigation.
Gotchas
- ⚠
reset --hardandcheckout -- <file>discard uncommitted work with no reflog entry — it's gone.git stashfirst if unsure. - ⚠
git clean -fddeletes untracked files irreversibly; alwaysgit clean -n(dry run) first. - ⚠
pulldefaults to merge, creating noise commits on feature branches.git pull --rebase(or setpull.rebase = true) keeps them linear — but only on unshared branches. - ⚠
cherry-pickduplicates a commit under a new sha; cherry-picking across branches you'll later merge causes "already applied" conflicts. - ⚠
commit --amendrewrites the last commit — safe only if unpushed.
Verify before done
- [ ] No history rewrite (
rebase/amend/force) on a shared/public branch - [ ] Force-push, if any, is
--force-with-leaseon your own branch only - [ ] Commits atomic, conventional-format, refactor separated from behavior change
- [ ] Tests run after every conflict resolution (textual merge ≠ semantic correctness)
- [ ] Loom: staged specific files (never
git add -A/.); stayed within the worktree - [ ]
rerereenabled for repeated/long-lived merge work