ruby-maat 1.0.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/.commitlintrc.json +2 -1
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +23 -1
- data/.standard.yml +3 -0
- data/CHANGELOG.md +178 -0
- data/CLAUDE.md +100 -50
- data/README.md +99 -8
- data/lib/ruby_maat/analysis/churn.rb +2 -2
- data/lib/ruby_maat/analysis/effort.rb +5 -5
- data/lib/ruby_maat/analysis_presets.rb +313 -0
- data/lib/ruby_maat/app.rb +25 -0
- data/lib/ruby_maat/change_record.rb +13 -3
- data/lib/ruby_maat/cli.rb +259 -5
- data/lib/ruby_maat/generators/base_generator.rb +289 -0
- data/lib/ruby_maat/generators/git_generator.rb +207 -0
- data/lib/ruby_maat/generators/svn_generator.rb +201 -0
- data/lib/ruby_maat/groupers/merge_commit_grouper.rb +152 -0
- data/lib/ruby_maat/parsers/git2_parser.rb +33 -10
- data/lib/ruby_maat/parsers/git_parser.rb +2 -2
- data/lib/ruby_maat/parsers/mercurial_parser.rb +1 -1
- data/lib/ruby_maat/parsers/perforce_parser.rb +2 -2
- data/lib/ruby_maat/parsers/tfs_parser.rb +3 -3
- data/lib/ruby_maat/vcs_detector.rb +75 -0
- data/lib/ruby_maat/version.rb +1 -1
- data/lib/ruby_maat.rb +1 -0
- data/release-please-config.json +57 -0
- metadata +19 -11
- data/.mailmap +0 -3
- data/.release-please-config.json +0 -33
- data/CI_CD_SETUP.md +0 -180
- data/README_RUBY.md +0 -300
- data/RELEASE_PLEASE_SETUP.md +0 -198
- data/RUBY_MAAT.md +0 -227
|
@@ -105,7 +105,7 @@ module RubyMaat
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
# Filter by minimum revisions and format results
|
|
108
|
-
filtered_results = results.values.
|
|
108
|
+
filtered_results = results.values.filter_map do |result|
|
|
109
109
|
next if result[:revisions].size < min_revs
|
|
110
110
|
|
|
111
111
|
{
|
|
@@ -114,7 +114,7 @@ module RubyMaat
|
|
|
114
114
|
deleted: result[:deleted],
|
|
115
115
|
commits: result[:revisions].size
|
|
116
116
|
}
|
|
117
|
-
end
|
|
117
|
+
end
|
|
118
118
|
|
|
119
119
|
# Sort by total churn descending
|
|
120
120
|
filtered_results.sort_by! { |r| -(r[:added] + r[:deleted]) }
|
|
@@ -76,13 +76,13 @@ module RubyMaat
|
|
|
76
76
|
results = []
|
|
77
77
|
|
|
78
78
|
entity_authors.each do |entity, authors|
|
|
79
|
-
total_revisions = authors.values.
|
|
79
|
+
total_revisions = authors.values.sum(&:size)
|
|
80
80
|
next if total_revisions < min_revs
|
|
81
81
|
|
|
82
82
|
# Find author with most revisions
|
|
83
83
|
main_author, revisions = authors.max_by { |_author, revs| revs.size }
|
|
84
84
|
|
|
85
|
-
total_revisions = authors.values.
|
|
85
|
+
total_revisions = authors.values.sum(&:size)
|
|
86
86
|
|
|
87
87
|
results << {
|
|
88
88
|
entity: entity,
|
|
@@ -122,14 +122,14 @@ module RubyMaat
|
|
|
122
122
|
results = []
|
|
123
123
|
|
|
124
124
|
entity_contributions.each do |entity, authors|
|
|
125
|
-
total_revisions = authors.values.
|
|
125
|
+
total_revisions = authors.values.sum(&:size)
|
|
126
126
|
next if total_revisions < min_revs
|
|
127
127
|
|
|
128
128
|
# Calculate fractal value: 1 - sum(p_i^2) where p_i is proportion of each author
|
|
129
|
-
sum_of_squares = authors.values.
|
|
129
|
+
sum_of_squares = authors.values.sum do |revisions|
|
|
130
130
|
proportion = revisions.size.to_f / total_revisions
|
|
131
131
|
proportion**2
|
|
132
|
-
end
|
|
132
|
+
end
|
|
133
133
|
|
|
134
134
|
fractal_value = 1.0 - sum_of_squares
|
|
135
135
|
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module RubyMaat
|
|
6
|
+
module AnalysisPresets
|
|
7
|
+
# Analysis configurations with appropriate time ranges and descriptions
|
|
8
|
+
ANALYSIS_CONFIGS = {
|
|
9
|
+
"authors" => {
|
|
10
|
+
name: "Developer Activity Analysis",
|
|
11
|
+
description: "Shows number of developers working on each module",
|
|
12
|
+
time_sensitive: true,
|
|
13
|
+
presets: {
|
|
14
|
+
"team-activity" => {
|
|
15
|
+
description: "Team activity patterns (6 months)",
|
|
16
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
17
|
+
},
|
|
18
|
+
"recent-team" => {
|
|
19
|
+
description: "Recent team changes (3 months)",
|
|
20
|
+
since: -> { (Date.today - 90).strftime("%Y-%m-%d") }
|
|
21
|
+
},
|
|
22
|
+
"team-history" => {
|
|
23
|
+
description: "Complete team evolution (2 years)",
|
|
24
|
+
since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
"coupling" => {
|
|
30
|
+
name: "Logical Coupling Analysis",
|
|
31
|
+
description: "Finds modules that change together (hidden dependencies)",
|
|
32
|
+
time_sensitive: true,
|
|
33
|
+
presets: {
|
|
34
|
+
"recent-coupling" => {
|
|
35
|
+
description: "Current coupling patterns (3 months)",
|
|
36
|
+
since: -> { (Date.today - 90).strftime("%Y-%m-%d") }
|
|
37
|
+
},
|
|
38
|
+
"coupling-trends" => {
|
|
39
|
+
description: "Coupling evolution (1 year)",
|
|
40
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
41
|
+
},
|
|
42
|
+
"architecture-review" => {
|
|
43
|
+
description: "Deep architecture analysis (6 months)",
|
|
44
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
"age" => {
|
|
50
|
+
name: "Code Age Analysis",
|
|
51
|
+
description: "Shows how recently each module was modified",
|
|
52
|
+
time_sensitive: false,
|
|
53
|
+
presets: {
|
|
54
|
+
"full-history" => {
|
|
55
|
+
description: "Complete age analysis (all history)"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
"abs-churn" => {
|
|
61
|
+
name: "Absolute Code Churn",
|
|
62
|
+
description: "Total lines added/deleted over time",
|
|
63
|
+
time_sensitive: true,
|
|
64
|
+
presets: {
|
|
65
|
+
"recent-churn" => {
|
|
66
|
+
description: "Recent development activity (6 months)",
|
|
67
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
68
|
+
},
|
|
69
|
+
"yearly-churn" => {
|
|
70
|
+
description: "Annual churn patterns (1 year)",
|
|
71
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
72
|
+
},
|
|
73
|
+
"project-churn" => {
|
|
74
|
+
description: "Project lifecycle churn (2 years)",
|
|
75
|
+
since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
"author-churn" => {
|
|
81
|
+
name: "Author Churn Analysis",
|
|
82
|
+
description: "Code churn by individual developers",
|
|
83
|
+
time_sensitive: true,
|
|
84
|
+
presets: {
|
|
85
|
+
"contributor-activity" => {
|
|
86
|
+
description: "Current contributor patterns (6 months)",
|
|
87
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
88
|
+
},
|
|
89
|
+
"team-contributions" => {
|
|
90
|
+
description: "Team contribution history (1 year)",
|
|
91
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
"entity-churn" => {
|
|
97
|
+
name: "Module Churn Analysis",
|
|
98
|
+
description: "Churn by individual modules/files",
|
|
99
|
+
time_sensitive: true,
|
|
100
|
+
presets: {
|
|
101
|
+
"hotspot-analysis" => {
|
|
102
|
+
description: "Current hotspots (6 months)",
|
|
103
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
104
|
+
},
|
|
105
|
+
"stability-review" => {
|
|
106
|
+
description: "Module stability patterns (1 year)",
|
|
107
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
"entity-ownership" => {
|
|
113
|
+
name: "Code Ownership Analysis",
|
|
114
|
+
description: "Shows ownership distribution of code",
|
|
115
|
+
time_sensitive: true,
|
|
116
|
+
presets: {
|
|
117
|
+
"current-ownership" => {
|
|
118
|
+
description: "Current ownership patterns (1 year)",
|
|
119
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
120
|
+
},
|
|
121
|
+
"ownership-evolution" => {
|
|
122
|
+
description: "Ownership changes over time (2 years)",
|
|
123
|
+
since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
"main-dev" => {
|
|
129
|
+
name: "Main Developer Analysis",
|
|
130
|
+
description: "Identifies primary developer for each module",
|
|
131
|
+
time_sensitive: true,
|
|
132
|
+
presets: {
|
|
133
|
+
"current-maintainers" => {
|
|
134
|
+
description: "Current module maintainers (1 year)",
|
|
135
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
136
|
+
},
|
|
137
|
+
"maintainer-history" => {
|
|
138
|
+
description: "Maintainer evolution (2 years)",
|
|
139
|
+
since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
"entity-effort" => {
|
|
145
|
+
name: "Development Effort Analysis",
|
|
146
|
+
description: "Effort distribution across modules",
|
|
147
|
+
time_sensitive: true,
|
|
148
|
+
presets: {
|
|
149
|
+
"effort-focus" => {
|
|
150
|
+
description: "Recent effort distribution (6 months)",
|
|
151
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
152
|
+
},
|
|
153
|
+
"effort-trends" => {
|
|
154
|
+
description: "Effort patterns over time (1 year)",
|
|
155
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
"communication" => {
|
|
161
|
+
name: "Team Communication Analysis",
|
|
162
|
+
description: "Developer collaboration patterns",
|
|
163
|
+
time_sensitive: true,
|
|
164
|
+
presets: {
|
|
165
|
+
"team-collaboration" => {
|
|
166
|
+
description: "Current collaboration patterns (6 months)",
|
|
167
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
168
|
+
},
|
|
169
|
+
"communication-trends" => {
|
|
170
|
+
description: "Communication evolution (1 year)",
|
|
171
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
"summary" => {
|
|
177
|
+
name: "Project Summary",
|
|
178
|
+
description: "High-level project statistics",
|
|
179
|
+
time_sensitive: true,
|
|
180
|
+
presets: {
|
|
181
|
+
"project-overview" => {
|
|
182
|
+
description: "Current project state (1 year)",
|
|
183
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
184
|
+
},
|
|
185
|
+
"full-summary" => {
|
|
186
|
+
description: "Complete project history"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
"revisions" => {
|
|
192
|
+
name: "Revision Count Analysis",
|
|
193
|
+
description: "Number of changes per module",
|
|
194
|
+
time_sensitive: true,
|
|
195
|
+
presets: {
|
|
196
|
+
"activity-hotspots" => {
|
|
197
|
+
description: "Current activity patterns (6 months)",
|
|
198
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
199
|
+
},
|
|
200
|
+
"change-history" => {
|
|
201
|
+
description: "Complete change patterns (2 years)",
|
|
202
|
+
since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
# Analyses that don't benefit from time filtering
|
|
208
|
+
"identity" => {
|
|
209
|
+
name: "Identity Analysis",
|
|
210
|
+
description: "Raw parsed data (debugging/export)",
|
|
211
|
+
time_sensitive: false,
|
|
212
|
+
presets: {
|
|
213
|
+
"full-data" => {
|
|
214
|
+
description: "Complete dataset export"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
"soc" => {
|
|
220
|
+
name: "Sum of Coupling",
|
|
221
|
+
description: "Total coupling strength per module",
|
|
222
|
+
time_sensitive: true,
|
|
223
|
+
presets: {
|
|
224
|
+
"coupling-strength" => {
|
|
225
|
+
description: "Current coupling analysis (6 months)",
|
|
226
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
"refactoring-main-dev" => {
|
|
232
|
+
name: "Refactoring Main Developer",
|
|
233
|
+
description: "Main developer in refactoring contexts",
|
|
234
|
+
time_sensitive: true,
|
|
235
|
+
presets: {
|
|
236
|
+
"refactoring-leads" => {
|
|
237
|
+
description: "Recent refactoring activity (1 year)",
|
|
238
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
"main-dev-by-revs" => {
|
|
244
|
+
name: "Main Developer (by Revisions)",
|
|
245
|
+
description: "Main developer by revision count",
|
|
246
|
+
time_sensitive: true,
|
|
247
|
+
presets: {
|
|
248
|
+
"revision-leaders" => {
|
|
249
|
+
description: "Current revision leaders (1 year)",
|
|
250
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
"fragmentation" => {
|
|
256
|
+
name: "Development Fragmentation",
|
|
257
|
+
description: "How fragmented development effort is",
|
|
258
|
+
time_sensitive: true,
|
|
259
|
+
presets: {
|
|
260
|
+
"team-fragmentation" => {
|
|
261
|
+
description: "Current team fragmentation (6 months)",
|
|
262
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
"messages" => {
|
|
268
|
+
name: "Commit Message Analysis",
|
|
269
|
+
description: "Analyzes commit message patterns",
|
|
270
|
+
time_sensitive: true,
|
|
271
|
+
presets: {
|
|
272
|
+
"message-patterns" => {
|
|
273
|
+
description: "Recent commit patterns (6 months)",
|
|
274
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
275
|
+
},
|
|
276
|
+
"communication-style" => {
|
|
277
|
+
description: "Team communication style (1 year)",
|
|
278
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}.freeze
|
|
283
|
+
|
|
284
|
+
def self.analysis_config(analysis_name)
|
|
285
|
+
ANALYSIS_CONFIGS[analysis_name]
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def self.available_analyses
|
|
289
|
+
ANALYSIS_CONFIGS.keys.sort
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def self.analysis_description(analysis_name)
|
|
293
|
+
config = ANALYSIS_CONFIGS[analysis_name]
|
|
294
|
+
return "Unknown analysis" unless config
|
|
295
|
+
"#{config[:name]} - #{config[:description]}"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def self.presets_for_analysis(analysis_name)
|
|
299
|
+
config = ANALYSIS_CONFIGS[analysis_name]
|
|
300
|
+
return {} unless config
|
|
301
|
+
|
|
302
|
+
# Evaluate lambda functions in preset options
|
|
303
|
+
config[:presets].transform_values do |preset|
|
|
304
|
+
preset.transform_values { |v| v.is_a?(Proc) ? v.call : v }
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def self.time_sensitive?(analysis_name)
|
|
309
|
+
config = ANALYSIS_CONFIGS[analysis_name]
|
|
310
|
+
config ? config[:time_sensitive] : false
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
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
|
|