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
22 changes: 12 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ rustix = { version = "0.38", features = ["process"] }
clap = { version = "4.5", features = ["derive"] }
clap_complete = "4.5"

# Progress bars; major pinned to the one self_update resolves to avoid a duplicate copy.
indicatif = { version = "0.17", features = ["rayon"] }

# JSONC config editing (preserve comments + key order on agent config writes)
jsonc-parser = { version = "0.26", features = ["cst", "serde"] }

Expand Down
1 change: 1 addition & 0 deletions crates/codegraph-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ toml = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true }
clap_complete = { workspace = true }
indicatif = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
time = { workspace = true }
Expand Down
70 changes: 66 additions & 4 deletions crates/codegraph-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use std::collections::{HashMap, HashSet, VecDeque};
use std::fs;
use std::io::{self, BufRead, BufReader, Write};
use std::io::{self, BufRead, BufReader, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

Expand All @@ -24,6 +24,7 @@ use codegraph_mcp::McpServer;
use codegraph_resolve::ReferenceResolver;
use codegraph_store::queries::SearchResult;
use codegraph_store::Store;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use serde::Serialize;
use serde_json::json;
use time::format_description::well_known::Rfc3339;
Expand Down Expand Up @@ -604,7 +605,7 @@ fn cmd_index(path: Option<PathBuf>, force: bool, quiet: bool, verbose: bool) ->
if force {
remove_db_files(&project)?;
}
let result = index_project(&project, true, verbose)?;
let result = index_project_inner(&project, true, verbose, quiet)?;
if !quiet {
print_index_result(&result);
}
Expand All @@ -620,7 +621,22 @@ fn cmd_sync(path: Option<PathBuf>, quiet: bool) -> Result<()> {
// sync_project_once self-discovers candidate files via scan_project, so it works
// for a cold CLI invocation with no daemon. Hash-gated skip + per-file delete/reinsert
// + full re-resolve makes the result equivalent to `index --force`.
let outcome = codegraph_watch::sync_project_once(&project)?;
if !quiet {
eprintln!("Scanning files…");
}
let bar = spinner(
quiet,
"{spinner:.green} Syncing {pos}/{len} files ({elapsed})",
);
let mut bar_len_set = false;
let outcome = codegraph_watch::sync_project_once_with_progress(&project, |done, total| {
if !bar_len_set {
bar.set_length(total as u64);
bar_len_set = true;
}
bar.set_position(done as u64);
})?;
bar.finish_and_clear();
if !quiet {
println!(
"Synced: {} reindexed, {} skipped (unchanged), {} removed in {}",
Expand Down Expand Up @@ -1025,7 +1041,43 @@ struct IndexSummary {
duration_ms: i64,
}

// Progress is a pure side effect: it only counts/displays and never gates,
// reorders, or alters extraction, so golden byte-equivalence is preserved. It
// draws to stderr (stdout carries JSON / golden output) and is hidden when
// stderr is not a TTY or `--quiet`, so CI logs and pipes stay clean.
fn progress_bar(len: u64, quiet: bool, template: &str) -> ProgressBar {
if quiet || !io::stderr().is_terminal() {
return ProgressBar::hidden();
}
let bar = ProgressBar::with_draw_target(Some(len), ProgressDrawTarget::stderr());
if let Ok(style) = ProgressStyle::with_template(template) {
bar.set_style(style.progress_chars("=>-"));
}
bar
}

fn spinner(quiet: bool, template: &str) -> ProgressBar {
if quiet || !io::stderr().is_terminal() {
return ProgressBar::hidden();
}
let bar = ProgressBar::with_draw_target(None, ProgressDrawTarget::stderr());
if let Ok(style) = ProgressStyle::with_template(template) {
bar.set_style(style);
}
bar.enable_steady_tick(std::time::Duration::from_millis(100));
bar
}

fn index_project(project: &Path, clear_first: bool, verbose: bool) -> Result<IndexSummary> {
index_project_inner(project, clear_first, verbose, false)
}

fn index_project_inner(
project: &Path,
clear_first: bool,
verbose: bool,
quiet: bool,
) -> Result<IndexSummary> {
let started = std::time::Instant::now();
if clear_first {
remove_db_files(project)?;
Expand All @@ -1037,6 +1089,9 @@ fn index_project(project: &Path, clear_first: bool, verbose: bool) -> Result<Ind
ignore_dirs: config.indexing.ignore_dirs.clone(),
parallel: true,
};
if !quiet {
eprintln!("Scanning files…");
}
let files = codegraph_extract::engine::scan_project(project, &options)?;
let mut store = open_store(project)?;
let before = store.counts()?;
Expand Down Expand Up @@ -1064,9 +1119,14 @@ fn index_project(project: &Path, clear_first: bool, verbose: bool) -> Result<Ind
let mut spill = SpillWriter::new(codegraph_dir(project))?;
let mut pending_nodes: Vec<Node> = Vec::with_capacity(NODE_FLUSH_ROWS);

let bar = progress_bar(
files.len() as u64,
quiet,
"{spinner:.green} Indexing [{bar:30}] {pos}/{len} files ({elapsed}) {wide_msg}",
);
for relative in files {
if verbose {
println!("indexing {relative}");
bar.set_message(relative.clone());
}
let full = project.join(&relative);
let source = fs::read_to_string(&full)
Expand Down Expand Up @@ -1107,7 +1167,9 @@ fn index_project(project: &Path, clear_first: bool, verbose: bool) -> Result<Ind

spill.write_edges(&result.edges)?;
spill.write_refs(&result.unresolved_references)?;
bar.inc(1);
}
bar.finish_and_clear();

if !pending_nodes.is_empty() {
store.upsert_nodes(&pending_nodes)?;
Expand Down
30 changes: 24 additions & 6 deletions crates/codegraph-mcp/tests/golden_mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,27 @@ use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, MutexGuard};

use codegraph_mcp::McpServer;
use serde_json::{json, Value};

static TEMP_SEQ: AtomicU64 = AtomicU64::new(0);

/// Serializes every test that is sensitive to the PROCESS-GLOBAL
/// `CODEGRAPH_MCP_TOOLS` env var (read in `schemas.rs`). cargo runs tests
/// multi-threaded in ONE process, so the allowlist test's `set_var` would
/// otherwise race the default-surface readers (which assert the 4-tool
/// surface) and intermittently observe the 2-tool allowlist instead. Every
/// such test acquires this SHARED lock for the env-set→read window.
static ENV_LOCK: Mutex<()> = Mutex::new(());

/// Acquire [`ENV_LOCK`], recovering from poisoning so one failing test does not
/// cascade-poison the rest of the suite.
fn lock_env() -> MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}

fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
Expand Down Expand Up @@ -188,6 +203,7 @@ fn initialize_matches_golden() {

#[test]
fn tools_list_matches_upstream_names_and_schemas() {
let _env = lock_env();
let project = setup_mini_project();
let (req, golden_resp) = load_golden("tools_list");
let resp = roundtrip(project.path(), req);
Expand Down Expand Up @@ -252,6 +268,7 @@ fn default_project_indexed_serves_full_tools_list_after_initialize() {
// the server with the agent's project root as cwd and no projectPath, so the
// CLI must default the project to that indexed root. With a Some(indexed)
// default_project, the initialize->tools/list handshake must expose 4 tools.
let _env = lock_env();
let project = index_fixture(&[(
"src/app.ts",
"export function greet(name: string): string {\n return `hi ${name}`;\n}\n",
Expand Down Expand Up @@ -322,14 +339,15 @@ fn default_project_unindexed_serves_empty_tools_list() {
#[test]
fn tools_list_honors_codegraph_mcp_tools_allowlist() {
// CODEGRAPH_MCP_TOOLS replaces the default surface with exactly the named
// tools — any of the 8 (tools.ts:711-740). Serialized to avoid env-var
// races with other tests in the same process.
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
let _guard = ENV_LOCK.lock().unwrap();
// tools — any of the 8 (tools.ts:711-740). Serialized via the shared
// ENV_LOCK to avoid env-var races with the default-surface readers in the
// same process.
let _env = lock_env();

let project = setup_mini_project();
// SAFETY: single-threaded test section guarded by ENV_LOCK.
// SAFETY: single-threaded test section guarded by ENV_LOCK; remove_var runs
// before any assertion that could panic, so a failed assert never leaks the
// var to other tests.
unsafe { std::env::set_var("CODEGRAPH_MCP_TOOLS", "impact,files") };
let resp = roundtrip(
project.path(),
Expand Down
4 changes: 3 additions & 1 deletion crates/codegraph-watch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ pub use git::{
GitHookName, GitHookResult, DEFAULT_SYNC_HOOKS,
};
pub use policy::{watch_disabled_reason, WatchPolicy, CODEGRAPH_NO_WATCH};
pub use sync::{sync_changed_paths, sync_project_once, SyncOutcome};
pub use sync::{
sync_changed_paths, sync_project_once, sync_project_once_with_progress, SyncOutcome,
};
pub use watcher::{start_serve_watcher, PendingFile, ProjectWatcher, WatchOptions};
pub use worktree::{
detect_worktree_index_mismatch, git_worktree_root, worktree_mismatch_notice,
Expand Down
Loading