first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -46,5 +46,5 @@ impl ChangedFiles {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "changed_files_test.rs"]
|
||||
#[path = "changed_files_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Helper function to create a PathBuf from a string
|
||||
fn pb(path: &str) -> PathBuf {
|
||||
PathBuf::from(path)
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::path::Path;
|
||||
|
||||
use string_offset::ByteOffset;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use warp_util::standardized_path::StandardizedPath;
|
||||
|
||||
mod naive;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
@@ -99,7 +101,8 @@ pub fn chunk_code<'a>(code: &'a str, path: &'a Path) -> Vec<Fragment<'a>> {
|
||||
/// could not be chunked for any reason.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn try_chunk_code_semantically<'a>(code: &'a str, path: &'a Path) -> Option<Vec<Fragment<'a>>> {
|
||||
let language = languages::language_by_filename(path)?;
|
||||
let standardized_path = StandardizedPath::try_from_local(path).ok()?;
|
||||
let language = languages::language_by_filename(&standardized_path)?;
|
||||
semantic::chunk_code(code, path, MAX_BYTES_PER_CHUNK, &language.grammar).ok()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::index::full_source_code_embedding::chunker::{coalesce_fragments, Fragment};
|
||||
use std::path::Path;
|
||||
|
||||
use itertools::Itertools;
|
||||
use line_span::{LineSpan, LineSpans};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::index::full_source_code_embedding::chunker::{coalesce_fragments, Fragment};
|
||||
|
||||
/// Chunks the given file into [`Fragment`]s. Each chunk is at most `num_lines_per_chunk` lines long, and contains at most `max_bytes_per_chunk` bytes.
|
||||
pub(super) fn chunk_code<'a>(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_chunker() {
|
||||
let code = "This is some text content\nthat should be chunked\nusing the naive chunker\nbecause the language isn't recognized.";
|
||||
|
||||
@@ -48,7 +48,11 @@ pub(super) fn chunk_code<'a>(
|
||||
// of the allocator).
|
||||
//
|
||||
// See: https://github.com/tree-sitter/tree-sitter/issues/3129
|
||||
#[cfg(all(target_os = "linux", target_env = "gnu", not(feature = "jemalloc")))]
|
||||
#[cfg(all(
|
||||
any(target_os = "linux", target_os = "freebsd"),
|
||||
target_env = "gnu",
|
||||
not(feature = "jemalloc")
|
||||
))]
|
||||
unsafe {
|
||||
nix::libc::malloc_trim(0);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use languages::language_by_filename;
|
||||
use warp_util::standardized_path::StandardizedPath;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -33,12 +34,14 @@ fn main() {
|
||||
"#;
|
||||
|
||||
let max_chunk_size = 128;
|
||||
let language_path =
|
||||
StandardizedPath::try_new("/test.rs").expect("test path should be absolute");
|
||||
|
||||
let chunks = chunk_code(
|
||||
source_code,
|
||||
Path::new("test.rs"),
|
||||
max_chunk_size,
|
||||
&language_by_filename(Path::new("test.rs"))
|
||||
&language_by_filename(&language_path)
|
||||
.expect("Rust language must exist")
|
||||
.grammar,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_channel;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -5,32 +11,31 @@ use futures::stream::AbortHandle;
|
||||
use galaxy_core::safe_error;
|
||||
use galaxyui::{Entity, ModelContext, ModelHandle};
|
||||
use ignore::gitignore::Gitignore;
|
||||
use instant::Instant;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use repo_metadata::entry::IgnoredPathStrategy;
|
||||
use repo_metadata::entry::{BudgetExceededBehavior, IgnoredPathStrategy};
|
||||
use repo_metadata::Repository;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use galaxyui_core::{Entity, ModelContext, ModelHandle};
|
||||
|
||||
use super::fragment_metadata::{
|
||||
FragmentMetadata, LeafToFragmentMetadata, LeafToFragmentMetadataUpdates,
|
||||
};
|
||||
use super::manager::{
|
||||
CodebaseIndexFinishedStatus, CodebaseIndexStatus, FragmentMetadataLookupError,
|
||||
RetrieveFileError,
|
||||
};
|
||||
use super::merkle_tree::{MerkleTree, SerializedCodebaseIndex};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use super::search_shaping::build_fragments_from_file_contents;
|
||||
use super::search_shaping::{fragments_to_context_locations, ReadFragmentResult};
|
||||
use super::store_client::StoreClient;
|
||||
use super::sync_client::{FlushFragmentResult, SyncOperationError};
|
||||
use super::{
|
||||
fragment_metadata::{FragmentMetadata, LeafToFragmentMetadata, LeafToFragmentMetadataUpdates},
|
||||
manager::{CodebaseIndexFinishedStatus, CodebaseIndexStatus, RetrieveFileError},
|
||||
merkle_tree::{MerkleTree, SerializedCodebaseIndex},
|
||||
store_client::StoreClient,
|
||||
sync_client::{FlushFragmentResult, SyncOperationError},
|
||||
CodebaseContextConfig, ContentHash, EmbeddingConfig, Error, Fragment, NodeHash, RepoMetadata,
|
||||
};
|
||||
use crate::{
|
||||
index::locations::{CodeContextLocation, FileFragmentLocation},
|
||||
telemetry::{AITelemetryEvent, CodebaseContextSyncType},
|
||||
workspace::{WorkspaceMetadata, WorkspaceMetadataEvent},
|
||||
};
|
||||
use instant::Instant;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use crate::index::locations::CodeContextLocation;
|
||||
use crate::telemetry::{AITelemetryEvent, CodebaseContextSyncType};
|
||||
use crate::workspace::{WorkspaceMetadata, WorkspaceMetadataEvent};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "local_fs")] {
|
||||
@@ -44,12 +49,11 @@ cfg_if::cfg_if! {
|
||||
Entry,
|
||||
matches_gitignores,
|
||||
full_source_code_embedding::sync_client::CodebaseIndexSyncOperation,
|
||||
full_source_code_embedding::FragmentLocation
|
||||
};
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::interval_timer::IntervalTimer;
|
||||
use galaxyui::r#async::Timer;
|
||||
use galaxyui::SingletonEntity;
|
||||
use galaxyui_core::r#async::Timer;
|
||||
use galaxyui_core::SingletonEntity;
|
||||
use galaxy_core::sync_queue::SyncQueue;
|
||||
use sha2::Digest;
|
||||
}
|
||||
@@ -316,7 +320,9 @@ pub enum CodebaseIndexEvent {
|
||||
retrieval_id: RetrievalID,
|
||||
error: Error,
|
||||
},
|
||||
SyncStateUpdated,
|
||||
SyncStateUpdated {
|
||||
root_path: PathBuf,
|
||||
},
|
||||
IndexMetadataUpdated {
|
||||
root_path: PathBuf,
|
||||
event: WorkspaceMetadataEvent,
|
||||
@@ -498,6 +504,13 @@ impl CodebaseIndex {
|
||||
store_client: Arc<dyn StoreClient>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if self
|
||||
.pending_file_changes
|
||||
.as_ref()
|
||||
.is_none_or(|changed_files| changed_files.is_empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let last_server_synced_root_node = self.last_server_synced_root_node();
|
||||
let old_state = self.update_tree_sync_state(
|
||||
TreeSourceSyncState::Syncing {
|
||||
@@ -876,7 +889,9 @@ impl CodebaseIndex {
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> TreeSourceSyncState {
|
||||
let old_state = std::mem::replace(&mut self.tree_sync_state, new_state);
|
||||
ctx.emit(CodebaseIndexEvent::SyncStateUpdated);
|
||||
ctx.emit(CodebaseIndexEvent::SyncStateUpdated {
|
||||
root_path: self.repo_path.clone(),
|
||||
});
|
||||
old_state
|
||||
}
|
||||
|
||||
@@ -885,7 +900,9 @@ impl CodebaseIndex {
|
||||
if let TreeSourceSyncState::Syncing { sync_progress, .. } = &mut self.tree_sync_state {
|
||||
*sync_progress = Some(progress);
|
||||
|
||||
ctx.emit(CodebaseIndexEvent::SyncStateUpdated);
|
||||
ctx.emit(CodebaseIndexEvent::SyncStateUpdated {
|
||||
root_path: self.repo_path.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,6 +937,9 @@ impl CodebaseIndex {
|
||||
// First traverse the repo path to retrieve all files we want to parse.
|
||||
let mut files = Vec::new();
|
||||
let mut remaining_file_quotas = max_num_files_limit;
|
||||
// Codebase embedding must not operate on a partial tree: the file limit
|
||||
// is an intentional cost cap, so exceeding it fails the build rather
|
||||
// than silently indexing a breadth-first subset of the repository.
|
||||
let entry = Entry::build_tree(
|
||||
&repo_path,
|
||||
&mut files,
|
||||
@@ -928,6 +948,7 @@ impl CodebaseIndex {
|
||||
MAX_DEPTH,
|
||||
0,
|
||||
&IgnoredPathStrategy::Exclude, // override_ignore_for_files
|
||||
BudgetExceededBehavior::FailFast,
|
||||
)?;
|
||||
|
||||
Ok(BuildFileTreeResult {
|
||||
@@ -1318,6 +1339,30 @@ impl CodebaseIndex {
|
||||
self.leaf_node_to_fragment_metadatas.get(leaf_hash.as_ref())
|
||||
}
|
||||
|
||||
pub(super) fn fragment_metadatas_from_hashes(
|
||||
&self,
|
||||
root_hash: &NodeHash,
|
||||
content_hashes: &[ContentHash],
|
||||
) -> Result<HashMap<ContentHash, Vec<FragmentMetadata>>, FragmentMetadataLookupError> {
|
||||
let current_root_hash = self
|
||||
.last_server_synced_root_node()
|
||||
.ok_or(FragmentMetadataLookupError::IndexNotSynced)?;
|
||||
if ¤t_root_hash != root_hash {
|
||||
return Err(FragmentMetadataLookupError::RootHashMismatch {
|
||||
requested: root_hash.clone(),
|
||||
current: current_root_hash,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(content_hashes
|
||||
.iter()
|
||||
.filter_map(|hash| {
|
||||
self.fragment_metadatas_from_hash(hash)
|
||||
.map(|metadata| (hash.clone(), metadata.clone()))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn repo_metadata(&self) -> RepoMetadata {
|
||||
RepoMetadata {
|
||||
path: Some(self.repo_path.to_string_lossy().to_string()),
|
||||
@@ -1342,12 +1387,20 @@ impl CodebaseIndex {
|
||||
}
|
||||
|
||||
pub(super) fn codebase_index_status(&self) -> CodebaseIndexStatus {
|
||||
let has_synced_version = self.last_server_synced_root_node().is_some();
|
||||
let root_hash = self.last_server_synced_root_node();
|
||||
let has_synced_version = root_hash.is_some();
|
||||
#[cfg(feature = "local_fs")]
|
||||
let has_pending_file_changes = self
|
||||
.pending_file_changes
|
||||
.as_ref()
|
||||
.is_some_and(|changes| !changes.is_empty());
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
let has_pending_file_changes = false;
|
||||
match &self.tree_sync_state {
|
||||
TreeSourceSyncState::Synced {
|
||||
server_sync_result, ..
|
||||
} => CodebaseIndexStatus {
|
||||
has_pending: false,
|
||||
has_pending: has_pending_file_changes,
|
||||
has_synced_version,
|
||||
last_sync_successful: Some(match server_sync_result {
|
||||
ServerSyncResult::Success => CodebaseIndexFinishedStatus::Completed,
|
||||
@@ -1356,18 +1409,21 @@ impl CodebaseIndex {
|
||||
}
|
||||
}),
|
||||
sync_progress: None,
|
||||
root_hash: root_hash.clone(),
|
||||
},
|
||||
TreeSourceSyncState::InitializeTreeFailure(e) => CodebaseIndexStatus {
|
||||
has_pending: false,
|
||||
has_synced_version,
|
||||
last_sync_successful: Some(CodebaseIndexFinishedStatus::Failed(e.into())),
|
||||
sync_progress: None,
|
||||
root_hash: root_hash.clone(),
|
||||
},
|
||||
TreeSourceSyncState::Syncing { sync_progress, .. } => CodebaseIndexStatus {
|
||||
has_pending: true,
|
||||
has_synced_version,
|
||||
last_sync_successful: None,
|
||||
sync_progress: *sync_progress,
|
||||
root_hash: root_hash.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1622,82 +1678,21 @@ impl CodebaseIndex {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert fragments into CodeContextLocations. This function groups and dedupes fragments in the same file.
|
||||
// It also allows the caller to define a context line number surrounding the relevant fragment.
|
||||
fn process_fragments(
|
||||
&self,
|
||||
fragments: Vec<Fragment>,
|
||||
context_lines: usize,
|
||||
) -> HashSet<CodeContextLocation> {
|
||||
// Map to collect fragments by file path
|
||||
let mut fragments_by_path: HashMap<&PathBuf, Vec<Range<usize>>> = HashMap::new();
|
||||
let mut whole_files = HashSet::new();
|
||||
|
||||
// First pass - collect all fragments and their line ranges by file path
|
||||
for fragment in &fragments {
|
||||
if let Some(metadata) = self
|
||||
.fragment_metadatas_from_hash(&fragment.content_hash)
|
||||
.and_then(|metadatas| {
|
||||
metadatas.iter().find(|m| {
|
||||
m.absolute_path == fragment.location.absolute_path
|
||||
&& m.location.byte_range == fragment.location.byte_range
|
||||
})
|
||||
})
|
||||
{
|
||||
// Add line range with context to the appropriate file's collection
|
||||
let path = &fragment.location.absolute_path;
|
||||
let start = metadata.location.start_line.saturating_sub(context_lines);
|
||||
let end = metadata.location.end_line + 1 + context_lines; // Make the range inclusive on both ends
|
||||
|
||||
fragments_by_path.entry(path).or_default().push(start..end);
|
||||
} else {
|
||||
// Fallback to whole file if metadata not found
|
||||
whole_files.insert(fragment.location.absolute_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass - process each file's fragments
|
||||
let mut result = HashSet::new();
|
||||
|
||||
// Process each file's fragments
|
||||
for (path, mut line_ranges) in fragments_by_path {
|
||||
if line_ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We can skip the fragments if the entire file is already included in the context.
|
||||
if whole_files.contains(path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sort ranges by start position
|
||||
line_ranges.sort_by_key(|range| range.start);
|
||||
|
||||
// Merge overlapping or adjacent ranges
|
||||
let mut merged_ranges: Vec<Range<usize>> = Vec::new();
|
||||
for range in line_ranges {
|
||||
if let Some(last) = merged_ranges.last_mut() {
|
||||
// If current range overlaps or is adjacent to the last one, merge them
|
||||
if range.start <= last.end {
|
||||
last.end = last.end.max(range.end);
|
||||
} else {
|
||||
merged_ranges.push(range);
|
||||
}
|
||||
} else {
|
||||
merged_ranges.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
// Add file fragment location with all merged ranges
|
||||
result.insert(CodeContextLocation::Fragment(FileFragmentLocation {
|
||||
path: path.clone(),
|
||||
line_ranges: merged_ranges,
|
||||
}));
|
||||
}
|
||||
|
||||
// Add whole files to the result set
|
||||
result.extend(whole_files.into_iter().map(CodeContextLocation::WholeFile));
|
||||
result
|
||||
// Keep local and remote search aligned by using the same fragment-to-context expansion
|
||||
// helper for range merging, deduping, and context-line handling.
|
||||
fragments_to_context_locations(
|
||||
fragments,
|
||||
|content_hash| {
|
||||
self.fragment_metadatas_from_hash(content_hash)
|
||||
.map(Vec::as_slice)
|
||||
},
|
||||
context_lines,
|
||||
)
|
||||
}
|
||||
|
||||
/// A new index built from a snapshot. This constructor builds the index and starts
|
||||
@@ -2100,13 +2095,14 @@ impl CodebaseIndex {
|
||||
match entry.and_then(|entry| dunce::canonicalize(entry.path())) {
|
||||
Ok(child_path) => {
|
||||
// Ignore paths that are excluded by .gitignore, end with .git, or are symlinks.
|
||||
if matches_gitignores(
|
||||
&child_path,
|
||||
is_dir,
|
||||
&*gitignores,
|
||||
false, /* check_ancestors */
|
||||
) || child_path.ends_with(".git")
|
||||
if child_path.ends_with(".git")
|
||||
|| child_path.is_symlink()
|
||||
|| matches_gitignores(
|
||||
&child_path,
|
||||
child_path.is_dir(),
|
||||
&*gitignores,
|
||||
false, /* check_ancestors */
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -2244,13 +2240,14 @@ impl CodebaseIndex {
|
||||
match entry.and_then(|entry| dunce::canonicalize(entry.path())) {
|
||||
Ok(child_path) => {
|
||||
// Ignore paths that are excluded by .gitignore, end with .git, or are symlinks.
|
||||
if matches_gitignores(
|
||||
&child_path,
|
||||
is_dir,
|
||||
&*gitignores,
|
||||
false, /* check_ancestors */
|
||||
) || child_path.ends_with(".git")
|
||||
if child_path.ends_with(".git")
|
||||
|| child_path.is_symlink()
|
||||
|| matches_gitignores(
|
||||
&child_path,
|
||||
child_path.is_dir(),
|
||||
&*gitignores,
|
||||
false, /* check_ancestors */
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -2320,98 +2317,22 @@ impl CodebaseIndex {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ReadFragmentResult {
|
||||
pub successfully_read: Vec<Fragment>,
|
||||
pub fail_to_read: Vec<ContentHash>,
|
||||
pub fail_to_read_path: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub(super) async fn build_fragments_from_metadata(
|
||||
metadatas: impl IntoIterator<Item = (ContentHash, FragmentMetadata)>,
|
||||
) -> ReadFragmentResult {
|
||||
let mut fragments = Vec::new();
|
||||
let mut fail_to_read = Vec::new();
|
||||
let mut fail_to_read_path = Vec::new();
|
||||
|
||||
// Group fragments by file path
|
||||
let mut fragments_by_path: HashMap<_, Vec<_>> = HashMap::new();
|
||||
for (content_hash, metadata) in metadatas {
|
||||
fragments_by_path
|
||||
.entry(metadata.absolute_path)
|
||||
.or_default()
|
||||
.push((content_hash, metadata.location.byte_range));
|
||||
}
|
||||
|
||||
// Process each file and its fragments
|
||||
for (file_path, file_fragments) in fragments_by_path {
|
||||
let mut has_failed_to_read_fragments = false;
|
||||
// Read the file content once
|
||||
if let Ok(file_content) = async_fs::read_to_string(&file_path).await {
|
||||
// Process all fragments for this file
|
||||
for (content_hash, fragment_ranges) in file_fragments {
|
||||
let start_idx = fragment_ranges.start.as_usize();
|
||||
let end_idx = fragment_ranges.end.as_usize();
|
||||
|
||||
if start_idx <= end_idx
|
||||
&& end_idx <= file_content.len()
|
||||
&& file_content.is_char_boundary(start_idx)
|
||||
&& file_content.is_char_boundary(end_idx)
|
||||
{
|
||||
let content = file_content[start_idx..end_idx].to_string();
|
||||
if content.is_empty() {
|
||||
log::trace!(
|
||||
"Fragment for {:?} with range {:?} is empty",
|
||||
file_path.display(),
|
||||
fragment_ranges
|
||||
);
|
||||
fail_to_read.push(content_hash);
|
||||
has_failed_to_read_fragments = true;
|
||||
} else if ContentHash::from_content(&content) != content_hash {
|
||||
log::trace!(
|
||||
"Fragment for {:?} with range {:?} does not match its content hash",
|
||||
file_path.display(),
|
||||
fragment_ranges
|
||||
);
|
||||
fail_to_read.push(content_hash);
|
||||
has_failed_to_read_fragments = true;
|
||||
} else {
|
||||
fragments.push(Fragment {
|
||||
content,
|
||||
content_hash,
|
||||
location: FragmentLocation {
|
||||
absolute_path: file_path.clone(),
|
||||
byte_range: fragment_ranges,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log::trace!("Invalid byte range {fragment_ranges:?} for file: {file_path:?}");
|
||||
fail_to_read.push(content_hash);
|
||||
has_failed_to_read_fragments = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::trace!("Failed to read file: {file_path:?}");
|
||||
fail_to_read.extend(
|
||||
file_fragments
|
||||
.into_iter()
|
||||
.map(|(content_hash, _)| content_hash),
|
||||
);
|
||||
has_failed_to_read_fragments = true;
|
||||
}
|
||||
|
||||
if has_failed_to_read_fragments {
|
||||
fail_to_read_path.push(file_path);
|
||||
let metadatas = metadatas.into_iter().collect::<Vec<_>>();
|
||||
let mut file_contents = HashMap::new();
|
||||
for path in metadatas
|
||||
.iter()
|
||||
.map(|(_, metadata)| metadata.absolute_path.clone())
|
||||
.collect::<HashSet<_>>()
|
||||
{
|
||||
if let Ok(file_content) = async_fs::read_to_string(&path).await {
|
||||
file_contents.insert(path, file_content);
|
||||
}
|
||||
}
|
||||
|
||||
ReadFragmentResult {
|
||||
successfully_read: fragments,
|
||||
fail_to_read,
|
||||
fail_to_read_path,
|
||||
}
|
||||
build_fragments_from_file_contents(metadatas, &file_contents)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
#![allow(clippy::single_range_in_vec_init)]
|
||||
use chrono::Utc;
|
||||
use string_offset::ByteOffset;
|
||||
use virtual_fs::{Stub, VirtualFS};
|
||||
|
||||
use crate::index::full_source_code_embedding::changed_files::ChangedFiles;
|
||||
use crate::index::full_source_code_embedding::codebase_index::MAX_DEPTH;
|
||||
use crate::index::full_source_code_embedding::fragment_metadata::{
|
||||
FragmentLocation, LeafToFragmentMetadata,
|
||||
};
|
||||
|
||||
use crate::index::full_source_code_embedding::merkle_tree::MerkleHash;
|
||||
use crate::index::full_source_code_embedding::merkle_tree::MerkleTree;
|
||||
use crate::index::full_source_code_embedding::store_client::MockStoreClient;
|
||||
use crate::index::full_source_code_embedding::{
|
||||
ContentHash, EmbeddingConfig, Fragment, FragmentMetadata,
|
||||
};
|
||||
use crate::index::locations::{CodeContextLocation, FileFragmentLocation};
|
||||
use futures::executor::block_on;
|
||||
use galaxy_util::standardized_path::StandardizedPath;
|
||||
use galaxyui::{App, SingletonEntity};
|
||||
use repo_metadata::DirectoryWatcher;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Utc;
|
||||
use futures::executor::block_on;
|
||||
use repo_metadata::DirectoryWatcher;
|
||||
use string_offset::ByteOffset;
|
||||
use virtual_fs::{Stub, VirtualFS};
|
||||
use warp_util::standardized_path::StandardizedPath;
|
||||
use galaxyui_core::{App, SingletonEntity};
|
||||
|
||||
use super::{
|
||||
CodebaseIndex, CodebaseIndexTimeStampMetadata, TreeSourceSyncState,
|
||||
CodebaseIndex, CodebaseIndexTimeStampMetadata, ServerSyncResult, TreeSourceSyncState,
|
||||
DEFAULT_INCREMENAL_SYNC_FLUSH_INTERVAL,
|
||||
};
|
||||
use crate::index::full_source_code_embedding::changed_files::ChangedFiles;
|
||||
use crate::index::full_source_code_embedding::codebase_index::MAX_DEPTH;
|
||||
use crate::index::full_source_code_embedding::fragment_metadata::{
|
||||
FragmentLocation, LeafToFragmentMetadata,
|
||||
};
|
||||
use crate::index::full_source_code_embedding::merkle_tree::{MerkleHash, MerkleTree};
|
||||
use crate::index::full_source_code_embedding::store_client::MockStoreClient;
|
||||
use crate::index::full_source_code_embedding::{
|
||||
ContentHash, EmbeddingConfig, Fragment, FragmentMetadata,
|
||||
};
|
||||
use crate::index::locations::{CodeContextLocation, FileFragmentLocation};
|
||||
|
||||
impl CodebaseIndex {
|
||||
fn new_for_test(
|
||||
@@ -104,6 +102,86 @@ fn create_test_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synced_index_with_queued_file_changes_reports_pending_status() {
|
||||
VirtualFS::test(
|
||||
"synced_index_with_queued_file_changes_reports_pending_status",
|
||||
|dirs, mut sandbox| {
|
||||
App::test((), |mut app| async move {
|
||||
app.add_singleton_model(DirectoryWatcher::new);
|
||||
|
||||
let repo_name = "warp-virtual";
|
||||
sandbox.mkdir(repo_name);
|
||||
sandbox.with_files(vec![Stub::FileWithContent(
|
||||
format!("{repo_name}/existing_file").as_str(),
|
||||
"existing content",
|
||||
)]);
|
||||
|
||||
let repo_path = dunce::canonicalize(dirs.tests().join(repo_name)).unwrap();
|
||||
let build_file_tree_result =
|
||||
block_on(CodebaseIndex::build_file_tree(repo_path.clone(), None)).unwrap();
|
||||
let (tree, _) =
|
||||
block_on(MerkleTree::try_new(build_file_tree_result.file_tree)).unwrap();
|
||||
|
||||
let mut index = CodebaseIndex::new_for_test(Default::default(), &mut app);
|
||||
index.tree_sync_state = TreeSourceSyncState::Synced {
|
||||
tree,
|
||||
server_sync_result: ServerSyncResult::Success,
|
||||
};
|
||||
|
||||
let mut changed_files = ChangedFiles::default();
|
||||
changed_files.upsertions.insert(repo_path.join("new_file"));
|
||||
index.pending_file_changes = Some(changed_files);
|
||||
|
||||
let status = index.codebase_index_status();
|
||||
assert!(status.has_pending());
|
||||
assert!(status.has_synced_version());
|
||||
assert_eq!(status.last_sync_successful(), Some(true));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synced_index_without_pending_file_changes_stays_ready_after_flush() {
|
||||
VirtualFS::test(
|
||||
"synced_index_without_pending_file_changes_stays_ready_after_flush",
|
||||
|dirs, mut sandbox| {
|
||||
App::test((), |mut app| async move {
|
||||
app.add_singleton_model(DirectoryWatcher::new);
|
||||
|
||||
let repo_name = "warp-virtual";
|
||||
sandbox.mkdir(repo_name);
|
||||
sandbox.with_files(vec![Stub::FileWithContent(
|
||||
format!("{repo_name}/existing_file").as_str(),
|
||||
"existing content",
|
||||
)]);
|
||||
|
||||
let repo_path = dunce::canonicalize(dirs.tests().join(repo_name)).unwrap();
|
||||
let build_file_tree_result =
|
||||
block_on(CodebaseIndex::build_file_tree(repo_path, None)).unwrap();
|
||||
let (tree, _) =
|
||||
block_on(MerkleTree::try_new(build_file_tree_result.file_tree)).unwrap();
|
||||
|
||||
let mut test_index = CodebaseIndex::new_for_test(Default::default(), &mut app);
|
||||
test_index.tree_sync_state = TreeSourceSyncState::Synced {
|
||||
tree,
|
||||
server_sync_result: ServerSyncResult::Success,
|
||||
};
|
||||
let index = app.add_model(|_| test_index);
|
||||
|
||||
index.update(&mut app, |index, ctx| {
|
||||
index.flush_pending_file_changes(ctx);
|
||||
|
||||
let status = index.codebase_index_status();
|
||||
assert!(!status.has_pending());
|
||||
assert!(status.has_synced_version());
|
||||
assert_eq!(status.last_sync_successful(), Some(true));
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_empty_fragments() {
|
||||
App::test((), |mut app| async move {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use std::{collections::HashMap, ops::Range, path::PathBuf};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use string_offset::ByteOffset;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use itertools::Itertools;
|
||||
use repo_metadata::{BuildTreeError, DirectoryWatcher, Repository};
|
||||
@@ -13,12 +11,12 @@ cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "local_fs")] {
|
||||
use chrono::Utc;
|
||||
use super::changed_files::ChangedFiles;
|
||||
use crate::index::path_passes_filters;
|
||||
use crate::index::{is_git_internal_path, matches_gitignores};
|
||||
use ignore::gitignore::Gitignore;
|
||||
use notify_debouncer_full::notify::{RecursiveMode, WatchFilter};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use watcher::{BulkFilesystemWatcher, BulkFilesystemWatcherEvent};
|
||||
use galaxyui::r#async::Timer;
|
||||
use galaxyui_core::r#async::Timer;
|
||||
use galaxy_core::{send_telemetry_from_ctx, report_if_error};
|
||||
use crate::telemetry::AITelemetryEvent;
|
||||
use instant::Instant;
|
||||
@@ -27,21 +25,16 @@ cfg_if::cfg_if! {
|
||||
}
|
||||
}
|
||||
use galaxy_core::safe_anyhow;
|
||||
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
|
||||
use galaxyui_core::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
|
||||
|
||||
use super::{
|
||||
codebase_index::{CodebaseIndexEvent, RetrievalID, SyncProgress},
|
||||
fragment_metadata::FragmentMetadata,
|
||||
priority_queue::{BuildQueue, Priority},
|
||||
snapshot::*,
|
||||
store_client::StoreClient,
|
||||
CodebaseIndex, EmbeddingConfig, Error as CodebaseIndexError, NodeHash,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
index::locations::CodeContextLocation,
|
||||
workspace::{WorkspaceMetadata, WorkspaceMetadataEvent},
|
||||
};
|
||||
use super::codebase_index::{CodebaseIndexEvent, RetrievalID, SyncProgress};
|
||||
use super::fragment_metadata::FragmentMetadata;
|
||||
use super::priority_queue::{BuildQueue, Priority};
|
||||
use super::snapshot::*;
|
||||
use super::store_client::StoreClient;
|
||||
use super::{CodebaseIndex, ContentHash, EmbeddingConfig, Error as CodebaseIndexError, NodeHash};
|
||||
use crate::index::locations::CodeContextLocation;
|
||||
use crate::workspace::{WorkspaceMetadata, WorkspaceMetadataEvent};
|
||||
|
||||
/// The interval for debouncing filesystem events.
|
||||
const REPO_WATCHER_DEBOUNCE_DURATION: Duration = Duration::from_secs(10);
|
||||
@@ -69,6 +62,19 @@ pub enum RetrieveFileError {
|
||||
IndexNotFound,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FragmentMetadataLookupError {
|
||||
#[error("Codebase index not found")]
|
||||
IndexNotFound,
|
||||
#[error("Codebase index has no synced root hash")]
|
||||
IndexNotSynced,
|
||||
#[error("Codebase index root hash mismatch: requested {requested}, current {current}")]
|
||||
RootHashMismatch {
|
||||
requested: NodeHash,
|
||||
current: NodeHash,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum CodebaseIndexManagerEvent {
|
||||
RetrievalRequestCompleted {
|
||||
retrieval_id: RetrievalID,
|
||||
@@ -79,7 +85,9 @@ pub enum CodebaseIndexManagerEvent {
|
||||
retrieval_id: RetrievalID,
|
||||
error_message: String,
|
||||
},
|
||||
SyncStateUpdated,
|
||||
SyncStateUpdated {
|
||||
root_path: PathBuf,
|
||||
},
|
||||
IndexMetadataUpdated {
|
||||
root_path: PathBuf,
|
||||
event: WorkspaceMetadataEvent,
|
||||
@@ -87,7 +95,9 @@ pub enum CodebaseIndexManagerEvent {
|
||||
RemoveExpiredIndexMetadata {
|
||||
expired_metadata: Arc<Vec<PathBuf>>,
|
||||
},
|
||||
NewIndexCreated,
|
||||
NewIndexCreated {
|
||||
root_path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
/// User-facing indexing errors.
|
||||
@@ -132,6 +142,7 @@ pub struct CodebaseIndexStatus {
|
||||
pub(super) has_synced_version: bool,
|
||||
pub(super) last_sync_successful: Option<CodebaseIndexFinishedStatus>,
|
||||
pub(super) sync_progress: Option<SyncProgress>,
|
||||
pub(super) root_hash: Option<NodeHash>,
|
||||
}
|
||||
|
||||
impl CodebaseIndexStatus {
|
||||
@@ -156,17 +167,127 @@ impl CodebaseIndexStatus {
|
||||
pub fn sync_progress(&self) -> Option<&SyncProgress> {
|
||||
self.sync_progress.as_ref()
|
||||
}
|
||||
|
||||
pub fn root_hash(&self) -> Option<&NodeHash> {
|
||||
self.root_hash.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct CodebaseIndexStatusEventKey {
|
||||
has_pending: bool,
|
||||
has_synced_version: bool,
|
||||
last_sync_status: Option<CodebaseIndexFinishedStatusEventKey>,
|
||||
sync_progress: Option<SyncProgressEventKey>,
|
||||
root_hash: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&CodebaseIndexStatus> for CodebaseIndexStatusEventKey {
|
||||
fn from(status: &CodebaseIndexStatus) -> Self {
|
||||
Self {
|
||||
has_pending: status.has_pending,
|
||||
has_synced_version: status.has_synced_version,
|
||||
last_sync_status: status
|
||||
.last_sync_successful
|
||||
.as_ref()
|
||||
.map(CodebaseIndexFinishedStatusEventKey::from),
|
||||
sync_progress: status
|
||||
.sync_progress
|
||||
.as_ref()
|
||||
.map(SyncProgressEventKey::from),
|
||||
root_hash: status.root_hash.as_ref().map(ToString::to_string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum CodebaseIndexFinishedStatusEventKey {
|
||||
Completed,
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
impl From<&CodebaseIndexFinishedStatus> for CodebaseIndexFinishedStatusEventKey {
|
||||
fn from(status: &CodebaseIndexFinishedStatus) -> Self {
|
||||
match status {
|
||||
CodebaseIndexFinishedStatus::Completed => Self::Completed,
|
||||
CodebaseIndexFinishedStatus::Failed(error) => Self::Failed(error.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum SyncProgressEventKey {
|
||||
Discovering {
|
||||
total_nodes: usize,
|
||||
},
|
||||
Syncing {
|
||||
completed_nodes: usize,
|
||||
total_nodes: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&SyncProgress> for SyncProgressEventKey {
|
||||
fn from(progress: &SyncProgress) -> Self {
|
||||
match progress {
|
||||
SyncProgress::Discovering { total_nodes } => Self::Discovering {
|
||||
total_nodes: *total_nodes,
|
||||
},
|
||||
SyncProgress::Syncing {
|
||||
completed_nodes,
|
||||
total_nodes,
|
||||
} => Self::Syncing {
|
||||
completed_nodes: *completed_nodes,
|
||||
total_nodes: *total_nodes,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
pub enum BuildSource<'a> {
|
||||
FromPath(&'a Path),
|
||||
FromPersistedMetadata(WorkspaceMetadata),
|
||||
}
|
||||
pub struct CodebaseIndexManagerConfig {
|
||||
persisted_index_metadata: Vec<WorkspaceMetadata>,
|
||||
max_index_count: Option<usize>,
|
||||
max_files_repo_limit: usize,
|
||||
embedding_generation_batch_size: usize,
|
||||
store_client: Arc<dyn StoreClient>,
|
||||
indexing_enabled: bool,
|
||||
restore_persisted_indices_on_startup: bool,
|
||||
}
|
||||
|
||||
impl CodebaseIndexManagerConfig {
|
||||
pub fn new(
|
||||
persisted_index_metadata: Vec<WorkspaceMetadata>,
|
||||
max_index_count: Option<usize>,
|
||||
max_files_repo_limit: usize,
|
||||
embedding_generation_batch_size: usize,
|
||||
store_client: Arc<dyn StoreClient>,
|
||||
indexing_enabled: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
persisted_index_metadata,
|
||||
max_index_count,
|
||||
max_files_repo_limit,
|
||||
embedding_generation_batch_size,
|
||||
store_client,
|
||||
indexing_enabled,
|
||||
restore_persisted_indices_on_startup: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn defer_persisted_index_restore(mut self) -> Self {
|
||||
self.restore_persisted_indices_on_startup = false;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Manager for the codebase index states across the app.
|
||||
pub struct CodebaseIndexManager {
|
||||
codebase_indices: HashMap<PathBuf, ModelHandle<CodebaseIndex>>,
|
||||
|
||||
last_emitted_codebase_index_statuses: HashMap<PathBuf, CodebaseIndexStatusEventKey>,
|
||||
|
||||
store_client: Arc<dyn StoreClient>,
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
@@ -179,6 +300,11 @@ pub struct CodebaseIndexManager {
|
||||
max_files_repo_limit: usize,
|
||||
|
||||
embedding_generation_batch_size: usize,
|
||||
|
||||
indexing_enabled: bool,
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
snapshot_storage: Option<SnapshotStorage>,
|
||||
}
|
||||
|
||||
impl CodebaseIndexManager {
|
||||
@@ -189,25 +315,100 @@ impl CodebaseIndexManager {
|
||||
max_files_repo_limit: usize,
|
||||
embedding_generation_batch_size: usize,
|
||||
store_client: Arc<dyn StoreClient>,
|
||||
indexing_enabled: bool,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let config = CodebaseIndexManagerConfig::new(
|
||||
persisted_index_metadata,
|
||||
max_index_count,
|
||||
max_files_repo_limit,
|
||||
embedding_generation_batch_size,
|
||||
store_client,
|
||||
indexing_enabled,
|
||||
);
|
||||
Self::new_with_config(config, ctx)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(unused_variables))]
|
||||
pub fn new_with_config(
|
||||
config: CodebaseIndexManagerConfig,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
#[cfg(feature = "local_fs")]
|
||||
{
|
||||
report_if_error!(migrate_snapshots_to_secure_dir_if_needed());
|
||||
Self::new_with_snapshot_storage(config, SnapshotStorage::app_default(), ctx)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
{
|
||||
Self::new_internal(config, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub fn new_with_snapshot_storage(
|
||||
config: CodebaseIndexManagerConfig,
|
||||
snapshot_storage: Option<SnapshotStorage>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self::new_internal(config, snapshot_storage, ctx)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(unused_variables))]
|
||||
fn new_internal(
|
||||
config: CodebaseIndexManagerConfig,
|
||||
#[cfg(feature = "local_fs")] snapshot_storage: Option<SnapshotStorage>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let CodebaseIndexManagerConfig {
|
||||
persisted_index_metadata,
|
||||
max_index_count,
|
||||
max_files_repo_limit,
|
||||
embedding_generation_batch_size,
|
||||
store_client,
|
||||
indexing_enabled,
|
||||
restore_persisted_indices_on_startup,
|
||||
} = config;
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "local_fs")] {
|
||||
let file_watcher = ctx.add_model(|ctx| BulkFilesystemWatcher::new(REPO_WATCHER_DEBOUNCE_DURATION, ctx));
|
||||
ctx.subscribe_to_model(&file_watcher, Self::handle_watcher_event);
|
||||
}
|
||||
}
|
||||
if !indexing_enabled {
|
||||
log::debug!(
|
||||
"Codebase indexing disabled for this launch mode; skipping restore of {:?} persisted codebase indices",
|
||||
persisted_index_metadata.len()
|
||||
);
|
||||
|
||||
return Self {
|
||||
codebase_indices: HashMap::new(),
|
||||
last_emitted_codebase_index_statuses: HashMap::new(),
|
||||
store_client,
|
||||
#[cfg(feature = "local_fs")]
|
||||
watcher: file_watcher,
|
||||
build_queue: BuildQueue::empty(),
|
||||
max_indices: max_index_count,
|
||||
max_files_repo_limit,
|
||||
embedding_generation_batch_size,
|
||||
indexing_enabled,
|
||||
#[cfg(feature = "local_fs")]
|
||||
snapshot_storage,
|
||||
};
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Received {:?} persisted codebase indices",
|
||||
persisted_index_metadata.len()
|
||||
);
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
report_if_error!(migrate_snapshots_to_secure_dir_if_needed());
|
||||
|
||||
let (invalid_metadata, valid_metadata) =
|
||||
split_snapshot_metadata_by_validity(persisted_index_metadata);
|
||||
let (invalid_metadata, valid_metadata) = split_snapshot_metadata_by_validity(
|
||||
persisted_index_metadata,
|
||||
snapshot_storage.as_ref(),
|
||||
);
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
let (invalid_metadata, valid_metadata) = (persisted_index_metadata, Vec::new());
|
||||
|
||||
ctx.emit(CodebaseIndexManagerEvent::RemoveExpiredIndexMetadata {
|
||||
expired_metadata: Arc::new(
|
||||
@@ -217,16 +418,18 @@ impl CodebaseIndexManager {
|
||||
.collect(),
|
||||
),
|
||||
});
|
||||
|
||||
if let Some(snapshot_file_dir) = snapshot_dir() {
|
||||
clean_up_snapshot_files(&snapshot_file_dir, &valid_metadata);
|
||||
#[cfg(feature = "local_fs")]
|
||||
if let Some(snapshot_storage) = snapshot_storage.as_ref() {
|
||||
clean_up_snapshot_files(snapshot_storage.path(), &valid_metadata);
|
||||
}
|
||||
|
||||
// For the moment, we've decided to load all snapshots regardless of the index count.
|
||||
let build_queue = BuildQueue::new_with_persisted(valid_metadata);
|
||||
let build_queue =
|
||||
BuildQueue::new_with_persisted(valid_metadata, restore_persisted_indices_on_startup);
|
||||
|
||||
let mut me = Self {
|
||||
codebase_indices: HashMap::new(),
|
||||
last_emitted_codebase_index_statuses: HashMap::new(),
|
||||
store_client,
|
||||
#[cfg(feature = "local_fs")]
|
||||
watcher: file_watcher,
|
||||
@@ -234,12 +437,12 @@ impl CodebaseIndexManager {
|
||||
max_indices: max_index_count,
|
||||
max_files_repo_limit,
|
||||
embedding_generation_batch_size,
|
||||
indexing_enabled,
|
||||
#[cfg(feature = "local_fs")]
|
||||
snapshot_storage,
|
||||
};
|
||||
|
||||
// Start building the first index in the queue.
|
||||
if let Some(next_repo) = me.build_queue.pick_next_sync() {
|
||||
me.build_and_sync_codebase_index(BuildSource::FromPersistedMetadata(next_repo), ctx);
|
||||
}
|
||||
me.start_next_queued_index(ctx);
|
||||
|
||||
me
|
||||
}
|
||||
@@ -250,6 +453,7 @@ impl CodebaseIndexManager {
|
||||
let file_watcher = ctx.add_model(|_| BulkFilesystemWatcher::new_for_test());
|
||||
Self {
|
||||
codebase_indices: HashMap::new(),
|
||||
last_emitted_codebase_index_statuses: HashMap::new(),
|
||||
store_client,
|
||||
#[cfg(feature = "local_fs")]
|
||||
watcher: file_watcher,
|
||||
@@ -257,6 +461,9 @@ impl CodebaseIndexManager {
|
||||
max_indices: None,
|
||||
max_files_repo_limit: 0,
|
||||
embedding_generation_batch_size: 100,
|
||||
indexing_enabled: true,
|
||||
#[cfg(feature = "local_fs")]
|
||||
snapshot_storage: SnapshotStorage::app_default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,8 +513,15 @@ impl CodebaseIndexManager {
|
||||
|
||||
// Remove snapshots from disk.
|
||||
let to_drop_clone = to_drop.clone();
|
||||
#[cfg(feature = "local_fs")]
|
||||
let snapshot_storage = self.snapshot_storage.clone();
|
||||
ctx.spawn(
|
||||
async move { Self::drop_index_snapshots(to_drop_clone).await },
|
||||
async move {
|
||||
#[cfg(feature = "local_fs")]
|
||||
Self::drop_index_snapshots(snapshot_storage, to_drop_clone).await;
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
let _ = to_drop_clone;
|
||||
},
|
||||
|_, _, _| {},
|
||||
);
|
||||
|
||||
@@ -317,11 +531,15 @@ impl CodebaseIndexManager {
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove the gien index snapshots from disk.
|
||||
async fn drop_index_snapshots(to_drop: Vec<PathBuf>) {
|
||||
if let Some(snapshot_dir) = snapshot_dir() {
|
||||
/// Remove the given index snapshots from disk.
|
||||
#[cfg(feature = "local_fs")]
|
||||
async fn drop_index_snapshots(
|
||||
snapshot_storage: Option<SnapshotStorage>,
|
||||
to_drop: Vec<PathBuf>,
|
||||
) {
|
||||
if let Some(snapshot_storage) = snapshot_storage {
|
||||
for codebase_root in &to_drop {
|
||||
Self::drop_index_snapshot(&snapshot_dir, codebase_root).await;
|
||||
Self::drop_index_snapshot(snapshot_storage.path(), codebase_root).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,6 +563,7 @@ impl CodebaseIndexManager {
|
||||
|
||||
// Drop the in-memory index.
|
||||
self.codebase_indices.remove(root_path);
|
||||
self.last_emitted_codebase_index_statuses.remove(root_path);
|
||||
|
||||
// Stop the filewatcher from receiving events for this codebase.
|
||||
#[cfg(feature = "local_fs")]
|
||||
@@ -359,11 +578,16 @@ impl CodebaseIndexManager {
|
||||
|
||||
// Remove snapshot from disk.
|
||||
let root_path_clone = root_path.clone();
|
||||
#[cfg(feature = "local_fs")]
|
||||
let snapshot_storage = self.snapshot_storage.clone();
|
||||
ctx.spawn(
|
||||
async move {
|
||||
if let Some(snapshot_dir) = snapshot_dir() {
|
||||
Self::drop_index_snapshot(&snapshot_dir, &root_path_clone).await;
|
||||
#[cfg(feature = "local_fs")]
|
||||
if let Some(snapshot_storage) = snapshot_storage {
|
||||
Self::drop_index_snapshot(snapshot_storage.path(), &root_path_clone).await;
|
||||
}
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
let _ = root_path_clone;
|
||||
},
|
||||
|_, _, _| {},
|
||||
);
|
||||
@@ -451,6 +675,7 @@ impl CodebaseIndexManager {
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn handle_watcher_event(
|
||||
&mut self,
|
||||
_: ModelHandle<BulkFilesystemWatcher>,
|
||||
event: &BulkFilesystemWatcherEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
@@ -462,6 +687,9 @@ impl CodebaseIndexManager {
|
||||
}
|
||||
|
||||
pub fn handle_active_session_changed(&mut self, active_directory: &Path) {
|
||||
if !self.is_indexing_enabled() {
|
||||
return;
|
||||
}
|
||||
let Some(root_path) = self.root_path_for_codebase(active_directory) else {
|
||||
return;
|
||||
};
|
||||
@@ -517,11 +745,17 @@ impl CodebaseIndexManager {
|
||||
|
||||
/// Ensures the current number of indices is below the maximum.
|
||||
pub fn can_create_new_indices(&self) -> bool {
|
||||
if !self.is_indexing_enabled() {
|
||||
return false;
|
||||
}
|
||||
self.max_indices
|
||||
.is_none_or(|max_indices| self.codebase_indices.len() < max_indices)
|
||||
}
|
||||
|
||||
pub fn handle_session_bootstrapped(&mut self, working_directory: &Path) {
|
||||
if !self.is_indexing_enabled() {
|
||||
return;
|
||||
}
|
||||
let Some(root_path) = self.root_path_for_codebase(working_directory) else {
|
||||
return;
|
||||
};
|
||||
@@ -553,6 +787,22 @@ impl CodebaseIndexManager {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fragment_metadatas_from_hashes(
|
||||
&self,
|
||||
repo_path: &Path,
|
||||
root_hash: &NodeHash,
|
||||
content_hashes: &[ContentHash],
|
||||
app: &AppContext,
|
||||
) -> Result<HashMap<ContentHash, Vec<FragmentMetadata>>, FragmentMetadataLookupError> {
|
||||
let (codebase_index, _) = self
|
||||
.get_codebase_index_internal(repo_path)
|
||||
.map_err(|_| FragmentMetadataLookupError::IndexNotFound)?;
|
||||
|
||||
codebase_index
|
||||
.as_ref(app)
|
||||
.fragment_metadatas_from_hashes(root_hash, content_hashes)
|
||||
}
|
||||
|
||||
pub fn get_codebase_paths(&self) -> impl Iterator<Item = &PathBuf> {
|
||||
self.codebase_indices.keys()
|
||||
}
|
||||
@@ -561,13 +811,37 @@ impl CodebaseIndexManager {
|
||||
self.codebase_indices.len()
|
||||
}
|
||||
|
||||
pub fn index_directory(&mut self, directory: PathBuf, ctx: &mut ModelContext<Self>) {
|
||||
let directory = dunce::canonicalize(&directory).unwrap_or(directory);
|
||||
if !self.codebase_indices.contains_key(&directory) {
|
||||
self.build_and_sync_codebase_index(BuildSource::FromPath(&directory), ctx);
|
||||
// Starting a new codebase index should be considered into sync state updates.
|
||||
ctx.emit(CodebaseIndexManagerEvent::SyncStateUpdated);
|
||||
pub fn is_indexing_enabled(&self) -> bool {
|
||||
self.indexing_enabled
|
||||
}
|
||||
|
||||
pub fn start_persisted_index_restore(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
if !self.is_indexing_enabled() {
|
||||
return;
|
||||
}
|
||||
if self.build_queue.start() {
|
||||
self.start_next_queued_index(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index_directory(&mut self, directory: PathBuf, ctx: &mut ModelContext<Self>) -> bool {
|
||||
if !self.is_indexing_enabled() {
|
||||
return false;
|
||||
}
|
||||
if self.root_path_for_codebase(&directory).is_none() {
|
||||
if !self.build_and_sync_codebase_index(BuildSource::FromPath(&directory), ctx) {
|
||||
return false;
|
||||
}
|
||||
let indexed_directory = self
|
||||
.root_path_for_codebase(&directory)
|
||||
.unwrap_or_else(|| directory.clone());
|
||||
self.record_codebase_index_status(&indexed_directory, ctx);
|
||||
// Starting a new codebase index should be considered into sync state updates.
|
||||
ctx.emit(CodebaseIndexManagerEvent::NewIndexCreated {
|
||||
root_path: indexed_directory,
|
||||
});
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
@@ -577,9 +851,21 @@ impl CodebaseIndexManager {
|
||||
gitignores: Arc<Vec<Gitignore>>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let watch_filter = WatchFilter::with_filter(Arc::new(move |path| {
|
||||
path_passes_filters(path, gitignores.as_slice())
|
||||
}));
|
||||
// The codebase indexer only cares about source files:
|
||||
// skip anything inside `.git/` and anything matched by gitignore
|
||||
// (including descendants of an ignored ancestor directory).
|
||||
// The same predicate gates both directory descent and event emission.
|
||||
let filter = Arc::new(move |path: &Path| {
|
||||
!is_git_internal_path(path)
|
||||
&& !matches_gitignores(
|
||||
path,
|
||||
path.is_dir(),
|
||||
gitignores.as_slice(),
|
||||
true, /* check_ancestors */
|
||||
)
|
||||
});
|
||||
|
||||
let watch_filter = WatchFilter::with_filter(filter.clone(), filter);
|
||||
self.watcher.update(ctx, |watcher, _ctx| {
|
||||
std::mem::drop(watcher.register_path(
|
||||
root_path,
|
||||
@@ -607,9 +893,12 @@ impl CodebaseIndexManager {
|
||||
&mut self,
|
||||
build_source: BuildSource,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
) -> bool {
|
||||
if !self.is_indexing_enabled() {
|
||||
return false;
|
||||
}
|
||||
if !self.can_create_new_indices() {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let repo_path = match build_source {
|
||||
@@ -624,7 +913,7 @@ impl CodebaseIndexManager {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!("Failed to canonicalize repository path: {e:?}");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -635,7 +924,7 @@ impl CodebaseIndexManager {
|
||||
Ok(handle) => handle,
|
||||
Err(e) => {
|
||||
log::error!("Failed to start tracking repository: {e:?}");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -646,11 +935,15 @@ impl CodebaseIndexManager {
|
||||
.codebase_indices
|
||||
.entry(canonical_key)
|
||||
.or_insert_with(|| {
|
||||
#[cfg(feature = "local_fs")]
|
||||
let snapshot_storage = self.snapshot_storage.clone();
|
||||
let index = Self::build_and_sync_codebase_index_internal(
|
||||
self.store_client.clone(),
|
||||
handle,
|
||||
self.max_files_repo_limit,
|
||||
self.embedding_generation_batch_size,
|
||||
#[cfg(feature = "local_fs")]
|
||||
snapshot_storage,
|
||||
ctx,
|
||||
);
|
||||
|
||||
@@ -666,6 +959,7 @@ impl CodebaseIndexManager {
|
||||
index.update_timestamps_from_metadata(metadata);
|
||||
});
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Checks whether a snapshot exists for the index and attempts to load it;
|
||||
@@ -675,6 +969,7 @@ impl CodebaseIndexManager {
|
||||
repository: ModelHandle<Repository>,
|
||||
max_files_repo_limit: usize,
|
||||
embedding_generation_batch_size: usize,
|
||||
#[cfg(feature = "local_fs")] snapshot_storage: Option<SnapshotStorage>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> ModelHandle<CodebaseIndex> {
|
||||
let codebase_index = ctx.add_model(|ctx| {
|
||||
@@ -684,13 +979,17 @@ impl CodebaseIndexManager {
|
||||
.as_ref(ctx)
|
||||
.root_dir()
|
||||
.to_local_path()
|
||||
.is_some_and(|p| has_snapshot(&p))
|
||||
.is_some_and(|p| {
|
||||
snapshot_storage
|
||||
.as_ref()
|
||||
.is_some_and(|storage| storage.has_snapshot(&p))
|
||||
})
|
||||
{
|
||||
if let Some(snapshot_dir) = snapshot_dir() {
|
||||
if let Some(snapshot_storage) = snapshot_storage.as_ref() {
|
||||
let read_snapshot_start_time = Instant::now();
|
||||
match read_snapshot(
|
||||
store_client.clone(),
|
||||
snapshot_dir.as_path(),
|
||||
snapshot_storage.path(),
|
||||
repository.clone(),
|
||||
max_files_repo_limit,
|
||||
embedding_generation_batch_size,
|
||||
@@ -733,6 +1032,7 @@ impl CodebaseIndexManager {
|
||||
|
||||
fn handle_codebase_index_event(
|
||||
&mut self,
|
||||
_: ModelHandle<CodebaseIndex>,
|
||||
event: &CodebaseIndexEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
@@ -753,8 +1053,8 @@ impl CodebaseIndexManager {
|
||||
fragments: fragments.clone(),
|
||||
out_of_sync_delay: *out_of_sync_delay,
|
||||
}),
|
||||
CodebaseIndexEvent::SyncStateUpdated => {
|
||||
ctx.emit(CodebaseIndexManagerEvent::SyncStateUpdated)
|
||||
CodebaseIndexEvent::SyncStateUpdated { root_path } => {
|
||||
self.maybe_emit_sync_state_updated(root_path, ctx);
|
||||
}
|
||||
CodebaseIndexEvent::IndexMetadataUpdated { root_path, event } => {
|
||||
ctx.emit(CodebaseIndexManagerEvent::IndexMetadataUpdated {
|
||||
@@ -785,11 +1085,42 @@ impl CodebaseIndexManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_emit_sync_state_updated(&mut self, root_path: &Path, ctx: &mut ModelContext<Self>) {
|
||||
if self.record_codebase_index_status(root_path, ctx) {
|
||||
ctx.emit(CodebaseIndexManagerEvent::SyncStateUpdated {
|
||||
root_path: root_path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn record_codebase_index_status(
|
||||
&mut self,
|
||||
root_path: &Path,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> bool {
|
||||
let root_path = dunce::canonicalize(root_path).unwrap_or_else(|_| root_path.to_path_buf());
|
||||
let Some(status) = self.get_codebase_index_status_for_path(root_path.as_path(), ctx) else {
|
||||
return false;
|
||||
};
|
||||
let key = CodebaseIndexStatusEventKey::from(&status);
|
||||
match self.last_emitted_codebase_index_statuses.get(&root_path) {
|
||||
Some(previous_key) if previous_key == &key => false,
|
||||
Some(_) | None => {
|
||||
self.last_emitted_codebase_index_statuses
|
||||
.insert(root_path, key);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_index_build_finished(&mut self, finished_repo: &Path, ctx: &mut ModelContext<Self>) {
|
||||
let Ok(_) = self.get_codebase_index_internal(finished_repo) else {
|
||||
return;
|
||||
};
|
||||
self.start_next_queued_index(ctx);
|
||||
}
|
||||
|
||||
fn start_next_queued_index(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
if let Some(next_repo) = self.build_queue.pick_next_sync() {
|
||||
self.build_and_sync_codebase_index(BuildSource::FromPersistedMetadata(next_repo), ctx);
|
||||
}
|
||||
@@ -810,6 +1141,20 @@ impl CodebaseIndexManager {
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn with_indexed_codebase<T>(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
on_found: impl FnOnce(&mut Self, &Path, &mut ModelContext<Self>) -> T,
|
||||
on_missing: impl FnOnce(&mut Self, &Path, &mut ModelContext<Self>) -> T,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> T {
|
||||
let Some(indexed_repo_path) = self.root_path_for_codebase(path) else {
|
||||
return on_missing(self, path, ctx);
|
||||
};
|
||||
|
||||
on_found(self, indexed_repo_path.as_path(), ctx)
|
||||
}
|
||||
|
||||
fn get_codebase_index_internal(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -909,15 +1254,15 @@ impl CodebaseIndexManager {
|
||||
}
|
||||
};
|
||||
|
||||
let snapshot_dir = match snapshot_dir() {
|
||||
Some(dir) => dir,
|
||||
let snapshot_storage = match self.snapshot_storage.as_ref() {
|
||||
Some(storage) => storage,
|
||||
None => {
|
||||
log::warn!("No snapshot directory to write to");
|
||||
Self::schedule_next_snapshot_write(repo_path, ctx);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let snapshot_path = snapshot_path(&snapshot_dir, repo_path.as_path());
|
||||
let snapshot_path = snapshot_storage.snapshot_path(repo_path.as_path());
|
||||
|
||||
// Update timestamp eagerly so concurrent calls to has_unsnapshotted_changes()
|
||||
// won't trigger a duplicate snapshot while the background write is in progress.
|
||||
@@ -974,6 +1319,9 @@ impl CodebaseIndexManager {
|
||||
directory_path: &Path,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> anyhow::Result<()> {
|
||||
if !self.is_indexing_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
// Find the root path for this directory's codebase
|
||||
let Some(repo_path) = self.root_path_for_codebase(directory_path) else {
|
||||
return Err(anyhow::anyhow!("Failed to find root path for directory"));
|
||||
@@ -987,7 +1335,7 @@ impl CodebaseIndexManager {
|
||||
codebase_index.update(ctx, |index, _ctx| {
|
||||
// Check if the index is in a state where it can perform incremental updates
|
||||
let status = index.codebase_index_status();
|
||||
if status.has_pending {
|
||||
if status.last_sync_successful() != Some(true) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1013,3 +1361,7 @@ impl Entity for CodebaseIndexManager {
|
||||
}
|
||||
|
||||
impl SingletonEntity for CodebaseIndexManager {}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "manager_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use chrono::Utc;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use repo_metadata::DirectoryWatcher;
|
||||
use galaxyui_core::App;
|
||||
|
||||
use super::{
|
||||
BuildSource, CodebaseIndexFinishedStatus, CodebaseIndexManager, CodebaseIndexManagerConfig,
|
||||
CodebaseIndexStatus, CodebaseIndexStatusEventKey, CodebaseIndexingError, SyncProgress,
|
||||
};
|
||||
use crate::index::full_source_code_embedding::store_client::MockStoreClient;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::index::full_source_code_embedding::SnapshotStorage;
|
||||
use crate::workspace::WorkspaceMetadata;
|
||||
|
||||
fn workspace_metadata(path: impl Into<PathBuf>) -> WorkspaceMetadata {
|
||||
WorkspaceMetadata {
|
||||
path: path.into(),
|
||||
navigated_ts: None,
|
||||
modified_ts: None,
|
||||
queried_ts: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn codebase_index_status(
|
||||
has_pending: bool,
|
||||
has_synced_version: bool,
|
||||
last_sync_successful: Option<CodebaseIndexFinishedStatus>,
|
||||
sync_progress: Option<SyncProgress>,
|
||||
) -> CodebaseIndexStatus {
|
||||
CodebaseIndexStatus {
|
||||
has_pending,
|
||||
has_synced_version,
|
||||
last_sync_successful,
|
||||
sync_progress,
|
||||
root_hash: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codebase_index_status_event_key_matches_identical_statuses() {
|
||||
let first_status = codebase_index_status(
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
Some(SyncProgress::Syncing {
|
||||
completed_nodes: 1,
|
||||
total_nodes: 2,
|
||||
}),
|
||||
);
|
||||
let duplicate_status = codebase_index_status(
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
Some(SyncProgress::Syncing {
|
||||
completed_nodes: 1,
|
||||
total_nodes: 2,
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
CodebaseIndexStatusEventKey::from(&first_status),
|
||||
CodebaseIndexStatusEventKey::from(&duplicate_status)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codebase_index_status_event_key_detects_semantic_changes() {
|
||||
let syncing_status = codebase_index_status(
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
Some(SyncProgress::Syncing {
|
||||
completed_nodes: 1,
|
||||
total_nodes: 2,
|
||||
}),
|
||||
);
|
||||
let completed_status = codebase_index_status(
|
||||
false,
|
||||
true,
|
||||
Some(CodebaseIndexFinishedStatus::Completed),
|
||||
None,
|
||||
);
|
||||
let failed_status = codebase_index_status(
|
||||
false,
|
||||
true,
|
||||
Some(CodebaseIndexFinishedStatus::Failed(
|
||||
CodebaseIndexingError::BuildTreeError,
|
||||
)),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_ne!(
|
||||
CodebaseIndexStatusEventKey::from(&syncing_status),
|
||||
CodebaseIndexStatusEventKey::from(&completed_status)
|
||||
);
|
||||
assert_ne!(
|
||||
CodebaseIndexStatusEventKey::from(&completed_status),
|
||||
CodebaseIndexStatusEventKey::from(&failed_status)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initializes_with_indexing_enabled_when_configured() {
|
||||
App::test((), |app| async move {
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new(
|
||||
vec![workspace_metadata("repo")],
|
||||
Some(1),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
true,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.read(&app, |manager, _| {
|
||||
assert!(manager.is_indexing_enabled());
|
||||
assert_eq!(manager.num_active_indices(), 0);
|
||||
assert!(manager.can_create_new_indices());
|
||||
});
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn initializes_with_indexing_disabled_when_configured() {
|
||||
App::test((), |app| async move {
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new(
|
||||
vec![workspace_metadata("repo")],
|
||||
Some(1),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
false,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.read(&app, |manager, _| {
|
||||
assert!(!manager.is_indexing_enabled());
|
||||
assert_eq!(manager.num_active_indices(), 0);
|
||||
assert!(!manager.can_create_new_indices());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn initializes_with_injected_snapshot_storage_when_configured() {
|
||||
App::test((), |app| async move {
|
||||
let snapshot_dir = tempfile::tempdir().unwrap();
|
||||
let storage = SnapshotStorage::from_dir(snapshot_dir.path().join("daemon")).unwrap();
|
||||
let expected_snapshot_dir = storage.path().to_path_buf();
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new_with_snapshot_storage(
|
||||
CodebaseIndexManagerConfig::new(
|
||||
vec![workspace_metadata("repo")],
|
||||
Some(1),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
false,
|
||||
),
|
||||
Some(storage),
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.read(&app, |manager, _| {
|
||||
let snapshot_storage = manager.snapshot_storage.as_ref().unwrap();
|
||||
assert_eq!(snapshot_storage.path(), expected_snapshot_dir);
|
||||
assert!(!snapshot_storage.is_app_default());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_index_restore_starts_on_startup_by_default() {
|
||||
App::test((), |app| async move {
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new(
|
||||
Vec::new(),
|
||||
Some(1),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
true,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.read(&app, |manager, _| {
|
||||
assert!(manager.build_queue.is_running());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn deferred_persisted_index_restore_starts_once() {
|
||||
App::test((), |mut app| async move {
|
||||
app.add_singleton_model(DirectoryWatcher::new);
|
||||
|
||||
let snapshot_dir = tempfile::tempdir().unwrap();
|
||||
let storage = SnapshotStorage::from_dir(snapshot_dir.path().join("daemon")).unwrap();
|
||||
let first_repo = tempfile::tempdir().unwrap();
|
||||
let second_repo = tempfile::tempdir().unwrap();
|
||||
let mut first_metadata = workspace_metadata(first_repo.path());
|
||||
first_metadata.modified_ts = Some(Utc::now());
|
||||
let mut second_metadata = workspace_metadata(second_repo.path());
|
||||
second_metadata.modified_ts = Some(Utc::now());
|
||||
std::fs::write(storage.snapshot_path(first_repo.path()), b"snapshot").unwrap();
|
||||
std::fs::write(storage.snapshot_path(second_repo.path()), b"snapshot").unwrap();
|
||||
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new_with_snapshot_storage(
|
||||
CodebaseIndexManagerConfig::new(
|
||||
vec![first_metadata, second_metadata],
|
||||
Some(2),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
true,
|
||||
)
|
||||
.defer_persisted_index_restore(),
|
||||
Some(storage),
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.update(&mut app, |manager, ctx| {
|
||||
assert!(!manager.build_queue.is_running());
|
||||
assert_eq!(manager.build_queue.queued_metadata().into_iter().count(), 2);
|
||||
|
||||
manager.start_persisted_index_restore(ctx);
|
||||
assert!(manager.build_queue.is_running());
|
||||
assert_eq!(manager.build_queue.queued_metadata().into_iter().count(), 1);
|
||||
|
||||
manager.start_persisted_index_restore(ctx);
|
||||
assert_eq!(manager.build_queue.queued_metadata().into_iter().count(), 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_create_new_indices_honors_max_limit_when_enabled() {
|
||||
App::test((), |mut app| async move {
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new(
|
||||
Vec::new(),
|
||||
Some(1),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
true,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.update(&mut app, |manager, ctx| {
|
||||
assert!(manager.can_create_new_indices());
|
||||
manager.update_max_limits(Some(0), 1000, 32, ctx);
|
||||
assert!(!manager.can_create_new_indices());
|
||||
});
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn index_directory_is_noop_when_indexing_disabled() {
|
||||
App::test((), |mut app| async move {
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new(
|
||||
Vec::new(),
|
||||
Some(1),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
false,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.update(&mut app, |manager, ctx| {
|
||||
assert!(!manager.index_directory(PathBuf::from("repo"), ctx));
|
||||
assert_eq!(manager.num_active_indices(), 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_directory_reports_when_max_index_limit_prevents_creation() {
|
||||
App::test((), |mut app| async move {
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new(
|
||||
Vec::new(),
|
||||
Some(0),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
true,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.update(&mut app, |manager, ctx| {
|
||||
assert!(!manager.index_directory(PathBuf::from("repo"), ctx));
|
||||
assert_eq!(manager.num_active_indices(), 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_and_sync_is_noop_when_indexing_disabled() {
|
||||
App::test((), |mut app| async move {
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new(
|
||||
Vec::new(),
|
||||
Some(1),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
false,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.update(&mut app, |manager, ctx| {
|
||||
assert!(!manager
|
||||
.build_and_sync_codebase_index(BuildSource::FromPath(Path::new("repo")), ctx));
|
||||
assert_eq!(manager.num_active_indices(), 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_incremental_sync_returns_err_when_enabled_and_index_missing() {
|
||||
App::test((), |mut app| async move {
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new(
|
||||
Vec::new(),
|
||||
Some(1),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
true,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.update(&mut app, |manager, ctx| {
|
||||
let result = manager.trigger_incremental_sync_for_path(Path::new("repo"), ctx);
|
||||
assert!(result.is_err());
|
||||
});
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn trigger_incremental_sync_returns_ok_when_indexing_disabled() {
|
||||
App::test((), |mut app| async move {
|
||||
let manager = app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new(
|
||||
Vec::new(),
|
||||
Some(1),
|
||||
1000,
|
||||
32,
|
||||
Arc::new(MockStoreClient),
|
||||
false,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
manager.update(&mut app, |manager, ctx| {
|
||||
let result = manager.trigger_incremental_sync_for_path(Path::new("repo"), ctx);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
//! Common types for hashes that identify codebase embedding state.
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use generic_array::GenericArray;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{digest::OutputSizeUser, Digest, Sha256};
|
||||
use std::{fmt, str::FromStr, sync::Arc};
|
||||
|
||||
use crate::index::full_source_code_embedding::chunker::Fragment;
|
||||
use sha2::digest::OutputSizeUser;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use super::Error;
|
||||
use crate::index::full_source_code_embedding::chunker::Fragment;
|
||||
|
||||
/// The hash of an *intermediate* node in the [`MerkleTree`].
|
||||
///
|
||||
@@ -223,5 +226,5 @@ impl fmt::Display for MerkleHash {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "hash_test.rs"]
|
||||
#[path = "hash_tests.rs"]
|
||||
mod hash_test;
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::MerkleHash;
|
||||
use crate::index::full_source_code_embedding::chunker::Fragment;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[test]
|
||||
fn test_fragment_hash_from_content() {
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::{chunker::Fragment, Error};
|
||||
use super::chunker::Fragment;
|
||||
use super::Error;
|
||||
|
||||
mod hash;
|
||||
mod node;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::index::{
|
||||
THREADPOOL, {DirectoryEntry, Entry, FileMetadata},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use galaxy_util::standardized_path::StandardizedPath;
|
||||
use itertools::Itertools;
|
||||
@@ -10,26 +10,18 @@ use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use repo_metadata::entry::is_file_parsable;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use string_offset::ByteOffset;
|
||||
|
||||
use crate::index::full_source_code_embedding::{
|
||||
chunker::chunk_code,
|
||||
fragment_metadata::{FragmentMetadata, LeafToFragmentMetadataUpdates},
|
||||
Error,
|
||||
};
|
||||
|
||||
use super::{
|
||||
hash::MerkleHash,
|
||||
serialized_tree::{SerializedFilesystemInfo, SerializedMerkleNode},
|
||||
tree::UpdateFileResult,
|
||||
ContentHash, DirEntryOrFragment, NodeHash,
|
||||
use super::hash::MerkleHash;
|
||||
use super::serialized_tree::{SerializedFilesystemInfo, SerializedMerkleNode};
|
||||
use super::tree::UpdateFileResult;
|
||||
use super::{ContentHash, DirEntryOrFragment, NodeHash};
|
||||
use crate::index::full_source_code_embedding::chunker::chunk_code;
|
||||
use crate::index::full_source_code_embedding::fragment_metadata::{
|
||||
FragmentMetadata, LeafToFragmentMetadataUpdates,
|
||||
};
|
||||
use crate::index::full_source_code_embedding::Error;
|
||||
use crate::index::{DirectoryEntry, Entry, FileMetadata, THREADPOOL};
|
||||
|
||||
/// ID that uniquely identifies a node in the merkle tree. It contains the node type
|
||||
/// as well as metadata that distinguishes nodes of the same type.
|
||||
@@ -707,5 +699,5 @@ impl NodeMask {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "node_test.rs"]
|
||||
#[path = "node_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
+4
-5
@@ -1,12 +1,11 @@
|
||||
use crate::index::full_source_code_embedding::{
|
||||
fragment_metadata::LeafToFragmentMetadataUpdates, merkle_tree::DirEntryOrFragment,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use repo_metadata::{DirectoryEntry, Entry};
|
||||
use virtual_fs::{Stub, VirtualFS};
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{MerkleNode, NodeMask};
|
||||
use crate::index::full_source_code_embedding::fragment_metadata::LeafToFragmentMetadataUpdates;
|
||||
use crate::index::full_source_code_embedding::merkle_tree::DirEntryOrFragment;
|
||||
|
||||
/// Tests that node hashes for directories are sorted (meaning they are resilient to files within
|
||||
/// the directory being in a different order).
|
||||
@@ -1,18 +1,17 @@
|
||||
use std::{
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use super::{hash::MerkleHash, node::NodeId, MerkleTree, NodeHash, NodeLens};
|
||||
|
||||
use crate::index::full_source_code_embedding::fragment_metadata::{
|
||||
FragmentLocation, LeafToFragmentMetadata,
|
||||
};
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use string_offset::ByteOffset;
|
||||
|
||||
use super::hash::MerkleHash;
|
||||
use super::node::NodeId;
|
||||
use super::{MerkleTree, NodeHash, NodeLens};
|
||||
use crate::index::full_source_code_embedding::fragment_metadata::{
|
||||
FragmentLocation, LeafToFragmentMetadata,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct SerializedCodebaseIndex {
|
||||
tree: SerializedMerkleTree,
|
||||
@@ -196,5 +195,5 @@ impl SerializedMerkleNode {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "serialized_tree_test.rs"]
|
||||
#[path = "serialized_tree_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
+1
-2
@@ -2,12 +2,11 @@ use futures::executor::block_on;
|
||||
use serde_json;
|
||||
use virtual_fs::VirtualFS;
|
||||
|
||||
use super::SerializedCodebaseIndex;
|
||||
use crate::index::full_source_code_embedding::merkle_tree::{
|
||||
construct_test_merkle_tree, MerkleTree,
|
||||
};
|
||||
|
||||
use super::SerializedCodebaseIndex;
|
||||
|
||||
#[test]
|
||||
fn round_trip_index_serialize_deserialize_json() {
|
||||
VirtualFS::test("test_nodes_from_path_json", |dirs, mut sandbox| {
|
||||
@@ -1,8 +1,9 @@
|
||||
use super::MerkleTree;
|
||||
use crate::index::full_source_code_embedding::fragment_metadata::LeafToFragmentMetadata;
|
||||
use repo_metadata::{DirectoryEntry, Entry};
|
||||
use virtual_fs::{Stub, VirtualFS};
|
||||
|
||||
use super::MerkleTree;
|
||||
use crate::index::full_source_code_embedding::fragment_metadata::LeafToFragmentMetadata;
|
||||
|
||||
/// Construct a test Merkle tree with the following structure:
|
||||
/// ```
|
||||
/// root.txt
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
use crate::index::Entry;
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use cfg_if::cfg_if;
|
||||
use std::{
|
||||
collections::{HashSet, VecDeque},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use super::node::{ChildrenPath, MerkleNode, NodeLens, NodeMask};
|
||||
use super::serialized_tree::SerializedMerkleTree;
|
||||
use super::DirEntryOrFragment;
|
||||
use crate::index::full_source_code_embedding::fragment_metadata::{
|
||||
LeafToFragmentMetadata, LeafToFragmentMetadataUpdates,
|
||||
};
|
||||
use crate::index::full_source_code_embedding::Error;
|
||||
|
||||
use super::{
|
||||
node::{ChildrenPath, MerkleNode, NodeLens, NodeMask},
|
||||
serialized_tree::SerializedMerkleTree,
|
||||
DirEntryOrFragment,
|
||||
};
|
||||
use crate::index::Entry;
|
||||
|
||||
pub(super) enum UpdateFileResult {
|
||||
Deleted,
|
||||
@@ -210,5 +206,5 @@ impl MerkleTree {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tree_test.rs"]
|
||||
#[path = "tree_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
+2
-4
@@ -1,11 +1,9 @@
|
||||
use crate::index::full_source_code_embedding::merkle_tree::{
|
||||
construct_test_merkle_tree, node::ChildrenPath,
|
||||
};
|
||||
use futures::executor::block_on;
|
||||
|
||||
use virtual_fs::VirtualFS;
|
||||
|
||||
use super::*;
|
||||
use crate::index::full_source_code_embedding::merkle_tree::construct_test_merkle_tree;
|
||||
use crate::index::full_source_code_embedding::merkle_tree::node::ChildrenPath;
|
||||
|
||||
#[test]
|
||||
fn test_nodes_from_path() {
|
||||
@@ -5,19 +5,21 @@ mod fragment_metadata;
|
||||
pub mod manager;
|
||||
mod merkle_tree;
|
||||
mod priority_queue;
|
||||
pub mod search_shaping;
|
||||
mod snapshot;
|
||||
pub mod store_client;
|
||||
mod sync_client;
|
||||
|
||||
use std::{ops::Range, path::PathBuf, time::Duration};
|
||||
pub use sync_client::SyncTask;
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
pub use codebase_index::{CodebaseIndex, RetrievalID, SyncProgress};
|
||||
pub use fragment_metadata::{FragmentLocation as FragmentMetadataLocation, FragmentMetadata};
|
||||
pub use merkle_tree::{ContentHash, NodeHash};
|
||||
|
||||
use fragment_metadata::FragmentMetadata;
|
||||
use galaxy_graphql::queries::rerank_fragments::FragmentLocationInput;
|
||||
pub use snapshot::SnapshotStorage;
|
||||
use string_offset::ByteOffset;
|
||||
pub use sync_client::SyncTask;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -87,6 +89,7 @@ pub enum EmbeddingConfig {
|
||||
Voyage3_5_Lite_512,
|
||||
#[default]
|
||||
Voyage3_5_512,
|
||||
Voyage4_512,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -115,6 +118,9 @@ impl From<EmbeddingConfig> for galaxy_graphql::full_source_code_embedding::Embed
|
||||
EmbeddingConfig::Voyage3_5_Lite_512 => {
|
||||
galaxy_graphql::full_source_code_embedding::EmbeddingConfig::Voyage35Lite512
|
||||
}
|
||||
EmbeddingConfig::Voyage4_512 => {
|
||||
warp_graphql::full_source_code_embedding::EmbeddingConfig::Voyage4512
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +144,9 @@ impl TryFrom<galaxy_graphql::full_source_code_embedding::EmbeddingConfig> for Em
|
||||
galaxy_graphql::full_source_code_embedding::EmbeddingConfig::Voyage35512 => {
|
||||
Ok(Self::Voyage3_5_512)
|
||||
}
|
||||
warp_graphql::full_source_code_embedding::EmbeddingConfig::Voyage4512 => {
|
||||
Ok(Self::Voyage4_512)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,6 +170,36 @@ pub struct Fragment {
|
||||
location: FragmentLocation,
|
||||
}
|
||||
|
||||
impl Fragment {
|
||||
pub fn from_byte_range(
|
||||
content: String,
|
||||
content_hash: ContentHash,
|
||||
absolute_path: PathBuf,
|
||||
byte_range: Range<ByteOffset>,
|
||||
) -> Self {
|
||||
Self {
|
||||
content,
|
||||
content_hash,
|
||||
location: FragmentLocation {
|
||||
absolute_path,
|
||||
byte_range,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content_hash(&self) -> &ContentHash {
|
||||
&self.content_hash
|
||||
}
|
||||
|
||||
pub fn absolute_path(&self) -> &Path {
|
||||
&self.location.absolute_path
|
||||
}
|
||||
|
||||
pub fn byte_range(&self) -> Range<ByteOffset> {
|
||||
self.location.byte_range.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Fragment> for galaxy_graphql::full_source_code_embedding::Fragment {
|
||||
fn from(val: Fragment) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -18,6 +18,14 @@ struct QueueEntry {
|
||||
metadata: WorkspaceMetadata,
|
||||
}
|
||||
|
||||
/// Controls whether queued builds may be consumed.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum BuildQueueState {
|
||||
Paused,
|
||||
#[default]
|
||||
Running,
|
||||
}
|
||||
|
||||
impl Hash for QueueEntry {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.metadata.path.hash(state);
|
||||
@@ -35,6 +43,7 @@ impl Eq for QueueEntry {}
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct BuildQueue {
|
||||
queue: PriorityQueue<QueueEntry, Priority>,
|
||||
state: BuildQueueState,
|
||||
}
|
||||
|
||||
impl BuildQueue {
|
||||
@@ -46,7 +55,10 @@ impl BuildQueue {
|
||||
self.queue.iter().map(|(entry, _)| entry.metadata.clone())
|
||||
}
|
||||
|
||||
pub(super) fn new_with_persisted(snapshots_to_load: Vec<WorkspaceMetadata>) -> Self {
|
||||
pub(super) fn new_with_persisted(
|
||||
snapshots_to_load: Vec<WorkspaceMetadata>,
|
||||
start_immediately: bool,
|
||||
) -> Self {
|
||||
let mut queue = PriorityQueue::new();
|
||||
queue.extend(
|
||||
snapshots_to_load
|
||||
@@ -54,12 +66,35 @@ impl BuildQueue {
|
||||
.sorted_by(WorkspaceMetadata::most_recently_touched)
|
||||
.map(|entry| (QueueEntry { metadata: entry }, Priority::PersistedSnapshot)),
|
||||
);
|
||||
let state = if start_immediately {
|
||||
BuildQueueState::Running
|
||||
} else {
|
||||
BuildQueueState::Paused
|
||||
};
|
||||
|
||||
Self { queue }
|
||||
Self { queue, state }
|
||||
}
|
||||
|
||||
pub(super) fn is_running(&self) -> bool {
|
||||
self.state == BuildQueueState::Running
|
||||
}
|
||||
|
||||
/// Starts consuming queued builds. Returns whether the queue transitioned to running.
|
||||
pub(super) fn start(&mut self) -> bool {
|
||||
match self.state {
|
||||
BuildQueueState::Paused => {
|
||||
self.state = BuildQueueState::Running;
|
||||
true
|
||||
}
|
||||
BuildQueueState::Running => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pulls the next index root path to sync from the priority queue and returns it.
|
||||
pub fn pick_next_sync(&mut self) -> Option<WorkspaceMetadata> {
|
||||
if !self.is_running() {
|
||||
return None;
|
||||
}
|
||||
self.queue.pop().map(|(entry, _priority)| entry.metadata)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{ContentHash, Fragment, FragmentLocation, FragmentMetadata};
|
||||
use crate::index::locations::{CodeContextLocation, FileFragmentLocation};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ReadFragmentResult {
|
||||
pub successfully_read: Vec<Fragment>,
|
||||
pub fail_to_read: Vec<ContentHash>,
|
||||
pub fail_to_read_path: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn build_fragments_from_file_contents(
|
||||
metadatas: impl IntoIterator<Item = (ContentHash, FragmentMetadata)>,
|
||||
file_contents: &HashMap<PathBuf, String>,
|
||||
) -> ReadFragmentResult {
|
||||
let mut fragments = Vec::new();
|
||||
let mut fail_to_read = Vec::new();
|
||||
let mut fail_to_read_path = Vec::new();
|
||||
|
||||
// Group fragments by file path.
|
||||
let mut fragments_by_path: HashMap<_, Vec<_>> = HashMap::new();
|
||||
for (content_hash, metadata) in metadatas {
|
||||
fragments_by_path
|
||||
.entry(metadata.absolute_path)
|
||||
.or_default()
|
||||
.push((content_hash, metadata.location.byte_range));
|
||||
}
|
||||
|
||||
// Process each file and its fragments.
|
||||
for (file_path, file_fragments) in fragments_by_path {
|
||||
let mut has_failed_to_read_fragments = false;
|
||||
if let Some(file_content) = file_contents.get(&file_path) {
|
||||
// Process all fragments for this file.
|
||||
for (content_hash, fragment_ranges) in file_fragments {
|
||||
let start_idx = fragment_ranges.start.as_usize();
|
||||
let end_idx = fragment_ranges.end.as_usize();
|
||||
|
||||
if start_idx <= end_idx
|
||||
&& end_idx <= file_content.len()
|
||||
&& file_content.is_char_boundary(start_idx)
|
||||
&& file_content.is_char_boundary(end_idx)
|
||||
{
|
||||
let content = file_content[start_idx..end_idx].to_string();
|
||||
if content.is_empty() {
|
||||
log::trace!(
|
||||
"Fragment for {:?} with range {:?} is empty",
|
||||
file_path.display(),
|
||||
fragment_ranges
|
||||
);
|
||||
fail_to_read.push(content_hash);
|
||||
has_failed_to_read_fragments = true;
|
||||
} else if ContentHash::from_content(&content) != content_hash {
|
||||
log::trace!(
|
||||
"Fragment for {:?} with range {:?} does not match its content hash",
|
||||
file_path.display(),
|
||||
fragment_ranges
|
||||
);
|
||||
fail_to_read.push(content_hash);
|
||||
has_failed_to_read_fragments = true;
|
||||
} else {
|
||||
fragments.push(Fragment {
|
||||
content,
|
||||
content_hash,
|
||||
location: FragmentLocation {
|
||||
absolute_path: file_path.clone(),
|
||||
byte_range: fragment_ranges,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log::trace!("Invalid byte range {fragment_ranges:?} for file: {file_path:?}");
|
||||
fail_to_read.push(content_hash);
|
||||
has_failed_to_read_fragments = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::trace!("Failed to read file: {file_path:?}");
|
||||
fail_to_read.extend(
|
||||
file_fragments
|
||||
.into_iter()
|
||||
.map(|(content_hash, _)| content_hash),
|
||||
);
|
||||
has_failed_to_read_fragments = true;
|
||||
}
|
||||
|
||||
if has_failed_to_read_fragments {
|
||||
fail_to_read_path.push(file_path);
|
||||
}
|
||||
}
|
||||
|
||||
ReadFragmentResult {
|
||||
successfully_read: fragments,
|
||||
fail_to_read,
|
||||
fail_to_read_path,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert fragments into CodeContextLocations. This function groups and dedupes fragments in the same file.
|
||||
// It also allows the caller to define a context line number surrounding the relevant fragment.
|
||||
pub fn fragments_to_context_locations<'a>(
|
||||
fragments: Vec<Fragment>,
|
||||
metadata_for_hash: impl Fn(&ContentHash) -> Option<&'a [FragmentMetadata]>,
|
||||
context_lines: usize,
|
||||
) -> HashSet<CodeContextLocation> {
|
||||
// Map to collect fragments by file path.
|
||||
let mut fragments_by_path: HashMap<&PathBuf, Vec<Range<usize>>> = HashMap::new();
|
||||
let mut whole_files = HashSet::new();
|
||||
|
||||
// First pass - collect all fragments and their line ranges by file path.
|
||||
for fragment in &fragments {
|
||||
if let Some(metadata) = metadata_for_hash(&fragment.content_hash).and_then(|metadatas| {
|
||||
metadatas.iter().find(|m| {
|
||||
m.absolute_path == fragment.location.absolute_path
|
||||
&& m.location.byte_range == fragment.location.byte_range
|
||||
})
|
||||
}) {
|
||||
// Add line range with context to the appropriate file's collection.
|
||||
let path = &fragment.location.absolute_path;
|
||||
let start = metadata.location.start_line.saturating_sub(context_lines);
|
||||
let end = metadata.location.end_line + 1 + context_lines;
|
||||
|
||||
fragments_by_path.entry(path).or_default().push(start..end);
|
||||
} else {
|
||||
// Fallback to whole file if metadata not found.
|
||||
whole_files.insert(fragment.location.absolute_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass - process each file's fragments.
|
||||
let mut result = HashSet::new();
|
||||
|
||||
// Process each file's fragments.
|
||||
for (path, mut line_ranges) in fragments_by_path {
|
||||
if line_ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We can skip the fragments if the entire file is already included in the context.
|
||||
if whole_files.contains(path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sort ranges by start position.
|
||||
line_ranges.sort_by_key(|range| range.start);
|
||||
|
||||
// Merge overlapping or adjacent ranges.
|
||||
let mut merged_ranges: Vec<Range<usize>> = Vec::new();
|
||||
for range in line_ranges {
|
||||
if let Some(last) = merged_ranges.last_mut() {
|
||||
// If current range overlaps or is adjacent to the last one, merge them.
|
||||
if range.start <= last.end {
|
||||
last.end = last.end.max(range.end);
|
||||
} else {
|
||||
merged_ranges.push(range);
|
||||
}
|
||||
} else {
|
||||
merged_ranges.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
// Add file fragment location with all merged ranges.
|
||||
result.insert(CodeContextLocation::Fragment(FileFragmentLocation {
|
||||
path: path.clone(),
|
||||
line_ranges: merged_ranges,
|
||||
}));
|
||||
}
|
||||
|
||||
// Add whole files to the result set.
|
||||
result.extend(whole_files.into_iter().map(CodeContextLocation::WholeFile));
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "search_shaping_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,133 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use string_offset::ByteOffset;
|
||||
|
||||
use super::super::{ContentHash, Fragment, FragmentLocation, FragmentMetadata};
|
||||
use super::{build_fragments_from_file_contents, fragments_to_context_locations};
|
||||
use crate::index::locations::{CodeContextLocation, FileFragmentLocation};
|
||||
|
||||
fn metadata(
|
||||
path: &str,
|
||||
byte_range: Range<ByteOffset>,
|
||||
start_line: usize,
|
||||
end_line: usize,
|
||||
) -> FragmentMetadata {
|
||||
FragmentMetadata {
|
||||
absolute_path: PathBuf::from(path),
|
||||
location: super::super::fragment_metadata::FragmentLocation {
|
||||
start_line,
|
||||
end_line,
|
||||
byte_range,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn fragment(content: &str, path: &str, byte_range: Range<ByteOffset>) -> Fragment {
|
||||
Fragment {
|
||||
content: content.to_string(),
|
||||
content_hash: ContentHash::from_content(content),
|
||||
location: FragmentLocation {
|
||||
absolute_path: PathBuf::from(path),
|
||||
byte_range,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_fragments_from_exact_byte_ranges() {
|
||||
let path = PathBuf::from("/repo/src/lib.rs");
|
||||
let content = "before\nneedle\nπ-after".to_string();
|
||||
let fragment_content = "needle";
|
||||
let start = content.find(fragment_content).unwrap();
|
||||
let end = start + fragment_content.len();
|
||||
let content_hash = ContentHash::from_content(fragment_content);
|
||||
let metadata = metadata(
|
||||
path.to_string_lossy().as_ref(),
|
||||
ByteOffset::from(start)..ByteOffset::from(end),
|
||||
2,
|
||||
2,
|
||||
);
|
||||
|
||||
let result = build_fragments_from_file_contents(
|
||||
[(content_hash.clone(), metadata)],
|
||||
&HashMap::from([(path.clone(), content)]),
|
||||
);
|
||||
|
||||
assert_eq!(result.fail_to_read.len(), 0);
|
||||
assert_eq!(result.successfully_read.len(), 1);
|
||||
let fragment = &result.successfully_read[0];
|
||||
assert_eq!(fragment.content, fragment_content);
|
||||
assert_eq!(fragment.content_hash, content_hash);
|
||||
assert_eq!(fragment.location.absolute_path, path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_hashes_and_byte_ranges() {
|
||||
let path = PathBuf::from("/repo/src/lib.rs");
|
||||
let content = "abcπdef".to_string();
|
||||
let bad_hash_metadata = metadata(
|
||||
path.to_string_lossy().as_ref(),
|
||||
ByteOffset::from(0)..ByteOffset::from(3),
|
||||
1,
|
||||
1,
|
||||
);
|
||||
let invalid_boundary_metadata = metadata(
|
||||
path.to_string_lossy().as_ref(),
|
||||
ByteOffset::from(4)..ByteOffset::from(5),
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
let result = build_fragments_from_file_contents(
|
||||
[
|
||||
(ContentHash::from_content("not abc"), bad_hash_metadata),
|
||||
(ContentHash::from_content("π"), invalid_boundary_metadata),
|
||||
],
|
||||
&HashMap::from([(path.clone(), content)]),
|
||||
);
|
||||
|
||||
assert!(result.successfully_read.is_empty());
|
||||
assert_eq!(result.fail_to_read.len(), 2);
|
||||
assert_eq!(result.fail_to_read_path, vec![path]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shapes_fragments_into_merged_context_locations() {
|
||||
let path = "/repo/src/lib.rs";
|
||||
let fragment_a = fragment("a", path, ByteOffset::from(0)..ByteOffset::from(1));
|
||||
let fragment_b = fragment("b", path, ByteOffset::from(2)..ByteOffset::from(3));
|
||||
let metadata_a = metadata(path, ByteOffset::from(0)..ByteOffset::from(1), 10, 12);
|
||||
let metadata_b = metadata(path, ByteOffset::from(2)..ByteOffset::from(3), 15, 17);
|
||||
let metadata_by_hash = HashMap::from([
|
||||
(fragment_a.content_hash.clone(), vec![metadata_a]),
|
||||
(fragment_b.content_hash.clone(), vec![metadata_b]),
|
||||
]);
|
||||
|
||||
let result = fragments_to_context_locations(
|
||||
vec![fragment_a, fragment_b],
|
||||
|hash| metadata_by_hash.get(hash).map(Vec::as_slice),
|
||||
2,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
HashSet::from([CodeContextLocation::Fragment(FileFragmentLocation {
|
||||
path: PathBuf::from(path),
|
||||
line_ranges: std::iter::once(8..20).collect(),
|
||||
})])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falls_back_to_whole_file_when_metadata_is_missing() {
|
||||
let path = "/repo/src/lib.rs";
|
||||
let fragment = fragment("a", path, ByteOffset::from(0)..ByteOffset::from(1));
|
||||
let result = fragments_to_context_locations(vec![fragment], |_| None, 2);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
HashSet::from([CodeContextLocation::WholeFile(PathBuf::from(path))])
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use galaxyui::ModelHandle;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use repo_metadata::Repository;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use galaxyui_core::ModelHandle;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "local_fs")] {
|
||||
use super::Error as CodebaseIndexError;
|
||||
use std::sync::Arc;
|
||||
use galaxyui::ModelContext;
|
||||
use galaxyui_core::ModelContext;
|
||||
use anyhow::Context;
|
||||
use galaxy_core::safe_info;
|
||||
use super::{store_client::StoreClient, CodebaseIndex, EmbeddingConfig};
|
||||
@@ -33,11 +34,47 @@ const REPO_SNAPSHOT_SHELF_LIFE_DURATION: Duration =
|
||||
|
||||
/// Subdirectory inside the app's statedirectory that holds snapshot files.
|
||||
const REPO_SNAPSHOT_SUBDIR_NAME: &str = "codebase_index_snapshots";
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SnapshotStorage {
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
impl SnapshotStorage {
|
||||
/// Construct snapshot storage using the app's default secure snapshot directory.
|
||||
pub fn app_default() -> Option<Self> {
|
||||
snapshot_dir().map(|dir| Self { dir })
|
||||
}
|
||||
|
||||
/// Construct snapshot storage rooted at the supplied directory, creating it if needed.
|
||||
pub fn from_dir(dir: PathBuf) -> Option<Self> {
|
||||
if !dir.is_dir() {
|
||||
std::fs::create_dir_all(&dir).ok()?;
|
||||
}
|
||||
Some(Self { dir })
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.dir
|
||||
}
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub(super) fn is_app_default(&self) -> bool {
|
||||
self.dir == default_snapshot_dir_path()
|
||||
}
|
||||
|
||||
pub(super) fn has_snapshot(&self, repo_path: &Path) -> bool {
|
||||
self.snapshot_path(repo_path).is_file()
|
||||
}
|
||||
|
||||
pub(super) fn snapshot_path(&self, repo_path: &Path) -> PathBuf {
|
||||
snapshot_path(&self.dir, repo_path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits a list of codebase indices into invalid and valid indices,
|
||||
/// based on their last write date and whether they have a corresponding snapshot file.
|
||||
pub(super) fn split_snapshot_metadata_by_validity(
|
||||
persisted_codebase_indices: Vec<WorkspaceMetadata>,
|
||||
snapshot_storage: Option<&SnapshotStorage>,
|
||||
) -> (Vec<WorkspaceMetadata>, Vec<WorkspaceMetadata>) {
|
||||
let now = Utc::now();
|
||||
persisted_codebase_indices
|
||||
@@ -48,7 +85,8 @@ pub(super) fn split_snapshot_metadata_by_validity(
|
||||
index_metadata.path
|
||||
);
|
||||
index_metadata.is_expired(now, REPO_SNAPSHOT_SHELF_LIFE_DAYS)
|
||||
|| !has_snapshot(&index_metadata.path)
|
||||
|| !snapshot_storage
|
||||
.is_some_and(|storage| storage.has_snapshot(&index_metadata.path))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -159,12 +197,7 @@ pub(super) fn read_snapshot(
|
||||
}
|
||||
|
||||
pub(super) fn has_snapshot(repo_path: &Path) -> bool {
|
||||
let Some(snapshot_dir) = snapshot_dir() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let snapshot_path = snapshot_path(snapshot_dir.as_path(), repo_path);
|
||||
snapshot_path.is_file()
|
||||
SnapshotStorage::app_default().is_some_and(|storage| storage.has_snapshot(repo_path))
|
||||
}
|
||||
|
||||
/// Construct a directory to store index snapshots, if it doesn't already exist,
|
||||
@@ -175,9 +208,7 @@ pub(super) fn snapshot_dir() -> Option<PathBuf> {
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
{
|
||||
let base_dir =
|
||||
galaxy_core::paths::secure_state_dir().unwrap_or_else(galaxy_core::paths::state_dir);
|
||||
let snapshot_dir_path = base_dir.join(REPO_SNAPSHOT_SUBDIR_NAME);
|
||||
let snapshot_dir_path = default_snapshot_dir_path();
|
||||
|
||||
if !snapshot_dir_path.is_dir() {
|
||||
std::fs::create_dir_all(&snapshot_dir_path).ok()?;
|
||||
@@ -186,9 +217,15 @@ pub(super) fn snapshot_dir() -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn default_snapshot_dir_path() -> PathBuf {
|
||||
galaxy_core::paths::secure_state_dir()
|
||||
.unwrap_or_else(galaxy_core::paths::state_dir)
|
||||
.join(REPO_SNAPSHOT_SUBDIR_NAME)
|
||||
}
|
||||
/// Constructs a snapshot path given a base directory and the codebase index's root path.
|
||||
pub(super) fn snapshot_path(snapshot_dir: &Path, repo_path: &Path) -> PathBuf {
|
||||
// Use a hash the repo_path to create a unique filename
|
||||
// Use a hash of the repo_path to create a unique filename
|
||||
let mut hasher = DefaultHasher::new();
|
||||
repo_path.hash(&mut hasher);
|
||||
let snapshot_file_name = format!("snapshot_{}", hasher.finish());
|
||||
|
||||
@@ -1,7 +1,57 @@
|
||||
use chrono::Duration;
|
||||
use chrono::{Duration, Utc};
|
||||
use virtual_fs::{Stub, VirtualFS};
|
||||
|
||||
use super::*;
|
||||
fn workspace_metadata(path: impl Into<PathBuf>) -> WorkspaceMetadata {
|
||||
WorkspaceMetadata {
|
||||
path: path.into(),
|
||||
navigated_ts: None,
|
||||
modified_ts: Some(Utc::now()),
|
||||
queried_ts: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn snapshot_storage_app_default_matches_snapshot_dir() {
|
||||
VirtualFS::test(
|
||||
"snapshot_storage_app_default_matches_snapshot_dir",
|
||||
|_dirs, _sandbox| {
|
||||
let storage = SnapshotStorage::app_default().unwrap();
|
||||
assert_eq!(storage.path(), snapshot_dir().unwrap());
|
||||
assert!(storage.is_app_default());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_snapshot_metadata_by_validity_uses_injected_snapshot_dir() {
|
||||
let snapshot_dir = tempfile::tempdir().unwrap();
|
||||
let storage = SnapshotStorage::from_dir(snapshot_dir.path().join("daemon")).unwrap();
|
||||
let repo_path = PathBuf::from("/remote/repo");
|
||||
std::fs::write(storage.snapshot_path(&repo_path), b"snapshot").unwrap();
|
||||
|
||||
let (invalid_metadata, valid_metadata) =
|
||||
split_snapshot_metadata_by_validity(vec![workspace_metadata(&repo_path)], Some(&storage));
|
||||
|
||||
assert!(invalid_metadata.is_empty());
|
||||
assert_eq!(valid_metadata.len(), 1);
|
||||
assert_eq!(valid_metadata[0].path, repo_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_snapshot_metadata_by_validity_rejects_missing_injected_snapshot() {
|
||||
let snapshot_dir = tempfile::tempdir().unwrap();
|
||||
let storage = SnapshotStorage::from_dir(snapshot_dir.path().join("daemon")).unwrap();
|
||||
let repo_path = PathBuf::from("/remote/repo");
|
||||
|
||||
let (invalid_metadata, valid_metadata) =
|
||||
split_snapshot_metadata_by_validity(vec![workspace_metadata(&repo_path)], Some(&storage));
|
||||
|
||||
assert_eq!(invalid_metadata.len(), 1);
|
||||
assert_eq!(invalid_metadata[0].path, repo_path);
|
||||
assert!(valid_metadata.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_up_snapshot_files() {
|
||||
@@ -71,12 +121,7 @@ fn test_clean_up_snapshot_files() {
|
||||
.unwrap();
|
||||
|
||||
// Create test metadata that only includes the valid file
|
||||
let metadata = vec![WorkspaceMetadata {
|
||||
path: test_path,
|
||||
navigated_ts: None,
|
||||
modified_ts: None,
|
||||
queried_ts: None,
|
||||
}];
|
||||
let metadata = vec![workspace_metadata(test_path)];
|
||||
|
||||
// Run cleanup
|
||||
clean_up_snapshot_files(&snapshot_dir_absolute_path, &metadata);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Debug;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::{
|
||||
CodebaseContextConfig, ContentHash, EmbeddingConfig, Error, Fragment, NodeHash, RepoMetadata,
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use galaxy_core::sync_queue::{IsTransientError, SyncQueue, SyncQueueTaskTrait};
|
||||
use itertools::Itertools;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::mem;
|
||||
use std::ops::AddAssign;
|
||||
use std::pin::Pin;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
mem,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{CodebaseContextConfig, NodeHash};
|
||||
|
||||
use crate::index::full_source_code_embedding::store_client::IntermediateNode;
|
||||
use anyhow::{anyhow, Result};
|
||||
use itertools::Itertools;
|
||||
use galaxy_core::sync_queue::{IsTransientError, SyncQueue, SyncQueueTaskTrait};
|
||||
|
||||
use super::changed_files::ChangedFiles;
|
||||
use super::codebase_index::{build_fragments_from_metadata, SyncProgress};
|
||||
use super::fragment_metadata::LeafToFragmentMetadataMapping;
|
||||
use super::merkle_tree::{MerkleTree, NodeLens};
|
||||
use super::store_client::StoreClient;
|
||||
use super::{
|
||||
changed_files::ChangedFiles,
|
||||
codebase_index::{build_fragments_from_metadata, SyncProgress},
|
||||
fragment_metadata::LeafToFragmentMetadataMapping,
|
||||
merkle_tree::{MerkleTree, NodeLens},
|
||||
store_client::StoreClient,
|
||||
EmbeddingConfig, Error, RepoMetadata,
|
||||
CodebaseContextConfig, ContentHash, EmbeddingConfig, Error, Fragment, NodeHash, RepoMetadata,
|
||||
};
|
||||
use super::{ContentHash, Fragment};
|
||||
use crate::index::full_source_code_embedding::store_client::IntermediateNode;
|
||||
|
||||
const SYNC_NODE_BATCH_SIZE: usize = 500;
|
||||
// Minimum node batch size used for updates.
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use futures::executor::block_on;
|
||||
use virtual_fs::VirtualFS;
|
||||
|
||||
use super::batch_leaves_by_size;
|
||||
use crate::index::full_source_code_embedding::merkle_tree::{construct_test_merkle_tree, NodeLens};
|
||||
|
||||
use virtual_fs::VirtualFS;
|
||||
|
||||
/// Collect all leaf nodes from a merkle tree by walking it recursively.
|
||||
fn collect_leaves<'a>(node: NodeLens<'a>) -> Vec<NodeLens<'a>> {
|
||||
if node.is_leaf() {
|
||||
|
||||
Reference in New Issue
Block a user