g1t/services/repos/src/import.rs

192 lines6,849 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents as a team: lifecycle, merge queue, billing and a new shell1//! Importing a repository from another git host.
2//!
A public import copies every branch and tag, so an imported library keeps its releases3//! This finds a public repository over smart HTTP and its default branch, so
4//! an address that does not work is refused before anything is made; the
5//! copy itself, every branch and tag, is mirror.rs's (`Endpoint::anonymous`).
6//! Only public repositories reachable over https are imported this way.
Agents as a team: lifecycle, merge queue, billing and a new shell7
8use worker::js_sys::Uint8Array;
9use worker::{Fetch, Headers, Method, Request, RequestInit, Result, Url};
10
A public import copies every branch and tag, so an imported library keeps its releases11use crate::land::read_pkt_lines;
Agents as a team: lifecycle, merge queue, billing and a new shell12
13const HEADS: &str = "refs/heads/";
14/// Some hosts only speak the smart protocol to something that says it is git.
15const USER_AGENT: &str = "git/2.45.0 (g1t import)";
16
17/// What the other host says its default branch is and where it points.
18#[derive(Debug, PartialEq, Eq)]
19pub struct Remote {
20 pub branch: String,
21 pub head: String,
22}
23
24/// The address to import from, tidied, or `None` if it is not one g1t will
25/// fetch: it must be https, with no credentials in it.
26pub fn clean_url(url: &str) -> Option<String> {
27 let parsed = Url::parse(url.trim()).ok()?;
28 let plain = parsed.scheme() == "https"
29 && parsed.username().is_empty()
30 && parsed.password().is_none()
31 && parsed.host_str().is_some()
32 && parsed.query().is_none();
33 if !plain {
34 return None;
35 }
36 let host = parsed.host_str()?;
37 let path = parsed.path().trim_end_matches('/');
38 (path.len() > 1).then(|| format!("https://{host}{path}"))
39}
40
41/// The default branch and its head, from a ref advertisement.
42fn parse_remote(bytes: &[u8]) -> Option<Remote> {
43 let (lines, _) = read_pkt_lines(bytes);
44 let mut head = None;
45 let mut branch = None;
46 let mut branches = Vec::new();
47 for line in lines {
48 let mut parts = line.splitn(2, |byte| *byte == 0);
49 let reference = std::str::from_utf8(parts.next()?).ok()?.trim_end();
50 // The first ref carries the capabilities, one of which names the
51 // branch HEAD points to.
52 if let Some(capabilities) = parts
53 .next()
54 .and_then(|bytes| std::str::from_utf8(bytes).ok())
55 {
56 branch = capabilities
57 .split(' ')
58 .find_map(|capability| capability.trim().strip_prefix("symref=HEAD:refs/heads/"))
59 .map(str::to_owned);
60 }
61 let Some((hash, name)) = reference.split_once(' ') else {
62 continue;
63 };
64 if name == "HEAD" {
65 head = Some(hash.to_owned());
66 } else if let Some(name) = name.strip_prefix(HEADS) {
67 branches.push((name.to_owned(), hash.to_owned()));
68 }
69 }
70 // Without a symref, the branch HEAD agrees with; failing that, main.
71 let branch = branch.or_else(|| {
72 let head = head.as_deref()?;
73 branches
74 .iter()
75 .find(|(_, hash)| hash == head)
76 .map(|(name, _)| name.clone())
77 })?;
78 let head = branches
79 .iter()
80 .find(|(name, _)| *name == branch)
81 .map(|(_, hash)| hash.clone())
82 .or(head)?;
83 Some(Remote { branch, head })
84}
85
86fn request(method: Method, url: &str, body: Option<Vec<u8>>) -> Result<Request> {
87 let headers = Headers::new();
88 headers.set("user-agent", USER_AGENT)?;
89 if body.is_some() {
90 headers.set("content-type", "application/x-git-upload-pack-request")?;
91 headers.set("accept", "application/x-git-upload-pack-result")?;
92 }
93 let mut init = RequestInit::new();
94 init.with_method(method).with_headers(headers);
95 if let Some(body) = body {
96 init.with_body(Some(Uint8Array::from(body.as_slice()).into()));
97 }
98 Request::new_with_init(url, &init)
99}
100
101/// Asks the other host what it has. `Err` in the inner result is a reason
102/// to show the person importing.
103pub async fn discover(url: &str) -> Result<std::result::Result<Remote, String>> {
104 let request = request(
105 Method::Get,
106 &format!("{url}/info/refs?service=git-upload-pack"),
107 None,
108 )?;
109 let mut response = match Fetch::Request(request).send().await {
110 Ok(response) => response,
111 Err(_) => return Ok(Err("That address could not be reached.".to_owned())),
112 };
113 if response.status_code() != 200 {
114 return Ok(Err(
115 "No public git repository was found at that address. Private repositories cannot be imported."
116 .to_owned(),
117 ));
118 }
119 let bytes = response.bytes().await?;
120 Ok(parse_remote(&bytes).ok_or_else(|| "That repository is empty.".to_owned()))
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 fn pkt(payload: &str) -> Vec<u8> {
128 format!("{:04x}{payload}", payload.len() + 4).into_bytes()
129 }
130
131 #[test]
132 fn only_plain_https_addresses_are_fetched() {
133 assert_eq!(
134 clean_url(" https://github.com/syntaqx/hello/ ").as_deref(),
135 Some("https://github.com/syntaqx/hello")
136 );
137 assert_eq!(
138 clean_url("https://github.com/syntaqx/hello.git").as_deref(),
139 Some("https://github.com/syntaqx/hello.git")
140 );
141 for bad in [
142 "http://github.com/a/b",
143 "git@github.com:a/b.git",
144 "https://user:secret@github.com/a/b",
145 "https://github.com",
146 "https://github.com/a/b?x=1",
147 "not a url",
148 ] {
149 assert_eq!(clean_url(bad), None, "{bad}");
150 }
151 }
152
153 #[test]
154 fn the_default_branch_comes_from_the_symref() {
155 let trunk = "c71546fcd893ef8b0f57388b65e620d759705dda";
156 let other = "4807077b296e6edbf410d55e72749d3e1170c291";
157 let advertisement = [
158 pkt("# service=git-upload-pack\n"),
159 b"0000".to_vec(),
160 pkt(&format!(
161 "{trunk} HEAD\0multi_ack side-band-64k symref=HEAD:refs/heads/trunk agent=git/x\n"
162 )),
163 pkt(&format!("{other} refs/heads/feature\n")),
164 pkt(&format!("{trunk} refs/heads/trunk\n")),
165 b"0000".to_vec(),
166 ]
167 .concat();
168 assert_eq!(
169 parse_remote(&advertisement),
170 Some(Remote {
171 branch: "trunk".to_owned(),
172 head: trunk.to_owned()
173 })
174 );
175 }
176
177 #[test]
178 fn without_a_symref_the_branch_head_points_to_is_used() {
179 let main = "c71546fcd893ef8b0f57388b65e620d759705dda";
180 let advertisement = [
181 pkt("# service=git-upload-pack\n"),
182 b"0000".to_vec(),
183 pkt(&format!("{main} HEAD\0side-band-64k\n")),
184 pkt(&format!("{main} refs/heads/main\n")),
185 b"0000".to_vec(),
186 ]
187 .concat();
188 assert_eq!(parse_remote(&advertisement).unwrap().branch, "main");
189 let empty = [pkt("# service=git-upload-pack\n"), b"00000000".to_vec()].concat();
190 assert_eq!(parse_remote(&empty), None);
191 }
192}