spec_driven_docs/self_depend/
txn.rs1use std::process::{Command, Stdio};
10
11use camino::{Utf8Path, Utf8PathBuf};
12use semver::Version;
13use serde::Serialize;
14
15use crate::adapters::fs::write_atomic;
16use crate::error::AppError;
17use crate::self_depend::manager::{Detected, Manager};
18use crate::self_depend::pin;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
22pub struct Moved {
23 pub manager: Manager,
25 pub from: String,
27 pub to: String,
29 pub files: Vec<Utf8PathBuf>,
31}
32
33struct Snapshot {
35 path: Utf8PathBuf,
36 bytes: Option<Vec<u8>>,
37}
38
39impl Snapshot {
40 fn take(path: Utf8PathBuf) -> std::io::Result<Self> {
41 let bytes = match std::fs::read(&path) {
42 Ok(bytes) => Some(bytes),
43 Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
44 Err(error) => return Err(error),
45 };
46 Ok(Self { path, bytes })
47 }
48
49 fn restore(&self) -> std::io::Result<()> {
50 self.bytes.as_ref().map_or_else(
51 || match std::fs::remove_file(&self.path) {
52 Ok(()) => Ok(()),
53 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
54 Err(error) => Err(error),
55 },
56 |bytes| write_atomic(&self.path, bytes),
57 )
58 }
59}
60
61pub fn sync(target: &Utf8Path, detected: &Detected, to: &Version) -> Result<Moved, AppError> {
70 let file = detected.file.as_ref().ok_or_else(|| {
71 AppError::Refused(format!("{} names no file in this target", detected.manager))
72 })?;
73 let manager_path = target.join(file);
74 let text = std::fs::read_to_string(&manager_path)?;
75 let (moved_text, held) = pin::rewrite(detected.manager, &text, to)
76 .map_err(|reason| AppError::Refused(format!("{file}: {reason}")))?;
77 let spelled_to = if held.spelled.starts_with('v') {
78 format!("v{to}")
79 } else {
80 to.to_string()
81 };
82
83 let Some(lock) = detected.manager.lock_file() else {
84 write_atomic(&manager_path, moved_text.as_bytes())?;
85 return Ok(Moved {
86 manager: detected.manager,
87 from: held.spelled,
88 to: spelled_to,
89 files: vec![file.clone()],
90 });
91 };
92
93 let before_manager = Snapshot::take(manager_path.clone())?;
95 let before_lock = Snapshot::take(target.join(lock))?;
96 write_atomic(&manager_path, moved_text.as_bytes())?;
97 let outcome = refresh_lock(target, &text);
98 if let Err(error) = outcome {
99 before_manager.restore()?;
100 before_lock.restore()?;
101 return Err(error);
102 }
103 Ok(Moved {
104 manager: detected.manager,
105 from: held.spelled,
106 to: spelled_to,
107 files: vec![file.clone(), Utf8PathBuf::from(lock)],
108 })
109}
110
111fn refresh_lock(target: &Utf8Path, flake: &str) -> Result<(), AppError> {
113 let input = pin::input_name(flake).ok_or_else(|| {
114 AppError::Refused("flake.nix names this tool at a URL no input attribute holds".to_string())
115 })?;
116 let output = Command::new("nix")
117 .args(["flake", "update", &input])
118 .current_dir(target)
119 .stdin(Stdio::null())
120 .stdout(Stdio::piped())
121 .stderr(Stdio::piped())
122 .output()
123 .map_err(|error| {
124 if error.kind() == std::io::ErrorKind::NotFound {
125 AppError::Refused(
126 "nix is not on PATH, so the lock cannot follow the tag".to_string(),
127 )
128 } else {
129 AppError::Io(error)
130 }
131 })?;
132 if output.status.success() {
133 return Ok(());
134 }
135 let stderr = String::from_utf8_lossy(&output.stderr);
136 let mut tail: Vec<&str> = stderr.lines().rev().take(5).collect();
137 tail.reverse();
138 Err(AppError::Refused(format!(
139 "nix flake update {input} failed ({}); both files were put back: {}",
140 output.status,
141 tail.join(" | ")
142 )))
143}