feat(runway): git-backed merger with REBASE - #460
Draft
behinddwalls wants to merge 1 commit into
Draft
Conversation
## Summary ### Why? Runway's `merger` extension has had exactly one implementation — `noop`, which always succeeds. Nothing actually merges anything. This lands the first real backend: a `Merger` driven by the `git` CLI against a local checkout. It ships REBASE only. The strategy-specific apply paths are small and independent, but the machinery underneath them — the pinned git runtime, the reset/apply/push cycle, contention retry, dry-run discard, conflict classification — is shared and is the bulk of what needs review. Landing it with one strategy keeps that review separable from the per-strategy mechanics that follow in this stack. ### What? Adds `runway/extension/merger/git`. **Model.** A request is an ordered list of steps; each step names a change (github-family URIs, each ending in a full head commit SHA) and a strategy. Steps apply in order on top of the target tip — earlier steps are the in-flight base, the last is the candidate. Each step yields one `StepResult` whose outputs are the revisions it produced, in application order. Because the URI carries only the head SHA, a change *is* its head commit; multi-commit fidelity is not expressible in the current contract. **REBASE** cherry-picks each URI's head commit onto the tip. A pick whose content is already on the target is reported with no output rather than failing, which is what makes redelivery idempotent; an empty pick is rolled back. **Atomicity and contention.** Nothing reaches the remote until the final push. A step that fails to apply aborts its in-progress git operation and returns without pushing. If the push is rejected because the remote tip moved between reset and push, the whole reset/apply/push cycle retries up to a bounded number of attempts, re-fetching the tip to confirm that is what happened. **Dry run.** `CheckMergeability` runs the identical apply path but never pushes, then resets the checkout and reports empty outputs. Intermediate steps are still committed locally so a multi-step check sees the same conflict surface a real merge would. **Runtime hygiene.** Every invocation uses an explicitly pinned git runtime (executable, exec-path, template dir) and a scrubbed environment — no system or global config, no interactive prompts. That leaves no ambient identity, so the committer name and email are injected per-invocation. Conflicts surface as `merger.ErrConflict` and unusable requests as `merger.ErrInvalidRequest`; both are terminal, everything else is a plain error the consumer retries. `isConcreteStrategy` currently admits only REBASE, so a step naming SQUASH_REBASE, MERGE, or PROMOTE is rejected as an invalid request until its apply path lands. The merger is not wired into the server yet — that is the last change in this stack. Until then the server still builds the noop factory, so this adds no production behavior. ## Test Plan ✅ `bazel test //runway/...` — 6/6 targets pass, including the new `//runway/extension/merger/git` suite (20s) The suite drives a real git binary against throwaway local repos: single and stacked URIs, already-landed changes, multi-step requests, conflicts, checkout recovery after a prior conflict, contention retry when the remote tip moves mid-cycle, give-up after max attempts, DEFAULT resolution, invalid requests, and the dry-run paths. It also asserts the pinned runtime is used and that the command environment is not inherited.
This was referenced Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why?
Runway's
mergerextension has had exactly one implementation —noop, which always succeeds. Nothing actually merges anything. This lands the first real backend: aMergerdriven by thegitCLI against a local checkout.It ships REBASE only. The strategy-specific apply paths are small and independent, but the machinery underneath them — the pinned git runtime, the reset/apply/push cycle, contention retry, dry-run discard, conflict classification — is shared and is the bulk of what needs review. Landing it with one strategy keeps that review separable from the per-strategy mechanics that follow in this stack.
What?
Adds
runway/extension/merger/git.Model. A request is an ordered list of steps; each step names a change (github-family URIs, each ending in a full head commit SHA) and a strategy. Steps apply in order on top of the target tip — earlier steps are the in-flight base, the last is the candidate. Each step yields one
StepResultwhose outputs are the revisions it produced, in application order. Because the URI carries only the head SHA, a change is its head commit; multi-commit fidelity is not expressible in the current contract.REBASE cherry-picks each URI's head commit onto the tip. A pick whose content is already on the target is reported with no output rather than failing, which is what makes redelivery idempotent; an empty pick is rolled back.
Atomicity and contention. Nothing reaches the remote until the final push. A step that fails to apply aborts its in-progress git operation and returns without pushing. If the push is rejected because the remote tip moved between reset and push, the whole reset/apply/push cycle retries up to a bounded number of attempts, re-fetching the tip to confirm that is what happened.
Dry run.
CheckMergeabilityruns the identical apply path but never pushes, then resets the checkout and reports empty outputs. Intermediate steps are still committed locally so a multi-step check sees the same conflict surface a real merge would.Runtime hygiene. Every invocation uses an explicitly pinned git runtime (executable, exec-path, template dir) and a scrubbed environment — no system or global config, no interactive prompts. That leaves no ambient identity, so the committer name and email are injected per-invocation.
Conflicts surface as
merger.ErrConflictand unusable requests asmerger.ErrInvalidRequest; both are terminal, everything else is a plain error the consumer retries.isConcreteStrategycurrently admits only REBASE, so a step naming SQUASH_REBASE, MERGE, or PROMOTE is rejected as an invalid request until its apply path lands.The merger is not wired into the server yet — that is the last change in this stack. Until then the server still builds the noop factory, so this adds no production behavior.
Test Plan
✅
bazel test //runway/...— 6/6 targets pass, including the new//runway/extension/merger/gitsuite (20s)The suite drives a real git binary against throwaway local repos: single and stacked URIs, already-landed changes, multi-step requests, conflicts, checkout recovery after a prior conflict, contention retry when the remote tip moves mid-cycle, give-up after max attempts, DEFAULT resolution, invalid requests, and the dry-run paths. It also asserts the pinned runtime is used and that the command environment is not inherited.
Stack