pretty-git 0.1.4 → 0.1.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -1
- data/README.md +78 -6
- data/README.ru.md +76 -3
- data/lib/pretty_git/analytics/languages.rb +54 -15
- data/lib/pretty_git/app.rb +22 -15
- data/lib/pretty_git/cli.rb +1 -3
- data/lib/pretty_git/cli_helpers.rb +145 -19
- data/lib/pretty_git/constants.rb +15 -0
- data/lib/pretty_git/filters.rb +41 -22
- data/lib/pretty_git/git/provider.rb +38 -13
- data/lib/pretty_git/logger.rb +20 -0
- data/lib/pretty_git/utils/path_utils.rb +30 -0
- data/lib/pretty_git/utils/time_utils.rb +39 -0
- data/lib/pretty_git/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7bd982eee724a56adcb2aa941fdecc2c95309c1bf681a5c3b9a1be0b8bf0306f
|
4
|
+
data.tar.gz: 1ef12cd144493563bbe99a869e81fe6aa9436136dc7aac52b713d5436975a48a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fba3dc59a53571d80cf38e13a555b2ad62b44964f29039a1fe68000349f642d024bb854542d62d58608dd8f9a8169548b0444a0fc8488cccc9a8e3497ba68ade
|
7
|
+
data.tar.gz: 1f8037f3e7ef97e13b60be37c6d447c684586ffb78017d1905fc6b75d4d055ce533d4a87f04baf6e15fa734fa20fcbfe8854f5f4ef429c402106cbbf4a9a70b5
|
data/CHANGELOG.md
CHANGED
@@ -5,7 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
|
-
|
8
|
+
### Added
|
9
|
+
- Docs: `docs/testing.md` covering the golden workflow, snapshot update/validation; linked from `README.md`, `README.ru.md`, and `CONTRIBUTING.md`.
|
10
|
+
- Tests: determinism invariants for YAML/XML renderers (stable output regardless of input order).
|
11
|
+
|
12
|
+
### Changed
|
13
|
+
- Completions: refreshed bash/zsh completions — added short flags `-f`/`-o`/`-l`, values for `--time-bucket` (`day|week|month`), and repo path completion as the 2nd positional argument.
|
14
|
+
|
15
|
+
## [0.1.5] - 2025-08-17
|
16
|
+
### Added
|
17
|
+
- CLI: warn to stderr when `--theme`/`--no-color` are used with non-console `--format` values.
|
18
|
+
- Docs: expanded Filters documentation (branches, authors, paths, time semantics, verbose diagnostics), schemas/examples pointers, performance and CI usage.
|
19
|
+
- Tests: unit tests for `PrettyGit::Utils::TimeUtils`.
|
20
|
+
|
21
|
+
### Changed
|
22
|
+
- Internals: extracted time parsing/normalization to `PrettyGit::Utils::TimeUtils` and centralized verbose logging via `PrettyGit::Logger`. `Git::Provider` routes verbose messages through the centralized logger (stderr).
|
23
|
+
- Verbose mode: documentation clarified to note that diagnostics are printed to stderr for easier CI parsing.
|
24
|
+
|
25
|
+
### Deprecated
|
26
|
+
- Filters: legacy `:until` keyword in `PrettyGit::Filters` initialization is accepted for backward compatibility and emits a deprecation warning; use `:until_at` instead.
|
27
|
+
|
28
|
+
### Fixed
|
29
|
+
- Filters: allow initialization via a single Hash argument (legacy call sites) while preserving `Struct` keyword semantics.
|
9
30
|
|
10
31
|
## [0.1.4] - 2025-08-17
|
11
32
|
### Added
|
data/README.md
CHANGED
@@ -58,6 +58,13 @@ Generator of rich reports for a local Git repository: summary, activity, authors
|
|
58
58
|
* **Exports**: `console`, `json`, `csv`, `md`, `yaml`, `xml`.
|
59
59
|
* **Output**: to stdout or file via `--out`.
|
60
60
|
|
61
|
+
## ❓ Why Pretty Git
|
62
|
+
* **One tool, many views**: activity, authorship, hotspots, languages, ownership — consistent UX and outputs.
|
63
|
+
* **Deterministic results**: stable sorting and formatting make it reliable for CI and diffs.
|
64
|
+
* **Format-first**: JSON/CSV/Markdown/YAML/XML out of the box with strict and documented rules.
|
65
|
+
* **Fast enough for daily use**: streams `git log` and aggregates in-memory; tips below for large repos.
|
66
|
+
* **Safe defaults**: sensible path and binary ignores for the `languages` report; colorized console output with themes.
|
67
|
+
|
61
68
|
## ⚙️ Requirements
|
62
69
|
* **Ruby**: >= 3.4 (recommended 3.4.x)
|
63
70
|
* **Git**: installed and available in `PATH`
|
@@ -142,12 +149,15 @@ Key options:
|
|
142
149
|
* **--no-color** Disable colors in console
|
143
150
|
* **--theme** `basic|bright|mono` — console theme (default `basic`; `mono` forces monochrome)
|
144
151
|
* **--metric** `bytes|files|loc` — metric for `languages` report (default `bytes`)
|
152
|
+
* **--verbose** Print debug information (effective git command, filters)
|
145
153
|
|
146
154
|
Examples with multiple values:
|
147
155
|
|
148
156
|
```bash
|
149
|
-
# Multiple branches
|
157
|
+
# Multiple branches (treated as explicit revisions)
|
150
158
|
pretty-git summary . --branch main --branch develop
|
159
|
+
## This is equivalent to:
|
160
|
+
## git log main develop -- ...
|
151
161
|
|
152
162
|
# Filter authors (include/exclude)
|
153
163
|
pretty-git authors . --author alice@example.com --exclude-author bot@company
|
@@ -157,7 +167,30 @@ pretty-git files . --path app,lib --exclude-path vendor,node_modules
|
|
157
167
|
```
|
158
168
|
|
159
169
|
### Filters
|
160
|
-
Filters apply at commit fetch and later aggregation.
|
170
|
+
Filters apply at commit fetch and later aggregation. Below — exact semantics and tips.
|
171
|
+
|
172
|
+
#### Branches / revisions
|
173
|
+
* `--branch BRANCH` may be provided multiple times.
|
174
|
+
* Multiple branches are treated as explicit revisions to `git log` (no implicit merge-base range). Example: `--branch main --branch develop` → `git log main develop -- ...`.
|
175
|
+
* If no branches are specified, the repository’s current `HEAD` is used.
|
176
|
+
|
177
|
+
#### Authors
|
178
|
+
* `--author` and `--exclude-author` accept name or email substrings (case-insensitive match by `git log`).
|
179
|
+
* Multiple values may be provided by repeating the option.
|
180
|
+
|
181
|
+
#### Paths
|
182
|
+
* `--path` and `--exclude-path` accept comma-separated values or repeated options.
|
183
|
+
* Globs are supported by git pathspec. Excludes are translated to `:(exclude)pattern` and applied consistently.
|
184
|
+
* When only excludes are present, `.` is included to ensure the pathspec is valid (mirrors tests in `spec/pretty_git/git/provider_spec.rb`).
|
185
|
+
|
186
|
+
#### Time period
|
187
|
+
* `--since` / `--until`: ISO8601 (e.g. `2025-01-31T12:00:00Z`) or `YYYY-MM-DD`.
|
188
|
+
* Date-only values are interpreted as UTC midnight to avoid timezone drift in different environments.
|
189
|
+
* Time values are normalized to UTC in outputs.
|
190
|
+
|
191
|
+
#### Verbose diagnostics
|
192
|
+
* `--verbose` prints the effective `git log` command and active filters to stderr.
|
193
|
+
* Useful for debugging filters, CI logs, or when reproducing results locally.
|
161
194
|
|
162
195
|
### Output format
|
163
196
|
Set via `--format`. For file formats it’s recommended to use `--out`.
|
@@ -397,6 +430,13 @@ _Example terminal output (theme: basic)._
|
|
397
430
|
{"report":"summary","generated_at":"2025-01-31T00:00:00Z","totals":{"commits":123}}
|
398
431
|
```
|
399
432
|
|
433
|
+
## 🧾 Schemas and Examples
|
434
|
+
Machine-readable examples and schemas live under `docs/export_schemas/` and `docs/examples/`.
|
435
|
+
|
436
|
+
* **Schemas**: see `docs/export_schemas/README.md` for JSON and XML schema notes.
|
437
|
+
* **Examples**: example payloads for JSON/XML for each report under `docs/examples/`.
|
438
|
+
* Intended use: validation in CI, contract documentation, and integration tests.
|
439
|
+
|
400
440
|
### CSV
|
401
441
|
* **Structure**: flat table, first line is header.
|
402
442
|
* **Encoding**: UTF‑8 without BOM.
|
@@ -495,16 +535,46 @@ These lists mirror the implementation in `lib/pretty_git/analytics/languages.rb`
|
|
495
535
|
## 🔁 Determinism and Sorting
|
496
536
|
Output is deterministic given the same input. Sorting for files/authors: by changes (desc), then by commits (desc), then by path/name (asc). Limits are applied after sorting; `all` or `0` means no limit.
|
497
537
|
|
538
|
+
## ⚡ Performance Tips
|
539
|
+
* Prefer narrowing by `--path`/`--exclude-path` and `--since/--until` on large repositories.
|
540
|
+
* Use multiple `--branch` only when you explicitly want to include several heads; otherwise rely on current `HEAD`.
|
541
|
+
* For CI, cache the repository and fetch shallow history if full history is unnecessary for your report.
|
542
|
+
|
543
|
+
## 🤖 CI Usage
|
544
|
+
Examples for common pipelines:
|
545
|
+
|
546
|
+
```yaml
|
547
|
+
# GitHub Actions (excerpt)
|
548
|
+
jobs:
|
549
|
+
reports:
|
550
|
+
runs-on: ubuntu-latest
|
551
|
+
steps:
|
552
|
+
- uses: actions/checkout@v4
|
553
|
+
- uses: ruby/setup-ruby@v1
|
554
|
+
with:
|
555
|
+
ruby-version: '3.4'
|
556
|
+
- run: gem install pretty-git
|
557
|
+
- run: pretty-git authors . --format json --out authors.json
|
558
|
+
- uses: actions/upload-artifact@v4
|
559
|
+
with:
|
560
|
+
name: authors-report
|
561
|
+
path: authors.json
|
562
|
+
```
|
563
|
+
|
498
564
|
## 🪟 Windows Notes
|
499
|
-
Primary targets — macOS/Linux. Windows is supported best‑effort
|
500
|
-
|
501
|
-
|
502
|
-
*
|
565
|
+
Primary targets — macOS/Linux. Windows is supported best‑effort. See detailed notes in [docs/windows.md](docs/windows.md).
|
566
|
+
|
567
|
+
Highlights:
|
568
|
+
* Running via Git Bash/WSL is recommended.
|
569
|
+
* CRLF output from git is handled by the parser; exports use UTF‑8 with LF.
|
570
|
+
* Path filters are normalized to Unicode NFC when available; otherwise pass‑through.
|
571
|
+
* Colors can be disabled by `--no-color` or `--theme mono`.
|
503
572
|
|
504
573
|
## 🩺 Diagnostics and Errors
|
505
574
|
Typical issues and solutions:
|
506
575
|
|
507
576
|
* **Unknown report/format** — check the first argument and `--format`.
|
577
|
+
* **Debugging** — add `--verbose` to see the effective `git log` command and applied filters.
|
508
578
|
* **Invalid date format** — use ISO8601 or `YYYY-MM-DD` (e.g., `2025-01-31` or `2025-01-31T12:00:00Z`).
|
509
579
|
* **Git not available** — ensure `git` is installed and in the `PATH`.
|
510
580
|
* **Empty result** — check your filters (`--since/--until`, `--branch`, `--path`); your selection might be too narrow.
|
@@ -527,5 +597,7 @@ bundle exec rubocop
|
|
527
597
|
|
528
598
|
Style — RuboCop clean. Tests cover aggregators, renderers, CLI, and integration scenarios (determinism, format correctness).
|
529
599
|
|
600
|
+
For detailed testing strategy, determinism rules, and golden tests workflow (how to run/update snapshots), see `docs/testing.md`.
|
601
|
+
|
530
602
|
## 📄 License
|
531
603
|
MIT © Contributors
|
data/README.ru.md
CHANGED
@@ -20,6 +20,7 @@
|
|
20
20
|
|
21
21
|
## Содержание
|
22
22
|
- [Возможности](#возможности)
|
23
|
+
- [Почему Pretty Git](#почему-pretty-git)
|
23
24
|
- [Требования](#требования)
|
24
25
|
- [Установка](#установка)
|
25
26
|
- [Быстрый старт](#быстрый-старт)
|
@@ -41,11 +42,14 @@
|
|
41
42
|
- [Экспорт в форматы](#экспорт-в-форматы)
|
42
43
|
- [Console](#console)
|
43
44
|
- [JSON](#json)
|
45
|
+
- [Схемы и примеры](#схемы-и-примеры)
|
44
46
|
- [CSV](#csv)
|
45
47
|
- [Markdown](#markdown)
|
46
48
|
- [YAML](#yaml)
|
47
49
|
- [XML](#xml)
|
48
50
|
- [Детерминизм и сортировка](#детерминизм-и-сортировка)
|
51
|
+
- [Советы по производительности](#советы-по-производительности)
|
52
|
+
- [Использование в CI](#использование-в-ci)
|
49
53
|
- [Советы по Windows](#советы-по-windows)
|
50
54
|
- [Диагностика и ошибки](#диагностика-и-ошибки)
|
51
55
|
- [FAQ](#faq)
|
@@ -58,6 +62,13 @@
|
|
58
62
|
* __Экспорт__: `console`, `json`, `csv`, `md`, `yaml`, `xml`.
|
59
63
|
* __Вывод__: в stdout или файл через `--out`.
|
60
64
|
|
65
|
+
## ❓ Почему Pretty Git
|
66
|
+
* __Один инструмент — много представлений__: активность, авторство, риск, языки, владение — единый UX и форматы.
|
67
|
+
* __Детерминированные результаты__: стабильные сортировки и форматирование — удобно для CI и диффов.
|
68
|
+
* __Сразу форматы__: JSON/CSV/Markdown/YAML/XML из коробки с чёткими правилами.
|
69
|
+
* __Достаточно быстро__: потоковый `git log` и ин‑мемори агрегации; советы для больших репозиториев ниже.
|
70
|
+
* __Безопасные дефолты__: разумные игноры путей и бинарников для отчёта `languages`; цветной консольный вывод с темами.
|
71
|
+
|
61
72
|
## ⚙️ Требования
|
62
73
|
* __Ruby__: >= 3.4 (рекомендуется 3.4.x)
|
63
74
|
* __Git__: установлен и доступен в `PATH`
|
@@ -142,12 +153,15 @@ pretty-git <report> <repo_path> [options]
|
|
142
153
|
* __--no-color__ Отключить цвета в консоли
|
143
154
|
* __--theme__ `basic|bright|mono` — тема оформления консольного вывода (по умолчанию `basic`; `mono` принудительно отключает цвета)
|
144
155
|
* __--metric__ `bytes|files|loc` — метрика для отчёта `languages` (по умолчанию `bytes`)
|
156
|
+
* **--verbose** Печатать отладочную информацию (эффективная команда git, применённые фильтры)
|
145
157
|
|
146
158
|
Примеры значений с несколькими параметрами:
|
147
159
|
|
148
160
|
```bash
|
149
|
-
# Несколько веток
|
161
|
+
# Несколько веток (трактуются как явные ревизии)
|
150
162
|
pretty-git summary . --branch main --branch develop
|
163
|
+
## Эквивалентно:
|
164
|
+
## git log main develop -- ...
|
151
165
|
|
152
166
|
# Фильтрация по авторам (включая/исключая)
|
153
167
|
pretty-git authors . --author alice@example.com --exclude-author bot@company
|
@@ -157,7 +171,30 @@ pretty-git files . --path app,lib --exclude-path vendor,node_modules
|
|
157
171
|
```
|
158
172
|
|
159
173
|
### Фильтры
|
160
|
-
Фильтры применяются на этапе выборки коммитов и последующей агрегации.
|
174
|
+
Фильтры применяются на этапе выборки коммитов и последующей агрегации. Ниже — точные правила и советы.
|
175
|
+
|
176
|
+
#### Ветки / ревизии
|
177
|
+
* `--branch BRANCH` можно указывать несколько раз.
|
178
|
+
* Несколько веток трактуются как явные ревизии для `git log` (без неявного диапазона от merge-base). Пример: `--branch main --branch develop` → `git log main develop -- ...`.
|
179
|
+
* Если ветки не указаны — используется текущий `HEAD` репозитория.
|
180
|
+
|
181
|
+
#### Авторы
|
182
|
+
* `--author` и `--exclude-author` принимают подстроки имени или email (регистронезависимо по логике `git log`).
|
183
|
+
* Можно передавать несколько значений повтором опции.
|
184
|
+
|
185
|
+
#### Пути
|
186
|
+
* `--path` и `--exclude-path` принимают значения через запятую или повтором опции.
|
187
|
+
* Поддерживаются glob‑маски git pathspec. Исключения переводятся в `:(exclude)pattern` и применяются последовательно.
|
188
|
+
* Если заданы только исключения — автоматически добавляется `.` для корректного pathspec (как в тестах `spec/pretty_git/git/provider_spec.rb`).
|
189
|
+
|
190
|
+
#### Период времени
|
191
|
+
* `--since` / `--until`: ISO8601 (например, `2025-01-31T12:00:00Z`) или `YYYY-MM-DD`.
|
192
|
+
* Даты без времени интерпретируются как полночь UTC, чтобы исключить сдвиги между средами.
|
193
|
+
* На выводе время нормализуется к UTC.
|
194
|
+
|
195
|
+
#### Подробная диагностика (verbose)
|
196
|
+
* `--verbose` печатает эффективную команду `git log` и активные фильтры в stderr.
|
197
|
+
* Полезно для отладки фильтров, логов CI и воспроизведения результатов локально.
|
161
198
|
|
162
199
|
### Формат вывода
|
163
200
|
Задаётся через `--format`. Для файловых форматов рекомендуется использовать `--out`.
|
@@ -423,6 +460,13 @@ _Пример вывода в терминале (тема: basic)._
|
|
423
460
|
{"report":"summary","generated_at":"2025-01-31T00:00:00Z","totals":{"commits":123}}
|
424
461
|
```
|
425
462
|
|
463
|
+
## 🧾 Схемы и примеры
|
464
|
+
Машиночитаемые примеры и схемы лежат в `docs/export_schemas/` и `docs/examples/`.
|
465
|
+
|
466
|
+
* __Схемы__: смотрите `docs/export_schemas/README.md` с заметками по JSON и XML схемам.
|
467
|
+
* __Примеры__: примеры нагрузок для JSON/XML по каждому отчёту — в `docs/examples/`.
|
468
|
+
* Назначение: валидация в CI, документация контрактов и интеграционные тесты.
|
469
|
+
|
426
470
|
### CSV
|
427
471
|
* __Структура__: плоская таблица, первая строка — заголовок.
|
428
472
|
* __Кодировка__: UTF‑8, без BOM.
|
@@ -496,6 +540,32 @@ _Пример вывода в терминале (тема: basic)._
|
|
496
540
|
## 🔁 Детерминизм и сортировка
|
497
541
|
Вывод детерминирован при одинаковых входных данных. Сортировка для файлов/авторов: по количеству изменений (desc), затем по числу коммитов (desc), затем по пути/имени (asc). Лимиты применяются поверх отсортированного списка; значение `all` или `0` означает отсутствие ограничения.
|
498
542
|
|
543
|
+
## ⚡ Советы по производительности
|
544
|
+
* На больших репозиториях сужайте выборку `--path/--exclude-path` и `--since/--until`.
|
545
|
+
* Несколько `--branch` используйте, только если нужно включить несколько голов; иначе полагайтесь на текущий `HEAD`.
|
546
|
+
* В CI кешируйте репозиторий и используйте shallow‑fetch, если полная история не нужна для выбранного отчёта.
|
547
|
+
|
548
|
+
## 🤖 Использование в CI
|
549
|
+
Пример для GitHub Actions:
|
550
|
+
|
551
|
+
```yaml
|
552
|
+
# GitHub Actions (фрагмент)
|
553
|
+
jobs:
|
554
|
+
reports:
|
555
|
+
runs-on: ubuntu-latest
|
556
|
+
steps:
|
557
|
+
- uses: actions/checkout@v4
|
558
|
+
- uses: ruby/setup-ruby@v1
|
559
|
+
with:
|
560
|
+
ruby-version: '3.4'
|
561
|
+
- run: gem install pretty-git
|
562
|
+
- run: pretty-git authors . --format json --out authors.json
|
563
|
+
- uses: actions/upload-artifact@v4
|
564
|
+
with:
|
565
|
+
name: authors-report
|
566
|
+
path: authors.json
|
567
|
+
```
|
568
|
+
|
499
569
|
## 🪟 Советы по Windows
|
500
570
|
Целевая платформа — macOS/Linux. Windows поддерживается в режиме best‑effort:
|
501
571
|
* Запуск через Git Bash/WSL допустим
|
@@ -505,7 +575,8 @@ _Пример вывода в терминале (тема: basic)._
|
|
505
575
|
## 🩺 Диагностика и ошибки
|
506
576
|
Типичные ошибки и решения:
|
507
577
|
|
508
|
-
*
|
578
|
+
* **Неизвестный отчёт/формат** — проверьте первый аргумент и `--format`.
|
579
|
+
* **Отладка** — добавьте `--verbose`, чтобы увидеть фактическую команду `git log` и применённые фильтры.
|
509
580
|
* __Неверный формат даты__ — используйте ISO8601 или `YYYY-MM-DD` (например, `2025-01-31` или `2025-01-31T12:00:00Z`).
|
510
581
|
* __Git недоступен__ — убедитесь, что `git` установлен и доступен в `PATH`.
|
511
582
|
* __Пустой результат__ — проверьте фильтры (`--since/--until`, `--branch`, `--path`), возможно, выборка слишком узкая.
|
@@ -528,5 +599,7 @@ bundle exec rubocop
|
|
528
599
|
|
529
600
|
Стиль — RuboCop без ошибок. Тесты покрывают агрегаторы, рендереры, CLI и интеграционные сценарии (детерминизм, корректность форматов).
|
530
601
|
|
602
|
+
Подробнее о стратегии тестирования, правилах детерминизма и процессе работы с golden‑тестами (как запускать/обновлять снапшоты) см. `docs/testing.md`.
|
603
|
+
|
531
604
|
## 📄 Лицензия
|
532
605
|
MIT © Contributors
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'time'
|
4
|
+
require 'json'
|
5
|
+
require 'find'
|
4
6
|
|
5
7
|
module PrettyGit
|
6
8
|
module Analytics
|
@@ -79,30 +81,46 @@ module PrettyGit
|
|
79
81
|
# Default metric: bytes (similar to GitHub Linguist approach).
|
80
82
|
# rubocop:disable Metrics/ClassLength
|
81
83
|
class Languages
|
84
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
82
85
|
def self.call(_enum, filters)
|
83
86
|
repo = filters.repo_path
|
84
|
-
|
87
|
+
prof = ENV['PG_PROF'] == '1'
|
88
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) if prof
|
85
89
|
metric = (filters.metric || 'bytes').to_s
|
90
|
+
items = calculate(repo, include_globs: filters.paths, exclude_globs: filters.exclude_paths, metric: metric)
|
86
91
|
totals = compute_totals(items)
|
87
92
|
items = add_percents(items, totals, metric)
|
88
93
|
items = add_colors(items)
|
89
94
|
items = sort_and_limit(items, filters.limit, metric)
|
90
95
|
|
91
|
-
build_result(repo, items, totals, metric)
|
96
|
+
res = build_result(repo, items, totals, metric)
|
97
|
+
if prof
|
98
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
99
|
+
elapsed = (t1 - t0)
|
100
|
+
files = totals[:files]
|
101
|
+
warn format(
|
102
|
+
'[pg_prof] languages: time=%<sec>.3fs files=%<files>d metric=%<metric>s',
|
103
|
+
{ sec: elapsed, files: files, metric: metric }
|
104
|
+
)
|
105
|
+
summary = { component: 'languages', time_sec: elapsed, files: files, metric: metric }
|
106
|
+
warn("[pg_prof_json] #{summary.to_json}")
|
107
|
+
end
|
108
|
+
res
|
92
109
|
end
|
110
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
93
111
|
|
94
112
|
# rubocop:disable Metrics/AbcSize
|
95
|
-
def self.calculate(repo_path, include_globs:, exclude_globs:)
|
113
|
+
def self.calculate(repo_path, include_globs:, exclude_globs:, metric: 'bytes')
|
96
114
|
by_lang = Hash.new { |h, k| h[k] = { bytes: 0, files: 0, loc: 0 } }
|
97
115
|
Dir.chdir(repo_path) do
|
98
|
-
each_source_file(include_globs, exclude_globs) do |
|
99
|
-
basename = File.basename(
|
100
|
-
ext = File.extname(
|
116
|
+
each_source_file(include_globs, exclude_globs) do |path|
|
117
|
+
basename = File.basename(path)
|
118
|
+
ext = File.extname(path).downcase
|
101
119
|
lang = FILENAME_TO_LANG[basename] || EXT_TO_LANG[ext]
|
102
120
|
next unless lang
|
103
121
|
|
104
|
-
size = safe_file_size(
|
105
|
-
lines = safe_count_lines(
|
122
|
+
size = safe_file_size(path)
|
123
|
+
lines = metric == 'loc' ? safe_count_lines(path) : 0
|
106
124
|
agg = by_lang[lang]
|
107
125
|
agg[:bytes] += size
|
108
126
|
agg[:files] += 1
|
@@ -113,14 +131,33 @@ module PrettyGit
|
|
113
131
|
end
|
114
132
|
# rubocop:enable Metrics/AbcSize
|
115
133
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
files =
|
134
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
135
|
+
def self.each_source_file(include_globs, exclude_globs, &block)
|
136
|
+
# Traverse tree with early prune for vendor/binary paths, then apply include/exclude
|
137
|
+
files = []
|
138
|
+
Find.find('.') do |path|
|
139
|
+
rel = path.sub(%r{^\./}, '')
|
140
|
+
# Prune vendor dirs early
|
141
|
+
if File.directory?(path)
|
142
|
+
dir = File.basename(path)
|
143
|
+
if VENDOR_DIRS.include?(dir)
|
144
|
+
Find.prune
|
145
|
+
next
|
146
|
+
end
|
147
|
+
next
|
148
|
+
end
|
149
|
+
next unless File.file?(path)
|
150
|
+
next if rel.empty?
|
151
|
+
next if vendor_path?(rel) || binary_ext?(rel)
|
152
|
+
|
153
|
+
files << rel
|
154
|
+
end
|
155
|
+
|
120
156
|
files = filter_includes(files, include_globs)
|
121
157
|
files = filter_excludes(files, exclude_globs)
|
122
|
-
files.each { |rel|
|
158
|
+
files.each { |rel| block.call(rel) }
|
123
159
|
end
|
160
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
124
161
|
|
125
162
|
def self.safe_file_size(path)
|
126
163
|
File.size(path)
|
@@ -141,7 +178,8 @@ module PrettyGit
|
|
141
178
|
return files if globs.empty?
|
142
179
|
|
143
180
|
allowed = globs.flat_map { |g| Dir.glob(g) }
|
144
|
-
|
181
|
+
allowed_map = allowed.each_with_object({}) { |f, h| h[f] = true }
|
182
|
+
files.select { |f| allowed_map[f] }
|
145
183
|
end
|
146
184
|
|
147
185
|
def self.filter_excludes(files, globs)
|
@@ -149,7 +187,8 @@ module PrettyGit
|
|
149
187
|
return files if globs.empty?
|
150
188
|
|
151
189
|
blocked = globs.flat_map { |g| Dir.glob(g) }
|
152
|
-
|
190
|
+
blocked_map = blocked.each_with_object({}) { |f, h| h[f] = true }
|
191
|
+
files.reject { |f| blocked_map[f] }
|
153
192
|
end
|
154
193
|
|
155
194
|
def self.vendor_path?(path)
|
data/lib/pretty_git/app.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'open3'
|
3
4
|
require_relative 'git/provider'
|
4
5
|
require_relative 'analytics/summary'
|
5
6
|
require_relative 'analytics/activity'
|
@@ -37,7 +38,9 @@ module PrettyGit
|
|
37
38
|
private
|
38
39
|
|
39
40
|
def ensure_repo!(path)
|
40
|
-
|
41
|
+
# Use git to reliably detect work-trees/worktrees/bare repos
|
42
|
+
stdout, _stderr, status = Open3.capture3('git', 'rev-parse', '--is-inside-work-tree', chdir: path)
|
43
|
+
return if status.success? && stdout.to_s.strip == 'true'
|
41
44
|
|
42
45
|
raise ArgumentError, "Not a git repository: #{path}"
|
43
46
|
end
|
@@ -47,21 +50,25 @@ module PrettyGit
|
|
47
50
|
end
|
48
51
|
|
49
52
|
def renderer_for(filters, io)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
when 'md'
|
57
|
-
Render::MarkdownRenderer.new(io: io)
|
58
|
-
when 'yaml'
|
59
|
-
Render::YamlRenderer.new(io: io)
|
60
|
-
when 'xml'
|
61
|
-
Render::XmlRenderer.new(io: io)
|
62
|
-
else
|
63
|
-
Render::JsonRenderer.new(io: io)
|
53
|
+
if filters.format == 'console'
|
54
|
+
return Render::ConsoleRenderer.new(
|
55
|
+
io: io,
|
56
|
+
color: !filters.no_color && filters.theme != 'mono',
|
57
|
+
theme: filters.theme
|
58
|
+
)
|
64
59
|
end
|
60
|
+
|
61
|
+
dispatch = {
|
62
|
+
'csv' => Render::CsvRenderer,
|
63
|
+
'md' => Render::MarkdownRenderer,
|
64
|
+
'yaml' => Render::YamlRenderer,
|
65
|
+
'xml' => Render::XmlRenderer,
|
66
|
+
'json' => Render::JsonRenderer
|
67
|
+
}
|
68
|
+
klass = dispatch[filters.format]
|
69
|
+
raise ArgumentError, "Unknown format: #{filters.format}" unless klass
|
70
|
+
|
71
|
+
klass.new(io: io)
|
65
72
|
end
|
66
73
|
|
67
74
|
def analytics_for(report, enum, filters)
|
data/lib/pretty_git/cli.rb
CHANGED
@@ -9,9 +9,6 @@ require_relative 'cli_helpers'
|
|
9
9
|
module PrettyGit
|
10
10
|
# Command-line interface entry point.
|
11
11
|
class CLI
|
12
|
-
SUPPORTED_REPORTS = %w[summary activity authors files heatmap languages hotspots churn ownership].freeze
|
13
|
-
SUPPORTED_FORMATS = %w[console json csv md yaml xml].freeze
|
14
|
-
|
15
12
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
16
13
|
def self.run(argv = ARGV, out: $stdout, err: $stderr)
|
17
14
|
options = {
|
@@ -28,6 +25,7 @@ module PrettyGit
|
|
28
25
|
out: nil,
|
29
26
|
no_color: false,
|
30
27
|
theme: 'basic',
|
28
|
+
_verbose: false,
|
31
29
|
_version: false,
|
32
30
|
_help: false
|
33
31
|
}
|
@@ -1,29 +1,71 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'optparse'
|
4
|
+
require 'fileutils'
|
4
5
|
require_relative 'filters'
|
5
6
|
require_relative 'app'
|
7
|
+
require_relative 'constants'
|
6
8
|
|
7
9
|
module PrettyGit
|
8
10
|
# Helpers extracted from `PrettyGit::CLI` to keep the CLI class small
|
9
11
|
# and RuboCop-compliant. Provides parser configuration and execution utilities.
|
10
12
|
# rubocop:disable Metrics/ModuleLength
|
11
13
|
module CLIHelpers
|
12
|
-
REPORTS =
|
13
|
-
FORMATS =
|
14
|
-
METRICS =
|
14
|
+
REPORTS = PrettyGit::Constants::REPORTS
|
15
|
+
FORMATS = PrettyGit::Constants::FORMATS
|
16
|
+
METRICS = PrettyGit::Constants::METRICS
|
17
|
+
THEMES = PrettyGit::Constants::THEMES
|
15
18
|
|
16
19
|
module_function
|
17
20
|
|
21
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
18
22
|
def configure_parser(opts, options)
|
19
|
-
opts.banner =
|
23
|
+
opts.banner = <<~BANNER
|
24
|
+
Usage: pretty-git [REPORT] [REPO] [options]
|
25
|
+
|
26
|
+
Reports: #{REPORTS.join(', ')}
|
27
|
+
Formats: #{FORMATS.join(', ')}
|
28
|
+
Themes: #{THEMES.join(', ')}
|
29
|
+
BANNER
|
30
|
+
opts.separator('')
|
31
|
+
opts.separator('Repository and branch:')
|
20
32
|
add_repo_options(opts, options)
|
33
|
+
opts.separator('')
|
34
|
+
opts.separator('Time, authors, and bucketing:')
|
21
35
|
add_time_author_options(opts, options)
|
36
|
+
opts.separator('')
|
37
|
+
opts.separator('Paths and limits:')
|
22
38
|
add_path_limit_options(opts, options)
|
39
|
+
opts.separator('')
|
40
|
+
opts.separator('Format and output:')
|
23
41
|
add_format_output_options(opts, options)
|
42
|
+
opts.separator('')
|
43
|
+
opts.separator('Metrics (languages report only):')
|
24
44
|
add_metric_options(opts, options)
|
45
|
+
opts.separator('')
|
46
|
+
opts.separator('Other:')
|
25
47
|
add_misc_options(opts, options)
|
48
|
+
opts.separator('')
|
49
|
+
opts.separator('Examples:')
|
50
|
+
opts.separator(' # Summary in JSON to stdout')
|
51
|
+
opts.separator(' $ pretty-git summary . --format json')
|
52
|
+
opts.separator('')
|
53
|
+
opts.separator(' # Authors since a date with a limit')
|
54
|
+
opts.separator(' $ pretty-git authors ~/repo --since 2025-01-01 --limit 20')
|
55
|
+
opts.separator('')
|
56
|
+
opts.separator(' # Files with includes/excludes (globs)')
|
57
|
+
opts.separator(' $ pretty-git files . --path app/**/*.rb --exclude-path spec/**')
|
58
|
+
opts.separator('')
|
59
|
+
opts.separator(' # Activity bucketed by week to Markdown file')
|
60
|
+
opts.separator(' $ pretty-git activity . --time-bucket week --format md --out out/report.md')
|
61
|
+
opts.separator('')
|
62
|
+
opts.separator(' # Languages by LOC to CSV (redirect to file)')
|
63
|
+
opts.separator(' $ pretty-git languages . --metric loc --format csv > langs.csv')
|
64
|
+
opts.separator('')
|
65
|
+
opts.separator(' # Hotspots on main with a higher limit to YAML')
|
66
|
+
opts.separator(' $ pretty-git hotspots . --branch main --limit 50 --format yaml')
|
26
67
|
end
|
68
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
27
69
|
|
28
70
|
def add_repo_options(opts, options)
|
29
71
|
opts.on('--repo PATH', 'Path to git repository (default: .)') { |val| options[:repo] = val }
|
@@ -47,8 +89,14 @@ module PrettyGit
|
|
47
89
|
def add_format_output_options(opts, options)
|
48
90
|
opts.on('--format FMT', 'console|json|csv|md|yaml|xml') { |val| options[:format] = val }
|
49
91
|
opts.on('--out FILE', 'Output file path') { |val| options[:out] = val }
|
50
|
-
opts.on('--no-color', 'Disable colors in console output')
|
51
|
-
|
92
|
+
opts.on('--no-color', 'Disable colors in console output') do
|
93
|
+
options[:no_color] = true
|
94
|
+
options[:_no_color_provided] = true
|
95
|
+
end
|
96
|
+
opts.on('--theme NAME', 'console color theme: basic|bright|mono') do |val|
|
97
|
+
options[:theme] = val
|
98
|
+
options[:_theme_provided] = true
|
99
|
+
end
|
52
100
|
end
|
53
101
|
|
54
102
|
def add_metric_options(opts, options)
|
@@ -60,6 +108,7 @@ module PrettyGit
|
|
60
108
|
def add_misc_options(opts, options)
|
61
109
|
opts.on('--version', 'Show version') { options[:_version] = true }
|
62
110
|
opts.on('--help', 'Show help') { options[:_help] = true }
|
111
|
+
opts.on('--verbose', 'Verbose output (debug)') { options[:_verbose] = true }
|
63
112
|
end
|
64
113
|
|
65
114
|
def parse_limit(str)
|
@@ -75,9 +124,14 @@ module PrettyGit
|
|
75
124
|
code = handle_version_help(options, parser, out)
|
76
125
|
return code unless code.nil?
|
77
126
|
|
78
|
-
base_ok =
|
127
|
+
base_ok = valid_base?(options)
|
79
128
|
conflicts_ok = validate_conflicts(options, err)
|
80
|
-
|
129
|
+
if base_ok && conflicts_ok
|
130
|
+
early = warn_ignores(options, err)
|
131
|
+
return 0 if early == :early_exit
|
132
|
+
|
133
|
+
return nil
|
134
|
+
end
|
81
135
|
|
82
136
|
print_validation_errors(options, err)
|
83
137
|
1
|
@@ -96,19 +150,47 @@ module PrettyGit
|
|
96
150
|
end
|
97
151
|
|
98
152
|
def valid_report?(report) = REPORTS.include?(report)
|
99
|
-
def valid_theme?(theme) =
|
153
|
+
def valid_theme?(theme) = THEMES.include?(theme)
|
154
|
+
def valid_format?(fmt) = FORMATS.include?(fmt)
|
155
|
+
|
156
|
+
def valid_base?(options)
|
157
|
+
valid_report?(options[:report]) &&
|
158
|
+
valid_theme?(options[:theme]) &&
|
159
|
+
valid_metric?(options[:metric]) &&
|
160
|
+
valid_format?(options[:format])
|
161
|
+
end
|
100
162
|
|
101
163
|
def valid_metric?(metric)
|
102
164
|
metric.nil? || METRICS.include?(metric)
|
103
165
|
end
|
104
166
|
|
105
167
|
def print_validation_errors(options, err)
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
168
|
+
print_report_error(options, err)
|
169
|
+
print_theme_error(options, err)
|
170
|
+
print_format_error(options, err)
|
171
|
+
print_metric_error(options, err)
|
172
|
+
end
|
173
|
+
|
174
|
+
def print_report_error(options, err)
|
175
|
+
return if valid_report?(options[:report])
|
176
|
+
|
177
|
+
err.puts "Unknown report: #{options[:report]}."
|
178
|
+
err.puts "Supported: #{REPORTS.join(', ')}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def print_theme_error(options, err)
|
182
|
+
return if valid_theme?(options[:theme])
|
183
|
+
|
184
|
+
err.puts "Unknown theme: #{options[:theme]}. Supported: #{THEMES.join(', ')}"
|
185
|
+
end
|
186
|
+
|
187
|
+
def print_format_error(options, err)
|
188
|
+
return if valid_format?(options[:format])
|
189
|
+
|
190
|
+
err.puts "Unknown format: #{options[:format]}. Supported: #{FORMATS.join(', ')}"
|
191
|
+
end
|
192
|
+
|
193
|
+
def print_metric_error(options, err)
|
112
194
|
return if valid_metric?(options[:metric])
|
113
195
|
|
114
196
|
err.puts "Unknown metric: #{options[:metric]}. Supported: #{METRICS.join(', ')}"
|
@@ -125,12 +207,45 @@ module PrettyGit
|
|
125
207
|
ok
|
126
208
|
end
|
127
209
|
|
210
|
+
# Print non-fatal warnings for flags that won't have effect with current options
|
211
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
212
|
+
def warn_ignores(options, err)
|
213
|
+
return unless err
|
214
|
+
|
215
|
+
fmt = options[:format]
|
216
|
+
return unless fmt && fmt != 'console'
|
217
|
+
|
218
|
+
err.puts "Warning: --theme has no effect when --format=#{fmt}" if options[:theme]
|
219
|
+
err.puts "Warning: --no-color has no effect when --format=#{fmt}" if options[:no_color]
|
220
|
+
|
221
|
+
# Exit early only if user explicitly provided these console-only flags
|
222
|
+
# AND no other significant options that warrant running the app were provided.
|
223
|
+
return unless options[:_theme_provided] || options[:_no_color_provided]
|
224
|
+
|
225
|
+
significant = significant_options?(options)
|
226
|
+
:early_exit unless significant
|
227
|
+
end
|
228
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
229
|
+
|
230
|
+
def significant_options?(options)
|
231
|
+
return true if options[:out]
|
232
|
+
return true if options[:metric]
|
233
|
+
return true if options[:time_bucket]
|
234
|
+
return true if options[:limit] && options[:limit] != 10
|
235
|
+
|
236
|
+
any_collection_present?(options, %i[branches authors exclude_authors paths exclude_paths])
|
237
|
+
end
|
238
|
+
|
239
|
+
def any_collection_present?(options, keys)
|
240
|
+
keys.any? { |k| options[k]&.any? }
|
241
|
+
end
|
242
|
+
|
128
243
|
def build_filters(options)
|
129
244
|
Filters.new(
|
130
245
|
repo_path: options[:repo],
|
131
246
|
branches: options[:branches],
|
132
247
|
since: options[:since],
|
133
|
-
|
248
|
+
until_at: options[:until],
|
134
249
|
authors: options[:authors],
|
135
250
|
exclude_authors: options[:exclude_authors],
|
136
251
|
paths: options[:paths],
|
@@ -141,14 +256,25 @@ module PrettyGit
|
|
141
256
|
format: options[:format],
|
142
257
|
out: options[:out],
|
143
258
|
no_color: options[:no_color],
|
144
|
-
theme: options[:theme]
|
259
|
+
theme: options[:theme],
|
260
|
+
verbose: options[:_verbose]
|
145
261
|
)
|
146
262
|
end
|
147
263
|
|
148
264
|
def execute(report, filters, options, out, err)
|
149
265
|
if options[:out]
|
150
|
-
|
151
|
-
|
266
|
+
begin
|
267
|
+
dir = File.dirname(options[:out])
|
268
|
+
FileUtils.mkdir_p(dir) unless dir.nil? || dir == '.'
|
269
|
+
File.open(options[:out], 'w') do |f|
|
270
|
+
return PrettyGit::App.new.run(report, filters, out: f, err: err)
|
271
|
+
end
|
272
|
+
rescue Errno::EACCES
|
273
|
+
err.puts "Cannot write to: #{options[:out]} (permission denied)"
|
274
|
+
return 2
|
275
|
+
rescue Errno::ENOENT
|
276
|
+
err.puts "Cannot write to: #{options[:out]} (directory not found)"
|
277
|
+
return 2
|
152
278
|
end
|
153
279
|
end
|
154
280
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrettyGit
|
4
|
+
module Constants
|
5
|
+
REPORTS = %w[
|
6
|
+
summary activity authors files heatmap languages hotspots churn ownership
|
7
|
+
].freeze
|
8
|
+
|
9
|
+
FORMATS = %w[console json csv md yaml xml].freeze
|
10
|
+
|
11
|
+
METRICS = %w[bytes files loc].freeze
|
12
|
+
|
13
|
+
THEMES = %w[basic bright mono].freeze
|
14
|
+
end
|
15
|
+
end
|
data/lib/pretty_git/filters.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'time'
|
4
|
+
require_relative 'utils/time_utils'
|
4
5
|
|
5
6
|
module PrettyGit
|
6
7
|
Filters = Struct.new(
|
7
8
|
:repo_path,
|
8
9
|
:branches,
|
9
10
|
:since,
|
10
|
-
:
|
11
|
+
:until_at,
|
11
12
|
:authors,
|
12
13
|
:exclude_authors,
|
13
14
|
:paths,
|
@@ -19,35 +20,53 @@ module PrettyGit
|
|
19
20
|
:out,
|
20
21
|
:no_color,
|
21
22
|
:theme,
|
23
|
+
:verbose,
|
22
24
|
keyword_init: true
|
23
25
|
) do
|
24
|
-
|
25
|
-
|
26
|
+
# Backward-compat: allow initializing with `until:` keyword by remapping to :until_at
|
27
|
+
# Preserve Struct keyword_init behavior by overriding initialize instead of .new
|
28
|
+
def initialize(*args, **kwargs)
|
29
|
+
# Accept a single Hash positional argument for backward compatibility
|
30
|
+
kwargs = args.first if (kwargs.nil? || kwargs.empty?) && args.length == 1 && args.first.is_a?(Hash)
|
31
|
+
kwargs ||= {}
|
32
|
+
|
33
|
+
if kwargs.key?(:until)
|
34
|
+
Kernel.warn('[pretty-git] DEPRECATION: Filters initialized with :until. Use :until_at instead.')
|
35
|
+
kwargs = kwargs.dup
|
36
|
+
kwargs[:until_at] = kwargs.delete(:until)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Keyword-init struct: prefer keyword form consistently to keep initialize simple
|
40
|
+
super(**kwargs)
|
26
41
|
end
|
27
42
|
|
28
|
-
|
29
|
-
|
43
|
+
# Backward-compat: support filters.until and filters.until=
|
44
|
+
def until
|
45
|
+
self[:until_at]
|
46
|
+
end
|
47
|
+
|
48
|
+
def until=(val)
|
49
|
+
self[:until_at] = val
|
30
50
|
end
|
31
51
|
|
32
|
-
|
52
|
+
# Backward-compat for hash-style access used in older specs
|
53
|
+
def [](key)
|
54
|
+
key = :until_at if key == :until
|
55
|
+
super
|
56
|
+
end
|
33
57
|
|
34
|
-
|
35
|
-
|
36
|
-
|
58
|
+
def []=(key, value)
|
59
|
+
key = :until_at if key == :until
|
60
|
+
super
|
61
|
+
end
|
37
62
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
t = val.is_a?(Time) ? val : Time.parse(val.to_s)
|
46
|
-
end
|
47
|
-
t.utc.iso8601
|
48
|
-
rescue ArgumentError
|
49
|
-
raise ArgumentError, "Invalid datetime: #{val} (expected ISO8601 or YYYY-MM-DD)"
|
63
|
+
def since_iso8601
|
64
|
+
PrettyGit::Utils::TimeUtils.to_utc_iso8601(since)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Keep method name for backwards compatibility across the codebase
|
68
|
+
def until_iso8601
|
69
|
+
PrettyGit::Utils::TimeUtils.to_utc_iso8601(self[:until_at])
|
50
70
|
end
|
51
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
52
71
|
end
|
53
72
|
end
|
@@ -2,7 +2,10 @@
|
|
2
2
|
|
3
3
|
require 'open3'
|
4
4
|
require 'time'
|
5
|
+
require 'json'
|
5
6
|
require_relative '../types'
|
7
|
+
require_relative '../logger'
|
8
|
+
require_relative '../utils/path_utils'
|
6
9
|
|
7
10
|
module PrettyGit
|
8
11
|
module Git
|
@@ -17,10 +20,18 @@ module PrettyGit
|
|
17
20
|
end
|
18
21
|
|
19
22
|
# Returns Enumerator of PrettyGit::Types::Commit
|
20
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
23
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
21
24
|
def each_commit
|
22
25
|
Enumerator.new do |yld|
|
26
|
+
prof = ENV['PG_PROF'] == '1'
|
27
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
28
|
+
headers = 0
|
29
|
+
numstat_lines = 0
|
23
30
|
cmd = build_git_command
|
31
|
+
PrettyGit::Logger.verbose(
|
32
|
+
"[pretty-git] git cmd: #{cmd.join(' ')} (cwd=#{@filters.repo_path})",
|
33
|
+
@filters.verbose
|
34
|
+
)
|
24
35
|
Open3.popen3(*cmd, chdir: @filters.repo_path) do |_stdin, stdout, stderr, wait_thr|
|
25
36
|
current = nil
|
26
37
|
stdout.each_line do |line|
|
@@ -28,6 +39,7 @@ module PrettyGit
|
|
28
39
|
# Try to start a new commit from header on any line
|
29
40
|
header = start_commit_from_header(line)
|
30
41
|
if header
|
42
|
+
headers += 1 if prof
|
31
43
|
# emit previous commit if any
|
32
44
|
emit_current(yld, current)
|
33
45
|
current = header
|
@@ -36,6 +48,7 @@ module PrettyGit
|
|
36
48
|
|
37
49
|
next if line.empty?
|
38
50
|
|
51
|
+
numstat_lines += 1 if prof
|
39
52
|
append_numstat_line(current, line)
|
40
53
|
end
|
41
54
|
|
@@ -47,9 +60,26 @@ module PrettyGit
|
|
47
60
|
raise StandardError, (err && !err.empty? ? err : "git log failed with status #{status.exitstatus}")
|
48
61
|
end
|
49
62
|
end
|
63
|
+
|
64
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
65
|
+
if prof
|
66
|
+
# Emit a compact profile to stderr
|
67
|
+
elapsed = (t1 - t0)
|
68
|
+
warn format(
|
69
|
+
'[pg_prof] git_provider: time=%<sec>.3fs headers=%<headers>d numstat_lines=%<num>d',
|
70
|
+
{ sec: elapsed, headers: headers, num: numstat_lines }
|
71
|
+
)
|
72
|
+
summary = {
|
73
|
+
component: 'git_provider',
|
74
|
+
time_sec: elapsed,
|
75
|
+
headers: headers,
|
76
|
+
numstat_lines: numstat_lines
|
77
|
+
}
|
78
|
+
warn("[pg_prof_json] #{summary.to_json}")
|
79
|
+
end
|
50
80
|
end
|
51
81
|
end
|
52
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
82
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
53
83
|
|
54
84
|
private
|
55
85
|
|
@@ -73,10 +103,6 @@ module PrettyGit
|
|
73
103
|
yld << commit
|
74
104
|
end
|
75
105
|
|
76
|
-
def record_separator?(line)
|
77
|
-
line == SEP_RECORD
|
78
|
-
end
|
79
|
-
|
80
106
|
def start_commit_from_header(line)
|
81
107
|
sha, author_name, author_email, authored_at, subject = line.split(SEP_FIELD, 5)
|
82
108
|
return nil unless subject
|
@@ -121,13 +147,13 @@ module PrettyGit
|
|
121
147
|
|
122
148
|
def add_author_and_branch_filters(args)
|
123
149
|
@filters.authors&.each { |a| args << "--author=#{a}" }
|
124
|
-
|
150
|
+
# Treat branches as explicit revisions to include
|
151
|
+
@filters.branches&.each { |b| args << b }
|
125
152
|
end
|
126
153
|
|
127
|
-
# rubocop:disable Metrics/AbcSize
|
128
154
|
def add_path_filters(args)
|
129
|
-
path_args =
|
130
|
-
exclude_args =
|
155
|
+
path_args = PrettyGit::Utils::PathUtils.normalize_globs(@filters.paths)
|
156
|
+
exclude_args = PrettyGit::Utils::PathUtils.normalize_globs(@filters.exclude_paths)
|
131
157
|
|
132
158
|
# Nothing to filter by
|
133
159
|
return if path_args.empty? && exclude_args.empty?
|
@@ -137,15 +163,14 @@ module PrettyGit
|
|
137
163
|
# If only excludes provided, include all paths first
|
138
164
|
args << '.' if path_args.empty? && !exclude_args.empty?
|
139
165
|
|
140
|
-
# Include patterns
|
166
|
+
# Include patterns (normalized)
|
141
167
|
args.concat(path_args) unless path_args.empty?
|
142
168
|
|
143
|
-
# Exclude patterns via git pathspec magic with glob
|
169
|
+
# Exclude patterns via git pathspec magic with glob (normalized)
|
144
170
|
exclude_args.each do |pat|
|
145
171
|
args << ":(exclude,glob)#{pat}"
|
146
172
|
end
|
147
173
|
end
|
148
|
-
# rubocop:enable Metrics/AbcSize
|
149
174
|
|
150
175
|
# rubocop:disable Metrics/CyclomaticComplexity
|
151
176
|
def exclude_author?(name, email)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrettyGit
|
4
|
+
# Minimal centralized logger for PrettyGit.
|
5
|
+
# Writes to stderr by default; can accept custom IO via keyword.
|
6
|
+
module Logger
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def warn(msg, err: $stderr)
|
10
|
+
err.puts(msg)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Convenience: only emit when enabled is truthy
|
14
|
+
def verbose(msg, enabled, err: $stderr)
|
15
|
+
return unless enabled
|
16
|
+
|
17
|
+
warn(msg, err: err)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrettyGit
|
4
|
+
module Utils
|
5
|
+
# Utilities for path/glob normalization and handling cross-platform quirks.
|
6
|
+
module PathUtils
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# Normalize a string path or glob to Unicode NFC form.
|
10
|
+
# Returns nil if input is nil.
|
11
|
+
def normalize_nfc(str)
|
12
|
+
return nil if str.nil?
|
13
|
+
|
14
|
+
s = str.to_s
|
15
|
+
# Only normalize if supported in this Ruby build; otherwise return as-is
|
16
|
+
if s.respond_to?(:unicode_normalize)
|
17
|
+
s.unicode_normalize(:nfc)
|
18
|
+
else
|
19
|
+
s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Normalize each entry in an Array-like collection to NFC and compact
|
24
|
+
# nils. Returns an Array.
|
25
|
+
def normalize_globs(collection)
|
26
|
+
Array(collection).compact.map { |p| normalize_nfc(p) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module PrettyGit
|
6
|
+
module Utils
|
7
|
+
# Utilities for time parsing and ISO8601 normalization used by filters.
|
8
|
+
module TimeUtils
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# Converts various time inputs to ISO8601 in UTC.
|
12
|
+
# Accepts Time, String(ISO8601), or String(YYYY-MM-DD) treated as UTC midnight.
|
13
|
+
# Returns nil for nil/blank input. Raises ArgumentError for invalid values.
|
14
|
+
def to_utc_iso8601(val)
|
15
|
+
return nil if val.nil? || val.to_s.strip.empty?
|
16
|
+
|
17
|
+
parse_to_time(val).utc.iso8601
|
18
|
+
rescue ArgumentError
|
19
|
+
raise ArgumentError, "Invalid datetime: #{val} (expected ISO8601 or YYYY-MM-DD)"
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_to_time(val)
|
23
|
+
return val if val.is_a?(Time)
|
24
|
+
return parse_date_only(val) if val.is_a?(String) && date_only?(val)
|
25
|
+
|
26
|
+
Time.parse(val.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse_date_only(str)
|
30
|
+
y, m, d = str.split('-').map(&:to_i)
|
31
|
+
Time.new(y, m, d, 0, 0, 0, '+00:00')
|
32
|
+
end
|
33
|
+
|
34
|
+
def date_only?(str)
|
35
|
+
!!(str =~ /^\d{4}-\d{2}-\d{2}$/)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/pretty_git/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pretty-git
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pretty Git Authors
|
@@ -74,8 +74,10 @@ files:
|
|
74
74
|
- lib/pretty_git/app.rb
|
75
75
|
- lib/pretty_git/cli.rb
|
76
76
|
- lib/pretty_git/cli_helpers.rb
|
77
|
+
- lib/pretty_git/constants.rb
|
77
78
|
- lib/pretty_git/filters.rb
|
78
79
|
- lib/pretty_git/git/provider.rb
|
80
|
+
- lib/pretty_git/logger.rb
|
79
81
|
- lib/pretty_git/render/console_renderer.rb
|
80
82
|
- lib/pretty_git/render/csv_renderer.rb
|
81
83
|
- lib/pretty_git/render/json_renderer.rb
|
@@ -85,6 +87,8 @@ files:
|
|
85
87
|
- lib/pretty_git/render/xml_renderer.rb
|
86
88
|
- lib/pretty_git/render/yaml_renderer.rb
|
87
89
|
- lib/pretty_git/types.rb
|
90
|
+
- lib/pretty_git/utils/path_utils.rb
|
91
|
+
- lib/pretty_git/utils/time_utils.rb
|
88
92
|
- lib/pretty_git/version.rb
|
89
93
|
homepage: https://github.com/MikoMikocchi/pretty-git
|
90
94
|
licenses:
|