pretty-git 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -2
- data/README.md +112 -19
- data/README.ru.md +110 -16
- data/lib/pretty_git/analytics/languages.rb +54 -15
- data/lib/pretty_git/app.rb +22 -15
- data/lib/pretty_git/cli.rb +7 -6
- data/lib/pretty_git/cli_helpers.rb +157 -18
- data/lib/pretty_git/constants.rb +15 -0
- data/lib/pretty_git/filters.rb +41 -13
- data/lib/pretty_git/git/provider.rb +67 -10
- data/lib/pretty_git/logger.rb +20 -0
- 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/utils/path_utils.rb +30 -0
- data/lib/pretty_git/utils/time_utils.rb +39 -0
- data/lib/pretty_git/version.rb +1 -1
- metadata +7 -3
@@ -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
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrettyGit
|
4
|
+
module Utils
|
5
|
+
# Utilities for path/glob normalization and handling cross-platform quirks.
|
6
|
+
module PathUtils
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# Normalize a string path or glob to Unicode NFC form.
|
10
|
+
# Returns nil if input is nil.
|
11
|
+
def normalize_nfc(str)
|
12
|
+
return nil if str.nil?
|
13
|
+
|
14
|
+
s = str.to_s
|
15
|
+
# Only normalize if supported in this Ruby build; otherwise return as-is
|
16
|
+
if s.respond_to?(:unicode_normalize)
|
17
|
+
s.unicode_normalize(:nfc)
|
18
|
+
else
|
19
|
+
s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Normalize each entry in an Array-like collection to NFC and compact
|
24
|
+
# nils. Returns an Array.
|
25
|
+
def normalize_globs(collection)
|
26
|
+
Array(collection).compact.map { |p| normalize_nfc(p) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module PrettyGit
|
6
|
+
module Utils
|
7
|
+
# Utilities for time parsing and ISO8601 normalization used by filters.
|
8
|
+
module TimeUtils
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# Converts various time inputs to ISO8601 in UTC.
|
12
|
+
# Accepts Time, String(ISO8601), or String(YYYY-MM-DD) treated as UTC midnight.
|
13
|
+
# Returns nil for nil/blank input. Raises ArgumentError for invalid values.
|
14
|
+
def to_utc_iso8601(val)
|
15
|
+
return nil if val.nil? || val.to_s.strip.empty?
|
16
|
+
|
17
|
+
parse_to_time(val).utc.iso8601
|
18
|
+
rescue ArgumentError
|
19
|
+
raise ArgumentError, "Invalid datetime: #{val} (expected ISO8601 or YYYY-MM-DD)"
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_to_time(val)
|
23
|
+
return val if val.is_a?(Time)
|
24
|
+
return parse_date_only(val) if val.is_a?(String) && date_only?(val)
|
25
|
+
|
26
|
+
Time.parse(val.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse_date_only(str)
|
30
|
+
y, m, d = str.split('-').map(&:to_i)
|
31
|
+
Time.new(y, m, d, 0, 0, 0, '+00:00')
|
32
|
+
end
|
33
|
+
|
34
|
+
def date_only?(str)
|
35
|
+
!!(str =~ /^\d{4}-\d{2}-\d{2}$/)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/pretty_git/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pretty-git
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pretty Git Authors
|
@@ -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: []
|
@@ -76,8 +74,10 @@ files:
|
|
76
74
|
- lib/pretty_git/app.rb
|
77
75
|
- lib/pretty_git/cli.rb
|
78
76
|
- lib/pretty_git/cli_helpers.rb
|
77
|
+
- lib/pretty_git/constants.rb
|
79
78
|
- lib/pretty_git/filters.rb
|
80
79
|
- lib/pretty_git/git/provider.rb
|
80
|
+
- lib/pretty_git/logger.rb
|
81
81
|
- lib/pretty_git/render/console_renderer.rb
|
82
82
|
- lib/pretty_git/render/csv_renderer.rb
|
83
83
|
- lib/pretty_git/render/json_renderer.rb
|
@@ -87,14 +87,18 @@ files:
|
|
87
87
|
- lib/pretty_git/render/xml_renderer.rb
|
88
88
|
- lib/pretty_git/render/yaml_renderer.rb
|
89
89
|
- lib/pretty_git/types.rb
|
90
|
+
- lib/pretty_git/utils/path_utils.rb
|
91
|
+
- lib/pretty_git/utils/time_utils.rb
|
90
92
|
- lib/pretty_git/version.rb
|
91
93
|
homepage: https://github.com/MikoMikocchi/pretty-git
|
92
94
|
licenses:
|
93
95
|
- MIT
|
94
96
|
metadata:
|
97
|
+
homepage_uri: https://github.com/MikoMikocchi/pretty-git
|
95
98
|
source_code_uri: https://github.com/MikoMikocchi/pretty-git
|
96
99
|
changelog_uri: https://github.com/MikoMikocchi/pretty-git/blob/main/CHANGELOG.md
|
97
100
|
bug_tracker_uri: https://github.com/MikoMikocchi/pretty-git/issues
|
101
|
+
documentation_uri: https://github.com/MikoMikocchi/pretty-git#readme
|
98
102
|
rubygems_mfa_required: 'true'
|
99
103
|
rdoc_options: []
|
100
104
|
require_paths:
|