pretty-git 0.1.3 → 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 +19 -1
- data/README.md +34 -13
- data/README.ru.md +34 -13
- data/lib/pretty_git/cli.rb +6 -3
- data/lib/pretty_git/cli_helpers.rb +15 -2
- data/lib/pretty_git/filters.rb +11 -2
- data/lib/pretty_git/git/provider.rb +35 -3
- data/lib/pretty_git/render/csv_renderer.rb +1 -1
- data/lib/pretty_git/render/markdown_renderer.rb +51 -2
- 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 +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7e190585a91de27221e54bc2cbc58b5f6d26a1428c2e5cd5189ff0c18d362e6c
|
4
|
+
data.tar.gz: f8d2bc8f5d6c6a4887a745abd611a038166bcb54e95267a15d4868ee9d530003
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e11fd51d24ed1e63fd5c773f76a46510bbcedad23c02ace63b25d7d80c366479fd02fe2bfabaa58693389c222bafe4fd9e6fc0b157b7d598e96294a3f888fbef
|
7
|
+
data.tar.gz: 38fc87c6467d72ae6a1cb63a13b06c417e81f714d5b074feae961f121e061cea0733494020e0b8f562f36ea00b2d537a0d6fd5ac36eea942465680376f8eb71e
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,24 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
9
|
|
10
|
+
## [0.1.4] - 2025-08-17
|
11
|
+
### Added
|
12
|
+
- Integration tests for new reports exports: CSV/Markdown/YAML/XML for `hotspots`, `churn`, `ownership`.
|
13
|
+
- Schema validations: `rake validate:json`, `rake validate:xml` to ensure format compatibility.
|
14
|
+
- CI: expanded matrix to include macOS; smoke test for installed binary (`--help`, `--version`).
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
- Renderers (`MarkdownRenderer`, `YamlRenderer`, `XmlRenderer`): deterministic sorting for all new reports according to `docs/determinism.md`.
|
18
|
+
- XML: per-report root elements in XML exports to match XSDs (`hotspotsReport`, `churnReport`, `ownershipReport`, `languagesReport`, etc.).
|
19
|
+
- Documentation: `README.md` and `README.ru.md` updated with sections and examples for new reports and all export formats.
|
20
|
+
- CLI: keep `--time-bucket` permissive; default `time_bucket=nil`.
|
21
|
+
|
22
|
+
### Fixed
|
23
|
+
- Time parsing: interpret date-only inputs (`YYYY-MM-DD`) as UTC midnight and normalize to UTC ISO8601.
|
24
|
+
- CLI UX: error when `--metric` is used outside `languages` report.
|
25
|
+
- Tests/specs: updated XML specs to per-report roots; added timezone edge cases; fixed Open3 `popen3` stubs (`chdir:`) and integration requires.
|
26
|
+
|
27
|
+
|
10
28
|
## [0.1.3] - 2025-08-14
|
11
29
|
### Added
|
12
30
|
- New analytics reports: `hotspots`, `churn`, `ownership` with sorting, scoring, and limits.
|
@@ -30,7 +48,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
|
|
30
48
|
### Changed
|
31
49
|
- Languages: JSON language reinstated in the mapping and color scheme; sorting and percent calculations are based on the selected metric; percentages rounded to two decimals.
|
32
50
|
- Renderers: updated `csv`, `markdown`, and console renderers to work with dynamic metrics.
|
33
|
-
- Internal specs updated: `
|
51
|
+
- Internal specs updated: `docs/output_formats.md`, `docs/cli_spec.md`, `docs/languages_map.md`.
|
34
52
|
|
35
53
|
### Fixed
|
36
54
|
- Git provider: correct commit counting — emit a new commit when a header is read and remove the record separator from the subject (`lib/pretty_git/git/provider.rb`).
|
data/README.md
CHANGED
@@ -124,6 +124,10 @@ General form:
|
|
124
124
|
pretty-git <report> <repo_path> [options]
|
125
125
|
```
|
126
126
|
|
127
|
+
Notes:
|
128
|
+
* `<repo_path>` defaults to `.` if omitted.
|
129
|
+
* You can also pass the repository via `--repo PATH` as an alternative to the positional argument.
|
130
|
+
|
127
131
|
Available reports: `summary`, `activity`, `authors`, `files`, `heatmap`, `languages`, `hotspots`, `churn`, `ownership`.
|
128
132
|
|
129
133
|
Key options:
|
@@ -207,19 +211,31 @@ Markdown example:
|
|
207
211
|
pretty-git files . --paths app,lib --format csv
|
208
212
|
```
|
209
213
|
CSV columns: `path,commits,additions,deletions,changes`.
|
214
|
+
|
210
215
|
XML example:
|
211
216
|
```xml
|
212
|
-
|
213
|
-
|
214
|
-
<
|
217
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
218
|
+
<report>
|
219
|
+
<report>files</report>
|
215
220
|
<generated_at>2025-01-31T00:00:00Z</generated_at>
|
216
221
|
<repo_path>/abs/path/to/repo</repo_path>
|
217
|
-
<
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
</
|
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>
|
223
239
|
```
|
224
240
|
|
225
241
|
### 🔥 heatmap — commit heatmap
|
@@ -337,15 +353,20 @@ Notes:
|
|
337
353
|
|
338
354
|
XML example:
|
339
355
|
```xml
|
340
|
-
|
356
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
357
|
+
<report>
|
341
358
|
<report>ownership</report>
|
342
359
|
<generated_at>2025-01-31T00:00:00Z</generated_at>
|
343
360
|
<repo_path>.</repo_path>
|
344
361
|
<items>
|
345
|
-
<item
|
362
|
+
<item>
|
363
|
+
<path>lib/a.rb</path>
|
364
|
+
<owner>Alice <a@example.com></owner>
|
365
|
+
<owner_share>82.5</owner_share>
|
366
|
+
<authors>2</authors>
|
367
|
+
</item>
|
346
368
|
</items>
|
347
|
-
|
348
|
-
</ownership>
|
369
|
+
</report>
|
349
370
|
```
|
350
371
|
|
351
372
|
## 📤 Exports
|
data/README.ru.md
CHANGED
@@ -124,6 +124,10 @@ bundle exec bin/pretty-git activity . --time-bucket week --since 2025-01-01 \
|
|
124
124
|
pretty-git <report> <repo_path> [options]
|
125
125
|
```
|
126
126
|
|
127
|
+
Примечания:
|
128
|
+
* `<repo_path>` по умолчанию — `.` (если опущен).
|
129
|
+
* Репозиторий можно указать и через флаг `--repo PATH` как альтернативу позиционному аргументу.
|
130
|
+
|
127
131
|
Доступные отчёты: `summary`, `activity`, `authors`, `files`, `heatmap`, `languages`, `hotspots`, `churn`, `ownership`.
|
128
132
|
|
129
133
|
Ключевые опции:
|
@@ -207,19 +211,31 @@ CSV-колонки: `author,author_email,commits,additions,deletions,avg_commit_
|
|
207
211
|
pretty-git files . --paths app,lib --format csv
|
208
212
|
```
|
209
213
|
CSV-колонки: `path,commits,additions,deletions,changes`.
|
214
|
+
|
210
215
|
Пример XML:
|
211
216
|
```xml
|
212
|
-
|
213
|
-
|
214
|
-
<
|
217
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
218
|
+
<report>
|
219
|
+
<report>files</report>
|
215
220
|
<generated_at>2025-01-31T00:00:00Z</generated_at>
|
216
221
|
<repo_path>/abs/path/to/repo</repo_path>
|
217
|
-
<
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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>
|
223
239
|
```
|
224
240
|
|
225
241
|
### 🔥 heatmap — тепловая карта
|
@@ -335,15 +351,20 @@ CSV‑колонки: `path,owner,owner_share,authors`.
|
|
335
351
|
|
336
352
|
Пример XML:
|
337
353
|
```xml
|
338
|
-
|
354
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
355
|
+
<report>
|
339
356
|
<report>ownership</report>
|
340
357
|
<generated_at>2025-01-31T00:00:00Z</generated_at>
|
341
358
|
<repo_path>.</repo_path>
|
342
359
|
<items>
|
343
|
-
<item
|
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>
|
344
366
|
</items>
|
345
|
-
|
346
|
-
</ownership>
|
367
|
+
</report>
|
347
368
|
```
|
348
369
|
|
349
370
|
## 🚫 Игнорируемые директории и файлы
|
data/lib/pretty_git/cli.rb
CHANGED
@@ -12,7 +12,7 @@ module PrettyGit
|
|
12
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
|
@@ -16,7 +16,7 @@ module PrettyGit
|
|
16
16
|
module_function
|
17
17
|
|
18
18
|
def configure_parser(opts, options)
|
19
|
-
opts.banner = 'Usage: pretty-git [REPORT] [options]'
|
19
|
+
opts.banner = 'Usage: pretty-git [REPORT] [REPO] [options]'
|
20
20
|
add_repo_options(opts, options)
|
21
21
|
add_time_author_options(opts, options)
|
22
22
|
add_path_limit_options(opts, options)
|
@@ -75,7 +75,9 @@ module PrettyGit
|
|
75
75
|
code = handle_version_help(options, parser, out)
|
76
76
|
return code unless code.nil?
|
77
77
|
|
78
|
-
|
78
|
+
base_ok = valid_report?(options[:report]) && valid_theme?(options[:theme]) && valid_metric?(options[:metric])
|
79
|
+
conflicts_ok = validate_conflicts(options, err)
|
80
|
+
return nil if base_ok && conflicts_ok
|
79
81
|
|
80
82
|
print_validation_errors(options, err)
|
81
83
|
1
|
@@ -112,6 +114,17 @@ module PrettyGit
|
|
112
114
|
err.puts "Unknown metric: #{options[:metric]}. Supported: #{METRICS.join(', ')}"
|
113
115
|
end
|
114
116
|
|
117
|
+
# Returns true when flags are consistent; otherwise prints errors and returns false
|
118
|
+
def validate_conflicts(options, err)
|
119
|
+
ok = true
|
120
|
+
if options[:metric] && options[:report] != 'languages'
|
121
|
+
err.puts "--metric is only supported for 'languages' report"
|
122
|
+
ok = false
|
123
|
+
end
|
124
|
+
# time_bucket is accepted by multiple reports historically; do not enforce here.
|
125
|
+
ok
|
126
|
+
end
|
127
|
+
|
115
128
|
def build_filters(options)
|
116
129
|
Filters.new(
|
117
130
|
repo_path: options[:repo],
|
data/lib/pretty_git/filters.rb
CHANGED
@@ -31,14 +31,23 @@ module PrettyGit
|
|
31
31
|
|
32
32
|
private
|
33
33
|
|
34
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
34
35
|
def time_to_iso8601(val)
|
35
36
|
return nil if val.nil? || val.to_s.strip.empty?
|
36
37
|
|
37
|
-
|
38
|
-
|
38
|
+
# If value is a date without time, interpret as UTC midnight to avoid
|
39
|
+
# timezone-dependent shifts across environments.
|
40
|
+
if val.is_a?(String) && val.match?(/^\d{4}-\d{2}-\d{2}$/)
|
41
|
+
y, m, d = val.split('-').map(&:to_i)
|
42
|
+
t = Time.new(y, m, d, 0, 0, 0, '+00:00')
|
43
|
+
else
|
44
|
+
# Otherwise parse normally and normalize to UTC.
|
45
|
+
t = val.is_a?(Time) ? val : Time.parse(val.to_s)
|
46
|
+
end
|
39
47
|
t.utc.iso8601
|
40
48
|
rescue ArgumentError
|
41
49
|
raise ArgumentError, "Invalid datetime: #{val} (expected ISO8601 or YYYY-MM-DD)"
|
42
50
|
end
|
51
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
43
52
|
end
|
44
53
|
end
|
@@ -7,6 +7,7 @@ require_relative '../types'
|
|
7
7
|
module PrettyGit
|
8
8
|
module Git
|
9
9
|
# Streams commits from git CLI using `git log --numstat` and parses them.
|
10
|
+
# rubocop:disable Metrics/ClassLength
|
10
11
|
class Provider
|
11
12
|
SEP_RECORD = "\x1E" # record separator
|
12
13
|
SEP_FIELD = "\x1F" # unit separator
|
@@ -57,7 +58,7 @@ module PrettyGit
|
|
57
58
|
|
58
59
|
additions = current[:files].sum(&:additions)
|
59
60
|
deletions = current[:files].sum(&:deletions)
|
60
|
-
|
61
|
+
commit = Types::Commit.new(
|
61
62
|
sha: current[:sha],
|
62
63
|
author_name: current[:author_name],
|
63
64
|
author_email: current[:author_email],
|
@@ -67,6 +68,9 @@ module PrettyGit
|
|
67
68
|
deletions: deletions,
|
68
69
|
files: current[:files]
|
69
70
|
)
|
71
|
+
return if exclude_author?(commit.author_name, commit.author_email)
|
72
|
+
|
73
|
+
yld << commit
|
70
74
|
end
|
71
75
|
|
72
76
|
def record_separator?(line)
|
@@ -120,13 +124,41 @@ module PrettyGit
|
|
120
124
|
@filters.branches&.each { |b| args << "--branches=#{b}" }
|
121
125
|
end
|
122
126
|
|
127
|
+
# rubocop:disable Metrics/AbcSize
|
123
128
|
def add_path_filters(args)
|
124
129
|
path_args = Array(@filters.paths).compact
|
125
|
-
|
130
|
+
exclude_args = Array(@filters.exclude_paths).compact
|
131
|
+
|
132
|
+
# Nothing to filter by
|
133
|
+
return if path_args.empty? && exclude_args.empty?
|
126
134
|
|
127
135
|
args << '--'
|
128
|
-
|
136
|
+
|
137
|
+
# If only excludes provided, include all paths first
|
138
|
+
args << '.' if path_args.empty? && !exclude_args.empty?
|
139
|
+
|
140
|
+
# Include patterns as-is
|
141
|
+
args.concat(path_args) unless path_args.empty?
|
142
|
+
|
143
|
+
# Exclude patterns via git pathspec magic with glob
|
144
|
+
exclude_args.each do |pat|
|
145
|
+
args << ":(exclude,glob)#{pat}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
# rubocop:enable Metrics/AbcSize
|
149
|
+
|
150
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
151
|
+
def exclude_author?(name, email)
|
152
|
+
patterns = Array(@filters.exclude_authors).compact
|
153
|
+
return false if patterns.empty?
|
154
|
+
|
155
|
+
patterns.any? do |pat|
|
156
|
+
pn = pat.to_s
|
157
|
+
name&.downcase&.include?(pn.downcase) || email&.downcase&.include?(pn.downcase)
|
158
|
+
end
|
129
159
|
end
|
160
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
130
161
|
end
|
131
162
|
end
|
132
163
|
end
|
164
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -4,7 +4,7 @@ require 'csv'
|
|
4
4
|
|
5
5
|
module PrettyGit
|
6
6
|
module Render
|
7
|
-
# Renders CSV according to
|
7
|
+
# Renders CSV according to docs/output_formats.md and DR-001
|
8
8
|
class CsvRenderer
|
9
9
|
HEADERS = {
|
10
10
|
'activity' => %w[bucket timestamp commits additions deletions],
|
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
module PrettyGit
|
4
4
|
module Render
|
5
|
-
# Renders Markdown tables and sections per
|
5
|
+
# Renders Markdown tables and sections per docs/output_formats.md
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
6
7
|
class MarkdownRenderer
|
7
8
|
TITLES = {
|
8
9
|
'activity' => 'Activity',
|
@@ -33,7 +34,8 @@ module PrettyGit
|
|
33
34
|
|
34
35
|
headers = headers_for(report, result)
|
35
36
|
title = title_for(report)
|
36
|
-
|
37
|
+
rows = sort_rows(report, result[:items], result)
|
38
|
+
render_table(title, headers, rows)
|
37
39
|
end
|
38
40
|
|
39
41
|
private
|
@@ -82,6 +84,53 @@ module PrettyGit
|
|
82
84
|
end
|
83
85
|
@io.puts 'No data' if rows.empty?
|
84
86
|
end
|
87
|
+
|
88
|
+
# Deterministic ordering per docs/determinism.md
|
89
|
+
# NOTE: High branching is intentional to keep tie-breakers explicit and stable across formats.
|
90
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
91
|
+
def sort_rows(report, rows, ctx = nil)
|
92
|
+
return rows unless rows.is_a?(Array)
|
93
|
+
|
94
|
+
case report
|
95
|
+
when 'hotspots'
|
96
|
+
rows.sort_by { |r| [-to_f(r[:score]), -to_i(r[:commits]), -to_i(r[:changes]), to_s(r[:path])] }
|
97
|
+
when 'churn'
|
98
|
+
rows.sort_by { |r| [-to_i(r[:churn]), -to_i(r[:commits]), to_s(r[:path])] }
|
99
|
+
when 'ownership'
|
100
|
+
rows.sort_by { |r| [-to_f(r[:owner_share]), -to_i(r[:authors]), to_s(r[:path])] }
|
101
|
+
when 'files'
|
102
|
+
rows.sort_by { |r| [-to_i(r[:changes]), -to_i(r[:commits]), to_s(r[:path])] }
|
103
|
+
when 'authors'
|
104
|
+
rows.sort_by { |r| [-to_i(r[:commits]), -to_i(r[:additions]), -to_i(r[:deletions]), to_s(r[:author_email])] }
|
105
|
+
when 'languages'
|
106
|
+
metric = ctx && ctx[:metric] ? ctx[:metric].to_sym : :bytes
|
107
|
+
rows.sort_by { |r| [-to_i(r[metric]), to_s(r[:language])] }
|
108
|
+
when 'activity'
|
109
|
+
rows.sort_by { |r| [to_s(r[:timestamp])] }
|
110
|
+
when 'heatmap'
|
111
|
+
rows.sort_by { |r| [to_i(r[:dow] || r[:day] || r[:weekday]), to_i(r[:hour])] }
|
112
|
+
else
|
113
|
+
rows
|
114
|
+
end
|
115
|
+
end
|
116
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
117
|
+
|
118
|
+
def to_i(val)
|
119
|
+
Integer(val || 0)
|
120
|
+
rescue StandardError
|
121
|
+
0
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_f(val)
|
125
|
+
Float(val || 0.0)
|
126
|
+
rescue StandardError
|
127
|
+
0.0
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_s(val)
|
131
|
+
(val || '').to_s
|
132
|
+
end
|
85
133
|
end
|
134
|
+
# rubocop:enable Metrics/ClassLength
|
86
135
|
end
|
87
136
|
end
|
@@ -10,15 +10,29 @@ module PrettyGit
|
|
10
10
|
@io = io
|
11
11
|
end
|
12
12
|
|
13
|
-
|
13
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
14
|
+
def call(report, result, _filters)
|
15
|
+
ordered = apply_order(report, result)
|
14
16
|
doc = REXML::Document.new
|
15
17
|
doc << REXML::XMLDecl.new('1.0', 'UTF-8')
|
16
|
-
|
17
|
-
|
18
|
+
root_name = case report
|
19
|
+
when 'hotspots' then 'hotspotsReport'
|
20
|
+
when 'churn' then 'churnReport'
|
21
|
+
when 'ownership' then 'ownershipReport'
|
22
|
+
when 'languages' then 'languagesReport'
|
23
|
+
when 'files' then 'filesReport'
|
24
|
+
when 'authors' then 'authorsReport'
|
25
|
+
when 'activity' then 'activityReport'
|
26
|
+
when 'heatmap' then 'heatmapReport'
|
27
|
+
else 'report'
|
28
|
+
end
|
29
|
+
root = doc.add_element(root_name)
|
30
|
+
hash_to_xml(root, ordered)
|
18
31
|
formatter = REXML::Formatters::Pretty.new(2)
|
19
32
|
formatter.compact = true
|
20
33
|
formatter.write(doc, @io)
|
21
34
|
end
|
35
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
22
36
|
|
23
37
|
private
|
24
38
|
|
@@ -38,6 +52,60 @@ module PrettyGit
|
|
38
52
|
parent.text = obj.nil? ? '' : obj.to_s
|
39
53
|
end
|
40
54
|
end
|
55
|
+
|
56
|
+
# Deterministic ordering per docs/determinism.md
|
57
|
+
# NOTE: High branching is intentional to keep tie-breakers explicit and stable across formats.
|
58
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
59
|
+
def apply_order(report, result)
|
60
|
+
dup = Marshal.load(Marshal.dump(result))
|
61
|
+
items = dup[:items]
|
62
|
+
return dup unless items.is_a?(Array)
|
63
|
+
|
64
|
+
dup[:items] =
|
65
|
+
case report
|
66
|
+
when 'hotspots'
|
67
|
+
items.sort_by do |r|
|
68
|
+
[-to_f(r[:score]), -to_i(r[:commits]), -to_i(r[:changes]), to_s(r[:path])]
|
69
|
+
end
|
70
|
+
when 'churn'
|
71
|
+
items.sort_by { |r| [-to_i(r[:churn]), -to_i(r[:commits]), to_s(r[:path])] }
|
72
|
+
when 'ownership'
|
73
|
+
items.sort_by { |r| [-to_f(r[:owner_share]), -to_i(r[:authors]), to_s(r[:path])] }
|
74
|
+
when 'files'
|
75
|
+
items.sort_by { |r| [-to_i(r[:changes]), -to_i(r[:commits]), to_s(r[:path])] }
|
76
|
+
when 'authors'
|
77
|
+
items.sort_by do |r|
|
78
|
+
[-to_i(r[:commits]), -to_i(r[:additions]), -to_i(r[:deletions]), to_s(r[:author_email])]
|
79
|
+
end
|
80
|
+
when 'languages'
|
81
|
+
metric = (dup[:metric] || 'bytes').to_sym
|
82
|
+
items.sort_by { |r| [-to_i(r[metric]), to_s(r[:language])] }
|
83
|
+
when 'activity'
|
84
|
+
items.sort_by { |r| [to_s(r[:timestamp])] }
|
85
|
+
when 'heatmap'
|
86
|
+
items.sort_by { |r| [to_i(r[:dow] || r[:day] || r[:weekday]), to_i(r[:hour])] }
|
87
|
+
else
|
88
|
+
items
|
89
|
+
end
|
90
|
+
dup
|
91
|
+
end
|
92
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
93
|
+
|
94
|
+
def to_i(val)
|
95
|
+
Integer(val || 0)
|
96
|
+
rescue StandardError
|
97
|
+
0
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_f(val)
|
101
|
+
Float(val || 0.0)
|
102
|
+
rescue StandardError
|
103
|
+
0.0
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_s(val)
|
107
|
+
(val || '').to_s
|
108
|
+
end
|
41
109
|
end
|
42
110
|
end
|
43
111
|
end
|
@@ -10,9 +10,11 @@ module PrettyGit
|
|
10
10
|
@io = io
|
11
11
|
end
|
12
12
|
|
13
|
-
def call(
|
13
|
+
def call(report, result, _filters)
|
14
|
+
# Apply deterministic ordering for items where applicable
|
15
|
+
ordered = apply_order(report, result)
|
14
16
|
# Dump the entire result structure to YAML with string keys for safe parsing
|
15
|
-
@io.write(stringify_keys(
|
17
|
+
@io.write(stringify_keys(ordered).to_yaml)
|
16
18
|
end
|
17
19
|
|
18
20
|
private
|
@@ -29,6 +31,60 @@ module PrettyGit
|
|
29
31
|
obj
|
30
32
|
end
|
31
33
|
end
|
34
|
+
|
35
|
+
# Deterministic ordering per docs/determinism.md
|
36
|
+
# NOTE: High branching is intentional to keep tie-breakers explicit and stable across formats.
|
37
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
38
|
+
def apply_order(report, result)
|
39
|
+
dup = Marshal.load(Marshal.dump(result)) # deep dup
|
40
|
+
items = dup[:items]
|
41
|
+
return dup unless items.is_a?(Array)
|
42
|
+
|
43
|
+
dup[:items] =
|
44
|
+
case report
|
45
|
+
when 'hotspots'
|
46
|
+
items.sort_by do |r|
|
47
|
+
[-to_f(r[:score]), -to_i(r[:commits]), -to_i(r[:changes]), to_s(r[:path])]
|
48
|
+
end
|
49
|
+
when 'churn'
|
50
|
+
items.sort_by { |r| [-to_i(r[:churn]), -to_i(r[:commits]), to_s(r[:path])] }
|
51
|
+
when 'ownership'
|
52
|
+
items.sort_by { |r| [-to_f(r[:owner_share]), -to_i(r[:authors]), to_s(r[:path])] }
|
53
|
+
when 'files'
|
54
|
+
items.sort_by { |r| [-to_i(r[:changes]), -to_i(r[:commits]), to_s(r[:path])] }
|
55
|
+
when 'authors'
|
56
|
+
items.sort_by do |r|
|
57
|
+
[-to_i(r[:commits]), -to_i(r[:additions]), -to_i(r[:deletions]), to_s(r[:author_email])]
|
58
|
+
end
|
59
|
+
when 'languages'
|
60
|
+
metric = (dup[:metric] || 'bytes').to_sym
|
61
|
+
items.sort_by { |r| [-to_i(r[metric]), to_s(r[:language])] }
|
62
|
+
when 'activity'
|
63
|
+
items.sort_by { |r| [to_s(r[:timestamp])] }
|
64
|
+
when 'heatmap'
|
65
|
+
items.sort_by { |r| [to_i(r[:dow] || r[:day] || r[:weekday]), to_i(r[:hour])] }
|
66
|
+
else
|
67
|
+
items
|
68
|
+
end
|
69
|
+
dup
|
70
|
+
end
|
71
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
72
|
+
|
73
|
+
def to_i(val)
|
74
|
+
Integer(val || 0)
|
75
|
+
rescue StandardError
|
76
|
+
0
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_f(val)
|
80
|
+
Float(val || 0.0)
|
81
|
+
rescue StandardError
|
82
|
+
0.0
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_s(val)
|
86
|
+
(val || '').to_s
|
87
|
+
end
|
32
88
|
end
|
33
89
|
end
|
34
90
|
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.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pretty Git Authors
|
@@ -51,8 +51,6 @@ dependencies:
|
|
51
51
|
version: '4.0'
|
52
52
|
description: Generates structured analytics from local Git repositories with multiple
|
53
53
|
export formats.
|
54
|
-
email:
|
55
|
-
- ''
|
56
54
|
executables:
|
57
55
|
- pretty-git
|
58
56
|
extensions: []
|
@@ -92,9 +90,11 @@ homepage: https://github.com/MikoMikocchi/pretty-git
|
|
92
90
|
licenses:
|
93
91
|
- MIT
|
94
92
|
metadata:
|
93
|
+
homepage_uri: https://github.com/MikoMikocchi/pretty-git
|
95
94
|
source_code_uri: https://github.com/MikoMikocchi/pretty-git
|
96
95
|
changelog_uri: https://github.com/MikoMikocchi/pretty-git/blob/main/CHANGELOG.md
|
97
96
|
bug_tracker_uri: https://github.com/MikoMikocchi/pretty-git/issues
|
97
|
+
documentation_uri: https://github.com/MikoMikocchi/pretty-git#readme
|
98
98
|
rubygems_mfa_required: 'true'
|
99
99
|
rdoc_options: []
|
100
100
|
require_paths:
|