#[cfg(test)]
mod tests;
use crate::{
artifact::{ArtifactIdentity, Sha256Digest},
tool::{AdmittedTool, ExecutionContext, ExecutionEvidence, OutputLimits, ToolError},
};
use std::{ffi::OsString, fmt};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UntrackedFiles {
No,
Normal,
All,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IgnoreSubmodules {
None,
Untracked,
Dirty,
All,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StatusOptions {
pub untracked: UntrackedFiles,
pub ignore_submodules: IgnoreSubmodules,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GitQuery {
Revision,
Tree,
Status,
}
#[derive(Debug)]
pub enum GitFailure {
Tool(Box<ToolError>),
InvalidObjectId,
UnterminatedStatus,
}
#[derive(Debug)]
pub struct GitError {
pub query: GitQuery,
pub failure: GitFailure,
pub completed: Box<[Option<ExecutionEvidence>; 3]>,
}
impl fmt::Display for GitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Git {:?} observation failed", self.query)
}
}
impl std::error::Error for GitError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.failure {
GitFailure::Tool(source) => Some(source.as_ref()),
GitFailure::InvalidObjectId | GitFailure::UnterminatedStatus => None,
}
}
}
#[derive(Debug)]
pub struct GitObservations {
pub revision: String,
pub tree: String,
pub options: StatusOptions,
pub status_identity: ArtifactIdentity,
pub tool_identity: ArtifactIdentity,
pub revision_evidence: ExecutionEvidence,
pub tree_evidence: ExecutionEvidence,
pub status_evidence: ExecutionEvidence,
}
impl GitObservations {
#[must_use]
pub const fn is_dirty(&self) -> bool {
self.status_identity.bytes != 0
}
}
pub fn capture_git(
git: &AdmittedTool,
context: &ExecutionContext<'_>,
options: StatusOptions,
limits: OutputLimits,
) -> Result<GitObservations, GitError> {
let revision = observe(git, context, options, limits, GitQuery::Revision)?;
let tree = match observe(git, context, options, limits, GitQuery::Tree) {
Ok(evidence) => evidence,
Err(mut error) => {
error.completed[0] = Some(revision);
return Err(error);
}
};
let status = match observe(git, context, options, limits, GitQuery::Status) {
Ok(evidence) => evidence,
Err(mut error) => {
error.completed[0] = Some(revision);
error.completed[1] = Some(tree);
return Err(error);
}
};
let status_identity = ArtifactIdentity {
bytes: status.stdout.len() as u64,
sha256: Sha256Digest::compute(&status.stdout),
};
let revision_id = object_id(&revision.stdout);
let tree_id = object_id(&tree.stdout);
Ok(GitObservations {
revision: revision_id,
tree: tree_id,
options,
status_identity,
tool_identity: git.identity(),
revision_evidence: revision,
tree_evidence: tree,
status_evidence: status,
})
}
fn observe(
git: &AdmittedTool,
context: &ExecutionContext<'_>,
options: StatusOptions,
limits: OutputLimits,
query: GitQuery,
) -> Result<ExecutionEvidence, GitError> {
let mut completed = [None, None, None];
let index = match query {
GitQuery::Revision => 0,
GitQuery::Tree => 1,
GitQuery::Status => 2,
};
let evidence = match git.run(&arguments(query, options), context, limits) {
Ok(evidence) => evidence,
Err(source) => {
return Err(GitError {
query,
failure: GitFailure::Tool(Box::new(source)),
completed: Box::new(completed),
});
}
};
let failure = match query {
GitQuery::Revision | GitQuery::Tree if !valid_object_id(&evidence.stdout) => {
Some(GitFailure::InvalidObjectId)
}
GitQuery::Status if !evidence.stdout.is_empty() && evidence.stdout.last() != Some(&0) => {
Some(GitFailure::UnterminatedStatus)
}
_ => None,
};
if let Some(failure) = failure {
completed[index] = Some(evidence);
return Err(GitError {
query,
failure,
completed: Box::new(completed),
});
}
Ok(evidence)
}
fn valid_object_id(bytes: &[u8]) -> bool {
matches!(bytes.len(), 41 | 65)
&& bytes.last() == Some(&b'\n')
&& bytes[..bytes.len() - 1]
.iter()
.all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
}
fn object_id(bytes: &[u8]) -> String {
bytes[..bytes.len() - 1]
.iter()
.map(|&byte| char::from(byte))
.collect()
}
fn arguments(query: GitQuery, options: StatusOptions) -> Vec<OsString> {
let mut arguments = vec![
"--no-optional-locks".into(),
"-c".into(),
"core.fsmonitor=false".into(),
];
match query {
GitQuery::Revision => {
arguments.extend(["rev-parse".into(), "--verify".into(), "HEAD".into()]);
}
GitQuery::Tree => {
arguments.extend(["rev-parse".into(), "--verify".into(), "HEAD^{tree}".into()]);
}
GitQuery::Status => {
let untracked = match options.untracked {
UntrackedFiles::No => "no",
UntrackedFiles::Normal => "normal",
UntrackedFiles::All => "all",
};
let submodules = match options.ignore_submodules {
IgnoreSubmodules::None => "none",
IgnoreSubmodules::Untracked => "untracked",
IgnoreSubmodules::Dirty => "dirty",
IgnoreSubmodules::All => "all",
};
arguments.extend([
"status".into(),
"--porcelain=v1".into(),
"-z".into(),
format!("--untracked-files={untracked}").into(),
format!("--ignore-submodules={submodules}").into(),
]);
}
}
arguments
}