OXYGENHUB
# 86e682c242c6 oxygen / crates / oxy-core / src / oplog.rs
9.0 KiB · Rust · last changed Import the Oxygen specification and the first Rust implementation (root change) 1 month ago source blame history raw
ff0af1ee unknown 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
//!
86e682c2 Andy McCall ↶ 5
//! Encoding: `bookmarks` is an array of `[name, state_ref]` sorted by name and
ff0af1ee unknown 6
//! `workspaces` an array of `[workspace_id, change_hash]` sorted by id, since
86e682c2 Andy McCall ↶ 7
//! canonical maps carry only integer keys. Repository format 1 encoded a
8
//! bookmark as `[name, [heads]]`; `Op::decode_format1` reads that for migration.
ff0af1ee unknown 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::{array, bytes, check_version, field, oref, text, Hlc, ObjectError, FORMAT_VERSION};
14
use std::collections::BTreeMap;
15
16
#[derive(Clone, Debug, Default, PartialEq, Eq)]
17
pub struct View {
86e682c2 Andy McCall ↶ 18
    /// Bookmark name → ObjectRef of a `state` object (spec §10.1).
19
    pub bookmarks: BTreeMap<String, ObjectRef>,
20
    pub workspaces: BTreeMap<WorkspaceId, ChangeHash>,
21
}
22
23
/// A format-1 view, only produced by `Op::decode_format1`.
24
#[derive(Clone, Debug, Default, PartialEq, Eq)]
25
pub struct Format1View {
ff0af1ee unknown 26
    pub bookmarks: BTreeMap<String, Vec<ChangeHash>>,
27
    pub workspaces: BTreeMap<WorkspaceId, ChangeHash>,
28
}
29
30
#[derive(Clone, Debug, PartialEq, Eq)]
31
pub struct Op {
32
    pub parents: Vec<ObjectRef>,
33
    pub hlc: Hlc,
34
    pub command: String,
35
    pub view: View,
36
}
37
38
impl View {
39
    fn to_value(&self) -> Value {
40
        Value::map([
41
            (
42
                0,
43
                Value::Array(
44
                    self.bookmarks
45
                        .iter()
86e682c2 Andy McCall ↶ 46
                        .map(|(n, st)| Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())]))
ff0af1ee unknown 47
                        .collect(),
48
                ),
49
            ),
50
            (
51
                1,
52
                Value::Array(
53
                    self.workspaces
54
                        .iter()
55
                        .map(|(w, h)| Value::Array(vec![Value::Bytes(w.0.to_vec()), Value::Bytes(h.0.to_vec())]))
56
                        .collect(),
57
                ),
58
            ),
59
        ])
60
    }
61
62
    fn from_value(v: &Value) -> Result<View, ObjectError> {
63
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
64
        let mut view = View::default();
65
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
66
            let pair = array(b, "bookmark")?;
67
            if pair.len() != 2 {
68
                return Err(ObjectError::Schema("bookmark: expected [name, heads]".into()));
69
            }
70
            let name = text(&pair[0], "bookmark name")?.to_owned();
86e682c2 Andy McCall ↶ 71
            view.bookmarks.insert(name, oref(&pair[1], "bookmark state")?);
72
        }
73
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
74
        Ok(view)
75
    }
76
}
77
78
fn workspaces_from_value(v: &Value) -> Result<BTreeMap<WorkspaceId, ChangeHash>, ObjectError> {
79
    let mut out = BTreeMap::new();
80
    for w in array(v, "workspaces")? {
81
        let pair = array(w, "workspace")?;
82
        if pair.len() != 2 {
83
            return Err(ObjectError::Schema("workspace: expected [id, change]".into()));
84
        }
85
        let id = WorkspaceId::from_slice(bytes(&pair[0], "workspace id")?)
86
            .ok_or_else(|| ObjectError::Schema("workspace id: expected 16 bytes".into()))?;
87
        out.insert(id, oref(&pair[1], "workspace change")?);
88
    }
89
    Ok(out)
90
}
91
92
impl Format1View {
93
    fn from_value(v: &Value) -> Result<Format1View, ObjectError> {
94
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
95
        let mut view = Format1View::default();
96
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
97
            let pair = array(b, "bookmark")?;
98
            if pair.len() != 2 {
99
                return Err(ObjectError::Schema("bookmark: expected [name, heads]".into()));
100
            }
101
            let name = text(&pair[0], "bookmark name")?.to_owned();
ff0af1ee unknown 102
            let mut heads = array(&pair[1], "heads")?.iter().map(|h| oref(h, "head")).collect::<Result<Vec<_>, _>>()?;
103
            heads.sort();
104
            heads.dedup();
105
            view.bookmarks.insert(name, heads);
106
        }
86e682c2 Andy McCall ↶ 107
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
ff0af1ee unknown 108
        Ok(view)
109
    }
86e682c2 Andy McCall ↶ 110
111
    /// Format-1 encoding, kept for the migration test.
112
    #[cfg(test)]
113
    pub(crate) fn to_value(&self) -> Value {
114
        Value::map([
115
            (
116
                0,
117
                Value::Array(
118
                    self.bookmarks
119
                        .iter()
120
                        .map(|(n, hs)| {
121
                            Value::Array(vec![
122
                                Value::Text(n.clone()),
123
                                Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()),
124
                            ])
125
                        })
126
                        .collect(),
127
                ),
128
            ),
129
            (
130
                1,
131
                Value::Array(
132
                    self.workspaces
133
                        .iter()
134
                        .map(|(w, h)| Value::Array(vec![Value::Bytes(w.0.to_vec()), Value::Bytes(h.0.to_vec())]))
135
                        .collect(),
136
                ),
137
            ),
138
        ])
139
    }
140
}
141
142
/// A format-1 operation as read for migration.
143
#[derive(Clone, Debug, PartialEq, Eq)]
144
pub struct Format1Op {
145
    pub parents: Vec<ObjectRef>,
146
    pub hlc: Hlc,
147
    pub command: String,
148
    pub view: Format1View,
ff0af1ee unknown 149
}
150
86e682c2 Andy McCall ↶ 151
impl Format1Op {
152
    #[cfg(test)]
153
    pub(crate) fn encode(&self) -> Vec<u8> {
154
        cbor::encode(&Value::map([
155
            (0, Value::Uint(FORMAT_VERSION)),
156
            (1, Value::Array(self.parents.iter().map(|p| Value::Bytes(p.0.to_vec())).collect())),
157
            (2, self.hlc.to_value()),
158
            (3, Value::Text(self.command.clone())),
159
            (4, self.view.to_value()),
160
        ]))
161
    }
162
}
163
55e5a16a unknown ↶ 164
/// Suffix on the `command` of operations recorded by an automatic
165
/// working-copy snapshot rather than by a user command.
166
pub const SNAPSHOT_SUFFIX: &str = " (snapshot)";
167
ff0af1ee unknown 168
impl Op {
55e5a16a unknown ↶ 169
    /// Whether this operation only recorded a working-copy snapshot.
170
    pub fn is_snapshot(&self) -> bool {
171
        self.command.ends_with(SNAPSHOT_SUFFIX)
172
    }
173
ff0af1ee unknown 174
    pub fn to_value(&self) -> Value {
175
        Value::map([
176
            (0, Value::Uint(FORMAT_VERSION)),
177
            (1, Value::Array(self.parents.iter().map(|p| Value::Bytes(p.0.to_vec())).collect())),
178
            (2, self.hlc.to_value()),
179
            (3, Value::Text(self.command.clone())),
180
            (4, self.view.to_value()),
181
        ])
182
    }
183
    pub fn encode(&self) -> Vec<u8> {
184
        cbor::encode(&self.to_value())
185
    }
186
    pub fn hash(&self) -> ObjectRef {
187
        hash_object(ObjectType::Op, &self.encode())
188
    }
189
    pub fn decode(b: &[u8]) -> Result<Op, ObjectError> {
190
        let v = cbor::decode(b)?;
191
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
192
        check_version(m)?;
193
        Ok(Op {
194
            parents: array(field(m, 1, "parents")?, "parents")?.iter().map(|p| oref(p, "parent")).collect::<Result<Vec<_>, _>>()?,
195
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
196
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
197
            view: View::from_value(field(m, 4, "view")?)?,
198
        })
199
    }
86e682c2 Andy McCall ↶ 200
201
    /// Read an operation written by repository format 1.
202
    pub fn decode_format1(b: &[u8]) -> Result<Format1Op, ObjectError> {
203
        let v = cbor::decode(b)?;
204
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
205
        check_version(m)?;
206
        Ok(Format1Op {
207
            parents: array(field(m, 1, "parents")?, "parents")?.iter().map(|p| oref(p, "parent")).collect::<Result<Vec<_>, _>>()?,
208
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
209
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
210
            view: Format1View::from_value(field(m, 4, "view")?)?,
211
        })
212
    }
ff0af1ee unknown 213
}
214
215
#[cfg(test)]
216
mod tests {
217
    use super::*;
218
219
    #[test]
220
    fn op_round_trip() {
221
        let mut view = View::default();
86e682c2 Andy McCall ↶ 222
        view.bookmarks.insert("main".into(), ObjectRef([2; 32]));
223
        view.bookmarks.insert("dev".into(), ObjectRef([3; 32]));
ff0af1ee unknown 224
        view.workspaces.insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
225
        let op = Op { parents: vec![ObjectRef([7; 32])], hlc: Hlc { wall_ms: 5, logical: 1 }, command: "oxy commit".into(), view };
226
        let d = Op::decode(&op.encode()).unwrap();
86e682c2 Andy McCall ↶ 227
        assert_eq!(d, op);
ff0af1ee unknown 228
        assert_eq!(d.hash(), Op::decode(&d.encode()).unwrap().hash());
86e682c2 Andy McCall ↶ 229
        // A format-1 op does not decode as format 2, but does through the migration reader.
230
        let mut v1 = Format1View::default();
231
        v1.bookmarks.insert("main".into(), vec![ObjectRef([2; 32]), ObjectRef([1; 32])]);
232
        v1.workspaces.insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
233
        let old = Format1Op { parents: vec![], hlc: Hlc::default(), command: "oxy init".into(), view: v1 };
234
        assert!(Op::decode(&old.encode()).is_err());
235
        let back = Op::decode_format1(&old.encode()).unwrap();
236
        assert_eq!(back.view.bookmarks["main"], vec![ObjectRef([1; 32]), ObjectRef([2; 32])]);
ff0af1ee unknown 237
    }
238
}