OXYGENHUB

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

197 of 217 lines run, 13 of 32 functions · run #381 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 {
2952×
    pub fn encode(&self) -> Vec<u8> {
3052×
        let v = match self {
31
            Pin::Oxy {
3249×
                repo,
3349×
                heads,
3449×
                bookmark,
35
            } => {
3649×
                let mut heads = heads.clone();
3749×
                heads.sort();
3849×
                heads.dedup();
3949×
                Value::map([
4049×
                    (0, Value::Uint(FORMAT_VERSION)),
4149×
                    (1, Value::Uint(0)),
4249×
                    (2, Value::Bytes(repo.to_vec())),
43
                    (
44
                        3,
4555×
                        Value::Array(heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()),
46
                    ),
4749×
                    (4, Value::Text(bookmark.clone())),
48
                ])
49
            }
503×
            Pin::Git { sha, branch } => Value::map([
513×
                (0, Value::Uint(FORMAT_VERSION)),
523×
                (1, Value::Uint(1)),
533×
                (2, Value::Null),
543×
                (3, Value::Text(sha.clone())),
553×
                (4, Value::Text(branch.clone())),
563×
            ]),
57
        };
5852×
        cbor::encode(&v)
5952×
    }
60
6126×
    pub fn decode(b: &[u8]) -> Result<Pin, ObjectError> {
6226×
        let v = cbor::decode(b)?;
6326×
        let m = v
6426×
            .as_map()
6526×
            .ok_or_else(|| ObjectError::Schema("pin: expected map".into()))?;
6626×
        check_version(m)?;
6726×
        let bookmark = text(field(m, 4, "bookmark")?, "bookmark")?.to_owned();
6826×
        match uint(field(m, 1, "kind")?, "kind")? {
69
            0 => {
7021×
                let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")?
7121×
                    .try_into()
7221×
                    .map_err(|_| ObjectError::Schema("pin: repo id must be 16 bytes".into()))?;
7321×
                let heads = array(field(m, 3, "heads")?, "heads")?
7421×
                    .iter()
7522×
                    .map(|h| oref(h, "head"))
7621×
                    .collect::<Result<Vec<_>, _>>()?;
7721×
                Ok(Pin::Oxy {
7821×
                    repo,
7921×
                    heads,
8021×
                    bookmark,
8121×
                })
82
            }
83
            1 => Ok(Pin::Git {
845×
                sha: text(field(m, 3, "sha")?, "sha")?.to_owned(),
855×
                branch: bookmark,
86
            }),
870×
            k => Err(ObjectError::Schema(format!("pin: unknown kind {k}"))),
88
        }
8926×
    }
90
91
    /// A one-line description for listings.
920×
    pub fn summary(&self) -> String {
930×
        match self {
94
            Pin::Oxy {
950×
                heads, bookmark, ..
960×
            } => format!(
97
                "oxy {bookmark} {}",
980×
                heads
990×
                    .iter()
1000×
                    .map(|h| h.short(12))
1010×
                    .collect::<Vec<_>>()
1020×
                    .join(" ")
103
            ),
1040×
            Pin::Git { sha, branch } => format!("git {branch} {}", &sha[..sha.len().min(12)]),
105
        }
1060×
    }
107
1082×
    pub fn is_git(&self) -> bool {
1092×
        matches!(self, Pin::Git { .. })
1102×
    }
111
}
112
113
/// One `.oxymodules` entry.
114
#[derive(Clone, Debug, Default, PartialEq, Eq)]
115
pub struct Module {
116
    pub url: String,
117
    /// Oxygen bookmark or Git branch to track.
118
    pub bookmark: Option<String>,
119
    pub git: bool,
120
}
121
122
/// `.oxymodules`: `[path]` sections with `url`, and `bookmark` (Oxygen) or
123
/// `branch` (Git) keys.
124
#[derive(Clone, Debug, Default, PartialEq, Eq)]
125
pub struct Modules {
126
    pub entries: BTreeMap<String, Module>,
127
}
128
129
impl Modules {
13088×
    pub fn parse(text: &str) -> Modules {
13188×
        let mut out = Modules::default();
13288×
        let mut current: Option<String> = None;
133276×
        for line in text.lines() {
134276×
            let line = line.trim();
135276×
            if line.is_empty() || line.starts_with('#') {
13685×
                continue;
137191×
            }
138191×
            if let Some(inner) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
13990×
                let path = inner
14090×
                    .trim()
14190×
                    .trim_matches('"')
14290×
                    .trim_end_matches('/')
14390×
                    .to_string();
14490×
                out.entries.entry(path.clone()).or_default();
14590×
                current = Some(path);
14690×
                continue;
147101×
            }
148101×
            let (Some(path), Some((k, v))) = (&current, line.split_once('=')) else {
1490×
                continue;
150
            };
151101×
            let v = v.trim().trim_matches('"').to_string();
152101×
            let m = out.entries.get_mut(path).unwrap();
153101×
            match k.trim() {
154101×
                "url" => m.url = v,
15511×
                "bookmark" => {
1562×
                    m.bookmark = Some(v);
1572×
                    m.git = false;
1582×
                }
1599×
                "branch" => {
1602×
                    m.bookmark = Some(v);
1612×
                    m.git = true;
1622×
                }
1637×
                "kind" => m.git = v == "git",
1640×
                _ => {}
165
            }
166
        }
16788×
        out
16888×
    }
169
17012×
    pub fn to_text(&self) -> String {
17112×
        let mut s = String::new();
17213×
        for (path, m) in &self.entries {
17313×
            s.push_str(&format!("[{path}]\nurl = {}\n", m.url));
17413×
            if m.git {
1752×
                s.push_str("kind = git\n");
17611×
            }
17713×
            if let Some(b) = &m.bookmark {
1782×
                s.push_str(&format!(
179
                    "{} = {b}\n",
1802×
                    if m.git { "branch" } else { "bookmark" }
181
                ));
18211×
            }
18313×
            s.push('\n');
184
        }
18512×
        s
18612×
    }
187
188
    /// From Git's `.gitmodules` (`[submodule "name"]` with `path`, `url`, `branch`).
1892×
    pub fn from_gitmodules(text: &str) -> Modules {
1902×
        let mut out = Modules::default();
1912×
        let mut path: Option<String> = None;
1922×
        let mut url = String::new();
1932×
        let mut branch: Option<String> = None;
1942×
        let flush =
1955×
            |out: &mut Modules, path: &Option<String>, url: &str, branch: &Option<String>| {
1965×
                if let Some(p) = path {
1973×
                    out.entries.insert(
1983×
                        p.clone(),
1993×
                        Module {
2003×
                            url: url.to_string(),
2013×
                            bookmark: branch.clone(),
2023×
                            git: true,
2033×
                        },
2043×
                    );
2053×
                }
2065×
            };
20710×
        for line in text.lines() {
20810×
            let line = line.trim();
20910×
            if line.starts_with("[submodule") {
2103×
                flush(&mut out, &path, &url, &branch);
2113×
                path = None;
2123×
                url.clear();
2133×
                branch = None;
2143×
                continue;
2157×
            }
2167×
            if let Some((k, v)) = line.split_once('=') {
2177×
                let v = v.trim().to_string();
2187×
                match k.trim() {
2197×
                    "path" => path = Some(v.trim_end_matches('/').to_string()),
2204×
                    "url" => url = v,
2211×
                    "branch" => branch = Some(v),
2220×
                    _ => {}
223
                }
2240×
            }
225
        }
2262×
        flush(&mut out, &path, &url, &branch);
2272×
        out
2282×
    }
229
230
    /// Git's `.gitmodules` form of the Git-kind entries.
2312×
    pub fn to_gitmodules(&self) -> String {
2322×
        let mut s = String::new();
2334×
        for (path, m) in &self.entries {
2344×
            if !m.git {
2350×
                continue;
2364×
            }
2374×
            s.push_str(&format!(
2384×
                "[submodule \"{path}\"]\n\tpath = {path}\n\turl = {}\n",
2394×
                m.url
2404×
            ));
2414×
            if let Some(b) = &m.bookmark {
2422×
                s.push_str(&format!("\tbranch = {b}\n"));
2432×
            }
244
        }
2452×
        s
2462×
    }
247
}
248
249
#[cfg(test)]
250
mod tests {
251
    use super::*;
252
    use crate::hash::ObjectRef;
253
254
    #[test]
2551×
    fn pins_round_trip() {
2561×
        let o = Pin::Oxy {
2571×
            repo: [7; 16],
2581×
            heads: vec![ObjectRef([2; 32]), ObjectRef([1; 32])],
2591×
            bookmark: "main".into(),
2601×
        };
2611×
        let back = Pin::decode(&o.encode()).unwrap();
2621×
        assert_eq!(
263
            back,
2641×
            Pin::Oxy {
2651×
                repo: [7; 16],
2661×
                heads: vec![ObjectRef([1; 32]), ObjectRef([2; 32])],
2671×
                bookmark: "main".into()
2681×
            }
269
        );
2701×
        let g = Pin::Git {
2711×
            sha: "0cb4742f64cf204e5fafd0963749d84afc561892".into(),
2721×
            branch: "main".into(),
2731×
        };
2741×
        assert_eq!(Pin::decode(&g.encode()).unwrap(), g);
2751×
        assert!(g.is_git() && !o.is_git());
2761×
    }
277
278
    #[test]
2791×
    fn modules_files() {
2801×
        let text = "[libs/sub]\nurl = ../sub\nbookmark = main\n\n[vendor/x]\nurl = https://example.org/x.git\nkind = git\nbranch = trunk\n";
2811×
        let m = Modules::parse(text);
2821×
        assert_eq!(
2831×
            m.entries["libs/sub"],
2841×
            Module {
2851×
                url: "../sub".into(),
2861×
                bookmark: Some("main".into()),
2871×
                git: false
2881×
            }
289
        );
2901×
        assert_eq!(
2911×
            m.entries["vendor/x"],
2921×
            Module {
2931×
                url: "https://example.org/x.git".into(),
2941×
                bookmark: Some("trunk".into()),
2951×
                git: true
2961×
            }
297
        );
2981×
        assert_eq!(Modules::parse(&m.to_text()), m);
2991×
        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";
3001×
        let g = Modules::from_gitmodules(gm);
3011×
        assert_eq!(g.entries.len(), 2);
3021×
        assert!(g.entries["rstar-api"].git && g.entries["rstar-api"].bookmark.is_none());
3031×
        assert_eq!(g.entries["cstar-api"].bookmark.as_deref(), Some("dev"));
3041×
        assert!(
3051×
            g.to_gitmodules().contains("[submodule \"cstar-api\"]")
3061×
                && g.to_gitmodules().contains("branch = dev")
307
        );
3081×
    }
309
}