relay_knowledge/code/source/resolution/
mod.rs1use 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;