pretty-git 0.1.2 → 0.1.4
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 +34 -1
- data/README.md +144 -39
- data/README.ru.md +144 -39
- data/lib/pretty_git/analytics/churn.rb +75 -0
- data/lib/pretty_git/analytics/hotspots.rb +78 -0
- data/lib/pretty_git/analytics/ownership.rb +90 -0
- data/lib/pretty_git/app.rb +18 -16
- data/lib/pretty_git/cli.rb +7 -4
- data/lib/pretty_git/cli_helpers.rb +16 -3
- data/lib/pretty_git/filters.rb +11 -2
- data/lib/pretty_git/git/provider.rb +35 -3
- data/lib/pretty_git/render/console_renderer.rb +53 -16
- data/lib/pretty_git/render/csv_renderer.rb +20 -17
- data/lib/pretty_git/render/markdown_renderer.rb +85 -21
- data/lib/pretty_git/render/xml_renderer.rb +71 -3
- data/lib/pretty_git/render/yaml_renderer.rb +58 -2
- data/lib/pretty_git/version.rb +1 -1
- metadata +6 -3
data/README.ru.md
CHANGED
@@ -14,7 +14,7 @@
|
|
14
14
|
<br>
|
15
15
|
</p>
|
16
16
|
|
17
|
-
Генератор отчётов по локальному Git-репозиторию: сводка, активность, авторы, файлы,
|
17
|
+
Генератор отчётов по локальному Git-репозиторию: сводка, активность, авторы, файлы, теплокарта, языки, hotspots, churn, ownership. Вывод в консоль и форматы: JSON, CSV, Markdown, YAML, XML.
|
18
18
|
|
19
19
|
— Лицензия: MIT.
|
20
20
|
|
@@ -35,10 +35,13 @@
|
|
35
35
|
- [files — по файлам](#files--по-файлам)
|
36
36
|
- [heatmap — тепловая карта](#heatmap--тепловая-карта)
|
37
37
|
- [languages — языки](#languages--языки)
|
38
|
+
- [hotspots — рискованные файлы](#hotspots--рискованные-файлы)
|
39
|
+
- [churn — churn по файлам](#churn--churn-по-файлам)
|
40
|
+
- [ownership — владение кодом](#ownership--владение-кодом)
|
38
41
|
- [Экспорт в форматы](#экспорт-в-форматы)
|
39
42
|
- [Console](#console)
|
40
43
|
- [JSON](#json)
|
41
|
-
- [CSV
|
44
|
+
- [CSV](#csv)
|
42
45
|
- [Markdown](#markdown)
|
43
46
|
- [YAML](#yaml)
|
44
47
|
- [XML](#xml)
|
@@ -49,17 +52,17 @@
|
|
49
52
|
- [Разработка](#разработка)
|
50
53
|
- [Лицензия](#лицензия)
|
51
54
|
|
52
|
-
## Возможности
|
53
|
-
* __Отчёты__: `summary`, `activity`, `authors`, `files`, `heatmap`, `languages`.
|
55
|
+
## ✨ Возможности
|
56
|
+
* __Отчёты__: `summary`, `activity`, `authors`, `files`, `heatmap`, `languages`, `hotspots`, `churn`, `ownership`.
|
54
57
|
* __Фильтры__: ветки, авторы, пути, период времени.
|
55
58
|
* __Экспорт__: `console`, `json`, `csv`, `md`, `yaml`, `xml`.
|
56
59
|
* __Вывод__: в stdout или файл через `--out`.
|
57
60
|
|
58
|
-
## Требования
|
61
|
+
## ⚙️ Требования
|
59
62
|
* __Ruby__: >= 3.4 (рекомендуется 3.4.x)
|
60
63
|
* __Git__: установлен и доступен в `PATH`
|
61
64
|
|
62
|
-
## Установка
|
65
|
+
## 📦 Установка
|
63
66
|
|
64
67
|
### 🍺 Homebrew (рекомендуется)
|
65
68
|
```bash
|
@@ -101,7 +104,7 @@ bundle install
|
|
101
104
|
bundle exec pretty-git --help
|
102
105
|
```
|
103
106
|
|
104
|
-
## Быстрый старт
|
107
|
+
## 🚀 Быстрый старт
|
105
108
|
```bash
|
106
109
|
# Сводка репозитория в консоль
|
107
110
|
bundle exec bin/pretty-git summary .
|
@@ -114,21 +117,25 @@ bundle exec bin/pretty-git activity . --time-bucket week --since 2025-01-01 \
|
|
114
117
|
--paths app,lib --format csv --out activity.csv
|
115
118
|
```
|
116
119
|
|
117
|
-
## CLI и параметры
|
120
|
+
## 🧰 CLI и параметры
|
118
121
|
Общий вид:
|
119
122
|
|
120
123
|
```bash
|
121
124
|
pretty-git <report> <repo_path> [options]
|
122
125
|
```
|
123
126
|
|
124
|
-
|
127
|
+
Примечания:
|
128
|
+
* `<repo_path>` по умолчанию — `.` (если опущен).
|
129
|
+
* Репозиторий можно указать и через флаг `--repo PATH` как альтернативу позиционному аргументу.
|
130
|
+
|
131
|
+
Доступные отчёты: `summary`, `activity`, `authors`, `files`, `heatmap`, `languages`, `hotspots`, `churn`, `ownership`.
|
125
132
|
|
126
133
|
Ключевые опции:
|
127
134
|
* __--format, -f__ `console|json|csv|md|yaml|xml` (по умолчанию `console`)
|
128
135
|
* __--out, -o__ Путь для записи в файл
|
129
136
|
* __--limit, -l__ Число элементов в топах/выводе; `all` или `0` — без ограничения
|
130
137
|
* __--time-bucket__ `day|week|month` (для `activity`)
|
131
|
-
* __--since/--until__ Дата/время в ISO8601 или `YYYY-MM-DD`
|
138
|
+
* __--since/--until__ Дата/время в ISO8601 или `YYYY-MM-DD`
|
132
139
|
* __--branch__ Мульти-опция, можно указывать несколько веток
|
133
140
|
* __--author/--exclude-author__ Фильтрация по авторам
|
134
141
|
* __--path/--exclude-path__ Фильтрация по путям (через запятую или повтор опции)
|
@@ -165,15 +172,15 @@ pretty-git authors . --format csv --out authors.csv
|
|
165
172
|
* `1` — ошибка пользователя (неизвестный отчёт/формат, неверные аргументы)
|
166
173
|
* `2` — системная ошибка (ошибка Git и пр.)
|
167
174
|
|
168
|
-
## Отчёты и примеры
|
175
|
+
## 📊 Отчёты и примеры
|
169
176
|
|
170
|
-
### summary — сводка
|
177
|
+
### 🧭 summary — сводка
|
171
178
|
```bash
|
172
179
|
pretty-git summary . --format json
|
173
180
|
```
|
174
181
|
Содержит totals (commits, authors, additions, deletions) и топы по авторам/файлам.
|
175
182
|
|
176
|
-
### activity — активность (day/week/month)
|
183
|
+
### 📆 activity — активность (day/week/month)
|
177
184
|
```bash
|
178
185
|
pretty-git activity . --time-bucket week --format csv
|
179
186
|
```
|
@@ -186,7 +193,7 @@ CSV-колонки: `bucket,timestamp,commits,additions,deletions`.
|
|
186
193
|
]
|
187
194
|
```
|
188
195
|
|
189
|
-
### authors — по авторам
|
196
|
+
### 👤 authors — по авторам
|
190
197
|
```bash
|
191
198
|
pretty-git authors . --format md --limit 10
|
192
199
|
```
|
@@ -199,27 +206,39 @@ CSV-колонки: `author,author_email,commits,additions,deletions,avg_commit_
|
|
199
206
|
| Bob | b@example.com | 1 | 2 | 0 | 2.0 |
|
200
207
|
```
|
201
208
|
|
202
|
-
### files — по файлам
|
209
|
+
### 📁 files — по файлам
|
203
210
|
```bash
|
204
211
|
pretty-git files . --paths app,lib --format csv
|
205
212
|
```
|
206
213
|
CSV-колонки: `path,commits,additions,deletions,changes`.
|
214
|
+
|
207
215
|
Пример XML:
|
208
216
|
```xml
|
209
|
-
|
210
|
-
|
211
|
-
<
|
217
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
218
|
+
<report>
|
219
|
+
<report>files</report>
|
212
220
|
<generated_at>2025-01-31T00:00:00Z</generated_at>
|
213
221
|
<repo_path>/abs/path/to/repo</repo_path>
|
214
|
-
<
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
222
|
+
<items>
|
223
|
+
<item>
|
224
|
+
<path>app/models/user.rb</path>
|
225
|
+
<commits>42</commits>
|
226
|
+
<additions>2100</additions>
|
227
|
+
<deletions>1400</deletions>
|
228
|
+
<changes>3500</changes>
|
229
|
+
</item>
|
230
|
+
<item>
|
231
|
+
<path>app/services/auth.rb</path>
|
232
|
+
<commits>35</commits>
|
233
|
+
<additions>1500</additions>
|
234
|
+
<deletions>900</deletions>
|
235
|
+
<changes>2400</changes>
|
236
|
+
</item>
|
237
|
+
</items>
|
238
|
+
</report>
|
220
239
|
```
|
221
240
|
|
222
|
-
### heatmap — тепловая карта
|
241
|
+
### 🔥 heatmap — тепловая карта
|
223
242
|
```bash
|
224
243
|
pretty-git heatmap . --format json
|
225
244
|
```
|
@@ -231,7 +250,7 @@ dow,hour,commits
|
|
231
250
|
1,11,7
|
232
251
|
```
|
233
252
|
|
234
|
-
### languages — языки
|
253
|
+
### 🈺 languages — языки
|
235
254
|
```bash
|
236
255
|
pretty-git languages . --format md --limit 10
|
237
256
|
```
|
@@ -262,7 +281,93 @@ Markdown 1200 1.7
|
|
262
281
|
- CSV/MD: колонки динамические — `language,<metric>,percent`. В Markdown дополнительно присутствует колонка `color`.
|
263
282
|
- JSON/YAML/XML: полная структура отчёта, включая `color` на язык и метаданные (`report`, `generated_at`, `repo_path`).
|
264
283
|
|
265
|
-
|
284
|
+
### ⚠️ hotspots — рискованные файлы
|
285
|
+
```bash
|
286
|
+
pretty-git hotspots . --format csv --limit 20
|
287
|
+
```
|
288
|
+
Подсвечивает потенциально более рискованные файлы по сочетанию частоты и масштаба изменений.
|
289
|
+
|
290
|
+
CSV‑колонки: `path,score,commits,additions,deletions,changes`.
|
291
|
+
|
292
|
+
Пример:
|
293
|
+
```csv
|
294
|
+
path,score,commits,additions,deletions,changes
|
295
|
+
lib/a.rb,9.5,12,300,220,520
|
296
|
+
app/b.rb,7.1,8,140,60,200
|
297
|
+
```
|
298
|
+
|
299
|
+
Пример JSON:
|
300
|
+
```json
|
301
|
+
{
|
302
|
+
"report": "hotspots",
|
303
|
+
"generated_at": "2025-01-31T00:00:00Z",
|
304
|
+
"repo_path": ".",
|
305
|
+
"items": [
|
306
|
+
{"path": "lib/a.rb", "score": 9.5, "commits": 12, "additions": 300, "deletions": 220, "changes": 520}
|
307
|
+
]
|
308
|
+
}
|
309
|
+
```
|
310
|
+
|
311
|
+
### 🔄 churn — churn по файлам
|
312
|
+
```bash
|
313
|
+
pretty-git churn . --format md --limit 20
|
314
|
+
```
|
315
|
+
Оценивает churn (объём часто меняющегося кода) по файлам.
|
316
|
+
|
317
|
+
CSV‑колонки: `path,churn,commits,additions,deletions`.
|
318
|
+
|
319
|
+
Пример Markdown:
|
320
|
+
```markdown
|
321
|
+
| path | churn | commits | additions | deletions |
|
322
|
+
|---|---:|---:|---:|---:|
|
323
|
+
| lib/a.rb | 520 | 12 | 300 | 220 |
|
324
|
+
```
|
325
|
+
|
326
|
+
Пример YAML:
|
327
|
+
```yaml
|
328
|
+
report: churn
|
329
|
+
generated_at: '2025-01-31T00:00:00Z'
|
330
|
+
repo_path: .
|
331
|
+
items:
|
332
|
+
- path: lib/a.rb
|
333
|
+
churn: 520
|
334
|
+
commits: 12
|
335
|
+
additions: 300
|
336
|
+
deletions: 220
|
337
|
+
```
|
338
|
+
|
339
|
+
### 🏷️ ownership — владение кодом
|
340
|
+
```bash
|
341
|
+
pretty-git ownership . --format csv --limit 50
|
342
|
+
```
|
343
|
+
Показывает концентрацию владения файлами по основному владельцу.
|
344
|
+
|
345
|
+
CSV‑колонки: `path,owner,owner_share,authors`.
|
346
|
+
|
347
|
+
Примечания:
|
348
|
+
- `owner`: идентификатор автора (имя/email) с наибольшей долей правок.
|
349
|
+
- `owner_share`: доля правок владельца в процентах (0..100).
|
350
|
+
- `authors`: всего уникальных авторов, редактировавших файл.
|
351
|
+
|
352
|
+
Пример XML:
|
353
|
+
```xml
|
354
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
355
|
+
<report>
|
356
|
+
<report>ownership</report>
|
357
|
+
<generated_at>2025-01-31T00:00:00Z</generated_at>
|
358
|
+
<repo_path>.</repo_path>
|
359
|
+
<items>
|
360
|
+
<item>
|
361
|
+
<path>lib/a.rb</path>
|
362
|
+
<owner>Alice <a@example.com></owner>
|
363
|
+
<owner_share>82.5</owner_share>
|
364
|
+
<authors>2</authors>
|
365
|
+
</item>
|
366
|
+
</items>
|
367
|
+
</report>
|
368
|
+
```
|
369
|
+
|
370
|
+
## 🚫 Игнорируемые директории и файлы
|
266
371
|
|
267
372
|
Чтобы статистика по языкам оставалась релевантной, некоторые директории и типы файлов пропускаются по умолчанию.
|
268
373
|
|
@@ -287,11 +392,11 @@ vendor, node_modules, .git, .bundle, dist, build, out, target, coverage,
|
|
287
392
|
|
288
393
|
Списки соответствуют реализации в `lib/pretty_git/analytics/languages.rb` и могут изменяться.
|
289
394
|
|
290
|
-
## Экспорт в форматы
|
395
|
+
## 📤 Экспорт в форматы
|
291
396
|
|
292
397
|
Ниже — точные правила сериализации для каждого формата, чтобы обеспечивалась совместимость с популярными инструментами (Excel, BI, CI и т.п.).
|
293
398
|
|
294
|
-
### Console
|
399
|
+
### 🖥️ Console
|
295
400
|

|
296
401
|
_Пример вывода в терминале (тема: basic)._
|
297
402
|
* __Цвета__: заголовки и шапки таблиц подсвечены; суммы: `commits` — жёлтым, `+additions` — зелёным, `-deletions` — красным. `--no-color` полностью отключает раскраску.
|
@@ -305,7 +410,7 @@ _Пример вывода в терминале (тема: basic)._
|
|
305
410
|
* __Пустые наборы__: печатается `No data`.
|
306
411
|
* __Кодировка/переводы строк__: UTF‑8, LF (`\n`).
|
307
412
|
|
308
|
-
### JSON
|
413
|
+
### 🧾 JSON
|
309
414
|
* __Ключи__: `snake_case`.
|
310
415
|
* __Числа__: целые/вещественные без локализации (точка как разделитель).
|
311
416
|
* __Булевы__: `true/false`; __null__: `null`.
|
@@ -337,7 +442,7 @@ _Пример вывода в терминале (тема: basic)._
|
|
337
442
|
Bob,b@example.com,1,2,0,2.0
|
338
443
|
```
|
339
444
|
|
340
|
-
### Markdown
|
445
|
+
### 📝 Markdown
|
341
446
|
* __Таблицы__: стандартный синтаксис GitHub Flavored Markdown.
|
342
447
|
* __Выравнивание__: числовые колонки выравниваются по правому краю (`---:`).
|
343
448
|
* __Кодировка/переводы строк__: UTF‑8, LF.
|
@@ -350,7 +455,7 @@ _Пример вывода в терминале (тема: basic)._
|
|
350
455
|
| app/models/user.rb | 42 | 2100 | 1400 |
|
351
456
|
```
|
352
457
|
|
353
|
-
### YAML
|
458
|
+
### 📄 YAML
|
354
459
|
* __Структура__: полная иерархия результата.
|
355
460
|
* __Ключи__: сериализуются строками.
|
356
461
|
* __Числа/булевы/null__: стандарт YAML (`123`, `true/false`, `null`).
|
@@ -370,7 +475,7 @@ _Пример вывода в терминале (тема: basic)._
|
|
370
475
|
commits: 1
|
371
476
|
```
|
372
477
|
|
373
|
-
### XML
|
478
|
+
### 🗂️ XML
|
374
479
|
* __Структура__: элементы соответствуют ключам; массивы — повторяющиеся `<item>` или специализированные теги.
|
375
480
|
* __Атрибуты__: для компактных записей (например, строк файлового отчёта) основные поля могут быть атрибутами элемента.
|
376
481
|
* __Текстовые узлы__: применяются для скалярных значений при необходимости.
|
@@ -388,16 +493,16 @@ _Пример вывода в терминале (тема: basic)._
|
|
388
493
|
</authors>
|
389
494
|
```
|
390
495
|
|
391
|
-
## Детерминизм и сортировка
|
496
|
+
## 🔁 Детерминизм и сортировка
|
392
497
|
Вывод детерминирован при одинаковых входных данных. Сортировка для файлов/авторов: по количеству изменений (desc), затем по числу коммитов (desc), затем по пути/имени (asc). Лимиты применяются поверх отсортированного списка; значение `all` или `0` означает отсутствие ограничения.
|
393
498
|
|
394
|
-
## Советы по Windows
|
499
|
+
## 🪟 Советы по Windows
|
395
500
|
Целевая платформа — macOS/Linux. Windows поддерживается в режиме best‑effort:
|
396
501
|
* Запуск через Git Bash/WSL допустим
|
397
502
|
* Цвета можно отключить `--no-color`
|
398
503
|
* Аккуратное квотирование аргументов при работе с путями
|
399
504
|
|
400
|
-
## Диагностика и ошибки
|
505
|
+
## 🩺 Диагностика и ошибки
|
401
506
|
Типичные ошибки и решения:
|
402
507
|
|
403
508
|
* __Неизвестный отчёт/формат__ — проверьте значение первого аргумента и `--format`.
|
@@ -406,12 +511,12 @@ _Пример вывода в терминале (тема: basic)._
|
|
406
511
|
* __Пустой результат__ — проверьте фильтры (`--since/--until`, `--branch`, `--path`), возможно, выборка слишком узкая.
|
407
512
|
* __Проблемы с кодировкой CSV__ — файлы сохраняются в UTF‑8, при открытии в Excel выбирайте UTF‑8.
|
408
513
|
|
409
|
-
## FAQ
|
514
|
+
## ❓ FAQ
|
410
515
|
* __Почему Ruby 3.4+?__ Проект использует зависимости, согласованные с версией 3.4+, и ориентируется на актуальную экосистему.
|
411
516
|
* __Можно ли добавить новые форматы?__ Да, добавьте рендерер в `lib/pretty_git/render/` и зарегистрируйте его в приложении.
|
412
517
|
* __Откуда берутся данные?__ Из системного `git` через вызовы CLI.
|
413
518
|
|
414
|
-
## Разработка
|
519
|
+
## 🛠️ Разработка
|
415
520
|
```bash
|
416
521
|
# Установка зависимостей
|
417
522
|
bin/setup
|
@@ -423,5 +528,5 @@ bundle exec rubocop
|
|
423
528
|
|
424
529
|
Стиль — RuboCop без ошибок. Тесты покрывают агрегаторы, рендереры, CLI и интеграционные сценарии (детерминизм, корректность форматов).
|
425
530
|
|
426
|
-
## Лицензия
|
531
|
+
## 📄 Лицензия
|
427
532
|
MIT © Contributors
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module PrettyGit
|
6
|
+
module Analytics
|
7
|
+
# Churn: per-file volatility over the selected period.
|
8
|
+
# churn = additions + deletions (total changed lines)
|
9
|
+
class Churn
|
10
|
+
class << self
|
11
|
+
def call(enum, filters)
|
12
|
+
per_file = aggregate(enum)
|
13
|
+
items = build_items(per_file)
|
14
|
+
items = sort_and_limit(items, filters.limit)
|
15
|
+
build_result(filters, items)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def aggregate(enum)
|
21
|
+
acc = Hash.new { |h, k| h[k] = { commits: 0, additions: 0, deletions: 0 } }
|
22
|
+
enum.each do |commit|
|
23
|
+
seen = {}
|
24
|
+
commit.files&.each { |f| process_file_entry(acc, seen, f) }
|
25
|
+
end
|
26
|
+
acc
|
27
|
+
end
|
28
|
+
|
29
|
+
def process_file_entry(acc, seen, file_stat)
|
30
|
+
path = file_stat.path
|
31
|
+
unless seen[path]
|
32
|
+
acc[path][:commits] += 1
|
33
|
+
seen[path] = true
|
34
|
+
end
|
35
|
+
acc[path][:additions] += file_stat.additions.to_i
|
36
|
+
acc[path][:deletions] += file_stat.deletions.to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_items(per_file)
|
40
|
+
per_file.map do |path, v|
|
41
|
+
churn = v[:additions] + v[:deletions]
|
42
|
+
{
|
43
|
+
path: path,
|
44
|
+
churn: churn,
|
45
|
+
commits: v[:commits]
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def sort_and_limit(items, raw_limit)
|
51
|
+
limit = normalize_limit(raw_limit)
|
52
|
+
sorted = items.sort_by { |h| [-h[:churn], -h[:commits], h[:path].to_s] }
|
53
|
+
limit ? sorted.first(limit) : sorted
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_result(filters, items)
|
57
|
+
{
|
58
|
+
report: 'churn',
|
59
|
+
repo_path: File.expand_path(filters.repo_path),
|
60
|
+
period: { since: filters.since_iso8601, until: filters.until_iso8601 },
|
61
|
+
items: items,
|
62
|
+
generated_at: Time.now.utc.iso8601
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def normalize_limit(raw)
|
67
|
+
return nil if raw.nil? || raw == 'all'
|
68
|
+
|
69
|
+
n = raw.to_i
|
70
|
+
n <= 0 ? nil : n
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module PrettyGit
|
6
|
+
module Analytics
|
7
|
+
# Hotspots: files with the highest change activity over the selected period.
|
8
|
+
# score = commits * (additions + deletions)
|
9
|
+
class Hotspots
|
10
|
+
class << self
|
11
|
+
def call(enum, filters)
|
12
|
+
per_file = aggregate(enum)
|
13
|
+
items = build_items(per_file)
|
14
|
+
items = sort_and_limit(items, filters.limit)
|
15
|
+
build_result(filters, items)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def aggregate(enum)
|
21
|
+
acc = Hash.new { |h, k| h[k] = { commits: 0, additions: 0, deletions: 0 } }
|
22
|
+
enum.each do |commit|
|
23
|
+
seen = {}
|
24
|
+
commit.files&.each { |f| process_file_entry(acc, seen, f) }
|
25
|
+
end
|
26
|
+
acc
|
27
|
+
end
|
28
|
+
|
29
|
+
def process_file_entry(acc, seen, file_stat)
|
30
|
+
path = file_stat.path
|
31
|
+
unless seen[path]
|
32
|
+
acc[path][:commits] += 1
|
33
|
+
seen[path] = true
|
34
|
+
end
|
35
|
+
acc[path][:additions] += file_stat.additions.to_i
|
36
|
+
acc[path][:deletions] += file_stat.deletions.to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_items(per_file)
|
40
|
+
per_file.map do |path, v|
|
41
|
+
changes = v[:additions] + v[:deletions]
|
42
|
+
score = v[:commits] * changes
|
43
|
+
{
|
44
|
+
path: path,
|
45
|
+
score: score,
|
46
|
+
commits: v[:commits],
|
47
|
+
additions: v[:additions],
|
48
|
+
deletions: v[:deletions]
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def sort_and_limit(items, raw_limit)
|
54
|
+
limit = normalize_limit(raw_limit)
|
55
|
+
sorted = items.sort_by { |h| [-h[:score], -h[:commits], -h[:additions] - h[:deletions], h[:path].to_s] }
|
56
|
+
limit ? sorted.first(limit) : sorted
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_result(filters, items)
|
60
|
+
{
|
61
|
+
report: 'hotspots',
|
62
|
+
repo_path: File.expand_path(filters.repo_path),
|
63
|
+
period: { since: filters.since_iso8601, until: filters.until_iso8601 },
|
64
|
+
items: items,
|
65
|
+
generated_at: Time.now.utc.iso8601
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def normalize_limit(raw)
|
70
|
+
return nil if raw.nil? || raw == 'all'
|
71
|
+
|
72
|
+
n = raw.to_i
|
73
|
+
n <= 0 ? nil : n
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module PrettyGit
|
6
|
+
module Analytics
|
7
|
+
# Ownership: per-file code ownership based on change activity (churn).
|
8
|
+
# For each file, the owner is the author with the largest share of churn (adds+dels).
|
9
|
+
class Ownership
|
10
|
+
class << self
|
11
|
+
def call(enum, filters)
|
12
|
+
per_file = aggregate(enum)
|
13
|
+
items = build_items(per_file)
|
14
|
+
items = sort_and_limit(items, filters.limit)
|
15
|
+
build_result(filters, items)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Builds a map: path => { total_churn: N, authors: {"name <email>" => churn} }
|
21
|
+
def aggregate(enum)
|
22
|
+
acc = Hash.new { |h, k| h[k] = { total: 0, authors: Hash.new(0) } }
|
23
|
+
enum.each do |commit|
|
24
|
+
author_key = author_identity(commit)
|
25
|
+
commit.files&.each { |f| process_file_entry(acc, author_key, f) }
|
26
|
+
end
|
27
|
+
acc
|
28
|
+
end
|
29
|
+
|
30
|
+
def process_file_entry(acc, author_key, file_stat)
|
31
|
+
churn = file_stat.additions.to_i + file_stat.deletions.to_i
|
32
|
+
return if churn <= 0
|
33
|
+
|
34
|
+
path = file_stat.path
|
35
|
+
acc[path][:total] += churn
|
36
|
+
acc[path][:authors][author_key] += churn
|
37
|
+
end
|
38
|
+
|
39
|
+
def author_identity(commit)
|
40
|
+
name = commit.author_name.to_s.strip
|
41
|
+
email = commit.author_email.to_s.strip
|
42
|
+
email.empty? ? name : "#{name} <#{email}>"
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_items(per_file)
|
46
|
+
per_file.map do |path, v|
|
47
|
+
owner, share, authors_count = compute_owner(v[:authors], v[:total])
|
48
|
+
{
|
49
|
+
path: path,
|
50
|
+
owner: owner,
|
51
|
+
owner_share: share.round(2),
|
52
|
+
authors: authors_count
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def compute_owner(authors_map, total)
|
58
|
+
return [nil, 0.0, 0] if total.to_i <= 0 || authors_map.nil? || authors_map.empty?
|
59
|
+
|
60
|
+
author, owner_churn = authors_map.max_by { |a, c| [c, a] }
|
61
|
+
share = (owner_churn.to_f * 100.0) / total.to_f
|
62
|
+
[author, share, authors_map.size]
|
63
|
+
end
|
64
|
+
|
65
|
+
def sort_and_limit(items, raw_limit)
|
66
|
+
limit = normalize_limit(raw_limit)
|
67
|
+
sorted = items.sort_by { |h| [-h[:owner_share].to_f, h[:authors].to_i, h[:path].to_s] }
|
68
|
+
limit ? sorted.first(limit) : sorted
|
69
|
+
end
|
70
|
+
|
71
|
+
def build_result(filters, items)
|
72
|
+
{
|
73
|
+
report: 'ownership',
|
74
|
+
repo_path: File.expand_path(filters.repo_path),
|
75
|
+
period: { since: filters.since_iso8601, until: filters.until_iso8601 },
|
76
|
+
items: items,
|
77
|
+
generated_at: Time.now.utc.iso8601
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def normalize_limit(raw)
|
82
|
+
return nil if raw.nil? || raw == 'all'
|
83
|
+
|
84
|
+
n = raw.to_i
|
85
|
+
n <= 0 ? nil : n
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/pretty_git/app.rb
CHANGED
@@ -7,6 +7,9 @@ require_relative 'analytics/files'
|
|
7
7
|
require_relative 'analytics/authors'
|
8
8
|
require_relative 'analytics/heatmap'
|
9
9
|
require_relative 'analytics/languages'
|
10
|
+
require_relative 'analytics/hotspots'
|
11
|
+
require_relative 'analytics/churn'
|
12
|
+
require_relative 'analytics/ownership'
|
10
13
|
require_relative 'render/json_renderer'
|
11
14
|
require_relative 'render/console_renderer'
|
12
15
|
require_relative 'render/csv_renderer'
|
@@ -62,22 +65,21 @@ module PrettyGit
|
|
62
65
|
end
|
63
66
|
|
64
67
|
def analytics_for(report, enum, filters)
|
65
|
-
|
66
|
-
|
67
|
-
Analytics::
|
68
|
-
|
69
|
-
Analytics::
|
70
|
-
|
71
|
-
Analytics::
|
72
|
-
|
73
|
-
Analytics::
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
68
|
+
dispatch = {
|
69
|
+
'summary' => Analytics::Summary,
|
70
|
+
'activity' => Analytics::Activity,
|
71
|
+
'authors' => Analytics::Authors,
|
72
|
+
'files' => Analytics::Files,
|
73
|
+
'heatmap' => Analytics::Heatmap,
|
74
|
+
'languages' => Analytics::Languages,
|
75
|
+
'hotspots' => Analytics::Hotspots,
|
76
|
+
'churn' => Analytics::Churn,
|
77
|
+
'ownership' => Analytics::Ownership
|
78
|
+
}
|
79
|
+
klass = dispatch[report]
|
80
|
+
raise ArgumentError, "Unknown report: #{report}" unless klass
|
81
|
+
|
82
|
+
klass.call(enum, filters)
|
81
83
|
end
|
82
84
|
end
|
83
85
|
end
|
data/lib/pretty_git/cli.rb
CHANGED
@@ -9,10 +9,10 @@ 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].freeze
|
12
|
+
SUPPORTED_REPORTS = %w[summary activity authors files heatmap languages hotspots churn ownership].freeze
|
13
13
|
SUPPORTED_FORMATS = %w[console json csv md yaml xml].freeze
|
14
14
|
|
15
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
15
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
16
16
|
def self.run(argv = ARGV, out: $stdout, err: $stderr)
|
17
17
|
options = {
|
18
18
|
report: 'summary',
|
@@ -22,7 +22,7 @@ module PrettyGit
|
|
22
22
|
exclude_authors: [],
|
23
23
|
paths: [],
|
24
24
|
exclude_paths: [],
|
25
|
-
time_bucket:
|
25
|
+
time_bucket: nil,
|
26
26
|
limit: 10,
|
27
27
|
format: 'console',
|
28
28
|
out: nil,
|
@@ -46,6 +46,9 @@ module PrettyGit
|
|
46
46
|
return 1
|
47
47
|
end
|
48
48
|
|
49
|
+
# REPO positional arg (after REPORT), if still present and not an option
|
50
|
+
options[:repo] = argv.shift if argv[0] && argv[0] !~ /^-/
|
51
|
+
|
49
52
|
exit_code = CLIHelpers.validate_and_maybe_exit(options, parser, out, err)
|
50
53
|
return exit_code if exit_code
|
51
54
|
|
@@ -58,6 +61,6 @@ module PrettyGit
|
|
58
61
|
err.puts e.message
|
59
62
|
2
|
60
63
|
end
|
61
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
64
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
62
65
|
end
|
63
66
|
end
|