pretty-git 0.1.1 → 0.1.3

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.
@@ -15,7 +15,7 @@ module PrettyGit
15
15
  '.sh' => 'Shell', '.bash' => 'Shell', '.zsh' => 'Shell',
16
16
  '.ps1' => 'PowerShell', '.psm1' => 'PowerShell',
17
17
  '.bat' => 'Batchfile', '.cmd' => 'Batchfile',
18
- # Intentionally excluding JSON (usually data, not source code)
18
+ '.json' => 'JSON',
19
19
  '.yml' => 'YAML', '.yaml' => 'YAML', '.toml' => 'TOML', '.ini' => 'INI', '.xml' => 'XML',
20
20
  '.html' => 'HTML', '.htm' => 'HTML', '.css' => 'CSS', '.scss' => 'SCSS', '.sass' => 'SCSS',
21
21
  '.md' => 'Markdown', '.markdown' => 'Markdown',
@@ -43,6 +43,25 @@ module PrettyGit
43
43
  'Dockerfile' => 'Dockerfile'
44
44
  }.freeze
45
45
 
46
+ # Language → HEX color (without leading #) for exports (CSV/JSON/YAML/XML)
47
+ LANG_HEX_COLORS = {
48
+ 'Ruby' => 'cc342d',
49
+ 'JavaScript' => 'f1e05a', 'TypeScript' => '3178c6',
50
+ 'JSX' => 'f1e05a', 'TSX' => '3178c6',
51
+ 'Python' => '3572a5', 'Go' => '00add8', 'Rust' => 'dea584', 'Java' => 'b07219',
52
+ 'C' => '555555', 'C++' => 'f34b7d', 'C#' => '178600', 'Objective-C' => '438eff', 'Swift' => 'ffac45',
53
+ 'Kotlin' => 'a97bff', 'Scala' => 'c22d40', 'Groovy' => 'e69f56', 'Dart' => '00b4ab',
54
+ 'PHP' => '4f5d95', 'Perl' => '0298c3', 'R' => '198ce7', 'Lua' => '000080', 'Haskell' => '5e5086',
55
+ 'Elixir' => '6e4a7e', 'Erlang' => 'b83998',
56
+ 'Shell' => '89e051', 'PowerShell' => '012456', 'Batchfile' => 'c1f12e',
57
+ 'HTML' => 'e34c26', 'CSS' => '563d7c', 'SCSS' => 'c6538c',
58
+ 'JSON' => 'eeeeee',
59
+ 'YAML' => 'cb171e', 'TOML' => '9c4221', 'INI' => '6b7280', 'XML' => '0060ac',
60
+ 'Markdown' => '083fa1', 'Makefile' => '427819', 'Dockerfile' => '384d54',
61
+ 'SQL' => 'e38c00', 'GraphQL' => 'e10098', 'Proto' => '3b5998',
62
+ 'Svelte' => 'ff3e00', 'Vue' => '41b883'
63
+ }.freeze
64
+
46
65
  VENDOR_DIRS = %w[
47
66
  vendor node_modules .git .bundle dist build out target coverage
48
67
  .venv venv env __pycache__ .mypy_cache .pytest_cache .tox .eggs .ruff_cache
@@ -56,21 +75,25 @@ module PrettyGit
56
75
  .jar .class .dll .so .dylib
57
76
  .exe .bin .dat
58
77
  ].freeze
59
- # Computes language distribution by summing file sizes per language.
60
- # Similar to GitHub Linguist approach (bytes per language).
78
+ # Computes language distribution by bytes, files, and LOC per language.
79
+ # Default metric: bytes (similar to GitHub Linguist approach).
80
+ # rubocop:disable Metrics/ClassLength
61
81
  class Languages
62
82
  def self.call(_enum, filters)
63
83
  repo = filters.repo_path
64
84
  items = calculate(repo, include_globs: filters.paths, exclude_globs: filters.exclude_paths)
65
- total = total_bytes(items)
66
- items = add_percents(items, total)
67
- items = sort_and_limit(items, filters.limit)
85
+ metric = (filters.metric || 'bytes').to_s
86
+ totals = compute_totals(items)
87
+ items = add_percents(items, totals, metric)
88
+ items = add_colors(items)
89
+ items = sort_and_limit(items, filters.limit, metric)
68
90
 
69
- build_result(repo, items, total)
91
+ build_result(repo, items, totals, metric)
70
92
  end
71
93
 
94
+ # rubocop:disable Metrics/AbcSize
72
95
  def self.calculate(repo_path, include_globs:, exclude_globs:)
73
- by_lang = Hash.new(0)
96
+ by_lang = Hash.new { |h, k| h[k] = { bytes: 0, files: 0, loc: 0 } }
74
97
  Dir.chdir(repo_path) do
75
98
  each_source_file(include_globs, exclude_globs) do |abs_path|
76
99
  basename = File.basename(abs_path)
@@ -78,16 +101,17 @@ module PrettyGit
78
101
  lang = FILENAME_TO_LANG[basename] || EXT_TO_LANG[ext]
79
102
  next unless lang
80
103
 
81
- size = begin
82
- File.size(abs_path)
83
- rescue StandardError
84
- 0
85
- end
86
- by_lang[lang] += size
104
+ size = safe_file_size(abs_path)
105
+ lines = safe_count_lines(abs_path)
106
+ agg = by_lang[lang]
107
+ agg[:bytes] += size
108
+ agg[:files] += 1
109
+ agg[:loc] += lines
87
110
  end
88
111
  end
89
- by_lang.map { |lang, bytes| { language: lang, bytes: bytes } }
112
+ by_lang.map { |lang, h| { language: lang, bytes: h[:bytes], files: h[:files], loc: h[:loc] } }
90
113
  end
114
+ # rubocop:enable Metrics/AbcSize
91
115
 
92
116
  def self.each_source_file(include_globs, exclude_globs)
93
117
  # Build list of files under repo respecting includes/excludes
@@ -98,6 +122,20 @@ module PrettyGit
98
122
  files.each { |rel| yield File.expand_path(rel) }
99
123
  end
100
124
 
125
+ def self.safe_file_size(path)
126
+ File.size(path)
127
+ rescue StandardError
128
+ 0
129
+ end
130
+
131
+ def self.safe_count_lines(path)
132
+ count = 0
133
+ File.foreach(path) { |_l| count += 1 }
134
+ count
135
+ rescue StandardError
136
+ 0
137
+ end
138
+
101
139
  def self.filter_includes(files, globs)
102
140
  globs = Array(globs).compact
103
141
  return files if globs.empty?
@@ -123,33 +161,52 @@ module PrettyGit
123
161
  BINARY_EXTS.include?(File.extname(path).downcase)
124
162
  end
125
163
 
126
- def self.total_bytes(items)
127
- items.sum { |item| item[:bytes] }
164
+ def self.compute_totals(items)
165
+ {
166
+ bytes: items.sum { |i| i[:bytes] },
167
+ files: items.sum { |i| i[:files] },
168
+ loc: items.sum { |i| i[:loc] }
169
+ }
128
170
  end
129
171
 
130
- def self.add_percents(items, total)
172
+ def self.add_percents(items, totals, metric)
173
+ total = totals[metric.to_sym].to_f
131
174
  return items.map { |item| item.merge(percent: 0.0) } unless total.positive?
132
175
 
133
- items.map { |item| item.merge(percent: (item[:bytes] * 100.0 / total)) }
176
+ items.map do |item|
177
+ val = item[metric.to_sym].to_f
178
+ pct = (val * 100.0 / total).round(2)
179
+ item.merge(percent: pct)
180
+ end
181
+ end
182
+
183
+ def self.add_colors(items)
184
+ items.map do |item|
185
+ color = LANG_HEX_COLORS[item[:language]]
186
+ item.merge(color: color)
187
+ end
134
188
  end
135
189
 
136
- def self.sort_and_limit(items, limit)
137
- sorted = items.sort_by { |item| [-item[:percent], item[:language]] }
190
+ def self.sort_and_limit(items, limit, metric)
191
+ key = metric.to_sym
192
+ sorted = items.sort_by { |item| [-item[key], item[:language]] }
138
193
  lim = limit.to_i
139
194
  return sorted if lim <= 0
140
195
 
141
196
  sorted.first(lim)
142
197
  end
143
198
 
144
- def self.build_result(repo, items, total)
199
+ def self.build_result(repo, items, totals, metric)
145
200
  {
146
201
  report: 'languages',
147
202
  repo_path: repo,
203
+ metric: metric,
148
204
  generated_at: Time.now.utc.iso8601,
149
- totals: { bytes: total, languages: items.size },
205
+ totals: totals.merge(languages: items.size),
150
206
  items: items
151
207
  }
152
208
  end
153
209
  end
210
+ # rubocop:enable Metrics/ClassLength
154
211
  end
155
212
  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,7 +9,7 @@ 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].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
15
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@@ -7,9 +7,11 @@ require_relative 'app'
7
7
  module PrettyGit
8
8
  # Helpers extracted from `PrettyGit::CLI` to keep the CLI class small
9
9
  # and RuboCop-compliant. Provides parser configuration and execution utilities.
10
+ # rubocop:disable Metrics/ModuleLength
10
11
  module CLIHelpers
11
- REPORTS = %w[summary activity authors files heatmap languages].freeze
12
+ REPORTS = %w[summary activity authors files heatmap languages hotspots churn ownership].freeze
12
13
  FORMATS = %w[console json csv md yaml xml].freeze
14
+ METRICS = %w[bytes files loc].freeze
13
15
 
14
16
  module_function
15
17
 
@@ -19,6 +21,7 @@ module PrettyGit
19
21
  add_time_author_options(opts, options)
20
22
  add_path_limit_options(opts, options)
21
23
  add_format_output_options(opts, options)
24
+ add_metric_options(opts, options)
22
25
  add_misc_options(opts, options)
23
26
  end
24
27
 
@@ -48,6 +51,12 @@ module PrettyGit
48
51
  opts.on('--theme NAME', 'console color theme: basic|bright|mono') { |val| options[:theme] = val }
49
52
  end
50
53
 
54
+ def add_metric_options(opts, options)
55
+ opts.on('--metric NAME', 'languages metric: bytes|files|loc (default: bytes)') do |val|
56
+ options[:metric] = val
57
+ end
58
+ end
59
+
51
60
  def add_misc_options(opts, options)
52
61
  opts.on('--version', 'Show version') { options[:_version] = true }
53
62
  opts.on('--help', 'Show help') { options[:_help] = true }
@@ -66,7 +75,7 @@ module PrettyGit
66
75
  code = handle_version_help(options, parser, out)
67
76
  return code unless code.nil?
68
77
 
69
- return nil if valid_report?(options[:report]) && valid_theme?(options[:theme])
78
+ return nil if valid_report?(options[:report]) && valid_theme?(options[:theme]) && valid_metric?(options[:metric])
70
79
 
71
80
  print_validation_errors(options, err)
72
81
  1
@@ -87,15 +96,20 @@ module PrettyGit
87
96
  def valid_report?(report) = REPORTS.include?(report)
88
97
  def valid_theme?(theme) = %w[basic bright mono].include?(theme)
89
98
 
99
+ def valid_metric?(metric)
100
+ metric.nil? || METRICS.include?(metric)
101
+ end
102
+
90
103
  def print_validation_errors(options, err)
91
104
  supported = REPORTS.join(', ')
92
105
  unless valid_report?(options[:report])
93
106
  err.puts "Unknown report: #{options[:report]}."
94
107
  err.puts "Supported: #{supported}"
95
108
  end
96
- return if valid_theme?(options[:theme])
109
+ err.puts "Unknown theme: #{options[:theme]}. Supported: basic, bright, mono" unless valid_theme?(options[:theme])
110
+ return if valid_metric?(options[:metric])
97
111
 
98
- err.puts "Unknown theme: #{options[:theme]}. Supported: basic, bright, mono"
112
+ err.puts "Unknown metric: #{options[:metric]}. Supported: #{METRICS.join(', ')}"
99
113
  end
100
114
 
101
115
  def build_filters(options)
@@ -109,6 +123,7 @@ module PrettyGit
109
123
  paths: options[:paths],
110
124
  exclude_paths: options[:exclude_paths],
111
125
  time_bucket: options[:time_bucket],
126
+ metric: options[:metric],
112
127
  limit: options[:limit],
113
128
  format: options[:format],
114
129
  out: options[:out],
@@ -127,4 +142,5 @@ module PrettyGit
127
142
  PrettyGit::App.new.run(report, filters, out: out, err: err)
128
143
  end
129
144
  end
145
+ # rubocop:enable Metrics/ModuleLength
130
146
  end
@@ -13,6 +13,7 @@ module PrettyGit
13
13
  :paths,
14
14
  :exclude_paths,
15
15
  :time_bucket,
16
+ :metric,
16
17
  :limit,
17
18
  :format,
18
19
  :out,
@@ -16,7 +16,7 @@ module PrettyGit
16
16
  end
17
17
 
18
18
  # Returns Enumerator of PrettyGit::Types::Commit
19
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
19
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
20
20
  def each_commit
21
21
  Enumerator.new do |yld|
22
22
  cmd = build_git_command
@@ -24,17 +24,15 @@ module PrettyGit
24
24
  current = nil
25
25
  stdout.each_line do |line|
26
26
  line = line.chomp
27
- if record_separator?(line)
27
+ # Try to start a new commit from header on any line
28
+ header = start_commit_from_header(line)
29
+ if header
30
+ # emit previous commit if any
28
31
  emit_current(yld, current)
29
- current = nil
32
+ current = header
30
33
  next
31
34
  end
32
35
 
33
- if current.nil?
34
- current = start_commit_from_header(line)
35
- next if current
36
- end
37
-
38
36
  next if line.empty?
39
37
 
40
38
  append_numstat_line(current, line)
@@ -50,7 +48,7 @@ module PrettyGit
50
48
  end
51
49
  end
52
50
  end
53
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
51
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
54
52
 
55
53
  private
56
54
 
@@ -84,7 +82,7 @@ module PrettyGit
84
82
  author_name: author_name,
85
83
  author_email: author_email,
86
84
  authored_at: Time.parse(authored_at).utc.iso8601,
87
- message: subject,
85
+ message: subject.delete(SEP_RECORD),
88
86
  files: []
89
87
  }
90
88
  end
@@ -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
@@ -6,29 +6,34 @@ module PrettyGit
6
6
  module Render
7
7
  # Renders CSV according to specs/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
- write_csv(%w[language bytes percent], result[:items])
25
- else
26
- raise ArgumentError, "CSV output for report '#{report}' is not supported yet"
27
- end
23
+ headers = headers_for(report, result)
24
+ write_csv(headers, result[:items])
28
25
  end
29
26
 
30
27
  private
31
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
+
32
37
  def write_csv(headers, rows)
33
38
  csv = CSV.generate(force_quotes: false) do |out|
34
39
  out << headers
@@ -26,9 +26,11 @@ module PrettyGit
26
26
  def render(io, table, data, color: true)
27
27
  title(io, data, color)
28
28
  io.puts
29
- table_rows = rows(data[:items])
29
+ metric = (data[:metric] || 'bytes').to_s
30
+ table_rows = rows(data[:items], metric)
30
31
  colorizer = ->(row) { LANG_ANSI_COLOR_CODES[row[:language]] }
31
- table.print(%w[language bytes percent], table_rows, highlight_max: false, first_col_colorizer: colorizer)
32
+ headers = ['language', metric, 'percent']
33
+ table.print(headers, table_rows, highlight_max: false, first_col_colorizer: colorizer)
32
34
  io.puts
33
35
  io.puts "Generated at: #{data[:generated_at]}"
34
36
  end
@@ -37,9 +39,10 @@ module PrettyGit
37
39
  io.puts Colors.title("Languages for #{data[:repo_path]}", color)
38
40
  end
39
41
 
40
- def rows(items)
42
+ def rows(items, metric)
43
+ key = metric.to_sym
41
44
  items.map do |item|
42
- { language: item[:language], bytes: item[:bytes], percent: format('%.1f', item[:percent]) }
45
+ { language: item[:language], key => item[key], percent: format('%.2f', item[:percent]) }
43
46
  end
44
47
  end
45
48
  end