OXYGENHUB
# 5e388efbde57 oxygen / crates / oxy-core / src / oplog.rs
5.3 KiB · Rust · last changed Import the Oxygen specification and the first Rust implementation (root change) 1 month 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, [heads]]` sorted by name and
6
//! `workspaces` an array of `[workspace_id, change_hash]` sorted by id, since
7
//! canonical maps carry only integer keys.
8
9
use crate::cbor::{self, Value};
10
use crate::hash::{hash_object, ChangeHash, ObjectRef, ObjectType};
11
use crate::ids::WorkspaceId;
12
use crate::objects::{array, bytes, check_version, field, oref, text, Hlc, ObjectError, FORMAT_VERSION};
13
use std::collections::BTreeMap;
14
15
#[derive(Clone, Debug, Default, PartialEq, Eq)]
16
pub struct View {
17
    pub bookmarks: BTreeMap<String, Vec<ChangeHash>>,
18
    pub workspaces: BTreeMap<WorkspaceId, ChangeHash>,
19
}
20
21
#[derive(Clone, Debug, PartialEq, Eq)]
22
pub struct Op {
23
    pub parents: Vec<ObjectRef>,
24
    pub hlc: Hlc,
25
    pub command: String,
26
    pub view: View,
27
}
28
29
impl View {
30
    fn to_value(&self) -> Value {
31
        Value::map([
32
            (
33
                0,
34
                Value::Array(
35
                    self.bookmarks
36
                        .iter()
37
                        .map(|(n, hs)| {
38
                            Value::Array(vec![
39
                                Value::Text(n.clone()),
40
                                Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()),
41
                            ])
42
                        })
43
                        .collect(),
44
                ),
45
            ),
46
            (
47
                1,
48
                Value::Array(
49
                    self.workspaces
50
                        .iter()
51
                        .map(|(w, h)| Value::Array(vec![Value::Bytes(w.0.to_vec()), Value::Bytes(h.0.to_vec())]))
52
                        .collect(),
53
                ),
54
            ),
55
        ])
56
    }
57
58
    fn from_value(v: &Value) -> Result<View, ObjectError> {
59
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
60
        let mut view = View::default();
61
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
62
            let pair = array(b, "bookmark")?;
63
            if pair.len() != 2 {
64
                return Err(ObjectError::Schema("bookmark: expected [name, heads]".into()));
65
            }
66
            let name = text(&pair[0], "bookmark name")?.to_owned();
67
            let mut heads = array(&pair[1], "heads")?.iter().map(|h| oref(h, "head")).collect::<Result<Vec<_>, _>>()?;
68
            heads.sort();
69
            heads.dedup();
70
            view.bookmarks.insert(name, heads);
71
        }
72
        for w in array(field(m, 1, "workspaces")?, "workspaces")? {
73
            let pair = array(w, "workspace")?;
74
            if pair.len() != 2 {
75
                return Err(ObjectError::Schema("workspace: expected [id, change]".into()));
76
            }
77
            let id = WorkspaceId::from_slice(bytes(&pair[0], "workspace id")?)
78
                .ok_or_else(|| ObjectError::Schema("workspace id: expected 16 bytes".into()))?;
79
            view.workspaces.insert(id, oref(&pair[1], "workspace change")?);
80
        }
81
        Ok(view)
82
    }
83
}
84
85
/// Suffix on the `command` of operations recorded by an automatic
86
/// working-copy snapshot rather than by a user command.
87
pub const SNAPSHOT_SUFFIX: &str = " (snapshot)";
88
89
impl Op {
90
    /// Whether this operation only recorded a working-copy snapshot.
91
    pub fn is_snapshot(&self) -> bool {
92
        self.command.ends_with(SNAPSHOT_SUFFIX)
93
    }
94
95
    pub fn to_value(&self) -> Value {
96
        Value::map([
97
            (0, Value::Uint(FORMAT_VERSION)),
98
            (1, Value::Array(self.parents.iter().map(|p| Value::Bytes(p.0.to_vec())).collect())),
99
            (2, self.hlc.to_value()),
100
            (3, Value::Text(self.command.clone())),
101
            (4, self.view.to_value()),
102
        ])
103
    }
104
    pub fn encode(&self) -> Vec<u8> {
105
        cbor::encode(&self.to_value())
106
    }
107
    pub fn hash(&self) -> ObjectRef {
108
        hash_object(ObjectType::Op, &self.encode())
109
    }
110
    pub fn decode(b: &[u8]) -> Result<Op, ObjectError> {
111
        let v = cbor::decode(b)?;
112
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
113
        check_version(m)?;
114
        Ok(Op {
115
            parents: array(field(m, 1, "parents")?, "parents")?.iter().map(|p| oref(p, "parent")).collect::<Result<Vec<_>, _>>()?,
116
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
117
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
118
            view: View::from_value(field(m, 4, "view")?)?,
119
        })
120
    }
121
}
122
123
#[cfg(test)]
124
mod tests {
125
    use super::*;
126
127
    #[test]
128
    fn op_round_trip() {
129
        let mut view = View::default();
130
        view.bookmarks.insert("main".into(), vec![ObjectRef([2; 32]), ObjectRef([1; 32])]);
131
        view.bookmarks.insert("dev".into(), vec![]);
132
        view.workspaces.insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
133
        let op = Op { parents: vec![ObjectRef([7; 32])], hlc: Hlc { wall_ms: 5, logical: 1 }, command: "oxy commit".into(), view };
134
        let d = Op::decode(&op.encode()).unwrap();
135
        // Heads are normalized to sorted order on decode.
136
        assert_eq!(d.view.bookmarks["main"], vec![ObjectRef([1; 32]), ObjectRef([2; 32])]);
137
        assert_eq!(d.command, op.command);
138
        assert_eq!(d.parents, op.parents);
139
        assert_eq!(d.hash(), Op::decode(&d.encode()).unwrap().hash());
140
    }
141
}