1use crate::adapter::Fs;
15use crate::error::SessionError;
16use crate::layout::StorePaths;
17use crate::manifest::{HistoryRecord, read_records};
18use crate::session::{find_record, journal_path, load_state};
19
20fn walk_parents(
25 records: &[HistoryRecord],
26 start: &str,
27 n: usize,
28 spec: &str,
29) -> Result<String, SessionError> {
30 let mut cur = start.to_owned();
31 for _ in 0..n {
32 let rec = find_record(records, &cur)
33 .ok_or_else(|| SessionError::new(format!("unknown record {cur}")))?;
34 cur = rec
35 .parent
36 .clone()
37 .ok_or_else(|| SessionError::new(format!("revspec {spec} goes past the root")))?;
38 }
39 Ok(cur)
40}
41
42fn resolve_time(records: &[HistoryRecord], target: u128) -> Option<String> {
45 records
46 .iter()
47 .filter_map(|r| {
48 r.timestamp_ms
49 .filter(|&ts| ts <= target)
50 .map(|ts| (ts, r.seq, &r.id))
51 })
52 .max_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)))
53 .map(|(_, _, id)| id.clone())
54}
55
56fn resolve_latest(records: &[HistoryRecord], label: &str) -> Option<String> {
59 records
60 .iter()
61 .filter(|r| r.op_kind.as_deref() == Some(label) || r.label.as_deref() == Some(label))
62 .max_by_key(|r| r.seq)
63 .map(|r| r.id.clone())
64}
65
66fn resolve_id_or_prefix(records: &[HistoryRecord], s: &str) -> Result<String, SessionError> {
69 if let Some(rec) = find_record(records, s) {
71 return Ok(rec.id.clone());
72 }
73 let matches: Vec<&HistoryRecord> = records.iter().filter(|r| r.id.starts_with(s)).collect();
75 match matches.len() {
76 0 => Err(SessionError::new(format!("no record matching {s}"))),
77 1 => Ok(matches[0].id.clone()),
78 n => Err(SessionError::new(format!(
79 "ambiguous revspec {s} matches {n} records"
80 ))),
81 }
82}
83
84pub fn resolve_revspec(
90 records: &[HistoryRecord],
91 head: Option<&str>,
92 spec: &str,
93) -> Result<String, SessionError> {
94 let s = spec.trim();
95
96 if s.is_empty() {
98 return Err(SessionError::new("empty revspec"));
99 }
100
101 if s == "@head" || s == "HEAD" || s == "@" {
103 return head
104 .map(str::to_owned)
105 .ok_or_else(|| SessionError::new("no HEAD in session"));
106 }
107
108 if let Some(rest) = s.strip_prefix("@head").or_else(|| s.strip_prefix("HEAD")) {
110 let head_id = head.ok_or_else(|| SessionError::new("no HEAD in session"))?;
112 if rest == "^" {
113 return walk_parents(records, head_id, 1, s);
114 }
115 if let Some(n_str) = rest.strip_prefix('~') {
116 let n = n_str
117 .parse::<usize>()
118 .map_err(|_| SessionError::new(format!("unrecognized HEAD revspec: {s}")))?;
119 return walk_parents(records, head_id, n, s);
120 }
121 return Err(SessionError::new(format!("unrecognized HEAD revspec: {s}")));
122 }
123
124 if let Some(ts_str) = s.strip_prefix("@time:") {
126 let target = ts_str
127 .parse::<u128>()
128 .map_err(|_| SessionError::new(format!("invalid @time timestamp: {ts_str}")))?;
129 return resolve_time(records, target)
130 .ok_or_else(|| SessionError::new(format!("no record at or before time {target}")));
131 }
132
133 if let Some(label) = s.strip_prefix("@latest:") {
135 return resolve_latest(records, label)
136 .ok_or_else(|| SessionError::new(format!("no record matching @latest:{label}")));
137 }
138
139 if let Ok(n) = s.parse::<u64>() {
141 return records
142 .iter()
143 .find(|r| r.seq == n)
144 .map(|r| r.id.clone())
145 .ok_or_else(|| SessionError::new(format!("no record with seq {n}")));
146 }
147
148 resolve_id_or_prefix(records, s)
150}
151
152pub fn resolve_revspec_for(
156 fs: &impl Fs,
157 paths: &StorePaths,
158 doc_id: &str,
159 spec: &str,
160) -> Result<String, SessionError> {
161 let state = load_state(fs, paths, doc_id)?;
162 let records = read_records(fs, &journal_path(paths, doc_id))?;
163 resolve_revspec(&records, state.head.as_deref(), spec)
164}
165
166#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::adapter::{FakeClock, FakeRng, MemFs};
172 use crate::layout::StorePaths;
173 use crate::session::record_state;
174
175 fn make_chain() -> Vec<HistoryRecord> {
184 let mut r0 = HistoryRecord::new("r0", 0, None, "snap0");
185 r0.timestamp_ms = Some(100);
186 r0.op_kind = Some("edit".to_owned());
187
188 let mut r1 = HistoryRecord::new("r1", 1, Some("r0".to_owned()), "snap1");
189 r1.timestamp_ms = Some(200);
190 r1.op_kind = Some("external".to_owned());
191
192 let mut r2 = HistoryRecord::new("r2", 2, Some("r1".to_owned()), "snap2");
193 r2.timestamp_ms = Some(300);
194 r2.op_kind = Some("edit".to_owned());
195 r2.label = Some("v1.0".to_owned());
196
197 let mut r3 = HistoryRecord::new("r3", 3, Some("r2".to_owned()), "snap3");
198 r3.timestamp_ms = Some(400);
199 r3.op_kind = Some("edit".to_owned());
200
201 vec![r0, r1, r2, r3]
202 }
203
204 const HEAD: &str = "r3";
205
206 #[test]
209 fn head_forms() {
210 let records = make_chain();
211 assert_eq!(
212 resolve_revspec(&records, Some(HEAD), "@head").unwrap(),
213 "r3"
214 );
215 assert_eq!(resolve_revspec(&records, Some(HEAD), "HEAD").unwrap(), "r3");
216 assert_eq!(resolve_revspec(&records, Some(HEAD), "@").unwrap(), "r3");
217 }
218
219 #[test]
222 fn head_tilde() {
223 let records = make_chain();
224 assert_eq!(
225 resolve_revspec(&records, Some(HEAD), "@head~1").unwrap(),
226 "r2"
227 );
228 assert_eq!(
229 resolve_revspec(&records, Some(HEAD), "HEAD~2").unwrap(),
230 "r1"
231 );
232 assert_eq!(
233 resolve_revspec(&records, Some(HEAD), "@head~0").unwrap(),
234 "r3"
235 );
236 assert_eq!(
237 resolve_revspec(&records, Some(HEAD), "@head^").unwrap(),
238 "r2"
239 );
240 }
241
242 #[test]
245 fn head_tilde_past_root_errors() {
246 let records = make_chain();
247 assert!(resolve_revspec(&records, Some(HEAD), "@head~99").is_err());
248 }
249
250 #[test]
253 fn head_no_session_errors() {
254 let records = make_chain();
255 assert!(resolve_revspec(&records, None, "@head").is_err());
256 }
257
258 #[test]
261 fn seq_lookup() {
262 let records = make_chain();
263 assert_eq!(resolve_revspec(&records, Some(HEAD), "0").unwrap(), "r0");
264 assert_eq!(resolve_revspec(&records, Some(HEAD), "2").unwrap(), "r2");
265 assert!(resolve_revspec(&records, Some(HEAD), "9").is_err());
266 }
267
268 #[test]
271 fn time_at_or_before() {
272 let records = make_chain();
273 assert_eq!(
275 resolve_revspec(&records, Some(HEAD), "@time:250").unwrap(),
276 "r1"
277 );
278 assert_eq!(
280 resolve_revspec(&records, Some(HEAD), "@time:400").unwrap(),
281 "r3"
282 );
283 assert!(resolve_revspec(&records, Some(HEAD), "@time:50").is_err());
285 }
286
287 #[test]
290 fn latest_label() {
291 let records = make_chain();
292 assert_eq!(
294 resolve_revspec(&records, Some(HEAD), "@latest:external").unwrap(),
295 "r1"
296 );
297 assert_eq!(
299 resolve_revspec(&records, Some(HEAD), "@latest:v1.0").unwrap(),
300 "r2"
301 );
302 assert!(resolve_revspec(&records, Some(HEAD), "@latest:nope").is_err());
304 }
305
306 #[test]
309 fn id_exact_and_prefix() {
310 let records = make_chain();
311 assert_eq!(resolve_revspec(&records, Some(HEAD), "r2").unwrap(), "r2");
313
314 assert_eq!(resolve_revspec(&records, Some(HEAD), "r0").unwrap(), "r0");
317
318 let mut extra = records.clone();
320 let ra1 = HistoryRecord::new("ra1", 10, None, "snapA");
321 let ra2 = HistoryRecord::new("ra2", 11, None, "snapB");
322 extra.push(ra1);
323 extra.push(ra2);
324
325 assert!(resolve_revspec(&extra, Some(HEAD), "ra").is_err());
327 assert_eq!(resolve_revspec(&extra, Some(HEAD), "ra1").unwrap(), "ra1");
329 }
330
331 #[test]
334 fn malformed() {
335 let records = make_chain();
336 assert!(resolve_revspec(&records, Some(HEAD), "").is_err());
337 assert!(resolve_revspec(&records, Some(HEAD), "@head~x").is_err());
338 assert!(resolve_revspec(&records, Some(HEAD), "@bogus").is_err());
339 }
340
341 #[test]
344 fn resolve_revspec_for_smoke() {
345 let fs = MemFs::new();
346 let paths = StorePaths::new("/data");
347 let clock = FakeClock(std::time::SystemTime::UNIX_EPOCH);
348 let rng = FakeRng(0);
349
350 record_state(&fs, &paths, &clock, &rng, "doc1", b"v1", None).unwrap(); record_state(&fs, &paths, &clock, &rng, "doc1", b"v2", None).unwrap(); let head_id = resolve_revspec_for(&fs, &paths, "doc1", "@head").unwrap();
355 assert_eq!(head_id, "r1");
356
357 let parent_id = resolve_revspec_for(&fs, &paths, "doc1", "@head~1").unwrap();
359 assert_eq!(parent_id, "r0");
360 }
361}