Skip to main content

relay_knowledge/code/source/resolution/
mod.rs

1//! Git/filesystem ref and snapshot resolution.
2
3use 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;