#[cfg(test)]
mod tests;
use crate::tool::{AdmittedTool, CompletedExecution, ExecutionContext, OutputLimits, ToolError};
use ic_host_artifacts::artifact::{ArtifactIdentity, Sha256Digest};
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<CompletedExecution>; 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 options: StatusOptions,
pub status_identity: ArtifactIdentity,
pub tool_identity: ArtifactIdentity,
revision_evidence: CompletedExecution,
tree_evidence: CompletedExecution,
status_evidence: CompletedExecution,
}
impl GitObservations {
#[must_use]
pub fn revision(&self) -> &str {
object_id(&self.revision_evidence.stdout)
}
#[must_use]
pub fn tree(&self) -> &str {
object_id(&self.tree_evidence.stdout)
}
#[must_use]
pub const fn revision_evidence(&self) -> &CompletedExecution {
&self.revision_evidence
}
#[must_use]
pub const fn tree_evidence(&self) -> &CompletedExecution {
&self.tree_evidence
}
#[must_use]
pub const fn status_evidence(&self) -> &CompletedExecution {
&self.status_evidence
}
#[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 mut completed = [None, None, None];
for (index, query) in [GitQuery::Revision, GitQuery::Tree, GitQuery::Status]
.into_iter()
.enumerate()
{
let evidence = match git
.run(&arguments(query, options), context, limits)
.and_then(CompletedExecution::require_success)
.and_then(CompletedExecution::require_complete)
{
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,
};
completed[index] = Some(evidence);
if let Some(failure) = failure {
return Err(GitError {
query,
failure,
completed: Box::new(completed),
});
}
}
let [Some(revision), Some(tree), Some(status)] = completed else {
unreachable!("every ordered query completed successfully");
};
let status_identity = ArtifactIdentity {
bytes: status.stdout.len() as u64,
sha256: Sha256Digest::compute(&status.stdout),
};
Ok(GitObservations {
options,
status_identity,
tool_identity: git.identity(),
revision_evidence: revision,
tree_evidence: tree,
status_evidence: status,
})
}
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]) -> &str {
std::str::from_utf8(&bytes[..bytes.len() - 1])
.expect("captured object ID was validated as ASCII plus LF")
}
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
}