1use super::service_protocol::Cleanup;
16use std::collections::HashMap;
17use std::fs::{self, File};
18use std::io::Write;
19use std::path::{Path, PathBuf};
20
21pub const OPEN_EXTENSION: &str = "intent";
23
24pub const DONE_EXTENSION: &str = "done";
26
27const TEMPORARY_EXTENSION: &str = "tmp";
29
30#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct Intent {
41 pub seq: u64,
43 pub repository: PathBuf,
45 pub common_dir: PathBuf,
47 pub reference: String,
49 pub old: String,
51 pub new: String,
53 pub ids: Vec<String>,
55 pub commits: Vec<String>,
57 pub paths: Vec<String>,
59 pub derived: bool,
62 pub cleanup: Option<Cleanup>,
64 pub index: PathBuf,
66 pub git_dir: PathBuf,
69}
70
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73pub enum Recovery {
74 Drop,
76 Finish,
78}
79
80pub fn recovery_for(intent: &Intent, current: &str, new_is_ancestor_of_current: bool) -> Recovery {
91 if current == intent.new || (current != intent.old && new_is_ancestor_of_current) {
92 return Recovery::Finish;
93 }
94 return Recovery::Drop;
95}
96
97pub fn intent_text(intent: &Intent) -> String {
99 let cleanup: String = intent.cleanup.as_ref().map_or_else(String::new, |found| {
100 return format!("{}\t{}\t{}", found.git_dir.display(), found.scratch.display(), u8::from(found.reftable));
101 });
102 return format!(
103 "seq={}\nrepository={}\ncommon={}\nref={}\nold={}\nnew={}\nids={}\ncommits={}\npaths={}\nderived={}\ncleanup={}\nindex={}\ngitdir={}\n",
104 intent.seq,
105 intent.repository.display(),
106 intent.common_dir.display(),
107 intent.reference,
108 intent.old,
109 intent.new,
110 intent.ids.join("\t"),
111 intent.commits.join("\t"),
112 intent.paths.join("\t"),
113 u8::from(intent.derived),
114 cleanup,
115 intent.index.display(),
116 intent.git_dir.display(),
117 );
118}
119
120pub fn parse_intent(text: &str) -> Option<Intent> {
130 let value = |key: &str| -> Option<&str> { return text.lines().find_map(|line| { return line.strip_prefix(key)?.strip_prefix('='); }); };
131 let list = |key: &str| -> Option<Vec<String>> {
132 return Some(value(key)?.split('\t').filter(|item| { return !item.is_empty(); }).map(str::to_string).collect());
133 };
134 let index: PathBuf = value("index").map_or_else(|| { return PathBuf::from(value("repository").unwrap_or("")).join(".git").join("index"); }, PathBuf::from);
135 let worktree_git_dir: PathBuf = value("gitdir").map_or_else(|| { return index.parent().map(Path::to_path_buf).unwrap_or_default(); }, PathBuf::from);
137 return Some(Intent {
138 seq: value("seq")?.parse().ok()?,
139 repository: PathBuf::from(value("repository")?),
140 common_dir: PathBuf::from(value("common")?),
141 reference: value("ref")?.to_string(),
142 old: value("old")?.to_string(),
143 new: value("new")?.to_string(),
144 ids: list("ids")?,
145 commits: list("commits")?,
146 paths: list("paths")?,
147 derived: value("derived") == Some("1"),
149 index,
150 git_dir: worktree_git_dir,
151 cleanup: value("cleanup").and_then(|entry| {
152 let mut parts = entry.split('\t');
153 let (git_dir, scratch, reftable) = (parts.next()?, parts.next()?, parts.next()?);
154 return Some(Cleanup { git_dir: PathBuf::from(git_dir), scratch: PathBuf::from(scratch), reftable: reftable == "1" });
155 }),
156 });
157}
158
159pub fn batch_path(dir: &Path, seq: u64, extension: &str) -> PathBuf {
161 return dir.join(format!("{seq}.{extension}"));
162}
163
164pub fn write_intent(dir: &Path, intent: &Intent) -> std::io::Result<()> {
174 let temporary: PathBuf = batch_path(dir, intent.seq, TEMPORARY_EXTENSION);
175 let mut file: File = File::create(&temporary)?;
176 file.write_all(intent_text(intent).as_bytes())?;
177 file.sync_all()?;
178 fs::rename(&temporary, batch_path(dir, intent.seq, OPEN_EXTENSION))?;
179 return File::open(dir)?.sync_all();
180}
181
182pub fn mark_done(dir: &Path, seq: u64) -> std::io::Result<()> {
184 return fs::rename(batch_path(dir, seq, OPEN_EXTENSION), batch_path(dir, seq, DONE_EXTENSION));
185}
186
187pub fn list_intents(dir: &Path, extension: &str) -> Vec<(PathBuf, Option<Intent>)> {
197 let Ok(entries) = fs::read_dir(dir) else {
198 return Vec::new();
199 };
200 let mut found: Vec<(PathBuf, Option<Intent>)> = Vec::new();
201 for entry in entries.flatten() {
202 let path: PathBuf = entry.path();
203 match path.extension().and_then(|text| { return text.to_str(); }) {
204 Some(TEMPORARY_EXTENSION) => {
205 let _removed = fs::remove_file(&path);
206 }
207 Some(found_extension) if found_extension == extension => {
208 let parsed: Option<Intent> = fs::read_to_string(&path).ok().and_then(|text| { return parse_intent(text.as_str()); });
209 found.push((path, parsed));
210 }
211 _ => {}
212 }
213 }
214 found.sort_by_key(|(path, intent)| {
215 return intent.as_ref().map_or_else(|| { return path.to_string_lossy().len() as u64; }, |parsed| { return parsed.seq; });
216 });
217 return found;
218}
219
220pub fn landed_commits(dir: &Path) -> HashMap<String, String> {
222 let mut landed: HashMap<String, String> = HashMap::new();
223 for (_, intent) in list_intents(dir, DONE_EXTENSION) {
224 if let Some(parsed) = intent {
225 landed.extend(parsed.ids.into_iter().zip(parsed.commits));
226 }
227 }
228 return landed;
229}
230
231#[cfg(test)]
233#[path = "service_journal_tests.rs"]
234mod tests;