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
data/lib/pretty_git/app.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'open3'
|
3
4
|
require_relative 'git/provider'
|
4
5
|
require_relative 'analytics/summary'
|
5
6
|
require_relative 'analytics/activity'
|
@@ -37,7 +38,9 @@ module PrettyGit
|
|
37
38
|
private
|
38
39
|
|
39
40
|
def ensure_repo!(path)
|
40
|
-
|
41
|
+
# Use git to reliably detect work-trees/worktrees/bare repos
|
42
|
+
stdout, _stderr, status = Open3.capture3('git', 'rev-parse', '--is-inside-work-tree', chdir: path)
|
43
|
+
return if status.success? && stdout.to_s.strip == 'true'
|
41
44
|
|
42
45
|
raise ArgumentError, "Not a git repository: #{path}"
|
43
46
|
end
|
@@ -47,21 +50,25 @@ module PrettyGit
|
|
47
50
|
end
|
48
51
|
|
49
52
|
def renderer_for(filters, io)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
when 'md'
|
57
|
-
Render::MarkdownRenderer.new(io: io)
|
58
|
-
when 'yaml'
|
59
|
-
Render::YamlRenderer.new(io: io)
|
60
|
-
when 'xml'
|
61
|
-
Render::XmlRenderer.new(io: io)
|
62
|
-
else
|
63
|
-
Render::JsonRenderer.new(io: io)
|
53
|
+
if filters.format == 'console'
|
54
|
+
return Render::ConsoleRenderer.new(
|
55
|
+
io: io,
|
56
|
+
color: !filters.no_color && filters.theme != 'mono',
|
57
|
+
theme: filters.theme
|
58
|
+
)
|
64
59
|
end
|
60
|
+
|
61
|
+
dispatch = {
|
62
|
+
'csv' => Render::CsvRenderer,
|
63
|
+
'md' => Render::MarkdownRenderer,
|
64
|
+
'yaml' => Render::YamlRenderer,
|
65
|
+
'xml' => Render::XmlRenderer,
|
66
|
+
'json' => Render::JsonRenderer
|
67
|
+
}
|
68
|
+
klass = dispatch[filters.format]
|
69
|
+
raise ArgumentError, "Unknown format: #{filters.format}" unless klass
|
70
|
+
|
71
|
+
klass.new(io: io)
|
65
72
|
end
|
66
73
|
|
67
74
|
def analytics_for(report, enum, filters)
|
data/lib/pretty_git/cli.rb
CHANGED
@@ -9,10 +9,7 @@ require_relative 'cli_helpers'
|
|
9
9
|
module PrettyGit
|
10
10
|
# Command-line interface entry point.
|
11
11
|
class CLI
|
12
|
-
|
13
|
-
SUPPORTED_FORMATS = %w[console json csv md yaml xml].freeze
|
14
|
-
|
15
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
12
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
16
13
|
def self.run(argv = ARGV, out: $stdout, err: $stderr)
|
17
14
|
options = {
|
18
15
|
report: 'summary',
|
@@ -22,12 +19,13 @@ module PrettyGit
|
|
22
19
|
exclude_authors: [],
|
23
20
|
paths: [],
|
24
21
|
exclude_paths: [],
|
25
|
-
time_bucket:
|
22
|
+
time_bucket: nil,
|
26
23
|
limit: 10,
|
27
24
|
format: 'console',
|
28
25
|
out: nil,
|
29
26
|
no_color: false,
|
30
27
|
theme: 'basic',
|
28
|
+
_verbose: false,
|
31
29
|
_version: false,
|
32
30
|
_help: false
|
33
31
|
}
|
@@ -46,6 +44,9 @@ module PrettyGit
|
|
46
44
|
return 1
|
47
45
|
end
|
48
46
|
|
47
|
+
# REPO positional arg (after REPORT), if still present and not an option
|
48
|
+
options[:repo] = argv.shift if argv[0] && argv[0] !~ /^-/
|
49
|
+
|
49
50
|
exit_code = CLIHelpers.validate_and_maybe_exit(options, parser, out, err)
|
50
51
|
return exit_code if exit_code
|
51
52
|
|
@@ -58,6 +59,6 @@ module PrettyGit
|
|
58
59
|
err.puts e.message
|
59
60
|
2
|
60
61
|
end
|
61
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
62
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
62
63
|
end
|
63
64
|
end
|
@@ -1,29 +1,71 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'optparse'
|
4
|
+
require 'fileutils'
|
4
5
|
require_relative 'filters'
|
5
6
|
require_relative 'app'
|
7
|
+
require_relative 'constants'
|
6
8
|
|
7
9
|
module PrettyGit
|
8
10
|
# Helpers extracted from `PrettyGit::CLI` to keep the CLI class small
|
9
11
|
# and RuboCop-compliant. Provides parser configuration and execution utilities.
|
10
12
|
# rubocop:disable Metrics/ModuleLength
|
11
13
|
module CLIHelpers
|
12
|
-
REPORTS =
|
13
|
-
FORMATS =
|
14
|
-
METRICS =
|
14
|
+
REPORTS = PrettyGit::Constants::REPORTS
|
15
|
+
FORMATS = PrettyGit::Constants::FORMATS
|
16
|
+
METRICS = PrettyGit::Constants::METRICS
|
17
|
+
THEMES = PrettyGit::Constants::THEMES
|
15
18
|
|
16
19
|
module_function
|
17
20
|
|
21
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
18
22
|
def configure_parser(opts, options)
|
19
|
-
opts.banner =
|
23
|
+
opts.banner = <<~BANNER
|
24
|
+
Usage: pretty-git [REPORT] [REPO] [options]
|
25
|
+
|
26
|
+
Reports: #{REPORTS.join(', ')}
|
27
|
+
Formats: #{FORMATS.join(', ')}
|
28
|
+
Themes: #{THEMES.join(', ')}
|
29
|
+
BANNER
|
30
|
+
opts.separator('')
|
31
|
+
opts.separator('Repository and branch:')
|
20
32
|
add_repo_options(opts, options)
|
33
|
+
opts.separator('')
|
34
|
+
opts.separator('Time, authors, and bucketing:')
|
21
35
|
add_time_author_options(opts, options)
|
36
|
+
opts.separator('')
|
37
|
+
opts.separator('Paths and limits:')
|
22
38
|
add_path_limit_options(opts, options)
|
39
|
+
opts.separator('')
|
40
|
+
opts.separator('Format and output:')
|
23
41
|
add_format_output_options(opts, options)
|
42
|
+
opts.separator('')
|
43
|
+
opts.separator('Metrics (languages report only):')
|
24
44
|
add_metric_options(opts, options)
|
45
|
+
opts.separator('')
|
46
|
+
opts.separator('Other:')
|
25
47
|
add_misc_options(opts, options)
|
48
|
+
opts.separator('')
|
49
|
+
opts.separator('Examples:')
|
50
|
+
opts.separator(' # Summary in JSON to stdout')
|
51
|
+
opts.separator(' $ pretty-git summary . --format json')
|
52
|
+
opts.separator('')
|
53
|
+
opts.separator(' # Authors since a date with a limit')
|
54
|
+
opts.separator(' $ pretty-git authors ~/repo --since 2025-01-01 --limit 20')
|
55
|
+
opts.separator('')
|
56
|
+
opts.separator(' # Files with includes/excludes (globs)')
|
57
|
+
opts.separator(' $ pretty-git files . --path app/**/*.rb --exclude-path spec/**')
|
58
|
+
opts.separator('')
|
59
|
+
opts.separator(' # Activity bucketed by week to Markdown file')
|
60
|
+
opts.separator(' $ pretty-git activity . --time-bucket week --format md --out out/report.md')
|
61
|
+
opts.separator('')
|
62
|
+
opts.separator(' # Languages by LOC to CSV (redirect to file)')
|
63
|
+
opts.separator(' $ pretty-git languages . --metric loc --format csv > langs.csv')
|
64
|
+
opts.separator('')
|
65
|
+
opts.separator(' # Hotspots on main with a higher limit to YAML')
|
66
|
+
opts.separator(' $ pretty-git hotspots . --branch main --limit 50 --format yaml')
|
26
67
|
end
|
68
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
27
69
|
|
28
70
|
def add_repo_options(opts, options)
|
29
71
|
opts.on('--repo PATH', 'Path to git repository (default: .)') { |val| options[:repo] = val }
|
@@ -47,8 +89,14 @@ module PrettyGit
|
|
47
89
|
def add_format_output_options(opts, options)
|
48
90
|
opts.on('--format FMT', 'console|json|csv|md|yaml|xml') { |val| options[:format] = val }
|
49
91
|
opts.on('--out FILE', 'Output file path') { |val| options[:out] = val }
|
50
|
-
opts.on('--no-color', 'Disable colors in console output')
|
51
|
-
|
92
|
+
opts.on('--no-color', 'Disable colors in console output') do
|
93
|
+
options[:no_color] = true
|
94
|
+
options[:_no_color_provided] = true
|
95
|
+
end
|
96
|
+
opts.on('--theme NAME', 'console color theme: basic|bright|mono') do |val|
|
97
|
+
options[:theme] = val
|
98
|
+
options[:_theme_provided] = true
|
99
|
+
end
|
52
100
|
end
|
53
101
|
|
54
102
|
def add_metric_options(opts, options)
|
@@ -60,6 +108,7 @@ module PrettyGit
|
|
60
108
|
def add_misc_options(opts, options)
|
61
109
|
opts.on('--version', 'Show version') { options[:_version] = true }
|
62
110
|
opts.on('--help', 'Show help') { options[:_help] = true }
|
111
|
+
opts.on('--verbose', 'Verbose output (debug)') { options[:_verbose] = true }
|
63
112
|
end
|
64
113
|
|
65
114
|
def parse_limit(str)
|
@@ -75,7 +124,14 @@ module PrettyGit
|
|
75
124
|
code = handle_version_help(options, parser, out)
|
76
125
|
return code unless code.nil?
|
77
126
|
|
78
|
-
|
127
|
+
base_ok = valid_base?(options)
|
128
|
+
conflicts_ok = validate_conflicts(options, err)
|
129
|
+
if base_ok && conflicts_ok
|
130
|
+
early = warn_ignores(options, err)
|
131
|
+
return 0 if early == :early_exit
|
132
|
+
|
133
|
+
return nil
|
134
|
+
end
|
79
135
|
|
80
136
|
print_validation_errors(options, err)
|
81
137
|
1
|
@@ -94,30 +150,102 @@ module PrettyGit
|
|
94
150
|
end
|
95
151
|
|
96
152
|
def valid_report?(report) = REPORTS.include?(report)
|
97
|
-
def valid_theme?(theme) =
|
153
|
+
def valid_theme?(theme) = THEMES.include?(theme)
|
154
|
+
def valid_format?(fmt) = FORMATS.include?(fmt)
|
155
|
+
|
156
|
+
def valid_base?(options)
|
157
|
+
valid_report?(options[:report]) &&
|
158
|
+
valid_theme?(options[:theme]) &&
|
159
|
+
valid_metric?(options[:metric]) &&
|
160
|
+
valid_format?(options[:format])
|
161
|
+
end
|
98
162
|
|
99
163
|
def valid_metric?(metric)
|
100
164
|
metric.nil? || METRICS.include?(metric)
|
101
165
|
end
|
102
166
|
|
103
167
|
def print_validation_errors(options, err)
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
168
|
+
print_report_error(options, err)
|
169
|
+
print_theme_error(options, err)
|
170
|
+
print_format_error(options, err)
|
171
|
+
print_metric_error(options, err)
|
172
|
+
end
|
173
|
+
|
174
|
+
def print_report_error(options, err)
|
175
|
+
return if valid_report?(options[:report])
|
176
|
+
|
177
|
+
err.puts "Unknown report: #{options[:report]}."
|
178
|
+
err.puts "Supported: #{REPORTS.join(', ')}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def print_theme_error(options, err)
|
182
|
+
return if valid_theme?(options[:theme])
|
183
|
+
|
184
|
+
err.puts "Unknown theme: #{options[:theme]}. Supported: #{THEMES.join(', ')}"
|
185
|
+
end
|
186
|
+
|
187
|
+
def print_format_error(options, err)
|
188
|
+
return if valid_format?(options[:format])
|
189
|
+
|
190
|
+
err.puts "Unknown format: #{options[:format]}. Supported: #{FORMATS.join(', ')}"
|
191
|
+
end
|
192
|
+
|
193
|
+
def print_metric_error(options, err)
|
110
194
|
return if valid_metric?(options[:metric])
|
111
195
|
|
112
196
|
err.puts "Unknown metric: #{options[:metric]}. Supported: #{METRICS.join(', ')}"
|
113
197
|
end
|
114
198
|
|
199
|
+
# Returns true when flags are consistent; otherwise prints errors and returns false
|
200
|
+
def validate_conflicts(options, err)
|
201
|
+
ok = true
|
202
|
+
if options[:metric] && options[:report] != 'languages'
|
203
|
+
err.puts "--metric is only supported for 'languages' report"
|
204
|
+
ok = false
|
205
|
+
end
|
206
|
+
# time_bucket is accepted by multiple reports historically; do not enforce here.
|
207
|
+
ok
|
208
|
+
end
|
209
|
+
|
210
|
+
# Print non-fatal warnings for flags that won't have effect with current options
|
211
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
212
|
+
def warn_ignores(options, err)
|
213
|
+
return unless err
|
214
|
+
|
215
|
+
fmt = options[:format]
|
216
|
+
return unless fmt && fmt != 'console'
|
217
|
+
|
218
|
+
err.puts "Warning: --theme has no effect when --format=#{fmt}" if options[:theme]
|
219
|
+
err.puts "Warning: --no-color has no effect when --format=#{fmt}" if options[:no_color]
|
220
|
+
|
221
|
+
# Exit early only if user explicitly provided these console-only flags
|
222
|
+
# AND no other significant options that warrant running the app were provided.
|
223
|
+
return unless options[:_theme_provided] || options[:_no_color_provided]
|
224
|
+
|
225
|
+
significant = significant_options?(options)
|
226
|
+
:early_exit unless significant
|
227
|
+
end
|
228
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
229
|
+
|
230
|
+
def significant_options?(options)
|
231
|
+
return true if options[:out]
|
232
|
+
return true if options[:metric]
|
233
|
+
return true if options[:time_bucket]
|
234
|
+
return true if options[:limit] && options[:limit] != 10
|
235
|
+
|
236
|
+
any_collection_present?(options, %i[branches authors exclude_authors paths exclude_paths])
|
237
|
+
end
|
238
|
+
|
239
|
+
def any_collection_present?(options, keys)
|
240
|
+
keys.any? { |k| options[k]&.any? }
|
241
|
+
end
|
242
|
+
|
115
243
|
def build_filters(options)
|
116
244
|
Filters.new(
|
117
245
|
repo_path: options[:repo],
|
118
246
|
branches: options[:branches],
|
119
247
|
since: options[:since],
|
120
|
-
|
248
|
+
until_at: options[:until],
|
121
249
|
authors: options[:authors],
|
122
250
|
exclude_authors: options[:exclude_authors],
|
123
251
|
paths: options[:paths],
|
@@ -128,14 +256,25 @@ module PrettyGit
|
|
128
256
|
format: options[:format],
|
129
257
|
out: options[:out],
|
130
258
|
no_color: options[:no_color],
|
131
|
-
theme: options[:theme]
|
259
|
+
theme: options[:theme],
|
260
|
+
verbose: options[:_verbose]
|
132
261
|
)
|
133
262
|
end
|
134
263
|
|
135
264
|
def execute(report, filters, options, out, err)
|
136
265
|
if options[:out]
|
137
|
-
|
138
|
-
|
266
|
+
begin
|
267
|
+
dir = File.dirname(options[:out])
|
268
|
+
FileUtils.mkdir_p(dir) unless dir.nil? || dir == '.'
|
269
|
+
File.open(options[:out], 'w') do |f|
|
270
|
+
return PrettyGit::App.new.run(report, filters, out: f, err: err)
|
271
|
+
end
|
272
|
+
rescue Errno::EACCES
|
273
|
+
err.puts "Cannot write to: #{options[:out]} (permission denied)"
|
274
|
+
return 2
|
275
|
+
rescue Errno::ENOENT
|
276
|
+
err.puts "Cannot write to: #{options[:out]} (directory not found)"
|
277
|
+
return 2
|
139
278
|
end
|
140
279
|
end
|
141
280
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrettyGit
|
4
|
+
module Constants
|
5
|
+
REPORTS = %w[
|
6
|
+
summary activity authors files heatmap languages hotspots churn ownership
|
7
|
+
].freeze
|
8
|
+
|
9
|
+
FORMATS = %w[console json csv md yaml xml].freeze
|
10
|
+
|
11
|
+
METRICS = %w[bytes files loc].freeze
|
12
|
+
|
13
|
+
THEMES = %w[basic bright mono].freeze
|
14
|
+
end
|
15
|
+
end
|
data/lib/pretty_git/filters.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'time'
|
4
|
+
require_relative 'utils/time_utils'
|
4
5
|
|
5
6
|
module PrettyGit
|
6
7
|
Filters = Struct.new(
|
7
8
|
:repo_path,
|
8
9
|
:branches,
|
9
10
|
:since,
|
10
|
-
:
|
11
|
+
:until_at,
|
11
12
|
:authors,
|
12
13
|
:exclude_authors,
|
13
14
|
:paths,
|
@@ -19,26 +20,53 @@ module PrettyGit
|
|
19
20
|
:out,
|
20
21
|
:no_color,
|
21
22
|
:theme,
|
23
|
+
:verbose,
|
22
24
|
keyword_init: true
|
23
25
|
) do
|
24
|
-
|
25
|
-
|
26
|
+
# Backward-compat: allow initializing with `until:` keyword by remapping to :until_at
|
27
|
+
# Preserve Struct keyword_init behavior by overriding initialize instead of .new
|
28
|
+
def initialize(*args, **kwargs)
|
29
|
+
# Accept a single Hash positional argument for backward compatibility
|
30
|
+
kwargs = args.first if (kwargs.nil? || kwargs.empty?) && args.length == 1 && args.first.is_a?(Hash)
|
31
|
+
kwargs ||= {}
|
32
|
+
|
33
|
+
if kwargs.key?(:until)
|
34
|
+
Kernel.warn('[pretty-git] DEPRECATION: Filters initialized with :until. Use :until_at instead.')
|
35
|
+
kwargs = kwargs.dup
|
36
|
+
kwargs[:until_at] = kwargs.delete(:until)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Keyword-init struct: prefer keyword form consistently to keep initialize simple
|
40
|
+
super(**kwargs)
|
26
41
|
end
|
27
42
|
|
28
|
-
|
29
|
-
|
43
|
+
# Backward-compat: support filters.until and filters.until=
|
44
|
+
def until
|
45
|
+
self[:until_at]
|
46
|
+
end
|
47
|
+
|
48
|
+
def until=(val)
|
49
|
+
self[:until_at] = val
|
30
50
|
end
|
31
51
|
|
32
|
-
|
52
|
+
# Backward-compat for hash-style access used in older specs
|
53
|
+
def [](key)
|
54
|
+
key = :until_at if key == :until
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
def []=(key, value)
|
59
|
+
key = :until_at if key == :until
|
60
|
+
super
|
61
|
+
end
|
33
62
|
|
34
|
-
def
|
35
|
-
|
63
|
+
def since_iso8601
|
64
|
+
PrettyGit::Utils::TimeUtils.to_utc_iso8601(since)
|
65
|
+
end
|
36
66
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
rescue ArgumentError
|
41
|
-
raise ArgumentError, "Invalid datetime: #{val} (expected ISO8601 or YYYY-MM-DD)"
|
67
|
+
# Keep method name for backwards compatibility across the codebase
|
68
|
+
def until_iso8601
|
69
|
+
PrettyGit::Utils::TimeUtils.to_utc_iso8601(self[:until_at])
|
42
70
|
end
|
43
71
|
end
|
44
72
|
end
|
@@ -2,11 +2,15 @@
|
|
2
2
|
|
3
3
|
require 'open3'
|
4
4
|
require 'time'
|
5
|
+
require 'json'
|
5
6
|
require_relative '../types'
|
7
|
+
require_relative '../logger'
|
8
|
+
require_relative '../utils/path_utils'
|
6
9
|
|
7
10
|
module PrettyGit
|
8
11
|
module Git
|
9
12
|
# Streams commits from git CLI using `git log --numstat` and parses them.
|
13
|
+
# rubocop:disable Metrics/ClassLength
|
10
14
|
class Provider
|
11
15
|
SEP_RECORD = "\x1E" # record separator
|
12
16
|
SEP_FIELD = "\x1F" # unit separator
|
@@ -16,10 +20,18 @@ module PrettyGit
|
|
16
20
|
end
|
17
21
|
|
18
22
|
# Returns Enumerator of PrettyGit::Types::Commit
|
19
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
23
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
20
24
|
def each_commit
|
21
25
|
Enumerator.new do |yld|
|
26
|
+
prof = ENV['PG_PROF'] == '1'
|
27
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
28
|
+
headers = 0
|
29
|
+
numstat_lines = 0
|
22
30
|
cmd = build_git_command
|
31
|
+
PrettyGit::Logger.verbose(
|
32
|
+
"[pretty-git] git cmd: #{cmd.join(' ')} (cwd=#{@filters.repo_path})",
|
33
|
+
@filters.verbose
|
34
|
+
)
|
23
35
|
Open3.popen3(*cmd, chdir: @filters.repo_path) do |_stdin, stdout, stderr, wait_thr|
|
24
36
|
current = nil
|
25
37
|
stdout.each_line do |line|
|
@@ -27,6 +39,7 @@ module PrettyGit
|
|
27
39
|
# Try to start a new commit from header on any line
|
28
40
|
header = start_commit_from_header(line)
|
29
41
|
if header
|
42
|
+
headers += 1 if prof
|
30
43
|
# emit previous commit if any
|
31
44
|
emit_current(yld, current)
|
32
45
|
current = header
|
@@ -35,6 +48,7 @@ module PrettyGit
|
|
35
48
|
|
36
49
|
next if line.empty?
|
37
50
|
|
51
|
+
numstat_lines += 1 if prof
|
38
52
|
append_numstat_line(current, line)
|
39
53
|
end
|
40
54
|
|
@@ -46,9 +60,26 @@ module PrettyGit
|
|
46
60
|
raise StandardError, (err && !err.empty? ? err : "git log failed with status #{status.exitstatus}")
|
47
61
|
end
|
48
62
|
end
|
63
|
+
|
64
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
65
|
+
if prof
|
66
|
+
# Emit a compact profile to stderr
|
67
|
+
elapsed = (t1 - t0)
|
68
|
+
warn format(
|
69
|
+
'[pg_prof] git_provider: time=%<sec>.3fs headers=%<headers>d numstat_lines=%<num>d',
|
70
|
+
{ sec: elapsed, headers: headers, num: numstat_lines }
|
71
|
+
)
|
72
|
+
summary = {
|
73
|
+
component: 'git_provider',
|
74
|
+
time_sec: elapsed,
|
75
|
+
headers: headers,
|
76
|
+
numstat_lines: numstat_lines
|
77
|
+
}
|
78
|
+
warn("[pg_prof_json] #{summary.to_json}")
|
79
|
+
end
|
49
80
|
end
|
50
81
|
end
|
51
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
82
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
52
83
|
|
53
84
|
private
|
54
85
|
|
@@ -57,7 +88,7 @@ module PrettyGit
|
|
57
88
|
|
58
89
|
additions = current[:files].sum(&:additions)
|
59
90
|
deletions = current[:files].sum(&:deletions)
|
60
|
-
|
91
|
+
commit = Types::Commit.new(
|
61
92
|
sha: current[:sha],
|
62
93
|
author_name: current[:author_name],
|
63
94
|
author_email: current[:author_email],
|
@@ -67,10 +98,9 @@ module PrettyGit
|
|
67
98
|
deletions: deletions,
|
68
99
|
files: current[:files]
|
69
100
|
)
|
70
|
-
|
101
|
+
return if exclude_author?(commit.author_name, commit.author_email)
|
71
102
|
|
72
|
-
|
73
|
-
line == SEP_RECORD
|
103
|
+
yld << commit
|
74
104
|
end
|
75
105
|
|
76
106
|
def start_commit_from_header(line)
|
@@ -117,16 +147,43 @@ module PrettyGit
|
|
117
147
|
|
118
148
|
def add_author_and_branch_filters(args)
|
119
149
|
@filters.authors&.each { |a| args << "--author=#{a}" }
|
120
|
-
|
150
|
+
# Treat branches as explicit revisions to include
|
151
|
+
@filters.branches&.each { |b| args << b }
|
121
152
|
end
|
122
153
|
|
123
154
|
def add_path_filters(args)
|
124
|
-
path_args =
|
125
|
-
|
155
|
+
path_args = PrettyGit::Utils::PathUtils.normalize_globs(@filters.paths)
|
156
|
+
exclude_args = PrettyGit::Utils::PathUtils.normalize_globs(@filters.exclude_paths)
|
157
|
+
|
158
|
+
# Nothing to filter by
|
159
|
+
return if path_args.empty? && exclude_args.empty?
|
126
160
|
|
127
161
|
args << '--'
|
128
|
-
|
162
|
+
|
163
|
+
# If only excludes provided, include all paths first
|
164
|
+
args << '.' if path_args.empty? && !exclude_args.empty?
|
165
|
+
|
166
|
+
# Include patterns (normalized)
|
167
|
+
args.concat(path_args) unless path_args.empty?
|
168
|
+
|
169
|
+
# Exclude patterns via git pathspec magic with glob (normalized)
|
170
|
+
exclude_args.each do |pat|
|
171
|
+
args << ":(exclude,glob)#{pat}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
176
|
+
def exclude_author?(name, email)
|
177
|
+
patterns = Array(@filters.exclude_authors).compact
|
178
|
+
return false if patterns.empty?
|
179
|
+
|
180
|
+
patterns.any? do |pat|
|
181
|
+
pn = pat.to_s
|
182
|
+
name&.downcase&.include?(pn.downcase) || email&.downcase&.include?(pn.downcase)
|
183
|
+
end
|
129
184
|
end
|
185
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
130
186
|
end
|
131
187
|
end
|
132
188
|
end
|
189
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrettyGit
|
4
|
+
# Minimal centralized logger for PrettyGit.
|
5
|
+
# Writes to stderr by default; can accept custom IO via keyword.
|
6
|
+
module Logger
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def warn(msg, err: $stderr)
|
10
|
+
err.puts(msg)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Convenience: only emit when enabled is truthy
|
14
|
+
def verbose(msg, enabled, err: $stderr)
|
15
|
+
return unless enabled
|
16
|
+
|
17
|
+
warn(msg, err: err)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -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],
|