Skip to main content

tig_core/
push.rs

1//! smart HTTP への push (sans-io の状態機械)。
2//!
3//! fetch 側 (`clone`) と同じ方針で、HTTP の送受信は呼び出し側の責務とする。
4//!
5//! ```text
6//! GET  <url>/info/refs?service=git-receive-pack   (advertisement)
7//! POST <url>/git-receive-pack                     (update commands + packfile)
8//! ```
9//!
10//! 送る packfile は呼び出し側が用意する (bundle の全 object を `pack::write_pack`
11//! で詰め直す等)。remote が既に持つ object が含まれていても害はない。
12
13use alloc::vec::Vec;
14
15use crate::err::{Error, Result};
16use crate::oid::Oid;
17use crate::protov0::{self, Command, PushReport, ZERO_OID};
18
19/// advertisement の要求先 (repository URL からの相対 path)。
20pub const INFO_REFS_PATH: &str = "info/refs?service=git-receive-pack";
21/// update の要求先 (repository URL からの相対 path)。
22pub const RECEIVE_PACK_PATH: &str = "git-receive-pack";
23/// POST body の Content-Type。
24pub const REQUEST_CONTENT_TYPE: &str = "application/x-git-receive-pack-request";
25
26/// 呼び出し側が送るべき HTTP request。path は repository URL からの相対。
27#[derive(Debug, PartialEq, Eq)]
28pub enum Request {
29    Get { path: &'static str },
30    Post { path: &'static str, body: Vec<u8> },
31}
32
33enum State {
34    InfoRefs,
35    Send { side_band_64k: bool },
36    Done,
37}
38
39pub struct Push {
40    state: State,
41    /// push したい (ref 名, 新しい oid)。
42    updates: Vec<(Vec<u8>, Oid)>,
43    pack: Vec<u8>,
44    commands: Vec<Command>,
45    report: Option<PushReport>,
46}
47
48/// push の結果。
49pub struct PushOutcome {
50    /// 全 ref が既に一致しており、送信自体を行わなかった。
51    pub up_to_date: bool,
52    /// 送信した場合の report-status。
53    pub report: Option<PushReport>,
54}
55
56impl Push {
57    pub fn new(updates: Vec<(Vec<u8>, Oid)>, pack: Vec<u8>) -> Self {
58        Self {
59            state: State::InfoRefs,
60            updates,
61            pack,
62            commands: Vec::new(),
63            report: None,
64        }
65    }
66
67    /// 次に送るべき request。None なら完了。
68    pub fn next_request(&self) -> Option<Request> {
69        match &self.state {
70            State::InfoRefs => Some(Request::Get {
71                path: INFO_REFS_PATH,
72            }),
73            State::Send { side_band_64k } => Some(Request::Post {
74                path: RECEIVE_PACK_PATH,
75                body: protov0::push_request(&self.commands, *side_band_64k, &self.pack),
76            }),
77            State::Done => None,
78        }
79    }
80
81    /// 直前の request に対する response body を渡して状態を進める。
82    pub fn on_response(&mut self, body: &[u8]) -> Result<()> {
83        match &self.state {
84            State::InfoRefs => {
85                let adv = protov0::parse_receive_advertisement(body)?;
86                if !adv.report_status {
87                    return Err(Error::Unsupported("server without report-status"));
88                }
89                for (name, new) in &self.updates {
90                    let old = adv
91                        .refs
92                        .iter()
93                        .find(|(n, _)| n == name)
94                        .map_or(ZERO_OID, |(_, oid)| *oid);
95                    if old != *new {
96                        self.commands.push(Command {
97                            old,
98                            new: *new,
99                            name: name.clone(),
100                        });
101                    }
102                }
103                if self.commands.is_empty() {
104                    self.state = State::Done;
105                } else {
106                    self.state = State::Send {
107                        side_band_64k: adv.side_band_64k,
108                    };
109                }
110            }
111            State::Send { side_band_64k } => {
112                let report = protov0::parse_report_status(body, *side_band_64k)?;
113                // 途中で切れた response を成功と誤認しないよう、送った全 command に
114                // ちょうど 1 つの結果があることを要求する。
115                for command in &self.commands {
116                    let n = report
117                        .results
118                        .iter()
119                        .filter(|(name, _)| *name == command.name)
120                        .count();
121                    if n != 1 {
122                        return Err(Error::Corrupt("report-status missing ref result"));
123                    }
124                }
125                self.report = Some(report);
126                self.state = State::Done;
127            }
128            State::Done => return Err(Error::Corrupt("response after completion")),
129        }
130        Ok(())
131    }
132
133    pub fn is_done(&self) -> bool {
134        matches!(self.state, State::Done)
135    }
136
137    pub fn finish(self) -> Result<PushOutcome> {
138        if !self.is_done() {
139            return Err(Error::Corrupt("push not finished"));
140        }
141        Ok(PushOutcome {
142            up_to_date: self.report.is_none(),
143            report: self.report,
144        })
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::pkt;
152
153    fn oid(n: u8) -> Oid {
154        Oid::from_bytes([n; 20])
155    }
156
157    fn advertisement(refs: &[(&str, Oid)]) -> Vec<u8> {
158        let mut b = Vec::new();
159        pkt::write_line(&mut b, b"# service=git-receive-pack");
160        pkt::write_flush(&mut b);
161        if refs.is_empty() {
162            pkt::write_line(
163                &mut b,
164                b"0000000000000000000000000000000000000000 capabilities^{}\0report-status side-band-64k",
165            );
166        } else {
167            for (i, (name, oid)) in refs.iter().enumerate() {
168                let caps = if i == 0 {
169                    "\0report-status side-band-64k"
170                } else {
171                    ""
172                };
173                pkt::write_line(&mut b, format!("{oid} {name}{caps}").as_bytes());
174            }
175        }
176        pkt::write_flush(&mut b);
177        b
178    }
179
180    fn ok_report(name: &str) -> Vec<u8> {
181        let mut inner = Vec::new();
182        pkt::write_line(&mut inner, b"unpack ok");
183        pkt::write_line(&mut inner, format!("ok {name}").as_bytes());
184        pkt::write_flush(&mut inner);
185        let mut body = Vec::new();
186        let mut payload = alloc::vec![1u8];
187        payload.extend_from_slice(&inner);
188        pkt::write_data(&mut body, &payload);
189        pkt::write_flush(&mut body);
190        body
191    }
192
193    #[test]
194    fn create_ref_on_empty_repository() {
195        let mut push = Push::new(
196            alloc::vec![(b"refs/heads/main".to_vec(), oid(0x11))],
197            b"PACK".to_vec(),
198        );
199        assert!(matches!(push.next_request(), Some(Request::Get { .. })));
200        push.on_response(&advertisement(&[])).unwrap();
201
202        let Some(Request::Post { body, .. }) = push.next_request() else {
203            panic!("update request expected");
204        };
205        let text = String::from_utf8_lossy(&body);
206        assert!(text.contains("0000000000000000000000000000000000000000 1111"));
207        assert!(body.ends_with(b"PACK"));
208
209        push.on_response(&ok_report("refs/heads/main")).unwrap();
210        let outcome = push.finish().unwrap();
211        assert!(!outcome.up_to_date);
212        assert!(outcome.report.unwrap().is_success());
213    }
214
215    #[test]
216    fn up_to_date_skips_send() {
217        let mut push = Push::new(
218            alloc::vec![(b"refs/heads/main".to_vec(), oid(0x11))],
219            b"PACK".to_vec(),
220        );
221        push.next_request();
222        push.on_response(&advertisement(&[("refs/heads/main", oid(0x11))]))
223            .unwrap();
224        assert!(push.is_done());
225        assert!(push.finish().unwrap().up_to_date);
226    }
227
228    // unpack ok の直後で切れた response (ref の結果なし) を成功と誤認しない。
229    #[test]
230    fn truncated_report_rejected() {
231        let mut push = Push::new(
232            alloc::vec![(b"refs/heads/main".to_vec(), oid(0x11))],
233            b"PACK".to_vec(),
234        );
235        push.next_request();
236        push.on_response(&advertisement(&[])).unwrap();
237
238        let mut inner = Vec::new();
239        pkt::write_line(&mut inner, b"unpack ok");
240        pkt::write_flush(&mut inner);
241        let mut body = Vec::new();
242        let mut payload = alloc::vec![1u8];
243        payload.extend_from_slice(&inner);
244        pkt::write_data(&mut body, &payload);
245        pkt::write_flush(&mut body);
246
247        assert_eq!(
248            push.on_response(&body).unwrap_err(),
249            Error::Corrupt("report-status missing ref result")
250        );
251    }
252
253    #[test]
254    fn update_uses_advertised_old_oid() {
255        let mut push = Push::new(
256            alloc::vec![(b"refs/heads/main".to_vec(), oid(0x22))],
257            b"PACK".to_vec(),
258        );
259        push.next_request();
260        push.on_response(&advertisement(&[("refs/heads/main", oid(0x11))]))
261            .unwrap();
262        let Some(Request::Post { body, .. }) = push.next_request() else {
263            panic!("update request expected");
264        };
265        let text = String::from_utf8_lossy(&body);
266        assert!(text.contains(&format!("{} {}", oid(0x11), oid(0x22))));
267    }
268}