1use alloc::collections::BTreeMap;
4use std::collections::HashSet;
5
6use camino::Utf8PathBuf;
7use derive_more::Display;
8use serde::{Deserialize, Serialize};
9
10use crate::commit::{lock, prune, remove_if_present, to_toml, write};
11use crate::digest::Digest;
12use crate::errors::{MergeError, Result};
13use crate::path::RelPath;
14use crate::target::{
15 ANSWERS, CONFIG, CONFLICTS, DIR, LOCK, STATE, State, Target, UNDO, UNDO_BLOBS, V1,
16 read_optional, read_toml,
17};
18
19const RECORDS: [&str; 4] = [CONFIG, ANSWERS, LOCK, STATE];
23
24#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Display)]
26pub(crate) enum Location {
27 File(RelPath),
29 #[display("{DIR}/{CONFLICTS}/{_0}")]
31 Sidecar(RelPath),
32 #[display("{DIR}/{_0}")]
34 Record(&'static str),
35}
36
37impl Location {
38 fn under(&self, target: &Target) -> Utf8PathBuf {
40 match self {
41 Self::File(path) => path.under(target.root()),
42 Self::Sidecar(path) => target.sidecar(path),
43 Self::Record(name) => target.dir().join(name),
44 }
45 }
46
47 fn parse(key: &str) -> Option<Self> {
49 let Some(inner) = key.strip_prefix(".devset/") else {
50 return RelPath::new(key).ok().map(Self::File);
51 };
52 if let Some(path) = inner.strip_prefix("conflicts/") {
53 return RelPath::new(path).ok().map(Self::Sidecar);
54 }
55 RECORDS.into_iter().find(|&name| name == inner).map(Self::Record)
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(deny_unknown_fields)]
62struct Change {
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 before: Option<Digest>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 after: Option<Digest>,
69}
70
71#[derive(Debug, Default, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub(crate) struct Undo {
75 version: V1,
77 #[serde(default)]
79 files: BTreeMap<String, Change>,
80}
81
82impl Undo {
83 pub(crate) fn load(target: &Target) -> Result<Self> {
85 Ok(read_toml(&target.dir(), &format!("{CONFLICTS}/{DIR}/{UNDO}"))?.unwrap_or_default())
86 }
87
88 pub(crate) fn record(
92 &mut self, target: &Target, location: &Location, bytes: Option<&[u8]>,
93 ) -> Result<()> {
94 let after = bytes.map(Digest::of);
95 let key = location.to_string();
96 if let Some(change) = self.files.get_mut(&key) {
97 change.after = after;
98 if change.before == after {
99 self.files.remove(&key);
100 }
101 return Ok(());
102 }
103 let current = read_optional(&location.under(target))?;
104 let before = current.as_deref().map(Digest::of);
105 if before == after {
106 return Ok(());
107 }
108 if let (Some(current), Some(digest)) = (¤t, before) {
109 let blob = target.unfinished().join(UNDO_BLOBS).join(digest.to_string());
110 if !blob.is_file() {
111 write(&blob, current)?;
112 }
113 }
114 self.files.insert(key, Change { before, after });
115 Ok(())
116 }
117
118 pub(crate) fn save(&self, target: &Target) -> Result<()> {
120 write(&target.unfinished().join(UNDO), to_toml(self)?.as_bytes())
121 }
122
123 fn changes(&self) -> impl Iterator<Item = (Location, Change)> + '_ {
125 self.files.iter().filter_map(|(key, &change)| Some((Location::parse(key)?, change)))
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
131pub enum Revert {
132 Restore(RelPath),
134 Remove(RelPath),
136}
137
138#[derive(Debug)]
145pub struct Rollback {
146 undo: Undo,
148 reverts: Vec<Revert>,
150 changed: Vec<String>,
152}
153
154impl Rollback {
155 pub fn of(target: &Target) -> Result<Self> {
161 if !target.dir().join(CONFLICTS).is_dir() {
162 return Err(MergeError::NothingToAbort.into());
163 }
164 let undo = Undo::load(target)?;
165 let (mut reverts, mut changed) = (Vec::new(), Vec::new());
166 for (location, change) in undo.changes() {
167 let now = read_optional(&location.under(target))?.as_deref().map(Digest::of);
168 if now == change.before {
170 continue;
171 }
172 if now != change.after {
173 changed.push(location.to_string());
174 }
175 if let Location::File(path) = location {
176 reverts.push(match change.before {
177 Some(_) => Revert::Restore(path),
178 None => Revert::Remove(path),
179 });
180 }
181 }
182 Ok(Self { undo, reverts, changed })
183 }
184
185 #[must_use]
187 pub fn reverts(&self) -> &[Revert] {
188 &self.reverts
189 }
190
191 #[must_use]
193 pub fn changed(&self) -> &[String] {
194 &self.changed
195 }
196
197 pub fn apply(self, target: &Target, force: bool) -> Result<Vec<Revert>> {
210 if !force && !self.changed.is_empty() {
211 return Err(MergeError::ChangedSince { paths: self.changed }.into());
212 }
213 let _guard = lock(target)?;
214 let mut changes: Vec<(Location, Change)> = self.undo.changes().collect();
215 changes.sort_by_key(|(location, _)| match location {
218 Location::Record(name) => RECORDS.iter().position(|record| record == name),
219 Location::File(_) | Location::Sidecar(_) => None,
220 });
221 let blobs = target.unfinished().join(UNDO_BLOBS);
222 for (location, change) in &changes {
223 let file = location.under(target);
224 match change.before {
225 None => remove_if_present(&file)?,
226 Some(digest) => {
227 let saved = read_optional(&blobs.join(digest.to_string()))?
228 .filter(|bytes| Digest::of(bytes) == digest)
229 .ok_or_else(|| MergeError::CorruptUndo { path: location.to_string() })?;
230 write(&file, &saved)?;
231 },
232 }
233 }
234 let state: State = read_toml(&target.dir(), STATE)?.unwrap_or_default();
235 let live: HashSet<Digest> = state.bases().collect();
236 prune(target, &live)?;
237 remove_if_present(&target.dir().join(CONFLICTS))?;
238 Ok(self.reverts)
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::Location;
245 use crate::path::RelPath;
246
247 #[test]
248 fn locations_round_trip_through_their_keys() {
249 let path = || RelPath::new(".github/ci.yml").expect("a valid path");
250 for location in
251 [Location::File(path()), Location::Sidecar(path()), Location::Record("lock.toml")]
252 {
253 assert_eq!(Location::parse(&location.to_string()), Some(location), "as written");
254 }
255 for key in [".devset/base/x", ".devset/conflicts/.devset/undo.toml", "../escape"] {
256 assert_eq!(Location::parse(key), None, "{key} names no location");
257 }
258 }
259}