ruby-maat 1.2.0 → 1.3.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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +78 -0
- data/lib/ruby_maat/app.rb +25 -0
- data/lib/ruby_maat/change_record.rb +13 -3
- data/lib/ruby_maat/cli.rb +6 -0
- data/lib/ruby_maat/generators/base_generator.rb +22 -0
- data/lib/ruby_maat/generators/git_generator.rb +35 -4
- data/lib/ruby_maat/groupers/merge_commit_grouper.rb +152 -0
- data/lib/ruby_maat/parsers/git2_parser.rb +32 -9
- data/lib/ruby_maat/version.rb +1 -1
- data/lib/ruby_maat.rb +1 -0
- data/release-please-config.json +50 -1
- metadata +12 -9
- data/.mailmap +0 -3
- data/CI_CD_SETUP.md +0 -180
- data/README_RUBY.md +0 -300
- data/RUBY_MAAT.md +0 -227
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d51369f42199d48e3ad5afb4d19737b75cccb62eb8dca64a54a0ac025a05f37c
|
|
4
|
+
data.tar.gz: 54ed689f672b96a2825961a9d5d22772d0a5d67b7ae2ea827377b001bbe09605
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8cd8d93ee615d5f9917733ef693a07cbbb9a5122944fd12210d19904aa00302d57d5d997c39eb05d112cee1a4cc2d5fc26497626d7a9257df44ef724051c6df2
|
|
7
|
+
data.tar.gz: 635137995c30cd76f544bde641cc969fee86b229ca2d7fa667b00b23b4f22b8b96e30066424c8f584766bfa70408c04d8d43e3c539687a452bc48884ef8c5516
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,84 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.4.1](https://github.com/viamin/ruby-maat/compare/v1.4.0...v1.4.1) (2026-09-21)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* 37: Can we identify merge commits to link commits to PRs? ([#44](https://github.com/viamin/ruby-maat/issues/44)) ([52ebb51](https://github.com/viamin/ruby-maat/commit/52ebb51b9f169ed1b0f28e880bbc4f876eb3cd61))
|
|
14
|
+
|
|
15
|
+
## [1.4.0](https://github.com/viamin/ruby-maat/compare/v1.3.4...v1.4.0) (2026-09-19)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* identify merge commits to link commits to PRs for coupling analysis ([#37](https://github.com/viamin/ruby-maat/issues/37)) ([ee16386](https://github.com/viamin/ruby-maat/commit/ee16386459d589886525d5d3b4ee7da2bef144cc))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* 37: Can we identify merge commits to link commits to PRs? ([#45](https://github.com/viamin/ruby-maat/issues/45)) ([6d57c5d](https://github.com/viamin/ruby-maat/commit/6d57c5d91bcc4f5dc6b4bb539021db327af1712d))
|
|
26
|
+
* address remaining review comments on merge commit grouper ([69d62a4](https://github.com/viamin/ruby-maat/commit/69d62a47205d5426d5a948ad25ac8c79fa2e9554))
|
|
27
|
+
* address remaining review comments on PR [#45](https://github.com/viamin/ruby-maat/issues/45) ([09f4279](https://github.com/viamin/ruby-maat/commit/09f4279bbd486ff470af3842cd2b7a0f422e1947))
|
|
28
|
+
* address review comments on MergeCommitGrouper ([15be4b5](https://github.com/viamin/ruby-maat/commit/15be4b54a728c4d06ff08fffedf92d0f4d257e2c))
|
|
29
|
+
* allow hyphens in author names in Git2Parser regexes ([dac123f](https://github.com/viamin/ruby-maat/commit/dac123f124cb5f82d3891427377d090f67fa668a))
|
|
30
|
+
* check for nil instead of empty parent_revisions in --group-by-merge validation ([a2c1b98](https://github.com/viamin/ruby-maat/commit/a2c1b9808d54132e5abe58ac854b3ef03879590c))
|
|
31
|
+
* clarify Set require comment to address review feedback ([4996924](https://github.com/viamin/ruby-maat/commit/4996924c3877ae698a3920253a39d1262402fbcb))
|
|
32
|
+
* improve MergeCommitGrouper correctness and performance ([cd6b500](https://github.com/viamin/ruby-maat/commit/cd6b500c413e32ee7b870be1a98faa08d0742a28))
|
|
33
|
+
* reject mixed-format logs in --group-by-merge validation ([21004c3](https://github.com/viamin/ruby-maat/commit/21004c31e8daa69d0876648bf3a2409f12a8248f))
|
|
34
|
+
* rewrite feature-branch dates to merge commit date for consistency ([a6a4cfc](https://github.com/viamin/ruby-maat/commit/a6a4cfcd417e99a58caf96376b9b6ab81a3ce9df))
|
|
35
|
+
* validate parent metadata for --group-by-merge and optimize octopus merge graph walks ([28742c0](https://github.com/viamin/ruby-maat/commit/28742c00443cb78cdbd02e9127d8db3d0089f0c4))
|
|
36
|
+
|
|
37
|
+
## [1.3.4](https://github.com/viamin/ruby-maat/compare/v1.3.3...v1.3.4) (2025-09-08)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Improvements
|
|
41
|
+
|
|
42
|
+
* simplify CI workflow and remove redundant release task ([b37fcd1](https://github.com/viamin/ruby-maat/commit/b37fcd11a885a9a45103b74c22894aa21c5ab824))
|
|
43
|
+
|
|
44
|
+
## [1.3.3](https://github.com/viamin/ruby-maat/compare/v1.3.2...v1.3.3) (2025-09-08)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
### Bug Fixes
|
|
48
|
+
|
|
49
|
+
* use correct task name rubygems:push instead of rubygem:push ([10d4795](https://github.com/viamin/ruby-maat/commit/10d4795b56346189cfd140dc1fa7c0aa7458b64d))
|
|
50
|
+
|
|
51
|
+
## [1.3.2](https://github.com/viamin/ruby-maat/compare/v1.3.1...v1.3.2) (2025-09-08)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
### Bug Fixes
|
|
55
|
+
|
|
56
|
+
* add description to release task to satisfy RuboCop ([51c8bb4](https://github.com/viamin/ruby-maat/commit/51c8bb40d3057de0a71f33db2ae6bc5eac423ee0))
|
|
57
|
+
* override release task to skip tag creation ([7f22222](https://github.com/viamin/ruby-maat/commit/7f22222df12e90d8507be5baa584a6cce34470fa))
|
|
58
|
+
|
|
59
|
+
## [1.3.1](https://github.com/viamin/ruby-maat/compare/v1.3.0...v1.3.1) (2025-09-08)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
### Bug Fixes
|
|
63
|
+
|
|
64
|
+
* **Gemfile.lock:** update ruby-maat version to 1.3.0 ([c5f8f1d](https://github.com/viamin/ruby-maat/commit/c5f8f1d659220d0db4015ac21fbb285d454a8a53))
|
|
65
|
+
* **release:** update version to 1.3.0 and enhance release-please configuration ([76c5222](https://github.com/viamin/ruby-maat/commit/76c5222720ee66c5e4238720c07f9e33edf1587e))
|
|
66
|
+
* **release:** update version to 1.3.0 and enhance release-please configuration ([6e96292](https://github.com/viamin/ruby-maat/commit/6e962922db3179f5b3d51c13db513ec89ea23fcb))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
### Dependencies
|
|
70
|
+
|
|
71
|
+
* **deps:** bump rexml from 3.4.2 to 3.4.3 ([10300aa](https://github.com/viamin/ruby-maat/commit/10300aa75eca1441fa7a9d80b285f0195ca5c078))
|
|
72
|
+
|
|
73
|
+
## [1.3.0](https://github.com/viamin/ruby-maat/compare/v1.2.0...v1.3.0) (2025-09-03)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
### Features
|
|
77
|
+
|
|
78
|
+
* add command preview functionality before execution ([3801f15](https://github.com/viamin/ruby-maat/commit/3801f15cf31cca8d519082ce78890efa4b5b9c65))
|
|
79
|
+
* add command preview functionality before execution ([499b3d5](https://github.com/viamin/ruby-maat/commit/499b3d5c929175d2e394d2877ff0ab30e973739e))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
### Bug Fixes
|
|
83
|
+
|
|
84
|
+
* move release-type out of packages section of release-please-config ([ee87f5b](https://github.com/viamin/ruby-maat/commit/ee87f5b1a2e4d48ddc36ea516a9a968b23e75f39))
|
|
85
|
+
|
|
8
86
|
## [1.2.0](https://github.com/viamin/ruby-maat/compare/ruby-maat-v1.1.3...ruby-maat/v1.2.0) (2025-08-17)
|
|
9
87
|
|
|
10
88
|
|
data/lib/ruby_maat/app.rb
CHANGED
|
@@ -44,6 +44,7 @@ module RubyMaat
|
|
|
44
44
|
change_records = parser.parse
|
|
45
45
|
|
|
46
46
|
# Apply data transformations
|
|
47
|
+
change_records = apply_merge_grouping(change_records)
|
|
47
48
|
change_records = apply_grouping(change_records)
|
|
48
49
|
change_records = apply_temporal_grouping(change_records)
|
|
49
50
|
change_records = apply_team_mapping(change_records)
|
|
@@ -94,6 +95,30 @@ module RubyMaat
|
|
|
94
95
|
end
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
def apply_merge_grouping(change_records)
|
|
99
|
+
return change_records unless @options[:group_by_merge]
|
|
100
|
+
return change_records if change_records.nil? || change_records.empty?
|
|
101
|
+
|
|
102
|
+
# Check for non-nil parent_revisions (not non-empty) because root commits
|
|
103
|
+
# legitimately have parent_revisions == [] when parsed with the parents format.
|
|
104
|
+
records_with_parents = change_records.count { |record| !record.parent_revisions.nil? }
|
|
105
|
+
records_without_parents = change_records.count { |record| record.parent_revisions.nil? }
|
|
106
|
+
|
|
107
|
+
if records_with_parents.zero?
|
|
108
|
+
raise ArgumentError,
|
|
109
|
+
"--group-by-merge requires parent revision metadata in the log. " \
|
|
110
|
+
"Please regenerate the log using the 'pr-coupling' preset or another format that includes parent hashes."
|
|
111
|
+
elsif records_without_parents.positive?
|
|
112
|
+
raise ArgumentError,
|
|
113
|
+
"--group-by-merge requires parent revision metadata for all commits, " \
|
|
114
|
+
"but the provided log mixes records with and without parent hashes. " \
|
|
115
|
+
"Please regenerate the log using the 'pr-coupling' preset or another format that includes parent hashes."
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
grouper = RubyMaat::Groupers::MergeCommitGrouper.new
|
|
119
|
+
grouper.group(change_records)
|
|
120
|
+
end
|
|
121
|
+
|
|
97
122
|
def apply_grouping(change_records)
|
|
98
123
|
return change_records unless @options[:group]
|
|
99
124
|
|
|
@@ -4,9 +4,10 @@ module RubyMaat
|
|
|
4
4
|
# Represents a single change/modification record from VCS
|
|
5
5
|
# This is the fundamental data structure that flows through the entire pipeline
|
|
6
6
|
class ChangeRecord
|
|
7
|
-
attr_reader :entity, :author, :date, :revision, :message, :loc_added, :loc_deleted
|
|
7
|
+
attr_reader :entity, :author, :date, :revision, :message, :loc_added, :loc_deleted, :parent_revisions
|
|
8
8
|
|
|
9
|
-
def initialize(entity:, author:, date:, revision:, message: nil, loc_added: nil, loc_deleted: nil
|
|
9
|
+
def initialize(entity:, author:, date:, revision:, message: nil, loc_added: nil, loc_deleted: nil,
|
|
10
|
+
parent_revisions: nil)
|
|
10
11
|
@entity = entity
|
|
11
12
|
@author = author
|
|
12
13
|
@date = date.is_a?(Date) ? date : Date.parse(date)
|
|
@@ -14,6 +15,14 @@ module RubyMaat
|
|
|
14
15
|
@message = message
|
|
15
16
|
@loc_added = loc_added.to_i if loc_added && (!loc_added.is_a?(Float) || !loc_added.nan?)
|
|
16
17
|
@loc_deleted = loc_deleted.to_i if loc_deleted && (!loc_deleted.is_a?(Float) || !loc_deleted.nan?)
|
|
18
|
+
@parent_revisions = parent_revisions
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# True when the commit has multiple parents (i.e., a merge commit).
|
|
22
|
+
# Returns false when parent metadata is unavailable (nil), since the parser
|
|
23
|
+
# could not determine the parent count.
|
|
24
|
+
def merge_commit?
|
|
25
|
+
parent_revisions.is_a?(Array) && parent_revisions.size >= 2
|
|
17
26
|
end
|
|
18
27
|
|
|
19
28
|
def to_h
|
|
@@ -24,7 +33,8 @@ module RubyMaat
|
|
|
24
33
|
revision: revision,
|
|
25
34
|
message: message,
|
|
26
35
|
loc_added: loc_added,
|
|
27
|
-
loc_deleted: loc_deleted
|
|
36
|
+
loc_deleted: loc_deleted,
|
|
37
|
+
parent_revisions: parent_revisions
|
|
28
38
|
}
|
|
29
39
|
end
|
|
30
40
|
|
data/lib/ruby_maat/cli.rb
CHANGED
|
@@ -362,6 +362,12 @@ module RubyMaat
|
|
|
362
362
|
raise ArgumentError, "Invalid date format for --age-time-now: #{date_str}. Use YYYY-MM-dd format."
|
|
363
363
|
end
|
|
364
364
|
|
|
365
|
+
opts.on("--group-by-merge",
|
|
366
|
+
"Group commits by merge commit for PR-level coupling analysis. " \
|
|
367
|
+
"Requires log generated with parent hashes (use --preset pr-coupling).") do
|
|
368
|
+
@options[:group_by_merge] = true
|
|
369
|
+
end
|
|
370
|
+
|
|
365
371
|
opts.on("--verbose-results",
|
|
366
372
|
"Includes additional analysis details together with the results. Only implemented for change coupling.") do
|
|
367
373
|
@options[:verbose_results] = true
|
|
@@ -101,6 +101,9 @@ module RubyMaat
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def execute_command(command)
|
|
104
|
+
# Show command preview and ask for confirmation
|
|
105
|
+
show_command_preview(command)
|
|
106
|
+
|
|
104
107
|
puts "Executing: #{command}" if @options[:verbose]
|
|
105
108
|
|
|
106
109
|
# Use proper subprocess execution to prevent command injection
|
|
@@ -262,6 +265,25 @@ module RubyMaat
|
|
|
262
265
|
puts "Please enter 'y' or 'n'"
|
|
263
266
|
end
|
|
264
267
|
end
|
|
268
|
+
|
|
269
|
+
def show_command_preview(command)
|
|
270
|
+
# Skip preview in non-interactive modes, when quiet is set, or during tests
|
|
271
|
+
return if @options[:quiet] || !$stdin.tty? || test_environment?
|
|
272
|
+
|
|
273
|
+
puts "\n" + "=" * 60
|
|
274
|
+
puts "COMMAND PREVIEW"
|
|
275
|
+
puts "=" * 60
|
|
276
|
+
puts "Repository: #{@repository_path}"
|
|
277
|
+
puts "Command: #{command}"
|
|
278
|
+
puts "=" * 60
|
|
279
|
+
puts "\nPress Enter to execute this command..."
|
|
280
|
+
$stdin.gets
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def test_environment?
|
|
284
|
+
# Check if we're in a test environment
|
|
285
|
+
!!(defined?(RSpec) || ENV["RUBY_MAAT_TEST"] == "true" || ENV["RSPEC_CORE_PID"])
|
|
286
|
+
end
|
|
265
287
|
end
|
|
266
288
|
end
|
|
267
289
|
end
|
|
@@ -48,6 +48,14 @@ module RubyMaat
|
|
|
48
48
|
no_renames: true,
|
|
49
49
|
all_branches: true
|
|
50
50
|
}
|
|
51
|
+
},
|
|
52
|
+
"pr-coupling" => {
|
|
53
|
+
description: "Format with parent hashes for PR-level coupling analysis (use with --group-by-merge)",
|
|
54
|
+
options: {
|
|
55
|
+
format: "git2-parents",
|
|
56
|
+
no_renames: true,
|
|
57
|
+
all_branches: true
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
60
|
}.freeze
|
|
53
61
|
|
|
@@ -72,6 +80,8 @@ module RubyMaat
|
|
|
72
80
|
case format
|
|
73
81
|
when "git2"
|
|
74
82
|
build_git2_command(options)
|
|
83
|
+
when "git2-parents"
|
|
84
|
+
build_git2_parents_command(options)
|
|
75
85
|
when "legacy"
|
|
76
86
|
build_legacy_command(options)
|
|
77
87
|
else
|
|
@@ -87,10 +97,12 @@ module RubyMaat
|
|
|
87
97
|
# Format selection
|
|
88
98
|
puts "Output formats:"
|
|
89
99
|
puts " 1. git2 (recommended, faster parsing)"
|
|
90
|
-
puts " 2.
|
|
100
|
+
puts " 2. git2-parents (git2 with parent hashes, for --group-by-merge)"
|
|
101
|
+
puts " 3. legacy (backward compatibility)"
|
|
91
102
|
|
|
92
|
-
format_choice = ask_integer("Choose format", 1,
|
|
93
|
-
|
|
103
|
+
format_choice = ask_integer("Choose format", 1, 3)
|
|
104
|
+
formats = {1 => "git2", 2 => "git2-parents", 3 => "legacy"}
|
|
105
|
+
options[:format] = formats[format_choice]
|
|
94
106
|
|
|
95
107
|
# Branch selection
|
|
96
108
|
options[:all_branches] = ask_yes_no("Include all branches?", options[:all_branches])
|
|
@@ -119,7 +131,7 @@ module RubyMaat
|
|
|
119
131
|
parts << "--all" if options[:all_branches]
|
|
120
132
|
parts << "--numstat"
|
|
121
133
|
parts << "--date=short"
|
|
122
|
-
parts << "--pretty=format:'--%h--%ad--%aN'"
|
|
134
|
+
parts << "--pretty=format:'--%h--%ad--%aN--%s'"
|
|
123
135
|
parts << "--no-renames" if options[:no_renames]
|
|
124
136
|
|
|
125
137
|
# Date filtering (with validation and shell escaping)
|
|
@@ -135,6 +147,25 @@ module RubyMaat
|
|
|
135
147
|
parts.join(" ")
|
|
136
148
|
end
|
|
137
149
|
|
|
150
|
+
def build_git2_parents_command(options)
|
|
151
|
+
parts = ["git", "log"]
|
|
152
|
+
|
|
153
|
+
parts << "--all" if options[:all_branches]
|
|
154
|
+
parts << "--numstat"
|
|
155
|
+
parts << "--date=short"
|
|
156
|
+
parts << "--pretty=format:'--%h--%p--%ad--%aN--%s'"
|
|
157
|
+
parts << "--no-renames" if options[:no_renames]
|
|
158
|
+
|
|
159
|
+
parts << "--after=#{shell_escape(validate_date(options[:since]))}" if options[:since]
|
|
160
|
+
parts << "--before=#{shell_escape(validate_date(options[:until]))}" if options[:until]
|
|
161
|
+
|
|
162
|
+
parts << "--author=#{shell_escape(options[:author])}" if options[:author]
|
|
163
|
+
|
|
164
|
+
parts << "--" << shell_escape(options[:path]) if options[:path]
|
|
165
|
+
|
|
166
|
+
parts.join(" ")
|
|
167
|
+
end
|
|
168
|
+
|
|
138
169
|
def build_legacy_command(options)
|
|
139
170
|
parts = ["git", "log"]
|
|
140
171
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# NOTE: Set has been a built-in class (autoloaded without require) since
|
|
4
|
+
# Ruby 3.2, which is this project's minimum version (see gemspec:
|
|
5
|
+
# required_ruby_version >= "3.2.0"). An explicit `require "set"` is
|
|
6
|
+
# unnecessary and triggers RuboCop's Lint/RedundantRequireStatement cop.
|
|
7
|
+
|
|
8
|
+
module RubyMaat
|
|
9
|
+
module Groupers
|
|
10
|
+
# Groups commits by their merge commit to enable PR-level coupling analysis.
|
|
11
|
+
#
|
|
12
|
+
# When Git uses a merge-based workflow (e.g., GitHub PRs), individual commits
|
|
13
|
+
# on feature branches are combined into merge commits on the main branch.
|
|
14
|
+
# This grouper identifies merge commits and rewrites the revision of child
|
|
15
|
+
# commits so that all commits belonging to a merge share the same revision.
|
|
16
|
+
#
|
|
17
|
+
# This allows the coupling analysis to treat all files changed in a PR as
|
|
18
|
+
# co-changing, giving more meaningful coupling results.
|
|
19
|
+
#
|
|
20
|
+
# Requires log data generated with parent hashes:
|
|
21
|
+
# git log --all --numstat --date=short --pretty=format:'--%h--%p--%ad--%aN--%s' --no-renames
|
|
22
|
+
class MergeCommitGrouper
|
|
23
|
+
def group(change_records)
|
|
24
|
+
commit_info = build_commit_info(change_records)
|
|
25
|
+
merge_map = build_merge_map(commit_info)
|
|
26
|
+
|
|
27
|
+
return change_records if merge_map.empty?
|
|
28
|
+
|
|
29
|
+
merge_dates = build_merge_dates(commit_info)
|
|
30
|
+
rewrite_records(change_records, merge_map, merge_dates)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_commit_info(records)
|
|
36
|
+
commits = {}
|
|
37
|
+
records.each do |record|
|
|
38
|
+
rev = record.revision
|
|
39
|
+
commits[rev] ||= {parents: record.parent_revisions || [], merge: false, date: record.date}
|
|
40
|
+
commits[rev][:merge] = true if record.merge_commit?
|
|
41
|
+
end
|
|
42
|
+
commits
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_merge_map(commits)
|
|
46
|
+
merge_map = {}
|
|
47
|
+
merges = commits.select { |_, info| info[:merge] }
|
|
48
|
+
|
|
49
|
+
merges.each do |merge_rev, info|
|
|
50
|
+
parents = info[:parents] || []
|
|
51
|
+
mainline_parent = parents[0]
|
|
52
|
+
# Skip this merge if it does not have a valid mainline parent present
|
|
53
|
+
# in the commit set, or if it is not a true merge (single parent).
|
|
54
|
+
next unless mainline_parent && parents.length > 1 && commits.key?(mainline_parent)
|
|
55
|
+
|
|
56
|
+
# Compute mainline ancestors once per merge commit and reuse across
|
|
57
|
+
# all feature parents, avoiding repeated graph walks for octopus merges.
|
|
58
|
+
mainline_ancestors = collect_ancestors(mainline_parent, commits)
|
|
59
|
+
# If we cannot determine any ancestors for the mainline parent (for
|
|
60
|
+
# example due to a filtered log), skip grouping for this merge to
|
|
61
|
+
# avoid incorrect rewrites with an incomplete graph.
|
|
62
|
+
next if mainline_ancestors.empty?
|
|
63
|
+
|
|
64
|
+
feature_parents = parents[1..].compact
|
|
65
|
+
feature_parents.each do |feature_parent|
|
|
66
|
+
feature_commits = find_feature_commits(feature_parent, mainline_ancestors, commits)
|
|
67
|
+
feature_commits.each { |rev| merge_map[rev] = merge_rev }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
merge_map
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Walk backward from the feature branch tip, collecting commits that belong
|
|
75
|
+
# to this merge. Excludes any commits reachable from the mainline parent so
|
|
76
|
+
# that intermediate merges from main into the feature branch don't pull in
|
|
77
|
+
# unrelated mainline history.
|
|
78
|
+
#
|
|
79
|
+
# +mainline_ancestors+ is a pre-computed Set of commits reachable from the
|
|
80
|
+
# mainline parent, passed in to avoid redundant graph walks when a merge
|
|
81
|
+
# has multiple feature parents (octopus merges).
|
|
82
|
+
def find_feature_commits(start, mainline_ancestors, commits)
|
|
83
|
+
visited = Set.new
|
|
84
|
+
queue = [start]
|
|
85
|
+
head = 0
|
|
86
|
+
|
|
87
|
+
while head < queue.length
|
|
88
|
+
current = queue[head]
|
|
89
|
+
head += 1
|
|
90
|
+
next if mainline_ancestors.include?(current) || visited.include?(current) || !commits.key?(current)
|
|
91
|
+
|
|
92
|
+
visited << current
|
|
93
|
+
|
|
94
|
+
parents = commits[current][:parents]
|
|
95
|
+
parents&.each { |p| queue << p }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
visited
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Collect all ancestors reachable from a given commit (inclusive).
|
|
102
|
+
def collect_ancestors(start, commits)
|
|
103
|
+
ancestors = Set.new
|
|
104
|
+
queue = [start]
|
|
105
|
+
head = 0
|
|
106
|
+
|
|
107
|
+
while head < queue.length
|
|
108
|
+
current = queue[head]
|
|
109
|
+
head += 1
|
|
110
|
+
next if ancestors.include?(current) || !commits.key?(current)
|
|
111
|
+
|
|
112
|
+
ancestors << current
|
|
113
|
+
|
|
114
|
+
parents = commits[current][:parents]
|
|
115
|
+
parents&.each { |p| queue << p }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
ancestors
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Build a lookup from merge revision to its date, used to align
|
|
122
|
+
# rewritten feature-branch records with the merge commit's date.
|
|
123
|
+
def build_merge_dates(commits)
|
|
124
|
+
dates = {}
|
|
125
|
+
commits.each do |rev, info|
|
|
126
|
+
dates[rev] = info[:date] if info[:merge]
|
|
127
|
+
end
|
|
128
|
+
dates
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def rewrite_records(records, merge_map, merge_dates)
|
|
132
|
+
records.map do |record|
|
|
133
|
+
merge_rev = merge_map[record.revision]
|
|
134
|
+
if merge_rev
|
|
135
|
+
ChangeRecord.new(
|
|
136
|
+
entity: record.entity,
|
|
137
|
+
author: record.author,
|
|
138
|
+
date: merge_dates[merge_rev] || record.date,
|
|
139
|
+
revision: merge_rev,
|
|
140
|
+
message: record.message,
|
|
141
|
+
loc_added: record.loc_added,
|
|
142
|
+
loc_deleted: record.loc_deleted,
|
|
143
|
+
parent_revisions: record.parent_revisions
|
|
144
|
+
)
|
|
145
|
+
else
|
|
146
|
+
record
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -5,17 +5,27 @@ module RubyMaat
|
|
|
5
5
|
module Parsers
|
|
6
6
|
# Git2 parser - preferred Git parser (more tolerant and faster)
|
|
7
7
|
#
|
|
8
|
-
#
|
|
8
|
+
# Supports two formats:
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
# --
|
|
10
|
+
# Standard format (without parent info):
|
|
11
|
+
# git log --all --numstat --date=short --pretty=format:'--%h--%ad--%aN--%s' --no-renames
|
|
12
|
+
#
|
|
13
|
+
# Format with parent hashes (for --group-by-merge / PR-level coupling):
|
|
14
|
+
# git log --all --numstat --date=short --pretty=format:'--%h--%p--%ad--%aN--%s' --no-renames
|
|
15
|
+
# This format is generated by the "pr-coupling" preset.
|
|
16
|
+
#
|
|
17
|
+
# Sample standard format:
|
|
18
|
+
# --586b4eb--2015-06-15--Adam Tornhill--Add new feature
|
|
12
19
|
# 35 0 src/code_maat/mining/vcs.clj
|
|
13
|
-
# 2 1 test/file.rb
|
|
14
20
|
#
|
|
15
|
-
#
|
|
21
|
+
# Sample parent-hash format (merge commit has multiple parent hashes):
|
|
22
|
+
# --abc123--def456 ghi789--2015-06-16--Jane Doe--Merge pull request #42
|
|
16
23
|
# 10 5 lib/example.rb
|
|
17
24
|
class Git2Parser < BaseParser
|
|
18
|
-
|
|
25
|
+
# Format with parent hashes: --hash--parent1 parent2--date--author--message
|
|
26
|
+
COMMIT_WITH_PARENTS = /^--([a-z0-9]+)--([a-z0-9 ]*)--(\d{4}-\d{2}-\d{2})--([^\r\n]+?)--([^\r\n]*)$/
|
|
27
|
+
# Standard format: --hash--date--author--message
|
|
28
|
+
COMMIT_SEPARATOR = /^--([a-z0-9]+)--(\d{4}-\d{2}-\d{2})--([^\r\n]+?)--([^\r\n]*)$/
|
|
19
29
|
CHANGE_PATTERN = /^(-|\d+)[\t ]{1,10}(-|\d+)[\t ]{1,10}([^\r\n]*)$/
|
|
20
30
|
|
|
21
31
|
protected
|
|
@@ -28,11 +38,22 @@ module RubyMaat
|
|
|
28
38
|
line.strip!
|
|
29
39
|
next if line.empty?
|
|
30
40
|
|
|
31
|
-
if (commit_match = line.match(
|
|
41
|
+
if (commit_match = line.match(COMMIT_WITH_PARENTS))
|
|
42
|
+
parents_str = commit_match[2].strip
|
|
43
|
+
current_commit = {
|
|
44
|
+
revision: commit_match[1],
|
|
45
|
+
parent_revisions: parents_str.empty? ? [] : parents_str.split,
|
|
46
|
+
date: parse_date(commit_match[3]),
|
|
47
|
+
author: commit_match[4].strip,
|
|
48
|
+
message: commit_match[5].strip
|
|
49
|
+
}
|
|
50
|
+
elsif (commit_match = line.match(COMMIT_SEPARATOR))
|
|
32
51
|
current_commit = {
|
|
33
52
|
revision: commit_match[1],
|
|
53
|
+
parent_revisions: nil,
|
|
34
54
|
date: parse_date(commit_match[2]),
|
|
35
|
-
author: commit_match[3].strip
|
|
55
|
+
author: commit_match[3].strip,
|
|
56
|
+
message: commit_match[4].strip
|
|
36
57
|
}
|
|
37
58
|
elsif current_commit && (change_match = line.match(CHANGE_PATTERN))
|
|
38
59
|
added = clean_numstat(change_match[1])
|
|
@@ -47,8 +68,10 @@ module RubyMaat
|
|
|
47
68
|
author: current_commit[:author],
|
|
48
69
|
date: current_commit[:date],
|
|
49
70
|
revision: current_commit[:revision],
|
|
71
|
+
message: current_commit[:message],
|
|
50
72
|
loc_added: added,
|
|
51
|
-
loc_deleted: deleted
|
|
73
|
+
loc_deleted: deleted,
|
|
74
|
+
parent_revisions: current_commit[:parent_revisions]
|
|
52
75
|
)
|
|
53
76
|
end
|
|
54
77
|
end
|
data/lib/ruby_maat/version.rb
CHANGED
data/lib/ruby_maat.rb
CHANGED
|
@@ -17,6 +17,7 @@ require_relative "ruby_maat/parsers/tfs_parser"
|
|
|
17
17
|
require_relative "ruby_maat/groupers/layer_grouper"
|
|
18
18
|
require_relative "ruby_maat/groupers/time_grouper"
|
|
19
19
|
require_relative "ruby_maat/groupers/team_mapper"
|
|
20
|
+
require_relative "ruby_maat/groupers/merge_commit_grouper"
|
|
20
21
|
|
|
21
22
|
# Analysis modules (load before app.rb since app references them)
|
|
22
23
|
require_relative "ruby_maat/analysis/base_analysis"
|
data/release-please-config.json
CHANGED
|
@@ -1,8 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
2
3
|
"packages": {
|
|
3
4
|
".": {
|
|
4
5
|
"release-type": "ruby",
|
|
5
|
-
"
|
|
6
|
+
"package-name": "ruby-maat",
|
|
7
|
+
"version-file": "lib/ruby_maat/version.rb",
|
|
8
|
+
"changelog-sections": [
|
|
9
|
+
{
|
|
10
|
+
"type": "feat",
|
|
11
|
+
"section": "Features",
|
|
12
|
+
"hidden": false
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "fix",
|
|
16
|
+
"section": "Bug Fixes",
|
|
17
|
+
"hidden": false
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "refactor",
|
|
21
|
+
"section": "Improvements",
|
|
22
|
+
"hidden": false
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "chore",
|
|
26
|
+
"section": "Maintenance",
|
|
27
|
+
"hidden": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "docs",
|
|
31
|
+
"section": "Documentation",
|
|
32
|
+
"hidden": false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "test",
|
|
36
|
+
"section": "Testing",
|
|
37
|
+
"hidden": true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "ci",
|
|
41
|
+
"section": "CI/CD",
|
|
42
|
+
"hidden": true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "deps",
|
|
46
|
+
"section": "Dependencies",
|
|
47
|
+
"hidden": false
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "BREAKING CHANGE",
|
|
51
|
+
"section": "Breaking Changes",
|
|
52
|
+
"hidden": false
|
|
53
|
+
}
|
|
54
|
+
]
|
|
6
55
|
}
|
|
7
56
|
}
|
|
8
57
|
}
|