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;
40pub(crate) use full_snapshot::clean_worktree_overlay_hash;
41#[cfg(test)]
42pub(crate) use full_snapshot::mutate_next_filesystem_full_snapshot_read;
43use incremental::{IncrementalSnapshotRequest, build_incremental_snapshot};
44pub use plan::{
45    CodeIndexPlan, prepare_full_index_plan, prepare_full_index_plan_with_workspace_detection,
46};
47use worktree_overlay::build_worktree_overlay_snapshot;
48
49pub(in crate::code) const MAX_INCREMENTAL_GITLINK_EXPANDED_PATHS: usize =
50    CodeIndexResourceBudget::DEFAULT_MAX_FILES_PER_BATCH;
51
52/// Builds a code index snapshot from a clean Git commit or incremental diff.
53pub fn build_index_snapshot(
54    registration: &CodeRepositoryRegistration,
55    selector: &CodeRepositorySelector,
56    mode: CodeIndexMode,
57    previous_hashes: Vec<CodeFileFingerprint>,
58) -> Result<CodeIndexSnapshot, CodeIndexError> {
59    build_index_snapshot_with_base_commit(registration, selector, mode, previous_hashes, None)
60}
61
62pub(crate) fn build_index_snapshot_with_base_commit(
63    registration: &CodeRepositoryRegistration,
64    selector: &CodeRepositorySelector,
65    mode: CodeIndexMode,
66    previous_hashes: Vec<CodeFileFingerprint>,
67    base_resolved_commit_sha: Option<String>,
68) -> Result<CodeIndexSnapshot, CodeIndexError> {
69    build_index_snapshot_with_workspace_detection(
70        registration,
71        selector,
72        mode,
73        previous_hashes,
74        base_resolved_commit_sha,
75        &CodeWorkspaceDetectionConfig::default(),
76    )
77}
78
79pub(crate) fn build_index_snapshot_with_workspace_detection(
80    registration: &CodeRepositoryRegistration,
81    selector: &CodeRepositorySelector,
82    mode: CodeIndexMode,
83    previous_hashes: Vec<CodeFileFingerprint>,
84    base_resolved_commit_sha: Option<String>,
85    workspace_detection: &CodeWorkspaceDetectionConfig,
86) -> Result<CodeIndexSnapshot, CodeIndexError> {
87    let root = PathBuf::from(&registration.root_path);
88    let previous_hashes = previous_hashes
89        .into_iter()
90        .map(|fingerprint| (fingerprint.path, fingerprint.blob_hash))
91        .collect::<BTreeMap<_, _>>();
92
93    match mode {
94        CodeIndexMode::Full => {
95            build_full_snapshot(registration, selector, &root, workspace_detection)
96        }
97        CodeIndexMode::Incremental { base_ref, head_ref } => build_incremental_snapshot(
98            registration,
99            selector,
100            &root,
101            IncrementalSnapshotRequest {
102                base_ref: &base_ref,
103                head_ref: &head_ref,
104                previous_hashes: &previous_hashes,
105                base_resolved_commit_sha: base_resolved_commit_sha.as_deref(),
106                workspace_detection,
107            },
108        ),
109        CodeIndexMode::WorktreeOverlay => build_worktree_overlay_snapshot(
110            registration,
111            selector,
112            &root,
113            &previous_hashes,
114            base_resolved_commit_sha.as_deref(),
115            workspace_detection,
116        ),
117    }
118}
119
120pub(crate) fn compute_worktree_overlay_identity(
121    registration: &CodeRepositoryRegistration,
122    selector: &CodeRepositorySelector,
123    previous_hashes: Vec<CodeFileFingerprint>,
124    base_resolved_commit_sha: Option<String>,
125) -> Result<(String, String), CodeIndexError> {
126    let root = PathBuf::from(&registration.root_path);
127    let previous_hashes = previous_hashes
128        .into_iter()
129        .map(|fingerprint| (fingerprint.path, fingerprint.blob_hash))
130        .collect::<BTreeMap<_, _>>();
131
132    worktree_overlay::worktree_overlay_identity(
133        registration,
134        selector,
135        &root,
136        &previous_hashes,
137        base_resolved_commit_sha.as_deref(),
138    )
139}
140
141pub fn changed_paths_for_diff(
142    root_path: impl AsRef<Path>,
143    base_ref: &str,
144    head_ref: &str,
145) -> Result<Vec<String>, CodeIndexError> {
146    changed_paths_for_diff_with_filters(root_path, base_ref, head_ref, &[], &[])
147}
148
149pub fn changed_paths_for_diff_with_path_filters(
150    root_path: impl AsRef<Path>,
151    base_ref: &str,
152    head_ref: &str,
153    path_filters: &[String],
154) -> Result<Vec<String>, CodeIndexError> {
155    changed_paths_for_diff_with_filters(root_path, base_ref, head_ref, path_filters, &[])
156}
157
158pub fn changed_paths_for_diff_with_filters(
159    root_path: impl AsRef<Path>,
160    base_ref: &str,
161    head_ref: &str,
162    path_filters: &[String],
163    language_filters: &[String],
164) -> Result<Vec<String>, CodeIndexError> {
165    if source_commit_is_filesystem(base_ref) || source_commit_is_filesystem(head_ref) {
166        if base_ref == head_ref {
167            return Ok(Vec::new());
168        }
169        let base_commit = resolve_repository_ref_with_filters(
170            root_path.as_ref(),
171            base_ref,
172            path_filters,
173            language_filters,
174        )?;
175        let head_commit = resolve_repository_ref_with_filters(
176            root_path.as_ref(),
177            head_ref,
178            path_filters,
179            language_filters,
180        )?;
181        if base_commit == head_commit {
182            return Ok(Vec::new());
183        }
184        let snapshot = scoped_source_snapshot_for_filters(
185            root_path.as_ref(),
186            head_ref,
187            path_filters,
188            language_filters,
189        )?;
190        return Ok(snapshot
191            .entries
192            .into_iter()
193            .map(|entry| entry.path)
194            .collect());
195    }
196    if source_kind(root_path.as_ref())?.is_filesystem() {
197        if base_ref == head_ref {
198            return Ok(Vec::new());
199        }
200        let snapshot = scoped_source_snapshot_for_filters(
201            root_path.as_ref(),
202            head_ref,
203            path_filters,
204            language_filters,
205        )?;
206        return Ok(snapshot
207            .entries
208            .into_iter()
209            .map(|entry| entry.path)
210            .collect());
211    }
212    let changes = diff_changes(root_path.as_ref(), base_ref, head_ref)?;
213
214    impact_paths::paths_from_changes_with_gitlinks(
215        root_path.as_ref(),
216        base_ref,
217        head_ref,
218        changes,
219        path_filters,
220        language_filters,
221    )
222}
223
224#[cfg(test)]
225pub(in crate::code) fn impact_paths_from_changes(changes: Vec<GitChange>) -> Vec<String> {
226    let mut paths = Vec::new();
227    for change in changes {
228        match change {
229            GitChange::AddedOrModified { path }
230            | GitChange::Deleted { path }
231            | GitChange::TypeChanged { path } => paths.push(path),
232            GitChange::Renamed { old_path, new_path } => {
233                paths.push(old_path);
234                paths.push(new_path);
235            }
236            GitChange::Copied { new_path, .. } => paths.push(new_path),
237        }
238    }
239    paths.sort();
240    paths.dedup();
241
242    paths
243}
244
245pub(crate) fn repository_uses_filesystem_source(
246    root_path: impl AsRef<Path>,
247) -> Result<bool, CodeIndexError> {
248    Ok(source_kind(root_path.as_ref())?.is_filesystem())
249}
250
251pub(in crate::code::index) fn tracked_entry_scope_for_selector(
252    registration: &CodeRepositoryRegistration,
253    selector: &CodeRepositorySelector,
254) -> TrackedEntryScope {
255    match scope::intersect_path_filters(&registration.path_filters, &selector.path_filters) {
256        Some(filters) => TrackedEntryScope::from_path_filters(filters.iter()),
257        None => TrackedEntryScope::empty(),
258    }
259}