Skip to main content

tig_core/
bundle.rs

1//! git bundle (v2 / v3) の読み込み。
2//!
3//! bundle は `git bundle create` が生成する単一ファイルで、「署名行 + (v3 のみ
4//! capability 行) + prerequisite / ref の列 + 空行 + packfile」から成る。
5//! オフライン配布した repository を読むための入力形式として用いる。
6
7use alloc::vec::Vec;
8
9use crate::err::{Error, Result};
10use crate::oid::Oid;
11use crate::pack::Pack;
12
13pub struct Bundle<'a> {
14    /// ref 名 (例: b"refs/heads/main") と指し先。ファイル中の出現順。
15    pub refs: Vec<(&'a [u8], Oid)>,
16    /// bundle に含まれない前提 commit。thin な bundle の履歴境界になる。
17    pub prerequisites: Vec<Oid>,
18    pub pack: Pack<'a>,
19}
20
21impl<'a> Bundle<'a> {
22    pub fn parse(data: &'a [u8]) -> Result<Self> {
23        let (signature, mut rest) = split_line(data)?;
24        let v3 = match signature {
25            b"# v2 git bundle" => false,
26            b"# v3 git bundle" => true,
27            _ => return Err(Error::Unsupported("bundle signature")),
28        };
29
30        // v3 の capability 行。object-format=sha1 以外は未対応として拒否する
31        // (黙って誤読しないため)。
32        if v3 {
33            while rest.first() == Some(&b'@') {
34                let (line, next) = split_line(rest)?;
35                rest = next;
36                match &line[1..] {
37                    b"object-format=sha1" => {}
38                    _ => return Err(Error::Unsupported("bundle capability")),
39                }
40            }
41        }
42
43        let mut refs = Vec::new();
44        let mut prerequisites = Vec::new();
45        loop {
46            let (line, next) = split_line(rest)?;
47            rest = next;
48            if line.is_empty() {
49                break;
50            }
51            if let Some(v) = line.strip_prefix(b"-") {
52                // "-<oid> <コメント>"。コメントは任意。
53                let hex = v.get(..40).ok_or(Error::Corrupt("bundle prerequisite"))?;
54                prerequisites.push(Oid::from_hex(hex)?);
55            } else {
56                let hex = line.get(..40).ok_or(Error::Corrupt("bundle ref"))?;
57                let name = line
58                    .get(41..)
59                    .filter(|n| !n.is_empty() && line[40] == b' ')
60                    .ok_or(Error::Corrupt("bundle ref name"))?;
61                refs.push((name, Oid::from_hex(hex)?));
62            }
63        }
64
65        Ok(Self {
66            refs,
67            prerequisites,
68            pack: Pack::parse(rest)?,
69        })
70    }
71
72    /// 名前で ref を引く。
73    pub fn find_ref(&self, name: &[u8]) -> Option<Oid> {
74        self.refs
75            .iter()
76            .find(|(n, _)| *n == name)
77            .map(|(_, oid)| *oid)
78    }
79}
80
81fn split_line(data: &[u8]) -> Result<(&[u8], &[u8])> {
82    let nl = data
83        .iter()
84        .position(|&b| b == b'\n')
85        .ok_or(Error::Corrupt("bundle header line"))?;
86    Ok((&data[..nl], &data[nl + 1..]))
87}
88
89/// refs と packfile から bundle v2 を構成する。
90///
91/// `shallow` は fetch が報告した履歴の打ち切り点。その commit の parent のうち
92/// pack に含まれないものを prerequisite として記録する (bundle に無い前提
93/// commit を明示し、本ライブラリの walk と git の双方が境界を判定できるように
94/// する)。
95pub fn write(refs: &[(&[u8], Oid)], shallow: &[Oid], pack_data: &[u8]) -> Result<Vec<u8>> {
96    use crate::object::{self, Kind};
97
98    let mut prerequisites: Vec<Oid> = Vec::new();
99    if !shallow.is_empty() {
100        let pack = Pack::parse(pack_data)?;
101        for oid in shallow {
102            let Some((Kind::Commit, body)) = pack.read_object(oid)? else {
103                return Err(Error::Corrupt("shallow oid not in pack"));
104            };
105            for parent in object::parse_commit(&body)?.parents {
106                if !pack.contains(&parent) && !prerequisites.contains(&parent) {
107                    prerequisites.push(parent);
108                }
109            }
110        }
111    }
112
113    let mut out = Vec::with_capacity(pack_data.len() + 64 * (refs.len() + 2));
114    out.extend_from_slice(b"# v2 git bundle\n");
115    for oid in &prerequisites {
116        out.push(b'-');
117        push_hex(&mut out, oid);
118        out.push(b'\n');
119    }
120    for (name, oid) in refs {
121        push_hex(&mut out, oid);
122        out.push(b' ');
123        out.extend_from_slice(name);
124        out.push(b'\n');
125    }
126    out.push(b'\n');
127    out.extend_from_slice(pack_data);
128    Ok(out)
129}
130
131fn push_hex(out: &mut Vec<u8>, oid: &Oid) {
132    const HEX: &[u8; 16] = b"0123456789abcdef";
133    for &b in oid.as_bytes() {
134        out.push(HEX[usize::from(b >> 4)]);
135        out.push(HEX[usize::from(b & 0xf)]);
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::sha1;
143    use alloc::vec;
144
145    /// entry 0 件の pack (正しい trailer 付き)。
146    fn empty_pack() -> Vec<u8> {
147        let mut c = Vec::new();
148        c.extend_from_slice(b"PACK");
149        c.extend_from_slice(&2u32.to_be_bytes());
150        c.extend_from_slice(&0u32.to_be_bytes());
151        let digest = sha1::digest(&c);
152        c.extend_from_slice(&digest);
153        c
154    }
155
156    #[test]
157    fn write_parse_roundtrip() {
158        let oid = Oid::from_bytes([0x42; 20]);
159        let pack = empty_pack();
160        let data = write(&[(b"refs/heads/main", oid)], &[], &pack).unwrap();
161
162        let bundle = Bundle::parse(&data).unwrap();
163        assert_eq!(bundle.refs, vec![(&b"refs/heads/main"[..], oid)]);
164        assert!(bundle.prerequisites.is_empty());
165        assert!(bundle.pack.is_empty());
166    }
167}