1use alloc::vec::Vec;
14
15use crate::err::{Error, Result};
16use crate::oid::Oid;
17use crate::protov0::{self, Command, PushReport, ZERO_OID};
18
19pub const INFO_REFS_PATH: &str = "info/refs?service=git-receive-pack";
21pub const RECEIVE_PACK_PATH: &str = "git-receive-pack";
23pub const REQUEST_CONTENT_TYPE: &str = "application/x-git-receive-pack-request";
25
26#[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 updates: Vec<(Vec<u8>, Oid)>,
43 pack: Vec<u8>,
44 commands: Vec<Command>,
45 report: Option<PushReport>,
46}
47
48pub struct PushOutcome {
50 pub up_to_date: bool,
52 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 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 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 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 #[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}