ic_host_process/provenance/
mod.rs1#[cfg(test)]
8mod tests;
9
10use crate::tool::{AdmittedTool, CompletedExecution, ExecutionContext, OutputLimits, ToolError};
11use ic_host_artifacts::artifact::{ArtifactIdentity, Sha256Digest};
12use std::{ffi::OsString, fmt};
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum UntrackedFiles {
17 No,
19 Normal,
21 All,
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum IgnoreSubmodules {
28 None,
30 Untracked,
32 Dirty,
34 All,
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub struct StatusOptions {
41 pub untracked: UntrackedFiles,
43 pub ignore_submodules: IgnoreSubmodules,
45}
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum GitQuery {
50 Revision,
52 Tree,
54 Status,
56}
57
58#[derive(Debug)]
60pub enum GitFailure {
61 Tool(Box<ToolError>),
63 InvalidObjectId,
65 UnterminatedStatus,
67}
68
69#[derive(Debug)]
71pub struct GitError {
72 pub query: GitQuery,
74 pub failure: GitFailure,
76 pub completed: Box<[Option<CompletedExecution>; 3]>,
79}
80
81impl fmt::Display for GitError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 write!(f, "Git {:?} observation failed", self.query)
84 }
85}
86
87impl std::error::Error for GitError {
88 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
89 match &self.failure {
90 GitFailure::Tool(source) => Some(source.as_ref()),
91 GitFailure::InvalidObjectId | GitFailure::UnterminatedStatus => None,
92 }
93 }
94}
95
96#[derive(Debug)]
98pub struct GitObservations {
99 pub revision: String,
101 pub tree: String,
103 pub options: StatusOptions,
105 pub status_identity: ArtifactIdentity,
107 pub tool_identity: ArtifactIdentity,
109 pub revision_evidence: CompletedExecution,
111 pub tree_evidence: CompletedExecution,
113 pub status_evidence: CompletedExecution,
115}
116
117impl GitObservations {
118 #[must_use]
121 pub const fn is_dirty(&self) -> bool {
122 self.status_identity.bytes != 0
123 }
124}
125
126pub fn capture_git(
138 git: &AdmittedTool,
139 context: &ExecutionContext<'_>,
140 options: StatusOptions,
141 limits: OutputLimits,
142) -> Result<GitObservations, GitError> {
143 let revision = observe(git, context, options, limits, GitQuery::Revision)?;
144 let tree = match observe(git, context, options, limits, GitQuery::Tree) {
145 Ok(evidence) => evidence,
146 Err(mut error) => {
147 error.completed[0] = Some(revision);
148 return Err(error);
149 }
150 };
151 let status = match observe(git, context, options, limits, GitQuery::Status) {
152 Ok(evidence) => evidence,
153 Err(mut error) => {
154 error.completed[0] = Some(revision);
155 error.completed[1] = Some(tree);
156 return Err(error);
157 }
158 };
159 let status_identity = ArtifactIdentity {
160 bytes: status.stdout.len() as u64,
161 sha256: Sha256Digest::compute(&status.stdout),
162 };
163 let revision_id = object_id(&revision.stdout);
165 let tree_id = object_id(&tree.stdout);
166 Ok(GitObservations {
167 revision: revision_id,
168 tree: tree_id,
169 options,
170 status_identity,
171 tool_identity: git.identity(),
172 revision_evidence: revision,
173 tree_evidence: tree,
174 status_evidence: status,
175 })
176}
177
178fn observe(
179 git: &AdmittedTool,
180 context: &ExecutionContext<'_>,
181 options: StatusOptions,
182 limits: OutputLimits,
183 query: GitQuery,
184) -> Result<CompletedExecution, GitError> {
185 let mut completed = [None, None, None];
186 let index = match query {
187 GitQuery::Revision => 0,
188 GitQuery::Tree => 1,
189 GitQuery::Status => 2,
190 };
191 let evidence = match git
192 .run(&arguments(query, options), context, limits)
193 .and_then(CompletedExecution::require_success)
194 .and_then(CompletedExecution::require_complete)
195 {
196 Ok(evidence) => evidence,
197 Err(source) => {
198 return Err(GitError {
199 query,
200 failure: GitFailure::Tool(Box::new(source)),
201 completed: Box::new(completed),
202 });
203 }
204 };
205 let failure = match query {
206 GitQuery::Revision | GitQuery::Tree if !valid_object_id(&evidence.stdout) => {
207 Some(GitFailure::InvalidObjectId)
208 }
209 GitQuery::Status if !evidence.stdout.is_empty() && evidence.stdout.last() != Some(&0) => {
210 Some(GitFailure::UnterminatedStatus)
211 }
212 _ => None,
213 };
214 if let Some(failure) = failure {
215 completed[index] = Some(evidence);
216 return Err(GitError {
217 query,
218 failure,
219 completed: Box::new(completed),
220 });
221 }
222 Ok(evidence)
223}
224
225fn valid_object_id(bytes: &[u8]) -> bool {
226 matches!(bytes.len(), 41 | 65)
227 && bytes.last() == Some(&b'\n')
228 && bytes[..bytes.len() - 1]
229 .iter()
230 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
231}
232
233fn object_id(bytes: &[u8]) -> String {
234 bytes[..bytes.len() - 1]
235 .iter()
236 .map(|&byte| char::from(byte))
237 .collect()
238}
239
240fn arguments(query: GitQuery, options: StatusOptions) -> Vec<OsString> {
241 let mut arguments = vec![
242 "--no-optional-locks".into(),
243 "-c".into(),
244 "core.fsmonitor=false".into(),
245 ];
246 match query {
247 GitQuery::Revision => {
248 arguments.extend(["rev-parse".into(), "--verify".into(), "HEAD".into()]);
249 }
250 GitQuery::Tree => {
251 arguments.extend(["rev-parse".into(), "--verify".into(), "HEAD^{tree}".into()]);
252 }
253 GitQuery::Status => {
254 let untracked = match options.untracked {
255 UntrackedFiles::No => "no",
256 UntrackedFiles::Normal => "normal",
257 UntrackedFiles::All => "all",
258 };
259 let submodules = match options.ignore_submodules {
260 IgnoreSubmodules::None => "none",
261 IgnoreSubmodules::Untracked => "untracked",
262 IgnoreSubmodules::Dirty => "dirty",
263 IgnoreSubmodules::All => "all",
264 };
265 arguments.extend([
266 "status".into(),
267 "--porcelain=v1".into(),
268 "-z".into(),
269 format!("--untracked-files={untracked}").into(),
270 format!("--ignore-submodules={submodules}").into(),
271 ]);
272 }
273 }
274 arguments
275}