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
22#[cfg(test)]
23use crate::code::source::changes::GitChange;
24use crate::code::{
25    CodeIndexError, identity, ids, parser,
26    parser::parse_indexed_file,
27    source::{
28        self, changes,
29        changes::{TrackedEntryScope, diff_changes},
30        git, gitlink as source_gitlink,
31        layout::{self as scope, scoped_source_snapshot_for_filters},
32        resolution::resolve_repository_ref_with_filters,
33        source_commit_is_filesystem, source_kind,
34    },
35};
36pub use deleted_symbols::deleted_symbol_names_for_diff;
37pub(crate) use filesystem_delta::changed_paths_for_filesystem_diff;
38use full_snapshot::build_full_snapshot;
39pub(crate) use full_snapshot::clean_worktree_overlay_hash;
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(crate) fn compute_worktree_overlay_identity(
120    registration: &CodeRepositoryRegistration,
121    selector: &CodeRepositorySelector,
122    previous_hashes: Vec<CodeFileFingerprint>,
123    base_resolved_commit_sha: Option<String>,
124) -> Result<(String, String), CodeIndexError> {
125    let root = PathBuf::from(&registration.root_path);
126    let previous_hashes = previous_hashes
127        .into_iter()
128        .map(|fingerprint| (fingerprint.path, fingerprint.blob_hash))
129        .collect::<BTreeMap<_, _>>();
130
131    worktree_overlay::worktree_overlay_identity(
132        registration,
133        selector,
134        &root,
135        &previous_hashes,
136        base_resolved_commit_sha.as_deref(),
137    )
138}
139
140pub fn changed_paths_for_diff(
141    root_path: impl AsRef<Path>,
142    base_ref: &str,
143    head_ref: &str,
144) -> Result<Vec<String>, CodeIndexError> {
145    changed_paths_for_diff_with_filters(root_path, base_ref, head_ref, &[], &[])
146}
147
148pub fn changed_paths_for_diff_with_path_filters(
149    root_path: impl AsRef<Path>,
150    base_ref: &str,
151    head_ref: &str,
152    path_filters: &[String],
153) -> Result<Vec<String>, CodeIndexError> {
154    changed_paths_for_diff_with_filters(root_path, base_ref, head_ref, path_filters, &[])
155}
156
157pub fn changed_paths_for_diff_with_filters(
158    root_path: impl AsRef<Path>,
159    base_ref: &str,
160    head_ref: &str,
161    path_filters: &[String],
162    language_filters: &[String],
163) -> Result<Vec<String>, CodeIndexError> {
164    if source_commit_is_filesystem(base_ref) || source_commit_is_filesystem(head_ref) {
165        if base_ref == head_ref {
166            return Ok(Vec::new());
167        }
168        let base_commit = resolve_repository_ref_with_filters(
169            root_path.as_ref(),
170            base_ref,
171            path_filters,
172            language_filters,
173        )?;
174        let head_commit = resolve_repository_ref_with_filters(
175            root_path.as_ref(),
176            head_ref,
177            path_filters,
178            language_filters,
179        )?;
180        if base_commit == head_commit {
181            return Ok(Vec::new());
182        }
183        let snapshot = scoped_source_snapshot_for_filters(
184            root_path.as_ref(),
185            head_ref,
186            path_filters,
187            language_filters,
188        )?;
189        return Ok(snapshot
190            .entries
191            .into_iter()
192            .map(|entry| entry.path)
193            .collect());
194    }
195    if source_kind(root_path.as_ref())?.is_filesystem() {
196        if base_ref == head_ref {
197            return Ok(Vec::new());
198        }
199        let snapshot = scoped_source_snapshot_for_filters(
200            root_path.as_ref(),
201            head_ref,
202            path_filters,
203            language_filters,
204        )?;
205        return Ok(snapshot
206            .entries
207            .into_iter()
208            .map(|entry| entry.path)
209            .collect());
210    }
211    let changes = diff_changes(root_path.as_ref(), base_ref, head_ref)?;
212
213    impact_paths::paths_from_changes_with_gitlinks(
214        root_path.as_ref(),
215        base_ref,
216        head_ref,
217        changes,
218        path_filters,
219        language_filters,
220    )
221}
222
223#[cfg(test)]
224pub(in crate::code) fn impact_paths_from_changes(changes: Vec<GitChange>) -> Vec<String> {
225    let mut paths = Vec::new();
226    for change in changes {
227        match change {
228            GitChange::AddedOrModified { path }
229            | GitChange::Deleted { path }
230            | GitChange::TypeChanged { path } => paths.push(path),
231            GitChange::Renamed { old_path, new_path } => {
232                paths.push(old_path);
233                paths.push(new_path);
234            }
235            GitChange::Copied { new_path, .. } => paths.push(new_path),
236        }
237    }
238    paths.sort();
239    paths.dedup();
240
241    paths
242}
243
244pub(crate) fn repository_uses_filesystem_source(
245    root_path: impl AsRef<Path>,
246) -> Result<bool, CodeIndexError> {
247    Ok(source_kind(root_path.as_ref())?.is_filesystem())
248}
249
250pub(in crate::code::index) fn tracked_entry_scope_for_selector(
251    registration: &CodeRepositoryRegistration,
252    selector: &CodeRepositorySelector,
253) -> TrackedEntryScope {
254    match scope::intersect_path_filters(&registration.path_filters, &selector.path_filters) {
255        Some(filters) => TrackedEntryScope::from_path_filters(filters.iter()),
256        None => TrackedEntryScope::empty(),
257    }
258}