OXYGENHUB

Coverage / crates/oxy-core/src/oplog.rs 90.6%

269 of 297 lines run, 43 of 61 functions · run #384 on main · run missed

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
    /// The workspace whose command recorded it (oxygen#99); absent in
45
    /// operations written before it was kept.
46
    pub workspace: Option<WorkspaceId>,
47
    /// For an undo that reverted an older operation in place: that
48
    /// operation, so a further undo goes on past it.
49
    pub undoes: Option<ObjectRef>,
50
}
51
52
impl View {
533928×
    fn to_value(&self) -> Value {
543928×
        Value::map([
55
            (
56
                0,
57
                Value::Array(
583928×
                    self.bookmarks
593928×
                        .iter()
604258×
                        .map(|(n, st)| {
614258×
                            Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])
624258×
                        })
633928×
                        .collect(),
64
                ),
65
            ),
66
            (
67
                1,
68
                Value::Array(
693928×
                    self.workspaces
703928×
                        .iter()
713928×
                        .map(|(w, h)| {
723865×
                            Value::Array(vec![
733865×
                                Value::Bytes(w.0.to_vec()),
743865×
                                Value::Bytes(h.0.to_vec()),
753865×
                            ])
763865×
                        })
773928×
                        .collect(),
78
                ),
79
            ),
80
            (
81
                2,
82
                Value::Array(
833928×
                    self.remotes
843928×
                        .iter()
853928×
                        .map(|(r, bms)| {
862105×
                            Value::Array(vec![
872105×
                                Value::Text(r.clone()),
88
                                Value::Array(
892105×
                                    bms.iter()
902181×
                                        .map(|(n, st)| {
912181×
                                            Value::Array(vec![
922181×
                                                Value::Text(n.clone()),
932181×
                                                Value::Bytes(st.0.to_vec()),
942181×
                                            ])
952181×
                                        })
962105×
                                        .collect(),
97
                                ),
98
                            ])
992105×
                        })
1003928×
                        .collect(),
101
                ),
102
            ),
103
            (
104
                3,
1053928×
                self.policy
1063928×
                    .map(|p| Value::Bytes(p.0.to_vec()))
1073928×
                    .unwrap_or(Value::Null),
108
            ),
109
            (
110
                4,
111
                Value::Array(
1123928×
                    self.tags
1133928×
                        .iter()
1143928×
                        .map(|(n, st)| {
115192×
                            Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])
116192×
                        })
1173928×
                        .collect(),
118
                ),
119
            ),
120
        ])
1213928×
    }
122
12321193×
    fn from_value(v: &Value) -> Result<View, ObjectError> {
12421193×
        let m = v
12521193×
            .as_map()
12621193×
            .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
12721193×
        let mut view = View::default();
12822915×
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
12922915×
            let pair = array(b, "bookmark")?;
13022915×
            if pair.len() != 2 {
1310×
                return Err(ObjectError::Schema(
1320×
                    "bookmark: expected [name, heads]".into(),
1330×
                ));
13422915×
            }
13522915×
            let name = text(&pair[0], "bookmark name")?.to_owned();
13622915×
            view.bookmarks
13722915×
                .insert(name, oref(&pair[1], "bookmark state")?);
138
        }
13921192×
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
14021192×
        if let Some(rs) = m.get(&2) {
14121192×
            for r in array(rs, "remotes")? {
1428356×
                let pair = array(r, "remote")?;
1438356×
                if pair.len() != 2 {
1440×
                    return Err(ObjectError::Schema(
1450×
                        "remote: expected [name, bookmarks]".into(),
1460×
                    ));
1478356×
                }
1488356×
                let name = text(&pair[0], "remote name")?.to_owned();
1498356×
                let mut bms = BTreeMap::new();
1508598×
                for b in array(&pair[1], "remote bookmarks")? {
1518598×
                    let bp = array(b, "remote bookmark")?;
1528598×
                    if bp.len() != 2 {
1530×
                        return Err(ObjectError::Schema(
1540×
                            "remote bookmark: expected [name, state]".into(),
1550×
                        ));
1568598×
                    }
1578598×
                    bms.insert(
1588598×
                        text(&bp[0], "bookmark name")?.to_owned(),
1598598×
                        oref(&bp[1], "bookmark state")?,
160
                    );
161
                }
1628356×
                view.remotes.insert(name, bms);
163
            }
1640×
        }
16521192×
        if let Some(p) = m.get(&3) {
16621192×
            view.policy = if p.is_null() {
16719323×
                None
168
            } else {
1691869×
                Some(oref(p, "policy")?)
170
            };
1710×
        }
17221192×
        if let Some(ts) = m.get(&4) {
17321192×
            for t in array(ts, "tags")? {
1741199×
                let pair = array(t, "tag")?;
1751199×
                if pair.len() != 2 {
1760×
                    return Err(ObjectError::Schema("tag: expected [name, state]".into()));
1771199×
                }
1781199×
                view.tags.insert(
1791199×
                    text(&pair[0], "tag name")?.to_owned(),
1801199×
                    oref(&pair[1], "tag state")?,
181
                );
182
            }
1830×
        }
18421192×
        Ok(view)
18521193×
    }
186
}
187
18821195×
fn workspaces_from_value(v: &Value) -> Result<BTreeMap<WorkspaceId, ChangeHash>, ObjectError> {
18921195×
    let mut out = BTreeMap::new();
19021195×
    for w in array(v, "workspaces")? {
19120373×
        let pair = array(w, "workspace")?;
19220373×
        if pair.len() != 2 {
1930×
            return Err(ObjectError::Schema(
1940×
                "workspace: expected [id, change]".into(),
1950×
            ));
19620373×
        }
19720373×
        let id = WorkspaceId::from_slice(bytes(&pair[0], "workspace id")?)
19820373×
            .ok_or_else(|| ObjectError::Schema("workspace id: expected 16 bytes".into()))?;
19920373×
        out.insert(id, oref(&pair[1], "workspace change")?);
200
    }
20121195×
    Ok(out)
20221195×
}
203
204
impl Format1View {
2053×
    fn from_value(v: &Value) -> Result<Format1View, ObjectError> {
2063×
        let m = v
2073×
            .as_map()
2083×
            .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
2093×
        let mut view = Format1View::default();
2103×
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
2113×
            let pair = array(b, "bookmark")?;
2123×
            if pair.len() != 2 {
2130×
                return Err(ObjectError::Schema(
2140×
                    "bookmark: expected [name, heads]".into(),
2150×
                ));
2163×
            }
2173×
            let name = text(&pair[0], "bookmark name")?.to_owned();
2183×
            let mut heads = array(&pair[1], "heads")?
2193×
                .iter()
2203×
                .map(|h| oref(h, "head"))
2213×
                .collect::<Result<Vec<_>, _>>()?;
2223×
            heads.sort();
2233×
            heads.dedup();
2243×
            view.bookmarks.insert(name, heads);
225
        }
2263×
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
2273×
        Ok(view)
2283×
    }
229
230
    /// Format-1 encoding, kept for the migration test.
231
    #[cfg(test)]
2324×
    pub(crate) fn to_value(&self) -> Value {
2334×
        Value::map([
234
            (
235
                0,
236
                Value::Array(
2374×
                    self.bookmarks
2384×
                        .iter()
2394×
                        .map(|(n, hs)| {
2404×
                            Value::Array(vec![
2414×
                                Value::Text(n.clone()),
242
                                Value::Array(
2435×
                                    hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect(),
244
                                ),
245
                            ])
2464×
                        })
2474×
                        .collect(),
248
                ),
249
            ),
250
            (
251
                1,
252
                Value::Array(
2534×
                    self.workspaces
2544×
                        .iter()
2554×
                        .map(|(w, h)| {
2564×
                            Value::Array(vec![
2574×
                                Value::Bytes(w.0.to_vec()),
2584×
                                Value::Bytes(h.0.to_vec()),
2594×
                            ])
2604×
                        })
2614×
                        .collect(),
262
                ),
263
            ),
264
        ])
2654×
    }
266
}
267
268
/// A format-1 operation as read for migration.
269
#[derive(Clone, Debug, PartialEq, Eq)]
270
pub struct Format1Op {
271
    pub parents: Vec<ObjectRef>,
272
    pub hlc: Hlc,
273
    pub command: String,
274
    pub view: Format1View,
275
}
276
277
impl Format1Op {
278
    #[cfg(test)]
2794×
    pub(crate) fn encode(&self) -> Vec<u8> {
2804×
        cbor::encode(&Value::map([
2814×
            (0, Value::Uint(FORMAT_VERSION)),
282
            (
283
                1,
284
                Value::Array(
2854×
                    self.parents
2864×
                        .iter()
2874×
                        .map(|p| Value::Bytes(p.0.to_vec()))
2884×
                        .collect(),
289
                ),
290
            ),
2914×
            (2, self.hlc.to_value()),
2924×
            (3, Value::Text(self.command.clone())),
2934×
            (4, self.view.to_value()),
294
        ]))
2954×
    }
296
}
297
298
/// Suffix on the `command` of operations recorded by an automatic
299
/// working-copy snapshot rather than by a user command.
300
pub const SNAPSHOT_SUFFIX: &str = " (snapshot)";
301
302
impl Op {
303
    /// Whether this operation only recorded a working-copy snapshot.
30457×
    pub fn is_snapshot(&self) -> bool {
30557×
        self.command.ends_with(SNAPSHOT_SUFFIX)
30657×
    }
307
3083928×
    pub fn to_value(&self) -> Value {
3093928×
        let mut fields = vec![
3103928×
            (0, Value::Uint(FORMAT_VERSION)),
311
            (
312
                1,
313
                Value::Array(
3143928×
                    self.parents
3153928×
                        .iter()
3163928×
                        .map(|p| Value::Bytes(p.0.to_vec()))
3173928×
                        .collect(),
318
                ),
319
            ),
3203928×
            (2, self.hlc.to_value()),
3213928×
            (3, Value::Text(self.command.clone())),
3223928×
            (4, self.view.to_value()),
323
        ];
324
        // Absent when unknown, so older operations keep their encoding.
3253928×
        if let Some(w) = &self.workspace {
3263921×
            fields.push((5, Value::Bytes(w.0.to_vec())));
3273921×
        }
3283928×
        if let Some(u) = &self.undoes {
3295×
            fields.push((6, Value::Bytes(u.0.to_vec())));
3303923×
        }
3313928×
        Value::map(fields)
3323928×
    }
3333928×
    pub fn encode(&self) -> Vec<u8> {
3343928×
        cbor::encode(&self.to_value())
3353928×
    }
3364×
    pub fn hash(&self) -> ObjectRef {
3374×
        hash_object(ObjectType::Op, &self.encode())
3384×
    }
33921193×
    pub fn decode(b: &[u8]) -> Result<Op, ObjectError> {
34021193×
        let v = cbor::decode(b)?;
34121193×
        let m = v
34221193×
            .as_map()
34321193×
            .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
34421193×
        check_version(m)?;
345
        Ok(Op {
34621193×
            parents: array(field(m, 1, "parents")?, "parents")?
34721193×
                .iter()
34821193×
                .map(|p| oref(p, "parent"))
34921193×
                .collect::<Result<Vec<_>, _>>()?,
35021193×
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
35121193×
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
35221193×
            view: View::from_value(field(m, 4, "view")?)?,
35321192×
            workspace: match m.get(&5) {
35421183×
                Some(v) => Some(
35521183×
                    WorkspaceId::from_slice(bytes(v, "workspace")?).ok_or_else(|| {
3560×
                        ObjectError::Schema("workspace: expected 16 bytes".into())
3570×
                    })?,
358
                ),
3599×
                None => None,
360
            },
36121192×
            undoes: m.get(&6).map(|v| oref(v, "undoes")).transpose()?,
362
        })
36321193×
    }
364
365
    /// Read an operation written by repository format 1.
3663×
    pub fn decode_format1(b: &[u8]) -> Result<Format1Op, ObjectError> {
3673×
        let v = cbor::decode(b)?;
3683×
        let m = v
3693×
            .as_map()
3703×
            .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
3713×
        check_version(m)?;
372
        Ok(Format1Op {
3733×
            parents: array(field(m, 1, "parents")?, "parents")?
3743×
                .iter()
3753×
                .map(|p| oref(p, "parent"))
3763×
                .collect::<Result<Vec<_>, _>>()?,
3773×
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
3783×
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
3793×
            view: Format1View::from_value(field(m, 4, "view")?)?,
380
        })
3813×
    }
382
}
383
384
#[cfg(test)]
385
mod tests {
386
    use super::*;
387
388
    #[test]
3891×
    fn op_round_trip() {
3901×
        let mut view = View::default();
3911×
        view.bookmarks.insert("main".into(), ObjectRef([2; 32]));
3921×
        view.bookmarks.insert("dev".into(), ObjectRef([3; 32]));
3931×
        view.workspaces
3941×
            .insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
3951×
        view.remotes
3961×
            .entry("origin".into())
3971×
            .or_default()
3981×
            .insert("main".into(), ObjectRef([4; 32]));
3991×
        view.policy = Some(ObjectRef([6; 32]));
4001×
        view.tags.insert("v1".into(), ObjectRef([8; 32]));
4011×
        let op = Op {
4021×
            parents: vec![ObjectRef([7; 32])],
4031×
            hlc: Hlc {
4041×
                wall_ms: 5,
4051×
                logical: 1,
4061×
            },
4071×
            command: "oxy commit".into(),
4081×
            view,
4091×
            workspace: None,
4101×
            undoes: None,
4111×
        };
4121×
        let d = Op::decode(&op.encode()).unwrap();
4131×
        assert_eq!(d, op);
414
        // The workspace and an undo's target travel when known.
4151×
        let mut known = op.clone();
4161×
        known.workspace = Some(WorkspaceId([3; 16]));
4171×
        known.undoes = Some(ObjectRef([1; 32]));
4181×
        assert_eq!(Op::decode(&known.encode()).unwrap(), known);
4191×
        assert_ne!(known.hash(), op.hash());
4201×
        assert_eq!(d.hash(), Op::decode(&d.encode()).unwrap().hash());
421
        // A format-1 op does not decode as format 2, but does through the migration reader.
4221×
        let mut v1 = Format1View::default();
4231×
        v1.bookmarks
4241×
            .insert("main".into(), vec![ObjectRef([2; 32]), ObjectRef([1; 32])]);
4251×
        v1.workspaces
4261×
            .insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
4271×
        let old = Format1Op {
4281×
            parents: vec![],
4291×
            hlc: Hlc::default(),
4301×
            command: "oxy init".into(),
4311×
            view: v1,
4321×
        };
4331×
        assert!(Op::decode(&old.encode()).is_err());
4341×
        let back = Op::decode_format1(&old.encode()).unwrap();
4351×
        assert_eq!(
4361×
            back.view.bookmarks["main"],
4371×
            vec![ObjectRef([1; 32]), ObjectRef([2; 32])]
438
        );
4391×
    }
440
}