Skip to main content

thin_fetch/repo/
mod.rs

1use std::str::FromStr;
2use std::{collections::HashMap, io::Read};
3
4use ureq::Body;
5use ureq::http::Response;
6use ureq::{BodyReader, http::StatusCode};
7
8use crate::error::{Error, WithError as _, bail};
9use crate::object::Object;
10use crate::parcel::{Parcel, ParcelDecoder};
11use crate::{Execute as _, HashKind, LsRefs, Reference};
12
13/// Remote Git repository.
14///
15/// # Example
16///
17/// ```rust
18/// use thin_fetch::{HashKind, Repo};
19///
20/// let url = "https://git.ddd.rip/ptrcnull/thin-fetch.git";
21/// let repo = Repo::new(url, HashKind::Sha1);
22///
23/// // perform an operation on the repo:
24/// let refs = repo.list_refs();
25/// ```
26pub struct Repo {
27    url: String,
28    hash_kind: HashKind,
29    object_cache: HashMap<String, Object>,
30}
31
32impl Repo {
33    /// Create new repo with a given URL, discovering the hash kind from the server.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error when the remote repo does not support v2 protocol,
38    /// or does not send `object-format` in capabilities.
39    pub fn new_discover<S: Into<String>>(url: S) -> Result<Self, Error> {
40        let mut repo = Self {
41            url: url.into(),
42            hash_kind: HashKind::Sha1,
43            object_cache: HashMap::new(),
44        };
45
46        let res = repo.discover().with_err_msg("could not discover")?;
47
48        if let Some(format) = &res.object_format {
49            repo.hash_kind = HashKind::from_str(format)?;
50        } else {
51            bail!("remote repo did not send object-format");
52        }
53
54        Ok(repo)
55    }
56
57    /// Create new repo with a given URL and hash kind.
58    ///
59    /// Assumes that the remote repo does support v2 protocol.
60    pub fn new<S: Into<String>>(url: S, hash_kind: HashKind) -> Self {
61        Self {
62            url: url.into(),
63            hash_kind,
64            object_cache: HashMap::new(),
65        }
66    }
67
68    /// Call the repo with a body that serializes into [`Parcel`](crate::Parcel).
69    ///
70    /// # Errors
71    ///
72    /// Returns an error when the HTTP request fails,
73    /// or the parcel cannot be parsed.
74    pub fn call<C: Into<Parcel>>(&self, cmd: C) -> Result<Parcel, Error> {
75        let dec = self.stream(cmd)?;
76        dec.collect()
77    }
78
79    /// Call the repo with a body that serializes into [`Parcel`](crate::Parcel).
80    ///
81    /// # Errors
82    ///
83    /// Returns an error when the HTTP request fails,
84    /// or the parcel cannot be parsed.
85    pub fn stream<C: Into<Parcel>>(
86        &self,
87        cmd: C,
88    ) -> Result<ParcelDecoder<BodyReader<'static>>, Error> {
89        let body: Parcel = cmd.into();
90
91        let mut data: Vec<u8> = vec![];
92        body.write_to(&mut data)
93            .with_err_msg("could not serialize parcel")?;
94
95        let agent = ureq::Agent::config_builder()
96            .http_status_as_error(false)
97            .build()
98            .new_agent();
99
100        let res = agent
101            .post(format!("{}/git-upload-pack", self.url))
102            .header("Git-Protocol", "version=2")
103            .header("Content-Type", "application/x-git-upload-pack-request")
104            .header("Accept", "application/x-git-upload-pack-result")
105            .send(data)
106            .with_err_msg("could not send HTTP request")?;
107
108        parse_response(res)
109    }
110
111    /// List all remote references.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error when the command fails to execute.
116    pub fn list_refs(&self) -> Result<Vec<Reference>, Error> {
117        let cmd = LsRefs {
118            peel: true,
119            symrefs: true,
120            unborn: true,
121            ref_prefix: None,
122            hash_kind: self.hash_kind,
123        };
124
125        cmd.execute(self)
126    }
127
128    /// Get value of single remote ref.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error when the command fails to execute.
133    pub fn get_ref(&self, needle: &str) -> Result<Option<Reference>, Error> {
134        let refs = self
135            .list_refs_prefix(needle)
136            .with_err_msg("could not get refs")?;
137
138        Ok(refs.into_iter().find(|r| r.name == needle))
139    }
140
141    /// List all remote references, filtering by a given prefix.
142    ///
143    /// # Errors
144    ///
145    /// Returns an error when the command fails to execute.
146    pub fn list_refs_prefix<P: Into<String>>(&self, prefix: P) -> Result<Vec<Reference>, Error> {
147        let cmd = LsRefs {
148            peel: true,
149            symrefs: true,
150            unborn: true,
151            ref_prefix: Some(prefix.into()),
152            hash_kind: self.hash_kind,
153        };
154
155        cmd.execute(self)
156    }
157
158    pub fn objects(&self) -> impl Iterator<Item = &Object> {
159        self.object_cache.values()
160    }
161}
162
163mod discover;
164mod fetch;
165mod get;
166
167fn parse_response(res: Response<Body>) -> Result<ParcelDecoder<BodyReader<'static>>, Error> {
168    let status = res.status();
169
170    if let Some(len) = res.headers().get("content-length")
171        && len == "0"
172    {
173        bail!("server returned empty body (status {})", status);
174    }
175
176    let body = res.into_body();
177    let mut reader = body.into_reader();
178
179    if status != StatusCode::OK {
180        let mut body = Vec::new();
181        reader
182            .read_to_end(&mut body)
183            .with_err_msg("could not read HTTP response")?;
184
185        if body.is_empty() {
186            bail!("HTTP remote error: {status}");
187        }
188
189        if let Ok(err) = str::from_utf8(&body) {
190            bail!("HTTP remote error: {status}: {err}")
191        }
192
193        bail!("HTTP remote error: {status} ({} bytes)", body.len());
194    }
195
196    Ok(ParcelDecoder::new(reader))
197}