Skip to main content

relay_knowledge/code/source/resolution/
mod.rs

1//! Git/filesystem ref and snapshot resolution.
2
3use std::{path::Path, time::Duration};
4
5use super::{
6    CodeIndexError,
7    git::{GitNulRecordBudget, git_nul_records_match_bounded},
8    scope::scoped_source_snapshot_for_filters,
9    source::{source_commit_is_filesystem, source_kind},
10};
11use crate::code::{resolve_git_ref_bounded, resolve_git_tree_bounded};
12
13const GITLINK_PROBE_BUDGET: GitNulRecordBudget = GitNulRecordBudget {
14    max_records: 1_000_000,
15    max_record_bytes: 1024 * 1024,
16    max_stderr_bytes: 64 * 1024,
17    timeout: Duration::from_secs(30),
18};
19
20pub fn resolve_repository_ref(
21    root_path: impl AsRef<Path>,
22    ref_selector: &str,
23) -> Result<String, CodeIndexError> {
24    resolve_repository_ref_with_path_filters(root_path, ref_selector, &[])
25}
26
27pub fn resolve_repository_ref_with_path_filters(
28    root_path: impl AsRef<Path>,
29    ref_selector: &str,
30    path_filters: &[String],
31) -> Result<String, CodeIndexError> {
32    resolve_repository_ref_with_filters(root_path, ref_selector, path_filters, &[])
33}
34
35pub fn resolve_repository_ref_with_filters(
36    root_path: impl AsRef<Path>,
37    ref_selector: &str,
38    path_filters: &[String],
39    language_filters: &[String],
40) -> Result<String, CodeIndexError> {
41    let root = root_path.as_ref();
42    if source_commit_is_filesystem(ref_selector) {
43        return Ok(ref_selector.to_owned());
44    }
45    if !source_kind(root)?.is_filesystem() {
46        return resolve_git_ref_bounded(root, ref_selector);
47    }
48
49    Ok(
50        scoped_source_snapshot_for_filters(root, ref_selector, path_filters, language_filters)?
51            .resolved_commit_sha,
52    )
53}
54
55pub fn resolve_repository_snapshot(
56    root_path: impl AsRef<Path>,
57    ref_selector: &str,
58) -> Result<(String, String), CodeIndexError> {
59    resolve_repository_snapshot_with_path_filters(root_path, ref_selector, &[])
60}
61
62pub fn resolve_repository_snapshot_with_path_filters(
63    root_path: impl AsRef<Path>,
64    ref_selector: &str,
65    path_filters: &[String],
66) -> Result<(String, String), CodeIndexError> {
67    resolve_repository_snapshot_with_filters(root_path, ref_selector, path_filters, &[])
68}
69
70pub fn resolve_repository_snapshot_with_filters(
71    root_path: impl AsRef<Path>,
72    ref_selector: &str,
73    path_filters: &[String],
74    language_filters: &[String],
75) -> Result<(String, String), CodeIndexError> {
76    let root = root_path.as_ref();
77    if source_commit_is_filesystem(ref_selector) {
78        return Ok((ref_selector.to_owned(), ref_selector.to_owned()));
79    }
80    if !source_kind(root)?.is_filesystem() {
81        let commit = resolve_git_ref_bounded(root, ref_selector)?;
82        if git_tree_has_scoped_gitlinks(root, &commit, path_filters)? {
83            let snapshot =
84                scoped_source_snapshot_for_filters(root, &commit, path_filters, language_filters)?;
85            return Ok((snapshot.resolved_commit_sha, snapshot.tree_hash));
86        }
87        return Ok((commit.clone(), resolve_git_tree_bounded(root, &commit)?));
88    }
89
90    let snapshot =
91        scoped_source_snapshot_for_filters(root, ref_selector, path_filters, language_filters)?;
92
93    Ok((snapshot.resolved_commit_sha, snapshot.tree_hash))
94}
95
96fn git_tree_has_scoped_gitlinks(
97    root: &Path,
98    commit: &str,
99    path_filters: &[String],
100) -> Result<bool, CodeIndexError> {
101    let filters = scoped_gitlink_filters(path_filters);
102    if filters.is_empty() {
103        return git_tree_has_gitlinks_under(root, commit, None);
104    }
105
106    for filter in filters {
107        if git_tree_has_gitlink_overlapping_filter(root, commit, &filter)? {
108            return Ok(true);
109        }
110    }
111
112    Ok(false)
113}
114
115fn git_tree_has_gitlink_overlapping_filter(
116    root: &Path,
117    commit: &str,
118    filter: &str,
119) -> Result<bool, CodeIndexError> {
120    for ancestor in path_and_ancestors(filter) {
121        if git_tree_exact_path_is_gitlink(root, commit, ancestor)? {
122            return Ok(true);
123        }
124    }
125
126    git_tree_has_gitlinks_under(root, commit, Some(filter))
127}
128
129fn git_tree_has_gitlinks_under(
130    root: &Path,
131    commit: &str,
132    scope: Option<&str>,
133) -> Result<bool, CodeIndexError> {
134    let mut args = vec!["ls-tree", "-r", "-z", commit];
135    if let Some(scope) = scope {
136        args.extend(["--", scope]);
137    }
138
139    git_nul_records_match_bounded(
140        root,
141        &args,
142        GITLINK_PROBE_BUDGET,
143        git_tree_record_is_gitlink,
144        "Git tree gitlink probe",
145    )
146}
147
148fn git_tree_exact_path_is_gitlink(
149    root: &Path,
150    commit: &str,
151    path: &str,
152) -> Result<bool, CodeIndexError> {
153    git_nul_records_match_bounded(
154        root,
155        &["ls-tree", "-z", commit, "--", path],
156        GITLINK_PROBE_BUDGET,
157        git_tree_record_is_gitlink,
158        "Git tree gitlink probe",
159    )
160}
161
162fn git_tree_record_is_gitlink(record: &[u8]) -> bool {
163    record.split(|byte| byte.is_ascii_whitespace()).next() == Some(b"160000".as_slice())
164}
165
166fn scoped_gitlink_filters(path_filters: &[String]) -> Vec<String> {
167    let mut filters = Vec::new();
168    for filter in path_filters {
169        let normalized = normalize_path_filter(filter);
170        if normalized.is_empty() {
171            continue;
172        }
173        if normalized == "." {
174            return Vec::new();
175        }
176        if !filters.iter().any(|existing| existing == normalized) {
177            filters.push(normalized.to_owned());
178        }
179    }
180
181    filters
182}
183
184fn path_and_ancestors(path: &str) -> Vec<&str> {
185    let mut ancestors = Vec::new();
186    let mut current = path;
187    while !current.is_empty() {
188        ancestors.push(current);
189        let Some((parent, _)) = current.rsplit_once('/') else {
190            break;
191        };
192        current = parent;
193    }
194
195    ancestors
196}
197
198fn normalize_path_filter(filter: &str) -> &str {
199    let mut filter = filter.trim_end_matches(['/', '\\']);
200    while let Some(stripped) = filter.strip_prefix("./") {
201        filter = stripped;
202    }
203
204    filter
205}
206
207#[cfg(test)]
208#[path = "mod_tests.rs"]
209mod tests;