Add 'downstack merge' command to merge stacks#1152
Conversation
|
This change is part of the following stack: Change managed by git-spice. |
265d671 to
3e389f6
Compare
gs branch merge (bm) and gs ci merge-guard for stacked PR merging
gs branch merge (bm) and gs ci merge-guard for stacked PR merginggs branch merge (bm) for stacked PR merging
3e389f6 to
0d9161c
Compare
0d9161c to
19e5015
Compare
e4b5f2e to
ddbdacb
Compare
Introduce `gs branch merge` (gs bm) to merge pull requests via the forge API and clean up merged branches locally. Key behaviors and safeguards: - Fetch trunk after each merge completes so upstack rebases use the latest remote state, avoiding silent change loss. - Halt with an error if any branch has unpushed local commits, preventing silent data loss during post-merge cleanup. - Assert the expected head SHA at merge time via forge APIs (GitHub ExpectedHeadOid, GitLab SHA, ShamHub) to reject races where the remote branch changes between plan and merge. - After moving an immediate child onto the new base, restack all further descendants in topological order so the entire upstack chain stays consistent without manual intervention.
The branch merge command still expected stale-base validation to live behind its older command-local API, but PR #1154 moved that behavior into submit-specific preflight code. Extract stale-base detection into `spice.FindStaleBases` so submit and branch merge use the same graph and forge-state check. Branch merge now mirrors submit diagnostics while keeping its own `--no-branch-check` bypass wording. Refs #1154
Submit commands should not require a supported forge just to discover that no stale-base check is needed. `FindStaleBases` now accepts a repository opener and calls it only after local branch graph analysis finds candidate base changes. This keeps `--no-publish` usable with unsupported remotes while preserving stale-base validation when published downstack changes exist.
- Move the merge command from `branch merge` to `downstack merge` so the public command reflects that it operates on a downstack. Keep room for a future single-branch `branch merge` command. - Expand command help with a stack diagram, merge order, blocking behavior, and the local merge queue model. - Resolve the default branch in `AfterApply`, matching nearby command patterns and removing the separate branch resolution helper. - Move stale-base validation into the merge handler so command wiring stays thin, matching the submit handler boundary. - Rename `ChangeChecksStatus` to `ChangeChecksState` so the method and return type use symmetrical state terminology. - Add documented forge status constants for GitHub, GitLab, and Bitbucket, including links to the upstream API references. GitHub `EXPECTED` now remains pending instead of passing. - Inline one-use merge/check helpers and add error context around handler and repository-provider failures. - Remove the delete-handler descendant restack behavior because branch deletion should not automatically restack the full descendant chain. - Make the merge confirmation default to proceed in non-interactive mode, following the view behavior expected by other commands. - Replace timer allocation with `time.After` now that timers are collected automatically. Out of scope: - Add a future single-branch `branch merge` command. - Report individual blocking status names in merge errors. - Teach merge waiting about server-side merge queues and richer merge actions. - Replace merge cleanup with `sync.Handler` after issue #1134 is fixed.
|
@ed-irl I'm still working through this. Marked it as draft so I don't accidentally merge it. Update: I've made a bunch of changes. |
ShamHub needs to simulate forge defaults that are not exposed through the git-spice merge command yet. Add a test-script configuration command so scripts can choose how REST merge calls behave when they omit a merge method. The new `shamhub config mergeMethod <value>` surface accepts `merge` and `squash`. The REST merge endpoint uses that setting as its default, while direct merge requests can still specify their method explicitly.
`downstack merge` needs to replay each surviving branch after its base is merged on the forge. The first review pass removed that behavior from branch deletion, which was the right boundary, but squash merges still need the branch replay before the next Change Request can be updated and merged. Move that work into the merge queue itself. After a merge settles, the handler syncs trunk through the repo sync handler, restacks the next branch, updates its Change Request through the submit handler, waits for checks on the updated head, and then continues the queue. Add a ShamHub squash-merge regression test that merges multiple branches through `downstack merge`. The test covers the case where the server creates squash commits, so the next branch must be replayed before it can be merged safely.
`--no-wait` is a fire-and-forget mode for a single merge request. It cannot safely drive the local merge queue because each later branch needs the previous merge to settle before sync, restack, submit, and checks can run. Reject multi-branch `downstack merge --no-wait` plans before prompting or merging. For the waited path, let repo sync own merged branch cleanup and merged-downstack bookkeeping instead of deleting branches directly from the merge handler. The no-wait script covers both the multi-branch rejection and the single-branch fire-and-forget path.
Move stale-base detection into `spice.FindStaleBases` so submit validation can use a shared graph and forge-state check instead of keeping that logic inside the submit handler. The submit handler still owns its user-facing diagnostics. It logs each stale local branch/base edge and returns the existing aggregate error that asks the user to run `gs repo sync` or submit with `--force`. `FindStaleBases` accepts a repository opener and calls it only after local graph analysis finds candidate bases. This keeps submit flows that do not need stale-base validation from requiring a supported forge. Extracted from #1152
Extracted from #1152 This adds forge-level APIs for merging changes and reading aggregate CI/checks state. GitHub, GitLab, Bitbucket, and ShamHub now expose the new forge methods where their backing APIs support them. Checks-state integration tests now cover pending, passing, and failing checks for ShamHub, GitHub, and GitLab. ShamHub also gained support for simulating checks state through its existing test forge system, including a `shamhub set-status` command for setting a PR's synthetic checks state during tests. Finally, this removes the unused `ErrMergeNotAllowed` sentinel from the forge contract. It was only declared and documented here, and was unused in the `branch-merge` branch as well. --------- Co-authored-by: Edmund Kohlwey <ed@irl.llc>
#: Conflicts: #: internal/forge/forge.go #: internal/forge/shamhub/checks.go #: internal/forge/shamhub/cli.go #: internal/gateway/bitbucket/api.go #: internal/gateway/gitlab/api.go
gs branch merge (bm) for stacked PR mergingDownstack merge now has script coverage for the CI states that must stop the local merge queue before or after a server-side merge. The tests assert full stderr for failed and pending checks, and use `cmpenvJSON` fixtures for ShamHub change state. That keeps the open and merged CR contracts visible without baking in test server ports. ShamHub API merges now get a deterministic default commit time from the CR head when no explicit time is provided. That keeps server-side merge SHAs stable for script fixtures while preserving the existing CLI merge path that can supply a script time. The test-script skill documents the `cmp` to `--update` to `$SHAMHUB_URL` workflow for ShamHub JSON dumps.
Sync retargets surviving upstack CRs on the forge after merged branch cleanup retargets the local stack metadata. Both operations are useful, but the old forge progress line looked like a duplicate of the local retarget message. Report forge retarget progress with the branch name and Change ID, such as `feature2: retargeting #2 onto main`, so the output makes clear that the CR is being edited. The merge queue no longer emits its own pre-restack progress line. It relies on the restack handler's completion log instead, matching repo sync's ownership of restack output. Script coverage was swept for the changed sync retarget log shape, and the direct merge retarget helper keeps its existing wording for paths that do not go through sync.
When `downstack merge` cannot compare a branch with its remote tracking ref, it still avoids blocking the merge as a false positive. That path is expected to be unusual because submitted branches normally have remote-tracking refs. Emit a warning with the branch name, remote ref, and underlying error before skipping the check. This keeps the current non-blocking behavior while making the missing safety signal visible to the user.
Mark `gs downstack merge` experimental while the local merge queue behavior is still settling. The merge plan now uses one branch graph snapshot to derive the downstack queue, change metadata, and stale-base validation. During execution, each queued change is checked against trunk, restacked only when needed, submitted as an update-only existing CR, and logged with the change URL before merging. This keeps squash-merge handling in the merge queue instead of delete cleanup, so the next branch can be restacked, submitted, checked, and merged after its base lands. Script coverage exercises the experimental gate, squash merges, CI failures on the next queued branch, and the updated progress output.
|
Thanks for the change, @ed-irl! |
Summary
This adds
gs downstack mergeas an experimental commandfor merging a stack of Change Requests bottom-up through the forge API.
The command acts as a local merge queue.
It merges the lowest open Change Request first,
waits for that merge to settle,
syncs trunk,
restacks and updates the next branch,
waits for CI checks on the updated Change Request,
and repeats until the requested downstack has been merged.
The command is gated behind the
downstackMergeexperimentwhile the merge-queue behavior is still settling.
Merge flow
For a stack like:
running
gs downstack mergefromfeature3merges:After each merge,
repo sync updates trunk,
deletes merged branches,
and retargets their upstacks.
The next queued branch is then restacked,
submitted as an update to its existing Change Request,
checked for passing CI,
and merged.
This keeps squash-merge handling in the merge queue itself,
rather than relying on delete cleanup to repair the stack afterward.
Test support
ShamHub now supports:
shamhub config mergeMethod mergeshamhub config mergeMethod squashThis lets script tests exercise server-side squash merge behavior
for
gs downstack merge.Refs #65