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