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