|
| 1 | +--- |
| 2 | +description: Merge a source branch into a destination branch via scripts/merge_branches.py, walk through each conflict interactively, commit as fix_conflicts, and post inline PR review comments tagging the authors who caused each conflict. |
| 3 | +--- |
| 4 | + |
| 5 | +# Merge Branches |
| 6 | + |
| 7 | +Run a branch-merge end-to-end: kick off `scripts/merge_branches.py`, resolve each conflict interactively with the user, build-verify, commit, push, and document every resolution as an inline review comment on the resulting PR. Follow the steps in order. Do not batch-skip steps — interactivity is the point. |
| 8 | + |
| 9 | +## Background |
| 10 | + |
| 11 | +- The merge script `scripts/merge_branches.py` creates a fresh branch off the destination, merges the source, and pushes a PR with conflict markers committed in-place. It also adds a comment to the PR linking the conflicting files on each side. |
| 12 | +- This skill picks up from that point: it resolves the conflict markers, verifies the build, commits, and posts per-conflict inline review comments tagging the people whose changes collided. |
| 13 | +- Graphite (`gt`) is the project's git wrapper — use it for commits and pushes. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Step 1: Gather inputs |
| 18 | + |
| 19 | +Ask the user (use `AskUserQuestion`): |
| 20 | + |
| 21 | +1. **Source branch** (the branch being merged *from*, e.g. `main-v0.14.2`). |
| 22 | +2. **Destination branch** (the branch being merged *into*, e.g. `main`). If the source has a default in `scripts/merge_paths.json`, offer it as a recommended option. |
| 23 | +3. **Use a worktree?** Recommend yes — merges leave the local branch in an unusual state. If yes, use `EnterWorktree` (creating one based on `origin/<dst>` via `git worktree add` if a custom base ref is needed). |
| 24 | + |
| 25 | +Confirm the choices in one sentence before proceeding. |
| 26 | + |
| 27 | +--- |
| 28 | + |
| 29 | +## Step 2: Fetch and run the merge script |
| 30 | + |
| 31 | +```bash |
| 32 | +git fetch origin <src-branch> <dst-branch> |
| 33 | +python3 scripts/merge_branches.py --src <src-branch> --dst <dst-branch> |
| 34 | +``` |
| 35 | + |
| 36 | +Capture the script's output. From it, extract: |
| 37 | +- The new merge branch name (`<user>/merge-<src>-into-<dst>-<timestamp>`). |
| 38 | +- The PR URL / number created by `gh pr create`. |
| 39 | +- The list of conflicted files (printed under `git status -s | grep -E ...`). |
| 40 | + |
| 41 | +If the script ran with `gt`-untracked state, run `gt track --parent <dst-branch>` so subsequent `gt modify`/`gt submit` work. |
| 42 | + |
| 43 | +**If the script errors out or `git status` shows entries that aren't `UU` (both modified)** — e.g. `DU` / `UD` (delete-vs-modify), `AU` / `UA` (rename-vs-modify), `AA` (both added) — the merge produced conflicts with no in-file markers. The marker-grep in Step 3 will miss them entirely. Surface these to the user explicitly, propose a resolution per file (keep / drop / port the change), and apply with `git rm` or `git add` once decided. Only then proceed to Step 3 for the remaining `UU` files. |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +## Step 3: Enumerate conflicts |
| 48 | + |
| 49 | +**Prerequisite** — before doing anything in this step, have the user mark all files in the PR as viewed on GitHub (or Reviewable). Otherwise the `fix_conflicts` commit will not appear as a separate revision in review, and the per-conflict inline comments in Step 8 will land in a diff that mixes the merge markers with the resolution, making the review unreadable. Confirm with the user that this is done before continuing. |
| 50 | + |
| 51 | +Use `grep` to find conflict markers in every reported file: |
| 52 | + |
| 53 | +```bash |
| 54 | +grep -n "<<<<<<\|>>>>>>\|||||||" <files> |
| 55 | +``` |
| 56 | + |
| 57 | +For each conflict region, capture: |
| 58 | +- The file and line range. |
| 59 | +- The **HEAD** (destination) section. |
| 60 | +- The **base** section (between `|||||||` and `=======`) — `git config merge.conflictstyle diff3` is set by the script, so the merge base is shown explicitly. |
| 61 | +- The **incoming** (source) section. |
| 62 | + |
| 63 | +Also count conflicts per file so the user knows how many decisions to expect. |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## Step 4: Identify authors for each conflict |
| 68 | + |
| 69 | +For each conflicting hunk, identify who introduced each side so they can be tagged later. Run on the merge-base: |
| 70 | + |
| 71 | +```bash |
| 72 | +MERGE_BASE=$(git merge-base origin/<src> origin/<dst>) |
| 73 | +# Author of dst-side change: |
| 74 | +git log --format='%h %an <%ae> %s' -S '<identifying-token>' "$MERGE_BASE..origin/<dst>" -- <file> |
| 75 | +# Author of src-side change: |
| 76 | +git log --format='%h %an <%ae> %s' -S '<identifying-token>' "$MERGE_BASE..origin/<src>" -- <file> |
| 77 | +``` |
| 78 | + |
| 79 | +Pick a distinctive token from each side of the conflict (a new identifier, a removed crate name, etc.). Look up the GitHub handle of the author by reading the PR (`mcp__github__pull_request_read` with the `#NNNN` from the commit subject) and capturing `user.login`. |
| 80 | + |
| 81 | +Record `{ file, line, side, author_handle, pr_number }` for each conflict — this drives Step 7. |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Step 5: Resolve conflicts one by one |
| 86 | + |
| 87 | +For **each** conflict region (in file order, top to bottom within a file): |
| 88 | + |
| 89 | +1. Show the user a tight summary: |
| 90 | + - **File and line.** |
| 91 | + - **What the destination side did** (and which PR / who). |
| 92 | + - **What the source side did** (and which PR / who). |
| 93 | + - **Proposed resolution** with reasoning. Common patterns: |
| 94 | + - *Orthogonal changes* → keep both (e.g. two independent params added at the same position). |
| 95 | + - *One side removed something now unused, the other added something now used* → drop the removed item, keep the added one. Verify "unused" with `grep` before claiming it. |
| 96 | + - *Same semantic change in different terms* → pick one, ensure all call sites match. |
| 97 | +2. Use `AskUserQuestion` with options: |
| 98 | + - **Accept** the proposed resolution. |
| 99 | + - **Edit** / give feedback (free text via "Other"). |
| 100 | + - **Skip** — leave the conflict markers, come back later. |
| 101 | +3. On accept, apply the change with `Edit` (replace the entire `<<<<<<<` … `>>>>>>>` block, including markers, with the resolved text). |
| 102 | +4. After all conflicts in a file are resolved, verify no markers remain in that file. |
| 103 | + |
| 104 | +Do NOT batch all proposals before asking — propose, ask, apply, then move on. The user wants to think through each one. |
| 105 | + |
| 106 | +--- |
| 107 | + |
| 108 | +## Step 6: Build-verify and catch silent merge skews |
| 109 | + |
| 110 | +Run `cargo check` on every crate that owns a conflicted file: |
| 111 | + |
| 112 | +```bash |
| 113 | +cargo check -p <crate> |
| 114 | +``` |
| 115 | + |
| 116 | +Textual conflicts are only half the story — the merge driver doesn't catch **silent API skews** where one side renamed a type or changed a signature and the other side's text merged cleanly but no longer typechecks. If `cargo check` fails: |
| 117 | + |
| 118 | +1. Read the error. |
| 119 | +2. Identify which side introduced the breaking change (usually `main`-side refactors). |
| 120 | +3. Propose a fix to the user with the same Accept / Edit / Skip flow as Step 5. |
| 121 | +4. Record it as an additional "silent skew" entry for Step 7. |
| 122 | + |
| 123 | +Repeat until the build is clean. |
| 124 | + |
| 125 | +--- |
| 126 | + |
| 127 | +## Step 7: Commit and push |
| 128 | + |
| 129 | +Stage the resolved files and create the `fix_conflicts` commit as a **new commit on top of the merge commit** (do not amend — the resolution needs to stay as its own diff in review). Then push. |
| 130 | + |
| 131 | +The commit message must satisfy the project's commitlint config (`commitlint.config.js` — `scope: subject` format, scope must be in `AllowedScopes`). For a generic merge resolution use `workspace: fix merge conflicts`; if the conflicts were limited to one crate, narrow the scope (e.g. `blockifier: fix merge conflicts`). |
| 132 | + |
| 133 | +Examples (pick what fits the user's workflow): |
| 134 | + |
| 135 | +```bash |
| 136 | +# Graphite (project default — gt modify -c adds a new commit, doesn't amend) |
| 137 | +git add <resolved-files> |
| 138 | +gt modify -cam "workspace: fix merge conflicts" |
| 139 | +gt submit |
| 140 | + |
| 141 | +# Plain git |
| 142 | +git add <resolved-files> |
| 143 | +git commit -m "workspace: fix merge conflicts" |
| 144 | +git push |
| 145 | +``` |
| 146 | + |
| 147 | +Confirm the PR was updated by checking the push output or `gh pr view <PR#>`. |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +## Step 8: Post per-file inline review comments |
| 152 | + |
| 153 | +Build a single PR review with one inline comment per conflict (and per silent-skew fix). Use `gh api` to POST to `/repos/{owner}/{repo}/pulls/{PR#}/reviews`. Each comment object needs: |
| 154 | + |
| 155 | +- `path` — repo-relative file path. |
| 156 | +- `line` — the line number of the resolved hunk in the **head** commit (the `fix_conflicts` commit). Use `git rev-parse HEAD` for `commit_id`. |
| 157 | +- `side: "RIGHT"`. |
| 158 | +- `body` — explain the conflict, the resolution, and cc the authors with `@<handle>`. |
| 159 | + |
| 160 | +Suggested comment template: |
| 161 | + |
| 162 | +```markdown |
| 163 | +**Conflict:** <one-line description>. |
| 164 | +- `<dst-branch>` (@<dst-author>, #<dst-pr>) <what they did>. |
| 165 | +- `<src-branch>` (@<src-author>, #<src-pr>) <what they did>. |
| 166 | + |
| 167 | +**Resolution:** <what we did and why>. |
| 168 | + |
| 169 | +cc @<dst-author> @<src-author> |
| 170 | +``` |
| 171 | + |
| 172 | +For silent skews (Step 6), label them as **"Silent merge skew (not a textual conflict)"** so reviewers know why a comment landed somewhere with no conflict markers. |
| 173 | + |
| 174 | +Write the review payload to a tmp JSON file (because the bodies contain newlines and markdown that don't survive `-F` flags cleanly) and submit: |
| 175 | + |
| 176 | +```bash |
| 177 | +gh api repos/<owner>/<repo>/pulls/<PR#>/reviews \ |
| 178 | + --method POST --input /tmp/pr_review.json |
| 179 | +``` |
| 180 | + |
| 181 | +The top-level `body` of the review should be a short header: *"Per-file conflict resolution notes (commit `fix_conflicts`). Source side: @<src-author>. Destination side: @<dst-author>. Inline notes below."* Set `event: "COMMENT"` (not `APPROVE` or `REQUEST_CHANGES`). |
| 182 | + |
| 183 | +Clean up `/tmp/pr_review.json` afterward. |
| 184 | + |
| 185 | +--- |
| 186 | + |
| 187 | +## Step 9: Report back |
| 188 | + |
| 189 | +Tell the user: |
| 190 | +- PR URL. |
| 191 | +- Commit SHA of `fix_conflicts`. |
| 192 | +- Number of conflicts resolved + number of silent skews fixed. |
| 193 | +- Link to the posted review. |
| 194 | + |
| 195 | +Then stop. Do not merge the PR — that's a separate decision. |
| 196 | + |
| 197 | +--- |
| 198 | + |
| 199 | +## Notes |
| 200 | + |
| 201 | +- **Never** mask a failing `cargo check` with `#[ignore]` or by deleting code — investigate the skew (Step 6). |
| 202 | +- **Never** amend the merge commit — keep `fix_conflicts` as a separate commit so reviewers can see the resolution diff cleanly. |
| 203 | +- If the user says **Skip** on any conflict, do not commit until they come back and resolve it — leaving conflict markers in a commit will break CI. |
| 204 | +- If `mcp__github__pull_request_read` is unavailable, fall back to `gh pr view <PR#> --json author` to fetch the GitHub handle. |
0 commit comments