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
1 change: 1 addition & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Add symbol-aware reference lookups to codesearch via `find_impact` MCP tool. Ret
- **`-with-csharp` release variants** — 6 release archives (3 plain + 3 with helper)
- **Gated integration test** — `csharp_helper_integration` cargo feature for full-pipeline testing
- **CI** — separate `csharp-integration-tests` job in `.github/workflows/ci.yml`
- **Stale-path resilience + derived alias** — moved/renamed indexed folders no longer crash serve: `git_remote` captured at registration, `reconcile_all_paths()` best-effort relocates by matching `remote.origin.url` (bounded depth, `CODESEARCH_RELOCATE_MAX_DEPTH`, default 3) else warn+skip; `codesearch index prune` for manual cleanup. The `--alias` flag was removed (alias always = directory name). `ReposConfig::reconcile()` hardens hand-edited `repos.json` on load. See AGENTS.md for details.

## Architecture

Expand Down
6 changes: 4 additions & 2 deletions .claude/commands/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ release — tagging happens only in `/release`.
`PR=$(gh pr view --json number --jq .number)`.

8. **Auto-merge after CI**
- `gh pr merge "$PR" --auto --merge` so the PR lands automatically once required checks pass.
- This repo **disallows merge commits** — always use `--squash`. NEVER `--merge`
(it fails with "Merge commits are not allowed on this repository").
- `gh pr merge "$PR" --auto --squash` so the PR lands automatically once required checks pass.
- If auto-merge is not enabled on the repo (command errors), fall back: poll
`gh pr checks "$PR" --watch`, then `gh pr merge "$PR" --merge` once green.
`gh pr checks "$PR" --watch`, then `gh pr merge "$PR" --squash` once green.

## Report
Branch, pending release version, doc updates made, PR URL, and merge status
Expand Down
5 changes: 3 additions & 2 deletions .claude/commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ to `develop`). Then **wait for the develop PR to actually merge** (auto-merge wa
matching history (e.g. `Release v1.0.142 — serve responsive during warmup`).
- Body ends with: `🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
- Capture the PR number: `RELEASE_PR=$(gh pr view develop --json number --jq .number)`.
4. `gh pr merge "$RELEASE_PR" --auto --merge`. Wait until `state` is
4. This repo **disallows merge commits** — always use `--squash`, never `--merge`.
`gh pr merge "$RELEASE_PR" --auto --squash`. Wait until `state` is
`MERGED` (poll as in Part 1). If auto-merge is unavailable, `gh pr checks "$RELEASE_PR" --watch`
then `gh pr merge "$RELEASE_PR" --merge`. If CI fails, STOP.
then `gh pr merge "$RELEASE_PR" --squash`. If CI fails, STOP.

## Part 3 — tag the release
1. `git fetch origin --tags && git checkout master && git pull --ff-only origin master`.
Expand Down
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ Add symbol-aware reference lookups to codesearch via `find_impact` MCP tool. Ret
- **Gated integration test** — `csharp_helper_integration` cargo feature for full-pipeline testing
- **CI** — separate `csharp-integration-tests` job in `.github/workflows/ci.yml`
- **Sequential phase-2 startup** — Phase 1 warms repos sequentially, Phase 2 runs gated C# SCIP rebuilds ordered by `last_changed_unix` under `Semaphore(concurrency)` via `CSHARP_SCIP_CONCURRENCY` env (default **2**, clamp [1,4])
- **`repos_meta` tracking** — `RepoMeta` (last_changed_unix, last_scip_indexed_unix) persisted in `repos.json` with debounced save (10s window)
- **`repos_meta` tracking** — `RepoMeta` (last_changed_unix, last_scip_indexed_unix, git_remote) persisted in `repos.json` with debounced save (10s window)
- **Stale-path resilience** — a renamed/moved indexed folder no longer crashes serve. `git_remote` (`remote.origin.url`) is captured at registration; on startup `ServeState::reconcile_all_paths()` best-effort relocates a missing repo by scanning the nearest existing ancestor (bounded depth, env `CODESEARCH_RELOCATE_MAX_DEPTH`, default 3) for a git root with a matching remote — exactly one match rewrites `repos.json`, otherwise warn + skip. Phase-2/Phase-3 also guard `path.exists()`. Manual cleanup via **`codesearch index prune`** (relocate-first, else unregister stale aliases)
- **Alias is always derived** — the user-settable `--alias`/`-a` flag was removed from `index add`; the alias always equals the (sanitized) directory name via `ReposConfig::register()`. The alias remains the internal identifier (repos.json key, groups, `project` arg); only user override is gone. The `index symbol <alias>` positional is a lookup key and is retained
- **Hand-edited `repos.json` tolerated** — `ReposConfig::reconcile()` runs in-memory on every load: drops empty-alias entries, drops orphan `repos_meta`, prunes group members referencing unknown aliases and empty groups. Never renames valid aliases, never crashes
- **TUI C# indicator** — in status column: green `C#·` ready, yellow `C#…` indexing, red `C#!` error; footer shows helper availability; Calls column with tool call count
- **Phase 2 & 3 TUI feedback** — Phase 2 pre-marks all queued candidates as `C#…` immediately on discovery (before semaphore slot); Phase 3 pre-warm sets `csharp_index_status = Indexing` before `batch-find-refs` and restores `Ready` after — TUI shows `C#…` throughout without touching `active_reindexes` (avoids blocking HTTP /reindex)
- **Selective ref cache invalidation** — incremental rebuilds only purge cached refs for affected symbols, not entire cache
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0



## [1.0.152] - 2026-06-02

### Added

- **Best-effort relocation of moved/renamed repositories** — every repo's git
remote (`remote.origin.url`) is now captured at registration. When a
registered folder is renamed or moved, `codesearch serve` no longer crashes:
on startup it reconciles all paths, and for each missing path it scans nearby
folders (bounded depth, override with `CODESEARCH_RELOCATE_MAX_DEPTH`, default
`3`) for a git checkout with the same remote. A single unambiguous match is
rewritten into `repos.json`; ambiguous/absent matches are logged and skipped
(the dead path is never indexed). Phase-2 (C# SCIP) and Phase-3 (pre-warm)
also guard `path.exists()` so a stale path can never reach heavy code paths.
- **`codesearch index prune`** — new command that relocates moved repos first,
then unregisters any remaining stale entries, printing a summary.

### Changed

- **The user-settable `--alias`/`-a` flag was removed from `index add`** — the
alias (the `repos.json` key, used by groups and the MCP `project` argument) is
now always derived from the repository directory name. In practice the alias
always had to equal the directory name, so a custom alias only caused
downstream mismatches. The `index symbol <alias>` positional (a lookup key) is
unchanged.

### Fixed

- **A hand-edited or corrupt-ish `repos.json` no longer crashes the app** — on
load the config is reconciled in memory: entries with empty/blank alias keys
are dropped, orphaned `repos_meta` is removed, and group members referencing
unknown aliases (and groups left empty) are pruned. Valid aliases are never
renamed (that would break group references).

## [1.0.146] - 2026-06-02

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "codesearch"
version = "1.0.146"
version = "1.0.152"
edition = "2021"
authors = ["codesearch contributors"]
license = "Apache-2.0"
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ codesearch index rm /path/to/my-project

# List registered repos
codesearch index list

# Remove stale entries (relocates moved repos first, then drops the rest)
codesearch index prune
```

`codesearch index add` is intended to be run from inside the repo you want to register.
Expand Down Expand Up @@ -312,17 +315,39 @@ Repos are registered via `codesearch index add`:

```bash
# Register a repo (creates index + adds to ~/.codesearch/repos.json)
codesearch index add /path/to/my-project --alias my-project
codesearch index add /path/to/my-project

# Remove a repo
codesearch index rm /path/to/my-project

# List registered repos
codesearch index list

# Clean up stale entries (relocates moved repos, drops the rest)
codesearch index prune
```

The repository **alias** (the key in `repos.json`, used for groups and the MCP
`project` argument) is always derived automatically from the directory name —
there is no `--alias` flag.

Serve reads `~/.codesearch/repos.json` on startup and manages all registered repos.

#### Moved or renamed repositories

If you rename or move a registered folder, serve does **not** crash. On startup
it tries to **relocate** each missing repo automatically: it captures every
repo's git remote (`remote.origin.url`) at registration, and on a missing path
it scans nearby folders (bounded depth, override with
`CODESEARCH_RELOCATE_MAX_DEPTH`, default `3`) for a git checkout with the same
remote. A single unambiguous match is rewritten into `repos.json`; otherwise the
entry is logged and skipped (never indexed against a dead path). Run
`codesearch index prune` to relocate what can be relocated and drop the rest.

A hand-edited `repos.json` is also tolerated: empty entries, orphaned metadata,
and group references to unknown repos are cleaned up on load rather than
crashing.

### Groups

Groups let you search across related repositories:
Expand Down
46 changes: 23 additions & 23 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ pub enum IndexCommands {
/// Create global index instead of local
#[arg(short = 'g', long)]
global: bool,

/// Alias for this repository (auto-generated from directory name if omitted)
#[arg(short, long)]
alias: Option<String>,
},

/// Remove the index (local or global, auto-detected)
Expand All @@ -49,6 +45,9 @@ pub enum IndexCommands {
#[arg(short = 'f', long)]
force: bool,
},

/// Remove stale entries from repos.json (relocates moved repos first)
Prune,
}

/// Cache subcommands
Expand Down Expand Up @@ -235,10 +234,6 @@ pub enum Commands {
#[arg(short = 'g', long)]
global: bool,

/// Alias for this repository (only with --add)
#[arg(short, long)]
alias: Option<String>,

/// Remove the index (local or global, auto-detected)
#[arg(long, visible_alias = "rm")]
remove: bool,
Expand Down Expand Up @@ -532,7 +527,6 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> {
symbols,
add,
global,
alias,
remove,
keep_config,
list,
Expand All @@ -543,11 +537,7 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> {
IndexCommands::Add {
path: add_path,
global,
alias,
} => {
crate::index::add_to_index(add_path, global, alias, cancel_token.clone())
.await
}
} => crate::index::add_to_index(add_path, global, cancel_token.clone()).await,
IndexCommands::Remove {
path: rm_path,
keep_config,
Expand All @@ -556,6 +546,7 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> {
IndexCommands::Symbol { alias, force } => {
trigger_symbol_reindex_via_api(&alias, force).await
}
IndexCommands::Prune => crate::index::prune_index().await,
}
} else {
// Flag-based backward-compat path
Expand All @@ -569,8 +560,7 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> {

if add || is_add_cmd {
let effective_path = if is_add_cmd { None } else { path };
crate::index::add_to_index(effective_path, global, alias, cancel_token.clone())
.await
crate::index::add_to_index(effective_path, global, cancel_token.clone()).await
} else if remove || is_rm_cmd {
let effective_path = if is_rm_cmd { None } else { path };
crate::index::remove_from_index(effective_path, keep_config).await
Expand Down Expand Up @@ -911,22 +901,32 @@ mod tests {
}

#[test]
fn test_cli_index_add_accepts_alias_flag() {
let cli = Cli::try_parse_from([
fn test_cli_index_add_rejects_alias_flag() {
// The user-settable alias was removed; the flag must no longer parse.
let result = Cli::try_parse_from([
"codesearch",
"index",
"add",
"/tmp/foo",
"--alias",
"myrepo",
])
.expect("cli parse should succeed");
]);
assert!(
result.is_err(),
"'--alias' flag should no longer be accepted on `index add`"
);
}

#[test]
fn test_cli_index_add_parses_without_alias() {
let cli = Cli::try_parse_from(["codesearch", "index", "add", "/tmp/foo"])
.expect("cli parse should succeed");
match cli.command {
Commands::Index {
command: Some(IndexCommands::Add { alias: Some(a), .. }),
command: Some(IndexCommands::Add { path: Some(p), .. }),
..
} => assert_eq!(a, "myrepo"),
_ => panic!("expected Index::Add subcommand with alias"),
} => assert_eq!(p, std::path::PathBuf::from("/tmp/foo")),
_ => panic!("expected Index::Add subcommand"),
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ pub const DEFAULT_EMBEDDING_DIMENSIONS: usize = 384;
/// Environment variable to override repos config file path.
pub const REPOS_CONFIG_ENV: &str = "CODESEARCH_REPOS_CONFIG";

/// Environment variable to override how deep relocation scans for a moved repo.
pub const RELOCATE_MAX_DEPTH_ENV: &str = "CODESEARCH_RELOCATE_MAX_DEPTH";

/// Default bounded depth for the relocation scan (directories below the nearest
/// existing ancestor of a stale repo path).
pub const DEFAULT_RELOCATE_MAX_DEPTH: usize = 3;

/// Environment variable to set MCP mode: "auto", "client", or "local".
pub const MCP_MODE_ENV: &str = "CODESEARCH_MCP_MODE";

Expand Down
Loading
Loading