Skip to main content

tig_core/
clone.rs

1//! smart HTTP からの clone (sans-io の状態機械)。
2//!
3//! HTTP client は持たない。呼び出し側は `next_request()` が返す request を
4//! repository URL に対して送り、response body を `on_response()` に渡す。
5//! これを request が無くなるまで繰り返し、`finish()` で結果を取り出す。
6//!
7//! ```text
8//! GET  <url>/info/refs?service=git-upload-pack   (capability advertisement)
9//! POST <url>/git-upload-pack                     (ls-refs)
10//! POST <url>/git-upload-pack                     (fetch)
11//! ```
12//!
13//! いずれの request にも `Git-Protocol: version=2` header を付けること。
14
15use alloc::vec::Vec;
16
17use crate::err::{Error, Result};
18use crate::oid::Oid;
19use crate::protov2::{self, RefEntry};
20
21/// capability advertisement の要求先 (repository URL からの相対 path)。
22pub const INFO_REFS_PATH: &str = "info/refs?service=git-upload-pack";
23/// command の要求先 (repository URL からの相対 path)。
24pub const UPLOAD_PACK_PATH: &str = "git-upload-pack";
25/// POST body の Content-Type。
26pub const REQUEST_CONTENT_TYPE: &str = "application/x-git-upload-pack-request";
27/// 全 request に付ける header。
28pub const PROTOCOL_HEADER: (&str, &str) = ("Git-Protocol", "version=2");
29
30/// 呼び出し側が送るべき HTTP request。path は repository URL からの相対。
31#[derive(Debug, PartialEq, Eq)]
32pub enum Request {
33    Get { path: &'static str },
34    Post { path: &'static str, body: Vec<u8> },
35}
36
37#[derive(Debug, Clone, Default)]
38pub struct CloneOptions {
39    /// shallow clone の深さ。None は全履歴。
40    pub depth: Option<u32>,
41    /// 取得する ref 名 (完全一致、例: b"refs/heads/main")。None は全 ref。
42    pub want_ref: Option<Vec<u8>>,
43}
44
45enum State {
46    InfoRefs,
47    LsRefs,
48    Fetch,
49    Done,
50}
51
52pub struct Clone {
53    state: State,
54    opts: CloneOptions,
55    refs: Vec<RefEntry>,
56    shallow: Vec<Oid>,
57    pack: Vec<u8>,
58}
59
60/// clone の結果。`bundle::write` にそのまま渡せる。
61pub struct CloneOutcome {
62    pub refs: Vec<RefEntry>,
63    /// 履歴を打ち切った commit (depth 指定時)。
64    pub shallow: Vec<Oid>,
65    pub pack: Vec<u8>,
66}
67
68impl Clone {
69    pub fn new(opts: CloneOptions) -> Self {
70        Self {
71            state: State::InfoRefs,
72            opts,
73            refs: Vec::new(),
74            shallow: Vec::new(),
75            pack: Vec::new(),
76        }
77    }
78
79    /// 次に送るべき request。None なら完了。
80    pub fn next_request(&self) -> Option<Request> {
81        match self.state {
82            State::InfoRefs => Some(Request::Get {
83                path: INFO_REFS_PATH,
84            }),
85            State::LsRefs => Some(Request::Post {
86                path: UPLOAD_PACK_PATH,
87                body: protov2::ls_refs_request(&[]),
88            }),
89            State::Fetch => Some(Request::Post {
90                path: UPLOAD_PACK_PATH,
91                body: protov2::fetch_request(&self.wants(), self.opts.depth),
92            }),
93            State::Done => None,
94        }
95    }
96
97    /// 直前の request に対する response body を渡して状態を進める。
98    pub fn on_response(&mut self, body: &[u8]) -> Result<()> {
99        match self.state {
100            State::InfoRefs => {
101                let adv = protov2::parse_advertisement(body)?;
102                if !adv.ls_refs || !adv.fetch {
103                    return Err(Error::Unsupported("server without ls-refs/fetch"));
104                }
105                if self.opts.depth.is_some() && !adv.fetch_shallow {
106                    return Err(Error::Unsupported("server without shallow fetch"));
107                }
108                self.state = State::LsRefs;
109            }
110            State::LsRefs => {
111                self.refs = protov2::parse_ls_refs(body)?;
112                if self.wants().is_empty() {
113                    return Err(Error::Corrupt("no matching ref to fetch"));
114                }
115                self.state = State::Fetch;
116            }
117            State::Fetch => {
118                let resp = protov2::parse_fetch_response(body)?;
119                self.shallow = resp.shallow;
120                self.pack = resp.pack;
121                self.state = State::Done;
122            }
123            State::Done => return Err(Error::Corrupt("response after completion")),
124        }
125        Ok(())
126    }
127
128    /// fetch で want する oid の一覧 (重複除去済み)。
129    fn wants(&self) -> Vec<Oid> {
130        let mut wants: Vec<Oid> = Vec::new();
131        for entry in &self.refs {
132            if let Some(name) = &self.opts.want_ref
133                && entry.name != *name
134            {
135                continue;
136            }
137            if !wants.contains(&entry.oid) {
138                wants.push(entry.oid);
139            }
140        }
141        wants
142    }
143
144    pub fn is_done(&self) -> bool {
145        matches!(self.state, State::Done)
146    }
147
148    pub fn finish(self) -> Result<CloneOutcome> {
149        if !self.is_done() {
150            return Err(Error::Corrupt("clone not finished"));
151        }
152        let refs = match &self.opts.want_ref {
153            None => self.refs,
154            Some(name) => self.refs.into_iter().filter(|e| e.name == *name).collect(),
155        };
156        Ok(CloneOutcome {
157            refs,
158            shallow: self.shallow,
159            pack: self.pack,
160        })
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::pkt;
168
169    fn advertisement() -> Vec<u8> {
170        let mut b = Vec::new();
171        pkt::write_line(&mut b, b"version 2");
172        pkt::write_line(&mut b, b"ls-refs");
173        pkt::write_line(&mut b, b"fetch=shallow");
174        pkt::write_flush(&mut b);
175        b
176    }
177
178    fn ls_refs_response(hex: &str) -> Vec<u8> {
179        let mut b = Vec::new();
180        pkt::write_line(&mut b, format!("{hex} refs/heads/main").as_bytes());
181        pkt::write_flush(&mut b);
182        b
183    }
184
185    fn fetch_response() -> Vec<u8> {
186        let mut b = Vec::new();
187        pkt::write_line(&mut b, b"packfile");
188        pkt::write_data(&mut b, &[1, b'P', b'A', b'C', b'K']);
189        pkt::write_flush(&mut b);
190        b
191    }
192
193    #[test]
194    fn walks_three_requests() {
195        let hex = "1111111111111111111111111111111111111111";
196        let mut clone = Clone::new(CloneOptions::default());
197
198        assert!(matches!(clone.next_request(), Some(Request::Get { .. })));
199        clone.on_response(&advertisement()).unwrap();
200
201        let Some(Request::Post { body, .. }) = clone.next_request() else {
202            panic!("ls-refs request expected");
203        };
204        assert!(String::from_utf8_lossy(&body).contains("command=ls-refs"));
205        clone.on_response(&ls_refs_response(hex)).unwrap();
206
207        let Some(Request::Post { body, .. }) = clone.next_request() else {
208            panic!("fetch request expected");
209        };
210        let text = String::from_utf8_lossy(&body);
211        assert!(text.contains("command=fetch"));
212        assert!(text.contains(&format!("want {hex}")));
213        clone.on_response(&fetch_response()).unwrap();
214
215        assert!(clone.is_done());
216        assert_eq!(clone.finish().unwrap().pack, b"PACK");
217    }
218
219    #[test]
220    fn depth_requires_shallow_capability() {
221        let mut b = Vec::new();
222        pkt::write_line(&mut b, b"version 2");
223        pkt::write_line(&mut b, b"ls-refs");
224        pkt::write_line(&mut b, b"fetch");
225        pkt::write_flush(&mut b);
226
227        let mut clone = Clone::new(CloneOptions {
228            depth: Some(1),
229            want_ref: None,
230        });
231        clone.next_request();
232        assert_eq!(
233            clone.on_response(&b).unwrap_err(),
234            Error::Unsupported("server without shallow fetch")
235        );
236    }
237
238    #[test]
239    fn missing_ref_is_error() {
240        let mut clone = Clone::new(CloneOptions {
241            depth: None,
242            want_ref: Some(b"refs/heads/nonexistent".to_vec()),
243        });
244        clone.next_request();
245        clone.on_response(&advertisement()).unwrap();
246        assert!(
247            clone
248                .on_response(&ls_refs_response(
249                    "1111111111111111111111111111111111111111"
250                ))
251                .is_err()
252        );
253    }
254}