1use std::collections::{BTreeMap, BTreeSet};
8use std::path::PathBuf;
9
10use cadvm_store::ObjectId;
11
12use crate::error::{CoreError, Result};
13use crate::model::{CommitBody, Manifest};
14use crate::repo::{Head, Repository};
15use crate::revision;
16use crate::status::working_tree_status;
17use crate::worktree;
18
19#[derive(Debug, Clone, Default)]
21pub struct RestoreOutcome {
22 pub written: Vec<PathBuf>,
23 pub deleted: Vec<PathBuf>,
24}
25
26fn working_hashes(repo: &Repository) -> Result<BTreeMap<PathBuf, ObjectId>> {
28 let mut map = BTreeMap::new();
29 for rel in worktree::scan_step_files(repo)? {
30 map.insert(rel.clone(), worktree::hash_working_file(repo, &rel)?);
31 }
32 Ok(map)
33}
34
35fn restore(
45 repo: &Repository,
46 baseline: &Manifest,
47 target: &Manifest,
48 force: bool,
49 only: Option<&BTreeSet<PathBuf>>,
50) -> Result<RestoreOutcome> {
51 let working = working_hashes(repo)?;
52 let selected = |path: &PathBuf| only.is_none_or(|set| set.contains(path));
53
54 if !force {
56 for (path, entry) in &target.files {
58 if !selected(path) {
59 continue;
60 }
61 let on_disk = working.get(path);
62 if on_disk == Some(&entry.raw_hash) {
63 continue; }
65 if let Some(disk_hash) = on_disk {
66 let baseline_hash = baseline.files.get(path).map(|e| &e.raw_hash);
67 if Some(disk_hash) != baseline_hash {
68 return Err(CoreError::WouldOverwriteDirtyFile(path.clone()));
70 }
71 }
72 }
73 if only.is_none() {
76 for (path, entry) in &baseline.files {
77 if target.files.contains_key(path) {
78 continue;
79 }
80 if let Some(disk_hash) = working.get(path) {
81 if disk_hash != &entry.raw_hash {
82 return Err(CoreError::WouldOverwriteDirtyFile(path.clone()));
83 }
84 }
85 }
86 }
87 }
88
89 let mut outcome = RestoreOutcome::default();
91
92 for (path, entry) in &target.files {
93 if !selected(path) {
94 continue;
95 }
96 if working.get(path) == Some(&entry.raw_hash) {
97 continue;
98 }
99 let content = repo.store().read_file_content(&entry.blob_ref)?;
100 let abs = worktree::abs_path(repo, path);
101 if let Some(parent) = abs.parent() {
102 std::fs::create_dir_all(parent).map_err(|e| CoreError::io(parent, e))?;
103 }
104 std::fs::write(&abs, &content).map_err(|e| CoreError::io(&abs, e))?;
105 outcome.written.push(path.clone());
106 }
107
108 if only.is_none() {
109 for path in baseline.files.keys() {
110 if target.files.contains_key(path) {
111 continue;
112 }
113 if !working.contains_key(path) {
115 continue;
116 }
117 let abs = worktree::abs_path(repo, path);
118 match std::fs::remove_file(&abs) {
119 Ok(()) => outcome.deleted.push(path.clone()),
120 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
121 Err(e) => return Err(CoreError::io(&abs, e)),
122 }
123 }
124 }
125
126 outcome.written.sort();
127 outcome.deleted.sort();
128 Ok(outcome)
129}
130
131#[derive(Debug, Clone)]
133pub struct CheckoutOutcome {
134 pub commit_id: ObjectId,
135 pub restore: RestoreOutcome,
136}
137
138pub fn checkout(
144 repo: &Repository,
145 rev: &str,
146 paths: &[PathBuf],
147 force: bool,
148) -> Result<CheckoutOutcome> {
149 let commit_id = revision::resolve(repo, rev)?;
150 let target = repo.manifest_of_commit(&commit_id)?;
151 let baseline = repo.head_manifest()?;
152
153 let only: Option<BTreeSet<PathBuf>> = if paths.is_empty() {
154 None
155 } else {
156 let set: BTreeSet<PathBuf> = paths.iter().cloned().collect();
157 for path in &set {
158 if !target.files.contains_key(path) {
159 return Err(CoreError::PathNotInRevision {
160 path: path.clone(),
161 rev: rev.to_string(),
162 });
163 }
164 }
165 Some(set)
166 };
167
168 let restore = restore(repo, &baseline, &target, force, only.as_ref())?;
169 Ok(CheckoutOutcome { commit_id, restore })
170}
171
172#[derive(Debug, Clone)]
174pub struct SwitchOutcome {
175 pub branch: String,
176 pub commit_id: Option<ObjectId>,
177 pub restore: RestoreOutcome,
178}
179
180pub fn switch(repo: &Repository, branch: &str, force: bool) -> Result<SwitchOutcome> {
183 if !repo.branch_exists(branch) {
184 return Err(CoreError::NoSuchBranch(branch.to_string()));
185 }
186
187 if !force && !working_tree_status(repo)?.is_clean() {
188 return Err(CoreError::DirtyWorkingTree {
189 action: "switch".to_string(),
190 });
191 }
192
193 let baseline = repo.head_manifest()?;
194 let commit_id = repo.read_ref(branch)?;
195 let target = match &commit_id {
196 Some(id) => repo.manifest_of_commit(id)?,
197 None => Manifest::empty(),
198 };
199 let restore = restore(repo, &baseline, &target, force, None)?;
200 repo.write_head(&Head::Branch(branch.to_string()))?;
201
202 Ok(SwitchOutcome {
203 branch: branch.to_string(),
204 commit_id,
205 restore,
206 })
207}
208
209#[derive(Debug, Clone)]
211pub struct RevertOutcome {
212 pub new_commit_id: ObjectId,
213 pub reverted_commit_id: ObjectId,
214 pub restore: RestoreOutcome,
215 pub branch: Option<String>,
216}
217
218pub fn revert(
223 repo: &Repository,
224 rev: &str,
225 force: bool,
226 timestamp_unix: i64,
227) -> Result<RevertOutcome> {
228 let target_id = revision::resolve(repo, rev)?;
229 let head_id = repo
230 .head_commit_id()?
231 .ok_or_else(|| CoreError::UnknownRevision("HEAD".to_string()))?;
232 if target_id != head_id {
233 return Err(CoreError::RevertNonHead(rev.to_string()));
234 }
235
236 if !force && !working_tree_status(repo)?.is_clean() {
237 return Err(CoreError::DirtyWorkingTree {
238 action: "revert".to_string(),
239 });
240 }
241
242 let head_commit = repo.read_commit(&head_id)?;
243 let parent_id = head_commit
244 .parents
245 .first()
246 .cloned()
247 .ok_or(CoreError::NoParent)?;
248 let parent_commit = repo.read_commit(&parent_id)?;
249
250 let baseline = repo.read_manifest(&head_commit.manifest)?;
252 let target = repo.read_manifest(&parent_commit.manifest)?;
253 let restore = restore(repo, &baseline, &target, force, None)?;
254
255 let body = CommitBody {
257 parents: vec![head_id.clone()],
258 manifest: parent_commit.manifest.clone(),
259 message: format!("Revert \"{}\"", head_commit.message),
260 timestamp_unix,
261 author: Some(crate::config::resolve_author(repo)?),
262 };
263 let new_commit_id = repo.write_commit(&body)?;
264
265 let branch = match repo.read_head()? {
266 Head::Branch(name) => {
267 repo.write_ref(&name, &new_commit_id)?;
268 Some(name)
269 }
270 Head::Detached(_) => {
271 repo.write_head(&Head::Detached(new_commit_id.clone()))?;
272 None
273 }
274 };
275
276 Ok(RevertOutcome {
277 new_commit_id,
278 reverted_commit_id: head_id,
279 restore,
280 branch,
281 })
282}