pr_01m47d24b0e6n91zwymwxg0vpx/services/repos/src/refs.rs

87 lines3,096 bytesCodeBlame
1//! Listing a repository's branches.
2//!
3//! The Artifacts binding reads commits, trees and blobs but does not list
4//! refs, so this asks the way a git client does: the ref advertisement of
5//! the smart HTTP protocol.
6
7use g1t_contracts::repos::{Branch, GitAccess};
8use worker::{Error, Fetch, Headers, Method, Request, RequestInit, Result};
9
10use crate::land::read_pkt_lines;
11
12const HEADS: &str = "refs/heads/";
13
14/// The branches in a ref advertisement, in the order advertised.
15fn parse_advertisement(bytes: &[u8]) -> Vec<Branch> {
16 let (lines, _) = read_pkt_lines(bytes);
17 lines
18 .into_iter()
19 .filter_map(|line| {
20 // `<hash> <ref>`, and on the first ref a NUL then capabilities.
21 let line = line.split(|byte| *byte == 0).next()?;
22 let line = std::str::from_utf8(line).ok()?.trim_end();
23 let (hash, name) = line.split_once(' ')?;
24 Some(Branch {
25 name: name.strip_prefix(HEADS)?.to_owned(),
26 hash: hash.to_owned(),
27 })
28 })
29 .collect()
30}
31
32pub async fn branches(access: &GitAccess) -> Result<Vec<Branch>> {
33 let headers = Headers::new();
34 headers.set("authorization", &format!("Bearer {}", access.token))?;
35 let mut init = RequestInit::new();
36 init.with_method(Method::Get).with_headers(headers);
37 let request = Request::new_with_init(
38 &format!("{}/info/refs?service=git-upload-pack", access.remote),
39 &init,
40 )?;
41 let mut response = Fetch::Request(request).send().await?;
42 let bytes = response.bytes().await?;
43 if response.status_code() != 200 {
44 return Err(Error::RustError(format!(
45 "listing refs returned {}",
46 response.status_code()
47 )));
48 }
49 Ok(parse_advertisement(&bytes))
50}
51
52#[cfg(test)]
53mod tests {
54 use super::parse_advertisement;
55
56 fn pkt(payload: &str) -> Vec<u8> {
57 format!("{:04x}{payload}", payload.len() + 4).into_bytes()
58 }
59
60 #[test]
61 fn branches_are_read_from_an_advertisement() {
62 let main = "c71546fcd893ef8b0f57388b65e620d759705dda";
63 let shout = "4807077b296e6edbf410d55e72749d3e1170c291";
64 let advertisement = [
65 pkt("# service=git-upload-pack\n"),
66 b"0000".to_vec(),
67 pkt(&format!(
68 "{main} HEAD\0side-band-64k symref=HEAD:refs/heads/main\n"
69 )),
70 pkt(&format!("{main} refs/heads/main\n")),
71 pkt(&format!("{shout} refs/heads/shout\n")),
72 pkt(&format!("{shout} refs/tags/v1.0.0\n")),
73 b"0000".to_vec(),
74 ]
75 .concat();
76 let branches = parse_advertisement(&advertisement);
77 let names: Vec<&str> = branches.iter().map(|branch| branch.name.as_str()).collect();
78 assert_eq!(names, ["main", "shout"]);
79 assert_eq!(branches[1].hash, shout);
80 }
81
82 #[test]
83 fn an_empty_repository_has_no_branches() {
84 let advertisement = [pkt("# service=git-upload-pack\n"), b"00000000".to_vec()].concat();
85 assert!(parse_advertisement(&advertisement).is_empty());
86 }
87}