From bd0770b9c0885137d5bf6a0302a86ebeef0548d1 Mon Sep 17 00:00:00 2001 From: AvivYossef-starkware Date: Mon, 11 May 2026 12:45:29 +0300 Subject: [PATCH] scripts: add merge-branches Claude Code skill --- .claude/commands/merge-branches.md | 190 +++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 .claude/commands/merge-branches.md diff --git a/.claude/commands/merge-branches.md b/.claude/commands/merge-branches.md new file mode 100644 index 00000000000..32a6176f7b7 --- /dev/null +++ b/.claude/commands/merge-branches.md @@ -0,0 +1,190 @@ +--- +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. +--- + +# Merge Branches + +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. + +## Background + +- 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. +- 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. +- Graphite (`gt`) is the project's git wrapper — use it for commits and pushes. + +--- + +## Step 1: Gather inputs + +Ask the user (use `AskUserQuestion`): + +1. **Source branch** (the branch being merged *from*, e.g. `main-v0.14.2`). +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. +3. **Use a worktree?** Recommend yes — merges leave the local branch in an unusual state. If yes, use `EnterWorktree` (creating one based on `origin/` via `git worktree add` if a custom base ref is needed). + +Confirm the choices in one sentence before proceeding. + +--- + +## Step 2: Fetch and run the merge script + +```bash +git fetch origin +python3 scripts/merge_branches.py --src --dst +``` + +Capture the script's output. From it, extract: +- The new merge branch name (`/merge--into--`). +- The PR URL / number created by `gh pr create`. +- The list of conflicted files (printed under `git status -s | grep -E ...`). + +If the script ran with `gt`-untracked state, run `gt track --parent ` so subsequent `gt modify`/`gt submit` work. + +--- + +## Step 3: Enumerate conflicts + +Use `grep` to find conflict markers in every reported file: + +```bash +grep -n "<<<<<<\|>>>>>>\|||||||" +``` + +For each conflict region, capture: +- The file and line range. +- The **HEAD** (destination) section. +- The **base** section (between `|||||||` and `=======`) — `git config merge.conflictstyle diff3` is set by the script, so the merge base is shown explicitly. +- The **incoming** (source) section. + +Also count conflicts per file so the user knows how many decisions to expect. + +--- + +## Step 4: Identify authors for each conflict + +For each conflicting hunk, identify who introduced each side so they can be tagged later. Run on the merge-base: + +```bash +MERGE_BASE=$(git merge-base origin/ origin/) +# Author of dst-side change: +git log --format='%h %an <%ae> %s' -S '' "$MERGE_BASE..origin/" -- +# Author of src-side change: +git log --format='%h %an <%ae> %s' -S '' "$MERGE_BASE..origin/" -- +``` + +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`. + +Record `{ file, line, side, author_handle, pr_number }` for each conflict — this drives Step 7. + +--- + +## Step 5: Resolve conflicts one by one + +For **each** conflict region (in file order, top to bottom within a file): + +1. Show the user a tight summary: + - **File and line.** + - **What the destination side did** (and which PR / who). + - **What the source side did** (and which PR / who). + - **Proposed resolution** with reasoning. Common patterns: + - *Orthogonal changes* → keep both (e.g. two independent params added at the same position). + - *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. + - *Same semantic change in different terms* → pick one, ensure all call sites match. +2. Use `AskUserQuestion` with options: + - **Accept** the proposed resolution. + - **Edit** / give feedback (free text via "Other"). + - **Skip** — leave the conflict markers, come back later. +3. On accept, apply the change with `Edit` (replace the entire `<<<<<<<` … `>>>>>>>` block, including markers, with the resolved text). +4. After all conflicts in a file are resolved, verify no markers remain in that file. + +Do NOT batch all proposals before asking — propose, ask, apply, then move on. The user wants to think through each one. + +--- + +## Step 6: Build-verify and catch silent merge skews + +Run `cargo check` on every crate that owns a conflicted file: + +```bash +cargo check -p +``` + +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: + +1. Read the error. +2. Identify which side introduced the breaking change (usually `main`-side refactors). +3. Propose a fix to the user with the same Accept / Edit / Skip flow as Step 5. +4. Record it as an additional "silent skew" entry for Step 7. + +Repeat until the build is clean. + +--- + +## Step 7: Commit and push via Graphite + +```bash +git add +gt modify -cam "fix_conflicts" +gt submit --force +``` + +Use `--force` — the merge script already pushed the branch, so Graphite will refuse a normal submit. The local state is authoritative at this point. + +Confirm the PR was updated by re-checking `gt submit` output or `gh pr view `. + +--- + +## Step 8: Post per-file inline review comments + +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: + +- `path` — repo-relative file path. +- `line` — the line number of the resolved hunk in the **head** commit (the `fix_conflicts` commit). Use `git rev-parse HEAD` for `commit_id`. +- `side: "RIGHT"`. +- `body` — explain the conflict, the resolution, and cc the authors with `@`. + +Suggested comment template: + +```markdown +**Conflict:** . +- `` (@, #) . +- `` (@, #) . + +**Resolution:** . + +cc @ @ +``` + +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. + +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: + +```bash +gh api repos///pulls//reviews \ + --method POST --input /tmp/pr_review.json +``` + +The top-level `body` of the review should be a short header: *"Per-file conflict resolution notes (commit `fix_conflicts`). Source side: @. Destination side: @. Inline notes below."* Set `event: "COMMENT"` (not `APPROVE` or `REQUEST_CHANGES`). + +Clean up `/tmp/pr_review.json` afterward. + +--- + +## Step 9: Report back + +Tell the user: +- PR URL. +- Commit SHA of `fix_conflicts`. +- Number of conflicts resolved + number of silent skews fixed. +- Link to the posted review. + +Then stop. Do not merge the PR — that's a separate decision. + +--- + +## Notes + +- **Never** mask a failing `cargo check` with `#[ignore]` or by deleting code — investigate the skew (Step 6). +- **Never** amend the merge commit — keep `fix_conflicts` as a separate commit so reviewers can see the resolution diff cleanly. +- 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. +- If `mcp__github__pull_request_read` is unavailable, fall back to `gh pr view --json author` to fetch the GitHub handle.