Skip to main content

thoughts_tool/git/
remote_refs.rs

1use anyhow::Context;
2use anyhow::Result;
3use gix_protocol::handshake::Ref as HandshakeRef;
4use schemars::JsonSchema;
5use serde::Deserialize;
6use serde::Serialize;
7use std::path::Path;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
10pub struct RemoteRef {
11    pub name: String,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub oid: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub peeled: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub target: Option<String>,
18}
19
20pub fn discover_remote_refs(repo_root: &Path, url: &str) -> Result<Vec<RemoteRef>> {
21    let repo = gix::open(repo_root).with_context(|| {
22        format!(
23            "Failed to open repository context for remote ref discovery: {}",
24            repo_root.display()
25        )
26    })?;
27    let remote = repo
28        .remote_at(url)
29        .with_context(|| format!("Failed to create remote for URL: {url}"))?;
30    let connection = remote
31        .connect(gix::remote::Direction::Fetch)
32        .with_context(|| format!("Failed to connect to remote: {url}"))?;
33    // Disable server-side prefix filtering so we receive ALL refs.
34    // With `remote_at()` there are no configured refspecs, so the default
35    // behavior (prefix_from_spec_as_filter_on_remote: true) would cause the
36    // server to return very few refs. Setting it to false ensures we get
37    // the complete ref advertisement (branches, tags, HEAD, etc.).
38    let options = gix::remote::ref_map::Options {
39        prefix_from_spec_as_filter_on_remote: false,
40        ..Default::default()
41    };
42
43    let (ref_map, _handshake) = connection
44        .ref_map(gix::progress::Discard, options)
45        .with_context(|| format!("Failed to list remote refs for: {url}"))?;
46
47    Ok(ref_map
48        .remote_refs
49        .into_iter()
50        .map(|remote_ref| match remote_ref {
51            HandshakeRef::Direct {
52                full_ref_name,
53                object,
54            } => RemoteRef {
55                name: bytes_to_string(full_ref_name.as_ref()),
56                oid: Some(object.to_string()),
57                peeled: None,
58                target: None,
59            },
60            HandshakeRef::Peeled {
61                full_ref_name,
62                tag,
63                object,
64            } => RemoteRef {
65                name: bytes_to_string(full_ref_name.as_ref()),
66                oid: Some(tag.to_string()),
67                peeled: Some(object.to_string()),
68                target: None,
69            },
70            HandshakeRef::Symbolic {
71                full_ref_name,
72                target,
73                tag,
74                object,
75            } => RemoteRef {
76                name: bytes_to_string(full_ref_name.as_ref()),
77                oid: Some(tag.unwrap_or(object).to_string()),
78                peeled: tag.map(|_| object.to_string()),
79                target: Some(bytes_to_string(target.as_ref())),
80            },
81            HandshakeRef::Unborn {
82                full_ref_name,
83                target,
84            } => RemoteRef {
85                name: bytes_to_string(full_ref_name.as_ref()),
86                oid: None,
87                peeled: None,
88                target: Some(bytes_to_string(target.as_ref())),
89            },
90        })
91        .collect())
92}
93
94fn bytes_to_string(bytes: &[u8]) -> String {
95    String::from_utf8_lossy(bytes).into_owned()
96}