repoglance 0.4.1__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {repoglance-0.4.1 → repoglance-0.4.2}/CHANGELOG.md +8 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/PKG-INFO +27 -5
- {repoglance-0.4.1 → repoglance-0.4.2}/README.md +26 -4
- {repoglance-0.4.1 → repoglance-0.4.2}/pyproject.toml +1 -1
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/__init__.py +1 -1
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/cli.py +6 -3
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/scanner.py +37 -7
- {repoglance-0.4.1 → repoglance-0.4.2}/tests/test_v041.py +14 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/.github/workflows/ci.yml +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/.github/workflows/release.yml +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/.github/workflows/repoglance.yml +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/.gitignore +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/.pre-commit-hooks.yaml +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/.repoglance-badge.json +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/CODE_OF_CONDUCT.md +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/CONTRIBUTING.md +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/Dockerfile +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/LICENSE +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/RELEASING.md +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/SECURITY.md +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/action.yml +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/assets/badge.svg +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/assets/demo.svg +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/assets/demo.tape +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/assets/showcase/flask.svg +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/assets/showcase/httpie.svg +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/assets/showcase/requests.svg +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/benchmarks/bench.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/docs/index.md +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/editors/vscode/README.md +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/editors/vscode/extension.js +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/editors/vscode/package.json +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/integrations/gitlab-ci.yml +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/mkdocs.yml +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/packaging/homebrew/repoglance.rb +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/__main__.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/badge.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/cache.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/complexity.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/config.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/dedup.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/gitinfo.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/languages.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/metrics.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/plugins.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/report.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/src/repoglance/vendored.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/tests/test_cli.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/tests/test_complexity.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/tests/test_features.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/tests/test_scanner.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/tests/test_scanner_extra.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/tests/test_v030.py +0 -0
- {repoglance-0.4.1 → repoglance-0.4.2}/tests/test_v040.py +0 -0
|
@@ -4,6 +4,14 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.4.2] - 2026-08-01
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Multicore scanning for large repositories**: complexity parsing now runs in
|
|
11
|
+
a process pool when a scan is big enough (CPU-bound work the GIL kept threads
|
|
12
|
+
from parallelizing), roughly halving cold-scan time on large monorepos. Force
|
|
13
|
+
the mode with `--processes` / `--threads`. Django (415k LOC): ~20.5s → ~10.1s.
|
|
14
|
+
|
|
7
15
|
## [0.4.1] - 2026-08-01
|
|
8
16
|
|
|
9
17
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repoglance
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Instant, gorgeous insight into any code repository — languages, complexity hotspots, TODOs and git activity in one terminal command.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SRJ-ai/repoglance
|
|
6
6
|
Project-URL: Repository, https://github.com/SRJ-ai/repoglance
|
|
@@ -278,18 +278,40 @@ docker run --rm -v "$PWD:/repo" repoglance /repo
|
|
|
278
278
|
A ready-to-copy GitLab CI job lives in
|
|
279
279
|
[`integrations/gitlab-ci.yml`](integrations/gitlab-ci.yml).
|
|
280
280
|
|
|
281
|
+
## Built for large monorepos
|
|
282
|
+
|
|
283
|
+
repoglance is designed to stay useful at the scale of a big-tech monorepo —
|
|
284
|
+
millions of lines, thousands of files — not just small projects:
|
|
285
|
+
|
|
286
|
+
- **Diff mode** (`--since <rev>`) analyzes only the files a change touches, so a
|
|
287
|
+
PR check on a giant repo stays fast regardless of total size.
|
|
288
|
+
- **Incremental cache** (`--cache`) reuses unchanged files by mtime + size —
|
|
289
|
+
repeat runs are near-instant (see below).
|
|
290
|
+
- **True multicore scanning**: for large repos it automatically uses a process
|
|
291
|
+
pool (complexity parsing is CPU-bound, so threads alone can't use every core),
|
|
292
|
+
roughly halving cold-scan time. Force it with `--processes` / `--threads`.
|
|
293
|
+
- **`.gitignore`-aware** and **vendored/generated exclusion**, so third-party
|
|
294
|
+
and generated code doesn't drown the signal.
|
|
295
|
+
- **Path scoping** with `--include` / `--exclude` globs for per-team slices of a
|
|
296
|
+
shared repo.
|
|
297
|
+
|
|
298
|
+
> Note: repoglance is an independent open-source project. It is not affiliated
|
|
299
|
+
> with, endorsed by, or used by any company named for scale comparison.
|
|
300
|
+
|
|
281
301
|
## Performance
|
|
282
302
|
|
|
283
303
|
Measured on Django (3,180 files, ~415k lines of code), single machine:
|
|
284
304
|
|
|
285
305
|
| Run | Time |
|
|
286
306
|
|---|--:|
|
|
287
|
-
| Cold scan
|
|
307
|
+
| Cold scan, threads | ~20.5 s |
|
|
308
|
+
| Cold scan, process pool (auto on large repos) | ~10.1 s |
|
|
288
309
|
| Re-run with `--cache` | ~1.1 s |
|
|
289
310
|
|
|
290
|
-
The cold scan is dominated by real per-function complexity parsing
|
|
291
|
-
|
|
292
|
-
|
|
311
|
+
The cold scan is dominated by real per-function complexity parsing. The process
|
|
312
|
+
pool spreads that across cores (~2× here); the incremental cache (`--cache
|
|
313
|
+
<file>`) reuses unchanged files by mtime + size, so repeat runs — the common
|
|
314
|
+
case in editors and CI — are roughly **20× faster** than a cold thread scan.
|
|
293
315
|
|
|
294
316
|
## Design goals
|
|
295
317
|
|
|
@@ -246,18 +246,40 @@ docker run --rm -v "$PWD:/repo" repoglance /repo
|
|
|
246
246
|
A ready-to-copy GitLab CI job lives in
|
|
247
247
|
[`integrations/gitlab-ci.yml`](integrations/gitlab-ci.yml).
|
|
248
248
|
|
|
249
|
+
## Built for large monorepos
|
|
250
|
+
|
|
251
|
+
repoglance is designed to stay useful at the scale of a big-tech monorepo —
|
|
252
|
+
millions of lines, thousands of files — not just small projects:
|
|
253
|
+
|
|
254
|
+
- **Diff mode** (`--since <rev>`) analyzes only the files a change touches, so a
|
|
255
|
+
PR check on a giant repo stays fast regardless of total size.
|
|
256
|
+
- **Incremental cache** (`--cache`) reuses unchanged files by mtime + size —
|
|
257
|
+
repeat runs are near-instant (see below).
|
|
258
|
+
- **True multicore scanning**: for large repos it automatically uses a process
|
|
259
|
+
pool (complexity parsing is CPU-bound, so threads alone can't use every core),
|
|
260
|
+
roughly halving cold-scan time. Force it with `--processes` / `--threads`.
|
|
261
|
+
- **`.gitignore`-aware** and **vendored/generated exclusion**, so third-party
|
|
262
|
+
and generated code doesn't drown the signal.
|
|
263
|
+
- **Path scoping** with `--include` / `--exclude` globs for per-team slices of a
|
|
264
|
+
shared repo.
|
|
265
|
+
|
|
266
|
+
> Note: repoglance is an independent open-source project. It is not affiliated
|
|
267
|
+
> with, endorsed by, or used by any company named for scale comparison.
|
|
268
|
+
|
|
249
269
|
## Performance
|
|
250
270
|
|
|
251
271
|
Measured on Django (3,180 files, ~415k lines of code), single machine:
|
|
252
272
|
|
|
253
273
|
| Run | Time |
|
|
254
274
|
|---|--:|
|
|
255
|
-
| Cold scan
|
|
275
|
+
| Cold scan, threads | ~20.5 s |
|
|
276
|
+
| Cold scan, process pool (auto on large repos) | ~10.1 s |
|
|
256
277
|
| Re-run with `--cache` | ~1.1 s |
|
|
257
278
|
|
|
258
|
-
The cold scan is dominated by real per-function complexity parsing
|
|
259
|
-
|
|
260
|
-
|
|
279
|
+
The cold scan is dominated by real per-function complexity parsing. The process
|
|
280
|
+
pool spreads that across cores (~2× here); the incremental cache (`--cache
|
|
281
|
+
<file>`) reuses unchanged files by mtime + size, so repeat runs — the common
|
|
282
|
+
case in editors and CI — are roughly **20× faster** than a cold thread scan.
|
|
261
283
|
|
|
262
284
|
## Design goals
|
|
263
285
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "repoglance"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Instant, gorgeous insight into any code repository — languages, complexity hotspots, TODOs and git activity in one terminal command."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -39,7 +39,8 @@ def _resolve(cli_val, cfg, key, default):
|
|
|
39
39
|
@click.option("--ignore", multiple=True, help="Extra directory name to ignore (repeatable).")
|
|
40
40
|
@click.option("--no-git", is_flag=True, help="Skip git history analysis.")
|
|
41
41
|
@click.option("--max-bytes", type=int, default=None, help="Skip files larger than this many bytes.")
|
|
42
|
-
@click.option("--jobs", type=int, default=None, help="
|
|
42
|
+
@click.option("--jobs", type=int, default=None, help="Number of parallel workers for scanning.")
|
|
43
|
+
@click.option("--processes/--threads", "use_processes", default=None, help="Force process- or thread-based parallelism (default: auto by repo size).")
|
|
43
44
|
@click.option("--duplicates", "want_dupes", is_flag=True, help="Detect duplicated code blocks.")
|
|
44
45
|
@click.option("--owners", "want_owners", is_flag=True, help="Attribute top complexity hotspots to authors (git blame).")
|
|
45
46
|
@click.option("--include-vendored", is_flag=True, help="Analyze vendored/generated files instead of excluding them.")
|
|
@@ -56,8 +57,9 @@ def _resolve(cli_val, cfg, key, default):
|
|
|
56
57
|
@click.version_option(__version__, "-V", "--version", prog_name="repoglance")
|
|
57
58
|
def main(path, as_json, as_md, as_csv, as_sarif, sarif_threshold, html_path, svg_path,
|
|
58
59
|
badge_path, badge_json_path, baseline_out, since_rev, include, exclude, ignore,
|
|
59
|
-
no_git, max_bytes, jobs, want_dupes, want_owners, include_vendored,
|
|
60
|
-
watch, compare_to, fail_on_regression, ci, max_complexity, max_todos,
|
|
60
|
+
no_git, max_bytes, jobs, use_processes, want_dupes, want_owners, include_vendored,
|
|
61
|
+
cache_path, watch, compare_to, fail_on_regression, ci, max_complexity, max_todos,
|
|
62
|
+
fail_under):
|
|
61
63
|
"""Instant, gorgeous insight into any code repository.
|
|
62
64
|
|
|
63
65
|
PATH defaults to the current directory. Configuration may be supplied via
|
|
@@ -80,6 +82,7 @@ def main(path, as_json, as_md, as_csv, as_sarif, sarif_threshold, html_path, svg
|
|
|
80
82
|
"max_bytes": max_bytes, "extra_ignores": ignores, "include": include,
|
|
81
83
|
"exclude": exclude, "changed_only": changed, "jobs": jobs,
|
|
82
84
|
"include_vendored": include_vendored, "keep_contents": want_dupes,
|
|
85
|
+
"processes": use_processes,
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
if watch:
|
|
@@ -8,9 +8,10 @@ so every file is read exactly once.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import fnmatch
|
|
11
|
+
import functools
|
|
11
12
|
import os
|
|
12
13
|
import subprocess
|
|
13
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
+
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
|
|
14
15
|
from dataclasses import dataclass, field
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Dict, Iterable, List, Optional
|
|
@@ -19,6 +20,10 @@ from .complexity import FuncScore, analyze_complexity
|
|
|
19
20
|
from .languages import lang_for
|
|
20
21
|
from .vendored import is_vendored
|
|
21
22
|
|
|
23
|
+
# Above this many files we default to process-based parallelism: complexity
|
|
24
|
+
# parsing is CPU-bound and the GIL prevents threads from using multiple cores.
|
|
25
|
+
_PROCESS_THRESHOLD = 400
|
|
26
|
+
|
|
22
27
|
# Directories never worth scanning (used only on the non-git fallback path).
|
|
23
28
|
IGNORE_DIRS = {
|
|
24
29
|
".git", ".hg", ".svn", "node_modules", "venv", ".venv", "env",
|
|
@@ -198,6 +203,7 @@ def scan(
|
|
|
198
203
|
include_vendored: bool = False,
|
|
199
204
|
keep_contents: bool = False,
|
|
200
205
|
cache_data: Optional[dict] = None,
|
|
206
|
+
processes: Optional[bool] = None,
|
|
201
207
|
) -> ScanResult:
|
|
202
208
|
"""Walk ``root`` and gather per-file line/language/complexity stats.
|
|
203
209
|
|
|
@@ -244,9 +250,7 @@ def scan(
|
|
|
244
250
|
else:
|
|
245
251
|
to_parse = todo
|
|
246
252
|
|
|
247
|
-
|
|
248
|
-
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
249
|
-
processed = list(pool.map(lambda p: _process_file(p, root_path, max_bytes), to_parse))
|
|
253
|
+
processed = _parallel_process(to_parse, root_path, max_bytes, keep_contents, jobs, processes)
|
|
250
254
|
|
|
251
255
|
for full, item in zip(to_parse, processed):
|
|
252
256
|
if item is None:
|
|
@@ -299,8 +303,33 @@ def _absorb(result, stat, todos, scores, vendored, include_vendored, text, keep_
|
|
|
299
303
|
result.contents[stat.path] = text
|
|
300
304
|
|
|
301
305
|
|
|
302
|
-
def
|
|
303
|
-
"""
|
|
306
|
+
def _parallel_process(to_parse, root_path, max_bytes, keep_contents, jobs, processes):
|
|
307
|
+
"""Analyze files in parallel. Uses processes for large, CPU-bound scans so
|
|
308
|
+
complexity parsing actually spreads across cores (threads can't, due to the
|
|
309
|
+
GIL); falls back to threads for small scans or when contents must be kept."""
|
|
310
|
+
if not to_parse:
|
|
311
|
+
return []
|
|
312
|
+
worker = functools.partial(
|
|
313
|
+
_process_file, root_path=root_path, max_bytes=max_bytes, keep_contents=keep_contents,
|
|
314
|
+
)
|
|
315
|
+
use_proc = processes if processes is not None else (
|
|
316
|
+
not keep_contents and len(to_parse) >= _PROCESS_THRESHOLD and (os.cpu_count() or 1) > 1
|
|
317
|
+
)
|
|
318
|
+
if use_proc:
|
|
319
|
+
try:
|
|
320
|
+
workers = jobs or min(8, os.cpu_count() or 2)
|
|
321
|
+
with ProcessPoolExecutor(max_workers=workers) as pool:
|
|
322
|
+
return list(pool.map(worker, to_parse, chunksize=16))
|
|
323
|
+
except Exception:
|
|
324
|
+
pass # fall back to threads if the process pool cannot start
|
|
325
|
+
workers = jobs or min(32, (os.cpu_count() or 4) * 4)
|
|
326
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
327
|
+
return list(pool.map(worker, to_parse))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _process_file(full: Path, root_path: Path, max_bytes: int, keep_contents: bool = False):
|
|
331
|
+
"""Read and analyze one file. Returns (FileStat, todos, scores, text, vendored)
|
|
332
|
+
or None. Top-level and picklable so it works with a process pool."""
|
|
304
333
|
fn = full.name
|
|
305
334
|
ext = fn.rsplit(".", 1)[-1] if "." in fn else ""
|
|
306
335
|
language = lang_for(ext, fn)
|
|
@@ -321,7 +350,8 @@ def _process_file(full: Path, root_path: Path, max_bytes: int):
|
|
|
321
350
|
if not vendored:
|
|
322
351
|
_collect_todos(text, rel, language, todos)
|
|
323
352
|
_score_complexity(text, rel, language, scores)
|
|
324
|
-
|
|
353
|
+
# Avoid shipping file text back across a process boundary unless needed.
|
|
354
|
+
return stat, todos, scores, (text if keep_contents else None), vendored
|
|
325
355
|
|
|
326
356
|
|
|
327
357
|
def _walk(root_path: Path, ignore: set):
|
|
@@ -7,6 +7,20 @@ from repoglance.cli import main
|
|
|
7
7
|
from repoglance.config import load_config
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
def test_process_and_thread_modes_agree(tmp_path):
|
|
11
|
+
from repoglance.scanner import scan
|
|
12
|
+
|
|
13
|
+
for i in range(5):
|
|
14
|
+
(tmp_path / f"m{i}.py").write_text(
|
|
15
|
+
f"def f{i}(x):\n if x:\n return {i}\n return 0\n", encoding="utf-8"
|
|
16
|
+
)
|
|
17
|
+
threaded = scan(tmp_path, processes=False)
|
|
18
|
+
forked = scan(tmp_path, processes=True)
|
|
19
|
+
assert [f.path for f in threaded.files] == [f.path for f in forked.files]
|
|
20
|
+
assert threaded.total_code == forked.total_code
|
|
21
|
+
assert {s.name for s in threaded.func_scores} == {s.name for s in forked.func_scores}
|
|
22
|
+
|
|
23
|
+
|
|
10
24
|
def test_tally_counts_and_orders():
|
|
11
25
|
pairs = gitinfo._tally("alice\nbob\nalice\n\nalice\n")
|
|
12
26
|
assert pairs[0] == ("alice", 3)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|