You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## Summary
### Why?
The git merger landed with REBASE only. `SQUASH_REBASE` and `MERGE` are part of the wire contract SubmitQueue already publishes against, and until they apply here a request naming either is rejected as an invalid request. This adds the two remaining transforming strategies on top of the shared apply machinery.
### What?
**SQUASH_REBASE** applies each change exactly like REBASE — replaying every commit it introduces — then collapses what that change produced into a single commit. The squash unit is the change: a change of ten commits becomes one, and a step whose change carries several URIs yields one commit per URI. Those URIs are a stack of pull requests, and squashing them together would erase the per-PR boundary the stack exists to express.
Two degenerate cases produce no output rather than an empty commit. A change already present on the target creates no commits, so there is nothing to squash. A change that does create commits whose net tree matches the base would squash to an empty commit, so the intermediates are dropped. Both keep redelivery idempotent.
**MERGE** creates a `--no-ff` merge commit per change, which keeps the change's original commits reachable through second-parent history — the property that separates it from the picking strategies, which rewrite those hashes. A change already contained in HEAD is skipped rather than merged again.
**Not every failed merge is a conflict.** `applyMerge` previously reported any `git merge` failure as `ErrConflict`, which tells the client its change collides with the target even when nothing collided. Failures are now classified, and the case that matters in practice is an unrelated history.
**Importing an unrelated history.** A repository migration arrives as an ordinary change in the target repo whose branch carries the source repo's whole history. Being in the target repo is what makes the commits fetchable; it says nothing about ancestry, and git refuses to merge two graphs with no common ancestor. `AllowUnrelatedHistories` lifts that refusal for a queue that exists to perform such imports. It is off by default because the refusal is a genuine safeguard — with it always on, merging the wrong object silently produces a nonsense result instead of failing. Without the option, the refusal is now reported as an invalid request rather than a conflict.
MERGE is the only strategy that can serve a migration: it is the only one that preserves the imported commits' original hashes, and the picking strategies have no range to compute across disjoint graphs, so they reject such a change explicitly and say so.
## Test Plan
✅ `bazel test //runway/extension/merger/git:go_default_test` — passes (58s)
New coverage: SQUASH_REBASE collapsing a multi-commit change into one commit while landing all of its content, a two-URI change yielding one squashed commit per URI in application order, SQUASH_REBASE over an already-landed change producing none, MERGE creating one merge commit for a multi-commit change, MERGE skipping a change already an ancestor of the tip, and the MERGE dry-run path.
For migration specifically: importing a three-commit unrelated history and asserting every imported commit is reachable **under its original hash**, the merge commit has two parents, and the target keeps its own files; redelivery of the same request being a no-op; the import rejected as an invalid request (explicitly not a conflict) when the option is off, leaving the remote and checkout untouched; and both picking strategies rejecting an unrelated history rather than rewriting it.
Copy file name to clipboardExpand all lines: runway/extension/merger/git/README.md
+14-2Lines changed: 14 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -38,9 +38,21 @@ Fetching by SHA guarantees the merger applies exactly the commit a URI names —
38
38
| Strategy | What it does | Outputs |
39
39
|---|---|---|
40
40
|`REBASE`| Cherry-picks every commit each change introduces onto the tip, in order. A commit already present on the target is skipped (no output), as is one that was empty to begin with. | one revision per newly-created commit |
41
+
|`SQUASH_REBASE`| Applies each change like `REBASE`, then collapses the commits it produced into a single commit (squash unit = the change, not the step). | one revision per change, or none for a change already present |
42
+
|`MERGE`| Creates a `--no-ff` merge commit per change, keeping the change's original commits reachable through second-parent history. A commit already contained in the tip is skipped. | the merge-commit revision(s) |
41
43
|`DEFAULT`| Resolved to the instance's configured default strategy before any step runs. | per the resolved strategy |
42
44
43
-
`REBASE` is the only strategy implemented so far. `SQUASH_REBASE`, `MERGE`, and `PROMOTE` are defined by the wire contract but not yet applied here — a step naming one is rejected as an invalid request.
45
+
`PROMOTE` is defined by the wire contract but not yet applied here — a step naming it is rejected as an invalid request.
46
+
47
+
## Importing an unrelated history
48
+
49
+
A repository migration arrives as an ordinary change in the target repo whose branch carries the source repo's entire history. Living in the target repo is what makes its commits fetchable; it says nothing about ancestry, and the two graphs still share no common ancestor, so git refuses the merge by default.
50
+
51
+
`MERGE` is the only strategy that can serve this, because it is the only one that leaves the imported commits reachable under their original hashes — the picking strategies would rewrite every one of them, and have no range to compute in the first place, so they reject such a change outright.
52
+
53
+
The refusal is lifted by a per-instance option rather than always: it is a real safeguard, and without it a merge of the wrong object fails loudly instead of quietly producing a nonsense result. A queue that exists to perform imports turns it on. A refusal that surfaces without the option is reported as an invalid request, not as a conflict — nothing collided.
54
+
55
+
Redelivery is safe: once imported, the source head is contained in the target, so the change is skipped rather than merged twice.
44
56
45
57
## Committing, dry-run, atomicity, contention
46
58
@@ -56,7 +68,7 @@ The distinction between the last two matters operationally: a commit that is mis
56
68
57
69
## Runtime and identity
58
70
59
-
Every git invocation uses the pinned runtime (explicit executable, exec-path, and template dir) and a scrubbed environment: no ambient configuration, no system or global git config, no interactive prompts. Because that leaves no ambient identity, the committer name and email are injected per-invocation, which the commit-creating `REBASE` strategy requires.
71
+
Every git invocation uses the pinned runtime (explicit executable, exec-path, and template dir) and a scrubbed environment: no ambient configuration, no system or global git config, no interactive prompts. Because that leaves no ambient identity, the committer name and email are injected per-invocation, which the commit-creating strategies (`REBASE`, `SQUASH_REBASE`, `MERGE`) require.
60
72
61
73
Scrubbing denies git ambient *configuration* — anything that could change what a merge produces. It deliberately does not deny it the means to reach the remote, so the agent socket, `PATH`, ssh-command, TLS and proxy variables are inherited when set. Without them an SSH remote cannot authenticate and git cannot even exec `ssh`; none of them can influence merge semantics. A deployment needing more can name additional variables on the runtime.
0 commit comments