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
1,546 linesCodeBlameRaw
1//! Landing a pull request by making commits on the branch it merges into:
2//! a merge commit, one squashed commit, or the branch's commits made again
3//! on top (`land` with a `method`); and rebasing a pull request's branch
4//! onto that branch (`update_pull_branch` with `rebase`).
5//!
6//! Each is worked out here, without a sandbox, from trees. The changes one
7//! side made since the two last agreed are applied to the other side's
8//! tree ([`apply`]): a file only that side changed is taken from it, and a
9//! file both changed is merged line by line, as git merges text
10//! (test_merge.rs `merge_text`). Lines both changed differently, a file
11//! one side deleted and the other changed, or a binary file both changed
12//! are a conflict, and nothing is made: the conflict names the files. Only
13//! the trees on the way to changed files are read and written. What is
14//! made is pushed as one pack, with whatever a fork's commits bring that
15//! the repository lacks, and the branch moves only if it is still where it
16//! was.
17//!
18//! The work is bounded for a Worker: at most [`MAX_LAND_FILES`] files
19//! applied at a time, [`MAX_LINE_MERGES`] of them merged line by line
20//! (each at most test_merge.rs `MAX_MERGE_BYTES`), and at most
21//! [`MAX_REBASE_COMMITS`] commits made again by a rebase. Beyond those g1t
22//! says it cannot do it itself, and nothing is changed.
23
24use std::cell::RefCell;
25use std::collections::{BTreeSet, HashMap, HashSet};
26
27use futures_util::future::{try_join, try_join_all};
28use g1t_contracts::repos::{
29 Branch, BuildQueueStepArgs, Commit, EntryKind, GitAccess, LandArgs, Landed, NeedsAgentReason, PullBranchUpdate, QueueStep,
30 Repo, TreeEntry,
31};
32use g1t_contracts::rules::MergeMethod;
33use g1t_contracts::time::parse_rfc3339;
34use g1t_contracts::{FailureCode, Outcome, User};
35use g1t_kit::now_ms;
36use g1t_scan::pack::{ObjectKind, extend_pack, object_id, parse_tree, write_pack};
37use worker::Result;
38
39use crate::catch_up::{BranchUpdate, Change, Signature, ancestors, commit_object_by, merge_tree, moved_since, read_dirs};
40use crate::diff::{FileEntry, changed_entries_up_to};
41use crate::registry::store_key;
42use crate::store::{GitRepo, GitStore, Scope};
43use crate::test_merge::{as_text, merge_text};
44use crate::{MAX_ANCESTRY, Repos, descends_from, land, nearest_ancestor_in};
45
46/// The most files one side's change may touch for g1t to apply it.
47pub(crate) const MAX_LAND_FILES: usize = 1000;
48/// The most files both sides changed that are merged line by line at once.
49pub(crate) const MAX_LINE_MERGES: usize = 100;
50/// The most commits a rebase makes again.
51pub(crate) const MAX_REBASE_COMMITS: usize = 250;
52
53/// Reads objects from a repository, then from a second one (a fork, or the
54/// repository a fork came from) for what the first lacks, and first of all
55/// from the objects made here and not yet pushed.
56pub(crate) struct Reader<'a, R> {
57 first: &'a R,
58 second: Option<&'a R>,
59 trees: RefCell<HashMap<String, Vec<TreeEntry>>>,
60 blobs: RefCell<HashMap<String, Vec<u8>>>,
61}
62
63/// What git's tree mode says an entry is.
64fn kind_of(mode: &str) -> EntryKind {
65 match mode {
66 "40000" | "040000" => EntryKind::Tree,
67 "100755" => EntryKind::Exec,
68 "120000" => EntryKind::Symlink,
69 "160000" => EntryKind::Gitlink,
70 _ => EntryKind::Blob,
71 }
72}
73
74impl<'a, R: GitRepo> Reader<'a, R> {
75 pub(crate) fn new(first: &'a R, second: Option<&'a R>) -> Self {
76 Reader { first, second, trees: RefCell::default(), blobs: RefCell::default() }
77 }
78
79 /// Keeps `objects`, made here, to be read before they are pushed.
80 fn keep(&self, objects: &[(ObjectKind, Vec<u8>)]) {
81 for (kind, bytes) in objects {
82 match kind {
83 ObjectKind::Tree => {
84 let entries = parse_tree(bytes)
85 .into_iter()
86 .map(|item| TreeEntry { kind: kind_of(&item.mode), name: item.name, hash: item.id })
87 .collect();
88 self.trees.borrow_mut().insert(object_id(ObjectKind::Tree, bytes), entries);
89 }
90 ObjectKind::Blob => {
91 self.blobs.borrow_mut().insert(object_id(ObjectKind::Blob, bytes), bytes.clone());
92 }
93 _ => {}
94 }
95 }
96 }
97}
98
99impl<R: GitRepo> GitRepo for Reader<'_, R> {
100 async fn access(&self, scope: Scope) -> Result<GitAccess> {
101 self.first.access(scope).await
102 }
103 async fn branches(&self) -> Result<Vec<Branch>> {
104 self.first.branches().await
105 }
106 async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>> {
107 let found = self.first.log(git_ref, limit).await?;
108 match self.second {
109 Some(second) if found.is_empty() => second.log(git_ref, limit).await,
110 _ => Ok(found),
111 }
112 }
113 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>> {
114 match (self.first.parents(commit_hash).await?, self.second) {
115 (None, Some(second)) => second.parents(commit_hash).await,
116 (found, _) => Ok(found),
117 }
118 }
119 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>> {
120 let made = self.trees.borrow().get(tree_hash).cloned();
121 if made.is_some() {
122 return Ok(made);
123 }
124 match (self.first.read_tree(tree_hash).await?, self.second) {
125 (None, Some(second)) => second.read_tree(tree_hash).await,
126 (found, _) => Ok(found),
127 }
128 }
129 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>> {
130 let made = self.blobs.borrow().get(blob_hash).cloned();
131 if made.is_some() {
132 return Ok(made);
133 }
134 match (self.first.read_blob(blob_hash).await?, self.second) {
135 (None, Some(second)) => second.read_blob(blob_hash).await,
136 (found, _) => Ok(found),
137 }
138 }
139 async fn read_file(&self, git_ref: &str, path: &str) -> Result<Option<Vec<u8>>> {
140 self.first.read_file(git_ref, path).await
141 }
142 async fn fork(&self, target_key: &str, default_branch_only: bool) -> Result<()> {
143 self.first.fork(target_key, default_branch_only).await
144 }
145}
146
147/// What becomes of one file a change touched, applied to another tree.
148#[derive(Debug, PartialEq, Eq)]
149pub(crate) enum Resolution {
150 /// The other tree has it as the change started from: put the change's
151 /// version there (`None`: remove it).
152 Take(Option<FileEntry>),
153 /// The other tree already has it as the change made it.
154 Keep,
155 /// Both changed the text: merge the three, as `kind`.
156 Content { kind: EntryKind, base: Option<String>, ours: String, theirs: String },
157 /// Both changed it in ways that cannot be merged.
158 Conflict,
159}
160
161fn text_kind(kind: EntryKind) -> bool {
162 matches!(kind, EntryKind::Blob | EntryKind::Exec)
163}
164
165/// How a file the change took from `base` to `ours` comes out where it is
166/// `theirs` (a directory's entry included, which a file cannot replace).
167pub(crate) fn resolve(base: Option<&FileEntry>, ours: Option<&FileEntry>, theirs: Option<&FileEntry>) -> Resolution {
168 if theirs == base {
169 return Resolution::Take(ours.cloned());
170 }
171 if theirs == ours {
172 return Resolution::Keep;
173 }
174 // Deleted on one side and changed on the other.
175 let (Some(ours), Some(theirs)) = (ours, theirs) else {
176 return Resolution::Conflict;
177 };
178 if ours.0 == EntryKind::Tree || theirs.0 == EntryKind::Tree {
179 return Resolution::Conflict;
180 }
181 let base_kind = base.map(|(kind, _)| *kind);
182 // A change of mode on one side is kept with the other's content.
183 let kind = if ours.0 == theirs.0 {
184 ours.0
185 } else if base_kind == Some(ours.0) {
186 theirs.0
187 } else if base_kind == Some(theirs.0) {
188 ours.0
189 } else {
190 return Resolution::Conflict;
191 };
192 if ours.1 == theirs.1 {
193 return Resolution::Take(Some((kind, ours.1.clone())));
194 }
195 if !text_kind(ours.0) || !text_kind(theirs.0) || base_kind.is_some_and(|kind| !text_kind(kind)) {
196 return Resolution::Conflict;
197 }
198 Resolution::Content { kind, base: base.map(|(_, hash)| hash.clone()), ours: ours.1.clone(), theirs: theirs.1.clone() }
199}
200
201/// A change applied to a tree.
202#[derive(Debug, PartialEq, Eq)]
203pub(crate) enum Applied {
204 /// The tree it makes.
205 Tree(String),
206 /// The files whose changes clash, by path.
207 Conflict(Vec<String>),
208 /// What g1t cannot apply itself.
209 Unsupported(String),
210}
211
212/// The change from tree `from` (`None`: nothing) to tree `to`, applied to
213/// tree `onto`. The objects it makes are added to `objects`, and kept in
214/// `git` for what is applied after it.
215pub(crate) async fn apply<R: GitRepo>(
216 git: &Reader<'_, R>,
217 from: Option<&str>,
218 to: &str,
219 onto: &str,
220 objects: &mut Vec<(ObjectKind, Vec<u8>)>,
221) -> Result<Applied> {
222 if from == Some(onto) || to == onto {
223 return Ok(Applied::Tree(to.to_owned()));
224 }
225 if from == Some(to) {
226 return Ok(Applied::Tree(onto.to_owned()));
227 }
228 let (ours, cut) = changed_entries_up_to(git, from, to, MAX_LAND_FILES).await?;
229 if cut {
230 return Ok(Applied::Unsupported(format!("it changes more than {MAX_LAND_FILES} files")));
231 }
232 // Each changed path as `onto` has it.
233 let dirs: BTreeSet<String> = ours.iter().flat_map(|(path, _, _)| ancestors(path).map(str::to_owned)).collect();
234 let found = read_dirs(git, onto, &dirs).await?;
235 let theirs_of = |path: &str| -> Option<FileEntry> {
236 let (parent, name) = path.rsplit_once('/').unwrap_or(("", path));
237 let (_, entries) = found.get(parent)?;
238 entries.iter().find(|entry| entry.name == name).map(|entry| (entry.kind, entry.hash.clone()))
239 };
240 let mut changes = Vec::new();
241 let mut conflicts = Vec::new();
242 let mut merges = Vec::new();
243 for (path, base, mine) in ours {
244 let theirs = theirs_of(&path);
245 match resolve(base.as_ref(), mine.as_ref(), theirs.as_ref()) {
246 Resolution::Take(entry) => changes.push(Change { path, entry }),
247 Resolution::Keep => {}
248 Resolution::Conflict => conflicts.push(path),
249 Resolution::Content { kind, base, ours, theirs } => merges.push((path, kind, base, ours, theirs)),
250 }
251 }
252 if merges.len() > MAX_LINE_MERGES {
253 return Ok(Applied::Unsupported(format!("both sides changed more than {MAX_LINE_MERGES} of the same files")));
254 }
255 let read = try_join_all(merges.iter().map(|(_, _, base, ours, theirs)| async move {
256 let base = async {
257 match base {
258 Some(hash) => git.read_blob(hash).await,
259 None => Ok(Some(Vec::new())),
260 }
261 };
262 let (base, (ours, theirs)) = try_join(base, try_join(git.read_blob(ours), git.read_blob(theirs))).await?;
263 Ok::<_, worker::Error>((base, ours, theirs))
264 }))
265 .await?;
266 let mut made: Vec<(ObjectKind, Vec<u8>)> = Vec::new();
267 for ((path, kind, ..), (base, ours, theirs)) in merges.into_iter().zip(read) {
268 let texts = (as_text(base), as_text(ours), as_text(theirs));
269 let (Ok(base), Ok(ours), Ok(theirs)) = texts else {
270 if matches!(texts, (Err(true), _, _) | (_, Err(true), _) | (_, _, Err(true))) {
271 conflicts.push(path);
272 continue;
273 }
274 return Ok(Applied::Unsupported(format!("{path} is too large to merge line by line")));
275 };
276 match merge_text(&base, &ours, &theirs) {
277 Some(merged) => {
278 let blob = merged.into_bytes();
279 let id = object_id(ObjectKind::Blob, &blob);
280 made.push((ObjectKind::Blob, blob));
281 changes.push(Change { path, entry: Some((kind, id)) });
282 }
283 None => conflicts.push(path),
284 }
285 }
286 if !conflicts.is_empty() {
287 conflicts.sort();
288 return Ok(Applied::Conflict(conflicts));
289 }
290 if changes.is_empty() {
291 return Ok(Applied::Tree(onto.to_owned()));
292 }
293 let trees: HashMap<String, Vec<TreeEntry>> = found.into_values().collect();
294 let merged = match merge_tree(onto, &trees, &changes) {
295 Ok(merged) => merged,
296 Err(why) => return Ok(Applied::Unsupported(why)),
297 };
298 made.extend(merged.objects.into_iter().map(|bytes| (ObjectKind::Tree, bytes)));
299 git.keep(&made);
300 objects.extend(made);
301 Ok(Applied::Tree(merged.tree))
302}
303
304/// The commits of a branch the branch it merges into does not have,
305/// oldest first and each after its parents: those reachable from the
306/// newest of `history` (the branch's first-parent chain) and not from
307/// `shared` (the other branch's). `None` when there are more than `limit`.
308pub(crate) async fn branch_commits<R: GitRepo>(
309 git: &R,
310 history: &[Commit],
311 shared: &HashSet<String>,
312 limit: usize,
313) -> Result<Option<Vec<Commit>>> {
314 let mut known: HashMap<String, Commit> = history.iter().map(|commit| (commit.hash.clone(), commit.clone())).collect();
315 let Some(head) = history.first() else {
316 return Ok(Some(Vec::new()));
317 };
318 let mut order = Vec::new();
319 let mut seen = HashSet::new();
320 // Depth first, a commit written once its parents are: the first
321 // parent's line before the others.
322 let mut stack = vec![(head.hash.clone(), false)];
323 while let Some((hash, parents_done)) = stack.pop() {
324 if parents_done {
325 if let Some(commit) = known.get(&hash) {
326 order.push(commit.clone());
327 }
328 continue;
329 }
330 if shared.contains(&hash) || !seen.insert(hash.clone()) {
331 continue;
332 }
333 if seen.len() > limit {
334 return Ok(None);
335 }
336 let commit = match known.get(&hash) {
337 Some(commit) => commit.clone(),
338 None => match git.log(&hash, 1).await?.into_iter().next() {
339 Some(commit) => {
340 known.insert(hash.clone(), commit.clone());
341 commit
342 }
343 // Not there to read: where the branch's history ends.
344 None => continue,
345 },
346 };
347 stack.push((hash, true));
348 for parent in commit.parents.iter().rev() {
349 stack.push((parent.clone(), false));
350 }
351 }
352 Ok(Some(order))
353}
354
355/// A branch's commits, made again.
356#[derive(Debug)]
357pub(crate) struct Replayed {
358 /// The last commit made, or where they were made onto if none was.
359 pub head: String,
360 /// The commits made, oldest first, each with the one it was made from.
361 pub made: Vec<(String, String)>,
362 /// The messages and authors of what was made, for the rules.
363 pub facts: Vec<g1t_contracts::rules::CommitFacts>,
364}
365
366/// How replaying went.
367#[derive(Debug)]
368pub(crate) enum Replay {
369 Done(Replayed),
370 /// A commit's changes clash with what is there: the commit, and the files.
371 Conflict { commit: Commit, paths: Vec<String> },
372 Unsupported { commit: Commit, why: String },
373}
374
375/// When a commit says it was authored, in seconds.
376fn authored_seconds(commit: &Commit) -> u64 {
377 parse_rfc3339(&commit.authored_at).unwrap_or(0) / 1000
378}
379
380/// Makes each of `commits` (oldest first) again on top of `onto` (a commit
381/// and its tree): its changes applied to the commit before, its author,
382/// date and message kept, committed by `committer` with `sign_off` added.
383/// Merge commits are left out, their changes already in what they merged,
384/// and so is a commit whose changes are all there already.
385pub(crate) async fn replay<R: GitRepo>(
386 git: &Reader<'_, R>,
387 commits: &[Commit],
388 onto: (&str, &str),
389 committer: &Signature<'_>,
390 sign_off: Option<&str>,
391 objects: &mut Vec<(ObjectKind, Vec<u8>)>,
392) -> Result<Replay> {
393 let trees: HashMap<&str, &str> = commits.iter().map(|commit| (commit.hash.as_str(), commit.tree_hash.as_str())).collect();
394 let (mut head, mut tree) = (onto.0.to_owned(), onto.1.to_owned());
395 let mut made = Vec::new();
396 let mut facts = Vec::new();
397 for commit in commits.iter().filter(|commit| commit.parents.len() <= 1) {
398 let from = match commit.parents.first() {
399 None => None,
400 Some(parent) => match trees.get(parent.as_str()) {
401 Some(tree) => Some((*tree).to_owned()),
402 None => match git.log(parent, 1).await?.into_iter().next() {
403 Some(parent) => Some(parent.tree_hash),
404 None => {
405 return Ok(Replay::Unsupported { commit: commit.clone(), why: format!("its parent {parent} could not be read") });
406 }
407 },
408 },
409 };
410 let applied = apply(git, from.as_deref(), &commit.tree_hash, &tree, objects).await?;
411 let new_tree = match applied {
412 Applied::Tree(new_tree) => new_tree,
413 Applied::Conflict(paths) => return Ok(Replay::Conflict { commit: commit.clone(), paths }),
414 Applied::Unsupported(why) => return Ok(Replay::Unsupported { commit: commit.clone(), why }),
415 };
416 if new_tree == tree {
417 continue;
418 }
419 let author = Signature { name: &commit.author.name, email: &commit.author.email, seconds: authored_seconds(commit) };
420 let message = with_trailers(&commit.message, sign_off.into_iter().map(str::to_owned).collect::<Vec<_>>().as_slice());
421 let bytes = commit_object_by(&new_tree, &[&head], &author, committer, &message);
422 let id = object_id(ObjectKind::Commit, &bytes);
423 objects.push((ObjectKind::Commit, bytes));
424 let mut fact = crate::rules::made_commit(&id, &message, committer.email, 1, Vec::new());
425 fact.author_email = Some(commit.author.email.clone());
426 facts.push(fact);
427 made.push((id.clone(), commit.hash.clone()));
428 head = id;
429 tree = new_tree;
430 }
431 Ok(Replay::Done(Replayed { head, made, facts }))
432}
433
434/// A trailer's key and who or what it names, compared without case: the
435/// address in `<...>` when there is one.
436fn trailer_parts(line: &str) -> Option<(String, String)> {
437 let (key, value) = line.trim().split_once(':')?;
438 if key.is_empty() || !key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') || !value.starts_with(' ') {
439 return None;
440 }
441 let value = value.trim();
442 let named = match (value.rfind('<'), value.rfind('>')) {
443 (Some(open), Some(close)) if open < close => &value[open + 1..close],
444 _ => value,
445 };
446 Some((key.to_ascii_lowercase(), named.to_lowercase()))
447}
448
449/// `message` with `trailers` added at its end, each unless one with the
450/// same key naming the same address is there already. They join the
451/// message's own trailers when its last paragraph is made of them, and
452/// otherwise start a paragraph of their own.
453pub(crate) fn with_trailers(message: &str, trailers: &[String]) -> String {
454 let message = message.trim_end();
455 let mut present: Vec<(String, String)> = message.lines().filter_map(trailer_parts).collect();
456 let mut adding: Vec<&str> = Vec::new();
457 for trailer in trailers {
458 match trailer_parts(trailer) {
459 Some(parts) if present.contains(&parts) => {}
460 Some(parts) => {
461 present.push(parts);
462 adding.push(trailer);
463 }
464 None => adding.push(trailer),
465 }
466 }
467 if adding.is_empty() {
468 return message.to_owned();
469 }
470 if message.is_empty() {
471 return adding.join("\n");
472 }
473 let ends_in_trailers = message
474 .rsplit_once("\n\n")
475 .is_some_and(|(_, last)| !last.trim().is_empty() && last.lines().all(|line| trailer_parts(line).is_some()));
476 let gap = if ends_in_trailers { "\n" } else { "\n\n" };
477 format!("{message}{gap}{}", adding.join("\n"))
478}
479
480/// The `Signed-off-by:` trailer for someone.
481pub(crate) fn sign_off_trailer(name: &str, email: &str) -> String {
482 format!("Signed-off-by: {name} <{email}>")
483}
484
485/// A `Co-authored-by:` trailer for everyone who wrote one of `commits`
486/// other than `author` (by address) and g1t itself, once each, in the
487/// order they first appear. Merge commits change nothing of their own and
488/// credit no one.
489pub(crate) fn co_authors(commits: &[Commit], author: &str) -> Vec<String> {
490 let mut seen: HashSet<String> = [author.to_lowercase(), g1t_contracts::system::EMAIL.to_owned()].into_iter().collect();
491 commits
492 .iter()
493 .filter(|commit| commit.parents.len() <= 1 && !commit.author.email.trim().is_empty())
494 .filter(|commit| seen.insert(commit.author.email.trim().to_lowercase()))
495 .map(|commit| format!("Co-authored-by: {} <{}>", commit.author.name, commit.author.email.trim()))
496 .collect()
497}
498
499/// `message` with its `Co-authored-by:` trailers made right, as a
500/// template may have written them from addresses alone: none for the
501/// commit's own author (`author_email`) or g1t, and one naming someone in
502/// `known` (trailers g1t made from the commits) as that one names them.
503pub(crate) fn tidy_co_authors(message: &str, author_email: &str, known: &[String]) -> String {
504 let email_of = |line: &str| {
505 line.rsplit_once('<')
506 .and_then(|(_, rest)| rest.split_once('>'))
507 .map(|(email, _)| email.trim().to_lowercase())
508 };
509 let author = author_email.trim().to_lowercase();
510 message
511 .lines()
512 .filter_map(|line| {
513 if !line.trim_start().to_lowercase().starts_with("co-authored-by:") {
514 return Some(line.to_owned());
515 }
516 let Some(email) = email_of(line) else { return Some(line.to_owned()) };
517 if email == author || email == g1t_contracts::system::EMAIL {
518 return None;
519 }
520 Some(known.iter().find(|trailer| email_of(trailer).as_deref() == Some(email.as_str())).cloned().unwrap_or_else(|| line.to_owned()))
521 })
522 .collect::<Vec<_>>()
523 .join("\n")
524}
525
526/// Files named in a sentence: a few, then how many more.
527pub(crate) fn name_files(paths: &[String]) -> String {
528 const SHOWN: usize = 5;
529 match paths {
530 [] => "some files".to_owned(),
531 [one] => one.clone(),
532 _ if paths.len() <= SHOWN => {
533 let (last, rest) = paths.split_last().expect("two or more");
534 format!("{} and {last}", rest.join(", "))
535 }
536 _ => format!("{} and {} more files", paths[..SHOWN].join(", "), paths.len() - SHOWN),
537 }
538}
539
540/// A commit's short hash.
541fn short(hash: &str) -> &str {
542 &hash[..hash.len().min(7)]
543}
544
545/// A commit as people know it: its short hash and title.
546fn named_commit(commit: &Commit) -> String {
547 let title = commit.message.lines().next().unwrap_or_default().trim();
548 format!("{} ({title})", &commit.hash[..commit.hash.len().min(7)])
549}
550
551/// What was made to land, ready to push.
552struct Made {
553 commit: String,
554 rebased: Vec<(String, String)>,
555 /// How many commits it brings, when that is known.
556 commits: Option<u32>,
557}
558
559/// Why a landing was refused, or that it was there already.
560enum NotMade {
561 Refused(Outcome<Landed>),
562 /// Lines clash in these files.
563 Clash { message: String, paths: Vec<String> },
564 /// Nothing to make: the branch already has every change.
565 AlreadyThere,
566}
567
568fn refused(code: FailureCode, message: String) -> NotMade {
569 NotMade::Refused(Outcome::fail(code, message))
570}
571
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`).
575async 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)]
596async 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)]
695async 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)]
748pub(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)]
762pub(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
803impl<S: GitStore> Repos<S> {
804 /// `land` by `method`, any but `fast_forward`: makes the commits on
805 /// `branch` of `target` from `new`, the checked head of the pull
806 /// request's branch (`history`, its first-parent chain, in
807 /// `source_git`), and moves `branch` to the last of them if it is still
808 /// where it was. If another change lands first, it is worked out again
809 /// once on the new head. The caller has checked who may do it.
810 #[allow(clippy::too_many_arguments)]
811 pub(crate) async fn land_by(
812 &self,
813 method: MergeMethod,
814 a: &LandArgs,
815 target: &Repo,
816 source_git: &S::Repo,
817 target_git: &S::Repo,
818 branch: &str,
819 history: &[Commit],
820 new: String,
821 from_fork: bool,
822 ) -> Result<Outcome<Landed>> {
823 let message = match method {
824 MergeMethod::Merge | MergeMethod::Squash => match a.message.as_deref().map(str::trim_end).filter(|text| !text.trim().is_empty()) {
825 Some(text) => text.to_owned(),
826 None => {
827 return Ok(Outcome::fail(FailureCode::Invalid, "Say what the commit's message is: its first line is its title."));
828 }
829 },
830 _ => String::new(),
831 };
832 let committer = self.commit_identity(&a.actor).await;
833 let author = match (&a.author, method) {
834 (Some(author), MergeMethod::Squash) => self.commit_identity(author).await,
835 _ => committer.clone(),
836 };
837 let sign_off = a.sign_off.then(|| sign_off_trailer(&committer.name, &committer.email));
838 // Whether the repository holds the pull request's commits: always
839 // for its own branch, and for a fork's once its head was kept there
840 // (test_merge.rs). Otherwise they are read from the fork, and sent
841 // with what is made.
842 let held = !from_fork || !target_git.log(&new, 1).await?.is_empty();
843 let reader = Reader::new(target_git, (!held).then_some(source_git));
844 let mut attempt = 0;
845 loop {
846 attempt += 1;
847 let target_history = target_git.log(branch, MAX_ANCESTRY).await?;
848 let Some(base) = target_history.first().cloned() else {
849 return Ok(Outcome::fail(
850 FailureCode::Conflict,
851 format!("{branch} has no commits yet, so there is nothing to make this pull request's commits on. Land it by fast-forward instead."),
852 ));
853 };
854 // There already: an attempt that published and stopped before
855 // the merge was recorded, or a branch with nothing new.
856 if base.hash == new || (held && descends_from(target_git, &target_history, &new).await?) {
857 let commit = match method {
858 MergeMethod::Merge => target_history
859 .iter()
860 .find(|commit| commit.parents.get(1) == Some(&new))
861 .map_or_else(|| base.hash.clone(), |commit| commit.hash.clone()),
862 _ => base.hash.clone(),
863 };
864 return Ok(Outcome::Ok(Landed { commit, previous: None, method, rebased: Vec::new() }));
865 }
866 let mut objects: Vec<(ObjectKind, Vec<u8>)> = Vec::new();
867 let seconds = now_ms() / 1000;
868 let by = Signature { name: &committer.name, email: &committer.email, seconds };
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 {
874 Ok(made) => made,
875 Err(NotMade::Refused(outcome)) => return Ok(outcome),
876 Err(NotMade::Clash { message, .. }) => return Ok(Outcome::fail(FailureCode::Conflict, message)),
877 Err(NotMade::AlreadyThere) => {
878 return Ok(Outcome::Ok(Landed { commit: base.hash.clone(), previous: None, method, rebased: Vec::new() }));
879 }
880 };
881 let pack = pack_for(held, source_git, &new, &merge_base, &objects).await?;
882 let target_access = target_git.access(Scope::Write).await?;
883 if !self.registry.reserve_public(target, pack.len() as u64, self.free_public_bytes).await? {
884 return Ok(Outcome::fail(FailureCode::Forbidden, "Public repository storage is full. Make room before adding more data."));
885 }
886 let bytes = pack.len() as u64;
887 let pushed = land::push_pack(&target_access, branch, Some(&base.hash), &made.commit, pack).await?;
888 self.registry.finish_public(target, bytes, bytes).await?;
889 self.refs_moved(&target.id).await;
890 match pushed {
891 Ok(()) => {
892 self.publish_push(target, &format!("refs/heads/{branch}"), Some(&base.hash), &made.commit, Some(&a.actor), made.commits)
893 .await?;
894 return Ok(Outcome::Ok(Landed {
895 commit: made.commit,
896 previous: Some(base.hash.clone()),
897 method,
898 rebased: made.rebased,
899 }));
900 }
901 // Most often another pull request landed meanwhile: once
902 // more, on top of it.
903 Err(_) if attempt < 2 => continue,
904 Err(reason) => {
905 return Ok(Outcome::fail(FailureCode::Conflict, format!("{branch} could not be updated: {reason}")));
906 }
907 }
908 }
909 }
910
911 /// `build_queue_step`: one merge queue entry landed by its method on
912 /// `onto`, a commit of the target, as `land_by` would land it on a
913 /// branch whose head that is, and `push_branch` pointed at what was
914 /// made. Nothing else moves. The work service decides who may merge;
915 /// the committer must still be able to.
916 pub(crate) async fn build_queue_step(&self, a: BuildQueueStepArgs) -> Result<Outcome<QueueStep>> {
917 let actor = Some(a.actor.clone());
918 let (Some(target), Some(source)) = (self.registry.by_id(&a.target_id).await?, self.registry.by_id(&a.source_id).await?) else {
919 return Ok(Outcome::fail(FailureCode::NotFound, "No such repository."));
920 };
921 if !crate::registry::can(&target, &actor, g1t_contracts::access::Capability::Merge) || !a.actor.verified {
922 return Ok(Outcome::fail(FailureCode::Forbidden, "Whoever queued it can no longer merge here."));
923 }
924 // A fork's change lands only on a repository in its network.
925 if source.id != target.id && self.pull_target(&source, Some(&target.id)).await?.is_none_or(|found| found.id != target.id) {
926 return Ok(Outcome::fail(FailureCode::NotFound, "No such repository."));
927 }
928 let from_fork = source.id != target.id;
929 self.live(&source).await?;
930 let source_git = self.store.open(&store_key(&source)).await?;
931 let target_git = self.store.open(&store_key(&target)).await?;
932 let refused = |message: String| Ok(Outcome::Ok(QueueStep::Refused { message }));
933 // What was queued is the commit: its branch may have moved since.
934 let (history, onto_history) = try_join(source_git.log(&a.commit, MAX_ANCESTRY), target_git.log(&a.onto, MAX_ANCESTRY)).await?;
935 let (Some(head), Some(base)) = (history.first(), onto_history.first()) else {
936 return refused(format!("{} or the state it is built on is no longer there.", short(&a.commit)));
937 };
938 let new = head.hash.clone();
939 let base = base.hash.clone();
940 let held = !from_fork || !target_git.log(&new, 1).await?.is_empty();
941 let reader = Reader::new(&target_git, (!held).then_some(&source_git));
942 let place = target.default_branch.clone();
943 let mut objects: Vec<(ObjectKind, Vec<u8>)> = Vec::new();
944 let committer = self.commit_identity(&a.actor).await;
945 let author = match (&a.author, a.method) {
946 (Some(author), MergeMethod::Squash) => self.commit_identity(author).await,
947 _ => committer.clone(),
948 };
949 let sign_off = a.sign_off.then(|| sign_off_trailer(&committer.name, &committer.email));
950 let seconds = now_ms() / 1000;
951 let by = Signature { name: &committer.name, email: &committer.email, seconds };
952 let author = Signature { name: &author.name, email: &author.email, seconds };
953 let step = queue_step_on(
954 &reader,
955 held,
956 a.method,
957 (&onto_history, &history),
958 a.message.as_deref(),
959 (&author, &by),
960 sign_off.as_deref(),
961 &place,
962 &mut objects,
963 )
964 .await?;
965 let (commit, rebased, merge_base) = match step {
966 StepMade::Made { commit, rebased, merge_base } => (commit, rebased, merge_base),
967 StepMade::Conflict { message, paths } => return Ok(Outcome::Ok(QueueStep::Conflict { message, paths })),
968 StepMade::Refused(message) => return refused(message),
969 };
970 // Nothing new when it was there already.
971 let pack = if commit == base {
972 write_pack(&objects)
973 } else {
974 pack_for(held, &source_git, &new, &merge_base, &objects).await?
975 };
976 let old = target_git.log(&a.push_branch, 1).await?.into_iter().next().map(|commit| commit.hash);
977 let target_access = target_git.access(Scope::Write).await?;
978 if !self.registry.reserve_public(&target, pack.len() as u64, self.free_public_bytes).await? {
979 return Ok(Outcome::fail(FailureCode::Forbidden, "Public repository storage is full. Make room before adding more data."));
980 }
981 let bytes = pack.len() as u64;
982 let pushed = land::push_pack(&target_access, &a.push_branch, old.as_deref(), &commit, pack).await?;
983 self.registry.finish_public(&target, bytes, bytes).await?;
984 self.refs_moved(&target.id).await;
985 Ok(match pushed {
986 Ok(()) => Outcome::Ok(QueueStep::Built { commit, rebased }),
987 Err(reason) => Outcome::fail(FailureCode::Conflict, format!("{} could not be updated: {reason}", a.push_branch)),
988 })
989 }
990
991 /// `update_pull_branch` by `rebase`: makes the commits of `branch` of
992 /// `source` again on the head of `base_branch` of `target` and moves
993 /// the branch there (a force push), if it is still at `expected_head`
994 /// (else its head when read). The caller has checked who may do it.
995 pub(crate) async fn rebase_up(
996 &self,
997 source: Repo,
998 target: Repo,
999 branch: String,
1000 base_branch: String,
1001 actor: &User,
1002 expected_head: Option<&str>,
1003 ) -> Result<Outcome<PullBranchUpdate>> {
1004 let from_fork = source.id != target.id;
1005 // A working copy removed after its pull request closed is made again.
1006 self.live(&source).await?;
1007 let source_git = self.store.open(&store_key(&source)).await?;
1008 let target_git = self.store.open(&store_key(&target)).await?;
1009 let (history, target_history) =
1010 try_join(source_git.log(&branch, MAX_ANCESTRY), target_git.log(&base_branch, MAX_ANCESTRY)).await?;
1011 let (Some(head), Some(base)) = (history.first(), target_history.first()) else {
1012 return Ok(Outcome::fail(FailureCode::Conflict, format!("{branch} has no commits to bring up to date.")));
1013 };
1014 if let Some(refused) = moved_since(&branch, &head.hash, expected_head) {
1015 return Ok(refused);
1016 }
1017 let needs_agent = |reason, detail: String, paths| Outcome::Ok(PullBranchUpdate::NeedsAgent { reason, detail, paths });
1018 let shared: HashSet<String> = target_history.iter().map(|commit| commit.hash.clone()).collect();
1019 let reader = Reader::new(&source_git, from_fork.then_some(&target_git));
1020 let Some(merge_base) = nearest_ancestor_in(&reader, &history, &shared).await? else {
1021 return Ok(needs_agent(
1022 NeedsAgentReason::Unsupported,
1023 format!("g1t could not find where this pull request left {base_branch}."),
1024 Vec::new(),
1025 ));
1026 };
1027 // On the base's head already, with no merge commits to straighten.
1028 let commits = match branch_commits(&reader, &history, &shared, MAX_REBASE_COMMITS).await? {
1029 Some(commits) => commits,
1030 None => {
1031 return Ok(needs_agent(
1032 NeedsAgentReason::Unsupported,
1033 format!("{branch} has more than {MAX_REBASE_COMMITS} commits, too many for g1t to rebase itself."),
1034 Vec::new(),
1035 ));
1036 }
1037 };
1038 if merge_base == base.hash && commits.iter().all(|commit| commit.parents.len() <= 1) {
1039 return Ok(Outcome::Ok(PullBranchUpdate::UpToDate { commit: head.hash.clone() }));
1040 }
1041 let committer = self.commit_identity(actor).await;
1042 let by = Signature { name: &committer.name, email: &committer.email, seconds: now_ms() / 1000 };
1043 let mut objects: Vec<(ObjectKind, Vec<u8>)> = Vec::new();
1044 let replayed = match replay(&reader, &commits, (&base.hash, &base.tree_hash), &by, None, &mut objects).await? {
1045 Replay::Done(replayed) => replayed,
1046 Replay::Conflict { commit, paths } => {
1047 return Ok(needs_agent(
1048 NeedsAgentReason::Conflicting,
1049 format!("{} and {base_branch} changed the same lines of some files.", named_commit(&commit)),
1050 paths,
1051 ));
1052 }
1053 Replay::Unsupported { commit, why } => {
1054 return Ok(needs_agent(
1055 NeedsAgentReason::Unsupported,
1056 format!("g1t could not rebase {} itself: {why}.", named_commit(&commit)),
1057 Vec::new(),
1058 ));
1059 }
1060 };
1061 let update = BranchUpdate {
1062 branch: &branch,
1063 head: &head.hash,
1064 commit: &replayed.head,
1065 fast_forward: false,
1066 commits: replayed.made.len() as u32,
1067 };
1068 if let Some(refused) = self.update_refused(&source, actor, &update, replayed.facts.clone(), true).await? {
1069 return Ok(refused);
1070 }
1071 // A fork lacks what the base branch gained since it was made.
1072 let pack = if from_fork {
1073 let target_access = target_git.access(Scope::Read).await?;
1074 let fetched = land::fetch_pack(&target_access, &base.hash, Some(&merge_base)).await?;
1075 extend_pack(&fetched, &objects).map_err(worker::Error::RustError)?
1076 } else {
1077 write_pack(&objects)
1078 };
1079 self.push_update(&source, &source_git, actor, &update, pack).await
1080 }
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085 use super::*;
1086 use g1t_contracts::repos::Signature as Person;
1087
1088 fn blob(hash: &str) -> Option<FileEntry> {
1089 Some((EntryKind::Blob, hash.to_owned()))
1090 }
1091
1092 #[test]
1093 fn a_file_the_other_side_left_alone_takes_the_change() {
1094 assert_eq!(resolve(blob("1").as_ref(), blob("2").as_ref(), blob("1").as_ref()), Resolution::Take(blob("2")));
1095 // Added, where nothing was.
1096 assert_eq!(resolve(None, blob("2").as_ref(), None), Resolution::Take(blob("2")));
1097 // Deleted.
1098 assert_eq!(resolve(blob("1").as_ref(), None, blob("1").as_ref()), Resolution::Take(None));
1099 // Made the same on both sides.
1100 assert_eq!(resolve(blob("1").as_ref(), blob("2").as_ref(), blob("2").as_ref()), Resolution::Keep);
1101 }
1102
1103 #[test]
1104 fn a_file_both_changed_is_merged_or_conflicts() {
1105 assert_eq!(
1106 resolve(blob("1").as_ref(), blob("2").as_ref(), blob("3").as_ref()),
1107 Resolution::Content { kind: EntryKind::Blob, base: Some("1".into()), ours: "2".into(), theirs: "3".into() }
1108 );
1109 // Deleted on one side, changed on the other.
1110 assert_eq!(resolve(blob("1").as_ref(), None, blob("3").as_ref()), Resolution::Conflict);
1111 assert_eq!(resolve(blob("1").as_ref(), blob("2").as_ref(), None), Resolution::Conflict);
1112 // A file where the other side has a directory.
1113 assert_eq!(resolve(None, blob("2").as_ref(), Some(&(EntryKind::Tree, "t".into()))), Resolution::Conflict);
1114 // Submodules moved two ways.
1115 let link = |hash: &str| Some((EntryKind::Gitlink, hash.to_owned()));
1116 assert_eq!(resolve(link("1").as_ref(), link("2").as_ref(), link("3").as_ref()), Resolution::Conflict);
1117 // Made executable on one side, changed on the other.
1118 assert_eq!(
1119 resolve(blob("1").as_ref(), Some(&(EntryKind::Exec, "1".into())), blob("2").as_ref()),
1120 Resolution::Content { kind: EntryKind::Exec, base: Some("1".into()), ours: "1".into(), theirs: "2".into() }
1121 );
1122 }
1123
1124 #[test]
1125 fn trailers_are_added_once_after_the_message() {
1126 let sign = sign_off_trailer("Ana", "ana@example.com");
1127 assert_eq!(with_trailers("Fix it\n", std::slice::from_ref(&sign)), "Fix it\n\nSigned-off-by: Ana <ana@example.com>");
1128 // Already signed off (case aside): not again.
1129 let signed = "Fix it\n\nsigned-off-by: Ana <ANA@example.com>\n";
1130 assert_eq!(with_trailers(signed, std::slice::from_ref(&sign)), signed.trim_end());
1131 // Joins trailers already ending the message.
1132 // A template's trailers, from addresses alone: the author's own and
1133 // g1t's go, and a known one takes its real name.
1134 let known = ["Co-authored-by: Bo Lin <bo@example.com>".to_owned()];
1135 let g1t = format!("Co-authored-by: g1t <{}>", g1t_contracts::system::EMAIL);
1136 let message = format!(
1137 "Fix\n\nCo-authored-by: ana <ANA@example.com>\nCo-authored-by: bo <bo@example.com>\n{g1t}\nCo-authored-by: cy <cy@example.com>"
1138 );
1139 assert_eq!(
1140 tidy_co_authors(&message, "ana@example.com", &known),
1141 "Fix\n\nCo-authored-by: Bo Lin <bo@example.com>\nCo-authored-by: cy <cy@example.com>"
1142 );
1143 let co = "Co-authored-by: Bo <bo@example.com>".to_owned();
1144 assert_eq!(
1145 with_trailers("Fix it\n\nBody.\n\nCo-authored-by: Cy <cy@example.com>", &[co.clone(), sign.clone()]),
1146 "Fix it\n\nBody.\n\nCo-authored-by: Cy <cy@example.com>\nCo-authored-by: Bo <bo@example.com>\nSigned-off-by: Ana <ana@example.com>"
1147 );
1148 // A body that only looks like it has a colon is not a trailer.
1149 assert_eq!(with_trailers("Fix it\n\nNote: see below\nmore text", std::slice::from_ref(&co)), "Fix it\n\nNote: see below\nmore text\n\nCo-authored-by: Bo <bo@example.com>");
1150 // Twice in one list: once.
1151 assert_eq!(with_trailers("Fix", &[co.clone(), co.clone()]), "Fix\n\nCo-authored-by: Bo <bo@example.com>");
1152 // Adding it to a message already holding it changes nothing.
1153 let once = with_trailers("Fix", std::slice::from_ref(&sign));
1154 assert_eq!(with_trailers(&once, std::slice::from_ref(&sign)), once);
1155 }
1156
1157 fn commit(hash: &str, parents: &[&str], name: &str, email: &str) -> Commit {
1158 Commit {
1159 hash: hash.to_owned(),
1160 tree_hash: String::new(),
1161 message: format!("{hash}\n"),
1162 author: Person { name: name.to_owned(), email: email.to_owned() },
1163 parents: parents.iter().map(|&parent| parent.to_owned()).collect(),
1164 authored_at: String::new(),
1165 }
1166 }
1167
1168 #[test]
1169 fn everyone_else_whose_commits_are_squashed_is_credited_once() {
1170 let commits = [
1171 commit("a", &["m"], "Ana", "ana@example.com"),
1172 commit("b", &["a"], "Bo", "bo@example.com"),
1173 commit("c", &["b"], "Bo again", "BO@example.com"),
1174 commit("d", &["c"], "g1t", g1t_contracts::system::EMAIL),
1175 // A merge commit brings nothing of its own.
1176 commit("e", &["d", "x"], "Cy", "cy@example.com"),
1177 commit("f", &["e"], "Di", "di@example.com"),
1178 ];
1179 assert_eq!(
1180 co_authors(&commits, "ANA@example.com"),
1181 ["Co-authored-by: Bo <bo@example.com>", "Co-authored-by: Di <di@example.com>"]
1182 );
1183 }
1184
1185 #[test]
1186 fn files_are_named_a_few_at_a_time() {
1187 let paths = |n: usize| (1..=n).map(|i| format!("f{i}")).collect::<Vec<_>>();
1188 assert_eq!(name_files(&paths(1)), "f1");
1189 assert_eq!(name_files(&paths(3)), "f1, f2 and f3");
1190 assert_eq!(name_files(&paths(8)), "f1, f2, f3, f4, f5 and 3 more files");
1191 }
1192
1193 /// A store of whole objects made in the test: commits by name, trees
1194 /// and blobs by their real ids.
1195 #[derive(Default)]
1196 struct Store {
1197 commits: HashMap<String, Commit>,
1198 trees: HashMap<String, Vec<TreeEntry>>,
1199 blobs: HashMap<String, Vec<u8>>,
1200 }
1201
1202 impl Store {
1203 fn tree(&mut self, files: &[(&str, &str)]) -> String {
1204 let mut here: Vec<TreeEntry> = Vec::new();
1205 let mut dirs: std::collections::BTreeMap<&str, Vec<(&str, &str)>> = Default::default();
1206 for (path, text) in files {
1207 match path.split_once('/') {
1208 Some((dir, rest)) => dirs.entry(dir).or_default().push((rest, text)),
1209 None => {
1210 let hash = object_id(ObjectKind::Blob, text.as_bytes());
1211 self.blobs.insert(hash.clone(), text.as_bytes().to_vec());
1212 here.push(TreeEntry { name: (*path).to_owned(), hash, kind: EntryKind::Blob });
1213 }
1214 }
1215 }
1216 for (dir, files) in dirs {
1217 let hash = self.tree(&files);
1218 here.push(TreeEntry { name: dir.to_owned(), hash, kind: EntryKind::Tree });
1219 }
1220 let id = object_id(ObjectKind::Tree, &crate::catch_up::encode_entries(&here));
1221 self.trees.insert(id.clone(), here);
1222 id
1223 }
1224
1225 fn commit(&mut self, hash: &str, parents: &[&str], author: &str, files: &[(&str, &str)]) -> Commit {
1226 let tree_hash = self.tree(files);
1227 let made = Commit {
1228 hash: hash.to_owned(),
1229 tree_hash,
1230 message: format!("Change {hash}\n"),
1231 author: Person { name: author.to_owned(), email: format!("{author}@example.com") },
1232 parents: parents.iter().map(|&parent| parent.to_owned()).collect(),
1233 authored_at: "2026-10-01T00:00:00.000Z".to_owned(),
1234 };
1235 self.commits.insert(hash.to_owned(), made.clone());
1236 made
1237 }
1238
1239 /// The files of a tree, read through `git`, by path.
1240 fn files(git: &Reader<'_, Store>, tree: &str) -> Vec<(String, String)> {
1241 let mut out = Vec::new();
1242 let mut todo = vec![(String::new(), tree.to_owned())];
1243 while let Some((prefix, tree)) = todo.pop() {
1244 for entry in block_on(git.read_tree(&tree)).unwrap().expect("tree") {
1245 let path = format!("{prefix}{}", entry.name);
1246 if entry.kind == EntryKind::Tree {
1247 todo.push((format!("{path}/"), entry.hash));
1248 } else {
1249 let bytes = block_on(git.read_blob(&entry.hash)).unwrap().expect("blob");
1250 out.push((path, String::from_utf8(bytes).unwrap()));
1251 }
1252 }
1253 }
1254 out.sort();
1255 out
1256 }
1257 }
1258
1259 impl GitRepo for Store {
1260 async fn access(&self, _scope: Scope) -> Result<GitAccess> {
1261 unimplemented!()
1262 }
1263 async fn branches(&self) -> Result<Vec<Branch>> {
1264 Ok(Vec::new())
1265 }
1266 async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>> {
1267 let mut out = Vec::new();
1268 let mut at = self.commits.get(git_ref);
1269 while let Some(commit) = at.filter(|_| (out.len() as u32) < limit) {
1270 out.push(commit.clone());
1271 at = commit.parents.first().and_then(|parent| self.commits.get(parent));
1272 }
1273 Ok(out)
1274 }
1275 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>> {
1276 Ok(self.commits.get(commit_hash).map(|commit| commit.parents.clone()))
1277 }
1278 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>> {
1279 Ok(self.trees.get(tree_hash).cloned())
1280 }
1281 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>> {
1282 Ok(self.blobs.get(blob_hash).cloned())
1283 }
1284 async fn read_file(&self, _git_ref: &str, _path: &str) -> Result<Option<Vec<u8>>> {
1285 Ok(None)
1286 }
1287 async fn fork(&self, _target_key: &str, _default_branch_only: bool) -> Result<()> {
1288 Ok(())
1289 }
1290 }
1291
1292 fn block_on<F: std::future::Future>(future: F) -> F::Output {
1293 let mut future = std::pin::pin!(future);
1294 let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
1295 loop {
1296 if let std::task::Poll::Ready(output) = future.as_mut().poll(&mut cx) {
1297 return output;
1298 }
1299 }
1300 }
1301
1302 fn files(list: &[(&str, &str)]) -> Vec<(String, String)> {
1303 list.iter().map(|(path, text)| ((*path).to_owned(), (*text).to_owned())).collect()
1304 }
1305
1306 /// `main` at m then b; the branch at m then h1, h2.
1307 fn diverged() -> (Store, [Commit; 4]) {
1308 let mut store = Store::default();
1309 let m = store.commit("m", &[], "ana", &[("README.md", "hi\n"), ("src/lib.rs", "a\nb\nc\nd\ne\n")]);
1310 let b = store.commit("b", &["m"], "ana", &[("README.md", "hello\n"), ("src/lib.rs", "a\nb\nc\nd\nE\n")]);
1311 let h1 = store.commit("h1", &["m"], "bo", &[("README.md", "hi\n"), ("src/lib.rs", "A\nb\nc\nd\ne\n")]);
1312 let h2 = store.commit(
1313 "h2",
1314 &["h1"],
1315 "cy",
1316 &[("README.md", "hi\n"), ("src/lib.rs", "A\nb\nc\nd\ne\n"), ("src/new.rs", "new\n")],
1317 );
1318 (store, [m, b, h1, h2])
1319 }
1320
1321 #[test]
1322 fn a_change_applies_line_by_line_where_both_sides_touched_a_file() {
1323 let (store, [m, b, _, h2]) = diverged();
1324 let git = Reader::new(&store, None);
1325 let mut objects = Vec::new();
1326 let Applied::Tree(tree) = block_on(apply(&git, Some(&m.tree_hash), &h2.tree_hash, &b.tree_hash, &mut objects)).unwrap() else {
1327 panic!("not applied")
1328 };
1329 assert_eq!(
1330 Store::files(&git, &tree),
1331 files(&[("README.md", "hello\n"), ("src/lib.rs", "A\nb\nc\nd\nE\n"), ("src/new.rs", "new\n")])
1332 );
1333 // The merged blob, the root and src: only what changed.
1334 assert_eq!(objects.iter().filter(|(kind, _)| *kind == ObjectKind::Blob).count(), 1);
1335 assert_eq!(objects.iter().filter(|(kind, _)| *kind == ObjectKind::Tree).count(), 2);
1336 }
1337
1338 #[test]
1339 fn clashing_lines_name_the_file_and_make_nothing() {
1340 let mut store = Store::default();
1341 let m = store.commit("m", &[], "ana", &[("a", "1\n"), ("b", "x\n")]);
1342 let h = store.commit("h", &["m"], "bo", &[("a", "2\n"), ("b", "y\n")]);
1343 let b = store.commit("b", &["m"], "ana", &[("a", "3\n"), ("b", "x\n")]);
1344 let git = Reader::new(&store, None);
1345 let mut objects = Vec::new();
1346 let applied = block_on(apply(&git, Some(&m.tree_hash), &h.tree_hash, &b.tree_hash, &mut objects)).unwrap();
1347 assert_eq!(applied, Applied::Conflict(vec!["a".into()]));
1348 assert!(objects.is_empty());
1349 }
1350
1351 #[test]
1352 fn a_branch_s_commits_come_oldest_first_and_stop_at_the_base() {
1353 let (mut store, [m, b, h1, h2]) = diverged();
1354 // The branch merged main in, then went on.
1355 let j = store.commit("j", &["h2", "b"], "bo", &[("README.md", "hello\n")]);
1356 let k = store.commit("k", &["j"], "bo", &[("README.md", "hello\n")]);
1357 let history = block_on(store.log("k", 100)).unwrap();
1358 let shared: HashSet<String> = [m.hash.clone(), b.hash.clone()].into_iter().collect();
1359 let commits = block_on(branch_commits(&store, &history, &shared, 10)).unwrap().unwrap();
1360 let order: Vec<&str> = commits.iter().map(|commit| commit.hash.as_str()).collect();
1361 assert_eq!(order, [h1.hash.as_str(), h2.hash.as_str(), j.hash.as_str(), k.hash.as_str()]);
1362 // Too many: none.
1363 assert!(block_on(branch_commits(&store, &history, &shared, 3)).unwrap().is_none());
1364 }
1365
1366 #[test]
1367 fn a_rebase_makes_each_commit_again_on_the_base_keeping_its_author() {
1368 let (store, [_, b, h1, h2]) = diverged();
1369 let git = Reader::new(&store, None);
1370 let committer = Signature { name: "Di", email: "di@example.com", seconds: 1_800_000_000 };
1371 let sign = sign_off_trailer("Di", "di@example.com");
1372 let mut objects = Vec::new();
1373 let commits = [h1.clone(), h2.clone()];
1374 let Replay::Done(replayed) = block_on(replay(&git, &commits, (&b.hash, &b.tree_hash), &committer, Some(&sign), &mut objects)).unwrap() else {
1375 panic!("not replayed")
1376 };
1377 assert_eq!(replayed.made.iter().map(|(_, original)| original.as_str()).collect::<Vec<_>>(), ["h1", "h2"]);
1378 let made: Vec<String> = objects
1379 .iter()
1380 .filter(|(kind, _)| *kind == ObjectKind::Commit)
1381 .map(|(_, bytes)| String::from_utf8(bytes.clone()).unwrap())
1382 .collect();
1383 assert_eq!(made.len(), 2);
1384 // The first on the base, the second on the first.
1385 assert!(made[0].contains(&format!("parent {}\n", b.hash)), "{}", made[0]);
1386 assert!(made[1].contains(&format!("parent {}\n", replayed.made[0].0)), "{}", made[1]);
1387 assert_eq!(replayed.head, replayed.made[1].0);
1388 // Their authors and dates kept, committed by whoever rebased.
1389 assert!(made[0].contains("author bo <bo@example.com> 1790812800 +0000\ncommitter Di <di@example.com> 1800000000 +0000\n"));
1390 assert!(made[1].ends_with("\n\nChange h2\n\nSigned-off-by: Di <di@example.com>\n"), "{}", made[1]);
1391 // The last holds both sides.
1392 let tree = made[1].lines().next().unwrap().strip_prefix("tree ").unwrap().to_owned();
1393 assert_eq!(
1394 Store::files(&git, &tree),
1395 files(&[("README.md", "hello\n"), ("src/lib.rs", "A\nb\nc\nd\nE\n"), ("src/new.rs", "new\n")])
1396 );
1397 }
1398
1399 #[test]
1400 fn a_rebase_stops_at_the_commit_that_clashes() {
1401 let mut store = Store::default();
1402 store.commit("m", &[], "ana", &[("a", "1\n"), ("b", "x\n")]);
1403 let h1 = store.commit("h1", &["m"], "bo", &[("a", "1\n"), ("b", "y\n")]);
1404 let h2 = store.commit("h2", &["h1"], "bo", &[("a", "2\n"), ("b", "y\n")]);
1405 let b = store.commit("b", &["m"], "ana", &[("a", "3\n"), ("b", "x\n")]);
1406 let git = Reader::new(&store, None);
1407 let committer = Signature { name: "Di", email: "di@example.com", seconds: 1 };
1408 let mut objects = Vec::new();
1409 let replayed = block_on(replay(&git, &[h1, h2], (&b.hash, &b.tree_hash), &committer, None, &mut objects)).unwrap();
1410 let Replay::Conflict { commit, paths } = replayed else { panic!("no conflict") };
1411 assert_eq!(commit.hash, "h2");
1412 assert_eq!(paths, ["a"]);
1413 assert_eq!(named_commit(&commit), "h2 (Change h2)");
1414 }
1415
1416 #[test]
1417 fn a_commit_whose_changes_are_there_already_is_left_out() {
1418 let mut store = Store::default();
1419 store.commit("m", &[], "ana", &[("a", "1\n")]);
1420 let h1 = store.commit("h1", &["m"], "bo", &[("a", "2\n")]);
1421 // The base made the same change.
1422 let b = store.commit("b", &["m"], "ana", &[("a", "2\n")]);
1423 let git = Reader::new(&store, None);
1424 let committer = Signature { name: "Di", email: "di@example.com", seconds: 1 };
1425 let mut objects = Vec::new();
1426 let Replay::Done(replayed) = block_on(replay(&git, &[h1], (&b.hash, &b.tree_hash), &committer, None, &mut objects)).unwrap() else {
1427 panic!("not replayed")
1428 };
1429 assert!(replayed.made.is_empty());
1430 assert_eq!(replayed.head, b.hash);
1431 }
1432
1433 #[test]
1434 fn a_merge_commit_has_both_parents_and_a_squash_one() {
1435 let (store, [m, b, _, h2]) = diverged();
1436 let git = Reader::new(&store, None);
1437 let committer = Signature { name: "Di", email: "di@example.com", seconds: 5 };
1438 let author = Signature { name: "Bo", email: "bo@example.com", seconds: 5 };
1439 let mut objects = Vec::new();
1440 let Applied::Tree(tree) = block_on(apply(&git, Some(&m.tree_hash), &h2.tree_hash, &b.tree_hash, &mut objects)).unwrap() else {
1441 panic!("not applied")
1442 };
1443 let merge = String::from_utf8(commit_object_by(&tree, &[&b.hash, &h2.hash], &committer, &committer, "Merge it")).unwrap();
1444 assert!(merge.contains(&format!("parent {}\nparent {}\n", b.hash, h2.hash)));
1445 let squash = String::from_utf8(commit_object_by(&tree, &[&b.hash], &author, &committer, "Squash it")).unwrap();
1446 assert_eq!(squash.matches("parent ").count(), 1);
1447 assert!(squash.contains("author Bo <bo@example.com> 5 +0000\ncommitter Di <di@example.com> 5 +0000\n"));
1448 }
1449
1450 #[test]
1451 fn reads_fall_back_to_the_second_repository_and_the_objects_made() {
1452 let mut fork = Store::default();
1453 let h = fork.commit("h", &[], "bo", &[("a", "fork\n")]);
1454 let repo = Store::default();
1455 let git = Reader::new(&repo, Some(&fork));
1456 assert!(block_on(git.read_tree(&h.tree_hash)).unwrap().is_some());
1457 assert_eq!(block_on(git.log("h", 1)).unwrap().len(), 1);
1458 let made = vec![(ObjectKind::Blob, b"made\n".to_vec())];
1459 git.keep(&made);
1460 assert_eq!(block_on(git.read_blob(&object_id(ObjectKind::Blob, b"made\n"))).unwrap().as_deref(), Some(&b"made\n"[..]));
1461 }
1462
1463 /// A commit made in a test, as the store would list it.
1464 fn made_commit(objects: &[(ObjectKind, Vec<u8>)], id: &str) -> Commit {
1465 let bytes = objects
1466 .iter()
1467 .find(|(kind, bytes)| *kind == ObjectKind::Commit && object_id(ObjectKind::Commit, bytes) == id)
1468 .map(|(_, bytes)| String::from_utf8(bytes.clone()).unwrap())
1469 .expect("made");
1470 let field = |name: &str| bytes.lines().filter_map(|line| line.strip_prefix(name)).map(str::to_owned).collect::<Vec<_>>();
1471 Commit {
1472 hash: id.to_owned(),
1473 tree_hash: field("tree ").remove(0),
1474 message: bytes.split_once("\n\n").map(|(_, message)| message.to_owned()).unwrap_or_default(),
1475 author: Person { name: "Di".to_owned(), email: "di@example.com".to_owned() },
1476 parents: field("parent "),
1477 authored_at: "2026-10-01T00:00:00.000Z".to_owned(),
1478 }
1479 }
1480
1481 #[test]
1482 fn each_queue_entry_lands_by_its_own_method_on_the_state_ahead_of_it() {
1483 let (mut store, [m, b, h1, h2]) = diverged();
1484 // A second change, from the same start, to another file.
1485 let g = store.commit("g", &["m"], "eve", &[("README.md", "hi\n"), ("src/lib.rs", "a\nb\nc\nd\ne\n"), ("docs.md", "docs\n")]);
1486 let k = store.commit("k", &["m"], "eve", &[("README.md", "hi\n"), ("src/lib.rs", "a\nb\nc\nd\ne\n"), ("k.md", "k\n")]);
1487 let git = Reader::new(&store, None);
1488 let by = Signature { name: "Di", email: "di@example.com", seconds: 7 };
1489 let author = Signature { name: "bo", email: "bo@example.com", seconds: 7 };
1490 let mut objects = Vec::new();
1491 let step = |method, onto: &[Commit], history: &[Commit], objects: &mut Vec<_>| {
1492 block_on(queue_step_on(&git, true, method, (onto, history), Some("Land it"), (&author, &by), None, "main", objects)).unwrap()
1493 };
1494 let onto = vec![b.clone(), m.clone()];
1495
1496 // The first squashes onto the default branch: one commit, one parent.
1497 let StepMade::Made { commit: first, .. } = step(MergeMethod::Squash, &onto, &[h2.clone(), h1.clone(), m.clone()], &mut objects) else {
1498 panic!("not squashed")
1499 };
1500 let first = made_commit(&objects, &first);
1501 assert_eq!(first.parents, ["b"]);
1502
1503 // The second is a merge commit on the first's state, so landing it
1504 // is a fast-forward from there.
1505 let onto = vec![first.clone(), b.clone(), m.clone()];
1506 let StepMade::Made { commit: second, .. } = step(MergeMethod::Merge, &onto, &[g.clone(), m.clone()], &mut objects) else {
1507 panic!("not merged")
1508 };
1509 let second = made_commit(&objects, &second);
1510 assert_eq!(second.parents, [first.hash.clone(), "g".to_owned()]);
1511 assert_eq!(
1512 Store::files(&git, &second.tree_hash),
1513 files(&[("README.md", "hello\n"), ("docs.md", "docs\n"), ("src/lib.rs", "A\nb\nc\nd\nE\n"), ("src/new.rs", "new\n")])
1514 );
1515
1516 // Queued again, its changes are there already: the state is kept.
1517 let onto = vec![second.clone(), first.clone(), b.clone(), m.clone()];
1518 let StepMade::Made { commit, .. } = step(MergeMethod::Rebase, &onto, &[h2.clone(), h1.clone(), m.clone()], &mut objects) else {
1519 panic!("not kept")
1520 };
1521 assert_eq!(commit, second.hash);
1522
1523 // A fast-forward needs its head to hold the state ahead of it.
1524 let StepMade::Refused(why) = step(MergeMethod::FastForward, &onto, &[k, m.clone()], &mut objects) else {
1525 panic!("fast-forwarded")
1526 };
1527 assert!(why.contains("fast-forward"), "{why}");
1528 }
1529
1530 #[test]
1531 fn a_fast_forward_entry_is_its_own_head_and_a_clash_names_the_files() {
1532 let (mut store, [m, b, h1, h2]) = diverged();
1533 let ahead = store.commit("f", &["b"], "eve", &[("README.md", "hello\n"), ("src/lib.rs", "a\nb\nc\nd\nE\n"), ("f.md", "f\n")]);
1534 let clash = store.commit("x", &["m"], "eve", &[("README.md", "hey\n"), ("src/lib.rs", "a\nb\nc\nd\ne\n")]);
1535 let git = Reader::new(&store, None);
1536 let by = Signature { name: "Di", email: "di@example.com", seconds: 7 };
1537 let mut objects = Vec::new();
1538 let onto = vec![b.clone(), m.clone()];
1539 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();
1540 assert!(matches!(made, StepMade::Made { ref commit, .. } if commit == "f"), "{made:?}");
1541 assert!(objects.is_empty());
1542 let clashed = block_on(queue_step_on(&git, true, MergeMethod::Rebase, (&onto, &[clash, m.clone()]), None, (&by, &by), None, "main", &mut objects)).unwrap();
1543 assert!(matches!(clashed, StepMade::Conflict { ref paths, .. } if paths == &["README.md".to_owned()]), "{clashed:?}");
1544 let _ = (h1, h2);
1545 }
1546}