use crate::auto_push_single_flight::{
PushGit, SingleFlightError, git_command, git_output, run_query,
};
use crate::diagnostic_log::debug;
use crate::merge_second_pass::{GitReply, GitRunner, Merged, conflicts_of_merge_tree};
use crate::merge_second_pass_tree::{AttributeProbe, attribute_tree_variables, report, resolve_tree};
use crate::path_bytes::path_message_text;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use crate::auto_push_reconcile_basics_and_limits::{TAG, argv};
pub(crate) struct PushRunner<'a> {
pub(crate) git: &'a PushGit,
}
impl GitRunner for PushRunner<'_> {
fn run(&self, arguments: &[&str], input: Option<&[u8]>, index_file: Option<&Path>) -> Result<GitReply, String> {
let mut command = git_command(self.git, &argv(arguments));
if let Some(index) = index_file {
command.env("GIT_INDEX_FILE", index);
}
command.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = match command.spawn() {
Ok(started) => started,
Err(error) => return Err(format!("git {} could not start: {error}", arguments.join(" "))),
};
let pipe = child.stdin.take();
let bytes: Vec<u8> = input.map(<[u8]>::to_vec).unwrap_or_default();
let writer = match crate::scoped_start::spawn_named("cli-git-second-pass-input", move || {
if let Some(mut open) = pipe
&& let Err(error) = open.write_all(bytes.as_slice())
{
debug(TAG, &format!("the input of a second-pass command was not taken: {error}"));
}
}) {
Ok(started) => started,
Err(error) => {
let _stopped = child.kill();
let _reaped = child.wait();
return Err(format!("no thread could be started to give git {} its input: {error}", arguments.join(" ")));
}
};
let output = match child.wait_with_output() {
Ok(finished) => finished,
Err(error) => return Err(format!("git {} did not finish: {error}", arguments.join(" "))),
};
if writer.join().is_err() {
return Err(format!("the input thread of git {} ended abnormally", arguments.join(" ")));
}
return Ok(GitReply { code: output.status.code(), stdout: output.stdout, stderr: output.stderr });
}
}
pub(crate) struct CommittedMerge {
pub(crate) oid: String,
pub(crate) merged: Vec<Merged>,
}
pub(crate) fn commit_merge(
git: &PushGit,
local: &str,
remote: &str,
message: &str,
) -> Result<Result<CommittedMerge, Vec<String>>, SingleFlightError> {
let probe = AttributeProbe { directory: git.cwd.as_path(), global_prefix: &[], variables: git.environment.as_slice(), runner: &PushRunner { git } };
let attributed = PushGit {
real_git: git.real_git.clone(),
cwd: git.cwd.clone(),
environment: git.environment.iter().cloned().chain(attribute_tree_variables(local, &probe)).collect(),
lfs: git.lfs,
};
let runner = PushRunner { git: &attributed };
let merge_arguments = [
"merge-tree",
"--write-tree",
"--no-messages",
"-z",
"--allow-unrelated-histories",
local,
remote,
];
let merged = match runner.run(&merge_arguments, None, None) {
Ok(reply) => reply,
Err(reason) => {
return Err(SingleFlightError::Git {
arguments: format!("merge-tree --write-tree {local} {remote}"),
code: None,
stderr: reason,
});
}
};
let (mut tree, conflicts) = conflicts_of_merge_tree(&merged.stdout);
let mut second_pass_files: Vec<Merged> = Vec::new();
if merged.code == Some(1) {
let common = common_dir_of(git)?;
let top = PathBuf::from(git_output(git, &argv(&["rev-parse", "--show-toplevel"]))?);
let from_top = PushGit {
real_git: attributed.real_git.clone(),
cwd: top,
environment: attributed.environment.clone(),
lfs: attributed.lfs,
};
match resolve_tree(&PushRunner { git: &from_top }, common.as_path(), &tree, &conflicts) {
Some(resolved) => {
tree = resolved.tree;
second_pass_files = resolved.merged;
}
None => {
let mut paths: Vec<String> = Vec::new();
for conflict in &conflicts {
paths.push(path_message_text(&conflict.path));
}
return Ok(Err(paths));
}
}
} else if merged.code != Some(0) {
return Err(SingleFlightError::Git {
arguments: format!("merge-tree --write-tree {local} {remote}"),
code: merged.code,
stderr: String::from_utf8_lossy(&merged.stderr).into_owned(),
});
}
let mut full_message = message.to_owned();
if !second_pass_files.is_empty() {
full_message.push_str("\n\n");
full_message.push_str(report("", &second_pass_files).join("\n").as_str());
}
let mut commit_tree = vec!["commit-tree"];
if signs_commits(git)? {
commit_tree.push("-S");
}
commit_tree.extend(["-p", local, "-p", remote, "-m", &full_message, &tree]);
let oid = git_output(git, &argv(&commit_tree))?;
return Ok(Ok(CommittedMerge { oid, merged: second_pass_files }));
}
pub(crate) fn common_dir_of(git: &PushGit) -> Result<PathBuf, SingleFlightError> {
let answered = git_output(git, &argv(&["rev-parse", "--git-common-dir"]))?;
return Ok(git.cwd.join(answered));
}
pub(crate) fn signs_commits(git: &PushGit) -> Result<bool, SingleFlightError> {
let asked = run_query(
git,
&argv(&["config", "--type=bool", "--get", "commit.gpgsign"]),
)?;
if asked.code == Some(1) {
return Ok(false);
}
if asked.code != Some(0) {
return Err(SingleFlightError::Git {
arguments: String::from("config --type=bool --get commit.gpgsign"),
code: asked.code,
stderr: String::from_utf8_lossy(&asked.stderr).into_owned(),
});
}
return Ok(asked.stdout.trim_ascii() == b"true");
}