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