differential_engine/plan/enumerate.rs
1//! Raw git output in, a classified diff view out. No I/O.
2
3use std::collections::HashSet;
4
5use crate::EngineError;
6use crate::config::Config;
7use crate::document::mark_generated;
8use crate::lang::LanguageRegistry;
9use crate::model::DiffView;
10use crate::parse::parse_canonical;
11use crate::rename_view::{merge_raw, merge_renames, parse_raw_z, parse_renames_z};
12use crate::shape::{Partition, partition};
13
14/// The three git outputs enumeration is built from.
15///
16/// Bytes, not paths or a repository: which commands produced these is the
17/// adapter's business, and the parsers were validated against real git output
18/// (ADR 0002).
19pub struct Enumeration<'a> {
20 /// `diff-tree -r -z --raw --full-index --no-renames`: authoritative modes,
21 /// full oids, dispositions.
22 pub raw_records: &'a [u8],
23 /// `diff-tree -r -U0 --no-renames --no-color --no-ext-diff`: the canonical
24 /// patch. Every hunk in the system comes from here.
25 pub canonical_patch: &'a [u8],
26 /// `diff-tree -r -M -z --name-status`: rename-detected annotations only
27 /// (ADR 0003). Never affects what exists.
28 pub rename_records: &'a [u8],
29}
30
31/// Build the canonical view.
32///
33/// Takes **no `Config` and no `LanguageRegistry`**, and that is the point:
34/// ADR 0012's "enumeration runs before and independently of config" used to be
35/// a property of statement order inside a 95-line function, and is now a
36/// property of this parameter list. Nothing reachable from here can remove a
37/// file or a hunk.
38pub fn build_view(e: &Enumeration<'_>) -> Result<DiffView, EngineError> {
39 let records = parse_raw_z(e.raw_records)?;
40 let dispositions = records
41 .iter()
42 .map(|r| (r.path.clone(), r.disposition()))
43 .collect();
44
45 // Canonical enumeration: every file, no exclusions (ADR 0005).
46 let mut view = parse_canonical(e.canonical_patch, &dispositions)?;
47 // Authoritative modes and oids overlay the patch; a count mismatch here is
48 // an enumeration hole and errors rather than being papered over.
49 merge_raw(&mut view, &records)?;
50 merge_renames(&mut view, &parse_renames_z(e.rename_records)?);
51 Ok(view)
52}
53
54/// Apply classification hints and compute the mechanical partition.
55///
56/// The only place config and languages enter the pipeline. Both tune how hunks
57/// are *described*, never which ones exist (ADR 0012, ADR 0015).
58///
59/// **The order of these two lines is load-bearing.** `partition` reads
60/// `file.generated` as part of the class key, so the hints must be applied
61/// first. Getting it backwards would not fail: every file would read as not
62/// generated, and generated hunks would quietly rejoin source classes — the
63/// exact bug the key component was added to remove. The two calls are adjacent
64/// and this is their only caller, which is the guard.
65pub fn classify(
66 view: &mut DiffView,
67 config: &Config,
68 attr_marked: &HashSet<Vec<u8>>,
69 langs: &LanguageRegistry,
70) -> Partition {
71 mark_generated(view, config, attr_marked);
72 partition(view, langs)
73}
74
75/// Does a `check-attr` value declare a path generated?
76///
77/// git answers `unspecified` / `unset` / `false` for "no"; anything else — a
78/// bare `true`, or a value — is a declaration. Which attribute names to ask
79/// about is config's business; what the answers *mean* is this.
80pub fn attr_marks_generated(value: &[u8]) -> bool {
81 value != b"unspecified" && value != b"unset" && value != b"false"
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn only_a_positive_attribute_value_declares_generated() {
90 for no in [&b"unspecified"[..], b"unset", b"false"] {
91 assert!(
92 !attr_marks_generated(no),
93 "{:?}",
94 String::from_utf8_lossy(no)
95 );
96 }
97 for yes in [&b"true"[..], b"linguist-generated", b"1", b""] {
98 assert!(
99 attr_marks_generated(yes),
100 "{:?}",
101 String::from_utf8_lossy(yes)
102 );
103 }
104 }
105}