fix(orbit-local): friendly errors for non-git and empty-git index paths

What does this MR do and why?

Running orbit index against a directory with no git repository reported success: exit 0, no output, no database created, and the MCP index tool returned an empty success the same way. Pointing it at a repository with no commits leaked a raw git error with advice about -- path separation that misleads a first-time user. Both cases now fail with a one-line error naming the condition and what to do, the two fixes proposed in #658 (closed).

Relates to #1010 (this completes its folded-in #658 (closed) item: friendly errors for non-git and empty-git directories passed to index).

Testing

Three new CLI integration tests: orbit index on a directory with a source file but no git repository must fail and name the condition; the MCP index tool on the same shape must return a recoverable tool error (isError: true) rather than an empty success; and index on a freshly initialized repository with no commits must fail with the friendly message and must not leak the raw ambiguous argument git error. All three fail on current main and pass with this change. The rest of the local CLI suite passes except one repo-map fixture assertion that fails identically on unmodified main in the same environment (macOS arm64), so it is unrelated to this change.

Performance Analysis

No hot path changed: an early return on the empty case became an early error.

  • This merge request does not introduce any performance regression. If a performance regression is expected, explain why.
Agent context — long-form analysis, file-by-file walkthroughs, profiler output, alternatives considered

Reproduced on macOS arm64 and Windows Server 2022 with v0.95.1: mkdir a directory, add a source file, orbit index <dir> exits 0 silently and creates no database; orbit sql then errors with "no local graph found. Index a repository first".

Root cause chain:

  • index_collect in crates/orbit-local/src/main.rs resolves repositories via Workspace::resolve_repos, and on an empty result logged info!("No git repositories found in {}") and returned Ok(Vec::new()).
  • The CLI's default log level is WARN (DEBUG only under -v), so that info! never reached stderr in a default run.
  • Schema initialization happens after that early return, so no DuckDB file was created, which is what makes the downstream "index a repository first" error misdirecting.
  • The MCP index tool (crates/orbit-local/src/mcp.rs) calls index_collect directly and serialized the empty success as [] with isError: false, so an agent could not detect the condition either.

The non-git fix replaces the early return with anyhow::bail! at that single point, covering both the CLI and the MCP tool. The error wording ("Pass a repository path, or a directory containing one.") deliberately differs from #658 (closed)'s draft ("is not inside a Git repository"): index also accepts a directory containing one or more repositories, so the message mirrors the command's own prompt description in config/prompts/local/index.yml instead. That file is untouched (its text makes no claim about the empty case, so no prompt version bump is needed). Happy to adopt #658 (closed)'s wording if preferred.

The no-commits fix sits in git_info: when the commit-hash fetch fails, a git rev-parse --quiet --verify HEAD probe classifies the empty-repository case and replaces the raw gitalisk error with #658 (closed)'s proposed message verbatim ("has no commits yet. Make at least one commit before indexing."), before the error reaches the indexing loop's per-repository skip logging. Other failure shapes keep the existing error chain unchanged. This also improves repo-map, which resolves git metadata through the same function.

Alternatives considered: a tracing::warn! (visible at the default level) without a failure exit was rejected because #658 (closed) explicitly proposed a non-zero exit, and a success exit code leaves the MCP path indistinguishable from a real empty success. The upstream gitalisk-core index_repositories already returns repository statistics that the caller discards; deriving the message from that count was unnecessary for this fix and would have widened the diff.

Interaction with !2194 (merged)'s failure capture (rebased onto): the no-commits reason now flows into the _orbit_manifest error row that work introduced, so orbit list shows "has no commits yet. Make at least one commit before indexing." instead of the raw git chain (verified by query against a built binary). The new unindexable_repo_records_error_in_manifest test uses a commitless repository as its fixture and passes unchanged with this branch, since it asserts a non-empty reason, not specific text.

Travels with !2203 (merged), which documents the script-free Windows install path: that page change sends more first-run users into orbit index, so this fix should land with or before it.

Scope note: this covers both cases in #658 (closed), so its checkbox on #1010 can close; the other three folded items there (#642 (closed), #653 (closed), #695 (closed)) are separate subsystems and are untouched. The non-git case was reproduced on macOS arm64 and Windows Server 2022 with released v0.95.1; the no-commits case follows #658 (closed)'s own reproduction and was verified against this branch's build (friendly message on stderr, exit 1, no raw git advice).

The integration tests follow the existing patterns in crates/integration-tests/tests/cli.rs: schema_errors_when_db_missing for the CLI surface (tempdir, run_cmd, assert on exit status and a stderr substring) and mcp_bad_sql_is_recoverable_tool_error for the MCP surface (mcp_roundtrip, assert isError and the message text).

This MR was prepared with assistance from Claude Code; the change and tests were verified by running the CLI integration suite locally.

Edited by Andrew Dunn

Merge request reports

Loading