OXYGENHUB
# 16aa9e03d85b oxygen / crates / oxy-core / src / subrepo.rs
10.9 KiB · Rust · last changed A subrepo at its pinned state is unmodified on any bookmark (closes #147) 9 days 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::{
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.
33
    pub fn same_state(&self, other: &Pin) -> bool {
34
        match (self, other) {
35
            (
36
                Pin::Oxy {
37
                    repo: r1,
38
                    heads: h1,
39
                    ..
40
                },
41
                Pin::Oxy {
42
                    repo: r2,
43
                    heads: h2,
44
                    ..
45
                },
46
            ) => {
47
                let (mut a, mut b) = (h1.clone(), h2.clone());
48
                a.sort();
49
                a.dedup();
50
                b.sort();
51
                b.dedup();
52
                r1 == r2 && a == b
53
            }
54
            (Pin::Git { sha: a, .. }, Pin::Git { sha: b, .. }) => a == b,
55
            _ => false,
56
        }
57
    }
58
59
    pub fn encode(&self) -> Vec<u8> {
60
        let v = match self {
61
            Pin::Oxy {
62
                repo,
63
                heads,
64
                bookmark,
65
            } => {
66
                let mut heads = heads.clone();
67
                heads.sort();
68
                heads.dedup();
69
                Value::map([
70
                    (0, Value::Uint(FORMAT_VERSION)),
71
                    (1, Value::Uint(0)),
72
                    (2, Value::Bytes(repo.to_vec())),
73
                    (
74
                        3,
75
                        Value::Array(heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()),
76
                    ),
77
                    (4, Value::Text(bookmark.clone())),
78
                ])
79
            }
80
            Pin::Git { sha, branch } => Value::map([
81
                (0, Value::Uint(FORMAT_VERSION)),
82
                (1, Value::Uint(1)),
83
                (2, Value::Null),
84
                (3, Value::Text(sha.clone())),
85
                (4, Value::Text(branch.clone())),
86
            ]),
87
        };
88
        cbor::encode(&v)
89
    }
90
91
    pub fn decode(b: &[u8]) -> Result<Pin, ObjectError> {
92
        let v = cbor::decode(b)?;
93
        let m = v
94
            .as_map()
95
            .ok_or_else(|| ObjectError::Schema("pin: expected map".into()))?;
96
        check_version(m)?;
97
        let bookmark = text(field(m, 4, "bookmark")?, "bookmark")?.to_owned();
98
        match uint(field(m, 1, "kind")?, "kind")? {
99
            0 => {
100
                let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")?
101
                    .try_into()
102
                    .map_err(|_| ObjectError::Schema("pin: repo id must be 16 bytes".into()))?;
103
                let heads = array(field(m, 3, "heads")?, "heads")?
104
                    .iter()
105
                    .map(|h| oref(h, "head"))
106
                    .collect::<Result<Vec<_>, _>>()?;
107
                Ok(Pin::Oxy {
108
                    repo,
109
                    heads,
110
                    bookmark,
111
                })
112
            }
113
            1 => Ok(Pin::Git {
114
                sha: text(field(m, 3, "sha")?, "sha")?.to_owned(),
115
                branch: bookmark,
116
            }),
117
            k => Err(ObjectError::Schema(format!("pin: unknown kind {k}"))),
118
        }
119
    }
120
121
    /// A one-line description for listings.
122
    pub fn summary(&self) -> String {
123
        match self {
124
            Pin::Oxy {
125
                heads, bookmark, ..
126
            } => format!(
127
                "oxy {bookmark} {}",
128
                heads
129
                    .iter()
130
                    .map(|h| h.short(12))
131
                    .collect::<Vec<_>>()
132
                    .join(" ")
133
            ),
134
            Pin::Git { sha, branch } => format!("git {branch} {}", &sha[..sha.len().min(12)]),
135
        }
136
    }
137
138
    pub fn is_git(&self) -> bool {
139
        matches!(self, Pin::Git { .. })
140
    }
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 {
160
    pub fn parse(text: &str) -> Modules {
161
        let mut out = Modules::default();
162
        let mut current: Option<String> = None;
163
        for line in text.lines() {
164
            let line = line.trim();
165
            if line.is_empty() || line.starts_with('#') {
166
                continue;
167
            }
168
            if let Some(inner) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
169
                let path = inner
170
                    .trim()
171
                    .trim_matches('"')
172
                    .trim_end_matches('/')
173
                    .to_string();
174
                out.entries.entry(path.clone()).or_default();
175
                current = Some(path);
176
                continue;
177
            }
178
            let (Some(path), Some((k, v))) = (&current, line.split_once('=')) else {
179
                continue;
180
            };
181
            let v = v.trim().trim_matches('"').to_string();
182
            let m = out.entries.get_mut(path).unwrap();
183
            match k.trim() {
184
                "url" => m.url = v,
185
                "bookmark" => {
186
                    m.bookmark = Some(v);
187
                    m.git = false;
188
                }
189
                "branch" => {
190
                    m.bookmark = Some(v);
191
                    m.git = true;
192
                }
193
                "kind" => m.git = v == "git",
194
                _ => {}
195
            }
196
        }
197
        out
198
    }
199
200
    pub fn to_text(&self) -> String {
201
        let mut s = String::new();
202
        for (path, m) in &self.entries {
203
            s.push_str(&format!("[{path}]\nurl = {}\n", m.url));
204
            if m.git {
205
                s.push_str("kind = git\n");
206
            }
207
            if let Some(b) = &m.bookmark {
208
                s.push_str(&format!(
209
                    "{} = {b}\n",
210
                    if m.git { "branch" } else { "bookmark" }
211
                ));
212
            }
213
            s.push('\n');
214
        }
215
        s
216
    }
217
218
    /// From Git's `.gitmodules` (`[submodule "name"]` with `path`, `url`, `branch`).
219
    pub fn from_gitmodules(text: &str) -> Modules {
220
        let mut out = Modules::default();
221
        let mut path: Option<String> = None;
222
        let mut url = String::new();
223
        let mut branch: Option<String> = None;
224
        let flush =
225
            |out: &mut Modules, path: &Option<String>, url: &str, branch: &Option<String>| {
226
                if let Some(p) = path {
227
                    out.entries.insert(
228
                        p.clone(),
229
                        Module {
230
                            url: url.to_string(),
231
                            bookmark: branch.clone(),
232
                            git: true,
233
                        },
234
                    );
235
                }
236
            };
237
        for line in text.lines() {
238
            let line = line.trim();
239
            if line.starts_with("[submodule") {
240
                flush(&mut out, &path, &url, &branch);
241
                path = None;
242
                url.clear();
243
                branch = None;
244
                continue;
245
            }
246
            if let Some((k, v)) = line.split_once('=') {
247
                let v = v.trim().to_string();
248
                match k.trim() {
249
                    "path" => path = Some(v.trim_end_matches('/').to_string()),
250
                    "url" => url = v,
251
                    "branch" => branch = Some(v),
252
                    _ => {}
253
                }
254
            }
255
        }
256
        flush(&mut out, &path, &url, &branch);
257
        out
258
    }
259
260
    /// Git's `.gitmodules` form of the Git-kind entries.
261
    pub fn to_gitmodules(&self) -> String {
262
        let mut s = String::new();
263
        for (path, m) in &self.entries {
264
            if !m.git {
265
                continue;
266
            }
267
            s.push_str(&format!(
268
                "[submodule \"{path}\"]\n\tpath = {path}\n\turl = {}\n",
269
                m.url
270
            ));
271
            if let Some(b) = &m.bookmark {
272
                s.push_str(&format!("\tbranch = {b}\n"));
273
            }
274
        }
275
        s
276
    }
277
}
278
279
#[cfg(test)]
280
mod tests {
281
    use super::*;
282
    use crate::hash::ObjectRef;
283
284
    #[test]
285
    fn pins_round_trip() {
286
        let o = Pin::Oxy {
287
            repo: [7; 16],
288
            heads: vec![ObjectRef([2; 32]), ObjectRef([1; 32])],
289
            bookmark: "main".into(),
290
        };
291
        let back = Pin::decode(&o.encode()).unwrap();
292
        assert_eq!(
293
            back,
294
            Pin::Oxy {
295
                repo: [7; 16],
296
                heads: vec![ObjectRef([1; 32]), ObjectRef([2; 32])],
297
                bookmark: "main".into()
298
            }
299
        );
300
        let g = Pin::Git {
301
            sha: "0cb4742f64cf204e5fafd0963749d84afc561892".into(),
302
            branch: "main".into(),
303
        };
304
        assert_eq!(Pin::decode(&g.encode()).unwrap(), g);
305
        assert!(g.is_git() && !o.is_git());
306
    }
307
308
    #[test]
309
    fn modules_files() {
310
        let text = "[libs/sub]\nurl = ../sub\nbookmark = main\n\n[vendor/x]\nurl = https://example.org/x.git\nkind = git\nbranch = trunk\n";
311
        let m = Modules::parse(text);
312
        assert_eq!(
313
            m.entries["libs/sub"],
314
            Module {
315
                url: "../sub".into(),
316
                bookmark: Some("main".into()),
317
                git: false
318
            }
319
        );
320
        assert_eq!(
321
            m.entries["vendor/x"],
322
            Module {
323
                url: "https://example.org/x.git".into(),
324
                bookmark: Some("trunk".into()),
325
                git: true
326
            }
327
        );
328
        assert_eq!(Modules::parse(&m.to_text()), m);
329
        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";
330
        let g = Modules::from_gitmodules(gm);
331
        assert_eq!(g.entries.len(), 2);
332
        assert!(g.entries["rstar-api"].git && g.entries["rstar-api"].bookmark.is_none());
333
        assert_eq!(g.entries["cstar-api"].bookmark.as_deref(), Some("dev"));
334
        assert!(
335
            g.to_gitmodules().contains("[submodule \"cstar-api\"]")
336
                && g.to_gitmodules().contains("branch = dev")
337
        );
338
    }
339
}