use crate::transaction_journal::{Base, Conclusion, RefFormat, SymbolicHead};
use crate::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git, run_git_checked};
use crate::ref_files::{RefFilesLayout, read_symbolic_head};
use crate::repo_read_adapter::invocation_report;
use crate::git_system_config::with_real_git;
use crate::repo_read_layout::read_layout_in;
use crate::sequencer_marker::markers_maybe_present;
use crate::empty_tree::empty_tree_name;
use crate::js_text::trim_javascript;
use crate::iso_time::now_iso;
use crate::config_snapshot::{ConfigSnapshot, read_config_snapshot};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InvocationLayout {
pub symbolic_head: SymbolicHead,
pub target_ref: String,
pub conclusion: Conclusion,
pub repository_root: PathBuf,
pub git_dir: PathBuf,
pub common_dir: PathBuf,
pub real_index_path: PathBuf,
pub object_directory: PathBuf,
pub registry_root: PathBuf,
pub ref_format: RefFormat,
pub object_format: String,
pub empty_tree_oid: String,
pub invoked_at: String,
pub config_snapshot: Option<ConfigSnapshot>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InvocationCapture {
pub base: Base,
pub layout: InvocationLayout,
}
fn decoded_trimmed(output: &GitOutput) -> Result<String, GitFailure> {
let text: &str = std::str::from_utf8(output.stdout.as_slice())
.map_err(|_| { return GitFailure(String::from("Git returned non-UTF-8 output.")); })?;
return Ok(String::from(trim_javascript(text)));
}
fn transaction_request(cwd: &Path, arguments: &[&str]) -> GitRequest {
let mut request: GitRequest = GitRequest::new(cwd, arguments);
request.without_prefix = true;
return request;
}
fn probe(context: &GitContext, cwd: &Path, arguments: &[&str]) -> Result<GitOutput, GitFailure> {
return match run_git(context, &transaction_request(cwd, arguments)) {
Ok(output) => Ok(output),
Err(error) => Err(GitFailure(format!(
"git {} could not start: {error}",
arguments.join(" ")
))),
};
}
fn required(context: &GitContext, cwd: &Path, arguments: &[&str]) -> Result<GitOutput, GitFailure> {
return run_git_checked(context, &transaction_request(cwd, arguments));
}
fn required_line(lines: &[&str], index: usize) -> Result<String, GitFailure> {
let line: &str = lines.get(index).copied().unwrap_or("");
if line.is_empty() {
return Err(GitFailure(String::from("Git returned incomplete repository layout.")));
}
return Ok(String::from(line));
}
fn absolute_reported(cwd: &Path, lines: &[&str], index: usize) -> Result<PathBuf, GitFailure> {
let reported: PathBuf = PathBuf::from(required_line(lines, index)?);
return Ok(if reported.is_absolute() {
reported
} else {
cwd.join(reported)
});
}
fn pseudoref_exists(context: &GitContext, cwd: &Path, name: &str) -> Result<bool, GitFailure> {
return Ok(probe(context, cwd, &["rev-parse", "--verify", "--quiet", name])?.code == Some(0));
}
pub fn resolve_conclusion_kind(
context: &GitContext,
cwd: &Path,
amend: bool,
ref_files: &RefFilesLayout<'_>,
) -> Result<Conclusion, GitFailure> {
if amend {
return Ok(Conclusion::Amend);
}
let names: [&str; 3] = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"];
let kinds: [Conclusion; 3] = [Conclusion::Merge, Conclusion::CherryPick, Conclusion::Revert];
let maybe_present: Vec<bool> = markers_maybe_present(ref_files, &names);
for ((name, kind), maybe) in names.into_iter().zip(kinds).zip(maybe_present) {
if maybe && pseudoref_exists(context, cwd, name)? {
return Ok(kind);
}
}
return Ok(Conclusion::None);
}
pub fn resolve_symbolic_head(
context: &GitContext,
cwd: &Path,
ref_files: &RefFilesLayout<'_>,
) -> Result<SymbolicHead, GitFailure> {
if let Some(read) = read_symbolic_head(ref_files) {
return Ok(read);
}
let output: GitOutput = probe(context, cwd, &["symbolic-ref", "--quiet", "HEAD"])?;
if output.code == Some(1) {
return Ok(SymbolicHead::Detached);
}
if output.code != Some(0) {
return Err(GitFailure(format!(
"git symbolic-ref HEAD failed: {}",
String::from_utf8_lossy(output.stderr.as_slice()).trim()
)));
}
return Ok(SymbolicHead::Branch(decoded_trimmed(&output)?));
}
pub fn resolve_ref_commit(context: &GitContext, cwd: &Path, reference: &str) -> Result<Base, GitFailure> {
let peeled: String = format!("{reference}^{{commit}}");
let output: GitOutput = probe(context, cwd, &["rev-parse", "--verify", "--quiet", peeled.as_str()])?;
if output.code != Some(0) {
return Ok(Base::Unborn);
}
return Ok(Base::Commit(decoded_trimmed(&output)?));
}
pub fn resolve_ref_format(context: &GitContext, cwd: &Path) -> Result<RefFormat, GitFailure> {
return resolve_ref_format_with(context, cwd, None);
}
pub fn resolve_ref_format_with(
context: &GitContext,
cwd: &Path,
snapshot: Option<&ConfigSnapshot>,
) -> Result<RefFormat, GitFailure> {
if snapshot.is_some_and(ConfigSnapshot::proves_files_ref_storage) {
return Ok(RefFormat::Files);
}
let output: GitOutput = probe(context, cwd, &["rev-parse", "--show-ref-format"])?;
let reftable: bool = output.code == Some(0) && decoded_trimmed(&output)? == "reftable";
return Ok(if reftable { RefFormat::Reftable } else { RefFormat::Files });
}
fn resolve_empty_tree(context: &GitContext, cwd: &Path, object_format: &str) -> Result<String, GitFailure> {
if let Some(known) = empty_tree_name(object_format) {
return Ok(String::from(known));
}
let hashed: GitOutput = required(context, cwd, &["hash-object", "-t", "tree", "--stdin"])?;
return decoded_trimmed(&hashed);
}
pub fn capture_invocation_layout(
context: &GitContext,
cwd: &Path,
amend: bool,
) -> Result<InvocationLayout, GitFailure> {
let invoked_at: String = now_iso();
let config_snapshot: Option<ConfigSnapshot> = read_config_snapshot(context, cwd);
let text: String = match read_layout_in(cwd, with_real_git(context.overlay.as_slice(), context.real_git.as_path()).as_slice()).as_ref().and_then(invocation_report) {
Some(read) => read,
None => {
let report: GitOutput = required(
context,
cwd,
&[
"rev-parse",
"--path-format=absolute",
"--show-toplevel",
"--absolute-git-dir",
"--git-common-dir",
"--git-path",
"index",
"--git-path",
"objects",
"--git-path",
"cli-git-transactions",
"--show-object-format",
],
)?;
decoded_trimmed(&report)?
}
};
let lines: Vec<&str> = text.split('\n').collect();
let git_dir: PathBuf = absolute_reported(cwd, &lines, 1)?;
let common_dir: PathBuf = absolute_reported(cwd, &lines, 2)?;
let object_format: String = required_line(&lines, 6)?;
let ref_format: RefFormat = resolve_ref_format_with(context, cwd, config_snapshot.as_ref())?;
let ref_files: RefFilesLayout<'_> = RefFilesLayout {
git_dir: git_dir.as_path(),
common_dir: common_dir.as_path(),
ref_format,
object_format: object_format.as_str(),
};
let symbolic_head: SymbolicHead = resolve_symbolic_head(context, cwd, &ref_files)?;
let target_ref: String = match &symbolic_head {
SymbolicHead::Branch(reference) => reference.clone(),
SymbolicHead::Detached => String::from("HEAD"),
};
let empty_tree_oid: String = resolve_empty_tree(context, cwd, object_format.as_str())?;
let repository_root: PathBuf = std::fs::canonicalize(absolute_reported(cwd, &lines, 0)?)
.map_err(|error| { return GitFailure(format!("repository root is unreadable: {error}")); })?;
return Ok(InvocationLayout {
symbolic_head,
target_ref,
conclusion: resolve_conclusion_kind(context, cwd, amend, &ref_files)?,
repository_root,
git_dir,
common_dir,
real_index_path: absolute_reported(cwd, &lines, 3)?,
object_directory: absolute_reported(cwd, &lines, 4)?,
registry_root: absolute_reported(cwd, &lines, 5)?,
ref_format,
object_format,
empty_tree_oid,
invoked_at,
config_snapshot,
});
}
pub fn capture_invocation_base(
context: &GitContext,
cwd: &Path,
layout: InvocationLayout,
) -> Result<InvocationCapture, GitFailure> {
let base: Base = resolve_ref_commit(context, cwd, layout.target_ref.as_str())?;
return Ok(InvocationCapture { base, layout });
}
pub fn base_revision(capture: &InvocationCapture) -> String {
return match &capture.base {
Base::Commit(oid) => oid.clone(),
Base::Unborn => capture.layout.empty_tree_oid.clone(),
};
}
#[cfg(test)]
#[path = "capture_invocation_tests.rs"]
mod tests;