Skip to main content

thoughts_tool/git/
remote_refs.rs

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