1use 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
54pub 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(®istration.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(®istration.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
254pub(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(®istration.path_filters, &selector.path_filters) {
312 Some(filters) => TrackedEntryScope::from_path_filters(filters.iter()),
313 None => TrackedEntryScope::empty(),
314 }
315}