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.
data/README.ru.md CHANGED
@@ -14,7 +14,7 @@
14
14
  <br>
15
15
  </p>
16
16
 
17
- Генератор отчётов по локальному Git-репозиторию: сводка, активность, авторы, файлы, тепловая карта. Вывод в консоль и форматы: JSON, CSV, Markdown, YAML, XML.
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 (DR-001)](#csv-dr-001)
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
- Доступные отчёты: `summary`, `activity`, `authors`, `files`, `heatmap`, `languages`.
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` (DR-005)
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
- <files>
210
- <item path="app/models/user.rb" commits="42" additions="2100" deletions="1400" changes="3500" />
211
- <item path="app/services/auth.rb" commits="35" additions="1500" deletions="900" changes="2400" />
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
- <report>files</report>
215
- <period>
216
- <since/>
217
- <until/>
218
- </period>
219
- </files>
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 &lt;a@example.com&gt;</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
  ![Console output — basic theme](docs/images/PrettyGitConsole.png)
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
@@ -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
- case report
66
- when 'summary'
67
- Analytics::Summary.call(enum, filters)
68
- when 'activity'
69
- Analytics::Activity.call(enum, filters)
70
- when 'authors'
71
- Analytics::Authors.call(enum, filters)
72
- when 'files'
73
- Analytics::Files.call(enum, filters)
74
- when 'heatmap'
75
- Analytics::Heatmap.call(enum, filters)
76
- when 'languages'
77
- Analytics::Languages.call(enum, filters)
78
- else
79
- raise ArgumentError, "Unknown report: #{report}"
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
@@ -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: 'week',
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