Skip to content

fix: avoid duplicate JSON in combined output when hotspot has no Git repo (#294)#297

Merged
BartWaardenburg merged 1 commit intofallow-rs:mainfrom
ChrisJr404:fix/combined-json-no-git-duplicate-294
May 6, 2026
Merged

fix: avoid duplicate JSON in combined output when hotspot has no Git repo (#294)#297
BartWaardenburg merged 1 commit intofallow-rs:mainfrom
ChrisJr404:fix/combined-json-no-git-duplicate-294

Conversation

@ChrisJr404
Copy link
Copy Markdown
Contributor

Closes #294.

Combined-mode --format json writes two top-level JSON documents to stdout when the project is outside a Git repository: first a { "error": true, "message": "hotspot analysis requires a git repository", ... } blob, then the normal combined report. That breaks fallow --format json | jq . and any agent or CI parser that assumes a single document. The nixpkgs build of 2.65.0 hit this in five combined-mode integration tests whose temp fixtures live outside any Git checkout.

Where the second blob comes from

crates/cli/src/health/hotspots.rs::fetch_churn_data is called from inside the health pipeline. When is_git_repo(opts.root) returns false it does:

if !churn::is_git_repo(opts.root) {
    let _ = emit_error("hotspot analysis requires a git repository", 2, opts.output);
    return None;
}

emit_error with OutputFormat::Json println!s a structured error object to stdout regardless of who called it. The function then returns None, which the caller already handles gracefully ((Vec::new(), None) for hotspots — see crates/cli/src/health/mod.rs:520). Combined mode then continues, builds the combined report, and print_combined_json println!s it. Two documents on stdout, one well-formed parser per side.

Fix

Treat a missing Git repo as unavailable hotspot data rather than a hard error. The hotspot pipeline already silently degrades to an empty section when fetch_churn_data returns None, so dropping the JSON emission lets combined mode keep its single-document contract. A non-fatal note: hotspot analysis skipped — no git repository found at project root goes to stderr unless --quiet is set.

This also fixes standalone fallow health --hotspots --format json outside a Git repo: it now emits one well-formed health report with empty hotspots and exits 0, which lines up with the first acceptable resolution suggested in the issue ("treat missing Git history for hotspot analysis as unavailable data in the combined report").

The other emit_error paths in fetch_churn_data cover --since malformed input — those are user-error invariants and are not reachable from combined mode (build_health_opts hard-codes since: None), so they keep their existing exit-2 contract.

Test

Adds combined_json_outside_git_repo_emits_single_document to crates/cli/tests/exit_code_tests.rs. The test:

  1. Creates a minimal TS project in a tempdir (/tmp/...), so is_git_repo walks up and returns false.
  2. Strips inherited GIT_DIR / GIT_WORK_TREE and points GIT_CONFIG_GLOBAL / GIT_CONFIG_SYSTEM at /dev/null so a parent-process git env can't redirect rev-parse upward (this matches the isolation pattern already used by health_tests.rs::git).
  3. Runs fallow --root <tempdir> --format json --quiet (combined mode).
  4. Asserts stdout parses as exactly one serde_json::Value (the original failure mode failed to parse JSON: trailing characters at line 6 column 1 reproduces here without the fix).
  5. Asserts the parsed envelope has schema_version and no top-level error key.

I confirmed the test fails on main and passes with the patch applied locally.

Verification

  • cargo test -p fallow-extract -p fallow-cli --bin fallow — 1755 pass, 0 fail
  • cargo test -p fallow-cli --test exit_code_tests — 16 pass, 0 fail (includes the new case)
  • cargo test -p fallow-cli --test health_tests — 30 pass, 0 fail
  • cargo test -p fallow-cli --lib — 1226 pass, 0 fail
  • cargo test -p fallow-extract --lib — 1392 pass, 0 fail
  • cargo clippy -p fallow-cli --bin fallow --tests — clean
  • cargo fmt --check — clean

…ow-rs#294)

Combined-mode `--format json` could write two top-level JSON values to
stdout when run outside a git repository: first a `{ "error": true,
"message": "hotspot analysis requires a git repository", ... }` blob
emitted by the hotspot pipeline, then the normal combined report. That
broke `fallow --format json | jq .` and any agent or CI parser that
expects a single document — `failed to parse JSON: trailing characters
at line 6 column 1` was the typical symptom.

`fetch_churn_data` already returns `None` for the no-git case, and the
caller already renders an empty hotspot section when that happens, so
the JSON emission was the only side effect that ever made it to stdout.
Drop the JSON emission in favor of a single stderr note (gated by
`--quiet`); standalone `fallow health --hotspots --format json` outside
a git repo now emits one well-formed health report with empty hotspots
and exits 0, matching the issue's preferred resolution.

Adds `combined_json_outside_git_repo_emits_single_document` to
`exit_code_tests.rs`. The test builds a minimal TS project in a tempdir
(no parent `.git`), strips inherited `GIT_DIR` / `GIT_WORK_TREE`, runs
combined mode with `--format json`, and asserts stdout parses as
exactly one JSON value with `schema_version` set and no top-level
`error` key.
@BartWaardenburg BartWaardenburg merged commit f4785be into fallow-rs:main May 6, 2026
18 checks passed
BartWaardenburg added a commit that referenced this pull request May 6, 2026
CHANGELOG entry under [Unreleased] documenting the behavior change for
the next release: combined-mode --format json now emits exactly one
JSON document outside a git repository, and standalone fallow health
--hotspots exits 0 with empty hotspot fields instead of exit 2 with a
JSON error.

docs/backwards-compatibility.md gains a 'Notable behavior changes
within v2' section so CI scripts that depended on the exit-2 signal
have a documented migration path (inspect hotspot_summary instead).
BartWaardenburg added a commit to fallow-rs/docs that referenced this pull request May 6, 2026
Updates the --hotspots and --ownership flag rows and the warning panel
in cli/health.mdx to reflect the v2.66.x behavior change: outside a
git repository the hotspot section now degrades to empty (with a
stderr note) and exits 0 instead of erroring, so combined-mode
--format json always emits a single JSON document. Tracks
fallow-rs/fallow#297.
@BartWaardenburg
Copy link
Copy Markdown
Collaborator

Released in v2.66.1. Thanks @ChrisJr404.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Combined JSON output can emit duplicate JSON when hotspot analysis lacks Git repo

2 participants