1use alloc::vec::Vec;
16
17use crate::err::{Error, Result};
18use crate::oid::Oid;
19use crate::protov2::{self, RefEntry};
20
21pub const INFO_REFS_PATH: &str = "info/refs?service=git-upload-pack";
23pub const UPLOAD_PACK_PATH: &str = "git-upload-pack";
25pub const REQUEST_CONTENT_TYPE: &str = "application/x-git-upload-pack-request";
27pub const PROTOCOL_HEADER: (&str, &str) = ("Git-Protocol", "version=2");
29
30#[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 pub depth: Option<u32>,
41 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
60pub struct CloneOutcome {
62 pub refs: Vec<RefEntry>,
63 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 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 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 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}