fix(scan): bound clones with a timeout + rolling worker pool, surface skipped repos#91
Conversation
An org-wide scan could freeze indefinitely on a single repository. Two
root causes, both platform-agnostic (GitHub/GHE, GitLab, Bitbucket
Cloud/DC, Azure all share this path):
1. gogit.Getrepos used go-git PlainClone with no deadline, so a stalled
connection to the git server hung the worker forever. It also
swallowed clone errors (logged, then returned a valid path with nil
error), so a failed clone looked like an empty-but-successful repo.
2. AnalyseReposList ran fixed batches with a barrier: the next batch
could not start until the slowest repo in the current batch finished,
so one slow/hung repo stalled up to Workers-1 idle workers and froze
overall progress ("Waiting for workers...").
Changes:
- gogit.Getrepos now takes a timeout and uses PlainCloneContext; on
failure it removes the partial clone and returns the error (timeouts
become an explicit "clone timed out" message).
- New CloneTimeout setting (minutes, default 15, 0 = disabled) threaded
through RepoParams -> goloc.Params -> gogit. Added to config_sample.json,
the webui platform defaults, and the advanced-options form.
- AnalyseReposList rewritten as a semaphore-bounded rolling worker pool
with a WaitGroup; same max concurrency as before, no batch barrier, so
one slow repo can no longer block the others.
- Skipped repositories (clone timeout/failure or analysis error) are now
recorded per run and persisted to Results/config/analysis_skipped.json,
then surfaced in the ResultsAll web page (+ /api/skipped-repositories)
and the GlobalReport PDF.
- Tests: clone-timeout + no-leak in gogit; getCloneTimeout and the
concurrency-safe skipRecorder in golc; save/load roundtrip and PDF
section rendering in utils.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses two Gitar review findings on #91: 1. Data race on the shared progress counter. AnalyseReposList passed &count (plain int) into every worker goroutine; the rolling pool runs them concurrently and each does count++/reads count. Convert the counter to atomic.Int64 across analyseRepoFunc / the five analyse*Repo funcs / performRepoAnalysis / analyseDirectory, and increment via count.Add(1). This also fixes a latent bug where the success log only read count (never incremented it), so every repo printed "1" — each completed repo now gets a unique sequence number. The same shared pattern in AnalyseReposListFile/analyseDirectory is fixed too. Adds TestAnalyseReposListConcurrentCountNoRace as a -race regression guard over the multithreaded path. 2. Clearing the webui "Clone timeout" field silently disabled the deadline: parseInt('')||0 -> 0, which getCloneTimeout reads as "no timeout" — re-introducing the exact hang this feature prevents. An empty/blank field now falls back to the default (15); an explicit "0" still disables the deadline. Verified: go test -race passes for ./pkg/... and -tags golc (incl. the new concurrency test); webui and resultsall binaries build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SonarCloud flagged two MAJOR "too many parameters" (go:S107) smells on the PR's new code: performRepoAnalysis (11 params) and analyseDirectory (9 params). The parameter counts were unchanged from main — they only entered the PR's new-code scope because the previous commit retouched those signature lines for the atomic counter — but the gate blocks the merge, and they are legitimate readability smells, so fix them properly. Introduce an analysisOptions struct bundling the exclusions, keyword/ name filters, and report-mode flags. performRepoAnalysis now takes (params, dest, spin, results, count, opts) = 6 params; analyseDirectory takes (dir, opts, destDir, count) = 4. The five analyse*Repo callers and AnalyseReposListFile build the struct; behavior is unchanged. AnalyseReposListFile itself (8 params) is intentionally left untouched: it is pre-existing and not in the PR's new-code scope, so editing its signature would needlessly pull a new S107 into scope. Verified: go build (webui, resultsall), go vet, and go test -race -tags golc all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Code Review ✅ Approved 2 resolved / 2 findingsImplements a rolling worker pool and repository clone timeouts to prevent scan hangs, while introducing a skipped repository reporting mechanism. A data race on the progress counter and silent deadline disabling were both addressed. ✅ 2 resolved✅ Bug: Data race on shared count pointer in rolling worker pool
✅ Edge Case: Cleared CloneTimeout field silently disables the deadline
OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |



Problem
gogit.Getreposused go-gitPlainClonewith no deadline, so a stalled TCP connection to the git server (VPN drop, proxy, server under load) hung the cloning goroutine forever. It also swallowed clone errors — it logged them but still returned a valid path with anilerror, so a failed clone was treated as an empty-but-successful repo.AnalyseReposListlaunched fixed batches ofWorkersand waited for all of a batch to finish before starting the next. One slow/hung repo therefore stalled up toWorkers-1idle workers and froze overall progress.A GitHub Enterprise (and any platform) org-wide scan could freeze for 30+ minutes on a single repository, showing a stuck progress bar and repeated
Waiting for workers.... Two independent root causes, both in the shared cross-platform analysis path:Together: a single bad repo could hang an entire org scan with no recovery and no record.
Fix
gogit.Getreposnow takes a timeout and usesPlainCloneContext. On failure it removes the partial clone and returns the error; a deadline surfaces as an explicitclone timed out after <d>message.CloneTimeoutsetting (minutes; default 15,0disables) threaded throughRepoParams → goloc.Params → gogit. Added toconfig_sample.json, the webui platform defaults, and the advanced-options form.AnalyseReposListrewritten as a semaphore-bounded pool +WaitGroup. Same max concurrency as before, but every finished repo frees its slot immediately, so one slow repo can no longer block the others (no more batch barrier /Waiting for workers...).Results/config/analysis_skipped.json, then shown in:GET /api/skipped-repositories)All of this is platform-agnostic — GitHub/GHE, GitLab, Bitbucket Cloud, Bitbucket DC, and Azure all flow through the same
performRepoAnalysis/gogit.Getrepospath, so the fix applies to every platform.Scope note
The "Skipped Repositories" report lists analysis-phase failures (the actionable ones tied to this bug). Intentional pre-analysis skips (archived/empty/excluded) remain summarized as counts, as before.
Tests
pkg/gogit: clone-timeout returns an error and leaves no temp clone behind; existing clone tests updated for the new signature.golc(tag):getCloneTimeoutdefaulting/disable logic; concurrency-safeskipRecorder.pkg/utils: skipped-repos save/load roundtrip, empty-clears-stale, missing-file; PDF section renders (empty / one / long-list-with-truncation) without panic.go test ./pkg/...,-tags=golc,-tags=resultsall; both binaries build with-trimpath.