OXYGENHUB
# 99e3e0af08df oxygen / crates / oxy-core / src / oplog.rs
14.1 KiB · Rust · last changed Unbound hosted repositories adopt the first pusher's id 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::{
14
    array, bytes, check_version, field, oref, text, Hlc, ObjectError, FORMAT_VERSION,
15
};
16
use std::collections::BTreeMap;
17
18
#[derive(Clone, Debug, Default, PartialEq, Eq)]
19
pub struct View {
20
    /// Bookmark name → ObjectRef of a `state` object (spec §10.1).
21
    pub bookmarks: BTreeMap<String, ObjectRef>,
22
    pub workspaces: BTreeMap<WorkspaceId, ChangeHash>,
23
    /// `remotes/<remote>/bookmarks/<name>`: remote → bookmark → state ObjectRef.
24
    pub remotes: BTreeMap<String, BTreeMap<String, ObjectRef>>,
25
    /// The `policy` reference (spec §10.1, §14.5): ObjectRef of the current policy.
26
    pub policy: Option<ObjectRef>,
27
    /// `tags/<name>`: immutable states (spec §10.1).
28
    pub tags: BTreeMap<String, ObjectRef>,
29
}
30
31
/// A format-1 view, only produced by `Op::decode_format1`.
32
#[derive(Clone, Debug, Default, PartialEq, Eq)]
33
pub struct Format1View {
34
    pub bookmarks: BTreeMap<String, Vec<ChangeHash>>,
35
    pub workspaces: BTreeMap<WorkspaceId, ChangeHash>,
36
}
37
38
#[derive(Clone, Debug, PartialEq, Eq)]
39
pub struct Op {
40
    pub parents: Vec<ObjectRef>,
41
    pub hlc: Hlc,
42
    pub command: String,
43
    pub view: View,
44
}
45
46
impl View {
47
    fn to_value(&self) -> Value {
48
        Value::map([
49
            (
50
                0,
51
                Value::Array(
52
                    self.bookmarks
53
                        .iter()
54
                        .map(|(n, st)| {
55
                            Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])
56
                        })
57
                        .collect(),
58
                ),
59
            ),
60
            (
61
                1,
62
                Value::Array(
63
                    self.workspaces
64
                        .iter()
65
                        .map(|(w, h)| {
66
                            Value::Array(vec![
67
                                Value::Bytes(w.0.to_vec()),
68
                                Value::Bytes(h.0.to_vec()),
69
                            ])
70
                        })
71
                        .collect(),
72
                ),
73
            ),
74
            (
75
                2,
76
                Value::Array(
77
                    self.remotes
78
                        .iter()
79
                        .map(|(r, bms)| {
80
                            Value::Array(vec![
81
                                Value::Text(r.clone()),
82
                                Value::Array(
83
                                    bms.iter()
84
                                        .map(|(n, st)| {
85
                                            Value::Array(vec![
86
                                                Value::Text(n.clone()),
87
                                                Value::Bytes(st.0.to_vec()),
88
                                            ])
89
                                        })
90
                                        .collect(),
91
                                ),
92
                            ])
93
                        })
94
                        .collect(),
95
                ),
96
            ),
97
            (
98
                3,
99
                self.policy
100
                    .map(|p| Value::Bytes(p.0.to_vec()))
101
                    .unwrap_or(Value::Null),
102
            ),
103
            (
104
                4,
105
                Value::Array(
106
                    self.tags
107
                        .iter()
108
                        .map(|(n, st)| {
109
                            Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])
110
                        })
111
                        .collect(),
112
                ),
113
            ),
114
        ])
115
    }
116
117
    fn from_value(v: &Value) -> Result<View, ObjectError> {
118
        let m = v
119
            .as_map()
120
            .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
121
        let mut view = View::default();
122
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
123
            let pair = array(b, "bookmark")?;
124
            if pair.len() != 2 {
125
                return Err(ObjectError::Schema(
126
                    "bookmark: expected [name, heads]".into(),
127
                ));
128
            }
129
            let name = text(&pair[0], "bookmark name")?.to_owned();
130
            view.bookmarks
131
                .insert(name, oref(&pair[1], "bookmark state")?);
132
        }
133
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
134
        if let Some(rs) = m.get(&2) {
135
            for r in array(rs, "remotes")? {
136
                let pair = array(r, "remote")?;
137
                if pair.len() != 2 {
138
                    return Err(ObjectError::Schema(
139
                        "remote: expected [name, bookmarks]".into(),
140
                    ));
141
                }
142
                let name = text(&pair[0], "remote name")?.to_owned();
143
                let mut bms = BTreeMap::new();
144
                for b in array(&pair[1], "remote bookmarks")? {
145
                    let bp = array(b, "remote bookmark")?;
146
                    if bp.len() != 2 {
147
                        return Err(ObjectError::Schema(
148
                            "remote bookmark: expected [name, state]".into(),
149
                        ));
150
                    }
151
                    bms.insert(
152
                        text(&bp[0], "bookmark name")?.to_owned(),
153
                        oref(&bp[1], "bookmark state")?,
154
                    );
155
                }
156
                view.remotes.insert(name, bms);
157
            }
158
        }
159
        if let Some(p) = m.get(&3) {
160
            view.policy = if p.is_null() {
161
                None
162
            } else {
163
                Some(oref(p, "policy")?)
164
            };
165
        }
166
        if let Some(ts) = m.get(&4) {
167
            for t in array(ts, "tags")? {
168
                let pair = array(t, "tag")?;
169
                if pair.len() != 2 {
170
                    return Err(ObjectError::Schema("tag: expected [name, state]".into()));
171
                }
172
                view.tags.insert(
173
                    text(&pair[0], "tag name")?.to_owned(),
174
                    oref(&pair[1], "tag state")?,
175
                );
176
            }
177
        }
178
        Ok(view)
179
    }
180
}
181
182
fn workspaces_from_value(v: &Value) -> Result<BTreeMap<WorkspaceId, ChangeHash>, ObjectError> {
183
    let mut out = BTreeMap::new();
184
    for w in array(v, "workspaces")? {
185
        let pair = array(w, "workspace")?;
186
        if pair.len() != 2 {
187
            return Err(ObjectError::Schema(
188
                "workspace: expected [id, change]".into(),
189
            ));
190
        }
191
        let id = WorkspaceId::from_slice(bytes(&pair[0], "workspace id")?)
192
            .ok_or_else(|| ObjectError::Schema("workspace id: expected 16 bytes".into()))?;
193
        out.insert(id, oref(&pair[1], "workspace change")?);
194
    }
195
    Ok(out)
196
}
197
198
impl Format1View {
199
    fn from_value(v: &Value) -> Result<Format1View, ObjectError> {
200
        let m = v
201
            .as_map()
202
            .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
203
        let mut view = Format1View::default();
204
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
205
            let pair = array(b, "bookmark")?;
206
            if pair.len() != 2 {
207
                return Err(ObjectError::Schema(
208
                    "bookmark: expected [name, heads]".into(),
209
                ));
210
            }
211
            let name = text(&pair[0], "bookmark name")?.to_owned();
212
            let mut heads = array(&pair[1], "heads")?
213
                .iter()
214
                .map(|h| oref(h, "head"))
215
                .collect::<Result<Vec<_>, _>>()?;
216
            heads.sort();
217
            heads.dedup();
218
            view.bookmarks.insert(name, heads);
219
        }
220
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
221
        Ok(view)
222
    }
223
224
    /// Format-1 encoding, kept for the migration test.
225
    #[cfg(test)]
226
    pub(crate) fn to_value(&self) -> Value {
227
        Value::map([
228
            (
229
                0,
230
                Value::Array(
231
                    self.bookmarks
232
                        .iter()
233
                        .map(|(n, hs)| {
234
                            Value::Array(vec![
235
                                Value::Text(n.clone()),
236
                                Value::Array(
237
                                    hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect(),
238
                                ),
239
                            ])
240
                        })
241
                        .collect(),
242
                ),
243
            ),
244
            (
245
                1,
246
                Value::Array(
247
                    self.workspaces
248
                        .iter()
249
                        .map(|(w, h)| {
250
                            Value::Array(vec![
251
                                Value::Bytes(w.0.to_vec()),
252
                                Value::Bytes(h.0.to_vec()),
253
                            ])
254
                        })
255
                        .collect(),
256
                ),
257
            ),
258
        ])
259
    }
260
}
261
262
/// A format-1 operation as read for migration.
263
#[derive(Clone, Debug, PartialEq, Eq)]
264
pub struct Format1Op {
265
    pub parents: Vec<ObjectRef>,
266
    pub hlc: Hlc,
267
    pub command: String,
268
    pub view: Format1View,
269
}
270
271
impl Format1Op {
272
    #[cfg(test)]
273
    pub(crate) fn encode(&self) -> Vec<u8> {
274
        cbor::encode(&Value::map([
275
            (0, Value::Uint(FORMAT_VERSION)),
276
            (
277
                1,
278
                Value::Array(
279
                    self.parents
280
                        .iter()
281
                        .map(|p| Value::Bytes(p.0.to_vec()))
282
                        .collect(),
283
                ),
284
            ),
285
            (2, self.hlc.to_value()),
286
            (3, Value::Text(self.command.clone())),
287
            (4, self.view.to_value()),
288
        ]))
289
    }
290
}
291
292
/// Suffix on the `command` of operations recorded by an automatic
293
/// working-copy snapshot rather than by a user command.
294
pub const SNAPSHOT_SUFFIX: &str = " (snapshot)";
295
296
impl Op {
297
    /// Whether this operation only recorded a working-copy snapshot.
298
    pub fn is_snapshot(&self) -> bool {
299
        self.command.ends_with(SNAPSHOT_SUFFIX)
300
    }
301
302
    pub fn to_value(&self) -> Value {
303
        Value::map([
304
            (0, Value::Uint(FORMAT_VERSION)),
305
            (
306
                1,
307
                Value::Array(
308
                    self.parents
309
                        .iter()
310
                        .map(|p| Value::Bytes(p.0.to_vec()))
311
                        .collect(),
312
                ),
313
            ),
314
            (2, self.hlc.to_value()),
315
            (3, Value::Text(self.command.clone())),
316
            (4, self.view.to_value()),
317
        ])
318
    }
319
    pub fn encode(&self) -> Vec<u8> {
320
        cbor::encode(&self.to_value())
321
    }
322
    pub fn hash(&self) -> ObjectRef {
323
        hash_object(ObjectType::Op, &self.encode())
324
    }
325
    pub fn decode(b: &[u8]) -> Result<Op, ObjectError> {
326
        let v = cbor::decode(b)?;
327
        let m = v
328
            .as_map()
329
            .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
330
        check_version(m)?;
331
        Ok(Op {
332
            parents: array(field(m, 1, "parents")?, "parents")?
333
                .iter()
334
                .map(|p| oref(p, "parent"))
335
                .collect::<Result<Vec<_>, _>>()?,
336
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
337
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
338
            view: View::from_value(field(m, 4, "view")?)?,
339
        })
340
    }
341
342
    /// Read an operation written by repository format 1.
343
    pub fn decode_format1(b: &[u8]) -> Result<Format1Op, ObjectError> {
344
        let v = cbor::decode(b)?;
345
        let m = v
346
            .as_map()
347
            .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
348
        check_version(m)?;
349
        Ok(Format1Op {
350
            parents: array(field(m, 1, "parents")?, "parents")?
351
                .iter()
352
                .map(|p| oref(p, "parent"))
353
                .collect::<Result<Vec<_>, _>>()?,
354
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
355
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
356
            view: Format1View::from_value(field(m, 4, "view")?)?,
357
        })
358
    }
359
}
360
361
#[cfg(test)]
362
mod tests {
363
    use super::*;
364
365
    #[test]
366
    fn op_round_trip() {
367
        let mut view = View::default();
368
        view.bookmarks.insert("main".into(), ObjectRef([2; 32]));
369
        view.bookmarks.insert("dev".into(), ObjectRef([3; 32]));
370
        view.workspaces
371
            .insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
372
        view.remotes
373
            .entry("origin".into())
374
            .or_default()
375
            .insert("main".into(), ObjectRef([4; 32]));
376
        view.policy = Some(ObjectRef([6; 32]));
377
        view.tags.insert("v1".into(), ObjectRef([8; 32]));
378
        let op = Op {
379
            parents: vec![ObjectRef([7; 32])],
380
            hlc: Hlc {
381
                wall_ms: 5,
382
                logical: 1,
383
            },
384
            command: "oxy commit".into(),
385
            view,
386
        };
387
        let d = Op::decode(&op.encode()).unwrap();
388
        assert_eq!(d, op);
389
        assert_eq!(d.hash(), Op::decode(&d.encode()).unwrap().hash());
390
        // A format-1 op does not decode as format 2, but does through the migration reader.
391
        let mut v1 = Format1View::default();
392
        v1.bookmarks
393
            .insert("main".into(), vec![ObjectRef([2; 32]), ObjectRef([1; 32])]);
394
        v1.workspaces
395
            .insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
396
        let old = Format1Op {
397
            parents: vec![],
398
            hlc: Hlc::default(),
399
            command: "oxy init".into(),
400
            view: v1,
401
        };
402
        assert!(Op::decode(&old.encode()).is_err());
403
        let back = Op::decode_format1(&old.encode()).unwrap();
404
        assert_eq!(
405
            back.view.bookmarks["main"],
406
            vec![ObjectRef([1; 32]), ObjectRef([2; 32])]
407
        );
408
    }
409
}