spec_driven_docs/self_depend/
leftovers.rs1use camino::{Utf8Path, Utf8PathBuf};
10use serde::Serialize;
11
12use crate::self_depend::manager::Detected;
13use crate::self_depend::{ENVRC, SYNC_LINE};
14
15pub const CATALOG: &[&str] = &[];
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21pub struct Leftover {
22 pub file: Utf8PathBuf,
24 pub reason: String,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
30pub struct Kept {
31 pub file: Utf8PathBuf,
33 pub line: Option<String>,
35 pub reason: String,
37}
38
39#[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#[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}