Skip to main content

relay_knowledge/code/index/
mod.rs

1//! Code index snapshot orchestration and impact seed discovery.
2
3use std::{
4    collections::BTreeMap,
5    path::{Path, PathBuf},
6};
7
8mod deleted_symbols;
9pub(in crate::code) mod filesystem_delta;
10mod full_snapshot;
11mod impact_paths;
12mod incremental;
13pub(in crate::code) mod plan;
14pub(in crate::code) mod snapshot;
15mod worktree_overlay;
16
17use crate::domain::{
18    CodeFileFingerprint, CodeIndexMode, CodeIndexResourceBudget, CodeIndexSnapshot,
19    CodeRepositoryRegistration, CodeRepositorySelector, CodeWorkspaceDetectionConfig,
20};
21
22use crate::code::source::changes::GitChange;
23use crate::code::{
24    CodeIndexError, identity, ids, parser,
25    parser::parse_indexed_file,
26    source::{
27        self, changes,
28        changes::{TrackedEntryScope, diff_changes},
29        git, gitlink as source_gitlink,
30        layout::{self as scope, scoped_source_snapshot_for_filters},
31        resolution::resolve_repository_ref_with_filters,
32        source_commit_is_filesystem, source_kind,
33    },
34};
35pub use deleted_symbols::deleted_symbol_names_for_diff;
36pub(crate) use filesystem_delta::changed_paths_for_filesystem_diff;
37use full_snapshot::build_full_snapshot;
38pub(crate) use full_snapshot::clean_worktree_overlay_hash;
39#[cfg(test)]
40pub(crate) use full_snapshot::mutate_next_filesystem_full_snapshot_read;
41use incremental::{IncrementalSnapshotRequest, build_incremental_snapshot};
42pub(crate) use plan::CodeIndexPlanRecovery;
43pub use plan::{
44    CodeIndexPlan, prepare_full_index_plan, prepare_full_index_plan_with_workspace_detection,
45};
46use worktree_overlay::build_worktree_overlay_snapshot;
47
48pub(in crate::code) const MAX_INCREMENTAL_GITLINK_EXPANDED_PATHS: usize =
49    CodeIndexResourceBudget::DEFAULT_MAX_FILES_PER_BATCH;
50pub(in crate::code) const MAX_INCREMENTAL_CHANGED_PATHS: usize =
51    changes::MAX_GIT_DIFF_CHANGED_PATHS;
52pub(in crate::code) const MAX_HISTORICAL_REUSE_CHANGED_PATHS: usize = 100;
53
54/// Builds a code index snapshot from a clean Git commit or incremental diff.
55pub fn build_index_snapshot(
56    registration: &CodeRepositoryRegistration,
57    selector: &CodeRepositorySelector,
58    mode: CodeIndexMode,
59    previous_hashes: Vec<CodeFileFingerprint>,
60) -> Result<CodeIndexSnapshot, CodeIndexError> {
61    build_index_snapshot_with_base_commit(registration, selector, mode, previous_hashes, None)
62}
63
64pub(crate) fn build_index_snapshot_with_base_commit(
65    registration: &CodeRepositoryRegistration,
66    selector: &CodeRepositorySelector,
67    mode: CodeIndexMode,
68    previous_hashes: Vec<CodeFileFingerprint>,
69    base_resolved_commit_sha: Option<String>,
70) -> Result<CodeIndexSnapshot, CodeIndexError> {
71    build_index_snapshot_with_workspace_detection(
72        registration,
73        selector,
74        mode,
75        previous_hashes,
76        base_resolved_commit_sha,
77        &CodeWorkspaceDetectionConfig::default(),
78    )
79}
80
81pub(crate) fn build_index_snapshot_with_workspace_detection(
82    registration: &CodeRepositoryRegistration,
83    selector: &CodeRepositorySelector,
84    mode: CodeIndexMode,
85    previous_hashes: Vec<CodeFileFingerprint>,
86    base_resolved_commit_sha: Option<String>,
87    workspace_detection: &CodeWorkspaceDetectionConfig,
88) -> Result<CodeIndexSnapshot, CodeIndexError> {
89    let root = PathBuf::from(&registration.root_path);
90    let previous_hashes = previous_hashes
91        .into_iter()
92        .map(|fingerprint| (fingerprint.path, fingerprint.blob_hash))
93        .collect::<BTreeMap<_, _>>();
94
95    match mode {
96        CodeIndexMode::Full => {
97            build_full_snapshot(registration, selector, &root, workspace_detection)
98        }
99        CodeIndexMode::Incremental { base_ref, head_ref } => build_incremental_snapshot(
100            registration,
101            selector,
102            &root,
103            IncrementalSnapshotRequest {
104                base_ref: &base_ref,
105                head_ref: &head_ref,
106                previous_hashes: &previous_hashes,
107                base_resolved_commit_sha: base_resolved_commit_sha.as_deref(),
108                workspace_detection,
109            },
110        ),
111        CodeIndexMode::WorktreeOverlay => build_worktree_overlay_snapshot(
112            registration,
113            selector,
114            &root,
115            &previous_hashes,
116            base_resolved_commit_sha.as_deref(),
117            workspace_detection,
118        ),
119    }
120}
121
122pub(crate) fn compute_worktree_overlay_identity(
123    registration: &CodeRepositoryRegistration,
124    selector: &CodeRepositorySelector,
125    previous_hashes: Vec<CodeFileFingerprint>,
126    base_resolved_commit_sha: Option<String>,
127) -> Result<(String, String), CodeIndexError> {
128    let root = PathBuf::from(&registration.root_path);
129    let previous_hashes = previous_hashes
130        .into_iter()
131        .map(|fingerprint| (fingerprint.path, fingerprint.blob_hash))
132        .collect::<BTreeMap<_, _>>();
133
134    worktree_overlay::worktree_overlay_identity(
135        registration,
136        selector,
137        &root,
138        &previous_hashes,
139        base_resolved_commit_sha.as_deref(),
140    )
141}
142
143pub fn changed_paths_for_diff(
144    root_path: impl AsRef<Path>,
145    base_ref: &str,
146    head_ref: &str,
147) -> Result<Vec<String>, CodeIndexError> {
148    changed_paths_for_diff_with_filters(root_path, base_ref, head_ref, &[], &[])
149}
150
151pub fn changed_paths_for_diff_with_path_filters(
152    root_path: impl AsRef<Path>,
153    base_ref: &str,
154    head_ref: &str,
155    path_filters: &[String],
156) -> Result<Vec<String>, CodeIndexError> {
157    changed_paths_for_diff_with_filters(root_path, base_ref, head_ref, path_filters, &[])
158}
159
160pub fn changed_paths_for_diff_with_filters(
161    root_path: impl AsRef<Path>,
162    base_ref: &str,
163    head_ref: &str,
164    path_filters: &[String],
165    language_filters: &[String],
166) -> Result<Vec<String>, CodeIndexError> {
167    if source_commit_is_filesystem(base_ref) || source_commit_is_filesystem(head_ref) {
168        if base_ref == head_ref {
169            return Ok(Vec::new());
170        }
171        let base_commit = resolve_repository_ref_with_filters(
172            root_path.as_ref(),
173            base_ref,
174            path_filters,
175            language_filters,
176        )?;
177        let head_commit = resolve_repository_ref_with_filters(
178            root_path.as_ref(),
179            head_ref,
180            path_filters,
181            language_filters,
182        )?;
183        if base_commit == head_commit {
184            return Ok(Vec::new());
185        }
186        let snapshot = scoped_source_snapshot_for_filters(
187            root_path.as_ref(),
188            head_ref,
189            path_filters,
190            language_filters,
191        )?;
192        return Ok(snapshot
193            .entries
194            .into_iter()
195            .map(|entry| entry.path)
196            .collect());
197    }
198    if source_kind(root_path.as_ref())?.is_filesystem() {
199        if base_ref == head_ref {
200            return Ok(Vec::new());
201        }
202        let snapshot = scoped_source_snapshot_for_filters(
203            root_path.as_ref(),
204            head_ref,
205            path_filters,
206            language_filters,
207        )?;
208        return Ok(snapshot
209            .entries
210            .into_iter()
211            .map(|entry| entry.path)
212            .collect());
213    }
214    let changes = diff_changes(root_path.as_ref(), base_ref, head_ref)?;
215
216    impact_paths::paths_from_changes_with_gitlinks(
217        root_path.as_ref(),
218        base_ref,
219        head_ref,
220        changes,
221        path_filters,
222        language_filters,
223        MAX_INCREMENTAL_GITLINK_EXPANDED_PATHS,
224    )
225}
226
227#[cfg(test)]
228pub(in crate::code) fn impact_paths_from_changes(changes: Vec<GitChange>) -> Vec<String> {
229    let mut paths = Vec::new();
230    for change in changes {
231        match change {
232            GitChange::AddedOrModified { path }
233            | GitChange::Deleted { path }
234            | GitChange::TypeChanged { path } => paths.push(path),
235            GitChange::Renamed { old_path, new_path } => {
236                paths.push(old_path);
237                paths.push(new_path);
238            }
239            GitChange::Copied { new_path, .. } => paths.push(new_path),
240        }
241    }
242    paths.sort();
243    paths.dedup();
244
245    paths
246}
247
248pub(crate) fn repository_uses_filesystem_source(
249    root_path: impl AsRef<Path>,
250) -> Result<bool, CodeIndexError> {
251    Ok(source_kind(root_path.as_ref())?.is_filesystem())
252}
253
254/// Checks whether a Git delta fits the stricter full-initialization reuse budget.
255pub(crate) fn historical_reuse_diff_fits_budget(
256    root_path: impl AsRef<Path>,
257    base_ref: &str,
258    head_ref: &str,
259    path_filters: &[String],
260    language_filters: &[String],
261) -> Result<bool, CodeIndexError> {
262    let changes = match diff_changes(root_path.as_ref(), base_ref, head_ref) {
263        Ok(changes) => changes,
264        Err(error) if error.is_incremental_changed_path_limit() => return Ok(false),
265        Err(error) => return Err(error),
266    };
267    if impacted_path_count(&changes) > MAX_HISTORICAL_REUSE_CHANGED_PATHS {
268        return Ok(false);
269    }
270    let copied_sources = changes
271        .iter()
272        .filter_map(|change| match change {
273            GitChange::Copied { old_path, .. } => Some(old_path.clone()),
274            _ => None,
275        })
276        .collect::<Vec<_>>();
277    let mut paths = match impact_paths::paths_from_changes_with_gitlinks(
278        root_path.as_ref(),
279        base_ref,
280        head_ref,
281        changes,
282        path_filters,
283        language_filters,
284        MAX_HISTORICAL_REUSE_CHANGED_PATHS,
285    ) {
286        Ok(paths) => paths,
287        Err(error) if error.is_gitlink_expansion_limit() => return Ok(false),
288        Err(error) => return Err(error),
289    };
290    paths.extend(copied_sources);
291    paths.sort();
292    paths.dedup();
293
294    Ok(paths.len() <= MAX_HISTORICAL_REUSE_CHANGED_PATHS)
295}
296
297pub(in crate::code) fn impacted_path_count(changes: &[GitChange]) -> usize {
298    changes
299        .iter()
300        .map(|change| match change {
301            GitChange::Renamed { .. } | GitChange::Copied { .. } => 2,
302            _ => 1,
303        })
304        .sum()
305}
306
307pub(in crate::code::index) fn tracked_entry_scope_for_selector(
308    registration: &CodeRepositoryRegistration,
309    selector: &CodeRepositorySelector,
310) -> TrackedEntryScope {
311    match scope::intersect_path_filters(&registration.path_filters, &selector.path_filters) {
312        Some(filters) => TrackedEntryScope::from_path_filters(filters.iter()),
313        None => TrackedEntryScope::empty(),
314    }
315}