1use super::path_bytes::os_from_path_bytes;
18use super::replay_object::{RawCommit, dropped_replay_headers, identity_environment, rewrite_commit_object};
19use super::replay_shadow::run_shadow_checked;
21use super::private_storage::write_private_file;
22use super::shadow_git::shadow_request;
23use super::transaction_git::{GitContext, GitFailure, GitRequest, run_git, run_git_checked};
24use std::ffi::OsString;
26use std::path::{Path, PathBuf};
28
29const MERGE_CONFLICT_EXIT: i32 = 1;
31
32#[derive(Clone, Debug, Eq, PartialEq)]
42pub enum ReplayMerge {
43 Merged {
45 tree_oid: String,
47 },
48 Conflict {
50 paths: Vec<Vec<u8>>,
52 },
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
66pub enum ReplayMergeError {
67 Git(GitFailure),
69 Rebuild(String),
71}
72
73pub fn replay_merge_base(
84 context: &GitContext,
85 shadow: &Path,
86 cwd: &Path,
87 base: Option<&str>,
88 empty_tree_oid: &str,
89) -> Result<String, ReplayMergeError> {
90 if let Some(commit) = base {
91 return Ok(String::from(commit));
92 }
93 let message: String = format!(
94 "tree {empty_tree_oid}\nauthor cli-git <cli-git@localhost> 0 +0000\ncommitter cli-git <cli-git@localhost> 0 +0000\n\ncli-git replay base for an unborn branch\n"
95 );
96 let written = run_shadow_checked(
97 context,
98 shadow,
99 cwd,
100 &["hash-object", "-t", "commit", "-w", "--stdin"],
101 Some(message.as_bytes()),
102 None,
103 &[],
104 )
105 .map_err(ReplayMergeError::Git)?;
106 return Ok(String::from_utf8_lossy(&written.stdout).trim().to_string());
107}
108
109pub fn merge_replay_tree(
121 context: &GitContext,
122 shadow: &Path,
123 worktree_root: &Path,
124 merge_base: &str,
125 current: &str,
126 prepared: &str,
127) -> Result<ReplayMerge, ReplayMergeError> {
128 let merge_base_argument: String = format!("--merge-base={merge_base}");
129 let request: GitRequest = shadow_request(
130 shadow,
131 worktree_root,
132 &[
133 "merge-tree",
134 "--write-tree",
135 "--name-only",
136 "-z",
137 merge_base_argument.as_str(),
138 current,
139 prepared,
140 ],
141 );
142 let output = run_git(context, &request).map_err(|error| {
143 return ReplayMergeError::Git(GitFailure(format!("git merge-tree could not start: {error}")));
144 })?;
145 let code: Option<i32> = output.code;
146 if code != Some(0) && code != Some(MERGE_CONFLICT_EXIT) {
147 return Err(ReplayMergeError::Git(GitFailure(format!(
148 "git merge-tree could not replay {prepared} onto {current} (exit {code:?}): {}",
149 String::from_utf8_lossy(&output.stderr).trim()
150 ))));
151 }
152 let mut fields = output.stdout.split(|byte| { return *byte == 0; });
155 let tree_oid: String = String::from_utf8_lossy(fields.next().unwrap_or_default()).into_owned();
156 if code == Some(0) {
157 return Ok(ReplayMerge::Merged { tree_oid });
158 }
159 let mut paths: Vec<Vec<u8>> = Vec::new();
160 for field in fields {
161 if field.is_empty() {
162 break;
163 }
164 if !paths.iter().any(|known| { return known.as_slice() == field; }) {
165 paths.push(field.to_vec());
166 }
167 }
168 paths.sort();
169 return Ok(ReplayMerge::Conflict { paths });
170}
171
172pub fn first_commit_touching(
183 context: &GitContext,
184 cwd: &Path,
185 base: Option<&str>,
186 current: &str,
187 paths: &[Vec<u8>],
188) -> Result<String, ReplayMergeError> {
189 let exclusion: String = base.map(|oid| { return format!("^{oid}"); }).unwrap_or_default();
190 let pathspecs: Vec<OsString> = paths
191 .iter()
192 .map(|path| {
193 let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
194 pathspec.extend_from_slice(path.as_slice());
195 return os_from_path_bytes(pathspec.as_slice());
196 })
197 .collect();
198 let mut arguments: Vec<&str> = vec!["rev-list", "--reverse", current];
200 if base.is_some() {
201 arguments.push(exclusion.as_str());
202 }
203 arguments.push("--");
204 let mut request: GitRequest = GitRequest::new(cwd, &arguments);
205 request.arguments.extend(pathspecs);
206 request.environment = vec![(OsString::from("GIT_LITERAL_PATHSPECS"), OsString::from("0"))];
207 let listed = run_git_checked(context, &request).map_err(ReplayMergeError::Git)?;
208 let text: String = String::from_utf8_lossy(&listed.stdout).into_owned();
210 return Ok(text
211 .lines()
212 .find(|line| { return !line.is_empty(); })
213 .map(String::from)
214 .unwrap_or_else(|| { return String::from(current); }));
215}
216
217#[derive(Clone, Debug, Eq, PartialEq)]
227pub struct ReplayedCommit {
228 pub oid: String,
230 pub dropped_headers: Vec<String>,
232}
233
234pub fn write_replayed_commit(
244 context: &GitContext,
245 shadow: &Path,
246 cwd: &Path,
247 directory: &Path,
248 attempt: u64,
249 commit: &RawCommit,
250 tree_oid: &str,
251 parent_oid: &str,
252 signing_key: Option<&str>,
253 global_arguments: &[String],
254) -> Result<ReplayedCommit, ReplayMergeError> {
255 if !commit.signed {
256 let rewritten: Vec<u8> = rewrite_commit_object(commit, tree_oid, parent_oid);
257 let written = run_shadow_checked(
258 context,
259 shadow,
260 cwd,
261 &["hash-object", "-t", "commit", "-w", "--stdin"],
262 Some(&rewritten),
263 None,
264 &[],
265 )
266 .map_err(ReplayMergeError::Git)?;
267 return Ok(ReplayedCommit {
268 oid: String::from_utf8_lossy(&written.stdout).trim().to_string(),
269 dropped_headers: Vec::new(),
270 });
271 }
272 let message_path: PathBuf = directory.join(format!("replay-message-{attempt}"));
273 write_private_file(message_path.as_path(), &commit.message)
274 .map_err(|error| { return ReplayMergeError::Rebuild(format!("message file {}: {error}", message_path.display())); })?;
275 let encoding: String = commit.encoding.clone().unwrap_or_else(|| { return String::from("utf8"); });
276 let signed_key: String = format!("-S{}", signing_key.unwrap_or(""));
277 let encoding_option: String = format!("i18n.commitEncoding={encoding}");
278 let message_text: String = message_path.to_string_lossy().into_owned();
279 let mut arguments: Vec<&str> = global_arguments.iter().map(String::as_str).collect();
281 arguments.extend([
282 "-c",
283 encoding_option.as_str(),
284 "commit-tree",
285 tree_oid,
286 "-p",
287 parent_oid,
288 signed_key.as_str(),
289 "-F",
290 message_text.as_str(),
291 ]);
292 let mut environment: Vec<(OsString, OsString)> = Vec::new();
293 for role in ["author", "committer"] {
294 let identity = identity_environment(commit, role).map_err(|error| { return ReplayMergeError::Rebuild(error.0); })?;
295 for (name, value) in identity {
296 environment.push((OsString::from(name), OsString::from(value)));
297 }
298 }
299 let signed = run_shadow_checked(context, shadow, cwd, &arguments, None, None, &environment)
300 .map_err(ReplayMergeError::Git)?;
301 return Ok(ReplayedCommit {
302 oid: String::from_utf8_lossy(&signed.stdout).trim().to_string(),
303 dropped_headers: dropped_replay_headers(commit),
304 });
305}
306
307#[cfg(test)]
308#[path = "replay_merge_tests.rs"]
309mod tests;