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