OXYGENHUB

Coverage / crates/oxy-core/src/subrepo.rs 90.9%

211 of 232 lines run, 14 of 34 functions · run #416 on main · run missed

1
//! Subrepositories (spec §7.8): the pin stored as a `subrepo` node's
2
//! content, and the `.oxymodules` file mapping paths to remotes.
3
//!
4
//! Pin encoding: `{0: 1, 1: kind, 2: locator, 3: pin, 4: bookmark}`; kind 0
5
//! is an Oxygen repository (locator = repository id, pin = sorted heads),
6
//! kind 1 a Git repository (locator = null, pin = commit sha as text).
7
8
use crate::cbor::{self, Value};
9
use crate::hash::ChangeHash;
10
use crate::objects::{
11
    array, bytes, check_version, field, oref, text, uint, ObjectError, FORMAT_VERSION,
12
};
13
use std::collections::BTreeMap;
14
15
#[derive(Clone, Debug, PartialEq, Eq)]
16
pub enum Pin {
17
    Oxy {
18
        repo: [u8; 16],
19
        heads: Vec<ChangeHash>,
20
        bookmark: String,
21
    },
22
    Git {
23
        sha: String,
24
        branch: String,
25
    },
26
}
27
28
impl Pin {
29
    /// Whether two pins name the same state: the same repository at the
30
    /// same heads, in any order, or the same Git commit. The bookmark or
31
    /// branch each names does not count (#147): a subrepo at its pinned
32
    /// state is at it whatever bookmark it sits on.
3352×
    pub fn same_state(&self, other: &Pin) -> bool {
3452×
        match (self, other) {
35
            (
36
                Pin::Oxy {
3750×
                    repo: r1,
3850×
                    heads: h1,
39
                    ..
40
                },
41
                Pin::Oxy {
4250×
                    repo: r2,
4350×
                    heads: h2,
44
                    ..
45
                },
46
            ) => {
4750×
                let (mut a, mut b) = (h1.clone(), h2.clone());
4850×
                a.sort();
4950×
                a.dedup();
5050×
                b.sort();
5150×
                b.dedup();
5250×
                r1 == r2 && a == b
53
            }
542×
            (Pin::Git { sha: a, .. }, Pin::Git { sha: b, .. }) => a == b,
550×
            _ => false,
56
        }
5752×
    }
58
5937×
    pub fn encode(&self) -> Vec<u8> {
6037×
        let v = match self {
61
            Pin::Oxy {
6235×
                repo,
6335×
                heads,
6435×
                bookmark,
65
            } => {
6635×
                let mut heads = heads.clone();
6735×
                heads.sort();
6835×
                heads.dedup();
6935×
                Value::map([
7035×
                    (0, Value::Uint(FORMAT_VERSION)),
7135×
                    (1, Value::Uint(0)),
7235×
                    (2, Value::Bytes(repo.to_vec())),
73
                    (
74
                        3,
7541×
                        Value::Array(heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()),
76
                    ),
7735×
                    (4, Value::Text(bookmark.clone())),
78
                ])
79
            }
802×
            Pin::Git { sha, branch } => Value::map([
812×
                (0, Value::Uint(FORMAT_VERSION)),
822×
                (1, Value::Uint(1)),
832×
                (2, Value::Null),
842×
                (3, Value::Text(sha.clone())),
852×
                (4, Value::Text(branch.clone())),
862×
            ]),
87
        };
8837×
        cbor::encode(&v)
8937×
    }
90
9163×
    pub fn decode(b: &[u8]) -> Result<Pin, ObjectError> {
9263×
        let v = cbor::decode(b)?;
9363×
        let m = v
9463×
            .as_map()
9563×
            .ok_or_else(|| ObjectError::Schema("pin: expected map".into()))?;
9663×
        check_version(m)?;
9763×
        let bookmark = text(field(m, 4, "bookmark")?, "bookmark")?.to_owned();
9863×
        match uint(field(m, 1, "kind")?, "kind")? {
99
            0 => {
10057×
                let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")?
10157×
                    .try_into()
10257×
                    .map_err(|_| ObjectError::Schema("pin: repo id must be 16 bytes".into()))?;
10357×
                let heads = array(field(m, 3, "heads")?, "heads")?
10457×
                    .iter()
10558×
                    .map(|h| oref(h, "head"))
10657×
                    .collect::<Result<Vec<_>, _>>()?;
10757×
                Ok(Pin::Oxy {
10857×
                    repo,
10957×
                    heads,
11057×
                    bookmark,
11157×
                })
112
            }
113
            1 => Ok(Pin::Git {
1146×
                sha: text(field(m, 3, "sha")?, "sha")?.to_owned(),
1156×
                branch: bookmark,
116
            }),
1170×
            k => Err(ObjectError::Schema(format!("pin: unknown kind {k}"))),
118
        }
11963×
    }
120
121
    /// A one-line description for listings.
1220×
    pub fn summary(&self) -> String {
1230×
        match self {
124
            Pin::Oxy {
1250×
                heads, bookmark, ..
1260×
            } => format!(
127
                "oxy {bookmark} {}",
1280×
                heads
1290×
                    .iter()
1300×
                    .map(|h| h.short(12))
1310×
                    .collect::<Vec<_>>()
1320×
                    .join(" ")
133
            ),
1340×
            Pin::Git { sha, branch } => format!("git {branch} {}", &sha[..sha.len().min(12)]),
135
        }
1360×
    }
137
1382×
    pub fn is_git(&self) -> bool {
1392×
        matches!(self, Pin::Git { .. })
1402×
    }
141
}
142
143
/// One `.oxymodules` entry.
144
#[derive(Clone, Debug, Default, PartialEq, Eq)]
145
pub struct Module {
146
    pub url: String,
147
    /// Oxygen bookmark or Git branch to track.
148
    pub bookmark: Option<String>,
149
    pub git: bool,
150
}
151
152
/// `.oxymodules`: `[path]` sections with `url`, and `bookmark` (Oxygen) or
153
/// `branch` (Git) keys.
154
#[derive(Clone, Debug, Default, PartialEq, Eq)]
155
pub struct Modules {
156
    pub entries: BTreeMap<String, Module>,
157
}
158
159
impl Modules {
16098×
    pub fn parse(text: &str) -> Modules {
16198×
        let mut out = Modules::default();
16298×
        let mut current: Option<String> = None;
163302×
        for line in text.lines() {
164302×
            let line = line.trim();
165302×
            if line.is_empty() || line.starts_with('#') {
16691×
                continue;
167211×
            }
168211×
            if let Some(inner) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
169100×
                let path = inner
170100×
                    .trim()
171100×
                    .trim_matches('"')
172100×
                    .trim_end_matches('/')
173100×
                    .to_string();
174100×
                out.entries.entry(path.clone()).or_default();
175100×
                current = Some(path);
176100×
                continue;
177111×
            }
178111×
            let (Some(path), Some((k, v))) = (&current, line.split_once('=')) else {
1790×
                continue;
180
            };
181111×
            let v = v.trim().trim_matches('"').to_string();
182111×
            let m = out.entries.get_mut(path).unwrap();
183111×
            match k.trim() {
184111×
                "url" => m.url = v,
18511×
                "bookmark" => {
1862×
                    m.bookmark = Some(v);
1872×
                    m.git = false;
1882×
                }
1899×
                "branch" => {
1902×
                    m.bookmark = Some(v);
1912×
                    m.git = true;
1922×
                }
1937×
                "kind" => m.git = v == "git",
1940×
                _ => {}
195
            }
196
        }
19798×
        out
19898×
    }
199
20013×
    pub fn to_text(&self) -> String {
20113×
        let mut s = String::new();
20214×
        for (path, m) in &self.entries {
20314×
            s.push_str(&format!("[{path}]\nurl = {}\n", m.url));
20414×
            if m.git {
2052×
                s.push_str("kind = git\n");
20612×
            }
20714×
            if let Some(b) = &m.bookmark {
2082×
                s.push_str(&format!(
209
                    "{} = {b}\n",
2102×
                    if m.git { "branch" } else { "bookmark" }
211
                ));
21212×
            }
21314×
            s.push('\n');
214
        }
21513×
        s
21613×
    }
217
218
    /// From Git's `.gitmodules` (`[submodule "name"]` with `path`, `url`, `branch`).
2192×
    pub fn from_gitmodules(text: &str) -> Modules {
2202×
        let mut out = Modules::default();
2212×
        let mut path: Option<String> = None;
2222×
        let mut url = String::new();
2232×
        let mut branch: Option<String> = None;
2242×
        let flush =
2255×
            |out: &mut Modules, path: &Option<String>, url: &str, branch: &Option<String>| {
2265×
                if let Some(p) = path {
2273×
                    out.entries.insert(
2283×
                        p.clone(),
2293×
                        Module {
2303×
                            url: url.to_string(),
2313×
                            bookmark: branch.clone(),
2323×
                            git: true,
2333×
                        },
2343×
                    );
2353×
                }
2365×
            };
23710×
        for line in text.lines() {
23810×
            let line = line.trim();
23910×
            if line.starts_with("[submodule") {
2403×
                flush(&mut out, &path, &url, &branch);
2413×
                path = None;
2423×
                url.clear();
2433×
                branch = None;
2443×
                continue;
2457×
            }
2467×
            if let Some((k, v)) = line.split_once('=') {
2477×
                let v = v.trim().to_string();
2487×
                match k.trim() {
2497×
                    "path" => path = Some(v.trim_end_matches('/').to_string()),
2504×
                    "url" => url = v,
2511×
                    "branch" => branch = Some(v),
2520×
                    _ => {}
253
                }
2540×
            }
255
        }
2562×
        flush(&mut out, &path, &url, &branch);
2572×
        out
2582×
    }
259
260
    /// Git's `.gitmodules` form of the Git-kind entries.
2612×
    pub fn to_gitmodules(&self) -> String {
2622×
        let mut s = String::new();
2634×
        for (path, m) in &self.entries {
2644×
            if !m.git {
2650×
                continue;
2664×
            }
2674×
            s.push_str(&format!(
2684×
                "[submodule \"{path}\"]\n\tpath = {path}\n\turl = {}\n",
2694×
                m.url
2704×
            ));
2714×
            if let Some(b) = &m.bookmark {
2722×
                s.push_str(&format!("\tbranch = {b}\n"));
2732×
            }
274
        }
2752×
        s
2762×
    }
277
}
278
279
#[cfg(test)]
280
mod tests {
281
    use super::*;
282
    use crate::hash::ObjectRef;
283
284
    #[test]
2851×
    fn pins_round_trip() {
2861×
        let o = Pin::Oxy {
2871×
            repo: [7; 16],
2881×
            heads: vec![ObjectRef([2; 32]), ObjectRef([1; 32])],
2891×
            bookmark: "main".into(),
2901×
        };
2911×
        let back = Pin::decode(&o.encode()).unwrap();
2921×
        assert_eq!(
293
            back,
2941×
            Pin::Oxy {
2951×
                repo: [7; 16],
2961×
                heads: vec![ObjectRef([1; 32]), ObjectRef([2; 32])],
2971×
                bookmark: "main".into()
2981×
            }
299
        );
3001×
        let g = Pin::Git {
3011×
            sha: "0cb4742f64cf204e5fafd0963749d84afc561892".into(),
3021×
            branch: "main".into(),
3031×
        };
3041×
        assert_eq!(Pin::decode(&g.encode()).unwrap(), g);
3051×
        assert!(g.is_git() && !o.is_git());
3061×
    }
307
308
    #[test]
3091×
    fn modules_files() {
3101×
        let text = "[libs/sub]\nurl = ../sub\nbookmark = main\n\n[vendor/x]\nurl = https://example.org/x.git\nkind = git\nbranch = trunk\n";
3111×
        let m = Modules::parse(text);
3121×
        assert_eq!(
3131×
            m.entries["libs/sub"],
3141×
            Module {
3151×
                url: "../sub".into(),
3161×
                bookmark: Some("main".into()),
3171×
                git: false
3181×
            }
319
        );
3201×
        assert_eq!(
3211×
            m.entries["vendor/x"],
3221×
            Module {
3231×
                url: "https://example.org/x.git".into(),
3241×
                bookmark: Some("trunk".into()),
3251×
                git: true
3261×
            }
327
        );
3281×
        assert_eq!(Modules::parse(&m.to_text()), m);
3291×
        let gm = "[submodule \"rstar-api\"]\n\tpath = rstar-api\n\turl = https://github.com/x/rstar-api.git\n[submodule \"c\"]\n\tpath = cstar-api\n\turl = https://github.com/x/cstar-api.git\n\tbranch = dev\n";
3301×
        let g = Modules::from_gitmodules(gm);
3311×
        assert_eq!(g.entries.len(), 2);
3321×
        assert!(g.entries["rstar-api"].git && g.entries["rstar-api"].bookmark.is_none());
3331×
        assert_eq!(g.entries["cstar-api"].bookmark.as_deref(), Some("dev"));
3341×
        assert!(
3351×
            g.to_gitmodules().contains("[submodule \"cstar-api\"]")
3361×
                && g.to_gitmodules().contains("branch = dev")
337
        );
3381×
    }
339
}