Skip to main content

relay_knowledge/code/source/
resolution.rs

1use 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}