1use super::replay_object::{RawCommit, dropped_replay_headers, identity_environment, rewrite_commit_object};
18use super::replay_shadow::run_shadow_checked;
20use super::private_storage::write_private_file;
21use super::shadow_git::shadow_request;
22use super::transaction_git::{GitContext, GitFailure, GitRequest, run_git, run_git_checked};
23use std::ffi::OsString;
25use std::path::{Path, PathBuf};
27
28const MERGE_CONFLICT_EXIT: i32 = 1;
30
31#[derive(Clone, Debug, Eq, PartialEq)]
41pub enum ReplayMerge {
42 Merged {
44 tree_oid: String,
46 },
47 Conflict {
49 paths: Vec<String>,
51 },
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
65pub enum ReplayMergeError {
66 Git(GitFailure),
68 Encoding(String),
70 Rebuild(String),
72}
73
74pub fn replay_merge_base(
85 context: &GitContext,
86 shadow: &Path,
87 cwd: &Path,
88 base: Option<&str>,
89 empty_tree_oid: &str,
90) -> Result<String, ReplayMergeError> {
91 if let Some(commit) = base {
92 return Ok(String::from(commit));
93 }
94 let message: String = format!(
95 "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"
96 );
97 let written = run_shadow_checked(
98 context,
99 shadow,
100 cwd,
101 &["hash-object", "-t", "commit", "-w", "--stdin"],
102 Some(message.as_bytes()),
103 None,
104 &[],
105 )
106 .map_err(ReplayMergeError::Git)?;
107 return Ok(String::from_utf8_lossy(&written.stdout).trim().to_string());
108}
109
110fn strict_utf8(bytes: &[u8]) -> Result<&str, ReplayMergeError> {
121 return std::str::from_utf8(bytes)
122 .map_err(|error| { return ReplayMergeError::Encoding(format!("merge output is not UTF-8: {error}")); });
123}
124
125pub fn merge_replay_tree(
137 context: &GitContext,
138 shadow: &Path,
139 worktree_root: &Path,
140 merge_base: &str,
141 current: &str,
142 prepared: &str,
143) -> Result<ReplayMerge, ReplayMergeError> {
144 let merge_base_argument: String = format!("--merge-base={merge_base}");
145 let request: GitRequest = shadow_request(
146 shadow,
147 worktree_root,
148 &[
149 "merge-tree",
150 "--write-tree",
151 "--name-only",
152 "-z",
153 merge_base_argument.as_str(),
154 current,
155 prepared,
156 ],
157 );
158 let output = run_git(context, &request).map_err(|error| {
159 return ReplayMergeError::Git(GitFailure(format!("git merge-tree could not start: {error}")));
160 })?;
161 let code: Option<i32> = output.code;
162 if code != Some(0) && code != Some(MERGE_CONFLICT_EXIT) {
163 return Err(ReplayMergeError::Git(GitFailure(format!(
164 "git merge-tree could not replay {prepared} onto {current} (exit {code:?}): {}",
165 String::from_utf8_lossy(&output.stderr).trim()
166 ))));
167 }
168 let text: &str = strict_utf8(&output.stdout)?;
169 let mut fields = text.split('\0');
171 let tree_oid: String = fields.next().unwrap_or_default().to_string();
172 if code == Some(0) {
173 return Ok(ReplayMerge::Merged { tree_oid });
174 }
175 let mut paths: Vec<String> = Vec::new();
176 for field in fields {
177 if field.is_empty() {
178 break;
179 }
180 if !paths.iter().any(|known| { return known == field; }) {
181 paths.push(String::from(field));
182 }
183 }
184 paths.sort_by(|left, right| { return left.as_bytes().cmp(right.as_bytes()); });
185 return Ok(ReplayMerge::Conflict { paths });
186}
187
188pub fn first_commit_touching(
199 context: &GitContext,
200 cwd: &Path,
201 base: Option<&str>,
202 current: &str,
203 paths: &[String],
204) -> Result<String, ReplayMergeError> {
205 let exclusion: String = base.map(|oid| { return format!("^{oid}"); }).unwrap_or_default();
206 let pathspecs: Vec<String> = paths.iter().map(|path| { return format!(":(top,literal){path}"); }).collect();
207 let mut arguments: Vec<&str> = vec!["rev-list", "--reverse", current];
209 if base.is_some() {
210 arguments.push(exclusion.as_str());
211 }
212 arguments.push("--");
213 arguments.extend(pathspecs.iter().map(String::as_str));
214 let mut request: GitRequest = GitRequest::new(cwd, &arguments);
215 request.environment = vec![(OsString::from("GIT_LITERAL_PATHSPECS"), OsString::from("0"))];
216 let listed = run_git_checked(context, &request).map_err(ReplayMergeError::Git)?;
217 let text: &str = strict_utf8(&listed.stdout)?;
218 return Ok(text
219 .lines()
220 .find(|line| { return !line.is_empty(); })
221 .map(String::from)
222 .unwrap_or_else(|| { return String::from(current); }));
223}
224
225#[derive(Clone, Debug, Eq, PartialEq)]
235pub struct ReplayedCommit {
236 pub oid: String,
238 pub dropped_headers: Vec<String>,
240}
241
242pub fn write_replayed_commit(
252 context: &GitContext,
253 shadow: &Path,
254 cwd: &Path,
255 directory: &Path,
256 attempt: u64,
257 commit: &RawCommit,
258 tree_oid: &str,
259 parent_oid: &str,
260 signing_key: Option<&str>,
261 global_arguments: &[String],
262) -> Result<ReplayedCommit, ReplayMergeError> {
263 if !commit.signed {
264 let rewritten: Vec<u8> = rewrite_commit_object(commit, tree_oid, parent_oid);
265 let written = run_shadow_checked(
266 context,
267 shadow,
268 cwd,
269 &["hash-object", "-t", "commit", "-w", "--stdin"],
270 Some(&rewritten),
271 None,
272 &[],
273 )
274 .map_err(ReplayMergeError::Git)?;
275 return Ok(ReplayedCommit {
276 oid: String::from_utf8_lossy(&written.stdout).trim().to_string(),
277 dropped_headers: Vec::new(),
278 });
279 }
280 let message_path: PathBuf = directory.join(format!("replay-message-{attempt}"));
281 write_private_file(message_path.as_path(), &commit.message)
282 .map_err(|error| { return ReplayMergeError::Rebuild(format!("message file {}: {error}", message_path.display())); })?;
283 let encoding: String = commit.encoding.clone().unwrap_or_else(|| { return String::from("utf8"); });
284 let signed_key: String = format!("-S{}", signing_key.unwrap_or(""));
285 let encoding_option: String = format!("i18n.commitEncoding={encoding}");
286 let message_text: String = message_path.to_string_lossy().into_owned();
287 let mut arguments: Vec<&str> = global_arguments.iter().map(String::as_str).collect();
289 arguments.extend([
290 "-c",
291 encoding_option.as_str(),
292 "commit-tree",
293 tree_oid,
294 "-p",
295 parent_oid,
296 signed_key.as_str(),
297 "-F",
298 message_text.as_str(),
299 ]);
300 let mut environment: Vec<(OsString, OsString)> = Vec::new();
301 for role in ["author", "committer"] {
302 let identity = identity_environment(commit, role).map_err(|error| { return ReplayMergeError::Rebuild(error.0); })?;
303 for (name, value) in identity {
304 environment.push((OsString::from(name), OsString::from(value)));
305 }
306 }
307 let signed = run_shadow_checked(context, shadow, cwd, &arguments, None, None, &environment)
308 .map_err(ReplayMergeError::Git)?;
309 return Ok(ReplayedCommit {
310 oid: String::from_utf8_lossy(&signed.stdout).trim().to_string(),
311 dropped_headers: dropped_replay_headers(commit),
312 });
313}
314
315#[cfg(test)]
316#[path = "replay_merge_tests.rs"]
317mod tests;