OXYGENHUB

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

249 of 273 lines run, 42 of 57 functions · run #267 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
}
45
46
impl View {
473242×
    fn to_value(&self) -> Value {
483242×
        Value::map([
49
            (
50
                0,
51
                Value::Array(
523242×
                    self.bookmarks
533242×
                        .iter()
543430×
                        .map(|(n, st)| {
553430×
                            Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])
563430×
                        })
573242×
                        .collect(),
58
                ),
59
            ),
60
            (
61
                1,
62
                Value::Array(
633242×
                    self.workspaces
643242×
                        .iter()
653242×
                        .map(|(w, h)| {
663174×
                            Value::Array(vec![
673174×
                                Value::Bytes(w.0.to_vec()),
683174×
                                Value::Bytes(h.0.to_vec()),
693174×
                            ])
703174×
                        })
713242×
                        .collect(),
72
                ),
73
            ),
74
            (
75
                2,
76
                Value::Array(
773242×
                    self.remotes
783242×
                        .iter()
793242×
                        .map(|(r, bms)| {
801840×
                            Value::Array(vec![
811840×
                                Value::Text(r.clone()),
82
                                Value::Array(
831840×
                                    bms.iter()
841886×
                                        .map(|(n, st)| {
851886×
                                            Value::Array(vec![
861886×
                                                Value::Text(n.clone()),
871886×
                                                Value::Bytes(st.0.to_vec()),
881886×
                                            ])
891886×
                                        })
901840×
                                        .collect(),
91
                                ),
92
                            ])
931840×
                        })
943242×
                        .collect(),
95
                ),
96
            ),
97
            (
98
                3,
993242×
                self.policy
1003242×
                    .map(|p| Value::Bytes(p.0.to_vec()))
1013242×
                    .unwrap_or(Value::Null),
102
            ),
103
            (
104
                4,
105
                Value::Array(
1063242×
                    self.tags
1073242×
                        .iter()
1083242×
                        .map(|(n, st)| {
10994×
                            Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])
11094×
                        })
1113242×
                        .collect(),
112
                ),
113
            ),
114
        ])
1153242×
    }
116
11717178×
    fn from_value(v: &Value) -> Result<View, ObjectError> {
11817178×
        let m = v
11917178×
            .as_map()
12017178×
            .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
12117178×
        let mut view = View::default();
12218122×
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
12318122×
            let pair = array(b, "bookmark")?;
12418122×
            if pair.len() != 2 {
1250×
                return Err(ObjectError::Schema(
1260×
                    "bookmark: expected [name, heads]".into(),
1270×
                ));
12818122×
            }
12918122×
            let name = text(&pair[0], "bookmark name")?.to_owned();
13018122×
            view.bookmarks
13118122×
                .insert(name, oref(&pair[1], "bookmark state")?);
132
        }
13317177×
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
13417177×
        if let Some(rs) = m.get(&2) {
13517177×
            for r in array(rs, "remotes")? {
1367429×
                let pair = array(r, "remote")?;
1377429×
                if pair.len() != 2 {
1380×
                    return Err(ObjectError::Schema(
1390×
                        "remote: expected [name, bookmarks]".into(),
1400×
                    ));
1417429×
                }
1427429×
                let name = text(&pair[0], "remote name")?.to_owned();
1437429×
                let mut bms = BTreeMap::new();
1447591×
                for b in array(&pair[1], "remote bookmarks")? {
1457591×
                    let bp = array(b, "remote bookmark")?;
1467591×
                    if bp.len() != 2 {
1470×
                        return Err(ObjectError::Schema(
1480×
                            "remote bookmark: expected [name, state]".into(),
1490×
                        ));
1507591×
                    }
1517591×
                    bms.insert(
1527591×
                        text(&bp[0], "bookmark name")?.to_owned(),
1537591×
                        oref(&bp[1], "bookmark state")?,
154
                    );
155
                }
1567429×
                view.remotes.insert(name, bms);
157
            }
1580×
        }
15917177×
        if let Some(p) = m.get(&3) {
16017177×
            view.policy = if p.is_null() {
16116519×
                None
162
            } else {
163658×
                Some(oref(p, "policy")?)
164
            };
1650×
        }
16617177×
        if let Some(ts) = m.get(&4) {
16717177×
            for t in array(ts, "tags")? {
168565×
                let pair = array(t, "tag")?;
169565×
                if pair.len() != 2 {
1700×
                    return Err(ObjectError::Schema("tag: expected [name, state]".into()));
171565×
                }
172565×
                view.tags.insert(
173565×
                    text(&pair[0], "tag name")?.to_owned(),
174565×
                    oref(&pair[1], "tag state")?,
175
                );
176
            }
1770×
        }
17817177×
        Ok(view)
17917178×
    }
180
}
181
18217180×
fn workspaces_from_value(v: &Value) -> Result<BTreeMap<WorkspaceId, ChangeHash>, ObjectError> {
18317180×
    let mut out = BTreeMap::new();
18417180×
    for w in array(v, "workspaces")? {
18516496×
        let pair = array(w, "workspace")?;
18616496×
        if pair.len() != 2 {
1870×
            return Err(ObjectError::Schema(
1880×
                "workspace: expected [id, change]".into(),
1890×
            ));
19016496×
        }
19116496×
        let id = WorkspaceId::from_slice(bytes(&pair[0], "workspace id")?)
19216496×
            .ok_or_else(|| ObjectError::Schema("workspace id: expected 16 bytes".into()))?;
19316496×
        out.insert(id, oref(&pair[1], "workspace change")?);
194
    }
19517180×
    Ok(out)
19617180×
}
197
198
impl Format1View {
1993×
    fn from_value(v: &Value) -> Result<Format1View, ObjectError> {
2003×
        let m = v
2013×
            .as_map()
2023×
            .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
2033×
        let mut view = Format1View::default();
2043×
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
2053×
            let pair = array(b, "bookmark")?;
2063×
            if pair.len() != 2 {
2070×
                return Err(ObjectError::Schema(
2080×
                    "bookmark: expected [name, heads]".into(),
2090×
                ));
2103×
            }
2113×
            let name = text(&pair[0], "bookmark name")?.to_owned();
2123×
            let mut heads = array(&pair[1], "heads")?
2133×
                .iter()
2143×
                .map(|h| oref(h, "head"))
2153×
                .collect::<Result<Vec<_>, _>>()?;
2163×
            heads.sort();
2173×
            heads.dedup();
2183×
            view.bookmarks.insert(name, heads);
219
        }
2203×
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
2213×
        Ok(view)
2223×
    }
223
224
    /// Format-1 encoding, kept for the migration test.
225
    #[cfg(test)]
2264×
    pub(crate) fn to_value(&self) -> Value {
2274×
        Value::map([
228
            (
229
                0,
230
                Value::Array(
2314×
                    self.bookmarks
2324×
                        .iter()
2334×
                        .map(|(n, hs)| {
2344×
                            Value::Array(vec![
2354×
                                Value::Text(n.clone()),
236
                                Value::Array(
2375×
                                    hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect(),
238
                                ),
239
                            ])
2404×
                        })
2414×
                        .collect(),
242
                ),
243
            ),
244
            (
245
                1,
246
                Value::Array(
2474×
                    self.workspaces
2484×
                        .iter()
2494×
                        .map(|(w, h)| {
2504×
                            Value::Array(vec![
2514×
                                Value::Bytes(w.0.to_vec()),
2524×
                                Value::Bytes(h.0.to_vec()),
2534×
                            ])
2544×
                        })
2554×
                        .collect(),
256
                ),
257
            ),
258
        ])
2594×
    }
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)]
2734×
    pub(crate) fn encode(&self) -> Vec<u8> {
2744×
        cbor::encode(&Value::map([
2754×
            (0, Value::Uint(FORMAT_VERSION)),
276
            (
277
                1,
278
                Value::Array(
2794×
                    self.parents
2804×
                        .iter()
2814×
                        .map(|p| Value::Bytes(p.0.to_vec()))
2824×
                        .collect(),
283
                ),
284
            ),
2854×
            (2, self.hlc.to_value()),
2864×
            (3, Value::Text(self.command.clone())),
2874×
            (4, self.view.to_value()),
288
        ]))
2894×
    }
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.
29835×
    pub fn is_snapshot(&self) -> bool {
29935×
        self.command.ends_with(SNAPSHOT_SUFFIX)
30035×
    }
301
3023242×
    pub fn to_value(&self) -> Value {
3033242×
        Value::map([
3043242×
            (0, Value::Uint(FORMAT_VERSION)),
305
            (
306
                1,
307
                Value::Array(
3083242×
                    self.parents
3093242×
                        .iter()
3103242×
                        .map(|p| Value::Bytes(p.0.to_vec()))
3113242×
                        .collect(),
312
                ),
313
            ),
3143242×
            (2, self.hlc.to_value()),
3153242×
            (3, Value::Text(self.command.clone())),
3163242×
            (4, self.view.to_value()),
317
        ])
3183242×
    }
3193242×
    pub fn encode(&self) -> Vec<u8> {
3203242×
        cbor::encode(&self.to_value())
3213242×
    }
3222×
    pub fn hash(&self) -> ObjectRef {
3232×
        hash_object(ObjectType::Op, &self.encode())
3242×
    }
32517178×
    pub fn decode(b: &[u8]) -> Result<Op, ObjectError> {
32617178×
        let v = cbor::decode(b)?;
32717178×
        let m = v
32817178×
            .as_map()
32917178×
            .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
33017178×
        check_version(m)?;
331
        Ok(Op {
33217178×
            parents: array(field(m, 1, "parents")?, "parents")?
33317178×
                .iter()
33417178×
                .map(|p| oref(p, "parent"))
33517178×
                .collect::<Result<Vec<_>, _>>()?,
33617178×
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
33717178×
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
33817178×
            view: View::from_value(field(m, 4, "view")?)?,
339
        })
34017178×
    }
341
342
    /// Read an operation written by repository format 1.
3433×
    pub fn decode_format1(b: &[u8]) -> Result<Format1Op, ObjectError> {
3443×
        let v = cbor::decode(b)?;
3453×
        let m = v
3463×
            .as_map()
3473×
            .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
3483×
        check_version(m)?;
349
        Ok(Format1Op {
3503×
            parents: array(field(m, 1, "parents")?, "parents")?
3513×
                .iter()
3523×
                .map(|p| oref(p, "parent"))
3533×
                .collect::<Result<Vec<_>, _>>()?,
3543×
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
3553×
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
3563×
            view: Format1View::from_value(field(m, 4, "view")?)?,
357
        })
3583×
    }
359
}
360
361
#[cfg(test)]
362
mod tests {
363
    use super::*;
364
365
    #[test]
3661×
    fn op_round_trip() {
3671×
        let mut view = View::default();
3681×
        view.bookmarks.insert("main".into(), ObjectRef([2; 32]));
3691×
        view.bookmarks.insert("dev".into(), ObjectRef([3; 32]));
3701×
        view.workspaces
3711×
            .insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
3721×
        view.remotes
3731×
            .entry("origin".into())
3741×
            .or_default()
3751×
            .insert("main".into(), ObjectRef([4; 32]));
3761×
        view.policy = Some(ObjectRef([6; 32]));
3771×
        view.tags.insert("v1".into(), ObjectRef([8; 32]));
3781×
        let op = Op {
3791×
            parents: vec![ObjectRef([7; 32])],
3801×
            hlc: Hlc {
3811×
                wall_ms: 5,
3821×
                logical: 1,
3831×
            },
3841×
            command: "oxy commit".into(),
3851×
            view,
3861×
        };
3871×
        let d = Op::decode(&op.encode()).unwrap();
3881×
        assert_eq!(d, op);
3891×
        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.
3911×
        let mut v1 = Format1View::default();
3921×
        v1.bookmarks
3931×
            .insert("main".into(), vec![ObjectRef([2; 32]), ObjectRef([1; 32])]);
3941×
        v1.workspaces
3951×
            .insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
3961×
        let old = Format1Op {
3971×
            parents: vec![],
3981×
            hlc: Hlc::default(),
3991×
            command: "oxy init".into(),
4001×
            view: v1,
4011×
        };
4021×
        assert!(Op::decode(&old.encode()).is_err());
4031×
        let back = Op::decode_format1(&old.encode()).unwrap();
4041×
        assert_eq!(
4051×
            back.view.bookmarks["main"],
4061×
            vec![ObjectRef([1; 32]), ObjectRef([2; 32])]
407
        );
4081×
    }
409
}