pretty-git 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9adec0f68c20ace4f919d32959eae6e9bfd79d412ca762f90a5b79320d018891
4
+ data.tar.gz: 7d7aad306dbd24fd20e54f89f39e3044cfa743c34a3259a06a085e14f03b7309
5
+ SHA512:
6
+ metadata.gz: 12914fd26d9c307551b19ca822c766927ec375fe6e77e85dc379e35e49080aec9ef0516648902d4b846e5d5d9145e471b88beeac2b88783b3f9b1417dca6401f
7
+ data.tar.gz: 612c40a60166e42be38f8318f9ab5f2e3e13f3e26cf9b97e580818741d124abc24b9fbd31c3c43c6faad4196f06a0d713684ff1c4d9d6189683f9ccc1e9c44bf
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mikhail Matskevich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,413 @@
1
+ # Pretty Git
2
+
3
+ [![CI](https://github.com/MikoMikocchi/pretty-git/actions/workflows/ci.yml/badge.svg)](https://github.com/MikoMikocchi/pretty-git/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ ![Ruby 3.4+](https://img.shields.io/badge/ruby-3.4%2B-red)
6
+
7
+ <p align="right">
8
+ <b>English</b> | <a href="./README.ru.md">Русский</a>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <img src="docs/images/PrettyGitIcon.png" alt="Pretty Git Logo" width="200">
13
+ <br>
14
+ </p>
15
+
16
+ Generator of rich reports for a local Git repository: summary, activity, authors, files, heatmap, languages. Output to Console and formats: JSON, CSV, Markdown, YAML, XML.
17
+
18
+ — License: MIT.
19
+
20
+ ## Table of Contents
21
+ - [Features](#features)
22
+ - [Requirements](#requirements)
23
+ - [Installation](#installation)
24
+ - [Quick Start](#quick-start)
25
+ - [CLI and Options](#cli-and-options)
26
+ - [Filters](#filters)
27
+ - [Output format](#output-format)
28
+ - [Write to file](#write-to-file)
29
+ - [Exit codes](#exit-codes)
30
+ - [Reports and Examples](#reports-and-examples)
31
+ - [summary — repository summary](#summary--repository-summary)
32
+ - [activity — activity (day/week/month)](#activity--activity-dayweekmonth)
33
+ - [authors — by authors](#authors--by-authors)
34
+ - [files — by files](#files--by-files)
35
+ - [heatmap — commit heatmap](#heatmap--commit-heatmap)
36
+ - [languages — languages](#languages--languages)
37
+ - [Exports](#exports)
38
+ - [Console](#console)
39
+ - [JSON](#json)
40
+ - [CSV (DR-001)](#csv-dr-001)
41
+ - [Markdown](#markdown)
42
+ - [YAML](#yaml)
43
+ - [XML](#xml)
44
+ - [Determinism and Sorting](#determinism-and-sorting)
45
+ - [Windows Notes](#windows-notes)
46
+ - [Diagnostics and Errors](#diagnostics-and-errors)
47
+ - [FAQ](#faq)
48
+ - [Development](#development)
49
+ - [License](#license)
50
+
51
+ ## Features
52
+ * **Reports**: `summary`, `activity`, `authors`, `files`, `heatmap`, `languages`.
53
+ * **Filters**: branches, authors, paths, time period.
54
+ * **Exports**: `console`, `json`, `csv`, `md`, `yaml`, `xml`.
55
+ * **Output**: to stdout or file via `--out`.
56
+
57
+ ## Requirements
58
+ * **Ruby**: >= 3.4 (recommended 3.4.x)
59
+ * **Git**: installed and available in `PATH`
60
+
61
+ ## Installation
62
+ Choose one:
63
+
64
+ 1) From source (recommended for development)
65
+
66
+ ```bash
67
+ git clone <repo_url>
68
+ cd pretty-git
69
+ bin/setup
70
+ # run:
71
+ bundle exec bin/pretty-git --help
72
+ ```
73
+
74
+ 2) As a gem (after the first release)
75
+
76
+ ```bash
77
+ gem install pretty-git
78
+ pretty-git --version
79
+ ```
80
+
81
+ 3) Via Bundler
82
+
83
+ ```ruby
84
+ # Gemfile
85
+ gem 'pretty-git', '~> 0.1'
86
+ ```
87
+ ```bash
88
+ bundle install
89
+ bundle exec pretty-git --help
90
+ ```
91
+
92
+ ## Quick Start
93
+ ```bash
94
+ # Repository summary to console
95
+ bundle exec bin/pretty-git summary .
96
+
97
+ # Authors in JSON written to file
98
+ bundle exec bin/pretty-git authors . --format json --out authors.json
99
+
100
+ # Weekly activity for period only for selected paths
101
+ bundle exec bin/pretty-git activity . --time-bucket week --since 2025-01-01 \
102
+ --paths app,lib --format csv --out activity.csv
103
+ ```
104
+
105
+ ## CLI and Options
106
+ General form:
107
+
108
+ ```bash
109
+ pretty-git <report> <repo_path> [options]
110
+ ```
111
+
112
+ Available reports: `summary`, `activity`, `authors`, `files`, `heatmap`, `languages`.
113
+
114
+ Key options:
115
+ * **--format, -f** `console|json|csv|md|yaml|xml` (default `console`)
116
+ * **--out, -o** Path to write output file
117
+ * **--limit, -l** Number of items shown; `all` or `0` — no limit
118
+ * **--time-bucket** `day|week|month` (for `activity`)
119
+ * **--since/--until** Date/time in ISO8601 or `YYYY-MM-DD` (DR-005)
120
+ * **--branch** Multi-option, can be specified multiple times
121
+ * **--author/--exclude-author** Filter by authors
122
+ * **--path/--exclude-path** Filter by paths (comma-separated or repeated option)
123
+ * **--no-color** Disable colors in console
124
+ * **--theme** `basic|bright|mono` — console theme (default `basic`; `mono` forces monochrome)
125
+
126
+ Examples with multiple values:
127
+
128
+ ```bash
129
+ # Multiple branches
130
+ pretty-git summary . --branch main --branch develop
131
+
132
+ # Filter authors (include/exclude)
133
+ pretty-git authors . --author alice@example.com --exclude-author bot@company
134
+
135
+ # Filter paths
136
+ pretty-git files . --path app,lib --exclude-path vendor,node_modules
137
+ ```
138
+
139
+ ### Filters
140
+ Filters apply at commit fetch and later aggregation. Date format: ISO8601 or `YYYY-MM-DD`. If timezone is omitted — your local zone is assumed; output timestamps are normalized to UTC.
141
+
142
+ ### Output format
143
+ Set via `--format`. For file formats it’s recommended to use `--out`.
144
+
145
+ ### Write to file
146
+ ```bash
147
+ pretty-git authors . --format csv --out authors.csv
148
+ ```
149
+
150
+ ### Exit codes
151
+ * `0` — success
152
+ * `1` — user error (unknown report/format, bad arguments)
153
+ * `2` — system error (git error etc.)
154
+
155
+ ## Reports and Examples
156
+
157
+ ### summary — repository summary
158
+ ```bash
159
+ pretty-git summary . --format json
160
+ ```
161
+ Contains totals (commits, authors, additions, deletions) and top authors/files.
162
+
163
+ ### activity — activity (day/week/month)
164
+ ```bash
165
+ pretty-git activity . --time-bucket week --format csv
166
+ ```
167
+ CSV columns: `bucket,timestamp,commits,additions,deletions`.
168
+ JSON example:
169
+ ```json
170
+ [
171
+ {"bucket":"week","timestamp":"2025-06-02T00:00:00Z","commits":120,"additions":3456,"deletions":2100},
172
+ {"bucket":"week","timestamp":"2025-06-09T00:00:00Z","commits":98,"additions":2890,"deletions":1760}
173
+ ]
174
+ ```
175
+
176
+ ### authors — by authors
177
+ ```bash
178
+ pretty-git authors . --format md --limit 10
179
+ ```
180
+ CSV columns: `author,author_email,commits,additions,deletions,avg_commit_size`.
181
+ Markdown example:
182
+ ```markdown
183
+ | author | author_email | commits | additions | deletions | avg_commit_size |
184
+ |---|---|---:|---:|---:|---:|
185
+ | Alice | a@example.com | 2 | 5 | 1 | 3.0 |
186
+ | Bob | b@example.com | 1 | 2 | 0 | 2.0 |
187
+ ```
188
+
189
+ ### files — by files
190
+ ```bash
191
+ pretty-git files . --paths app,lib --format csv
192
+ ```
193
+ CSV columns: `path,commits,additions,deletions,changes`.
194
+ XML example:
195
+ ```xml
196
+ <files>
197
+ <item path="app/models/user.rb" commits="42" additions="2100" deletions="1400" changes="3500" />
198
+ <item path="app/services/auth.rb" commits="35" additions="1500" deletions="900" changes="2400" />
199
+ <generated_at>2025-01-31T00:00:00Z</generated_at>
200
+ <repo_path>/abs/path/to/repo</repo_path>
201
+ <report>files</report>
202
+ <period>
203
+ <since/>
204
+ <until/>
205
+ </period>
206
+ </files>
207
+ ```
208
+
209
+ ### heatmap — commit heatmap
210
+ ```bash
211
+ pretty-git heatmap . --format json
212
+ ```
213
+ JSON: an array of buckets for (day-of-week × hour) with commit counts.
214
+ CSV example:
215
+ ```csv
216
+ dow,hour,commits
217
+ 1,10,5
218
+ 1,11,7
219
+ ```
220
+
221
+ ### languages — languages
222
+ ```bash
223
+ pretty-git languages . --format md --limit 10
224
+ ```
225
+ Determines language distribution in a repository by summing file bytes per language (similar to GitHub Linguist). Output includes language, total size (bytes) and percent share.
226
+
227
+ Console example:
228
+ ```text
229
+ Languages for .
230
+
231
+ language bytes percent
232
+ -------- ---------- -------
233
+ Ruby 123456 60.0
234
+ JavaScript 78901 38.3
235
+ Markdown 1200 1.7
236
+ ```
237
+
238
+ ![Console output — languages](docs/images/PrettyGitConsoleLanguages.png)
239
+
240
+ Notes:
241
+ - **Detection**: by file extensions and certain filenames (`Makefile`, `Dockerfile`).
242
+ - **Exclusions**: binary files and "vendor"-like directories are ignored. By default `vendor/`, `node_modules/`, `.git/`, build artifacts and caches are skipped. For Python projects additional directories are skipped: `.venv/`, `venv/`, `env/`, `__pycache__/`, `.mypy_cache/`, `.pytest_cache/`, `.tox/`, `.eggs/`, `.ruff_cache/`, `.ipynb_checkpoints/`.
243
+ - **JSON**: JSON is not counted as a language by default to avoid data files skewing statistics.
244
+ - **Path filters**: use `--path/--exclude-path` (glob patterns supported) to focus on relevant areas.
245
+ - **Limit**: `--limit N` restricts number of rows; `0`/`all` — no limit.
246
+ - **Console colors**: language names use approximate GitHub colors; `--no-color` disables, `--theme mono` makes output monochrome.
247
+
248
+ See also: [Ignored directories and files](#ignored-directories-and-files).
249
+
250
+ Export:
251
+ - CSV/MD: columns — `language,bytes,percent`.
252
+ - JSON/YAML/XML: full report structure including metadata (`report`, `generated_at`, `repo_path`).
253
+
254
+ ## Exports
255
+
256
+ Below are exact serialization rules for each format to ensure compatibility with common tools (Excel, BI, CI, etc.).
257
+
258
+ ### Console
259
+ ![Console output — basic theme](docs/images/PrettyGitConsole.png)
260
+ _Example terminal output (theme: basic)._
261
+ * **Colors**: headers and table heads highlighted; totals: `commits` — yellow, `+additions` — green, `-deletions` — red. `--no-color` fully disables coloring.
262
+ * **Themes**: `--theme basic|bright|mono`. `bright` — more saturated headers, `mono` — monochrome (same as `--no-color`).
263
+ * **Highlight max**: numeric columns underline max values in bold for quick scanning.
264
+ * **Terminal width**: table output respects terminal width; first column is gracefully truncated with ellipsis `…` if needed.
265
+ * **Encoding**: UTF‑8, LF line endings.
266
+ * **Purpose**: human-readable terminal output.
267
+ * **Layout**: boxed tables, auto-truncation of long values.
268
+
269
+ ### JSON
270
+ * **Keys**: `snake_case`.
271
+ * **Numbers**: integers/floats without localization (dot decimal separator).
272
+ * **Boolean**: `true/false`; **null**: `null`.
273
+ * **Date/time**: ISO8601 in UTC, e.g. `2025-01-31T00:00:00Z`.
274
+ * **Order**: fields arranged logically and consistently (e.g., `report`, `generated_at`, `repo_path`, then data).
275
+ * **Encoding/line endings**: UTF‑8, LF.
276
+ * **Suggested extension**: `.json`.
277
+ * **Example**:
278
+ ```json
279
+ {"report":"summary","generated_at":"2025-01-31T00:00:00Z","totals":{"commits":123}}
280
+ ```
281
+
282
+ ### CSV (DR-001)
283
+ * **Structure**: flat table, first line is header.
284
+ * **Encoding**: UTF‑8 without BOM.
285
+ * **Delimiter**: comma `,`.
286
+ * **Escaping**: RFC 4180 — fields with commas/quotes/newlines are enclosed in double quotes, double quotes inside are doubled.
287
+ * **Empty values**: empty cell (not `null`).
288
+ * **Numbers**: no thousand separators, dot as decimal.
289
+ * **Date/time**: ISO8601 UTC.
290
+ * **Column order**: fixed per report and stable.
291
+ * **Line endings**: LF.
292
+ * **Suggested extension**: `.csv`.
293
+ * **Excel**: specify UTF‑8 on import.
294
+ * **Example**:
295
+ ```csv
296
+ author,author_email,commits,additions,deletions,avg_commit_size
297
+ Alice,a@example.com,2,5,1,3.0
298
+ Bob,b@example.com,1,2,0,2.0
299
+ ```
300
+
301
+ ### Markdown
302
+ * **Tables**: GitHub Flavored Markdown.
303
+ * **Alignment**: numeric columns are right-aligned (`---:`).
304
+ * **Encoding/line endings**: UTF‑8, LF.
305
+ * **Suggested extension**: `.md`.
306
+ * **Empty datasets**: header-only table or a short `No data` message (depends on report).
307
+ * **Example**:
308
+ ```markdown
309
+ | path | commits | additions | deletions |
310
+ |---|---:|---:|---:|
311
+ | app/models/user.rb | 42 | 2100 | 1400 |
312
+ ```
313
+
314
+ ### YAML
315
+ * **Structure**: full result hierarchy.
316
+ * **Keys**: serialized as strings.
317
+ * **Numbers/boolean/null**: standard YAML (`123`, `true/false`, `null`).
318
+ * **Date/time**: ISO8601 UTC as strings.
319
+ * **Encoding/line endings**: UTF‑8, LF.
320
+ * **Suggested extension**: `.yml` or `.yaml`.
321
+ * **Example**:
322
+ ```yaml
323
+ report: authors
324
+ generated_at: "2025-01-31T00:00:00Z"
325
+ items:
326
+ - author: Alice
327
+ author_email: a@example.com
328
+ commits: 2
329
+ - author: Bob
330
+ author_email: b@example.com
331
+ commits: 1
332
+ ```
333
+
334
+ ### XML
335
+ * **Structure**: elements correspond to keys; arrays — repeated `<item>` or specialized tags.
336
+ * **Attributes**: for compact rows (e.g., files report) main fields may be attributes.
337
+ * **Text nodes**: used for scalar values when needed.
338
+ * **Escaping**: `& < > " ' ` per XML rules; CDATA may be used for arbitrary text.
339
+ * **Date/time**: ISO8601 UTC.
340
+ * **Encoding/line endings**: UTF‑8, LF; declaration `<?xml version="1.0" encoding="UTF-8"?>` may be added by the generator.
341
+ * **Suggested extension**: `.xml`.
342
+ * **Example**:
343
+ ```xml
344
+ <authors>
345
+ <item author="Alice" author_email="a@example.com" commits="2" />
346
+ <item author="Bob" author_email="b@example.com" commits="1" />
347
+ <generated_at>2025-01-31T00:00:00Z</generated_at>
348
+ <repo_path>/abs/path</repo_path>
349
+ </authors>
350
+ ```
351
+
352
+ ## Ignored directories and files
353
+
354
+ To keep language statistics meaningful, certain directories and file types are skipped by default.
355
+
356
+ **Directories ignored** (any path segment matching one of these):
357
+
358
+ ```
359
+ vendor, node_modules, .git, .bundle, dist, build, out, target, coverage,
360
+ .venv, venv, env, __pycache__, .mypy_cache, .pytest_cache, .tox, .eggs, .ruff_cache,
361
+ .ipynb_checkpoints
362
+ ```
363
+
364
+ **Binary/data extensions ignored**:
365
+
366
+ ```
367
+ .png, .jpg, .jpeg, .gif, .svg, .webp, .ico, .bmp,
368
+ .pdf, .zip, .tar, .gz, .tgz, .bz2, .7z, .rar,
369
+ .mp3, .ogg, .wav, .mp4, .mov, .avi, .mkv,
370
+ .woff, .woff2, .ttf, .otf, .eot,
371
+ .jar, .class, .dll, .so, .dylib,
372
+ .exe, .bin, .dat
373
+ ```
374
+
375
+ These lists mirror the implementation in `lib/pretty_git/analytics/languages.rb` and may evolve.
376
+
377
+ ## Determinism and Sorting
378
+ 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.
379
+
380
+ ## Windows Notes
381
+ Primary targets — macOS/Linux. Windows is supported best‑effort:
382
+ * Running via Git Bash/WSL is OK
383
+ * Colors can be disabled by `--no-color`
384
+ * Carefully quote arguments when working with paths
385
+
386
+ ## Diagnostics and Errors
387
+ Typical issues and solutions:
388
+
389
+ * **Unknown report/format** — check the first argument and `--format`.
390
+ * **Invalid date format** — use ISO8601 or `YYYY-MM-DD` (e.g., `2025-01-31` or `2025-01-31T12:00:00Z`).
391
+ * **Git not available** — ensure `git` is installed and in the `PATH`.
392
+ * **Empty result** — check your filters (`--since/--until`, `--branch`, `--path`); your selection might be too narrow.
393
+ * **CSV encoding issues** — files are saved as UTF‑8; when opening in Excel, pick UTF‑8.
394
+
395
+ ## FAQ
396
+ * **Why Ruby 3.4+?** The project uses dependencies aligned with Ruby 3.4+ and targets the current ecosystem.
397
+ * **New formats?** Yes, add a renderer under `lib/pretty_git/render/` and wire it in the app.
398
+ * **Where does data come from?** From system `git` via CLI calls.
399
+
400
+ ## Development
401
+ ```bash
402
+ # Install deps
403
+ bin/setup
404
+
405
+ # Run tests and linter
406
+ bundle exec rspec
407
+ bundle exec rubocop
408
+ ```
409
+
410
+ Style — RuboCop clean. Tests cover aggregators, renderers, CLI, and integration scenarios (determinism, format correctness).
411
+
412
+ ## License
413
+ MIT © Contributors
data/bin/pretty-git ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/pretty_git'
5
+ require_relative '../lib/pretty_git/cli'
6
+
7
+ exit PrettyGit::CLI.run(ARGV)
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module PrettyGit
6
+ module Analytics
7
+ # Activity analytics: buckets commits by day/week/month.
8
+ class Activity
9
+ class << self
10
+ def call(enum, filters)
11
+ bucket = (filters.time_bucket || 'week').to_s
12
+ groups = aggregate(enum, bucket)
13
+ items = groups_to_items(groups, bucket)
14
+ build_result(filters, bucket, items)
15
+ end
16
+
17
+ private
18
+
19
+ def aggregate(enum, bucket)
20
+ groups = Hash.new { |h, k| h[k] = { commits: 0, additions: 0, deletions: 0 } }
21
+ enum.each { |c| process_commit(groups, bucket, c) }
22
+ groups
23
+ end
24
+
25
+ def process_commit(groups, bucket, commit)
26
+ ts = Time.parse(commit.authored_at.to_s).utc
27
+ key_time = bucket_start(ts, bucket)
28
+ g = groups[key_time]
29
+ g[:commits] += 1
30
+ g[:additions] += commit.additions.to_i
31
+ g[:deletions] += commit.deletions.to_i
32
+ end
33
+
34
+ def groups_to_items(groups, bucket)
35
+ groups.keys.sort.map do |t|
36
+ v = groups[t]
37
+ {
38
+ bucket: bucket,
39
+ timestamp: t.utc.iso8601,
40
+ commits: v[:commits],
41
+ additions: v[:additions],
42
+ deletions: v[:deletions]
43
+ }
44
+ end
45
+ end
46
+
47
+ def build_result(filters, bucket, items)
48
+ {
49
+ report: 'activity',
50
+ repo_path: File.expand_path(filters.repo_path),
51
+ period: { since: filters.since_iso8601, until: filters.until_iso8601 },
52
+ bucket: bucket,
53
+ items: items,
54
+ generated_at: Time.now.utc.iso8601
55
+ }
56
+ end
57
+
58
+ def bucket_start(time, bucket)
59
+ return start_of_iso_week(time) if bucket == 'week'
60
+ return start_of_month(time) if bucket == 'month'
61
+
62
+ start_of_day(time)
63
+ end
64
+
65
+ def start_of_day(time)
66
+ Time.utc(time.year, time.month, time.day)
67
+ end
68
+
69
+ def start_of_iso_week(time)
70
+ # ISO week starts on Monday. Find Monday of the current week at 00:00 UTC.
71
+ wday = (time.wday + 6) % 7 # Monday=0 .. Sunday=6
72
+ base = time - (wday * 86_400)
73
+ Time.utc(base.year, base.month, base.day)
74
+ end
75
+
76
+ def start_of_month(time)
77
+ Time.utc(time.year, time.month, 1)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module PrettyGit
6
+ module Analytics
7
+ # Builds authors report: commits, additions, deletions, avg_commit_size
8
+ class Authors
9
+ class << self
10
+ # Computes aggregates from a commits enumerator
11
+ # Returns a Hash suitable for JSON/YAML serialization and renderers
12
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
13
+ def call(commits_enum, filters)
14
+ authors = Hash.new { |h, k| h[k] = base_author(k) }
15
+
16
+ commits_enum.each do |c|
17
+ key = [c.author_name, c.author_email]
18
+ a = authors[key]
19
+ a[:commits] += 1
20
+ a[:additions] += c.additions
21
+ a[:deletions] += c.deletions
22
+ end
23
+
24
+ rows = authors.values.map do |a|
25
+ size = a[:additions] + a[:deletions]
26
+ avg = a[:commits].positive? ? (size.to_f / a[:commits]).round(2) : 0.0
27
+ a.merge(avg_commit_size: avg)
28
+ end
29
+
30
+ rows.sort_by! { |a| [-a[:commits], -a[:additions], a[:author]] }
31
+ rows = rows.first(filters.limit) if filters.limit
32
+
33
+ {
34
+ report: 'authors',
35
+ repo_path: filters.repo_path,
36
+ period: { since: filters.since_iso8601, until: filters.until_iso8601 },
37
+ totals: {
38
+ authors: authors.length,
39
+ commits: rows.sum { |r| r[:commits] },
40
+ additions: rows.sum { |r| r[:additions] },
41
+ deletions: rows.sum { |r| r[:deletions] }
42
+ },
43
+ items: rows,
44
+ generated_at: Time.now.utc.iso8601
45
+ }
46
+ end
47
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
48
+
49
+ private
50
+
51
+ def base_author(key)
52
+ name, email = key
53
+ {
54
+ author: name,
55
+ author_email: email,
56
+ commits: 0,
57
+ additions: 0,
58
+ deletions: 0
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrettyGit
4
+ module Analytics
5
+ # Files analytics: aggregates by file path across commits
6
+ class Files
7
+ class << self
8
+ def call(enum, filters)
9
+ per_file = aggregate_per_file(enum)
10
+ limit = normalize_limit(filters.limit)
11
+ items = build_items(per_file)
12
+ items = sort_and_limit(items, limit)
13
+ build_result(filters, items)
14
+ end
15
+
16
+ private
17
+
18
+ def aggregate_per_file(enum)
19
+ per_file = Hash.new { |h, k| h[k] = { commits: 0, additions: 0, deletions: 0 } }
20
+ enum.each do |c|
21
+ seen_paths = {}
22
+ c.files&.each { |f| process_file_entry(per_file, seen_paths, f) }
23
+ end
24
+ per_file
25
+ end
26
+
27
+ def process_file_entry(per_file, seen_paths, file_stat)
28
+ path = file_stat.path
29
+ unless seen_paths[path]
30
+ per_file[path][:commits] += 1
31
+ seen_paths[path] = true
32
+ end
33
+ per_file[path][:additions] += file_stat.additions.to_i
34
+ per_file[path][:deletions] += file_stat.deletions.to_i
35
+ end
36
+
37
+ def build_items(per_file)
38
+ per_file.map do |path, v|
39
+ {
40
+ path: path,
41
+ commits: v[:commits],
42
+ additions: v[:additions],
43
+ deletions: v[:deletions],
44
+ changes: v[:additions] + v[:deletions]
45
+ }
46
+ end
47
+ end
48
+
49
+ def build_result(filters, items)
50
+ {
51
+ report: 'files',
52
+ repo_path: File.expand_path(filters.repo_path),
53
+ period: { since: filters.since_iso8601, until: filters.until_iso8601 },
54
+ items: items,
55
+ generated_at: Time.now.utc.iso8601
56
+ }
57
+ end
58
+
59
+ def normalize_limit(raw)
60
+ return nil if raw.nil? || raw == 'all'
61
+
62
+ n = raw.to_i
63
+ n <= 0 ? nil : n
64
+ end
65
+
66
+ def sort_and_limit(arr, limit)
67
+ sorted = arr.sort_by { |h| [-h[:changes], -h[:commits], h[:path].to_s] }
68
+ limit ? sorted.first(limit) : sorted
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end