Skip to main content

spec_driven_docs/self_depend/
leftovers.rs

1//! What a predecessor bump mechanism left, and what a clean never touches.
2//!
3//! One target runs one mover. A file a predecessor mechanism left beside
4//! the sync line is a second mover, dormant or not, so `status` reports it
5//! and `clean` removes it. The catalog of known predecessors is empty: no
6//! release of this tool shipped a mover before this verb, so a leftover is
7//! only ever one the operator names.
8
9use camino::{Utf8Path, Utf8PathBuf};
10use serde::Serialize;
11
12use crate::self_depend::manager::Detected;
13use crate::self_depend::{ENVRC, SYNC_LINE};
14
15/// Files a predecessor mechanism is known to have left, relative to a
16/// project root.
17pub const CATALOG: &[&str] = &[];
18
19/// One file a clean removes.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21pub struct Leftover {
22    /// The file, relative to the project root.
23    pub file: Utf8PathBuf,
24    /// Why it is a leftover.
25    pub reason: String,
26}
27
28/// One file a clean names and leaves byte-identical.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
30pub struct Kept {
31    /// The file, relative to the project root.
32    pub file: Utf8PathBuf,
33    /// The line inside it that a scan must not touch, where one is meant.
34    pub line: Option<String>,
35    /// Why it stays.
36    pub reason: String,
37}
38
39/// Every leftover the target carries: the catalog's, plus those named.
40#[must_use]
41pub fn find(target: &Utf8Path, also: &[Utf8PathBuf]) -> Vec<Leftover> {
42    let mut found = Vec::new();
43    for file in CATALOG {
44        if target.join(file).is_file() {
45            found.push(Leftover {
46                file: Utf8PathBuf::from(*file),
47                reason: "left by a predecessor bump mechanism".to_string(),
48            });
49        }
50    }
51    for file in also {
52        if target.join(file).is_file() && !found.iter().any(|held| &held.file == file) {
53            found.push(Leftover {
54                file: file.clone(),
55                reason: "named by the operator as a predecessor's file".to_string(),
56            });
57        }
58    }
59    found
60}
61
62/// What a clean never touches: the wired manager's file, its lock, and the
63/// sync line in the shell loader.
64#[must_use]
65pub fn kept(target: &Utf8Path, wired: &[&Detected]) -> Vec<Kept> {
66    let mut held = Vec::new();
67    for detected in wired {
68        if let Some(file) = detected.file.as_ref() {
69            held.push(Kept {
70                file: file.clone(),
71                line: detected
72                    .pin
73                    .as_ref()
74                    .map(|pin| format!("line {}", pin.line)),
75                reason: "the manager file the project owns; the pin moves in place".to_string(),
76            });
77        }
78        if let Some(lock) = detected.manager.lock_file()
79            && target.join(lock).is_file()
80        {
81            held.push(Kept {
82                file: Utf8PathBuf::from(lock),
83                line: None,
84                reason: "the lock the manager owns; it moves with the pin".to_string(),
85            });
86        }
87    }
88    if std::fs::read_to_string(target.join(ENVRC)).is_ok_and(|text| text.contains(SYNC_LINE)) {
89        held.push(Kept {
90            file: Utf8PathBuf::from(ENVRC),
91            line: Some(SYNC_LINE.to_string()),
92            reason: "the one mover; a scan for a predecessor must not match it".to_string(),
93        });
94    }
95    held
96}
97
98#[cfg(test)]
99mod tests {
100    #![allow(clippy::unwrap_used, reason = "a test panics as its failure signal")]
101
102    use super::*;
103
104    #[test]
105    fn a_named_file_is_a_leftover_only_where_it_exists() {
106        let dir = tempfile::tempdir().unwrap();
107        let root = Utf8Path::from_path(dir.path()).unwrap();
108        std::fs::write(root.join("bump.sh"), "#!/bin/sh\n").unwrap();
109        let found = find(
110            root,
111            &[Utf8PathBuf::from("bump.sh"), Utf8PathBuf::from("absent.sh")],
112        );
113        assert_eq!(found.len(), 1);
114        assert_eq!(found[0].file, "bump.sh");
115    }
116
117    #[test]
118    fn the_sync_line_is_named_as_kept() {
119        let dir = tempfile::tempdir().unwrap();
120        let root = Utf8Path::from_path(dir.path()).unwrap();
121        std::fs::write(root.join(ENVRC), format!("use flake\n{SYNC_LINE}\n")).unwrap();
122        let held = kept(root, &[]);
123        assert_eq!(held.len(), 1);
124        assert_eq!(held[0].line.as_deref(), Some(SYNC_LINE));
125    }
126}