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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -0
- data/README.md +125 -40
- data/README.ru.md +511 -0
- data/lib/pretty_git/analytics/churn.rb +75 -0
- data/lib/pretty_git/analytics/hotspots.rb +78 -0
- data/lib/pretty_git/analytics/languages.rb +80 -23
- data/lib/pretty_git/analytics/ownership.rb +90 -0
- data/lib/pretty_git/app.rb +18 -16
- data/lib/pretty_git/cli.rb +1 -1
- data/lib/pretty_git/cli_helpers.rb +20 -4
- data/lib/pretty_git/filters.rb +1 -0
- data/lib/pretty_git/git/provider.rb +8 -10
- data/lib/pretty_git/render/console_renderer.rb +53 -16
- data/lib/pretty_git/render/csv_renderer.rb +19 -14
- data/lib/pretty_git/render/languages_section.rb +7 -4
- data/lib/pretty_git/render/markdown_renderer.rb +35 -16
- data/lib/pretty_git/version.rb +1 -1
- metadata +6 -1
@@ -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
|
-
|
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
|
60
|
-
#
|
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
|
-
|
66
|
-
|
67
|
-
items =
|
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,
|
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
|
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 =
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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,
|
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.
|
127
|
-
|
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,
|
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
|
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
|
-
|
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,
|
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:
|
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
|
data/lib/pretty_git/app.rb
CHANGED
@@ -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
|
-
|
66
|
-
|
67
|
-
Analytics::
|
68
|
-
|
69
|
-
Analytics::
|
70
|
-
|
71
|
-
Analytics::
|
72
|
-
|
73
|
-
Analytics::
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
data/lib/pretty_git/cli.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
data/lib/pretty_git/filters.rb
CHANGED
@@ -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/
|
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
|
-
|
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 =
|
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/
|
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
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
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],
|
45
|
+
{ language: item[:language], key => item[key], percent: format('%.2f', item[:percent]) }
|
43
46
|
end
|
44
47
|
end
|
45
48
|
end
|