OXYGENHUB

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

249 of 273 lines run, 42 of 57 functions · run #263 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 {
473223×
    fn to_value(&self) -> Value {
483223×
        Value::map([
49
            (
50
                0,
51
                Value::Array(
523223×
                    self.bookmarks
533223×
                        .iter()
543411×
                        .map(|(n, st)| {
553411×
                            Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])
563411×
                        })
573223×
                        .collect(),
58
                ),
59
            ),
60
            (
61
                1,
62
                Value::Array(
633223×
                    self.workspaces
643223×
                        .iter()
653223×
                        .map(|(w, h)| {
663156×
                            Value::Array(vec![
673156×
                                Value::Bytes(w.0.to_vec()),
683156×
                                Value::Bytes(h.0.to_vec()),
693156×
                            ])
703156×
                        })
713223×
                        .collect(),
72
                ),
73
            ),
74
            (
75
                2,
76
                Value::Array(
773223×
                    self.remotes
783223×
                        .iter()
793223×
                        .map(|(r, bms)| {
801828×
                            Value::Array(vec![
811828×
                                Value::Text(r.clone()),
82
                                Value::Array(
831828×
                                    bms.iter()
841874×
                                        .map(|(n, st)| {
851874×
                                            Value::Array(vec![
861874×
                                                Value::Text(n.clone()),
871874×
                                                Value::Bytes(st.0.to_vec()),
881874×
                                            ])
891874×
                                        })
901828×
                                        .collect(),
91
                                ),
92
                            ])
931828×
                        })
943223×
                        .collect(),
95
                ),
96
            ),
97
            (
98
                3,
993223×
                self.policy
1003223×
                    .map(|p| Value::Bytes(p.0.to_vec()))
1013223×
                    .unwrap_or(Value::Null),
102
            ),
103
            (
104
                4,
105
                Value::Array(
1063223×
                    self.tags
1073223×
                        .iter()
1083223×
                        .map(|(n, st)| {
10994×
                            Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])
11094×
                        })
1113223×
                        .collect(),
112
                ),
113
            ),
114
        ])
1153223×
    }
116
11717146×
    fn from_value(v: &Value) -> Result<View, ObjectError> {
11817146×
        let m = v
11917146×
            .as_map()
12017146×
            .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
12117146×
        let mut view = View::default();
12218090×
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
12318090×
            let pair = array(b, "bookmark")?;
12418090×
            if pair.len() != 2 {
1250×
                return Err(ObjectError::Schema(
1260×
                    "bookmark: expected [name, heads]".into(),
1270×
                ));
12818090×
            }
12918090×
            let name = text(&pair[0], "bookmark name")?.to_owned();
13018090×
            view.bookmarks
13118090×
                .insert(name, oref(&pair[1], "bookmark state")?);
132
        }
13317145×
        view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?;
13417145×
        if let Some(rs) = m.get(&2) {
13517145×
            for r in array(rs, "remotes")? {
1367401×
                let pair = array(r, "remote")?;
1377401×
                if pair.len() != 2 {
1380×
                    return Err(ObjectError::Schema(
1390×
                        "remote: expected [name, bookmarks]".into(),
1400×
                    ));
1417401×
                }
1427401×
                let name = text(&pair[0], "remote name")?.to_owned();
1437401×
                let mut bms = BTreeMap::new();
1447563×
                for b in array(&pair[1], "remote bookmarks")? {
1457563×
                    let bp = array(b, "remote bookmark")?;
1467563×
                    if bp.len() != 2 {
1470×
                        return Err(ObjectError::Schema(
1480×
                            "remote bookmark: expected [name, state]".into(),
1490×
                        ));
1507563×
                    }
1517563×
                    bms.insert(
1527563×
                        text(&bp[0], "bookmark name")?.to_owned(),
1537563×
                        oref(&bp[1], "bookmark state")?,
154
                    );
155
                }
1567401×
                view.remotes.insert(name, bms);
157
            }
1580×
        }
15917145×
        if let Some(p) = m.get(&3) {
16017145×
            view.policy = if p.is_null() {
16116487×
                None
162
            } else {
163658×
                Some(oref(p, "policy")?)
164
            };
1650×
        }
16617145×
        if let Some(ts) = m.get(&4) {
16717145×
            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×
        }
17817145×
        Ok(view)
17917146×
    }
180
}
181
18217148×
fn workspaces_from_value(v: &Value) -> Result<BTreeMap<WorkspaceId, ChangeHash>, ObjectError> {
18317148×
    let mut out = BTreeMap::new();
18417148×
    for w in array(v, "workspaces")? {
18516468×
        let pair = array(w, "workspace")?;
18616468×
        if pair.len() != 2 {
1870×
            return Err(ObjectError::Schema(
1880×
                "workspace: expected [id, change]".into(),
1890×
            ));
19016468×
        }
19116468×
        let id = WorkspaceId::from_slice(bytes(&pair[0], "workspace id")?)
19216468×
            .ok_or_else(|| ObjectError::Schema("workspace id: expected 16 bytes".into()))?;
19316468×
        out.insert(id, oref(&pair[1], "workspace change")?);
194
    }
19517148×
    Ok(out)
19617148×
}
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
3023223×
    pub fn to_value(&self) -> Value {
3033223×
        Value::map([
3043223×
            (0, Value::Uint(FORMAT_VERSION)),
305
            (
306
                1,
307
                Value::Array(
3083223×
                    self.parents
3093223×
                        .iter()
3103223×
                        .map(|p| Value::Bytes(p.0.to_vec()))
3113223×
                        .collect(),
312
                ),
313
            ),
3143223×
            (2, self.hlc.to_value()),
3153223×
            (3, Value::Text(self.command.clone())),
3163223×
            (4, self.view.to_value()),
317
        ])
3183223×
    }
3193223×
    pub fn encode(&self) -> Vec<u8> {
3203223×
        cbor::encode(&self.to_value())
3213223×
    }
3222×
    pub fn hash(&self) -> ObjectRef {
3232×
        hash_object(ObjectType::Op, &self.encode())
3242×
    }
32517146×
    pub fn decode(b: &[u8]) -> Result<Op, ObjectError> {
32617146×
        let v = cbor::decode(b)?;
32717146×
        let m = v
32817146×
            .as_map()
32917146×
            .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
33017146×
        check_version(m)?;
331
        Ok(Op {
33217146×
            parents: array(field(m, 1, "parents")?, "parents")?
33317146×
                .iter()
33417146×
                .map(|p| oref(p, "parent"))
33517146×
                .collect::<Result<Vec<_>, _>>()?,
33617146×
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
33717146×
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
33817146×
            view: View::from_value(field(m, 4, "view")?)?,
339
        })
34017146×
    }
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
}