1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//! What:
//! The merge step of one replay: the tree from `git merge-tree --write-tree` of the prepared change onto the landed tip.
//! Why:
//! A commit that lost the race to another is prepared again over the tip the service names; the merge says whether the two
//! changes meet cleanly and, when they do not, which paths conflict, in byte order.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await mergeReplayTree({ gitPath, shadowPath, mergeBase, current, prepared, worktreeRoot });
//! ```
/// Shadow runs and the raw runner for a merge whose exit status matters.
use super::shadow_git::shadow_request;
use super::transaction_git::{GitContext, GitFailure, GitRequest, run_git};
/// Filesystem paths.
use std::path::Path;
/// `git merge-tree` exit status reporting conflicts; a tree ID still prints.
const MERGE_CONFLICT_EXIT: i32 = 1;
/// What:
/// The outcome of merging the prepared change onto the current target.
/// Why:
/// A clean merge yields a tree to rebuild a commit on; a conflict yields the paths that name the winning commit.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ReplayMerge = { kind: 'merged'; treeOid } | { kind: 'conflict'; paths };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ReplayMerge {
/// The change applies cleanly; the merged tree is in the shadow store.
Merged {
/// Merged tree.
tree_oid: String,
},
/// The change conflicts with the target.
Conflict {
/// Conflicting paths in Git path byte order, each once, as the exact bytes Git reported.
paths: Vec<Vec<u8>>,
},
}
/// What:
/// A merge or rebuild failure:
/// a Git command failed, or the commit could not be rebuilt.
/// Why:
/// Replay reports each as `transaction-failed`, never as a conflict.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// throw new CommitTransactionGitError(message);
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ReplayMergeError {
/// A Git command failed, or `merge-tree` exited above `1`.
Git(GitFailure),
/// The prepared commit could not be rebuilt.
Rebuild(String),
}
/// What:
/// Merges the prepared change onto the current target in the shadow:
/// the tree when clean, or the conflicted paths in byte order.
/// Why:
/// `git merge-tree --write-tree` gives the answer with exit `1` for a conflict and a tree ID in both cases;
/// any other non-zero exit is a failure.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await mergeReplayTree({ gitPath, shadowPath, mergeBase, current, prepared, worktreeRoot });
/// ```
pub fn merge_replay_tree(
context: &GitContext,
shadow: &Path,
worktree_root: &Path,
merge_base: &str,
current: &str,
prepared: &str,
) -> Result<ReplayMerge, ReplayMergeError> {
let merge_base_argument: String = format!("--merge-base={merge_base}");
let request: GitRequest = shadow_request(
shadow,
worktree_root,
&[
"merge-tree",
"--write-tree",
"--name-only",
"-z",
merge_base_argument.as_str(),
current,
prepared,
],
);
let output = run_git(context, &request).map_err(|error| {
return ReplayMergeError::Git(GitFailure(format!("git merge-tree could not start: {error}")));
})?;
let code: Option<i32> = output.code;
if code != Some(0) && code != Some(MERGE_CONFLICT_EXIT) {
return Err(ReplayMergeError::Git(GitFailure(format!(
"git merge-tree could not replay {prepared} onto {current} (exit {code:?}): {}",
String::from_utf8_lossy(&output.stderr).trim()
))));
}
// The first field is the tree; on a conflict the conflicted paths follow up to an empty record.
// The paths stay bytes: a name that is not UTF-8 is reported with its exact bytes.
let mut fields = output.stdout.split(|byte| { return *byte == 0; });
let tree_oid: String = String::from_utf8_lossy(fields.next().unwrap_or_default()).into_owned();
if code == Some(0) {
return Ok(ReplayMerge::Merged { tree_oid });
}
let mut paths: Vec<Vec<u8>> = Vec::new();
for field in fields {
if field.is_empty() {
break;
}
if !paths.iter().any(|known| { return known.as_slice() == field; }) {
paths.push(field.to_vec());
}
}
paths.sort();
return Ok(ReplayMerge::Conflict { paths });
}
#[cfg(test)]
#[path = "replay_merge_tests.rs"]
mod tests;