Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions runway/extension/merger/git/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["git_merger.go"],
importpath = "github.com/uber/submitqueue/runway/extension/merger/git",
visibility = ["//visibility:public"],
deps = [
"//api/base/mergestrategy/protopb:go_default_library",
"//api/runway/messagequeue:go_default_library",
"//api/runway/messagequeue/protopb:go_default_library",
"//platform/base/change/github:go_default_library",
"//platform/metrics:go_default_library",
"//runway/extension/merger:go_default_library",
"@com_github_uber_go_tally//:go_default_library",
"@org_uber_go_zap//:go_default_library",
],
)

go_test(
name = "go_default_test",
srcs = ["git_merger_test.go"],
data = [
"@git",
"@git//:git_receive_pack",
"@git//:git_upload_archive",
"@git//:git_upload_pack",
"@git//:templates",
"@git//:templates/description",
],
embed = [":go_default_library"],
env = {
"SUBMITQUEUE_TEST_GIT": "$(location @git//:git)",
"SUBMITQUEUE_TEST_GIT_TEMPLATE_DESCRIPTION": "$(location @git//:templates/description)",
},
deps = [
"//api/base/change/protopb:go_default_library",
"//api/base/mergestrategy/protopb:go_default_library",
"//api/runway/messagequeue:go_default_library",
"//api/runway/messagequeue/protopb:go_default_library",
"//runway/extension/merger:go_default_library",
"@com_github_stretchr_testify//assert:go_default_library",
"@com_github_stretchr_testify//require:go_default_library",
"@com_github_uber_go_tally//:go_default_library",
"@org_uber_go_zap//zaptest:go_default_library",
],
)
34 changes: 34 additions & 0 deletions runway/extension/merger/git/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# git merger

A `merger.Merger` backed by the `git` CLI operating on a local checkout. It applies a merge request's ordered steps onto a target branch, honoring each step's strategy, and — for a committing merge — pushes the result. It is constructed by the wiring layer (see [`service/runway`](../../../../service/runway)) with the checkout path, remote, target branch, pinned git runtime, committer identity, and the default strategy; the request itself carries only a queue name and the changes, so the merger reads change URIs straight from the payload (no store, no resolver).

## Model

A request is an ordered list of steps; each step names a change (a set of github-family URIs, each ending in a full head commit SHA) and a strategy. Steps are applied in order on top of the target tip — earlier steps are the in-flight base, the last step is the candidate. Each step yields one `StepResult`; the revisions a step produces on the target are its outputs, in application order.

The URI carries only the head commit SHA — no base and no commit range. A change is therefore its head commit; multi-commit fidelity for a rebase is not expressible in the current contract.

## Strategies

| Strategy | What it does | Outputs |
|---|---|---|
| `REBASE` | Cherry-picks each URI's head commit onto the tip, in order. A pick already present on the target is rebased out (reported, no output); an empty pick is rolled back. | one revision per newly-created commit |
| `DEFAULT` | Resolved to the instance's configured default strategy before any step runs. | per the resolved strategy |

`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.

## Committing, dry-run, atomicity, contention

`Merge` commits and reports outputs; `CheckMergeability` runs the identical apply but never pushes, then resets the checkout to discard the local commits and reports empty outputs. A multi-step check commits its intermediate steps locally so it sees the same conflict surface a real merge would.

For a committing merge 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 fails because the remote tip moved between reset and push, the whole reset/apply/push cycle is retried up to a bounded number of attempts; detection re-fetches the tip and compares it to the SHA the cycle was based on.

## Failure classification

A merge conflict surfaces as `merger.ErrConflict`; an unusable request (unsupported strategy, malformed URI) surfaces as `merger.ErrInvalidRequest`. Both are terminal — the controller publishes a `FAILED` result rather than retrying. Everything else (fetch/network/auth/push faults) is returned as a plain error for the consumer to retry.

## Runtime and identity

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.

Object availability relies on `git fetch <remote>` making the referenced SHAs reachable through the remote's refs. A deployment whose head commits live only under non-fetched refs (for example GitHub `refs/pull/*`) must arrange for those objects to be fetchable.
Loading