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;
10#[path = "full.rs"]
11mod full_snapshot;
12mod impact_paths;
13mod incremental;
14pub(in crate::code) mod plan;
15pub(in crate::code) mod snapshot;
16mod worktree_overlay;
17
18use crate::domain::{
19    CodeFileFingerprint, CodeIndexMode, CodeIndexResourceBudget, CodeIndexSnapshot,
20    CodeRepositoryRegistration, CodeRepositorySelector, CodeWorkspaceDetectionConfig,
21};
22
23#[cfg(test)]
24use crate::code::source::changes::GitChange;
25use crate::code::{
26    CodeIndexError, identity, ids, parser,
27    parser::parse_indexed_file,
28    source::{
29        self, changes,
30        changes::{TrackedEntryScope, diff_changes},
31        git, gitlink as source_gitlink,
32        layout::{self as scope, scoped_source_snapshot_for_filters},
33        resolution::resolve_repository_ref_with_filters,
34        source_commit_is_filesystem, source_kind,
35    },
36};
37pub use deleted_symbols::deleted_symbol_names_for_diff;
38pub(crate) use filesystem_delta::changed_paths_for_filesystem_diff;
39use full_snapshot::build_full_snapshot;
40#[cfg(test)]
41pub(crate) use full_snapshot::mutate_next_filesystem_full_snapshot_read;
42use incremental::{IncrementalSnapshotRequest, build_incremental_snapshot};
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;
50
51/// Builds a code index snapshot from a clean Git commit or incremental diff.
52pub fn build_index_snapshot(
53    registration: &CodeRepositoryRegistration,
54    selector: &CodeRepositorySelector,
55    mode: CodeIndexMode,
56    previous_hashes: Vec<CodeFileFingerprint>,
57) -> Result<CodeIndexSnapshot, CodeIndexError> {
58    build_index_snapshot_with_base_commit(registration, selector, mode, previous_hashes, None)
59}
60
61pub(crate) fn build_index_snapshot_with_base_commit(
62    registration: &CodeRepositoryRegistration,
63    selector: &CodeRepositorySelector,
64    mode: CodeIndexMode,
65    previous_hashes: Vec<CodeFileFingerprint>,
66    base_resolved_commit_sha: Option<String>,
67) -> Result<CodeIndexSnapshot, CodeIndexError> {
68    build_index_snapshot_with_workspace_detection(
69        registration,
70        selector,
71        mode,
72        previous_hashes,
73        base_resolved_commit_sha,
74        &CodeWorkspaceDetectionConfig::default(),
75    )
76}
77
78pub(crate) fn build_index_snapshot_with_workspace_detection(
79    registration: &CodeRepositoryRegistration,
80    selector: &CodeRepositorySelector,
81    mode: CodeIndexMode,
82    previous_hashes: Vec<CodeFileFingerprint>,
83    base_resolved_commit_sha: Option<String>,
84    workspace_detection: &CodeWorkspaceDetectionConfig,
85) -> Result<CodeIndexSnapshot, CodeIndexError> {
86    let root = PathBuf::from(&registration.root_path);
87    let previous_hashes = previous_hashes
88        .into_iter()
89        .map(|fingerprint| (fingerprint.path, fingerprint.blob_hash))
90        .collect::<BTreeMap<_, _>>();
91
92    match mode {
93        CodeIndexMode::Full => {
94            build_full_snapshot(registration, selector, &root, workspace_detection)
95        }
96        CodeIndexMode::Incremental { base_ref, head_ref } => build_incremental_snapshot(
97            registration,
98            selector,
99            &root,
100            IncrementalSnapshotRequest {
101                base_ref: &base_ref,
102                head_ref: &head_ref,
103                previous_hashes: &previous_hashes,
104                base_resolved_commit_sha: base_resolved_commit_sha.as_deref(),
105                workspace_detection,
106            },
107        ),
108        CodeIndexMode::WorktreeOverlay => build_worktree_overlay_snapshot(
109            registration,
110            selector,
111            &root,
112            &previous_hashes,
113            base_resolved_commit_sha.as_deref(),
114            workspace_detection,
115        ),
116    }
117}
118
119pub fn changed_paths_for_diff(
120    root_path: impl AsRef<Path>,
121    base_ref: &str,
122    head_ref: &str,
123) -> Result<Vec<String>, CodeIndexError> {
124    changed_paths_for_diff_with_filters(root_path, base_ref, head_ref, &[], &[])
125}
126
127pub fn changed_paths_for_diff_with_path_filters(
128    root_path: impl AsRef<Path>,
129    base_ref: &str,
130    head_ref: &str,
131    path_filters: &[String],
132) -> Result<Vec<String>, CodeIndexError> {
133    changed_paths_for_diff_with_filters(root_path, base_ref, head_ref, path_filters, &[])
134}
135
136pub fn changed_paths_for_diff_with_filters(
137    root_path: impl AsRef<Path>,
138    base_ref: &str,
139    head_ref: &str,
140    path_filters: &[String],
141    language_filters: &[String],
142) -> Result<Vec<String>, CodeIndexError> {
143    if source_commit_is_filesystem(base_ref) || source_commit_is_filesystem(head_ref) {
144        if base_ref == head_ref {
145            return Ok(Vec::new());
146        }
147        let base_commit = resolve_repository_ref_with_filters(
148            root_path.as_ref(),
149            base_ref,
150            path_filters,
151            language_filters,
152        )?;
153        let head_commit = resolve_repository_ref_with_filters(
154            root_path.as_ref(),
155            head_ref,
156            path_filters,
157            language_filters,
158        )?;
159        if base_commit == head_commit {
160            return Ok(Vec::new());
161        }
162        let snapshot = scoped_source_snapshot_for_filters(
163            root_path.as_ref(),
164            head_ref,
165            path_filters,
166            language_filters,
167        )?;
168        return Ok(snapshot
169            .entries
170            .into_iter()
171            .map(|entry| entry.path)
172            .collect());
173    }
174    if source_kind(root_path.as_ref())?.is_filesystem() {
175        if base_ref == head_ref {
176            return Ok(Vec::new());
177        }
178        let snapshot = scoped_source_snapshot_for_filters(
179            root_path.as_ref(),
180            head_ref,
181            path_filters,
182            language_filters,
183        )?;
184        return Ok(snapshot
185            .entries
186            .into_iter()
187            .map(|entry| entry.path)
188            .collect());
189    }
190    let changes = diff_changes(root_path.as_ref(), base_ref, head_ref)?;
191
192    impact_paths::paths_from_changes_with_gitlinks(
193        root_path.as_ref(),
194        base_ref,
195        head_ref,
196        changes,
197        path_filters,
198        language_filters,
199    )
200}
201
202#[cfg(test)]
203pub(in crate::code) fn impact_paths_from_changes(changes: Vec<GitChange>) -> Vec<String> {
204    let mut paths = Vec::new();
205    for change in changes {
206        match change {
207            GitChange::AddedOrModified { path }
208            | GitChange::Deleted { path }
209            | GitChange::TypeChanged { path } => paths.push(path),
210            GitChange::Renamed { old_path, new_path } => {
211                paths.push(old_path);
212                paths.push(new_path);
213            }
214            GitChange::Copied { new_path, .. } => paths.push(new_path),
215        }
216    }
217    paths.sort();
218    paths.dedup();
219
220    paths
221}
222
223pub(crate) fn repository_uses_filesystem_source(
224    root_path: impl AsRef<Path>,
225) -> Result<bool, CodeIndexError> {
226    Ok(source_kind(root_path.as_ref())?.is_filesystem())
227}
228
229pub(in crate::code::index) fn tracked_entry_scope_for_selector(
230    registration: &CodeRepositoryRegistration,
231    selector: &CodeRepositorySelector,
232) -> TrackedEntryScope {
233    match scope::intersect_path_filters(&registration.path_filters, &selector.path_filters) {
234        Some(filters) => TrackedEntryScope::from_path_filters(filters.iter()),
235        None => TrackedEntryScope::empty(),
236    }
237}