1use crate::io;
2use std::io::{Read, Cursor};
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum ClientError {
7 #[error("invalid server status")]
9 InvalidServerStatus,
10 #[error("invalid response content type, expected {0}, got {1}")]
11 InvalidContentType(&'static str, String),
12
13 #[error("invalid ref hash")]
15 InvalidRefHash,
16
17 #[error(transparent)]
18 RequestError(#[from] ureq::Error),
19 #[error(transparent)]
20 IOError(#[from] std::io::Error),
21 #[error(transparent)]
22 Utf8Error(#[from] std::string::FromUtf8Error),
23}
24
25pub struct Client {
26 url: String,
27 client: ureq::Agent,
28}
29
30impl Client {
31 pub fn new(url: &str) -> Self {
32 Self {
33 url: url.to_owned(),
34 client: ureq::AgentBuilder::new()
35 .user_agent("anni-fetch 0.2.0")
36 .build(),
37 }
38 }
39
40 pub fn handshake(&mut self) -> Result<PktIter, ClientError> {
41 let reader = self.client
42 .get(&format!("{}/info/refs?service=git-upload-pack", &self.url))
43 .set("Git-Protocol", "version=2")
44 .call()?
45 .into_reader();
46 Ok(PktIter::new(reader))
47 }
48
49 #[deprecated]
51 pub fn command(&self, command: &str, capabilities: Option<&[(&str, Option<&[&str]>)]>, arguments: &[&str]) -> Result<impl Read + Send, ClientError> {
52 let out = Vec::new();
53 let mut cursor = std::io::Cursor::new(out);
54 io::write_pktline(&mut cursor, &format!("command={}", command))?;
55 io::write_pktline(&mut cursor, "object-format=sha1")?;
56 io::write_pktline(&mut cursor, "agent=git/2.28.0")?;
57
58 if let Some(capabilities) = capabilities {
59 for (k, v) in capabilities {
60 if let Some(v) = v {
61 io::write_pktline(&mut cursor, &format!("{}={}", k, v.join(" ")))?;
62 } else {
63 io::write_pktline(&mut cursor, k)?;
64 }
65 }
66 }
67 io::write_packet(&mut cursor, 1)?;
68
69 for arg in arguments.iter() {
70 io::write_pktline(&mut cursor, arg)?;
71 }
72 io::write_packet(&mut cursor, 0)?;
73
74 Ok(self.client
75 .post(&format!("{}/git-upload-pack", &self.url))
76 .set("Git-Protocol", "version=2")
77 .set("Content-Type", "application/x-git-upload-pack-request")
78 .set("Accept", "application/x-git-upload-pack-result")
79 .send_bytes(&cursor.into_inner())?
80 .into_reader())
81 }
82
83 pub fn request(&self, body: Vec<u8>) -> Result<PktIter, ClientError> {
84 let response = self.client
85 .post(&format!("{}/git-upload-pack", &self.url))
86 .set("Git-Protocol", "version=2")
87 .set("Content-Type", "application/x-git-upload-pack-request")
88 .set("Accept", "application/x-git-upload-pack-result")
89 .send_bytes(&body)?;
90 if response.status() != 200 {
91 return Err(ClientError::InvalidServerStatus);
92 } else if response.content_type() != "application/x-git-upload-pack-result" {
93 return Err(ClientError::InvalidContentType("application/x-git-upload-pack-result", response.content_type().to_owned()));
94 }
95 let reader = response.into_reader();
96 Ok(PktIter::new(reader))
97 }
98
99 pub fn ls_ref(&self, prefix: &str) -> Result<String, ClientError> {
100 let iter = self.request(
101 RequestBuilder::new(true)
102 .command("ls-refs")
103 .argument("peel")
104 .argument("symrefs")
105 .argument(&format!("ref-prefix {}", prefix))
106 .build()
107 )?;
108 for msg in iter {
109 match msg {
110 Message::Normal(mut n) => {
111 n.truncate(40);
112 return Ok(String::from_utf8(n)?);
113 }
114 _ => {}
115 }
116 }
117 Err(ClientError::InvalidRefHash)
118 }
119
120 #[deprecated]
122 pub fn want_ref(&self, prefix: &str) -> Result<String, ClientError> {
123 Ok(format!("want {}", self.ls_ref(prefix)?))
124 }
125}
126
127pub struct RequestBuilder {
129 inner: Cursor<Vec<u8>>,
130 delimeter_written: bool,
131 flush_written: bool,
132}
133
134impl RequestBuilder {
135 pub fn new(auto_packet: bool) -> Self {
140 let mut inner = Default::default();
141 io::write_pktline(&mut inner, "object-format=sha1").unwrap();
142 io::write_pktline(&mut inner, "agent=git/2.28.0").unwrap();
143 Self {
144 inner,
145 delimeter_written: !auto_packet,
146 flush_written: !auto_packet,
147 }
148 }
149
150 pub fn command(mut self, command: &str) -> Self {
152 io::write_pktline(&mut self.inner, &format!("command={}", command)).unwrap();
153 self
154 }
155
156 pub fn capability(mut self, name: &str, value: &[&str]) -> Self {
158 if value.len() != 0 {
159 io::write_pktline(&mut self.inner, &format!("{}={}", name, value.join(" "))).unwrap();
160 } else {
161 io::write_pktline(&mut self.inner, name).unwrap();
162 }
163 self
164 }
165
166 pub fn argument(mut self, arg: &str) -> Self {
168 if !self.delimeter_written {
169 self = self.packet(Message::Delimeter);
170 self.delimeter_written = true;
171 }
172
173 io::write_pktline(&mut self.inner, arg).unwrap();
174 self
175 }
176
177 pub fn packet(mut self, packet: Message) -> Self {
179 match packet {
180 Message::Flush => io::write_packet(&mut self.inner, 0).unwrap(),
181 Message::Delimeter => io::write_packet(&mut self.inner, 1).unwrap(),
182 _ => panic!("invalid request packet type")
183 }
184 self
185 }
186
187 pub fn want(self, hash: &str) -> Self {
189 self.argument(&format!("want {}", hash))
190 }
191
192 pub fn have(self, hash: &str) -> Self {
194 self.argument(&format!("have {}", hash))
195 }
196
197 pub fn build(mut self) -> Vec<u8> {
199 if !self.flush_written {
200 self = self.packet(Message::Flush);
201 self.flush_written = true;
202 }
203 self.inner.into_inner()
204 }
205}
206
207#[derive(Debug, PartialEq)]
209pub enum Message {
210 Normal(Vec<u8>),
211 Flush,
215 Delimeter,
219 ResponseEnd,
223 PackStart,
233 PackData(Vec<u8>),
237 PackProgress(String),
241 PackError(String),
245}
246
247pub struct PktIter {
248 inner: Box<dyn Read + Send>,
249 is_data: bool,
250}
251
252impl PktIter {
253 pub fn new(reader: impl Read + Send + 'static) -> Self {
254 Self {
255 inner: Box::new(reader),
256 is_data: false,
257 }
258 }
259}
260
261impl Iterator for PktIter {
262 type Item = Message;
263
264 fn next(&mut self) -> Option<Self::Item> {
265 let (mut data, len) = io::read_pktline(&mut self.inner).unwrap();
266 if len == 0 && data.len() == 0 {
267 None
268 } else if len > 0 && self.is_data {
269 match data[0] {
270 1 => {
271 data.remove(0);
273 Some(Message::PackData(data))
274 }
275 2 => {
276 Some(Message::PackProgress(String::from_utf8_lossy(&data[1..]).trim().to_owned()))
278 }
279 3 => {
280 Some(Message::PackError(String::from_utf8_lossy(&data[1..]).trim().to_owned()))
282 }
283 _ => unreachable!(),
284 }
285 } else if data == b"packfile\n" {
286 self.is_data = true;
287 Some(Message::PackStart)
288 } else {
289 Some(match len {
290 0 => Message::Flush,
291 1 => Message::Delimeter,
292 2 => Message::ResponseEnd,
293 _ => Message::Normal(data),
294 })
295 }
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use crate::{Client, Pack};
302 use crate::client::Message::*;
303 use std::io::Cursor;
304 use crate::client::RequestBuilder;
305
306 #[test]
307 fn test_handshake() {
308 let v: Vec<_> = Client::new("https://github.com/project-anni/repo.git").handshake().unwrap().collect();
309 let (l, r) = v.split_at(3);
310 let (agent, r) = r.split_at(1);
311 assert_eq!(l, vec![
312 Normal(b"# service=git-upload-pack\n".to_vec()),
313 Flush,
314 Normal(b"version 2\n".to_vec()),
315 ]);
316 match &agent[0] {
317 Normal(n) => assert!(n.starts_with(b"agent=git/github-")),
318 _ => panic!("invalid agent message type"),
319 }
320 assert_eq!(r, vec![
321 Normal(b"ls-refs\n".to_vec()),
322 Normal(b"fetch=shallow filter\n".to_vec()),
323 Normal(b"server-option\n".to_vec()),
324 Normal(b"object-format=sha1\n".to_vec()),
325 Flush,
326 ]);
327 }
328
329 #[test]
330 fn test_ls_ref() {
331 let hash = Client::new("https://github.com/project-anni/anni-fetch.git")
332 .ls_ref("refs/tags")
333 .unwrap();
334 assert_eq!(hash, "9192b5e5f2941fd76aa5a08043dc8aa6a31831a2");
335 }
336
337 #[test]
338 fn test_fetch_iter() {
339 let client = Client::new("https://github.com/project-anni/repo.git");
340 let iter = client.request(
341 RequestBuilder::new(true)
342 .command("fetch")
343 .argument("thin-pack")
344 .argument("ofs-delta")
345 .argument("deepen 1")
346 .want(&client.ls_ref("HEAD").expect("failed to get sha1 of HEAD"))
347 .argument("done")
348 .build()
349 ).unwrap();
350 let mut pack = Vec::new();
351 for msg in iter {
352 match msg {
353 PackData(mut d) => pack.append(&mut d),
354 _ => {}
355 }
356 }
357 let mut cursor = Cursor::new(pack);
358 Pack::from_reader(&mut cursor).expect("invalid pack file");
359 }
360}