git_policy_cli/candidate_store.rs
1//! What:
2//! The per-invocation owner of candidate versions and candidate bytes.
3//! Why:
4//! Policies read committed or staged content,
5//! never whatever is in the live
6//! worktree.
7//! A version costs one listing process,
8//! its bytes come from one
9//! long-lived object reader,
10//! and neither count grows with the number of files.
11//! After a fix or replay changed the index,
12//! `invalidate` retires every version.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! // const store = new CandidateStore(git, prefix, overlay);
17//! // const version = await store.version({ kind: 'staged-against-head' });
18//! // const bytes = await store.bytes(version.candidates[0]);
19//! ```
20
21/// Import the layer's failure type and its closed list of causes.
22use super::candidate_error::{CandidateError, CandidateFailure};
23/// Import the candidate modes,
24/// the validated object name and its parser.
25use super::candidate_object::{CandidateMode, ObjectId, parse_object_id};
26/// Import the long-lived object reader.
27use super::candidate_reader::{ObjectReader, start_object_reader};
28/// Import the listing parser and its records.
29use super::candidate_record::{CandidateRecord, parse_raw_records};
30/// Import the stage-record parser of `git ls-files --stage`.
31use super::candidate_stage::{StageRecord, parse_stage_records};
32/// Import the tree-record parser of `git ls-tree -r`.
33use super::candidate_tree::{TreeRecord, parse_tree_records};
34/// Import the version types and builder.
35use super::candidate_version::{Candidate, CandidateSource, CandidateVersion, build_version};
36/// Import the captured-query runner and its line helper.
37use super::git_metadata::{MetadataOutput, run_metadata_git, strip_git_line};
38/// `OsString` is owned operating-system text of raw OS bytes (sibling `String` must be UTF-8).
39use std::ffi::OsString;
40/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw OS bytes.
41use std::path::{Path, PathBuf};
42/// What:
43/// `Rc<T>` is a shared,
44/// read-only handle to one heap value,
45/// counted so the value
46/// lives until its last handle is gone (siblings:
47/// `Box<T>`,
48/// a single owner,
49/// and
50/// `Arc<T>`,
51/// the thread-safe form).
52/// Why:
53/// Several policies hold the same version and the same bytes at once.
54/// Nothing
55/// here crosses threads,
56/// so `Arc` would add cost for no benefit.
57///
58/// In TS you'd write (pseudocode):
59/// ```ts
60/// // Every object reference in TS already behaves like this.
61/// ```
62use std::rc::Rc;
63
64/// What:
65/// Everything one invocation needs to list and read candidates.
66/// `Option<ObjectReader>` is "a running reader or nothing";
67/// `u64` is an unsigned
68/// 64-bit counter (siblings `u32`,
69/// `usize`).
70/// Why:
71/// The reader is started only when something needs it,
72/// so an invocation that
73/// reads no object starts no reader.
74/// `u64` cannot run out within a process.
75///
76/// In TS you'd write (pseudocode):
77/// ```ts
78/// class CandidateStore { #reader?: ObjectReader; #generation = 0n; #versions: [CandidateSource, CandidateVersion][] = [] }
79/// ```
80pub struct CandidateStore {
81 /// The real Git executable every listing and the reader start.
82 real_git: PathBuf,
83 /// The caller's arguments before the subcommand,
84 /// replayed so Git selects the same repository.
85 global_prefix: Vec<OsString>,
86 /// Added environment pairs,
87 /// such as a private index file or object directory.
88 overlay: Vec<(OsString, OsString)>,
89 /// The long-lived object reader,
90 /// once started.
91 reader: Option<ObjectReader>,
92 /// Counts invalidations;
93 /// candidates of an earlier generation are refused.
94 generation: u64,
95 /// Versions already listed in this generation,
96 /// by source.
97 versions: Vec<(CandidateSource, Rc<CandidateVersion>)>,
98 /// The commit every "against `HEAD`" listing compares with instead of the live `HEAD`:
99 /// `Some(Some(commit))` for a recorded base, `Some(None)` for a recorded unborn branch, `None` for live `HEAD`.
100 baseline: Option<Option<ObjectId>>,
101}
102
103/// What:
104/// Build the failure of a listing command that could not be started or exited unsuccessfully.
105/// Why:
106/// The message names the Git operation,
107/// so the person can rerun it.
108///
109/// In TS you'd write (pseudocode):
110/// ```ts
111/// function listingFailure(failure: CandidateFailure, operation: string, detail: string): CandidateError;
112/// ```
113fn listing_failure(failure: CandidateFailure, operation: &str, detail: &str) -> CandidateError {
114 return CandidateError::new(
115 failure,
116 format!("cli-git could not list candidate files: git {operation} {detail}").as_str(),
117 );
118}
119
120/// What:
121/// `impl CandidateStore { ... }` attaches the store's operations,
122/// like class methods.
123/// Why:
124/// The store is the only place that starts Git for candidates,
125/// which is what
126/// keeps the process count bounded.
127///
128/// In TS you'd write (pseudocode):
129/// ```ts
130/// class CandidateStore { version(source) {} bytes(candidate) {} invalidate() {} }
131/// ```
132impl CandidateStore {
133 /// What:
134 /// Create a store without starting any process.
135 /// `&[OsString]` borrows the global arguments;
136 /// `&[(OsString, OsString)]` borrows
137 /// environment pairs;
138 /// `.to_vec()` and `.to_path_buf()` copy them into owned storage.
139 /// Why:
140 /// The store outlives the caller's borrowed arguments,
141 /// and commands that
142 /// never ask for candidates pay nothing.
143 ///
144 /// In TS you'd write (pseudocode):
145 /// ```ts
146 /// constructor(realGit: string, globalPrefix: string[], overlay: [string, string][])
147 /// ```
148 pub fn new(
149 real_git: &Path,
150 global_prefix: &[OsString],
151 overlay: &[(OsString, OsString)],
152 ) -> CandidateStore {
153 return CandidateStore {
154 real_git: real_git.to_path_buf(),
155 global_prefix: global_prefix.to_vec(),
156 overlay: overlay.to_vec(),
157 // `None` is the "absent" variant: no reader yet.
158 reader: None,
159 generation: 0,
160 versions: Vec::new(),
161 baseline: None,
162 };
163 }
164
165 /// What:
166 /// Compare every "against `HEAD`" listing with a recorded base instead of the live `HEAD`.
167 /// Why:
168 /// A commit transaction reads its candidates against the base it recorded at invocation,
169 /// because another landing may have moved `HEAD` since.
170 ///
171 /// In TS you'd write (pseudocode):
172 /// ```ts
173 /// listChangedIndexPaths({ baseRevision }) // never live HEAD
174 /// ```
175 #[must_use]
176 pub fn with_baseline(mut self, baseline: Option<ObjectId>) -> CandidateStore {
177 self.baseline = Some(baseline);
178 return self;
179 }
180
181 /// What:
182 /// The commit the "against `HEAD`" listings compare with:
183 /// the recorded baseline when one was set, otherwise the commit `HEAD` names now.
184 /// Why:
185 /// One place decides the baseline, so the listings and the `HEAD` tree always agree.
186 ///
187 /// In TS you'd write (pseudocode):
188 /// ```ts
189 /// const baseline = recorded ?? await headCommit();
190 /// ```
191 fn baseline_commit(&mut self) -> Result<Option<ObjectId>, CandidateError> {
192 if let Some(recorded) = &self.baseline {
193 return Ok(recorded.clone());
194 }
195 return self.reader()?.head_commit();
196 }
197
198 /// What:
199 /// The running reader,
200 /// started on first use.
201 /// `&mut ObjectReader` lends it for changing.
202 /// Why:
203 /// One reader serves every object read of the invocation.
204 ///
205 /// In TS you'd write (pseudocode):
206 /// ```ts
207 /// #reader(): ObjectReader { return (this.#reader ??= startObjectReader(...)); }
208 /// ```
209 fn reader(&mut self) -> Result<&mut ObjectReader, CandidateError> {
210 if self.reader.is_none() {
211 // A trailing `?` returns a start failure to our caller, or unwraps the reader.
212 let started: ObjectReader = start_object_reader(
213 self.real_git.as_path(),
214 self.global_prefix.as_slice(),
215 self.overlay.as_slice(),
216 )?;
217 // `Some(...)` is the "present" variant.
218 self.reader = Some(started);
219 }
220 // `.as_mut()` lends the stored reader; `let Some(..) = .. else` unwraps it.
221 let Some(reader) = self.reader.as_mut() else {
222 // `Err(...)` is the failure variant. The reader was stored just above, so this is not expected.
223 return Err(CandidateError::new(
224 CandidateFailure::ReaderEnded,
225 "cli-git could not read a Git object: the object reader was not started.",
226 ));
227 };
228 // `Ok(...)` is the success variant.
229 return Ok(reader);
230 }
231
232 /// What:
233 /// Run one listing command and return its standard output.
234 /// `Vec<u8>` is an owned byte list (sibling `String` would require UTF-8).
235 /// Why:
236 /// Listings print raw pathname bytes.
237 /// A command that fails lists nothing
238 /// trustworthy,
239 /// so its output is discarded and Git's own message is reported.
240 ///
241 /// In TS you'd write (pseudocode):
242 /// ```ts
243 /// async #runListing(operation: string, args: string[]): Promise<Buffer>
244 /// ```
245 fn run_listing(
246 &self,
247 operation: &str,
248 command: &[OsString],
249 ) -> Result<Vec<u8>, CandidateError> {
250 // `.clone()` copies the stored prefix; `.extend_from_slice(..)` appends the command after it.
251 let mut arguments: Vec<OsString> = self.global_prefix.clone();
252 arguments.extend_from_slice(command);
253 // `match` on the run's `Result`: `Err(error)` means Git could not be started.
254 let output: MetadataOutput = match run_metadata_git(
255 self.real_git.as_path(),
256 arguments.as_slice(),
257 self.overlay.as_slice(),
258 ) {
259 Ok(captured) => captured,
260 Err(error) => {
261 return Err(listing_failure(
262 CandidateFailure::GitNotStarted,
263 operation,
264 format!("could not be started: {error}.").as_str(),
265 ));
266 }
267 };
268 if !output.success {
269 // `String::from_utf8_lossy` decodes Git's message for display, replacing invalid bytes.
270 return Err(listing_failure(
271 CandidateFailure::GitFailed,
272 operation,
273 format!(
274 "failed: {}",
275 String::from_utf8_lossy(output.stderr.as_slice()).trim()
276 )
277 .as_str(),
278 ));
279 }
280 return Ok(output.stdout);
281 }
282
283 /// What:
284 /// The name of the empty tree in this repository's hash format.
285 /// Why:
286 /// A repository without commits has no `HEAD` to compare the index with;
287 /// the
288 /// empty tree makes every staged entry an addition.
289 /// Git computes the name
290 /// from empty input,
291 /// so SHA-1 and SHA-256 repositories both work.
292 ///
293 /// In TS you'd write (pseudocode):
294 /// ```ts
295 /// async #emptyTree(): Promise<ObjectId>
296 /// ```
297 fn empty_tree(&self) -> Result<ObjectId, CandidateError> {
298 // The query runner gives Git no standard input, which is the empty tree's content.
299 let output: Vec<u8> = self.run_listing(
300 "hash-object",
301 &[
302 OsString::from("hash-object"),
303 OsString::from("-t"),
304 OsString::from("tree"),
305 OsString::from("--stdin"),
306 ],
307 )?;
308 let Some(tree) = parse_object_id(strip_git_line(output.as_slice())) else {
309 return Err(listing_failure(
310 CandidateFailure::ListingMalformed,
311 "hash-object",
312 "did not print the empty tree's object name.",
313 ));
314 };
315 return Ok(tree);
316 }
317
318 /// What:
319 /// List the index against a commit,
320 /// or against the empty tree when there is none,
321 /// limited to `pathspecs`.
322 /// `Option<&ObjectId>` is "a borrowed commit name or
323 /// nothing";
324 /// an empty `pathspecs` lists the whole index.
325 /// Why:
326 /// `diff-index --cached` reads only the index and the tree,
327 /// never the
328 /// worktree.
329 /// Rename detection is off,
330 /// so a renamed file is one deleted path
331 /// and one added path,
332 /// each judged on its own.
333 ///
334 /// In TS you'd write (pseudocode):
335 /// ```ts
336 /// async #listStaged(baseline: ObjectId | undefined, pathspecs: string[]): Promise<Buffer>
337 /// ```
338 fn list_staged(
339 &self,
340 baseline: Option<&ObjectId>,
341 pathspecs: &[OsString],
342 ) -> Result<Vec<u8>, CandidateError> {
343 let tree: ObjectId = match baseline {
344 Some(commit) => commit.clone(),
345 None => self.empty_tree()?,
346 };
347 // `vec![...]` builds the fixed part of the command; the pathspecs follow `--`.
348 let mut command: Vec<OsString> = vec![
349 OsString::from("diff-index"),
350 OsString::from("--cached"),
351 OsString::from("--raw"),
352 OsString::from("-z"),
353 OsString::from("--no-renames"),
354 OsString::from("--no-abbrev"),
355 OsString::from(tree.as_str()),
356 OsString::from("--"),
357 ];
358 command.extend_from_slice(pathspecs);
359 return self.run_listing("diff-index", command.as_slice());
360 }
361
362 /// What:
363 /// List what one commit changed against each of its parents.
364 /// Why:
365 /// `-m` lists a merge once per parent and `--root` lists a first commit
366 /// against nothing,
367 /// so every commit shape yields its changed paths.
368 ///
369 /// In TS you'd write (pseudocode):
370 /// ```ts
371 /// async #listCommitted(commit: ObjectId): Promise<Buffer>
372 /// ```
373 fn list_committed(&self, commit: &ObjectId) -> Result<Vec<u8>, CandidateError> {
374 return self.run_listing(
375 "diff-tree",
376 &[
377 OsString::from("diff-tree"),
378 OsString::from("--root"),
379 OsString::from("--no-commit-id"),
380 OsString::from("-r"),
381 OsString::from("-z"),
382 OsString::from("-m"),
383 OsString::from("--no-renames"),
384 OsString::from("--no-abbrev"),
385 OsString::from(commit.as_str()),
386 ],
387 );
388 }
389
390 /// What:
391 /// The version of one source,
392 /// listed once per generation and shared afterwards.
393 /// `Rc::clone(..)` makes another handle to the same version without copying it.
394 /// Why:
395 /// Every policy of a pass sees the same paths,
396 /// and asking again costs no process.
397 ///
398 /// In TS you'd write (pseudocode):
399 /// ```ts
400 /// async version(source: CandidateSource): Promise<CandidateVersion>
401 /// ```
402 pub fn version(
403 &mut self,
404 source: &CandidateSource,
405 ) -> Result<Rc<CandidateVersion>, CandidateError> {
406 // `for (known, version) in &self.versions` destructures each borrowed pair.
407 for (known, version) in &self.versions {
408 if known == source {
409 return Ok(Rc::clone(version));
410 }
411 }
412 let output: Vec<u8> = match source {
413 CandidateSource::StagedAgainstHead => {
414 let head: Option<ObjectId> = self.baseline_commit()?;
415 // `.as_ref()` turns `&Option<ObjectId>` into `Option<&ObjectId>`; `&[]` is no pathspec.
416 self.list_staged(head.as_ref(), &[])?
417 }
418 CandidateSource::StagedAgainstCommit(commit) => self.list_staged(Some(commit), &[])?,
419 CandidateSource::Committed(commit) => self.list_committed(commit)?,
420 };
421 let records: Vec<CandidateRecord> = parse_raw_records(output.as_slice())?;
422 // `Rc::new(..)` moves the built version into shared storage.
423 let version: Rc<CandidateVersion> = Rc::new(build_version(self.generation, records));
424 self.versions.push((source.clone(), Rc::clone(&version)));
425 return Ok(version);
426 }
427
428 /// What:
429 /// The index entries `pathspecs` select,
430 /// every conflict stage included,
431 /// with
432 /// repository-relative pathnames.
433 /// Why:
434 /// `ls-files --stage` reads only the index,
435 /// so comparing two of these listings
436 /// shows exactly which entries an operation on that index changed.
437 /// `--full-name`
438 /// keeps pathnames repository-relative from any directory.
439 ///
440 /// In TS you'd write (pseudocode):
441 /// ```ts
442 /// async stageRecords(pathspecs: string[]): Promise<StageRecord[]>
443 /// ```
444 pub fn stage_records(
445 &mut self,
446 pathspecs: &[OsString],
447 ) -> Result<Vec<StageRecord>, CandidateError> {
448 let mut command: Vec<OsString> = vec![
449 OsString::from("ls-files"),
450 OsString::from("--stage"),
451 OsString::from("-z"),
452 OsString::from("--full-name"),
453 OsString::from("--"),
454 ];
455 command.extend_from_slice(pathspecs);
456 let output: Vec<u8> = self.run_listing("ls-files", command.as_slice())?;
457 return parse_stage_records(output.as_slice());
458 }
459
460 /// What:
461 /// The changed-path records of the index against `HEAD`,
462 /// limited to `pathspecs`;
463 /// against the empty tree when no commit exists yet.
464 /// Why:
465 /// Which selected paths `HEAD` lacks makes them additions,
466 /// and which removed
467 /// paths it has makes them deletions.
468 /// The limit keeps a conflict elsewhere in the
469 /// index out of the listing.
470 ///
471 /// In TS you'd write (pseudocode):
472 /// ```ts
473 /// async headRecords(pathspecs: string[]): Promise<CandidateRecord[]>
474 /// ```
475 pub fn head_records(
476 &mut self,
477 pathspecs: &[OsString],
478 ) -> Result<Vec<CandidateRecord>, CandidateError> {
479 let head: Option<ObjectId> = self.baseline_commit()?;
480 let output: Vec<u8> = self.list_staged(head.as_ref(), pathspecs)?;
481 return parse_raw_records(output.as_slice());
482 }
483
484 /// What: Every file of the `HEAD` commit with its mode and object; nothing when the
485 /// branch has no commit yet.
486 /// Why: A policy that compares the candidate state with `HEAD` reads `HEAD`'s copy of
487 /// a file without naming the path in an object request, and learns its mode.
488 /// `--full-tree` lists from the top level whatever directory Git runs in.
489 ///
490 /// In TS you'd write (pseudocode):
491 /// ```ts
492 /// async headTreeRecords(): Promise<TreeRecord[]>
493 /// ```
494 pub fn head_tree_records(&mut self) -> Result<Vec<TreeRecord>, CandidateError> {
495 let Some(head) = self.baseline_commit()? else {
496 return Ok(Vec::new());
497 };
498 let output: Vec<u8> = self.run_listing(
499 "ls-tree",
500 &[
501 OsString::from("ls-tree"),
502 OsString::from("-r"),
503 OsString::from("-z"),
504 OsString::from("--full-tree"),
505 OsString::from(head.as_str()),
506 ],
507 )?;
508 return parse_tree_records(output.as_slice());
509 }
510
511 /// What: A version of the current generation built from records the caller derived.
512 /// `Vec<CandidateRecord>` is taken by value: the records move into the version.
513 /// Why: A predicted staging operation has no single listing command; its candidates
514 /// come from comparing index states. The version is not remembered by source,
515 /// because no source names it.
516 ///
517 /// In TS you'd write (pseudocode):
518 /// ```ts
519 /// derivedVersion(records: CandidateRecord[]): CandidateVersion
520 /// ```
521 pub fn derived_version(&mut self, records: Vec<CandidateRecord>) -> Rc<CandidateVersion> {
522 return Rc::new(build_version(self.generation, records));
523 }
524
525 /// What:
526 /// The records of one commit's own delta, each against every parent (or the empty tree for a root commit).
527 /// Why:
528 /// A pushed commit's changes are the content a manual-push scan reads; the listing is the one the
529 /// landed-delta source already uses.
530 ///
531 /// In TS you'd write (pseudocode):
532 /// ```ts
533 /// committedRecords(commit: ObjectId): CandidateRecord[]
534 /// ```
535 pub fn committed_records(
536 &mut self,
537 commit: &ObjectId,
538 ) -> Result<Vec<CandidateRecord>, CandidateError> {
539 let output: Vec<u8> = self.list_committed(commit)?;
540 return parse_raw_records(output.as_slice());
541 }
542
543 /// What:
544 /// Every blob entry of one tree, as records that add each path.
545 /// Why:
546 /// A push with no known published base scans the pushed tip's whole tree.
547 /// Submodule entries are not listed: their content is a commit name, which no content policy reads.
548 ///
549 /// In TS you'd write (pseudocode):
550 /// ```ts
551 /// treeRecords(tree: ObjectId): CandidateRecord[] // ls-tree -r, every blob as added
552 /// ```
553 pub fn tree_records(
554 &mut self,
555 tree: &ObjectId,
556 ) -> Result<Vec<CandidateRecord>, CandidateError> {
557 let listing: Vec<u8> = self.run_listing(
558 "ls-tree",
559 &[
560 OsString::from("ls-tree"),
561 OsString::from("--full-tree"),
562 OsString::from("-r"),
563 OsString::from("-z"),
564 OsString::from(tree.as_str()),
565 ],
566 )?;
567 let raw: Vec<u8> = added_raw_from_ls_tree(listing.as_slice())?;
568 return parse_raw_records(raw.as_slice());
569 }
570
571 /// What:
572 /// The exact bytes a candidate holds in its version.
573 /// `Rc<[u8]>` is a shared read-only byte list.
574 /// Why:
575 /// Bytes come from the object the listing named,
576 /// so an edit to the worktree
577 /// file after staging is invisible here.
578 /// A deleted path has no bytes.
579 /// A
580 /// gitlink has no blob in this repository,
581 /// so its bytes are the submodule
582 /// commit's name,
583 /// as in the TypeScript wrapper.
584 /// A candidate of a retired
585 /// version is refused instead of answered with bytes that may be outdated.
586 ///
587 /// In TS you'd write (pseudocode):
588 /// ```ts
589 /// async bytes(candidate: Candidate): Promise<Uint8Array>
590 /// ```
591 pub fn bytes(&mut self, candidate: &Candidate) -> Result<Rc<[u8]>, CandidateError> {
592 if candidate.identity.generation != self.generation {
593 return Err(CandidateError::new(
594 CandidateFailure::StaleCandidate,
595 "cli-git refused to read a candidate from a version that was invalidated by a fix or replay; \
596 list the version again and use its candidates.",
597 ));
598 }
599 let Some(object) = candidate.object.as_ref() else {
600 // `Rc::from(Vec::new())` is a shared empty byte list.
601 return Ok(Rc::from(Vec::new()));
602 };
603 if candidate.mode == CandidateMode::Gitlink {
604 // `.as_bytes()` views the name's text as bytes; `Rc::from(..)` copies them into shared storage.
605 return Ok(Rc::from(object.as_str().as_bytes()));
606 }
607 return self.reader()?.blob(object);
608 }
609
610 /// What:
611 /// Retire every version listed so far.
612 /// `+= 1` advances the generation.
613 /// Why:
614 /// A fix or replay rewrites index entries,
615 /// so earlier listings no longer
616 /// describe what would be committed.
617 /// Blob bytes stay remembered in the
618 /// reader:
619 /// an object name always denotes the same bytes.
620 ///
621 /// In TS you'd write (pseudocode):
622 /// ```ts
623 /// invalidate(): void { this.#versions = []; this.#generation += 1n; }
624 /// ```
625 pub fn invalidate(&mut self) {
626 self.versions.clear();
627 self.generation += 1;
628 }
629}
630
631/// Repository controls stay out of the release executable.
632#[cfg(test)]
633#[path = "candidate_store_tests.rs"]
634mod tests;
635
636/// Committed versions,
637/// invalidation and listing failures.
638#[cfg(test)]
639#[path = "candidate_store_lifecycle_tests.rs"]
640mod lifecycle_tests;
641
642/// The process-count bound and its per-file positive control.
643#[cfg(test)]
644#[path = "candidate_process_count_tests.rs"]
645mod process_count_tests;
646
647/// Unborn,
648/// SHA-256 and private-index listings.
649#[cfg(test)]
650#[path = "candidate_store_baseline_tests.rs"]
651mod baseline_tests;
652
653/// Listings that must be refused.
654#[cfg(test)]
655#[path = "candidate_store_failure_tests.rs"]
656mod failure_tests;
657
658/// What:
659/// Turn NUL-separated `ls-tree -r -z` records into raw diff records that add each blob.
660/// Why:
661/// The record parser is the one the diff listings use, so tree entries follow the same path and mode rules.
662///
663/// In TS you'd write (pseudocode):
664/// ```ts
665/// function addedRawFromLsTree(output: Uint8Array): Uint8Array;
666/// ```
667fn added_raw_from_ls_tree(output: &[u8]) -> Result<Vec<u8>, CandidateError> {
668 let mut raw: Vec<u8> = Vec::new();
669 for record in output
670 .split(|byte| {
671 return *byte == 0;
672 })
673 .filter(|record| {
674 return !record.is_empty();
675 })
676 {
677 let text = String::from_utf8_lossy(record);
678 let Some((metadata, path)) = text.split_once('\t') else {
679 return Err(CandidateError::new(
680 super::candidate_error::CandidateFailure::ListingMalformed,
681 "cli-git could not list the pushed tree: an entry lacks its path separator.",
682 ));
683 };
684 let fields: Vec<&str> = metadata.split(' ').collect();
685 if fields.len() != 3 {
686 return Err(CandidateError::new(
687 super::candidate_error::CandidateFailure::ListingMalformed,
688 "cli-git could not list the pushed tree: an entry lacks its mode, type or object name.",
689 ));
690 }
691 if fields[1] != "blob" {
692 continue;
693 }
694 let zeros = "0".repeat(fields[2].len());
695 raw.extend_from_slice(format!(":000000 {} {zeros} {} A", fields[0], fields[2]).as_bytes());
696 raw.push(0);
697 raw.extend_from_slice(path.as_bytes());
698 raw.push(0);
699 }
700 return Ok(raw);
701}
702/// The `HEAD` tree listing.
703#[cfg(test)]
704#[path = "candidate_store_tree_tests.rs"]
705mod tree_tests;