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