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.
@@ -9,14 +9,14 @@ module PrettyGit
9
9
  # and RuboCop-compliant. Provides parser configuration and execution utilities.
10
10
  # rubocop:disable Metrics/ModuleLength
11
11
  module CLIHelpers
12
- REPORTS = %w[summary activity authors files heatmap languages].freeze
12
+ REPORTS = %w[summary activity authors files heatmap languages hotspots churn ownership].freeze
13
13
  FORMATS = %w[console json csv md yaml xml].freeze
14
14
  METRICS = %w[bytes files loc].freeze
15
15
 
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
- return nil if valid_report?(options[:report]) && valid_theme?(options[:theme]) && valid_metric?(options[:metric])
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],
@@ -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
- t = val.is_a?(Time) ? val : Time.parse(val.to_s)
38
- t = t.getlocal if t.utc_offset.nil?
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
- yld << Types::Commit.new(
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
- return if path_args.empty?
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
- args.concat(path_args)
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
@@ -153,6 +153,7 @@ module PrettyGit
153
153
  end
154
154
 
155
155
  # Renders human-friendly console output with optional colors.
156
+ # rubocop:disable Metrics/ClassLength
156
157
  class ConsoleRenderer
157
158
  def initialize(io: $stdout, color: true, theme: 'basic')
158
159
  @io = io
@@ -162,22 +163,21 @@ module PrettyGit
162
163
  end
163
164
 
164
165
  def call(report, result, _filters)
165
- case report
166
- when 'summary'
167
- render_summary(result)
168
- when 'activity'
169
- render_activity(result)
170
- when 'authors'
171
- render_authors(result)
172
- when 'files'
173
- render_files(result)
174
- when 'heatmap'
175
- render_heatmap(result)
176
- when 'languages'
177
- LanguagesSection.render(@io, @table, result, color: @color)
178
- else
179
- @io.puts result.inspect
180
- end
166
+ handlers = {
167
+ 'summary' => method(:render_summary),
168
+ 'activity' => method(:render_activity),
169
+ 'authors' => method(:render_authors),
170
+ 'files' => method(:render_files),
171
+ 'heatmap' => method(:render_heatmap),
172
+ 'languages' => ->(data) { LanguagesSection.render(@io, @table, data, color: @color) },
173
+ 'hotspots' => method(:render_hotspots),
174
+ 'churn' => method(:render_churn),
175
+ 'ownership' => method(:render_ownership)
176
+ }
177
+ handler = handlers[report]
178
+ return @io.puts(result.inspect) unless handler
179
+
180
+ handler.call(result)
181
181
  end
182
182
 
183
183
  private
@@ -266,6 +266,42 @@ module PrettyGit
266
266
 
267
267
  # Languages rendering moved to PrettyGit::Render::LanguagesSection
268
268
 
269
+ def render_hotspots(data)
270
+ title "Hotspots for #{data[:repo_path]}"
271
+ line "Period: #{data.dig(:period, :since)} .. #{data.dig(:period, :until)}"
272
+
273
+ @io.puts
274
+ title 'Hotspots'
275
+ @table.print(%w[path score commits additions deletions], data[:items])
276
+
277
+ @io.puts
278
+ line "Generated at: #{data[:generated_at]}"
279
+ end
280
+
281
+ def render_churn(data)
282
+ title "Churn for #{data[:repo_path]}"
283
+ line "Period: #{data.dig(:period, :since)} .. #{data.dig(:period, :until)}"
284
+
285
+ @io.puts
286
+ title 'Churn'
287
+ @table.print(%w[path churn commits], data[:items])
288
+
289
+ @io.puts
290
+ line "Generated at: #{data[:generated_at]}"
291
+ end
292
+
293
+ def render_ownership(data)
294
+ title "Ownership for #{data[:repo_path]}"
295
+ line "Period: #{data.dig(:period, :since)} .. #{data.dig(:period, :until)}"
296
+
297
+ @io.puts
298
+ title 'Ownership'
299
+ @table.print(%w[path owner owner_share authors], data[:items])
300
+
301
+ @io.puts
302
+ line "Generated at: #{data[:generated_at]}"
303
+ end
304
+
269
305
  def title(text)
270
306
  @io.puts Colors.title(text, @color, @theme)
271
307
  end
@@ -276,5 +312,6 @@ module PrettyGit
276
312
 
277
313
  # table is handled by @table
278
314
  end
315
+ # rubocop:enable Metrics/ClassLength
279
316
  end
280
317
  end
@@ -4,33 +4,36 @@ require 'csv'
4
4
 
5
5
  module PrettyGit
6
6
  module Render
7
- # Renders CSV according to specs/output_formats.md and DR-001
7
+ # Renders CSV according to docs/output_formats.md and DR-001
8
8
  class CsvRenderer
9
+ HEADERS = {
10
+ 'activity' => %w[bucket timestamp commits additions deletions],
11
+ 'authors' => %w[author author_email commits additions deletions avg_commit_size],
12
+ 'files' => %w[path commits additions deletions changes],
13
+ 'heatmap' => %w[dow hour commits],
14
+ 'hotspots' => %w[path score commits additions deletions changes],
15
+ 'churn' => %w[path churn commits additions deletions],
16
+ 'ownership' => %w[path owner owner_share authors]
17
+ }.freeze
9
18
  def initialize(io: $stdout)
10
19
  @io = io
11
20
  end
12
21
 
13
22
  def call(report, result, _filters)
14
- case report
15
- when 'activity'
16
- write_csv(%w[bucket timestamp commits additions deletions], result[:items])
17
- when 'authors'
18
- write_csv(%w[author author_email commits additions deletions avg_commit_size], result[:items])
19
- when 'files'
20
- write_csv(%w[path commits additions deletions changes], result[:items])
21
- when 'heatmap'
22
- write_csv(%w[dow hour commits], result[:items])
23
- when 'languages'
24
- metric = (result[:metric] || 'bytes').to_s
25
- headers = ['language', metric, 'percent', 'color']
26
- write_csv(headers, result[:items])
27
- else
28
- raise ArgumentError, "CSV output for report '#{report}' is not supported yet"
29
- end
23
+ headers = headers_for(report, result)
24
+ write_csv(headers, result[:items])
30
25
  end
31
26
 
32
27
  private
33
28
 
29
+ def headers_for(report, result)
30
+ return ['language', (result[:metric] || 'bytes').to_s, 'percent', 'color'] if report == 'languages'
31
+
32
+ HEADERS.fetch(report) do
33
+ raise ArgumentError, "CSV output for report '#{report}' is not supported yet"
34
+ end
35
+ end
36
+
34
37
  def write_csv(headers, rows)
35
38
  csv = CSV.generate(force_quotes: false) do |out|
36
39
  out << headers
@@ -2,37 +2,54 @@
2
2
 
3
3
  module PrettyGit
4
4
  module Render
5
- # Renders Markdown tables and sections per specs/output_formats.md
5
+ # Renders Markdown tables and sections per docs/output_formats.md
6
+ # rubocop:disable Metrics/ClassLength
6
7
  class MarkdownRenderer
8
+ TITLES = {
9
+ 'activity' => 'Activity',
10
+ 'authors' => 'Authors',
11
+ 'files' => 'Top Files',
12
+ 'heatmap' => 'Heatmap',
13
+ 'languages' => 'Languages',
14
+ 'hotspots' => 'Hotspots',
15
+ 'churn' => 'Churn',
16
+ 'ownership' => 'Ownership'
17
+ }.freeze
18
+
19
+ HEADERS = {
20
+ 'activity' => %w[bucket timestamp commits additions deletions],
21
+ 'authors' => %w[author author_email commits additions deletions avg_commit_size],
22
+ 'files' => %w[path commits additions deletions changes],
23
+ 'heatmap' => %w[dow hour commits],
24
+ 'hotspots' => %w[path score commits additions deletions changes],
25
+ 'churn' => %w[path churn commits additions deletions],
26
+ 'ownership' => %w[path owner owner_share authors]
27
+ }.freeze
7
28
  def initialize(io: $stdout)
8
29
  @io = io
9
30
  end
10
31
 
11
- # rubocop:disable Metrics/CyclomaticComplexity
12
32
  def call(report, result, _filters)
13
- case report
14
- when 'summary'
15
- render_summary(result)
16
- when 'activity'
17
- render_table('Activity', %w[bucket timestamp commits additions deletions], result[:items])
18
- when 'authors'
19
- render_table('Authors', %w[author author_email commits additions deletions avg_commit_size], result[:items])
20
- when 'files'
21
- render_table('Top Files', %w[path commits additions deletions changes], result[:items])
22
- when 'heatmap'
23
- render_table('Heatmap', %w[dow hour commits], result[:items])
24
- when 'languages'
25
- metric = (result[:metric] || 'bytes').to_s
26
- headers = ['language', metric, 'percent', 'color']
27
- render_table('Languages', headers, result[:items])
28
- else
29
- @io.puts result.inspect
30
- end
33
+ return render_summary(result) if report == 'summary'
34
+
35
+ headers = headers_for(report, result)
36
+ title = title_for(report)
37
+ rows = sort_rows(report, result[:items], result)
38
+ render_table(title, headers, rows)
31
39
  end
32
- # rubocop:enable Metrics/CyclomaticComplexity
33
40
 
34
41
  private
35
42
 
43
+ def headers_for(report, result)
44
+ return ['language', (result[:metric] || 'bytes').to_s, 'percent', 'color'] if report == 'languages'
45
+
46
+ HEADERS.fetch(report, [])
47
+ end
48
+
49
+ def title_for(report)
50
+ TITLES.fetch(report, report.to_s.capitalize)
51
+ end
52
+
36
53
  def render_summary(data)
37
54
  header_summary(data)
38
55
  print_totals(data[:totals])
@@ -67,6 +84,53 @@ module PrettyGit
67
84
  end
68
85
  @io.puts 'No data' if rows.empty?
69
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
70
133
  end
134
+ # rubocop:enable Metrics/ClassLength
71
135
  end
72
136
  end
@@ -10,15 +10,29 @@ module PrettyGit
10
10
  @io = io
11
11
  end
12
12
 
13
- def call(_report, result, _filters)
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
- root = doc.add_element('report')
17
- hash_to_xml(root, result)
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(_report, result, _filters)
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(result).to_yaml)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PrettyGit
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.4'
5
5
  end
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.2
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: []
@@ -66,9 +64,12 @@ files:
66
64
  - lib/pretty_git.rb
67
65
  - lib/pretty_git/analytics/activity.rb
68
66
  - lib/pretty_git/analytics/authors.rb
67
+ - lib/pretty_git/analytics/churn.rb
69
68
  - lib/pretty_git/analytics/files.rb
70
69
  - lib/pretty_git/analytics/heatmap.rb
70
+ - lib/pretty_git/analytics/hotspots.rb
71
71
  - lib/pretty_git/analytics/languages.rb
72
+ - lib/pretty_git/analytics/ownership.rb
72
73
  - lib/pretty_git/analytics/summary.rb
73
74
  - lib/pretty_git/app.rb
74
75
  - lib/pretty_git/cli.rb
@@ -89,9 +90,11 @@ homepage: https://github.com/MikoMikocchi/pretty-git
89
90
  licenses:
90
91
  - MIT
91
92
  metadata:
93
+ homepage_uri: https://github.com/MikoMikocchi/pretty-git
92
94
  source_code_uri: https://github.com/MikoMikocchi/pretty-git
93
95
  changelog_uri: https://github.com/MikoMikocchi/pretty-git/blob/main/CHANGELOG.md
94
96
  bug_tracker_uri: https://github.com/MikoMikocchi/pretty-git/issues
97
+ documentation_uri: https://github.com/MikoMikocchi/pretty-git#readme
95
98
  rubygems_mfa_required: 'true'
96
99
  rdoc_options: []
97
100
  require_paths: