use crate::auto_push_dirty_paths::status_paths;
use crate::merge_isolation_message::fetch_head_merge_heads;
use crate::transaction_git::{GitContext, GitOutput, GitRequest, run_git};
use crate::merge_isolation_run_types::{Bail, MergeConfig};
use std::path::Path;
fn is_nul_byte(byte: &u8) -> bool {
return *byte == 0;
}
pub fn parse_maybe_bool(text: &str) -> Option<bool> {
match text.to_ascii_lowercase().as_str() {
"true" | "yes" | "on" | "1" => return Some(true),
"false" | "no" | "off" | "0" | "" => return Some(false),
_ => return None,
}
}
pub fn editor_would_open(auto_edit: Option<&str>, same_terminal: bool) -> Option<bool> {
match auto_edit {
Some(text) => return parse_maybe_bool(text),
None => return Some(same_terminal),
}
}
#[cfg(unix)]
fn device_and_inode(descriptor: std::os::fd::BorrowedFd<'_>) -> Option<(u64, u64)> {
use std::os::unix::fs::MetadataExt;
let file = std::fs::File::from(descriptor.try_clone_to_owned().ok()?);
let metadata = file.metadata().ok()?;
return Some((metadata.dev(), metadata.ino()));
}
pub fn stdin_and_stdout_are_one_terminal() -> bool {
use std::io::IsTerminal;
if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
return false;
}
#[cfg(unix)]
{
use std::os::fd::AsFd;
return device_and_inode(std::io::stdin().as_fd()) == device_and_inode(std::io::stdout().as_fd());
}
#[cfg(not(unix))]
{
return true;
}
}
pub struct Session {
pub context: GitContext,
}
impl Session {
pub fn run(&self, arguments: &[&str]) -> Result<GitOutput, String> {
return match run_git(&self.context, &GitRequest::new(Path::new("."), arguments)) {
Ok(output) => Ok(output),
Err(error) => Err(format!("git {} could not start: {error}", arguments.join(" "))),
};
}
pub fn run_with_input(&self, arguments: &[&str], input: Vec<u8>) -> Result<GitOutput, String> {
let mut request: GitRequest = GitRequest::new(Path::new("."), arguments);
request.input = Some(input);
return match run_git(&self.context, &request) {
Ok(output) => Ok(output),
Err(error) => Err(format!("git {} could not start: {error}", arguments.join(" "))),
};
}
pub fn text(&self, arguments: &[&str]) -> Result<String, String> {
let output: GitOutput = self.run(arguments)?;
if !output.succeeded() {
return Err(format!("git {} failed: {}", arguments.join(" "), output.error_text()));
}
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned());
}
}
pub fn read_config(session: &Session) -> Result<MergeConfig, String> {
let output: GitOutput = session.run(&[
"config",
"--null",
"--get-regexp",
"^(merge\\.(ff|stat|verifysignatures|autostash)|branch\\..*\\.mergeoptions|commit\\.gpgsign)$",
])?;
if output.code != Some(0) && output.code != Some(1) {
return Err(format!("git config failed: {}", output.error_text()));
}
let mut config: MergeConfig = MergeConfig { stat: true, ..MergeConfig::default() };
for entry in output.stdout.split(is_nul_byte) {
if entry.is_empty() {
continue;
}
let text: String = String::from_utf8_lossy(entry).into_owned();
let (key, value) = text.split_once('\n').unwrap_or((text.as_str(), "true"));
match key {
"merge.ff" if value.eq_ignore_ascii_case("only") => config.fast_forward_only = true,
"merge.ff" => config.fast_forward = parse_maybe_bool(value),
"merge.stat" => config.stat = parse_maybe_bool(value).unwrap_or(true),
"commit.gpgsign" => config.sign_commits = parse_maybe_bool(value).unwrap_or(false),
"merge.verifysignatures" | "merge.autostash" => {
config.deferred |= parse_maybe_bool(value).unwrap_or(true);
}
other if other.starts_with("branch.") => config.deferred = true,
_ => {}
}
}
return Ok(config);
}
pub fn lines_of(output: &GitOutput) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();
for line in String::from_utf8_lossy(&output.stdout).lines() {
let trimmed: &str = line.trim();
if !trimmed.is_empty() {
lines.push(String::from(trimmed));
}
}
return lines;
}
pub fn unsaved_paths(session: &Session) -> Result<Vec<Vec<u8>>, Bail> {
let status: GitOutput = session.run(&[
"--no-optional-locks",
"status",
"--porcelain=v1",
"-z",
"--untracked-files=no",
])?;
if !status.succeeded() {
return Err(Bail::from(format!("git status failed: {}", status.error_text())));
}
let mut entries = status.stdout.split(is_nul_byte);
while let Some(entry) = entries.next() {
if entry.len() >= 2 && (entry[0] == b'U' || entry[1] == b'U' || entry[..2] == *b"AA" || entry[..2] == *b"DD") {
return Err(Bail::Forward(None));
}
if entry.len() >= 2 && (matches!(entry[0], b'R' | b'C') || matches!(entry[1], b'R' | b'C')) {
entries.next();
}
}
return Ok(status_paths(status.stdout.as_slice()));
}
pub fn read_fetch_head(git_dir: &Path) -> Result<(String, String), Bail> {
let Ok(text) = std::fs::read_to_string(git_dir.join("FETCH_HEAD")) else {
return Err(Bail::Forward(None));
};
let mut heads: Vec<String> = fetch_head_merge_heads(text.as_str());
if heads.len() != 1 {
return Err(Bail::Forward(None));
}
return Ok((heads.remove(0), text));
}