Skip to main content

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}