Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

The merge queue builds, tests and lands each pull request by its own merge method. Each state was built in a sandbox with git merge commits, then landed by fast-forward as it was, so a queued pull request always landed as a merge commit whatever method was asked; and since every sandbox built its own copy of the entries ahead, an entry's state did not descend from the one that landed before it, and a batch fell back to testing again. Now the work service builds every state in the repository itself, with repos' new build_queue_step: the entry landed by its method (merge commit, squash, rebase or fast-forward) on the state of the entry ahead of it, or on the default branch for the first, with the commits land would make there (merge_methods.rs make_on, shared with land_by; queue_step_on decides a step), pushed to the attempt's g1t-queue/ branch. Landing is then a fast-forward to exactly the tested commit, recorded as the entry's method with that commit as its merge commit; settle's resume check reads merge_commit. A fast-forward entry must contain what is ahead of it; a clash names the files and the nearest entry ahead that changed one (clashed_with); an entry that cannot be built leaves, and those behind are built in the next batch without it. The method is the one asked for when merging into the queue, else the merge queue rule's merge_method, each checked against the repository and the branch's rules; the commit message written then is kept (work 0044: queue_entries merge_method, commit_title, commit_message), else the repository's default, worked out when first built and kept in the tested stack (QueueStackItem method, message, author) so that narrowing a failure rebuilds the same commits; probes are built the same way. The landing gate judges the entry's method. No sandbox builds queue states any more: queue_build returns no jobs. Docs: the merge queue guide (how entries are tested and land, by method; no plan needed for building, merge_group runs billed as workflow runs), pull requests' merge methods, the work architecture page, and the API's merge and queue descriptions (OpenAPI regenerated). Deploy work migration 0044 first, then repos, then work. Checked: cargo test for g1t-repos (341, new: each entry lands by its method on the state ahead, a fast-forward entry is its own head and a clash names the files) and g1t-work (186, new: an entry keeps how it lands and one queued before methods merges; a clash names the nearest entry ahead), the API's OpenAPI test, the CLI's operation table and reference.

syntaqxcommitted Parent4fff915Browse files
16 files+869−2630/16 viewed
+2−2
15851585 "Search all of g1t: repositories (name, description, topics, README), code on default branches (file names and contents), issues, pull requests, people and workspaces. Covers everything public, and private content in workspaces you belong to; signed out, public only. Write words, \"exact phrases\", -words to leave out, and qualifiers: repo:owner/name, org:workspace, language:rust, path:src/ (a glob with *), is:issue, is:pr, is:open, is:closed, is:merged, author:username, label:bug. type picks the kind of results (repositories, code, issues, pulls or people); without it, the qualifiers decide. Returns one page of results with the matches highlighted, code with line numbers, and how many there are of each kind."
15861586 }
15871587 Op::GetMergeQueue => {
1588− "A repository's merge queue: the pull requests waiting to land, in order, each with the state it is being tested in (the default branch with the pull requests ahead of it merged in) and how that went; then those that recently landed or left. With the queue on, merging a pull request adds it here."
1588+ "A repository's merge queue: the pull requests waiting to land, in order, each with the state it is being tested in (the default branch with the pull requests ahead of it landed in turn, each by its merge method) and how that went; then those that recently landed or left. With the queue on, merging a pull request adds it here."
15891589 }
15901590 Op::CreateRepo => {
15911591 "Create a repository in one of your workspaces, empty or as a copy of a public git repository elsewhere."
16931693 "What a pull request changes: the files it touches and their line-by-line diff against the commit it started from. Use it to review a pull request or to compare several made for the same issue. A large one lists every file (up to 300; truncated says when there were more) but has the lines of only the first, about 20,000 lines read: the files after that come with omitted true and no lines or counts. Ask again with paths (up to 50 at a time) to have those files' lines."
16941694 }
16951695 Op::MergePullRequest => {
1696− "Land a pull request on its base, the branch it merges into (the default branch unless it names another). merge_method says how: merge (a merge commit joins its branch to the base, its commits included), squash (one new commit holds everything it changed), rebase (each of its commits is made again on top of the base, in order) or fast_forward (the base moves to its head, exactly the commit that was checked; only when its branch contains the base's head). Left out, the repository's default_merge_method. A method the repository does not allow (allow_merge_commit, allow_squash_merge, allow_rebase_merge, allow_fast_forward on update_repo_settings), or that a rule for the base does not (allowed_merge_methods, and required linear history, which refuses merge), is refused. commit_title and commit_message are a merge or squash commit's message: its first line and the rest; left out, the repository's merge_commit_message or squash_commit_message decides. With sha, the merge is refused (409) if the pull request's head is no longer that commit. delete_branch deletes its branch once it has merged (left out, the repository's delete_branch_on_merge); restore_pull_request_head_branch brings it back. Merging needs the Write role or higher, and only once it is marked ready and it meets every rule that holds for its base (see rules and required_checks on get_pull_request: approvals, checks, deployments, merge windows and the rest, from the repository's and its workspace's rulesets); the refusal names the first rule not met. With ignore_checks, someone who may merge can bypass required checks where the rule allows it; with bypass_rules, someone a ruleset lists as a bypass actor merges past its rules, and it is recorded. Merging into the default branch closes the issues in its closes: the issue it was made for and those its title and description name after a closing keyword (Fixes #12), where the repository closes issues (auto_close_issues); the other pull requests still in progress for its issue close as superseded. Merging into another branch closes nothing. Where the repository has a merge queue, a pull request into the default branch joins the queue instead of landing at once. If its base has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests into its default branch to be up to date refuses instead: update_pull_request_branch, then merge again. Check status in the result to see whether it has landed, and merge_method and merge_commit for how."
1696+ "Land a pull request on its base, the branch it merges into (the default branch unless it names another). merge_method says how: merge (a merge commit joins its branch to the base, its commits included), squash (one new commit holds everything it changed), rebase (each of its commits is made again on top of the base, in order) or fast_forward (the base moves to its head, exactly the commit that was checked; only when its branch contains the base's head). Left out, the repository's default_merge_method. A method the repository does not allow (allow_merge_commit, allow_squash_merge, allow_rebase_merge, allow_fast_forward on update_repo_settings), or that a rule for the base does not (allowed_merge_methods, and required linear history, which refuses merge), is refused. commit_title and commit_message are a merge or squash commit's message: its first line and the rest; left out, the repository's merge_commit_message or squash_commit_message decides. With sha, the merge is refused (409) if the pull request's head is no longer that commit. delete_branch deletes its branch once it has merged (left out, the repository's delete_branch_on_merge); restore_pull_request_head_branch brings it back. Merging needs the Write role or higher, and only once it is marked ready and it meets every rule that holds for its base (see rules and required_checks on get_pull_request: approvals, checks, deployments, merge windows and the rest, from the repository's and its workspace's rulesets); the refusal names the first rule not met. With ignore_checks, someone who may merge can bypass required checks where the rule allows it; with bypass_rules, someone a ruleset lists as a bypass actor merges past its rules, and it is recorded. Merging into the default branch closes the issues in its closes: the issue it was made for and those its title and description name after a closing keyword (Fixes #12), where the repository closes issues (auto_close_issues); the other pull requests still in progress for its issue close as superseded. Merging into another branch closes nothing. Where the repository has a merge queue, a pull request into the default branch joins the queue instead of landing at once, to be tested and landed by merge_method (the merge queue rule's when absent) with the commit message given. If its base has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests into its default branch to be up to date refuses instead: update_pull_request_branch, then merge again. Check status in the result to see whether it has landed, and merge_method and merge_commit for how."
16971697 }
16981698 Op::GetPullRequestMergeOptions => {
16991699 "How a pull request may merge now, as the merge box offers it to you: the methods it may merge by (the repository's, as the rules for its base allow them) and the one offered first (default_method); each method's default commit title and message, and why one cannot be used now (unavailable); whether it is behind its base, can fast-forward as it is, should be offered an update (suggest_update) or must be up to date (require_up_to_date); whether it goes through the merge queue; whether you may turn on auto-merge; whether deleting its branch starts ticked and is possible; and whether commits made merging it are signed off."
+6−3
156156 [The merge queue](/guides/merge-queue/#who-answers-for-it) has the table of
157157 outcomes from a person's side.
158158
159−<Aside title="What a merge queue needs">
160− Building candidates takes sandboxes, so the merge queue needs a paid or
161− trial workspace, or the open-source pool for a public repository.
159+<Aside title="How states are built">
160+ `work` builds each candidate in the repository itself, through repos'
161+ `build_queue_step`: the entry landed by its own merge method on the
162+ state ahead of it, with the commits `land` would make there, pushed to
163+ the attempt's `g1t-queue/` branch. Landing is then a fast-forward to
164+ exactly that commit, and no sandbox is involved.
162165 [The merge queue](/guides/merge-queue/) has the settings.
163166 </Aside>
164167
+48−17
99 on, a pull request is tested together with everything ahead of it before it
1010 lands, and `main` only ever moves to a state whose required checks passed.
1111
12−The merge queue runs in g1t's sandboxes, which need
13−[the g1t plan](/guides/usage-and-billing/#the-g1t-plan) or the trial after
14−a card check; a public repository can use the open-source pool instead.
15−Without one, an entry fails at once with a message saying so; turn the
16−queue off to merge directly.
12+g1t builds the queue's states in the repository itself, without a
13+sandbox. What checks them is the repository's `merge_group`
14+[workflows](/guides/actions/), which run and are billed like any other
15+workflow run.
1716
1817 ## Turn it on
1918
6059
6160 ## How entries are tested
6261
63−g1t takes up to four entries (the rule's `max_entries_to_build`) from the front of the queue and tests them all
64−at once, speculatively, each in its own sandbox. Each sandbox builds `main`
65−with that entry and every entry ahead of it merged in, in queue order:
62+g1t takes up to four entries (the rule's `max_entries_to_build`) from the
63+front of the queue and tests them all at once, speculatively. Each entry's
64+state is the one before it with that entry landed on top, in queue order:
6665
6766 | Entry | Tested as |
6867 | --- | --- |
7170 | 3rd | `main` + #41 + #44 + #46 |
7271 | 4th | `main` + #41 + #44 + #46 + #47 |
7372
73+Each entry is landed on the state ahead of it by its own
74+[merge method](/guides/pull-requests/#merge-methods), exactly as merging it
75+there would:
76+
77+- **Merge commit**: a merge commit, with the message for the repository's
78+ merge commits or the one written when it was queued.
79+- **Squash**: one commit, by the pull request's author, with the squash
80+ message and its `Co-authored-by:` trailers.
81+- **Rebase**: each of its commits made again on top, keeping their authors.
82+- **Fast-forward**: its own head, which must already contain everything
83+ ahead of it; otherwise it fails, saying to bring it up to date or merge
84+ it another way.
85+
86+The method is the one asked for when it was merged into the queue (the
87+merge box's choice, `merge_method` in the API), else the merge queue
88+rule's `merge_method` (merge commit unless set). Both must be methods the
89+repository and the branch's [rules](/guides/rules/) allow, or merging is
90+refused, saying which are; linear history rules out merge commits.
91+A commit message written then (`commit_title`, `commit_message`) is kept
92+with the entry; otherwise the repository's default message is used, worked
93+out when the entry is first built. Sign-off is added where the repository
94+requires it. The same limits apply as to merging directly: g1t makes at
95+most 1,000 changed files, 100 files merged line by line and 250 rebased
96+commits itself.
97+
7498 If every entry passes, the four can land one after another without being
7599 tested again. The next batch starts when nothing is being tested. A batch
76100 that takes longer than 45 minutes (`check_response_timeout_minutes`) is tested again.
110134 Each build has its own attempt token. A result from an old attempt cannot
111135 replace a retry or put a removed entry back in the queue. Workflow results
112136 apply only to the combined commit still being checked.
113−Retries use separate Git branches, so a late sandbox cannot overwrite the
137+Retries use separate Git branches, so a late build cannot overwrite the
114138 new attempt's branch. Workflow results wait until the whole group has been
115139 registered; a dispatch error does not count as a pass.
116140
117141 ## How entries land
118142
119143 Entries land in order. When an entry has passed and everything ahead of it
120−has landed, `main` moves to exactly the state that was tested. The issue
121−closes and the other pull requests for it are superseded, as with any
122−merge.
144+has landed, `main` moves to exactly the state that was tested: since each
145+state was built on the one ahead of it, that is a fast-forward to the
146+commits its method made, which are the commits its checks ran on. The pull
147+request is recorded as merged by its method, with the commit `main` moved
148+to as its merge commit. The issue closes and the other pull requests for
149+it are superseded, as with any merge.
123150
124151 Current reviews and branch rules are checked again before publication,
125−against the combined commit's checks. Once publication starts, removal,
152+against the combined commit's checks and the entry's merge method. Once publication starts, removal,
126153 closing or conversion to draft returns a conflict until it finishes. If the
127154 service stops after updating Git, the next queue poll resumes bookkeeping
128155 for that same commit instead of publishing a different branch tip.
145172 ## When an entry fails
146173
147174 An entry fails when its `merge_group` workflows or required checks fail
148−on the combined state, when it does not merge cleanly with what is ahead of it, or
149−when the state cannot be built.
175+on the combined state, when its change clashes with what is ahead of it, or
176+when the state cannot be built (a fast-forward that does not contain what
177+is ahead, or more than g1t makes itself). One that cannot be built leaves
178+the queue, and the entries behind it are built in the next batch without
179+it.
150180
151181 ### Who answers for it
152182
210240 changes together, or may not be deterministic. Diagnosis stops after three
211241 extra builds, retaining the last combination demonstrated to fail.
212242
213−These combinations are built in sandboxes as the queue's states are, on
214−`g1t-queue/<id>` branches that are deleted afterwards, and never land. They
243+These combinations are built as the queue's states are, each entry by
244+its own method and with the same commit messages, on `g1t-queue/<id>`
245+branches that are deleted afterwards, and never land. They
215246 do not hold up the queue: the entries behind are tested again at once. When
216247 a combination cannot be built or checked, or its checks are still running
217248 past the rule's timeout when the queue next moves, g1t stops and reports
+5−0
357357 those it refuses and nothing changes: squash or merge instead of rebasing,
358358 or merge the base in a clone.
359359
360+With the [merge queue](/guides/merge-queue/#how-entries-are-tested) on,
361+the method chosen (or the queue rule's, when none is) decides how the
362+pull request's state is built in the queue, and what is tested is what
363+lands.
364+
360365 ### Commit messages
361366
362367 A merge commit and a squash commit get a message. You can write it in the
+0−0

Binary or large file; its contents are not shown.

+54−0
614614 pub sign_off: bool,
615615 }
616616
617+/// `build_queue_step`: one entry of a merge queue landed, by its method,
618+/// on a commit of the target that is not a branch's head: the default
619+/// branch, or the state built for the entries ahead. Makes the commits
620+/// `land` would make there (`merge`, `squash` and `rebase` new ones; a
621+/// `fast_forward` none, its head only if that contains `onto`) and points
622+/// `push_branch` of the target at the result, so that landing it later is
623+/// a fast-forward to exactly what was tested. The branch it merges into
624+/// does not move. Returns `Outcome<QueueStep>`.
625+#[derive(Debug, Serialize, Deserialize)]
626+#[serde(rename_all = "camelCase")]
627+pub struct BuildQueueStepArgs {
628+ /// The repository the queue lands on.
629+ pub target_id: String,
630+ /// The commit to build on.
631+ pub onto: String,
632+ /// Where the entry's change is: its fork, or the target itself.
633+ pub source_id: String,
634+ pub branch: String,
635+ /// The entry's head as it was queued; refused if the branch moved.
636+ pub commit: String,
637+ pub method: crate::rules::MergeMethod,
638+ /// For `merge` and `squash`: the commit's message. Required for those.
639+ #[serde(default, skip_serializing_if = "Option::is_none")]
640+ pub message: Option<String>,
641+ /// For `squash`: who the commit is by; `actor` when absent.
642+ #[serde(default, skip_serializing_if = "Option::is_none")]
643+ pub author: Option<User>,
644+ #[serde(default)]
645+ pub sign_off: bool,
646+ /// Whoever queued it: the committer, who must be able to merge.
647+ pub actor: User,
648+ /// The queue's branch of the target to point at the result.
649+ pub push_branch: String,
650+}
651+
652+/// What `build_queue_step` made.
653+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
654+#[serde(tag = "kind", rename_all = "snake_case")]
655+pub enum QueueStep {
656+ /// The state with the entry in it, now at `push_branch`: `onto` itself
657+ /// when it held the entry's changes already.
658+ Built {
659+ commit: String,
660+ /// For `rebase`: each commit made, with the one it was made from.
661+ #[serde(default)]
662+ rebased: Vec<(String, String)>,
663+ },
664+ /// Its change clashes with `onto` in these files.
665+ Conflict { message: String, paths: Vec<String> },
666+ /// It cannot be built there, and why: a fast-forward that does not
667+ /// contain `onto`, or more than g1t makes itself.
668+ Refused { message: String },
669+}
670+
617671 /// How a pull request's branch is brought up to date with its base
618672 /// (`update_pull_branch`).
619673 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+22−7
18851885 pub suggest_update: bool,
18861886 /// The repository requires it to be up to date before it merges.
18871887 pub require_up_to_date: bool,
1888− /// It merges through the merge queue, which lands it as tested
1889− /// whatever method is chosen.
1888+ /// It merges through the merge queue, which builds and tests it by the
1889+ /// method chosen (the queue rule's when none is) and lands it as tested.
18901890 pub merge_queue: bool,
18911891 /// The viewer may turn on auto-merge for it: the repository allows it,
18921892 /// it is open or a draft, and they may merge it.
22452245 pub viewer: Viewer,
22462246 }
22472247
2248−/// `queue_build`: the next batch to test for a repository, if nothing is
2249−/// being tested now. Returns `Vec<QueueJob>`, one per entry, each testing
2250−/// the default branch with that entry and everything ahead of it.
2248+/// `queue_build`: builds the next batch to test for a repository, if
2249+/// nothing is being tested now: each entry landed by its method on the
2250+/// state of the entries ahead of it, in the repository itself, with its
2251+/// `merge_group` workflows started. Returns `Vec<QueueJob>`, which is now
2252+/// always empty: no state is left for a sandbox to build.
22512253 #[derive(Debug, Serialize, Deserialize)]
22522254 #[serde(rename_all = "camelCase")]
22532255 pub struct QueueBuildArgs {
22652267 /// The branch of `source` holding it.
22662268 pub branch: String,
22672269 pub commit: String,
2270+ /// How it lands: the state is built with its commits made this way.
2271+ /// States recorded before methods were merged in by merge commit.
2272+ #[serde(default = "merge_method")]
2273+ pub method: MergeMethod,
2274+ /// For `merge` and `squash`: the commit's message, worked out when
2275+ /// the state was first built, so that building it again makes the
2276+ /// same commit.
2277+ #[serde(default, skip_serializing_if = "Option::is_none")]
2278+ pub message: Option<String>,
2279+ /// For `squash`: who the commit is by.
2280+ #[serde(default, skip_serializing_if = "Option::is_none")]
2281+ pub author: Option<User>,
22682282 }
22692283
2270−/// What a sandbox needs to build and check one combined state.
2284+/// One combined state to build and check. The work service builds every
2285+/// state in the repository itself now, so `queue_build` returns none.
22712286 #[derive(Clone, Debug, Serialize, Deserialize)]
22722287 #[serde(rename_all = "camelCase")]
22732288 pub struct QueueJob {
22932308 pub actor: User,
22942309 }
22952310
2296−/// `report_queue`: a sandbox's result for one combined state. Returns
2311+/// `report_queue`: the result of building one combined state. Returns
22972312 /// `Outcome<QueueState>`.
22982313 #[derive(Debug, Serialize, Deserialize)]
22992314 #[serde(rename_all = "camelCase")]
+6−0
11191119 source: RepoPath;
11201120 branch: string;
11211121 commit: string;
1122+ /** How it lands: the state is built with its commits made this way. */
1123+ method?: MergeMethod;
1124+ /** For merge and squash: the commit's message. */
1125+ message?: string;
1126+ /** For squash: who the commit is by. */
1127+ author?: User;
11221128 };
11231129
11241130 /** What a sandbox needs to build and check one combined state. */
+1−0
28632863 "behind" => reply(&repos.behind(args(body)?).await?),
28642864 "divergence" => reply(&repos.divergence(args(body)?).await?),
28652865 "land" => reply(&repos.land(args(body)?).await?),
2866+ "build_queue_step" => reply(&repos.build_queue_step(args(body)?).await?),
28662867 "update_pull_branch" => reply(&repos.update_pull_branch(args(body)?).await?),
28672868 // Services only: a pull request's test merge commit (test_merge.rs).
28682869 "pull_merge_ref" => reply(&repos.pull_merge_ref(args(body)?).await?),
+400−137
2626
2727 use futures_util::future::{try_join, try_join_all};
2828 use g1t_contracts::repos::{
29− Branch, Commit, EntryKind, GitAccess, LandArgs, Landed, NeedsAgentReason, PullBranchUpdate, Repo, TreeEntry,
29+ Branch, BuildQueueStepArgs, Commit, EntryKind, GitAccess, LandArgs, Landed, NeedsAgentReason, PullBranchUpdate, QueueStep,
30+ Repo, TreeEntry,
3031 };
3132 use g1t_contracts::rules::MergeMethod;
3233 use g1t_contracts::time::parse_rfc3339;
536537 }
537538 }
538539
540+/// A commit's short hash.
541+fn short(hash: &str) -> &str {
542+ &hash[..hash.len().min(7)]
543+}
544+
539545 /// A commit as people know it: its short hash and title.
540546 fn named_commit(commit: &Commit) -> String {
541547 let title = commit.message.lines().next().unwrap_or_default().trim();
553559 /// Why a landing was refused, or that it was there already.
554560 enum NotMade {
555561 Refused(Outcome<Landed>),
562+ /// Lines clash in these files.
563+ Clash { message: String, paths: Vec<String> },
556564 /// Nothing to make: the branch already has every change.
557565 AlreadyThere,
558566 }
561569 NotMade::Refused(Outcome::fail(code, message))
562570 }
563571
572+/// The pack that sends what was made: with what the pull request's commits
573+/// bring that the repository lacks, from the fork, when it does not hold
574+/// them (`held`).
575+async fn pack_for<R: GitRepo>(
576+ held: bool,
577+ source_git: &R,
578+ new: &str,
579+ merge_base: &str,
580+ objects: &[(ObjectKind, Vec<u8>)],
581+) -> Result<Vec<u8>> {
582+ if held {
583+ return Ok(write_pack(objects));
584+ }
585+ let source_access = source_git.access(Scope::Read).await?;
586+ let fetched = land::fetch_pack(&source_access, new, Some(merge_base)).await?;
587+ extend_pack(&fetched, objects).map_err(worker::Error::RustError)
588+}
589+
590+/// What `method` (any but `fast_forward`) makes of a pull request's
591+/// commits (`history`, its first-parent chain, headed by `new`) on the
592+/// head of `target_history`, with the objects made added to `objects`;
593+/// and the merge base, which a pack from a fork starts at. `branch`
594+/// names where it lands in what is said about a refusal.
595+#[allow(clippy::too_many_arguments)]
596+async fn make_on<R: GitRepo>(
597+ method: MergeMethod,
598+ reader: &Reader<'_, R>,
599+ target_history: &[Commit],
600+ history: &[Commit],
601+ new: &str,
602+ message: &str,
603+ (author, by): (&Signature<'_>, &Signature<'_>),
604+ sign_off: Option<&str>,
605+ branch: &str,
606+ objects: &mut Vec<(ObjectKind, Vec<u8>)>,
607+) -> Result<std::result::Result<(Made, String), NotMade>> {
608+ let Some(base) = target_history.first() else {
609+ return Ok(Err(refused(FailureCode::Conflict, format!("{branch} has no commits yet."))));
610+ };
611+ let shared: HashSet<String> = target_history.iter().map(|commit| commit.hash.clone()).collect();
612+ let merge_base = nearest_ancestor_in(reader, history, &shared).await?;
613+ let Some((merge_base, merge_base_tree)) = merge_base.and_then(|hash| {
614+ target_history.iter().find(|commit| commit.hash == hash).map(|commit| (hash, commit.tree_hash.clone()))
615+ }) else {
616+ return Ok(Err(refused(
617+ FailureCode::Conflict,
618+ format!("g1t could not find where this pull request left {branch}, so it cannot make its commits there."),
619+ )));
620+ };
621+ let made = match method {
622+ MergeMethod::Merge | MergeMethod::Squash => {
623+ let squash = method == MergeMethod::Squash;
624+ let co_authored = if squash {
625+ let commits = match branch_commits(reader, history, &shared, MAX_ANCESTRY as usize).await? {
626+ Some(commits) => commits,
627+ // Too many to walk: those on the branch's own line.
628+ None => {
629+ let mut own: Vec<Commit> = history.iter().take_while(|commit| !shared.contains(&commit.hash)).cloned().collect();
630+ own.reverse();
631+ own
632+ }
633+ };
634+ co_authors(&commits, author.email)
635+ } else {
636+ Vec::new()
637+ };
638+ let head = &history[0];
639+ made_merge(
640+ reader,
641+ (&merge_base_tree, &head.tree_hash, base),
642+ new,
643+ squash,
644+ message,
645+ &co_authored,
646+ sign_off,
647+ (author, by),
648+ branch,
649+ objects,
650+ )
651+ .await?
652+ }
653+ _ => {
654+ let commits = match branch_commits(reader, history, &shared, MAX_REBASE_COMMITS).await? {
655+ Some(commits) => commits,
656+ None => {
657+ return Ok(Err(refused(
658+ FailureCode::Conflict,
659+ format!(
660+ "This pull request has more than {MAX_REBASE_COMMITS} commits, too many for g1t to rebase itself. Squash or merge it instead."
661+ ),
662+ )));
663+ }
664+ };
665+ match replay(reader, &commits, (&base.hash, &base.tree_hash), by, sign_off, objects).await? {
666+ Replay::Done(replayed) if replayed.made.is_empty() => Err(NotMade::AlreadyThere),
667+ Replay::Done(replayed) => Ok(Made {
668+ commit: replayed.head,
669+ commits: Some(replayed.made.len() as u32),
670+ rebased: replayed.made,
671+ }),
672+ Replay::Conflict { commit, paths } => Err(NotMade::Clash {
673+ message: format!(
674+ "Rebasing stopped at {}: it and {branch} changed the same lines of {}. Rebase the branch onto {branch} and resolve them, or merge it another way.",
675+ named_commit(&commit),
676+ name_files(&paths)
677+ ),
678+ paths,
679+ }),
680+ Replay::Unsupported { commit, why } => Err(refused(
681+ FailureCode::Conflict,
682+ format!("g1t cannot rebase {} itself: {why}. Rebase the branch onto {branch}, or merge it another way.", named_commit(&commit)),
683+ )),
684+ }
685+ }
686+ };
687+ Ok(made.map(|made| (made, merge_base)))
688+}
689+
690+/// A merge commit (parents `base` and `new`) or a squashed commit
691+/// (parent `base`) whose tree is the pull request's changes since
692+/// `trees.0` (the merge base's tree; `trees.1` is its head's) applied
693+/// to `base`'s.
694+#[allow(clippy::too_many_arguments)]
695+async fn made_merge<R: GitRepo>(
696+ reader: &Reader<'_, R>,
697+ trees: (&str, &str, &Commit),
698+ new: &str,
699+ squash: bool,
700+ message: &str,
701+ co_authored: &[String],
702+ sign_off: Option<&str>,
703+ (author, committer): (&Signature<'_>, &Signature<'_>),
704+ branch: &str,
705+ objects: &mut Vec<(ObjectKind, Vec<u8>)>,
706+) -> Result<std::result::Result<Made, NotMade>> {
707+ let (merge_base_tree, head_tree, base) = trees;
708+ let verb = if squash { "squash" } else { "merge" };
709+ let tree = match apply(reader, Some(merge_base_tree), head_tree, &base.tree_hash, objects).await? {
710+ Applied::Tree(tree) => tree,
711+ Applied::Conflict(paths) => {
712+ return Ok(Err(NotMade::Clash {
713+ message: format!(
714+ "This pull request and {branch} changed the same lines of {}. Bring the pull request up to date with {branch} and resolve them, then merge again.",
715+ name_files(&paths)
716+ ),
717+ paths,
718+ }));
719+ }
720+ Applied::Unsupported(why) => {
721+ return Ok(Err(refused(
722+ FailureCode::Conflict,
723+ format!("g1t cannot {verb} this pull request itself: {why}. Bring it up to date with {branch}, or merge it another way."),
724+ )));
725+ }
726+ };
727+ // Squashing what is there already would make an empty commit.
728+ if squash && tree == base.tree_hash {
729+ return Ok(Err(NotMade::AlreadyThere));
730+ }
731+ let mut trailers: Vec<String> = co_authored.to_vec();
732+ trailers.extend(sign_off.map(str::to_owned));
733+ let by = if squash { &author.email } else { &committer.email };
734+ let message = with_trailers(&tidy_co_authors(message, by, co_authored), &trailers);
735+ let bytes = if squash {
736+ commit_object_by(&tree, &[&base.hash], author, committer, &message)
737+ } else {
738+ commit_object_by(&tree, &[&base.hash, new], committer, committer, &message)
739+ };
740+ let commit = object_id(ObjectKind::Commit, &bytes);
741+ objects.push((ObjectKind::Commit, bytes));
742+ Ok(Ok(Made { commit, rebased: Vec::new(), commits: squash.then_some(1) }))
743+}
744+
745+/// What one merge queue entry made: the state with it in, and the merge
746+/// base a pack from its fork starts at; or why it cannot be built.
747+#[derive(Debug)]
748+pub(crate) enum StepMade {
749+ Made { commit: String, rebased: Vec<(String, String)>, merge_base: String },
750+ Conflict { message: String, paths: Vec<String> },
751+ Refused(String),
752+}
753+
754+/// One merge queue entry (`history`, its first-parent chain from the
755+/// commit queued) landed by `method` on the head of `onto_history`, a
756+/// state of the queue: its commits made as landing would make them there,
757+/// with the objects made added to `objects`. A fast-forward makes none and
758+/// needs its head to contain that state; an entry whose changes are all
759+/// there already leaves the state as it is. `held`: the repository holds
760+/// the entry's commits (`git` reads a fork's otherwise).
761+#[allow(clippy::too_many_arguments)]
762+pub(crate) async fn queue_step_on<R: GitRepo>(
763+ git: &Reader<'_, R>,
764+ held: bool,
765+ method: MergeMethod,
766+ (onto_history, history): (&[Commit], &[Commit]),
767+ message: Option<&str>,
768+ signatures: (&Signature<'_>, &Signature<'_>),
769+ sign_off: Option<&str>,
770+ place: &str,
771+ objects: &mut Vec<(ObjectKind, Vec<u8>)>,
772+) -> Result<StepMade> {
773+ let (Some(head), Some(base)) = (history.first(), onto_history.first()) else {
774+ return Ok(StepMade::Refused("Its commit or the state it is built on is no longer there.".to_owned()));
775+ };
776+ let there = |commit: &str| StepMade::Made { commit: commit.to_owned(), rebased: Vec::new(), merge_base: base.hash.clone() };
777+ if base.hash == head.hash || (held && descends_from(git, onto_history, &head.hash).await?) {
778+ return Ok(there(&base.hash));
779+ }
780+ if method == MergeMethod::FastForward {
781+ return Ok(if descends_from(git, history, &base.hash).await? {
782+ there(&head.hash)
783+ } else {
784+ StepMade::Refused(format!(
785+ "It lands by fast-forward, so its branch must contain {place} with everything queued ahead of it. Bring it up to date, or merge it another way."
786+ ))
787+ });
788+ }
789+ let message = match message.map(str::trim_end).filter(|text| !text.trim().is_empty()) {
790+ Some(text) => text.to_owned(),
791+ None if method == MergeMethod::Rebase => String::new(),
792+ None => return Ok(StepMade::Refused("Say what the commit's message is: its first line is its title.".to_owned())),
793+ };
794+ Ok(match make_on(method, git, onto_history, history, &head.hash, &message, signatures, sign_off, place, objects).await? {
795+ Ok((made, merge_base)) => StepMade::Made { commit: made.commit, rebased: made.rebased, merge_base },
796+ Err(NotMade::AlreadyThere) => there(&base.hash),
797+ Err(NotMade::Clash { message, paths }) => StepMade::Conflict { message, paths },
798+ Err(NotMade::Refused(Outcome::Fail(failure))) => StepMade::Refused(failure.message),
799+ Err(NotMade::Refused(Outcome::Ok(_))) => StepMade::Refused("It could not be built.".to_owned()),
800+ })
801+}
802+
564803 impl<S: GitStore> Repos<S> {
565804 /// `land` by `method`, any but `fast_forward`: makes the commits on
566805 /// `branch` of `target` from `new`, the checked head of the pull
624863 };
625864 return Ok(Outcome::Ok(Landed { commit, previous: None, method, rebased: Vec::new() }));
626865 }
627− let shared: HashSet<String> = target_history.iter().map(|commit| commit.hash.clone()).collect();
628− let merge_base = nearest_ancestor_in(&reader, history, &shared).await?;
629− let Some((merge_base, merge_base_tree)) = merge_base.and_then(|hash| {
630− target_history.iter().find(|commit| commit.hash == hash).map(|commit| (hash, commit.tree_hash.clone()))
631− }) else {
632− return Ok(Outcome::fail(
633− FailureCode::Conflict,
634− format!("g1t could not find where this pull request left {branch}, so it cannot make its commits there."),
635− ));
636− };
637866 let mut objects: Vec<(ObjectKind, Vec<u8>)> = Vec::new();
638867 let seconds = now_ms() / 1000;
639868 let by = Signature { name: &committer.name, email: &committer.email, seconds };
640− let made = match method {
641− MergeMethod::Merge | MergeMethod::Squash => {
642− let squash = method == MergeMethod::Squash;
643− let co_authored = if squash {
644− let commits = match branch_commits(&reader, history, &shared, MAX_ANCESTRY as usize).await? {
645− Some(commits) => commits,
646− // Too many to walk: those on the branch's own line.
647− None => {
648− let mut own: Vec<Commit> = history.iter().take_while(|commit| !shared.contains(&commit.hash)).cloned().collect();
649− own.reverse();
650− own
651− }
652− };
653− co_authors(&commits, &author.email)
654− } else {
655− Vec::new()
656− };
657− let head = &history[0];
658− self.made_merge(
659− &reader,
660− (&merge_base_tree, &head.tree_hash, &base),
661− &new,
662− squash,
663− &message,
664− &co_authored,
665− sign_off.as_deref(),
666− (&Signature { name: &author.name, email: &author.email, seconds }, &by),
667− branch,
668− &mut objects,
669− )
670− .await?
671− }
672− _ => {
673− let commits = match branch_commits(&reader, history, &shared, MAX_REBASE_COMMITS).await? {
674− Some(commits) => commits,
675− None => {
676− return Ok(Outcome::fail(
677− FailureCode::Conflict,
678− format!(
679− "This pull request has more than {MAX_REBASE_COMMITS} commits, too many for g1t to rebase itself. Squash or merge it instead."
680− ),
681− ));
682− }
683− };
684− match replay(&reader, &commits, (&base.hash, &base.tree_hash), &by, sign_off.as_deref(), &mut objects).await? {
685− Replay::Done(replayed) if replayed.made.is_empty() => Err(NotMade::AlreadyThere),
686− Replay::Done(replayed) => Ok(Made {
687− commit: replayed.head,
688− commits: Some(replayed.made.len() as u32),
689− rebased: replayed.made,
690− }),
691− Replay::Conflict { commit, paths } => Err(refused(
692− FailureCode::Conflict,
693− format!(
694− "Rebasing stopped at {}: it and {branch} changed the same lines of {}. Rebase the branch onto {branch} and resolve them, or merge it another way.",
695− named_commit(&commit),
696− name_files(&paths)
697− ),
698− )),
699− Replay::Unsupported { commit, why } => Err(refused(
700− FailureCode::Conflict,
701− format!("g1t cannot rebase {} itself: {why}. Rebase the branch onto {branch}, or merge it another way.", named_commit(&commit)),
702− )),
703− }
704− }
705− };
706− let made = match made {
869+ let author = Signature { name: &author.name, email: &author.email, seconds };
870+ let made =
871+ make_on(method, &reader, &target_history, history, &new, &message, (&author, &by), sign_off.as_deref(), branch, &mut objects)
872+ .await?;
873+ let (made, merge_base) = match made {
707874 Ok(made) => made,
708875 Err(NotMade::Refused(outcome)) => return Ok(outcome),
876+ Err(NotMade::Clash { message, .. }) => return Ok(Outcome::fail(FailureCode::Conflict, message)),
709877 Err(NotMade::AlreadyThere) => {
710878 return Ok(Outcome::Ok(Landed { commit: base.hash.clone(), previous: None, method, rebased: Vec::new() }));
711879 }
712880 };
713− // What the pull request's commits bring that the repository
714− // lacks comes from the fork, with what was made after it.
715− let pack = if held {
716− write_pack(&objects)
717− } else {
718− let source_access = source_git.access(Scope::Read).await?;
719− let fetched = land::fetch_pack(&source_access, &new, Some(&merge_base)).await?;
720− extend_pack(&fetched, &objects).map_err(worker::Error::RustError)?
721− };
881+ let pack = pack_for(held, source_git, &new, &merge_base, &objects).await?;
722882 let target_access = target_git.access(Scope::Write).await?;
723883 let pushed = land::push_pack(&target_access, branch, Some(&base.hash), &made.commit, pack).await?;
724884 self.refs_moved(&target.id).await;
743903 }
744904 }
745905
746− /// A merge commit (parents `base` and `new`) or a squashed commit
747− /// (parent `base`) whose tree is the pull request's changes since
748− /// `trees.0` (the merge base's tree; `trees.1` is its head's) applied
749− /// to `base`'s.
750− #[allow(clippy::too_many_arguments)]
751− async fn made_merge<R: GitRepo>(
752− &self,
753− reader: &Reader<'_, R>,
754− trees: (&str, &str, &Commit),
755− new: &str,
756− squash: bool,
757− message: &str,
758− co_authored: &[String],
759− sign_off: Option<&str>,
760− (author, committer): (&Signature<'_>, &Signature<'_>),
761− branch: &str,
762− objects: &mut Vec<(ObjectKind, Vec<u8>)>,
763− ) -> Result<std::result::Result<Made, NotMade>> {
764− let (merge_base_tree, head_tree, base) = trees;
765− let verb = if squash { "squash" } else { "merge" };
766− let tree = match apply(reader, Some(merge_base_tree), head_tree, &base.tree_hash, objects).await? {
767− Applied::Tree(tree) => tree,
768− Applied::Conflict(paths) => {
769− return Ok(Err(refused(
770− FailureCode::Conflict,
771− format!(
772− "This pull request and {branch} changed the same lines of {}. Bring the pull request up to date with {branch} and resolve them, then merge again.",
773− name_files(&paths)
774− ),
775− )));
776− }
777− Applied::Unsupported(why) => {
778− return Ok(Err(refused(
779− FailureCode::Conflict,
780− format!("g1t cannot {verb} this pull request itself: {why}. Bring it up to date with {branch}, or merge it another way."),
781− )));
782− }
906+ /// `build_queue_step`: one merge queue entry landed by its method on
907+ /// `onto`, a commit of the target, as `land_by` would land it on a
908+ /// branch whose head that is, and `push_branch` pointed at what was
909+ /// made. Nothing else moves. The work service decides who may merge;
910+ /// the committer must still be able to.
911+ pub(crate) async fn build_queue_step(&self, a: BuildQueueStepArgs) -> Result<Outcome<QueueStep>> {
912+ let actor = Some(a.actor.clone());
913+ let (Some(target), Some(source)) = (self.registry.by_id(&a.target_id).await?, self.registry.by_id(&a.source_id).await?) else {
914+ return Ok(Outcome::fail(FailureCode::NotFound, "No such repository."));
783915 };
784− // Squashing what is there already would make an empty commit.
785− if squash && tree == base.tree_hash {
786− return Ok(Err(NotMade::AlreadyThere));
916+ if !crate::registry::can(&target, &actor, g1t_contracts::access::Capability::Merge) || !a.actor.verified {
917+ return Ok(Outcome::fail(FailureCode::Forbidden, "Whoever queued it can no longer merge here."));
787918 }
788− let mut trailers: Vec<String> = co_authored.to_vec();
789− trailers.extend(sign_off.map(str::to_owned));
790− let by = if squash { &author.email } else { &committer.email };
791− let message = with_trailers(&tidy_co_authors(message, by, co_authored), &trailers);
792− let bytes = if squash {
793− commit_object_by(&tree, &[&base.hash], author, committer, &message)
919+ // A fork's change lands only on a repository in its network.
920+ if source.id != target.id && self.pull_target(&source, Some(&target.id)).await?.is_none_or(|found| found.id != target.id) {
921+ return Ok(Outcome::fail(FailureCode::NotFound, "No such repository."));
922+ }
923+ let from_fork = source.id != target.id;
924+ self.live(&source).await?;
925+ let source_git = self.store.open(&store_key(&source)).await?;
926+ let target_git = self.store.open(&store_key(&target)).await?;
927+ let refused = |message: String| Ok(Outcome::Ok(QueueStep::Refused { message }));
928+ // What was queued is the commit: its branch may have moved since.
929+ let (history, onto_history) = try_join(source_git.log(&a.commit, MAX_ANCESTRY), target_git.log(&a.onto, MAX_ANCESTRY)).await?;
930+ let (Some(head), Some(base)) = (history.first(), onto_history.first()) else {
931+ return refused(format!("{} or the state it is built on is no longer there.", short(&a.commit)));
932+ };
933+ let new = head.hash.clone();
934+ let base = base.hash.clone();
935+ let held = !from_fork || !target_git.log(&new, 1).await?.is_empty();
936+ let reader = Reader::new(&target_git, (!held).then_some(&source_git));
937+ let place = target.default_branch.clone();
938+ let mut objects: Vec<(ObjectKind, Vec<u8>)> = Vec::new();
939+ let committer = self.commit_identity(&a.actor).await;
940+ let author = match (&a.author, a.method) {
941+ (Some(author), MergeMethod::Squash) => self.commit_identity(author).await,
942+ _ => committer.clone(),
943+ };
944+ let sign_off = a.sign_off.then(|| sign_off_trailer(&committer.name, &committer.email));
945+ let seconds = now_ms() / 1000;
946+ let by = Signature { name: &committer.name, email: &committer.email, seconds };
947+ let author = Signature { name: &author.name, email: &author.email, seconds };
948+ let step = queue_step_on(
949+ &reader,
950+ held,
951+ a.method,
952+ (&onto_history, &history),
953+ a.message.as_deref(),
954+ (&author, &by),
955+ sign_off.as_deref(),
956+ &place,
957+ &mut objects,
958+ )
959+ .await?;
960+ let (commit, rebased, merge_base) = match step {
961+ StepMade::Made { commit, rebased, merge_base } => (commit, rebased, merge_base),
962+ StepMade::Conflict { message, paths } => return Ok(Outcome::Ok(QueueStep::Conflict { message, paths })),
963+ StepMade::Refused(message) => return refused(message),
964+ };
965+ // Nothing new when it was there already.
966+ let pack = if commit == base {
967+ write_pack(&objects)
794968 } else {
795− commit_object_by(&tree, &[&base.hash, new], committer, committer, &message)
969+ pack_for(held, &source_git, &new, &merge_base, &objects).await?
796970 };
797− let commit = object_id(ObjectKind::Commit, &bytes);
798− objects.push((ObjectKind::Commit, bytes));
799− Ok(Ok(Made { commit, rebased: Vec::new(), commits: squash.then_some(1) }))
971+ let old = target_git.log(&a.push_branch, 1).await?.into_iter().next().map(|commit| commit.hash);
972+ let target_access = target_git.access(Scope::Write).await?;
973+ let pushed = land::push_pack(&target_access, &a.push_branch, old.as_deref(), &commit, pack).await?;
974+ self.refs_moved(&target.id).await;
975+ Ok(match pushed {
976+ Ok(()) => Outcome::Ok(QueueStep::Built { commit, rebased }),
977+ Err(reason) => Outcome::fail(FailureCode::Conflict, format!("{} could not be updated: {reason}", a.push_branch)),
978+ })
800979 }
801980
802981 /// `update_pull_branch` by `rebase`: makes the commits of `branch` of
12701449 git.keep(&made);
12711450 assert_eq!(block_on(git.read_blob(&object_id(ObjectKind::Blob, b"made\n"))).unwrap().as_deref(), Some(&b"made\n"[..]));
12721451 }
1452+
1453+ /// A commit made in a test, as the store would list it.
1454+ fn made_commit(objects: &[(ObjectKind, Vec<u8>)], id: &str) -> Commit {
1455+ let bytes = objects
1456+ .iter()
1457+ .find(|(kind, bytes)| *kind == ObjectKind::Commit && object_id(ObjectKind::Commit, bytes) == id)
1458+ .map(|(_, bytes)| String::from_utf8(bytes.clone()).unwrap())
1459+ .expect("made");
1460+ let field = |name: &str| bytes.lines().filter_map(|line| line.strip_prefix(name)).map(str::to_owned).collect::<Vec<_>>();
1461+ Commit {
1462+ hash: id.to_owned(),
1463+ tree_hash: field("tree ").remove(0),
1464+ message: bytes.split_once("\n\n").map(|(_, message)| message.to_owned()).unwrap_or_default(),
1465+ author: Person { name: "Di".to_owned(), email: "di@example.com".to_owned() },
1466+ parents: field("parent "),
1467+ authored_at: "2026-10-01T00:00:00.000Z".to_owned(),
1468+ }
1469+ }
1470+
1471+ #[test]
1472+ fn each_queue_entry_lands_by_its_own_method_on_the_state_ahead_of_it() {
1473+ let (mut store, [m, b, h1, h2]) = diverged();
1474+ // A second change, from the same start, to another file.
1475+ let g = store.commit("g", &["m"], "eve", &[("README.md", "hi\n"), ("src/lib.rs", "a\nb\nc\nd\ne\n"), ("docs.md", "docs\n")]);
1476+ let k = store.commit("k", &["m"], "eve", &[("README.md", "hi\n"), ("src/lib.rs", "a\nb\nc\nd\ne\n"), ("k.md", "k\n")]);
1477+ let git = Reader::new(&store, None);
1478+ let by = Signature { name: "Di", email: "di@example.com", seconds: 7 };
1479+ let author = Signature { name: "bo", email: "bo@example.com", seconds: 7 };
1480+ let mut objects = Vec::new();
1481+ let step = |method, onto: &[Commit], history: &[Commit], objects: &mut Vec<_>| {
1482+ block_on(queue_step_on(&git, true, method, (onto, history), Some("Land it"), (&author, &by), None, "main", objects)).unwrap()
1483+ };
1484+ let onto = vec![b.clone(), m.clone()];
1485+
1486+ // The first squashes onto the default branch: one commit, one parent.
1487+ let StepMade::Made { commit: first, .. } = step(MergeMethod::Squash, &onto, &[h2.clone(), h1.clone(), m.clone()], &mut objects) else {
1488+ panic!("not squashed")
1489+ };
1490+ let first = made_commit(&objects, &first);
1491+ assert_eq!(first.parents, ["b"]);
1492+
1493+ // The second is a merge commit on the first's state, so landing it
1494+ // is a fast-forward from there.
1495+ let onto = vec![first.clone(), b.clone(), m.clone()];
1496+ let StepMade::Made { commit: second, .. } = step(MergeMethod::Merge, &onto, &[g.clone(), m.clone()], &mut objects) else {
1497+ panic!("not merged")
1498+ };
1499+ let second = made_commit(&objects, &second);
1500+ assert_eq!(second.parents, [first.hash.clone(), "g".to_owned()]);
1501+ assert_eq!(
1502+ Store::files(&git, &second.tree_hash),
1503+ files(&[("README.md", "hello\n"), ("docs.md", "docs\n"), ("src/lib.rs", "A\nb\nc\nd\nE\n"), ("src/new.rs", "new\n")])
1504+ );
1505+
1506+ // Queued again, its changes are there already: the state is kept.
1507+ let onto = vec![second.clone(), first.clone(), b.clone(), m.clone()];
1508+ let StepMade::Made { commit, .. } = step(MergeMethod::Rebase, &onto, &[h2.clone(), h1.clone(), m.clone()], &mut objects) else {
1509+ panic!("not kept")
1510+ };
1511+ assert_eq!(commit, second.hash);
1512+
1513+ // A fast-forward needs its head to hold the state ahead of it.
1514+ let StepMade::Refused(why) = step(MergeMethod::FastForward, &onto, &[k, m.clone()], &mut objects) else {
1515+ panic!("fast-forwarded")
1516+ };
1517+ assert!(why.contains("fast-forward"), "{why}");
1518+ }
1519+
1520+ #[test]
1521+ fn a_fast_forward_entry_is_its_own_head_and_a_clash_names_the_files() {
1522+ let (mut store, [m, b, h1, h2]) = diverged();
1523+ let ahead = store.commit("f", &["b"], "eve", &[("README.md", "hello\n"), ("src/lib.rs", "a\nb\nc\nd\nE\n"), ("f.md", "f\n")]);
1524+ let clash = store.commit("x", &["m"], "eve", &[("README.md", "hey\n"), ("src/lib.rs", "a\nb\nc\nd\ne\n")]);
1525+ let git = Reader::new(&store, None);
1526+ let by = Signature { name: "Di", email: "di@example.com", seconds: 7 };
1527+ let mut objects = Vec::new();
1528+ let onto = vec![b.clone(), m.clone()];
1529+ let made = block_on(queue_step_on(&git, true, MergeMethod::FastForward, (&onto, &[ahead.clone(), b.clone(), m.clone()]), None, (&by, &by), None, "main", &mut objects)).unwrap();
1530+ assert!(matches!(made, StepMade::Made { ref commit, .. } if commit == "f"), "{made:?}");
1531+ assert!(objects.is_empty());
1532+ let clashed = block_on(queue_step_on(&git, true, MergeMethod::Rebase, (&onto, &[clash, m.clone()]), None, (&by, &by), None, "main", &mut objects)).unwrap();
1533+ assert!(matches!(clashed, StepMade::Conflict { ref paths, .. } if paths == &["README.md".to_owned()]), "{clashed:?}");
1534+ let _ = (h1, h2);
1535+ }
12731536 }
+7−0
1+-- How each merge queue entry lands (queue.rs): the method whoever queued
2+-- it asked for, else the merge queue rule's, and the commit message they
3+-- wrote for a merge or a squash. NULL method: queued before methods, when
4+-- every state was built with merge commits.
5+ALTER TABLE queue_entries ADD COLUMN merge_method TEXT;
6+ALTER TABLE queue_entries ADD COLUMN commit_title TEXT;
7+ALTER TABLE queue_entries ADD COLUMN commit_message TEXT;
+47−32
5050 use crate::Work;
5151 use crate::checks::{hash, new_token};
5252 use crate::decisions::NewDecision;
53−use crate::queue::{BRANCH_PREFIX, EntryRow};
53+use crate::queue::{BRANCH_PREFIX, Built, EntryRow};
5454 use crate::rows::ValueRow;
5555
5656 /// The most extra combinations built to narrow down one failure.
120120 }
121121
122122 impl Failure {
123− /// What a sandbox reported for a state that failed. `unchecked` is set
123+ /// What was reported for a state that failed. `unchecked` is set
124124 /// when the state was built but nothing could check it.
125125 pub(crate) fn of_report(report: &ReportQueueArgs, unchecked: Option<&[String]>) -> Failure {
126126 let (kind, check) = if report.conflict_with.is_some() || !report.conflicts.is_empty() {
231231 };
232232 }
233233 match failure.kind {
234− // The sandbox names the entry ahead that changed the same files.
234+ // The build names the entry ahead that changed the same files.
235235 FailureKind::Conflict => Plan::Blame(failure.conflict_with.filter(|n| ahead.contains(n)).into_iter().collect()),
236236 // Neither is about how the changes combine.
237237 FailureKind::Missing | FailureKind::Build => Plan::Blame(Vec::new()),
651651
652652 // --- Smaller combinations -------------------------------------------------
653653
654− /// One job for each smaller combination waiting to be built, and the
655− /// end of any that took too long.
656− pub(crate) async fn probe_jobs(&self, repo_id: &str) -> Result<Vec<QueueJob>> {
654+ /// Builds each smaller combination waiting to be built, and ends any
655+ /// that took too long.
656+ pub(crate) async fn build_probes(&self, repo_id: &str) -> Result<()> {
657657 let probes = self
658658 .db
659659 .prepare("SELECT * FROM queue_probes WHERE repo_id = ? AND state IN ('waiting', 'testing')")
662662 .await?
663663 .results::<ProbeRow>()?;
664664 if probes.is_empty() {
665− return Ok(Vec::new());
665+ return Ok(());
666666 }
667667 let rule = self.queue_rule(repo_id).await?;
668668 let stale = rfc3339(now_ms().saturating_sub(u64::from(rule.check_response_timeout_minutes.max(1)) * 60 * 1000));
669− let mut jobs = Vec::new();
670669 for probe in probes {
671670 if probe.state == "testing" {
672671 if probe.tested_at.as_deref().is_none_or(|at| at < stale.as_str()) {
674673 }
675674 continue;
676675 }
677− if let Some(job) = self.probe_job(&probe).await? {
678− jobs.push(job);
679− }
676+ self.build_probe(&probe).await?;
680677 }
681− Ok(jobs)
678+ Ok(())
682679 }
683680
684− async fn probe_job(&self, probe: &ProbeRow) -> Result<Option<QueueJob>> {
681+ /// Builds a smaller combination as the queue builds a state: from the
682+ /// base it was tested on, each entry landed by its method on the one
683+ /// before, every step pushed to the probe's branch; then reports it.
684+ async fn build_probe(&self, probe: &ProbeRow) -> Result<()> {
685685 let found = match self.entry_by_id(&probe.entry_id).await? {
686686 Some(row) => {
687687 let diagnosis = row
698698 None => None,
699699 };
700700 // Nothing is waiting on it any more.
701− let Some((diagnosis, pull, actor)) = found else {
701+ let Some((diagnosis, _, actor)) = found else {
702702 self.probe_result(probe, ProbeResult::Stopped, Some("Its failure was already reported.")).await?;
703− return Ok(None);
703+ return Ok(());
704704 };
705705 let repo: Outcome<Repo> = g1t_kit::call(
706706 &self.repos,
707707 "get_by_id",
708708 &GetByIdArgs {
709709 id: probe.repo_id.clone(),
710− viewer: Some(actor),
710+ viewer: Some(actor.clone()),
711711 },
712712 )
713713 .await?;
714714 let Outcome::Ok(repo) = crate::retired::unless_archived(repo) else {
715715 self.probe_result(probe, ProbeResult::Stopped, Some("The repository is archived.")).await?;
716− return Ok(None);
716+ return Ok(());
717717 };
718718 let subset = probe.subset();
719719 let mut stack = Vec::new();
749749 .first::<ValueRow>(None)
750750 .await?;
751751 if claimed.is_none() {
752− return Ok(None);
752+ return Ok(());
753753 }
754− Ok(Some(QueueJob {
754+ let sign_off = self.settings(&repo.id).await?.require_web_commit_sign_off;
755+ let mut report = ReportQueueArgs {
755756 entry_id: probe.id.clone(),
756757 token,
757− repo: RepoPath {
758− namespace: repo.namespace.clone(),
759− name: repo.name.clone(),
760− },
761− default_branch: repo.default_branch.clone(),
762− base_commit: diagnosis.base_commit.clone(),
763− branch: probe.branch(),
764− stack,
765− checks: Vec::new(),
766− contract_checks: Vec::new(),
767− actor: pull.owner().clone(),
768− }))
758+ combined_commit: None,
759+ results: Vec::new(),
760+ error: None,
761+ conflict_with: None,
762+ conflicts: Vec::new(),
763+ };
764+ let mut onto = diagnosis.base_commit.clone();
765+ for item in &stack {
766+ match self.build_step(&repo, &onto, item, &probe.branch(), &actor, sign_off).await? {
767+ Built::State(commit) => onto = commit,
768+ Built::Conflict { message, paths } => {
769+ report.error = Some(message);
770+ report.conflicts = paths;
771+ break;
772+ }
773+ Built::Failed(message) => {
774+ report.error = Some(message);
775+ break;
776+ }
777+ }
778+ }
779+ if report.error.is_none() {
780+ report.combined_commit = Some(onto);
781+ }
782+ self.report_probe(report).await?;
783+ Ok(())
769784 }
770785
771− /// A sandbox's result for a smaller combination: built, it waits for
786+ /// The result of building a smaller combination: built, it waits for
772787 /// its `merge_group` workflows as a queue state does.
773788 pub(crate) async fn report_probe(&self, a: ReportQueueArgs) -> Result<Outcome<QueueState>> {
774789 let probe = self
+14−2
22502250
22512251 // A repository that merges through a queue: it joins the queue, and
22522252 // lands once its state together with everything ahead has passed.
2253− // The queue lands the state it tested, whatever the method.
2253+ // Its state is built by the method asked for, else the merge queue
2254+ // rule's, and lands exactly as tested (queue.rs).
22542255 if settings.merge_queue {
22552256 if !a.actor.verified {
22562257 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
22572258 }
22582259 check!(allowed(Some(&a.actor), &repo, Capability::Merge));
2260+ let queued = match a.merge_method {
2261+ Some(_) => method,
2262+ None => {
2263+ let rule = self.queue_rule(&repo.id).await?;
2264+ match merging::choose_method(Some(rule.merge_method), &choosing, &requirements, &base) {
2265+ Ok(method) => method,
2266+ Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
2267+ }
2268+ }
2269+ };
22592270 self.record_merge_evaluations(&repo, &pull, &gate).await;
22602271 // Landing honours the same bypass request against the rules as
22612272 // they are then (queue.rs `settle`).
2262− return self.enqueue(&repo, &pull, &a.actor, a.keep_issue_open, a.bypass_rules).await;
2273+ let message = (a.commit_title.as_deref(), a.commit_message.as_deref());
2274+ return self.enqueue(&repo, &pull, &a.actor, a.keep_issue_open, a.bypass_rules, queued, message).await;
22632275 }
22642276
22652277 // The commit it makes, for a merge or a squash: what the merger
+201−58
22 //! of them, and only a combination that passed reaches the default branch.
33 //!
44 //! A batch of up to the merge queue rule's `max_entries_to_build` entries (4 unless
5−//! the default branch's rules say otherwise) is tested at once, speculatively: one
6−//! sandbox per entry builds the default branch with that entry and every
7−//! entry ahead of it merged in, and pushes the result to
5+//! the default branch's rules say otherwise) is tested at once, speculatively.
6+//! Each entry's state is built in the repository itself (repos
7+//! `build_queue_step`): the entry landed by its own method (merge commit,
8+//! squash, rebase or fast-forward: the one asked for when it was queued,
9+//! else the merge queue rule's) on the state of the entry ahead of it, or
10+//! on the default branch for the first, and pushed to
811 //! `g1t-queue/<entry>-<attempt>`. The repository's `merge_group` workflows then run
912 //! on that commit, and the default branch's required checks must pass
1013 //! there. Entries land in order, each by moving the default branch to its
11−//! tested state, once everything ahead has landed. One that fails leaves
14+//! tested state once everything ahead has landed: a fast-forward to
15+//! exactly the commits that were tested, each built as its method makes
16+//! them. One that fails leaves
1217 //! the queue with a failed check run; the entries behind it are tested
1318 //! again without it. Which entry answers for a failure, which entries
1419 //! ahead it breaks with, who is told and how it rejoins: integration.rs.
1621 use futures_util::future::try_join_all;
1722 use g1t_contracts::events::{ChecksEvent, QueueChanged};
1823 use g1t_contracts::repos::{
19− DeleteBranchArgs, GetByIdArgs, HeadArgs, LandArgs, Landed, Repo, RepoPath,
24+ BuildQueueStepArgs, DeleteBranchArgs, GetByIdArgs, HeadArgs, LandArgs, Landed, QueueStep, Repo, RepoPath,
2025 };
2126 use g1t_contracts::time::rfc3339;
2227 use g1t_contracts::work::*;
28+use g1t_contracts::rules::MergeMethod;
2329 use g1t_contracts::{FailureCode, Outcome, User, new_id};
2430 use g1t_kit::now_ms;
2531 use serde::Deserialize;
4450 WHERE id = ?6 AND state = 'waiting' RETURNING id AS value";
4551
4652 const ENQUEUE: &str = "INSERT INTO queue_entries
47− (id, repo_id, pull_id, number, state, enqueued_by, keep_issue_open, created_at, bypass_rules)
48− SELECT ?1, ?2, ?3, ?4, 'waiting', ?5, ?6, ?7, ?8
53+ (id, repo_id, pull_id, number, state, enqueued_by, keep_issue_open, created_at, bypass_rules,
54+ merge_method, commit_title, commit_message)
55+ SELECT ?1, ?2, ?3, ?4, 'waiting', ?5, ?6, ?7, ?8, ?9, ?10, ?11
4956 WHERE NOT EXISTS (SELECT 1 FROM queue_entries WHERE pull_id = ?3 AND state IN ('waiting', 'testing', 'passed'))
5057 RETURNING id AS value";
5158
149156 /// How many merge_group runs this attempt's dispatch registered.
150157 #[serde(default)]
151158 workflow_runs: u32,
159+ /// How it lands (`MergeMethod`); none for an entry queued before
160+ /// methods, whose states were built with merge commits.
161+ #[serde(default)]
162+ merge_method: Option<String>,
163+ /// What whoever queued it wrote for a merge or squash commit.
164+ #[serde(default)]
165+ commit_title: Option<String>,
166+ #[serde(default)]
167+ commit_message: Option<String>,
152168 }
153169
154170 impl EntryRow {
178194 attempt_branch(&self.id, self.attempt_id.as_deref())
179195 }
180196
197+ /// How it lands.
198+ pub(crate) fn method(&self) -> MergeMethod {
199+ MergeMethod::ALL
200+ .into_iter()
201+ .find(|method| self.merge_method.as_deref() == Some(method.as_str()))
202+ .unwrap_or(MergeMethod::Merge)
203+ }
204+
181205 /// Exactly what this attempt built, as recorded when it was claimed.
182206 pub(crate) fn tested_stack(&self) -> Option<Vec<QueueStackItem>> {
183207 self.tested_stack.as_deref().and_then(|stack| serde_json::from_str(stack).ok())
201225 }
202226 }
203227
228+/// What building one entry's state in the repository came to.
229+pub(crate) enum Built {
230+ /// The state's commit.
231+ State(String),
232+ /// Its change clashes with the state ahead in these files.
233+ Conflict { message: String, paths: Vec<String> },
234+ /// It could not be built, and why.
235+ Failed(String),
236+}
237+
238+/// The nearest entry ahead (each with the files its pull request changes)
239+/// that changed one of `paths`, which an entry's change clashed in.
240+pub(crate) fn clashed_with(ahead: &[(QueueStackItem, Vec<String>)], paths: &[String]) -> Option<u32> {
241+ ahead
242+ .iter()
243+ .rev()
244+ .find(|(_, files)| paths.iter().any(|path| files.contains(path)))
245+ .map(|(item, _)| item.number)
246+}
247+
204248 /// Why an entry left because what it would build is gone.
205249 const BRANCH_GONE: &str = "Its branch no longer exists.";
206250 const PULL_GONE: &str = "Its pull request no longer exists.";
268312 .await
269313 }
270314
271− /// Puts a pull request that may merge into the queue instead. Merging
272− /// it again while it is queued changes nothing.
315+ /// Puts a pull request that may merge into the queue instead, to land
316+ /// by `method` with the commit message `message` (title, body) asked
317+ /// for, if any. Merging it again while it is queued changes nothing.
318+ #[allow(clippy::too_many_arguments)]
273319 pub(crate) async fn enqueue(
274320 &self,
275321 repo: &Repo,
277323 actor: &User,
278324 keep_issue_open: bool,
279325 bypass_rules: bool,
326+ method: MergeMethod,
327+ message: (Option<&str>, Option<&str>),
280328 ) -> Result<Outcome<Pull>> {
281329 if self.queued_entry(&pull.id).await?.is_some() {
282330 return Ok(Outcome::Ok(pull.clone()));
294342 u32::from(keep_issue_open).into(),
295343 rfc3339(now).into(),
296344 u32::from(bypass_rules).into(),
345+ method.as_str().into(),
346+ message.0.map_or(JsValue::NULL, JsValue::from),
347+ message.1.map_or(JsValue::NULL, JsValue::from),
297348 ])?
298349 .first::<crate::rows::ValueRow>(None)
299350 .await?;
537588 Ok(())
538589 }
539590
540− /// The next batch to test, if nothing is being tested now: one job per
541− /// entry, each building the default branch with that entry and every
542− /// entry ahead of it.
591+ /// Builds the next batch, if nothing is being tested now: each entry
592+ /// landed by its own method on the state of the entries ahead of it,
593+ /// in the repository itself (`build_step`), and its `merge_group`
594+ /// workflows started there. No sandbox builds anything, so the jobs
595+ /// returned are always none.
543596 ///
544597 /// Smaller combinations a failure is being narrowed down with
545598 /// (integration.rs) are built alongside, whatever the queue is doing.
546599 pub(crate) async fn queue_build(&self, a: QueueBuildArgs) -> Result<Vec<QueueJob>> {
547600 // Also resumes a publication interrupted after the Git update.
548601 self.settle(&a.repo_id).await?;
549− let mut jobs = self.probe_jobs(&a.repo_id).await?;
550− jobs.extend(self.queue_batch(a).await?);
551− Ok(jobs)
602+ self.build_probes(&a.repo_id).await?;
603+ self.queue_batch(a).await?;
604+ Ok(Vec::new())
552605 }
553606
554− async fn queue_batch(&self, a: QueueBuildArgs) -> Result<Vec<QueueJob>> {
607+ async fn queue_batch(&self, a: QueueBuildArgs) -> Result<()> {
555608 let active = self.entries(&a.repo_id, true).await?;
556609 // How the default branch's merge queue rule batches (rulesets.rs).
557610 let rule = self.queue_rule(&a.repo_id).await?;
572625 return Box::pin(self.queue_batch(a)).await;
573626 }
574627 if active.iter().any(|row| row.state() != QueueState::Waiting) {
575− return Ok(Vec::new());
628+ return Ok(());
576629 }
577630 let batch: Vec<EntryRow> = active
578631 .into_iter()
579632 .take(rule.max_entries_to_build.max(1) as usize)
580633 .collect();
581634 let Some(first) = batch.first() else {
582− return Ok(Vec::new());
635+ return Ok(());
583636 };
584637 // Too few to start yet, and the oldest has not waited long enough.
585638 if batch.len() < rule.min_entries_to_merge as usize
586639 && first.created_at.as_str()
587640 > minutes_ago(u64::from(rule.min_entries_wait_minutes)).as_str()
588641 {
589− return Ok(Vec::new());
642+ return Ok(());
590643 }
591644 let Some(actor) = first.actor() else {
592− return Ok(Vec::new());
645+ return Ok(());
593646 };
594647 let repo: Outcome<Repo> = g1t_kit::call(
595648 &self.repos,
601654 )
602655 .await?;
603656 let Outcome::Ok(repo) = crate::retired::unless_archived(repo) else {
604− return Ok(Vec::new());
657+ return Ok(());
605658 };
606659 let base: Option<String> = g1t_kit::call(
607660 &self.repos,
613666 )
614667 .await?;
615668 let Some(base) = base else {
616− return Ok(Vec::new());
669+ return Ok(());
617670 };
618671
672+ let settings = self.settings(&repo.id).await?;
619673 // Each entry's change, as it is now.
620674 let mut items: Vec<(EntryRow, Pull, QueueStackItem)> = Vec::new();
621675 for row in batch {
648702 namespace: repo.namespace.clone(),
649703 name: repo.name.clone(),
650704 });
705+ // How it lands, and the commit message it lands with: worked
706+ // out once, so that the state is the one landing makes and
707+ // building it again makes the same commits.
708+ let method = row.method();
709+ let message = match self
710+ .merge_message_for(&repo, &pull, &settings, method, row.commit_title.as_deref(), row.commit_message.as_deref())
711+ .await?
712+ {
713+ Ok(message) => message,
714+ Err(problem) => {
715+ self.leave(&repo.id, &pull, QueueState::Removed, Some(problem)).await?;
716+ continue;
717+ }
718+ };
651719 let item = QueueStackItem {
652720 number: pull.number,
653721 title: pull.title.clone(),
657725 .clone()
658726 .unwrap_or_else(|| repo.default_branch.clone()),
659727 commit,
728+ method,
729+ message,
730+ author: crate::merging::squash_author(&pull, method),
660731 };
661732 items.push((row, pull, item));
662733 }
663734
735+ // Each built on the one before it: landing them in order moves the
736+ // default branch to each tested state in turn.
664737 let now = rfc3339(now_ms());
665− let mut jobs = Vec::new();
666− for index in 0..items.len() {
667− let (row, pull, _) = &items[index];
668− let stack: Vec<QueueStackItem> = items[..=index]
669− .iter()
670− .map(|(_, _, item)| item.clone())
671− .collect();
672− let ahead: Vec<u32> = stack[..index].iter().map(|item| item.number).collect();
738+ let mut onto = base.clone();
739+ let mut built: Vec<(QueueStackItem, Vec<String>)> = Vec::new();
740+ for (row, pull, item) in items {
741+ let mut stack: Vec<QueueStackItem> = built.iter().map(|(item, _)| item.clone()).collect();
742+ stack.push(item.clone());
743+ let ahead: Vec<u32> = built.iter().map(|(item, _)| item.number).collect();
673744 let token = new_token();
674745 let claimed = self
675746 .db
676747 .prepare(CLAIM_BUILD)
677748 .bind(&[
678749 hash(&token).into(),
679− stack[index].commit.as_str().into(),
750+ item.commit.as_str().into(),
680751 base.as_str().into(),
681752 serde_json::to_string(&ahead)?.into(),
682753 now.as_str().into(),
686757 ])?
687758 .first::<crate::rows::ValueRow>(None)
688759 .await?;
760+ // Another request is building it: what follows waits for it.
689761 if claimed.is_none() {
690− continue;
762+ break;
691763 }
692− // The sandbox reads the change and pushes the tested state as
693− // the pull request's owner (whoever asked g1t for it, or its
694− // author): a real account, whose token carries its memberships.
695− // (Whoever queued it may be g1t itself.)
696− let actor = pull.owner().clone();
764+ let Some(actor) = row.actor() else { break };
697765 let branch = attempt_branch(&row.id, Some(&hash(&token)));
698− jobs.push(QueueJob {
766+ let step = self.build_step(&repo, &onto, &item, &branch, &actor, settings.require_web_commit_sign_off).await?;
767+ let mut report = ReportQueueArgs {
699768 entry_id: row.id.clone(),
700769 token,
701− repo: RepoPath {
702− namespace: repo.namespace.clone(),
703− name: repo.name.clone(),
704− },
705− default_branch: repo.default_branch.clone(),
706− base_commit: base.clone(),
707− branch,
708− // The state is checked by its merge_group workflows.
709− contract_checks: Vec::new(),
710− stack,
711− checks: Vec::new(),
712− actor,
713− });
770+ combined_commit: None,
771+ results: Vec::new(),
772+ error: None,
773+ conflict_with: None,
774+ conflicts: Vec::new(),
775+ };
776+ let next = match step {
777+ Built::State(commit) => {
778+ report.combined_commit = Some(commit.clone());
779+ Some(commit)
780+ }
781+ Built::Conflict { message, paths } => {
782+ // The entry ahead whose change touched the same files.
783+ report.conflict_with = clashed_with(&built, &paths);
784+ report.error = Some(message);
785+ report.conflicts = paths;
786+ None
787+ }
788+ Built::Failed(message) => {
789+ report.error = Some(message);
790+ None
791+ }
792+ };
793+ self.report_queue(report).await?;
794+ // One that could not be built leaves; those behind it are built
795+ // in the next batch, without it.
796+ let Some(next) = next else { break };
797+ onto = next;
798+ built.push((item, pull.files.iter().map(|file| file.path.clone()).collect()));
714799 }
715− Ok(jobs)
800+ Ok(())
716801 }
717802
718− /// A sandbox's result for one combined state.
803+ /// `item` landed by its method on `onto`, a state of the queue, in the
804+ /// repository itself (repos `build_queue_step`), and `branch` pointed at
805+ /// the state that makes, as `actor` (whoever queued it).
806+ pub(crate) async fn build_step(
807+ &self,
808+ repo: &Repo,
809+ onto: &str,
810+ item: &QueueStackItem,
811+ branch: &str,
812+ actor: &User,
813+ sign_off: bool,
814+ ) -> Result<Built> {
815+ let Some(source_id) = self.source_of(repo, item).await? else {
816+ return Ok(Built::Failed(format!("#{}'s repository is no longer there.", item.number)));
817+ };
818+ // A state recorded before methods: merged in as it was then.
819+ let message = item.message.clone().or_else(|| {
820+ matches!(item.method, MergeMethod::Merge | MergeMethod::Squash).then(|| format!("Merge #{}: {}", item.number, item.title))
821+ });
822+ let step: Outcome<QueueStep> = g1t_kit::call(
823+ &self.repos,
824+ "build_queue_step",
825+ &BuildQueueStepArgs {
826+ target_id: repo.id.clone(),
827+ onto: onto.to_owned(),
828+ source_id,
829+ branch: item.branch.clone(),
830+ commit: item.commit.clone(),
831+ method: item.method,
832+ message,
833+ author: item.author.clone(),
834+ sign_off,
835+ actor: actor.clone(),
836+ push_branch: branch.to_owned(),
837+ },
838+ )
839+ .await?;
840+ Ok(match step {
841+ Outcome::Ok(QueueStep::Built { commit, .. }) => Built::State(commit),
842+ Outcome::Ok(QueueStep::Conflict { message, paths }) => Built::Conflict { message, paths },
843+ Outcome::Ok(QueueStep::Refused { message }) => Built::Failed(message),
844+ Outcome::Fail(failure) => Built::Failed(failure.message),
845+ })
846+ }
847+
848+ /// The repository holding a stack item's change: the queue's own, or
849+ /// its pull request's fork.
850+ async fn source_of(&self, repo: &Repo, item: &QueueStackItem) -> Result<Option<String>> {
851+ if item.source.namespace == repo.namespace && item.source.name == repo.name {
852+ return Ok(Some(repo.id.clone()));
853+ }
854+ // A fork's, through its pull request, whoever can read the fork.
855+ Ok(self.pull(&repo.id, item.number).await?.map(|pull| pull.source_id().to_owned()))
856+ }
857+
858+ /// The result of building one combined state.
719859 pub(crate) async fn report_queue(&self, a: ReportQueueArgs) -> Result<Outcome<QueueState>> {
720860 // A smaller combination built to narrow down a failure.
721861 if a.entry_id.starts_with("qpr_") {
12011341 // the merge record exists, only queue bookkeeping remains.
12021342 if row.landing_commit.is_some()
12031343 && pull.status == PullStatus::Merged
1204− && pull.head_commit == row.landing_commit
1344+ && pull.merge_commit == row.landing_commit
12051345 {
12061346 self.finish_landing(&row).await?;
12071347 continue;
12541394 row.combined_commit.as_deref().unwrap(),
12551395 row.bypass_rules != 0,
12561396 evidence.all(),
1397+ row.method(),
12571398 )
12581399 .await?;
12591400 if let Some(refusal) = gate.refusal() {
12891430 target_branch: None,
12901431 expected_commit: row.combined_commit.clone(),
12911432 target_id: None,
1292− // The state the queue tested lands as it is: the queue
1293− // rule's method is not applied to it.
1294− method: Some(g1t_contracts::rules::MergeMethod::FastForward),
1433+ // The state tested was built by the entry's method on what
1434+ // landed before it: it lands exactly as tested, the default
1435+ // branch moving to it.
1436+ method: Some(MergeMethod::FastForward),
12951437 message: None,
12961438 author: None,
12971439 sign_off: false,
13091451 landed = g1t_kit::call(&self.repos, "land", &land).await?;
13101452 }
13111453 let landed = match landed {
1312− Outcome::Ok(landed) => landed,
1454+ // Recorded as the method it was built by.
1455+ Outcome::Ok(landed) => Landed { method: row.method(), ..landed },
13131456 Outcome::Fail(_) => {
13141457 let abandoned = self
13151458 .db
+52−2
3636 let enqueue = |id: &str| {
3737 db.query_row(
3838 ENQUEUE,
39− params![id, "repo", "pull", 1, "{}", 0, "later", 0],
39+ params![id, "repo", "pull", 1, "{}", 0, "later", 0, "merge", None::<String>, None::<String>],
4040 |_| Ok(()),
4141 )
4242 .optional()
507507 fn a_bypass_asked_for_at_admission_is_kept_for_landing() {
508508 let db = database();
509509 db.execute("DELETE FROM queue_entries", []).unwrap();
510− assert!(exists(&db, ENQUEUE, params!["first", "repo", "pull", 1, "{}", 0, "later", 1]));
510+ assert!(exists(&db, ENQUEUE, params!["first", "repo", "pull", 1, "{}", 0, "later", 1, "merge", None::<String>, None::<String>]));
511511 assert_eq!(
512512 db.query_row("SELECT bypass_rules FROM queue_entries", [], |row| row.get::<_, u32>(0)).unwrap(),
513513 1
597597 "its branch no longer exists, so it cannot be tested or landed. Push the branch again and merge to rejoin."
598598 );
599599 }
600+
601+#[test]
602+fn an_entry_keeps_how_it_lands_and_one_queued_before_methods_merges() {
603+ let db = database();
604+ db.execute("DELETE FROM queue_entries", []).unwrap();
605+ assert!(exists(&db, ENQUEUE, params!["first", "repo", "pull", 1, "{}", 0, "later", 0, "squash", "Ship it", "All of it"]));
606+ let (method, title, body): (String, String, String) = db
607+ .query_row("SELECT merge_method, commit_title, commit_message FROM queue_entries", [], |row| {
608+ Ok((row.get(0)?, row.get(1)?, row.get(2)?))
609+ })
610+ .unwrap();
611+ assert_eq!((method.as_str(), title.as_str(), body.as_str()), ("squash", "Ship it", "All of it"));
612+ let row = |method: serde_json::Value| -> super::EntryRow {
613+ serde_json::from_value(serde_json::json!({
614+ "id": "e", "repo_id": "r", "pull_id": "p", "number": 1, "state": "waiting", "enqueued_by": "{}",
615+ "keep_issue_open": 0, "head_commit": null, "base_commit": null, "ahead": null, "combined_commit": null,
616+ "results": null, "error": null, "tested_at": null, "created_at": "now", "finished_at": null,
617+ "merge_method": method,
618+ }))
619+ .unwrap()
620+ };
621+ use g1t_contracts::rules::MergeMethod;
622+ assert_eq!(row("rebase".into()).method(), MergeMethod::Rebase);
623+ assert_eq!(row("fast_forward".into()).method(), MergeMethod::FastForward);
624+ // Its states were built with merge commits then.
625+ assert_eq!(row(serde_json::Value::Null).method(), MergeMethod::Merge);
626+}
627+
628+#[test]
629+fn a_clash_names_the_nearest_entry_ahead_that_changed_the_same_file() {
630+ let item = |number: u32| g1t_contracts::work::QueueStackItem {
631+ number,
632+ title: String::new(),
633+ source: g1t_contracts::repos::RepoPath { namespace: "n".into(), name: "r".into() },
634+ branch: "b".into(),
635+ commit: "c".into(),
636+ method: g1t_contracts::rules::MergeMethod::Squash,
637+ message: None,
638+ author: None,
639+ };
640+ let ahead = vec![
641+ (item(1), vec!["a.rs".to_owned(), "b.rs".to_owned()]),
642+ (item(2), vec!["b.rs".to_owned()]),
643+ (item(3), vec!["c.rs".to_owned()]),
644+ ];
645+ assert_eq!(super::clashed_with(&ahead, &["b.rs".to_owned()]), Some(2));
646+ assert_eq!(super::clashed_with(&ahead, &["a.rs".to_owned()]), Some(1));
647+ // With the default branch itself.
648+ assert_eq!(super::clashed_with(&ahead, &["d.rs".to_owned()]), None);
649+}
+4−3
824824 commit: &str,
825825 bypass_rules: bool,
826826 statuses: Vec<g1t_contracts::work::CommitStatus>,
827+ method: MergeMethod,
827828 ) -> Result<Gate> {
828− // The tested state lands as it was built, which the queue rule's
829− // method does not change (queue.rs): no method is judged.
830− let gate = self.merge_gate_at(repo, pull, Some(actor), false, true, Some((commit, statuses)), None).await?;
829+ // The tested state was built by the entry's method (queue.rs), which
830+ // the rules judge as they are now.
831+ let gate = self.merge_gate_at(repo, pull, Some(actor), false, true, Some((commit, statuses)), Some(method)).await?;
831832 Ok(if landing_honors_bypass(actor, bypass_rules) { gate } else { gate.without_bypass() })
832833 }
833834

Comments

Sign in to comment.