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.

1,273 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, Commit, EntryKind, GitAccess, LandArgs, Landed, NeedsAgentReason, PullBranchUpdate, Repo, TreeEntry,
30};
31use g1t_contracts::rules::MergeMethod;
32use g1t_contracts::time::parse_rfc3339;
33use g1t_contracts::{FailureCode, Outcome, User};
34use g1t_kit::now_ms;
35use g1t_scan::pack::{ObjectKind, extend_pack, object_id, parse_tree, write_pack};
36use worker::Result;
37
38use crate::catch_up::{BranchUpdate, Change, Signature, ancestors, commit_object_by, merge_tree, moved_since, read_dirs};
39use crate::diff::{FileEntry, changed_entries_up_to};
40use crate::registry::store_key;
41use crate::store::{GitRepo, GitStore, Scope};
42use crate::test_merge::{as_text, merge_text};
43use crate::{MAX_ANCESTRY, Repos, descends_from, land, nearest_ancestor_in};
44
45/// The most files one side's change may touch for g1t to apply it.
46pub(crate) const MAX_LAND_FILES: usize = 1000;
47/// The most files both sides changed that are merged line by line at once.
48pub(crate) const MAX_LINE_MERGES: usize = 100;
49/// The most commits a rebase makes again.
50pub(crate) const MAX_REBASE_COMMITS: usize = 250;
51
52/// Reads objects from a repository, then from a second one (a fork, or the
53/// repository a fork came from) for what the first lacks, and first of all
54/// from the objects made here and not yet pushed.
55pub(crate) struct Reader<'a, R> {
56 first: &'a R,
57 second: Option<&'a R>,
58 trees: RefCell<HashMap<String, Vec<TreeEntry>>>,
59 blobs: RefCell<HashMap<String, Vec<u8>>>,
60}
61
62/// What git's tree mode says an entry is.
63fn kind_of(mode: &str) -> EntryKind {
64 match mode {
65 "40000" | "040000" => EntryKind::Tree,
66 "100755" => EntryKind::Exec,
67 "120000" => EntryKind::Symlink,
68 "160000" => EntryKind::Gitlink,
69 _ => EntryKind::Blob,
70 }
71}
72
73impl<'a, R: GitRepo> Reader<'a, R> {
74 pub(crate) fn new(first: &'a R, second: Option<&'a R>) -> Self {
75 Reader { first, second, trees: RefCell::default(), blobs: RefCell::default() }
76 }
77
78 /// Keeps `objects`, made here, to be read before they are pushed.
79 fn keep(&self, objects: &[(ObjectKind, Vec<u8>)]) {
80 for (kind, bytes) in objects {
81 match kind {
82 ObjectKind::Tree => {
83 let entries = parse_tree(bytes)
84 .into_iter()
85 .map(|item| TreeEntry { kind: kind_of(&item.mode), name: item.name, hash: item.id })
86 .collect();
87 self.trees.borrow_mut().insert(object_id(ObjectKind::Tree, bytes), entries);
88 }
89 ObjectKind::Blob => {
90 self.blobs.borrow_mut().insert(object_id(ObjectKind::Blob, bytes), bytes.clone());
91 }
92 _ => {}
93 }
94 }
95 }
96}
97
98impl<R: GitRepo> GitRepo for Reader<'_, R> {
99 async fn access(&self, scope: Scope) -> Result<GitAccess> {
100 self.first.access(scope).await
101 }
102 async fn branches(&self) -> Result<Vec<Branch>> {
103 self.first.branches().await
104 }
105 async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>> {
106 let found = self.first.log(git_ref, limit).await?;
107 match self.second {
108 Some(second) if found.is_empty() => second.log(git_ref, limit).await,
109 _ => Ok(found),
110 }
111 }
112 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>> {
113 match (self.first.parents(commit_hash).await?, self.second) {
114 (None, Some(second)) => second.parents(commit_hash).await,
115 (found, _) => Ok(found),
116 }
117 }
118 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>> {
119 let made = self.trees.borrow().get(tree_hash).cloned();
120 if made.is_some() {
121 return Ok(made);
122 }
123 match (self.first.read_tree(tree_hash).await?, self.second) {
124 (None, Some(second)) => second.read_tree(tree_hash).await,
125 (found, _) => Ok(found),
126 }
127 }
128 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>> {
129 let made = self.blobs.borrow().get(blob_hash).cloned();
130 if made.is_some() {
131 return Ok(made);
132 }
133 match (self.first.read_blob(blob_hash).await?, self.second) {
134 (None, Some(second)) => second.read_blob(blob_hash).await,
135 (found, _) => Ok(found),
136 }
137 }
138 async fn read_file(&self, git_ref: &str, path: &str) -> Result<Option<Vec<u8>>> {
139 self.first.read_file(git_ref, path).await
140 }
141 async fn fork(&self, target_key: &str, default_branch_only: bool) -> Result<()> {
142 self.first.fork(target_key, default_branch_only).await
143 }
144}
145
146/// What becomes of one file a change touched, applied to another tree.
147#[derive(Debug, PartialEq, Eq)]
148pub(crate) enum Resolution {
149 /// The other tree has it as the change started from: put the change's
150 /// version there (`None`: remove it).
151 Take(Option<FileEntry>),
152 /// The other tree already has it as the change made it.
153 Keep,
154 /// Both changed the text: merge the three, as `kind`.
155 Content { kind: EntryKind, base: Option<String>, ours: String, theirs: String },
156 /// Both changed it in ways that cannot be merged.
157 Conflict,
158}
159
160fn text_kind(kind: EntryKind) -> bool {
161 matches!(kind, EntryKind::Blob | EntryKind::Exec)
162}
163
164/// How a file the change took from `base` to `ours` comes out where it is
165/// `theirs` (a directory's entry included, which a file cannot replace).
166pub(crate) fn resolve(base: Option<&FileEntry>, ours: Option<&FileEntry>, theirs: Option<&FileEntry>) -> Resolution {
167 if theirs == base {
168 return Resolution::Take(ours.cloned());
169 }
170 if theirs == ours {
171 return Resolution::Keep;
172 }
173 // Deleted on one side and changed on the other.
174 let (Some(ours), Some(theirs)) = (ours, theirs) else {
175 return Resolution::Conflict;
176 };
177 if ours.0 == EntryKind::Tree || theirs.0 == EntryKind::Tree {
178 return Resolution::Conflict;
179 }
180 let base_kind = base.map(|(kind, _)| *kind);
181 // A change of mode on one side is kept with the other's content.
182 let kind = if ours.0 == theirs.0 {
183 ours.0
184 } else if base_kind == Some(ours.0) {
185 theirs.0
186 } else if base_kind == Some(theirs.0) {
187 ours.0
188 } else {
189 return Resolution::Conflict;
190 };
191 if ours.1 == theirs.1 {
192 return Resolution::Take(Some((kind, ours.1.clone())));
193 }
194 if !text_kind(ours.0) || !text_kind(theirs.0) || base_kind.is_some_and(|kind| !text_kind(kind)) {
195 return Resolution::Conflict;
196 }
197 Resolution::Content { kind, base: base.map(|(_, hash)| hash.clone()), ours: ours.1.clone(), theirs: theirs.1.clone() }
198}
199
200/// A change applied to a tree.
201#[derive(Debug, PartialEq, Eq)]
202pub(crate) enum Applied {
203 /// The tree it makes.
204 Tree(String),
205 /// The files whose changes clash, by path.
206 Conflict(Vec<String>),
207 /// What g1t cannot apply itself.
208 Unsupported(String),
209}
210
211/// The change from tree `from` (`None`: nothing) to tree `to`, applied to
212/// tree `onto`. The objects it makes are added to `objects`, and kept in
213/// `git` for what is applied after it.
214pub(crate) async fn apply<R: GitRepo>(
215 git: &Reader<'_, R>,
216 from: Option<&str>,
217 to: &str,
218 onto: &str,
219 objects: &mut Vec<(ObjectKind, Vec<u8>)>,
220) -> Result<Applied> {
221 if from == Some(onto) || to == onto {
222 return Ok(Applied::Tree(to.to_owned()));
223 }
224 if from == Some(to) {
225 return Ok(Applied::Tree(onto.to_owned()));
226 }
227 let (ours, cut) = changed_entries_up_to(git, from, to, MAX_LAND_FILES).await?;
228 if cut {
229 return Ok(Applied::Unsupported(format!("it changes more than {MAX_LAND_FILES} files")));
230 }
231 // Each changed path as `onto` has it.
232 let dirs: BTreeSet<String> = ours.iter().flat_map(|(path, _, _)| ancestors(path).map(str::to_owned)).collect();
233 let found = read_dirs(git, onto, &dirs).await?;
234 let theirs_of = |path: &str| -> Option<FileEntry> {
235 let (parent, name) = path.rsplit_once('/').unwrap_or(("", path));
236 let (_, entries) = found.get(parent)?;
237 entries.iter().find(|entry| entry.name == name).map(|entry| (entry.kind, entry.hash.clone()))
238 };
239 let mut changes = Vec::new();
240 let mut conflicts = Vec::new();
241 let mut merges = Vec::new();
242 for (path, base, mine) in ours {
243 let theirs = theirs_of(&path);
244 match resolve(base.as_ref(), mine.as_ref(), theirs.as_ref()) {
245 Resolution::Take(entry) => changes.push(Change { path, entry }),
246 Resolution::Keep => {}
247 Resolution::Conflict => conflicts.push(path),
248 Resolution::Content { kind, base, ours, theirs } => merges.push((path, kind, base, ours, theirs)),
249 }
250 }
251 if merges.len() > MAX_LINE_MERGES {
252 return Ok(Applied::Unsupported(format!("both sides changed more than {MAX_LINE_MERGES} of the same files")));
253 }
254 let read = try_join_all(merges.iter().map(|(_, _, base, ours, theirs)| async move {
255 let base = async {
256 match base {
257 Some(hash) => git.read_blob(hash).await,
258 None => Ok(Some(Vec::new())),
259 }
260 };
261 let (base, (ours, theirs)) = try_join(base, try_join(git.read_blob(ours), git.read_blob(theirs))).await?;
262 Ok::<_, worker::Error>((base, ours, theirs))
263 }))
264 .await?;
265 let mut made: Vec<(ObjectKind, Vec<u8>)> = Vec::new();
266 for ((path, kind, ..), (base, ours, theirs)) in merges.into_iter().zip(read) {
267 let texts = (as_text(base), as_text(ours), as_text(theirs));
268 let (Ok(base), Ok(ours), Ok(theirs)) = texts else {
269 if matches!(texts, (Err(true), _, _) | (_, Err(true), _) | (_, _, Err(true))) {
270 conflicts.push(path);
271 continue;
272 }
273 return Ok(Applied::Unsupported(format!("{path} is too large to merge line by line")));
274 };
275 match merge_text(&base, &ours, &theirs) {
276 Some(merged) => {
277 let blob = merged.into_bytes();
278 let id = object_id(ObjectKind::Blob, &blob);
279 made.push((ObjectKind::Blob, blob));
280 changes.push(Change { path, entry: Some((kind, id)) });
281 }
282 None => conflicts.push(path),
283 }
284 }
285 if !conflicts.is_empty() {
286 conflicts.sort();
287 return Ok(Applied::Conflict(conflicts));
288 }
289 if changes.is_empty() {
290 return Ok(Applied::Tree(onto.to_owned()));
291 }
292 let trees: HashMap<String, Vec<TreeEntry>> = found.into_values().collect();
293 let merged = match merge_tree(onto, &trees, &changes) {
294 Ok(merged) => merged,
295 Err(why) => return Ok(Applied::Unsupported(why)),
296 };
297 made.extend(merged.objects.into_iter().map(|bytes| (ObjectKind::Tree, bytes)));
298 git.keep(&made);
299 objects.extend(made);
300 Ok(Applied::Tree(merged.tree))
301}
302
303/// The commits of a branch the branch it merges into does not have,
304/// oldest first and each after its parents: those reachable from the
305/// newest of `history` (the branch's first-parent chain) and not from
306/// `shared` (the other branch's). `None` when there are more than `limit`.
307pub(crate) async fn branch_commits<R: GitRepo>(
308 git: &R,
309 history: &[Commit],
310 shared: &HashSet<String>,
311 limit: usize,
312) -> Result<Option<Vec<Commit>>> {
313 let mut known: HashMap<String, Commit> = history.iter().map(|commit| (commit.hash.clone(), commit.clone())).collect();
314 let Some(head) = history.first() else {
315 return Ok(Some(Vec::new()));
316 };
317 let mut order = Vec::new();
318 let mut seen = HashSet::new();
319 // Depth first, a commit written once its parents are: the first
320 // parent's line before the others.
321 let mut stack = vec![(head.hash.clone(), false)];
322 while let Some((hash, parents_done)) = stack.pop() {
323 if parents_done {
324 if let Some(commit) = known.get(&hash) {
325 order.push(commit.clone());
326 }
327 continue;
328 }
329 if shared.contains(&hash) || !seen.insert(hash.clone()) {
330 continue;
331 }
332 if seen.len() > limit {
333 return Ok(None);
334 }
335 let commit = match known.get(&hash) {
336 Some(commit) => commit.clone(),
337 None => match git.log(&hash, 1).await?.into_iter().next() {
338 Some(commit) => {
339 known.insert(hash.clone(), commit.clone());
340 commit
341 }
342 // Not there to read: where the branch's history ends.
343 None => continue,
344 },
345 };
346 stack.push((hash, true));
347 for parent in commit.parents.iter().rev() {
348 stack.push((parent.clone(), false));
349 }
350 }
351 Ok(Some(order))
352}
353
354/// A branch's commits, made again.
355#[derive(Debug)]
356pub(crate) struct Replayed {
357 /// The last commit made, or where they were made onto if none was.
358 pub head: String,
359 /// The commits made, oldest first, each with the one it was made from.
360 pub made: Vec<(String, String)>,
361 /// The messages and authors of what was made, for the rules.
362 pub facts: Vec<g1t_contracts::rules::CommitFacts>,
363}
364
365/// How replaying went.
366#[derive(Debug)]
367pub(crate) enum Replay {
368 Done(Replayed),
369 /// A commit's changes clash with what is there: the commit, and the files.
370 Conflict { commit: Commit, paths: Vec<String> },
371 Unsupported { commit: Commit, why: String },
372}
373
374/// When a commit says it was authored, in seconds.
375fn authored_seconds(commit: &Commit) -> u64 {
376 parse_rfc3339(&commit.authored_at).unwrap_or(0) / 1000
377}
378
379/// Makes each of `commits` (oldest first) again on top of `onto` (a commit
380/// and its tree): its changes applied to the commit before, its author,
381/// date and message kept, committed by `committer` with `sign_off` added.
382/// Merge commits are left out, their changes already in what they merged,
383/// and so is a commit whose changes are all there already.
384pub(crate) async fn replay<R: GitRepo>(
385 git: &Reader<'_, R>,
386 commits: &[Commit],
387 onto: (&str, &str),
388 committer: &Signature<'_>,
389 sign_off: Option<&str>,
390 objects: &mut Vec<(ObjectKind, Vec<u8>)>,
391) -> Result<Replay> {
392 let trees: HashMap<&str, &str> = commits.iter().map(|commit| (commit.hash.as_str(), commit.tree_hash.as_str())).collect();
393 let (mut head, mut tree) = (onto.0.to_owned(), onto.1.to_owned());
394 let mut made = Vec::new();
395 let mut facts = Vec::new();
396 for commit in commits.iter().filter(|commit| commit.parents.len() <= 1) {
397 let from = match commit.parents.first() {
398 None => None,
399 Some(parent) => match trees.get(parent.as_str()) {
400 Some(tree) => Some((*tree).to_owned()),
401 None => match git.log(parent, 1).await?.into_iter().next() {
402 Some(parent) => Some(parent.tree_hash),
403 None => {
404 return Ok(Replay::Unsupported { commit: commit.clone(), why: format!("its parent {parent} could not be read") });
405 }
406 },
407 },
408 };
409 let applied = apply(git, from.as_deref(), &commit.tree_hash, &tree, objects).await?;
410 let new_tree = match applied {
411 Applied::Tree(new_tree) => new_tree,
412 Applied::Conflict(paths) => return Ok(Replay::Conflict { commit: commit.clone(), paths }),
413 Applied::Unsupported(why) => return Ok(Replay::Unsupported { commit: commit.clone(), why }),
414 };
415 if new_tree == tree {
416 continue;
417 }
418 let author = Signature { name: &commit.author.name, email: &commit.author.email, seconds: authored_seconds(commit) };
419 let message = with_trailers(&commit.message, sign_off.into_iter().map(str::to_owned).collect::<Vec<_>>().as_slice());
420 let bytes = commit_object_by(&new_tree, &[&head], &author, committer, &message);
421 let id = object_id(ObjectKind::Commit, &bytes);
422 objects.push((ObjectKind::Commit, bytes));
423 let mut fact = crate::rules::made_commit(&id, &message, committer.email, 1, Vec::new());
424 fact.author_email = Some(commit.author.email.clone());
425 facts.push(fact);
426 made.push((id.clone(), commit.hash.clone()));
427 head = id;
428 tree = new_tree;
429 }
430 Ok(Replay::Done(Replayed { head, made, facts }))
431}
432
433/// A trailer's key and who or what it names, compared without case: the
434/// address in `<...>` when there is one.
435fn trailer_parts(line: &str) -> Option<(String, String)> {
436 let (key, value) = line.trim().split_once(':')?;
437 if key.is_empty() || !key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') || !value.starts_with(' ') {
438 return None;
439 }
440 let value = value.trim();
441 let named = match (value.rfind('<'), value.rfind('>')) {
442 (Some(open), Some(close)) if open < close => &value[open + 1..close],
443 _ => value,
444 };
445 Some((key.to_ascii_lowercase(), named.to_lowercase()))
446}
447
448/// `message` with `trailers` added at its end, each unless one with the
449/// same key naming the same address is there already. They join the
450/// message's own trailers when its last paragraph is made of them, and
451/// otherwise start a paragraph of their own.
452pub(crate) fn with_trailers(message: &str, trailers: &[String]) -> String {
453 let message = message.trim_end();
454 let mut present: Vec<(String, String)> = message.lines().filter_map(trailer_parts).collect();
455 let mut adding: Vec<&str> = Vec::new();
456 for trailer in trailers {
457 match trailer_parts(trailer) {
458 Some(parts) if present.contains(&parts) => {}
459 Some(parts) => {
460 present.push(parts);
461 adding.push(trailer);
462 }
463 None => adding.push(trailer),
464 }
465 }
466 if adding.is_empty() {
467 return message.to_owned();
468 }
469 if message.is_empty() {
470 return adding.join("\n");
471 }
472 let ends_in_trailers = message
473 .rsplit_once("\n\n")
474 .is_some_and(|(_, last)| !last.trim().is_empty() && last.lines().all(|line| trailer_parts(line).is_some()));
475 let gap = if ends_in_trailers { "\n" } else { "\n\n" };
476 format!("{message}{gap}{}", adding.join("\n"))
477}
478
479/// The `Signed-off-by:` trailer for someone.
480pub(crate) fn sign_off_trailer(name: &str, email: &str) -> String {
481 format!("Signed-off-by: {name} <{email}>")
482}
483
484/// A `Co-authored-by:` trailer for everyone who wrote one of `commits`
485/// other than `author` (by address) and g1t itself, once each, in the
486/// order they first appear. Merge commits change nothing of their own and
487/// credit no one.
488pub(crate) fn co_authors(commits: &[Commit], author: &str) -> Vec<String> {
489 let mut seen: HashSet<String> = [author.to_lowercase(), g1t_contracts::system::EMAIL.to_owned()].into_iter().collect();
490 commits
491 .iter()
492 .filter(|commit| commit.parents.len() <= 1 && !commit.author.email.trim().is_empty())
493 .filter(|commit| seen.insert(commit.author.email.trim().to_lowercase()))
494 .map(|commit| format!("Co-authored-by: {} <{}>", commit.author.name, commit.author.email.trim()))
495 .collect()
496}
497
498/// `message` with its `Co-authored-by:` trailers made right, as a
499/// template may have written them from addresses alone: none for the
500/// commit's own author (`author_email`) or g1t, and one naming someone in
501/// `known` (trailers g1t made from the commits) as that one names them.
502pub(crate) fn tidy_co_authors(message: &str, author_email: &str, known: &[String]) -> String {
503 let email_of = |line: &str| {
504 line.rsplit_once('<')
505 .and_then(|(_, rest)| rest.split_once('>'))
506 .map(|(email, _)| email.trim().to_lowercase())
507 };
508 let author = author_email.trim().to_lowercase();
509 message
510 .lines()
511 .filter_map(|line| {
512 if !line.trim_start().to_lowercase().starts_with("co-authored-by:") {
513 return Some(line.to_owned());
514 }
515 let Some(email) = email_of(line) else { return Some(line.to_owned()) };
516 if email == author || email == g1t_contracts::system::EMAIL {
517 return None;
518 }
519 Some(known.iter().find(|trailer| email_of(trailer).as_deref() == Some(email.as_str())).cloned().unwrap_or_else(|| line.to_owned()))
520 })
521 .collect::<Vec<_>>()
522 .join("\n")
523}
524
525/// Files named in a sentence: a few, then how many more.
526pub(crate) fn name_files(paths: &[String]) -> String {
527 const SHOWN: usize = 5;
528 match paths {
529 [] => "some files".to_owned(),
530 [one] => one.clone(),
531 _ if paths.len() <= SHOWN => {
532 let (last, rest) = paths.split_last().expect("two or more");
533 format!("{} and {last}", rest.join(", "))
534 }
535 _ => format!("{} and {} more files", paths[..SHOWN].join(", "), paths.len() - SHOWN),
536 }
537}
538
539/// A commit as people know it: its short hash and title.
540fn named_commit(commit: &Commit) -> String {
541 let title = commit.message.lines().next().unwrap_or_default().trim();
542 format!("{} ({title})", &commit.hash[..commit.hash.len().min(7)])
543}
544
545/// What was made to land, ready to push.
546struct Made {
547 commit: String,
548 rebased: Vec<(String, String)>,
549 /// How many commits it brings, when that is known.
550 commits: Option<u32>,
551}
552
553/// Why a landing was refused, or that it was there already.
554enum NotMade {
555 Refused(Outcome<Landed>),
556 /// Nothing to make: the branch already has every change.
557 AlreadyThere,
558}
559
560fn refused(code: FailureCode, message: String) -> NotMade {
561 NotMade::Refused(Outcome::fail(code, message))
562}
563
564impl<S: GitStore> Repos<S> {
565 /// `land` by `method`, any but `fast_forward`: makes the commits on
566 /// `branch` of `target` from `new`, the checked head of the pull
567 /// request's branch (`history`, its first-parent chain, in
568 /// `source_git`), and moves `branch` to the last of them if it is still
569 /// where it was. If another change lands first, it is worked out again
570 /// once on the new head. The caller has checked who may do it.
571 #[allow(clippy::too_many_arguments)]
572 pub(crate) async fn land_by(
573 &self,
574 method: MergeMethod,
575 a: &LandArgs,
576 target: &Repo,
577 source_git: &S::Repo,
578 target_git: &S::Repo,
579 branch: &str,
580 history: &[Commit],
581 new: String,
582 from_fork: bool,
583 ) -> Result<Outcome<Landed>> {
584 let message = match method {
585 MergeMethod::Merge | MergeMethod::Squash => match a.message.as_deref().map(str::trim_end).filter(|text| !text.trim().is_empty()) {
586 Some(text) => text.to_owned(),
587 None => {
588 return Ok(Outcome::fail(FailureCode::Invalid, "Say what the commit's message is: its first line is its title."));
589 }
590 },
591 _ => String::new(),
592 };
593 let committer = self.commit_identity(&a.actor).await;
594 let author = match (&a.author, method) {
595 (Some(author), MergeMethod::Squash) => self.commit_identity(author).await,
596 _ => committer.clone(),
597 };
598 let sign_off = a.sign_off.then(|| sign_off_trailer(&committer.name, &committer.email));
599 // Whether the repository holds the pull request's commits: always
600 // for its own branch, and for a fork's once its head was kept there
601 // (test_merge.rs). Otherwise they are read from the fork, and sent
602 // with what is made.
603 let held = !from_fork || !target_git.log(&new, 1).await?.is_empty();
604 let reader = Reader::new(target_git, (!held).then_some(source_git));
605 let mut attempt = 0;
606 loop {
607 attempt += 1;
608 let target_history = target_git.log(branch, MAX_ANCESTRY).await?;
609 let Some(base) = target_history.first().cloned() else {
610 return Ok(Outcome::fail(
611 FailureCode::Conflict,
612 format!("{branch} has no commits yet, so there is nothing to make this pull request's commits on. Land it by fast-forward instead."),
613 ));
614 };
615 // There already: an attempt that published and stopped before
616 // the merge was recorded, or a branch with nothing new.
617 if base.hash == new || (held && descends_from(target_git, &target_history, &new).await?) {
618 let commit = match method {
619 MergeMethod::Merge => target_history
620 .iter()
621 .find(|commit| commit.parents.get(1) == Some(&new))
622 .map_or_else(|| base.hash.clone(), |commit| commit.hash.clone()),
623 _ => base.hash.clone(),
624 };
625 return Ok(Outcome::Ok(Landed { commit, previous: None, method, rebased: Vec::new() }));
626 }
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 };
637 let mut objects: Vec<(ObjectKind, Vec<u8>)> = Vec::new();
638 let seconds = now_ms() / 1000;
639 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 {
707 Ok(made) => made,
708 Err(NotMade::Refused(outcome)) => return Ok(outcome),
709 Err(NotMade::AlreadyThere) => {
710 return Ok(Outcome::Ok(Landed { commit: base.hash.clone(), previous: None, method, rebased: Vec::new() }));
711 }
712 };
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 };
722 let target_access = target_git.access(Scope::Write).await?;
723 let pushed = land::push_pack(&target_access, branch, Some(&base.hash), &made.commit, pack).await?;
724 self.refs_moved(&target.id).await;
725 match pushed {
726 Ok(()) => {
727 self.publish_push(target, &format!("refs/heads/{branch}"), Some(&base.hash), &made.commit, Some(&a.actor), made.commits)
728 .await?;
729 return Ok(Outcome::Ok(Landed {
730 commit: made.commit,
731 previous: Some(base.hash.clone()),
732 method,
733 rebased: made.rebased,
734 }));
735 }
736 // Most often another pull request landed meanwhile: once
737 // more, on top of it.
738 Err(_) if attempt < 2 => continue,
739 Err(reason) => {
740 return Ok(Outcome::fail(FailureCode::Conflict, format!("{branch} could not be updated: {reason}")));
741 }
742 }
743 }
744 }
745
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 }
783 };
784 // Squashing what is there already would make an empty commit.
785 if squash && tree == base.tree_hash {
786 return Ok(Err(NotMade::AlreadyThere));
787 }
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)
794 } else {
795 commit_object_by(&tree, &[&base.hash, new], committer, committer, &message)
796 };
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) }))
800 }
801
802 /// `update_pull_branch` by `rebase`: makes the commits of `branch` of
803 /// `source` again on the head of `base_branch` of `target` and moves
804 /// the branch there (a force push), if it is still at `expected_head`
805 /// (else its head when read). The caller has checked who may do it.
806 pub(crate) async fn rebase_up(
807 &self,
808 source: Repo,
809 target: Repo,
810 branch: String,
811 base_branch: String,
812 actor: &User,
813 expected_head: Option<&str>,
814 ) -> Result<Outcome<PullBranchUpdate>> {
815 let from_fork = source.id != target.id;
816 // A working copy removed after its pull request closed is made again.
817 self.live(&source).await?;
818 let source_git = self.store.open(&store_key(&source)).await?;
819 let target_git = self.store.open(&store_key(&target)).await?;
820 let (history, target_history) =
821 try_join(source_git.log(&branch, MAX_ANCESTRY), target_git.log(&base_branch, MAX_ANCESTRY)).await?;
822 let (Some(head), Some(base)) = (history.first(), target_history.first()) else {
823 return Ok(Outcome::fail(FailureCode::Conflict, format!("{branch} has no commits to bring up to date.")));
824 };
825 if let Some(refused) = moved_since(&branch, &head.hash, expected_head) {
826 return Ok(refused);
827 }
828 let needs_agent = |reason, detail: String, paths| Outcome::Ok(PullBranchUpdate::NeedsAgent { reason, detail, paths });
829 let shared: HashSet<String> = target_history.iter().map(|commit| commit.hash.clone()).collect();
830 let reader = Reader::new(&source_git, from_fork.then_some(&target_git));
831 let Some(merge_base) = nearest_ancestor_in(&reader, &history, &shared).await? else {
832 return Ok(needs_agent(
833 NeedsAgentReason::Unsupported,
834 format!("g1t could not find where this pull request left {base_branch}."),
835 Vec::new(),
836 ));
837 };
838 // On the base's head already, with no merge commits to straighten.
839 let commits = match branch_commits(&reader, &history, &shared, MAX_REBASE_COMMITS).await? {
840 Some(commits) => commits,
841 None => {
842 return Ok(needs_agent(
843 NeedsAgentReason::Unsupported,
844 format!("{branch} has more than {MAX_REBASE_COMMITS} commits, too many for g1t to rebase itself."),
845 Vec::new(),
846 ));
847 }
848 };
849 if merge_base == base.hash && commits.iter().all(|commit| commit.parents.len() <= 1) {
850 return Ok(Outcome::Ok(PullBranchUpdate::UpToDate { commit: head.hash.clone() }));
851 }
852 let committer = self.commit_identity(actor).await;
853 let by = Signature { name: &committer.name, email: &committer.email, seconds: now_ms() / 1000 };
854 let mut objects: Vec<(ObjectKind, Vec<u8>)> = Vec::new();
855 let replayed = match replay(&reader, &commits, (&base.hash, &base.tree_hash), &by, None, &mut objects).await? {
856 Replay::Done(replayed) => replayed,
857 Replay::Conflict { commit, paths } => {
858 return Ok(needs_agent(
859 NeedsAgentReason::Conflicting,
860 format!("{} and {base_branch} changed the same lines of some files.", named_commit(&commit)),
861 paths,
862 ));
863 }
864 Replay::Unsupported { commit, why } => {
865 return Ok(needs_agent(
866 NeedsAgentReason::Unsupported,
867 format!("g1t could not rebase {} itself: {why}.", named_commit(&commit)),
868 Vec::new(),
869 ));
870 }
871 };
872 let update = BranchUpdate {
873 branch: &branch,
874 head: &head.hash,
875 commit: &replayed.head,
876 fast_forward: false,
877 commits: replayed.made.len() as u32,
878 };
879 if let Some(refused) = self.update_refused(&source, actor, &update, replayed.facts.clone(), true).await? {
880 return Ok(refused);
881 }
882 // A fork lacks what the base branch gained since it was made.
883 let pack = if from_fork {
884 let target_access = target_git.access(Scope::Read).await?;
885 let fetched = land::fetch_pack(&target_access, &base.hash, Some(&merge_base)).await?;
886 extend_pack(&fetched, &objects).map_err(worker::Error::RustError)?
887 } else {
888 write_pack(&objects)
889 };
890 self.push_update(&source, &source_git, actor, &update, pack).await
891 }
892}
893
894#[cfg(test)]
895mod tests {
896 use super::*;
897 use g1t_contracts::repos::Signature as Person;
898
899 fn blob(hash: &str) -> Option<FileEntry> {
900 Some((EntryKind::Blob, hash.to_owned()))
901 }
902
903 #[test]
904 fn a_file_the_other_side_left_alone_takes_the_change() {
905 assert_eq!(resolve(blob("1").as_ref(), blob("2").as_ref(), blob("1").as_ref()), Resolution::Take(blob("2")));
906 // Added, where nothing was.
907 assert_eq!(resolve(None, blob("2").as_ref(), None), Resolution::Take(blob("2")));
908 // Deleted.
909 assert_eq!(resolve(blob("1").as_ref(), None, blob("1").as_ref()), Resolution::Take(None));
910 // Made the same on both sides.
911 assert_eq!(resolve(blob("1").as_ref(), blob("2").as_ref(), blob("2").as_ref()), Resolution::Keep);
912 }
913
914 #[test]
915 fn a_file_both_changed_is_merged_or_conflicts() {
916 assert_eq!(
917 resolve(blob("1").as_ref(), blob("2").as_ref(), blob("3").as_ref()),
918 Resolution::Content { kind: EntryKind::Blob, base: Some("1".into()), ours: "2".into(), theirs: "3".into() }
919 );
920 // Deleted on one side, changed on the other.
921 assert_eq!(resolve(blob("1").as_ref(), None, blob("3").as_ref()), Resolution::Conflict);
922 assert_eq!(resolve(blob("1").as_ref(), blob("2").as_ref(), None), Resolution::Conflict);
923 // A file where the other side has a directory.
924 assert_eq!(resolve(None, blob("2").as_ref(), Some(&(EntryKind::Tree, "t".into()))), Resolution::Conflict);
925 // Submodules moved two ways.
926 let link = |hash: &str| Some((EntryKind::Gitlink, hash.to_owned()));
927 assert_eq!(resolve(link("1").as_ref(), link("2").as_ref(), link("3").as_ref()), Resolution::Conflict);
928 // Made executable on one side, changed on the other.
929 assert_eq!(
930 resolve(blob("1").as_ref(), Some(&(EntryKind::Exec, "1".into())), blob("2").as_ref()),
931 Resolution::Content { kind: EntryKind::Exec, base: Some("1".into()), ours: "1".into(), theirs: "2".into() }
932 );
933 }
934
935 #[test]
936 fn trailers_are_added_once_after_the_message() {
937 let sign = sign_off_trailer("Ana", "ana@example.com");
938 assert_eq!(with_trailers("Fix it\n", std::slice::from_ref(&sign)), "Fix it\n\nSigned-off-by: Ana <ana@example.com>");
939 // Already signed off (case aside): not again.
940 let signed = "Fix it\n\nsigned-off-by: Ana <ANA@example.com>\n";
941 assert_eq!(with_trailers(signed, std::slice::from_ref(&sign)), signed.trim_end());
942 // Joins trailers already ending the message.
943 // A template's trailers, from addresses alone: the author's own and
944 // g1t's go, and a known one takes its real name.
945 let known = ["Co-authored-by: Bo Lin <bo@example.com>".to_owned()];
946 let g1t = format!("Co-authored-by: g1t <{}>", g1t_contracts::system::EMAIL);
947 let message = format!(
948 "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>"
949 );
950 assert_eq!(
951 tidy_co_authors(&message, "ana@example.com", &known),
952 "Fix\n\nCo-authored-by: Bo Lin <bo@example.com>\nCo-authored-by: cy <cy@example.com>"
953 );
954 let co = "Co-authored-by: Bo <bo@example.com>".to_owned();
955 assert_eq!(
956 with_trailers("Fix it\n\nBody.\n\nCo-authored-by: Cy <cy@example.com>", &[co.clone(), sign.clone()]),
957 "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>"
958 );
959 // A body that only looks like it has a colon is not a trailer.
960 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>");
961 // Twice in one list: once.
962 assert_eq!(with_trailers("Fix", &[co.clone(), co.clone()]), "Fix\n\nCo-authored-by: Bo <bo@example.com>");
963 // Adding it to a message already holding it changes nothing.
964 let once = with_trailers("Fix", std::slice::from_ref(&sign));
965 assert_eq!(with_trailers(&once, std::slice::from_ref(&sign)), once);
966 }
967
968 fn commit(hash: &str, parents: &[&str], name: &str, email: &str) -> Commit {
969 Commit {
970 hash: hash.to_owned(),
971 tree_hash: String::new(),
972 message: format!("{hash}\n"),
973 author: Person { name: name.to_owned(), email: email.to_owned() },
974 parents: parents.iter().map(|&parent| parent.to_owned()).collect(),
975 authored_at: String::new(),
976 }
977 }
978
979 #[test]
980 fn everyone_else_whose_commits_are_squashed_is_credited_once() {
981 let commits = [
982 commit("a", &["m"], "Ana", "ana@example.com"),
983 commit("b", &["a"], "Bo", "bo@example.com"),
984 commit("c", &["b"], "Bo again", "BO@example.com"),
985 commit("d", &["c"], "g1t", g1t_contracts::system::EMAIL),
986 // A merge commit brings nothing of its own.
987 commit("e", &["d", "x"], "Cy", "cy@example.com"),
988 commit("f", &["e"], "Di", "di@example.com"),
989 ];
990 assert_eq!(
991 co_authors(&commits, "ANA@example.com"),
992 ["Co-authored-by: Bo <bo@example.com>", "Co-authored-by: Di <di@example.com>"]
993 );
994 }
995
996 #[test]
997 fn files_are_named_a_few_at_a_time() {
998 let paths = |n: usize| (1..=n).map(|i| format!("f{i}")).collect::<Vec<_>>();
999 assert_eq!(name_files(&paths(1)), "f1");
1000 assert_eq!(name_files(&paths(3)), "f1, f2 and f3");
1001 assert_eq!(name_files(&paths(8)), "f1, f2, f3, f4, f5 and 3 more files");
1002 }
1003
1004 /// A store of whole objects made in the test: commits by name, trees
1005 /// and blobs by their real ids.
1006 #[derive(Default)]
1007 struct Store {
1008 commits: HashMap<String, Commit>,
1009 trees: HashMap<String, Vec<TreeEntry>>,
1010 blobs: HashMap<String, Vec<u8>>,
1011 }
1012
1013 impl Store {
1014 fn tree(&mut self, files: &[(&str, &str)]) -> String {
1015 let mut here: Vec<TreeEntry> = Vec::new();
1016 let mut dirs: std::collections::BTreeMap<&str, Vec<(&str, &str)>> = Default::default();
1017 for (path, text) in files {
1018 match path.split_once('/') {
1019 Some((dir, rest)) => dirs.entry(dir).or_default().push((rest, text)),
1020 None => {
1021 let hash = object_id(ObjectKind::Blob, text.as_bytes());
1022 self.blobs.insert(hash.clone(), text.as_bytes().to_vec());
1023 here.push(TreeEntry { name: (*path).to_owned(), hash, kind: EntryKind::Blob });
1024 }
1025 }
1026 }
1027 for (dir, files) in dirs {
1028 let hash = self.tree(&files);
1029 here.push(TreeEntry { name: dir.to_owned(), hash, kind: EntryKind::Tree });
1030 }
1031 let id = object_id(ObjectKind::Tree, &crate::catch_up::encode_entries(&here));
1032 self.trees.insert(id.clone(), here);
1033 id
1034 }
1035
1036 fn commit(&mut self, hash: &str, parents: &[&str], author: &str, files: &[(&str, &str)]) -> Commit {
1037 let tree_hash = self.tree(files);
1038 let made = Commit {
1039 hash: hash.to_owned(),
1040 tree_hash,
1041 message: format!("Change {hash}\n"),
1042 author: Person { name: author.to_owned(), email: format!("{author}@example.com") },
1043 parents: parents.iter().map(|&parent| parent.to_owned()).collect(),
1044 authored_at: "2026-10-01T00:00:00.000Z".to_owned(),
1045 };
1046 self.commits.insert(hash.to_owned(), made.clone());
1047 made
1048 }
1049
1050 /// The files of a tree, read through `git`, by path.
1051 fn files(git: &Reader<'_, Store>, tree: &str) -> Vec<(String, String)> {
1052 let mut out = Vec::new();
1053 let mut todo = vec![(String::new(), tree.to_owned())];
1054 while let Some((prefix, tree)) = todo.pop() {
1055 for entry in block_on(git.read_tree(&tree)).unwrap().expect("tree") {
1056 let path = format!("{prefix}{}", entry.name);
1057 if entry.kind == EntryKind::Tree {
1058 todo.push((format!("{path}/"), entry.hash));
1059 } else {
1060 let bytes = block_on(git.read_blob(&entry.hash)).unwrap().expect("blob");
1061 out.push((path, String::from_utf8(bytes).unwrap()));
1062 }
1063 }
1064 }
1065 out.sort();
1066 out
1067 }
1068 }
1069
1070 impl GitRepo for Store {
1071 async fn access(&self, _scope: Scope) -> Result<GitAccess> {
1072 unimplemented!()
1073 }
1074 async fn branches(&self) -> Result<Vec<Branch>> {
1075 Ok(Vec::new())
1076 }
1077 async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>> {
1078 let mut out = Vec::new();
1079 let mut at = self.commits.get(git_ref);
1080 while let Some(commit) = at.filter(|_| (out.len() as u32) < limit) {
1081 out.push(commit.clone());
1082 at = commit.parents.first().and_then(|parent| self.commits.get(parent));
1083 }
1084 Ok(out)
1085 }
1086 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>> {
1087 Ok(self.commits.get(commit_hash).map(|commit| commit.parents.clone()))
1088 }
1089 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>> {
1090 Ok(self.trees.get(tree_hash).cloned())
1091 }
1092 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>> {
1093 Ok(self.blobs.get(blob_hash).cloned())
1094 }
1095 async fn read_file(&self, _git_ref: &str, _path: &str) -> Result<Option<Vec<u8>>> {
1096 Ok(None)
1097 }
1098 async fn fork(&self, _target_key: &str, _default_branch_only: bool) -> Result<()> {
1099 Ok(())
1100 }
1101 }
1102
1103 fn block_on<F: std::future::Future>(future: F) -> F::Output {
1104 let mut future = std::pin::pin!(future);
1105 let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
1106 loop {
1107 if let std::task::Poll::Ready(output) = future.as_mut().poll(&mut cx) {
1108 return output;
1109 }
1110 }
1111 }
1112
1113 fn files(list: &[(&str, &str)]) -> Vec<(String, String)> {
1114 list.iter().map(|(path, text)| ((*path).to_owned(), (*text).to_owned())).collect()
1115 }
1116
1117 /// `main` at m then b; the branch at m then h1, h2.
1118 fn diverged() -> (Store, [Commit; 4]) {
1119 let mut store = Store::default();
1120 let m = store.commit("m", &[], "ana", &[("README.md", "hi\n"), ("src/lib.rs", "a\nb\nc\nd\ne\n")]);
1121 let b = store.commit("b", &["m"], "ana", &[("README.md", "hello\n"), ("src/lib.rs", "a\nb\nc\nd\nE\n")]);
1122 let h1 = store.commit("h1", &["m"], "bo", &[("README.md", "hi\n"), ("src/lib.rs", "A\nb\nc\nd\ne\n")]);
1123 let h2 = store.commit(
1124 "h2",
1125 &["h1"],
1126 "cy",
1127 &[("README.md", "hi\n"), ("src/lib.rs", "A\nb\nc\nd\ne\n"), ("src/new.rs", "new\n")],
1128 );
1129 (store, [m, b, h1, h2])
1130 }
1131
1132 #[test]
1133 fn a_change_applies_line_by_line_where_both_sides_touched_a_file() {
1134 let (store, [m, b, _, h2]) = diverged();
1135 let git = Reader::new(&store, None);
1136 let mut objects = Vec::new();
1137 let Applied::Tree(tree) = block_on(apply(&git, Some(&m.tree_hash), &h2.tree_hash, &b.tree_hash, &mut objects)).unwrap() else {
1138 panic!("not applied")
1139 };
1140 assert_eq!(
1141 Store::files(&git, &tree),
1142 files(&[("README.md", "hello\n"), ("src/lib.rs", "A\nb\nc\nd\nE\n"), ("src/new.rs", "new\n")])
1143 );
1144 // The merged blob, the root and src: only what changed.
1145 assert_eq!(objects.iter().filter(|(kind, _)| *kind == ObjectKind::Blob).count(), 1);
1146 assert_eq!(objects.iter().filter(|(kind, _)| *kind == ObjectKind::Tree).count(), 2);
1147 }
1148
1149 #[test]
1150 fn clashing_lines_name_the_file_and_make_nothing() {
1151 let mut store = Store::default();
1152 let m = store.commit("m", &[], "ana", &[("a", "1\n"), ("b", "x\n")]);
1153 let h = store.commit("h", &["m"], "bo", &[("a", "2\n"), ("b", "y\n")]);
1154 let b = store.commit("b", &["m"], "ana", &[("a", "3\n"), ("b", "x\n")]);
1155 let git = Reader::new(&store, None);
1156 let mut objects = Vec::new();
1157 let applied = block_on(apply(&git, Some(&m.tree_hash), &h.tree_hash, &b.tree_hash, &mut objects)).unwrap();
1158 assert_eq!(applied, Applied::Conflict(vec!["a".into()]));
1159 assert!(objects.is_empty());
1160 }
1161
1162 #[test]
1163 fn a_branch_s_commits_come_oldest_first_and_stop_at_the_base() {
1164 let (mut store, [m, b, h1, h2]) = diverged();
1165 // The branch merged main in, then went on.
1166 let j = store.commit("j", &["h2", "b"], "bo", &[("README.md", "hello\n")]);
1167 let k = store.commit("k", &["j"], "bo", &[("README.md", "hello\n")]);
1168 let history = block_on(store.log("k", 100)).unwrap();
1169 let shared: HashSet<String> = [m.hash.clone(), b.hash.clone()].into_iter().collect();
1170 let commits = block_on(branch_commits(&store, &history, &shared, 10)).unwrap().unwrap();
1171 let order: Vec<&str> = commits.iter().map(|commit| commit.hash.as_str()).collect();
1172 assert_eq!(order, [h1.hash.as_str(), h2.hash.as_str(), j.hash.as_str(), k.hash.as_str()]);
1173 // Too many: none.
1174 assert!(block_on(branch_commits(&store, &history, &shared, 3)).unwrap().is_none());
1175 }
1176
1177 #[test]
1178 fn a_rebase_makes_each_commit_again_on_the_base_keeping_its_author() {
1179 let (store, [_, b, h1, h2]) = diverged();
1180 let git = Reader::new(&store, None);
1181 let committer = Signature { name: "Di", email: "di@example.com", seconds: 1_800_000_000 };
1182 let sign = sign_off_trailer("Di", "di@example.com");
1183 let mut objects = Vec::new();
1184 let commits = [h1.clone(), h2.clone()];
1185 let Replay::Done(replayed) = block_on(replay(&git, &commits, (&b.hash, &b.tree_hash), &committer, Some(&sign), &mut objects)).unwrap() else {
1186 panic!("not replayed")
1187 };
1188 assert_eq!(replayed.made.iter().map(|(_, original)| original.as_str()).collect::<Vec<_>>(), ["h1", "h2"]);
1189 let made: Vec<String> = objects
1190 .iter()
1191 .filter(|(kind, _)| *kind == ObjectKind::Commit)
1192 .map(|(_, bytes)| String::from_utf8(bytes.clone()).unwrap())
1193 .collect();
1194 assert_eq!(made.len(), 2);
1195 // The first on the base, the second on the first.
1196 assert!(made[0].contains(&format!("parent {}\n", b.hash)), "{}", made[0]);
1197 assert!(made[1].contains(&format!("parent {}\n", replayed.made[0].0)), "{}", made[1]);
1198 assert_eq!(replayed.head, replayed.made[1].0);
1199 // Their authors and dates kept, committed by whoever rebased.
1200 assert!(made[0].contains("author bo <bo@example.com> 1790812800 +0000\ncommitter Di <di@example.com> 1800000000 +0000\n"));
1201 assert!(made[1].ends_with("\n\nChange h2\n\nSigned-off-by: Di <di@example.com>\n"), "{}", made[1]);
1202 // The last holds both sides.
1203 let tree = made[1].lines().next().unwrap().strip_prefix("tree ").unwrap().to_owned();
1204 assert_eq!(
1205 Store::files(&git, &tree),
1206 files(&[("README.md", "hello\n"), ("src/lib.rs", "A\nb\nc\nd\nE\n"), ("src/new.rs", "new\n")])
1207 );
1208 }
1209
1210 #[test]
1211 fn a_rebase_stops_at_the_commit_that_clashes() {
1212 let mut store = Store::default();
1213 store.commit("m", &[], "ana", &[("a", "1\n"), ("b", "x\n")]);
1214 let h1 = store.commit("h1", &["m"], "bo", &[("a", "1\n"), ("b", "y\n")]);
1215 let h2 = store.commit("h2", &["h1"], "bo", &[("a", "2\n"), ("b", "y\n")]);
1216 let b = store.commit("b", &["m"], "ana", &[("a", "3\n"), ("b", "x\n")]);
1217 let git = Reader::new(&store, None);
1218 let committer = Signature { name: "Di", email: "di@example.com", seconds: 1 };
1219 let mut objects = Vec::new();
1220 let replayed = block_on(replay(&git, &[h1, h2], (&b.hash, &b.tree_hash), &committer, None, &mut objects)).unwrap();
1221 let Replay::Conflict { commit, paths } = replayed else { panic!("no conflict") };
1222 assert_eq!(commit.hash, "h2");
1223 assert_eq!(paths, ["a"]);
1224 assert_eq!(named_commit(&commit), "h2 (Change h2)");
1225 }
1226
1227 #[test]
1228 fn a_commit_whose_changes_are_there_already_is_left_out() {
1229 let mut store = Store::default();
1230 store.commit("m", &[], "ana", &[("a", "1\n")]);
1231 let h1 = store.commit("h1", &["m"], "bo", &[("a", "2\n")]);
1232 // The base made the same change.
1233 let b = store.commit("b", &["m"], "ana", &[("a", "2\n")]);
1234 let git = Reader::new(&store, None);
1235 let committer = Signature { name: "Di", email: "di@example.com", seconds: 1 };
1236 let mut objects = Vec::new();
1237 let Replay::Done(replayed) = block_on(replay(&git, &[h1], (&b.hash, &b.tree_hash), &committer, None, &mut objects)).unwrap() else {
1238 panic!("not replayed")
1239 };
1240 assert!(replayed.made.is_empty());
1241 assert_eq!(replayed.head, b.hash);
1242 }
1243
1244 #[test]
1245 fn a_merge_commit_has_both_parents_and_a_squash_one() {
1246 let (store, [m, b, _, h2]) = diverged();
1247 let git = Reader::new(&store, None);
1248 let committer = Signature { name: "Di", email: "di@example.com", seconds: 5 };
1249 let author = Signature { name: "Bo", email: "bo@example.com", seconds: 5 };
1250 let mut objects = Vec::new();
1251 let Applied::Tree(tree) = block_on(apply(&git, Some(&m.tree_hash), &h2.tree_hash, &b.tree_hash, &mut objects)).unwrap() else {
1252 panic!("not applied")
1253 };
1254 let merge = String::from_utf8(commit_object_by(&tree, &[&b.hash, &h2.hash], &committer, &committer, "Merge it")).unwrap();
1255 assert!(merge.contains(&format!("parent {}\nparent {}\n", b.hash, h2.hash)));
1256 let squash = String::from_utf8(commit_object_by(&tree, &[&b.hash], &author, &committer, "Squash it")).unwrap();
1257 assert_eq!(squash.matches("parent ").count(), 1);
1258 assert!(squash.contains("author Bo <bo@example.com> 5 +0000\ncommitter Di <di@example.com> 5 +0000\n"));
1259 }
1260
1261 #[test]
1262 fn reads_fall_back_to_the_second_repository_and_the_objects_made() {
1263 let mut fork = Store::default();
1264 let h = fork.commit("h", &[], "bo", &[("a", "fork\n")]);
1265 let repo = Store::default();
1266 let git = Reader::new(&repo, Some(&fork));
1267 assert!(block_on(git.read_tree(&h.tree_hash)).unwrap().is_some());
1268 assert_eq!(block_on(git.log("h", 1)).unwrap().len(), 1);
1269 let made = vec![(ObjectKind::Blob, b"made\n".to_vec())];
1270 git.keep(&made);
1271 assert_eq!(block_on(git.read_blob(&object_id(ObjectKind::Blob, b"made\n"))).unwrap().as_deref(), Some(&b"made\n"[..]));
1272 }
1273}