OXYGENHUB
# e10ba80000e1 oxygen / crates / oxy-core / src / oplog.rs
15.4 KiB · Rust · last changed undo: this workspace's last command, saying what it undid 24 days ago source blame history raw
1
//! Operation log objects (spec §12.1): every mutation of local repository
2
//! state records an `Op` whose `view` is the complete set of references and
3
//! workspaces afterwards.
4
//!
5
//! Encoding: `bookmarks` is an array of `[name, state_ref]` sorted by name and
6
//! `workspaces` an array of `[workspace_id, change_hash]` sorted by id, since
7
//! canonical maps carry only integer keys. Repository format 1 encoded a
8
//! bookmark as `[name, [heads]]`; `Op::decode_format1` reads that for migration.
9
10
use crate::cbor::{self, Value};
11
use crate::hash::{hash_object, ChangeHash, ObjectRef, ObjectType};
12
use crate::ids::WorkspaceId;
13
use crate::objects::{
14
    array, bytes, check_version, field, oref, text, Hlc, ObjectError, FORMAT_VERSION,
15
};
16
use std::collections::BTreeMap;
17
18
#[derive(Clone, Debug, Default, PartialEq, Eq)]
19
pub struct View {
20
    /// Bookmark name → ObjectRef of a `state` object (spec §10.1).
21
    pub bookmarks: BTreeMap<String, ObjectRef>,
22
    pub workspaces: BTreeMap<WorkspaceId, ChangeHash>,
23
    /// `remotes/<remote>/bookmarks/<name>`: remote → bookmark → state ObjectRef.
24
    pub remotes: BTreeMap<String, BTreeMap<String, ObjectRef>>,
25
    /// The `policy` reference (spec §10.1, §14.5): ObjectRef of the current policy.
26
    pub policy: Option<ObjectRef>,
27
    /// `tags/<name>`: immutable states (spec §10.1).
28
    pub tags: BTreeMap<String, ObjectRef>,
29
}
30
31
/// A format-1 view, only produced by `Op::decode_format1`.
32
#[derive(Clone, Debug, Default, PartialEq, Eq)]
33
pub struct Format1View {
34
    pub bookmarks: BTreeMap<String, Vec<ChangeHash>>,
35
    pub workspaces: BTreeMap<WorkspaceId, ChangeHash>,
36
}
37
38
#[derive(Clone, Debug, PartialEq, Eq)]
39
pub struct Op {
40
    pub parents: Vec<ObjectRef>,
41
    pub hlc: Hlc,
42
    pub command: String,
43
    pub view: View,
44
    /// The workspace whose command recorded it (oxygen#99); absent in
45
    /// operations written before it was kept.
46
    pub workspace: Option<WorkspaceId>,
47
    /// For an undo that reverted an older operation in place: that
48
    /// operation, so a further undo goes on past it.
49
    pub undoes: Option<ObjectRef>,
50
}
51
52
impl View {
53
    fn to_value(&self) -> Value {
54
        Value::map([
55
            (
56
                0,
57
                Value::Array(
58
                    self.bookmarks
59
                        .iter()
60
                        .map(|(n, st)| {
61
                            Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])
62
                        })
63
                        .collect(),
64
                ),
65
            ),
66
            (
67
                1,
68
                Value::Array(
69
                    self.workspaces
70
                        .iter()
71
                        .map(|(w, h)| {
72
                            Value::Array(vec![
73
                                Value::Bytes(w.0.to_vec()),
74
                                Value::Bytes(h.0.to_vec()),
75
                            ])
76
                        })
77
                        .collect(),
78
                ),
79
            ),
80
            (
81
                2,
82
                Value::Array(
83
                    self.remotes
84
                        .iter()
85
                        .map(|(r, bms)| {
86
                            Value::Array(vec![
87
                                Value::Text(r.clone()),
88
                                Value::Array(
89
                                    bms.iter()
90
                                        .map(|(n, st)| {
91
                                            Value::Array(vec![
92
                                                Value::Text(n.clone()),
93
                                                Value::Bytes(st.0.to_vec()),
94
                                            ])
95
                                        })
96
                                        .collect(),
97
                                ),
98
                            ])
99
                        })
100
                        .collect(),
101
                ),
102
            ),
103
            (
104
                3,
105
                self.policy
106
                    .map(|p| Value::Bytes(p.0.to_vec()))
107
                    .unwrap_or(Value::Null),
108
            ),
109
            (
110
                4,
111
                Value::Array(
112
                    self.tags
113
                        .iter()
114
                        .map(|(n, st)| {
115
                            Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])
116
                        })
117
                        .collect(),
118
                ),
119
            ),
120
        ])
121
    }
122
123
    fn from_value(v: &Value) -> Result<View, ObjectError> {
124
        let m = v
125
            .as_map()
126
            .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
127
        let mut view = View::default();
128
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
129
            let pair = array(b, "bookmark")?;
130
            if pair.len() != 2 {
131
                return Err(ObjectError::Schema(
132
                    "bookmark: expected [name, heads]".into(),
133
                ));
134
            }
135
            let name = text(&pair[0], "bookmark name")?.to_owned();
136
            view.bookmarks
137
                .insert(name, oref(&pair[1], "bookmark state")?);
138
        }
139
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
140
        if let Some(rs) = m.get(&2) {
141
            for r in array(rs, "remotes")? {
142
                let pair = array(r, "remote")?;
143
                if pair.len() != 2 {
144
                    return Err(ObjectError::Schema(
145
                        "remote: expected [name, bookmarks]".into(),
146
                    ));
147
                }
148
                let name = text(&pair[0], "remote name")?.to_owned();
149
                let mut bms = BTreeMap::new();
150
                for b in array(&pair[1], "remote bookmarks")? {
151
                    let bp = array(b, "remote bookmark")?;
152
                    if bp.len() != 2 {
153
                        return Err(ObjectError::Schema(
154
                            "remote bookmark: expected [name, state]".into(),
155
                        ));
156
                    }
157
                    bms.insert(
158
                        text(&bp[0], "bookmark name")?.to_owned(),
159
                        oref(&bp[1], "bookmark state")?,
160
                    );
161
                }
162
                view.remotes.insert(name, bms);
163
            }
164
        }
165
        if let Some(p) = m.get(&3) {
166
            view.policy = if p.is_null() {
167
                None
168
            } else {
169
                Some(oref(p, "policy")?)
170
            };
171
        }
172
        if let Some(ts) = m.get(&4) {
173
            for t in array(ts, "tags")? {
174
                let pair = array(t, "tag")?;
175
                if pair.len() != 2 {
176
                    return Err(ObjectError::Schema("tag: expected [name, state]".into()));
177
                }
178
                view.tags.insert(
179
                    text(&pair[0], "tag name")?.to_owned(),
180
                    oref(&pair[1], "tag state")?,
181
                );
182
            }
183
        }
184
        Ok(view)
185
    }
186
}
187
188
fn workspaces_from_value(v: &Value) -> Result<BTreeMap<WorkspaceId, ChangeHash>, ObjectError> {
189
    let mut out = BTreeMap::new();
190
    for w in array(v, "workspaces")? {
191
        let pair = array(w, "workspace")?;
192
        if pair.len() != 2 {
193
            return Err(ObjectError::Schema(
194
                "workspace: expected [id, change]".into(),
195
            ));
196
        }
197
        let id = WorkspaceId::from_slice(bytes(&pair[0], "workspace id")?)
198
            .ok_or_else(|| ObjectError::Schema("workspace id: expected 16 bytes".into()))?;
199
        out.insert(id, oref(&pair[1], "workspace change")?);
200
    }
201
    Ok(out)
202
}
203
204
impl Format1View {
205
    fn from_value(v: &Value) -> Result<Format1View, ObjectError> {
206
        let m = v
207
            .as_map()
208
            .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
209
        let mut view = Format1View::default();
210
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
211
            let pair = array(b, "bookmark")?;
212
            if pair.len() != 2 {
213
                return Err(ObjectError::Schema(
214
                    "bookmark: expected [name, heads]".into(),
215
                ));
216
            }
217
            let name = text(&pair[0], "bookmark name")?.to_owned();
218
            let mut heads = array(&pair[1], "heads")?
219
                .iter()
220
                .map(|h| oref(h, "head"))
221
                .collect::<Result<Vec<_>, _>>()?;
222
            heads.sort();
223
            heads.dedup();
224
            view.bookmarks.insert(name, heads);
225
        }
226
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
227
        Ok(view)
228
    }
229
230
    /// Format-1 encoding, kept for the migration test.
231
    #[cfg(test)]
232
    pub(crate) fn to_value(&self) -> Value {
233
        Value::map([
234
            (
235
                0,
236
                Value::Array(
237
                    self.bookmarks
238
                        .iter()
239
                        .map(|(n, hs)| {
240
                            Value::Array(vec![
241
                                Value::Text(n.clone()),
242
                                Value::Array(
243
                                    hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect(),
244
                                ),
245
                            ])
246
                        })
247
                        .collect(),
248
                ),
249
            ),
250
            (
251
                1,
252
                Value::Array(
253
                    self.workspaces
254
                        .iter()
255
                        .map(|(w, h)| {
256
                            Value::Array(vec![
257
                                Value::Bytes(w.0.to_vec()),
258
                                Value::Bytes(h.0.to_vec()),
259
                            ])
260
                        })
261
                        .collect(),
262
                ),
263
            ),
264
        ])
265
    }
266
}
267
268
/// A format-1 operation as read for migration.
269
#[derive(Clone, Debug, PartialEq, Eq)]
270
pub struct Format1Op {
271
    pub parents: Vec<ObjectRef>,
272
    pub hlc: Hlc,
273
    pub command: String,
274
    pub view: Format1View,
275
}
276
277
impl Format1Op {
278
    #[cfg(test)]
279
    pub(crate) fn encode(&self) -> Vec<u8> {
280
        cbor::encode(&Value::map([
281
            (0, Value::Uint(FORMAT_VERSION)),
282
            (
283
                1,
284
                Value::Array(
285
                    self.parents
286
                        .iter()
287
                        .map(|p| Value::Bytes(p.0.to_vec()))
288
                        .collect(),
289
                ),
290
            ),
291
            (2, self.hlc.to_value()),
292
            (3, Value::Text(self.command.clone())),
293
            (4, self.view.to_value()),
294
        ]))
295
    }
296
}
297
298
/// Suffix on the `command` of operations recorded by an automatic
299
/// working-copy snapshot rather than by a user command.
300
pub const SNAPSHOT_SUFFIX: &str = " (snapshot)";
301
302
impl Op {
303
    /// Whether this operation only recorded a working-copy snapshot.
304
    pub fn is_snapshot(&self) -> bool {
305
        self.command.ends_with(SNAPSHOT_SUFFIX)
306
    }
307
308
    pub fn to_value(&self) -> Value {
309
        let mut fields = vec![
310
            (0, Value::Uint(FORMAT_VERSION)),
311
            (
312
                1,
313
                Value::Array(
314
                    self.parents
315
                        .iter()
316
                        .map(|p| Value::Bytes(p.0.to_vec()))
317
                        .collect(),
318
                ),
319
            ),
320
            (2, self.hlc.to_value()),
321
            (3, Value::Text(self.command.clone())),
322
            (4, self.view.to_value()),
323
        ];
324
        // Absent when unknown, so older operations keep their encoding.
325
        if let Some(w) = &self.workspace {
326
            fields.push((5, Value::Bytes(w.0.to_vec())));
327
        }
328
        if let Some(u) = &self.undoes {
329
            fields.push((6, Value::Bytes(u.0.to_vec())));
330
        }
331
        Value::map(fields)
332
    }
333
    pub fn encode(&self) -> Vec<u8> {
334
        cbor::encode(&self.to_value())
335
    }
336
    pub fn hash(&self) -> ObjectRef {
337
        hash_object(ObjectType::Op, &self.encode())
338
    }
339
    pub fn decode(b: &[u8]) -> Result<Op, ObjectError> {
340
        let v = cbor::decode(b)?;
341
        let m = v
342
            .as_map()
343
            .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
344
        check_version(m)?;
345
        Ok(Op {
346
            parents: array(field(m, 1, "parents")?, "parents")?
347
                .iter()
348
                .map(|p| oref(p, "parent"))
349
                .collect::<Result<Vec<_>, _>>()?,
350
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
351
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
352
            view: View::from_value(field(m, 4, "view")?)?,
353
            workspace: match m.get(&5) {
354
                Some(v) => Some(
355
                    WorkspaceId::from_slice(bytes(v, "workspace")?).ok_or_else(|| {
356
                        ObjectError::Schema("workspace: expected 16 bytes".into())
357
                    })?,
358
                ),
359
                None => None,
360
            },
361
            undoes: m.get(&6).map(|v| oref(v, "undoes")).transpose()?,
362
        })
363
    }
364
365
    /// Read an operation written by repository format 1.
366
    pub fn decode_format1(b: &[u8]) -> Result<Format1Op, ObjectError> {
367
        let v = cbor::decode(b)?;
368
        let m = v
369
            .as_map()
370
            .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
371
        check_version(m)?;
372
        Ok(Format1Op {
373
            parents: array(field(m, 1, "parents")?, "parents")?
374
                .iter()
375
                .map(|p| oref(p, "parent"))
376
                .collect::<Result<Vec<_>, _>>()?,
377
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
378
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
379
            view: Format1View::from_value(field(m, 4, "view")?)?,
380
        })
381
    }
382
}
383
384
#[cfg(test)]
385
mod tests {
386
    use super::*;
387
388
    #[test]
389
    fn op_round_trip() {
390
        let mut view = View::default();
391
        view.bookmarks.insert("main".into(), ObjectRef([2; 32]));
392
        view.bookmarks.insert("dev".into(), ObjectRef([3; 32]));
393
        view.workspaces
394
            .insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
395
        view.remotes
396
            .entry("origin".into())
397
            .or_default()
398
            .insert("main".into(), ObjectRef([4; 32]));
399
        view.policy = Some(ObjectRef([6; 32]));
400
        view.tags.insert("v1".into(), ObjectRef([8; 32]));
401
        let op = Op {
402
            parents: vec![ObjectRef([7; 32])],
403
            hlc: Hlc {
404
                wall_ms: 5,
405
                logical: 1,
406
            },
407
            command: "oxy commit".into(),
408
            view,
409
            workspace: None,
410
            undoes: None,
411
        };
412
        let d = Op::decode(&op.encode()).unwrap();
413
        assert_eq!(d, op);
414
        // The workspace and an undo's target travel when known.
415
        let mut known = op.clone();
416
        known.workspace = Some(WorkspaceId([3; 16]));
417
        known.undoes = Some(ObjectRef([1; 32]));
418
        assert_eq!(Op::decode(&known.encode()).unwrap(), known);
419
        assert_ne!(known.hash(), op.hash());
420
        assert_eq!(d.hash(), Op::decode(&d.encode()).unwrap().hash());
421
        // A format-1 op does not decode as format 2, but does through the migration reader.
422
        let mut v1 = Format1View::default();
423
        v1.bookmarks
424
            .insert("main".into(), vec![ObjectRef([2; 32]), ObjectRef([1; 32])]);
425
        v1.workspaces
426
            .insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
427
        let old = Format1Op {
428
            parents: vec![],
429
            hlc: Hlc::default(),
430
            command: "oxy init".into(),
431
            view: v1,
432
        };
433
        assert!(Op::decode(&old.encode()).is_err());
434
        let back = Op::decode_format1(&old.encode()).unwrap();
435
        assert_eq!(
436
            back.view.bookmarks["main"],
437
            vec![ObjectRef([1; 32]), ObjectRef([2; 32])]
438
        );
439
    }
440
}