OXYGENHUB
# c6aa5bd9d1c8 oxygen / crates / oxy-core / src / subrepo.rs
8.6 KiB · Rust · last changed Subrepositories: subrepo node kind with Oxygen and Git pins, .oxymodules, nested repositories skipped by the walker and moved by checkout, oxy submodule add|update|status|remove, Git gitlink import and export (spec 0.8 section 7.8) 1 month ago source blame history raw
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::{array, bytes, check_version, field, oref, text, uint, ObjectError, FORMAT_VERSION};
11
use std::collections::BTreeMap;
12
13
#[derive(Clone, Debug, PartialEq, Eq)]
14
pub enum Pin {
15
    Oxy { repo: [u8; 16], heads: Vec<ChangeHash>, bookmark: String },
16
    Git { sha: String, branch: String },
17
}
18
19
impl Pin {
20
    pub fn encode(&self) -> Vec<u8> {
21
        let v = match self {
22
            Pin::Oxy { repo, heads, bookmark } => {
23
                let mut heads = heads.clone();
24
                heads.sort();
25
                heads.dedup();
26
                Value::map([
27
                    (0, Value::Uint(FORMAT_VERSION)),
28
                    (1, Value::Uint(0)),
29
                    (2, Value::Bytes(repo.to_vec())),
30
                    (3, Value::Array(heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())),
31
                    (4, Value::Text(bookmark.clone())),
32
                ])
33
            }
34
            Pin::Git { sha, branch } => Value::map([
35
                (0, Value::Uint(FORMAT_VERSION)),
36
                (1, Value::Uint(1)),
37
                (2, Value::Null),
38
                (3, Value::Text(sha.clone())),
39
                (4, Value::Text(branch.clone())),
40
            ]),
41
        };
42
        cbor::encode(&v)
43
    }
44
45
    pub fn decode(b: &[u8]) -> Result<Pin, ObjectError> {
46
        let v = cbor::decode(b)?;
47
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("pin: expected map".into()))?;
48
        check_version(m)?;
49
        let bookmark = text(field(m, 4, "bookmark")?, "bookmark")?.to_owned();
50
        match uint(field(m, 1, "kind")?, "kind")? {
51
            0 => {
52
                let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")?.try_into().map_err(|_| ObjectError::Schema("pin: repo id must be 16 bytes".into()))?;
53
                let heads = array(field(m, 3, "heads")?, "heads")?.iter().map(|h| oref(h, "head")).collect::<Result<Vec<_>, _>>()?;
54
                Ok(Pin::Oxy { repo, heads, bookmark })
55
            }
56
            1 => Ok(Pin::Git { sha: text(field(m, 3, "sha")?, "sha")?.to_owned(), branch: bookmark }),
57
            k => Err(ObjectError::Schema(format!("pin: unknown kind {k}"))),
58
        }
59
    }
60
61
    /// A one-line description for listings.
62
    pub fn summary(&self) -> String {
63
        match self {
64
            Pin::Oxy { heads, bookmark, .. } => format!("oxy {bookmark} {}", heads.iter().map(|h| h.short(12)).collect::<Vec<_>>().join(" ")),
65
            Pin::Git { sha, branch } => format!("git {branch} {}", &sha[..sha.len().min(12)]),
66
        }
67
    }
68
69
    pub fn is_git(&self) -> bool {
70
        matches!(self, Pin::Git { .. })
71
    }
72
}
73
74
/// One `.oxymodules` entry.
75
#[derive(Clone, Debug, Default, PartialEq, Eq)]
76
pub struct Module {
77
    pub url: String,
78
    /// Oxygen bookmark or Git branch to track.
79
    pub bookmark: Option<String>,
80
    pub git: bool,
81
}
82
83
/// `.oxymodules`: `[path]` sections with `url`, and `bookmark` (Oxygen) or
84
/// `branch` (Git) keys.
85
#[derive(Clone, Debug, Default, PartialEq, Eq)]
86
pub struct Modules {
87
    pub entries: BTreeMap<String, Module>,
88
}
89
90
impl Modules {
91
    pub fn parse(text: &str) -> Modules {
92
        let mut out = Modules::default();
93
        let mut current: Option<String> = None;
94
        for line in text.lines() {
95
            let line = line.trim();
96
            if line.is_empty() || line.starts_with('#') {
97
                continue;
98
            }
99
            if let Some(inner) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
100
                let path = inner.trim().trim_matches('"').trim_end_matches('/').to_string();
101
                out.entries.entry(path.clone()).or_default();
102
                current = Some(path);
103
                continue;
104
            }
105
            let (Some(path), Some((k, v))) = (&current, line.split_once('=')) else { continue };
106
            let v = v.trim().trim_matches('"').to_string();
107
            let m = out.entries.get_mut(path).unwrap();
108
            match k.trim() {
109
                "url" => m.url = v,
110
                "bookmark" => {
111
                    m.bookmark = Some(v);
112
                    m.git = false;
113
                }
114
                "branch" => {
115
                    m.bookmark = Some(v);
116
                    m.git = true;
117
                }
118
                "kind" => m.git = v == "git",
119
                _ => {}
120
            }
121
        }
122
        out
123
    }
124
125
    pub fn to_text(&self) -> String {
126
        let mut s = String::new();
127
        for (path, m) in &self.entries {
128
            s.push_str(&format!("[{path}]\nurl = {}\n", m.url));
129
            if m.git {
130
                s.push_str("kind = git\n");
131
            }
132
            if let Some(b) = &m.bookmark {
133
                s.push_str(&format!("{} = {b}\n", if m.git { "branch" } else { "bookmark" }));
134
            }
135
            s.push('\n');
136
        }
137
        s
138
    }
139
140
    /// From Git's `.gitmodules` (`[submodule "name"]` with `path`, `url`, `branch`).
141
    pub fn from_gitmodules(text: &str) -> Modules {
142
        let mut out = Modules::default();
143
        let mut path: Option<String> = None;
144
        let mut url = String::new();
145
        let mut branch: Option<String> = None;
146
        let flush = |out: &mut Modules, path: &Option<String>, url: &str, branch: &Option<String>| {
147
            if let Some(p) = path {
148
                out.entries.insert(p.clone(), Module { url: url.to_string(), bookmark: branch.clone(), git: true });
149
            }
150
        };
151
        for line in text.lines() {
152
            let line = line.trim();
153
            if line.starts_with("[submodule") {
154
                flush(&mut out, &path, &url, &branch);
155
                path = None;
156
                url.clear();
157
                branch = None;
158
                continue;
159
            }
160
            if let Some((k, v)) = line.split_once('=') {
161
                let v = v.trim().to_string();
162
                match k.trim() {
163
                    "path" => path = Some(v.trim_end_matches('/').to_string()),
164
                    "url" => url = v,
165
                    "branch" => branch = Some(v),
166
                    _ => {}
167
                }
168
            }
169
        }
170
        flush(&mut out, &path, &url, &branch);
171
        out
172
    }
173
174
    /// Git's `.gitmodules` form of the Git-kind entries.
175
    pub fn to_gitmodules(&self) -> String {
176
        let mut s = String::new();
177
        for (path, m) in &self.entries {
178
            if !m.git {
179
                continue;
180
            }
181
            s.push_str(&format!("[submodule \"{path}\"]\n\tpath = {path}\n\turl = {}\n", m.url));
182
            if let Some(b) = &m.bookmark {
183
                s.push_str(&format!("\tbranch = {b}\n"));
184
            }
185
        }
186
        s
187
    }
188
}
189
190
#[cfg(test)]
191
mod tests {
192
    use super::*;
193
    use crate::hash::ObjectRef;
194
195
    #[test]
196
    fn pins_round_trip() {
197
        let o = Pin::Oxy { repo: [7; 16], heads: vec![ObjectRef([2; 32]), ObjectRef([1; 32])], bookmark: "main".into() };
198
        let back = Pin::decode(&o.encode()).unwrap();
199
        assert_eq!(back, Pin::Oxy { repo: [7; 16], heads: vec![ObjectRef([1; 32]), ObjectRef([2; 32])], bookmark: "main".into() });
200
        let g = Pin::Git { sha: "0cb4742f64cf204e5fafd0963749d84afc561892".into(), branch: "main".into() };
201
        assert_eq!(Pin::decode(&g.encode()).unwrap(), g);
202
        assert!(g.is_git() && !o.is_git());
203
    }
204
205
    #[test]
206
    fn modules_files() {
207
        let text = "[libs/sub]\nurl = ../sub\nbookmark = main\n\n[vendor/x]\nurl = https://example.org/x.git\nkind = git\nbranch = trunk\n";
208
        let m = Modules::parse(text);
209
        assert_eq!(m.entries["libs/sub"], Module { url: "../sub".into(), bookmark: Some("main".into()), git: false });
210
        assert_eq!(m.entries["vendor/x"], Module { url: "https://example.org/x.git".into(), bookmark: Some("trunk".into()), git: true });
211
        assert_eq!(Modules::parse(&m.to_text()), m);
212
        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";
213
        let g = Modules::from_gitmodules(gm);
214
        assert_eq!(g.entries.len(), 2);
215
        assert!(g.entries["rstar-api"].git && g.entries["rstar-api"].bookmark.is_none());
216
        assert_eq!(g.entries["cstar-api"].bookmark.as_deref(), Some("dev"));
217
        assert!(g.to_gitmodules().contains("[submodule \"cstar-api\"]") && g.to_gitmodules().contains("branch = dev"));
218
    }
219
}