Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 33 additions & 25 deletions src/commands/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,22 +118,8 @@ pub async fn execute_scan(

// 1b. Incremental: filter out unchanged files
if opts.incremental {
let cache = ScanCache::load()?;
let before_count = files.len();
let root_abs = root.canonicalize().unwrap_or_else(|_| root.clone());
files.retain(|f| {
let abs_path = root_abs.join(&f.path);
let Some(hash) = file_content_hash(&abs_path) else {
return true; // can't read file, rescan it
};
match cache.get(&root_abs, &f.path) {
Some(cached_hash) if cached_hash == hash => {
debug!(file = %f.path, "skipping unchanged file (incremental)");
false
}
_ => true,
}
});
retain_changed(&root, &mut files)?;
let skipped = before_count - files.len();
if skipped > 0 {
eprintln!(
Expand Down Expand Up @@ -276,16 +262,7 @@ pub async fn execute_scan(

// 6. Save scan cache for incremental mode
if opts.incremental {
let root_abs = root.canonicalize().unwrap_or_else(|_| root.clone());
let mut cache = ScanCache::load().unwrap_or_default();
for f in &files {
let abs_path = root_abs.join(&f.path);
let Some(hash) = file_content_hash(&abs_path) else {
continue; // can't read file, skip cache entry
};
cache.set(&root_abs, &f.path, &hash);
}
cache.save()?;
record_scanned(&root, &files)?;
debug!(cached = files.len(), "saved scan cache");
}

Expand Down Expand Up @@ -435,6 +412,37 @@ pub(crate) fn finalize_issues(
(report.issues, report.dropped)
}

/// Incremental mode: drop files whose content hash matches the scan cache.
pub(crate) fn retain_changed(root: &std::path::Path, files: &mut Vec<FileEntry>) -> Result<()> {
let cache = ScanCache::load()?;
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
files.retain(|f| {
let Some(hash) = file_content_hash(&root_abs.join(&f.path)) else {
return true; // can't read file, rescan it
};
match cache.get(&root_abs, &f.path) {
Some(cached_hash) if cached_hash == hash => {
debug!(file = %f.path, "skipping unchanged file (incremental)");
false
}
_ => true,
}
});
Ok(())
}

/// Incremental mode: remember the hashes of the files just scanned.
pub(crate) fn record_scanned(root: &std::path::Path, files: &[FileEntry]) -> Result<()> {
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let mut cache = ScanCache::load().unwrap_or_default();
for f in files {
if let Some(hash) = file_content_hash(&root_abs.join(&f.path)) {
cache.set(&root_abs, &f.path, &hash);
}
}
cache.save()
}

/// Compute a short SHA256 hash of a file's content for incremental scanning.
/// Returns None if the file cannot be read (caller should rescan it).
#[allow(clippy::format_collect)]
Expand Down
19 changes: 19 additions & 0 deletions src/engine/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,25 @@ pub fn walk_project(
Ok(entries)
}

/// Extensions that are program code (the rest of the defaults are docs/data).
const SOURCE_EXTENSIONS: &[&str] = &[
"rs", "py", "js", "ts", "tsx", "jsx", "go", "java", "kt", "rb", "c", "cpp", "h", "hpp", "cs",
"php", "swift", "scala", "vue", "svelte", "sh", "bash", "zsh", "ps1", "sql",
];

/// Value-first order: source files before docs/data/lockfiles, then shallower
/// directories first, then by path so the result is deterministic.
pub fn order_source_first(files: &mut [FileEntry]) {
files.sort_by_cached_key(|f| {
let p = Path::new(&f.path);
let is_source = p
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| SOURCE_EXTENSIONS.contains(&e.to_lowercase().as_str()));
(!is_source, p.components().count(), f.path.clone())
});
}

/// Split files into batches to avoid exceeding LLM token limits.
pub fn batch_files(files: &[FileEntry], max_chars: usize, max_files: usize) -> Vec<Vec<FileEntry>> {
let mut batches = Vec::new();
Expand Down
Loading
Loading