1use camino::{Utf8Path, Utf8PathBuf};
2
3use crate::error::{NewgitError, Result};
4use crate::materializer::create_dir_all;
5use crate::tracker::{TrackerDefinition, collect_all_files, collect_owned_files, content_rev};
6
7#[derive(Debug, Clone)]
14pub struct TrackerLane {
15 root: Utf8PathBuf,
16 tracker: String,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct CaptureOutcome {
21 pub rev: String,
22 pub files: usize,
23}
24
25impl TrackerLane {
26 pub fn new(snapshots_root: &Utf8Path, tracker: &str) -> Self {
27 Self {
28 root: snapshots_root.join(tracker),
29 tracker: tracker.to_owned(),
30 }
31 }
32
33 pub fn latest(&self) -> Option<String> {
34 let contents = std::fs::read_to_string(self.root.join("LATEST")).ok()?;
35 let rev = contents.trim().to_owned();
36 (!rev.is_empty()).then_some(rev)
37 }
38
39 pub fn set_latest(&self, rev: &str) -> Result<()> {
40 create_dir_all(&self.root)?;
41 let path = self.root.join("LATEST");
42 std::fs::write(&path, format!("{rev}\n")).map_err(|source| NewgitError::io(path, source))
43 }
44
45 pub fn has_rev(&self, rev: &str) -> bool {
46 self.rev_dir(rev).is_dir()
47 }
48
49 fn rev_dir(&self, rev: &str) -> Utf8PathBuf {
50 self.root.join(rev)
51 }
52
53 pub fn capture(
56 &self,
57 workspace: &Utf8Path,
58 definition: &TrackerDefinition,
59 ) -> Result<CaptureOutcome> {
60 if definition.paths.is_empty() {
61 return Err(NewgitError::TrackerHasNoPaths(self.tracker.clone()));
62 }
63 let files = collect_owned_files(workspace, definition)?;
64 self.store_snapshot(&files)
65 }
66
67 pub fn deposit(&self, dir: &Utf8Path) -> Result<CaptureOutcome> {
71 let files = collect_all_files(dir)?;
72 self.store_snapshot(&files)
73 }
74
75 fn store_snapshot(&self, files: &[(Utf8PathBuf, Utf8PathBuf)]) -> Result<CaptureOutcome> {
78 let rev = content_rev(files)?;
79
80 let rev_dir = self.rev_dir(&rev);
81 if !rev_dir.exists() {
82 let staging = self.root.join(format!("{rev}.tmp"));
83 if staging.exists() {
84 std::fs::remove_dir_all(&staging)
85 .map_err(|source| NewgitError::io(&staging, source))?;
86 }
87 for (relative, absolute) in files {
88 copy_file(absolute, &staging.join(relative))?;
89 }
90 create_dir_all(&staging)?;
91 std::fs::rename(&staging, &rev_dir)
92 .map_err(|source| NewgitError::io(&rev_dir, source))?;
93 }
94
95 Ok(CaptureOutcome {
96 rev,
97 files: files.len(),
98 })
99 }
100
101 pub fn rev_path(&self, rev: &str) -> Utf8PathBuf {
103 self.rev_dir(rev)
104 }
105
106 pub fn restore(
110 &self,
111 workspace: &Utf8Path,
112 definition: &TrackerDefinition,
113 rev: &str,
114 ) -> Result<usize> {
115 let rev_dir = self.rev_dir(rev);
116 if !rev_dir.is_dir() {
117 return Err(NewgitError::NoSnapshot {
118 tracker: self.tracker.clone(),
119 rev: rev.to_owned(),
120 });
121 }
122
123 clear_owned_paths(workspace, definition)?;
124
125 let files = collect_owned_files(&rev_dir, definition)?;
128 for (relative, absolute) in &files {
129 copy_file(absolute, &workspace.join(relative))?;
130 }
131 Ok(files.len())
132 }
133}
134
135pub fn clear_owned_paths(workspace: &Utf8Path, definition: &TrackerDefinition) -> Result<()> {
138 for owned in &definition.paths {
139 let target = workspace.join(owned);
140 if target.is_dir() {
141 std::fs::remove_dir_all(&target).map_err(|source| NewgitError::io(&target, source))?;
142 } else if target.is_file() {
143 std::fs::remove_file(&target).map_err(|source| NewgitError::io(&target, source))?;
144 }
145 }
146 Ok(())
147}
148
149pub fn copy_file(from: &Utf8Path, to: &Utf8Path) -> Result<()> {
150 if let Some(parent) = to.parent() {
151 create_dir_all(parent)?;
152 }
153 std::fs::copy(from, to)
154 .map(|_| ())
155 .map_err(|source| NewgitError::io(to.to_path_buf(), source))
156}