commenter 0.4.0 → 0.5.0
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/.rubocop.yml +4 -0
- data/CONTEXT.md +6 -0
- data/README.adoc +43 -0
- data/lib/commenter/ballot_diff.rb +64 -0
- data/lib/commenter/cli.rb +81 -0
- data/lib/commenter/github_integration.rb +9 -4
- data/lib/commenter/github_sync.rb +105 -0
- data/lib/commenter/version.rb +1 -1
- data/lib/commenter.rb +2 -0
- data/spec/commenter/ballot_diff_spec.rb +61 -0
- data/spec/commenter/github_sync_spec.rb +87 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 077fd6e045907b803d3f6b33d305bab0a4544b37ede3855fb26af1974f48385a
|
|
4
|
+
data.tar.gz: ffe50dadee86e040ce1b0b5fbb367f5e5233f4bd668248e9f473c2f9bfbccfb3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f3a1e36e43d5dc1d16717afab89546c3c8151c38c56c8b2f8cdbf5885a0cb5d38a34f6a6a1869892bc07a1ff0efe4f4d229a4137241565d74d4e4920555eba4b
|
|
7
|
+
data.tar.gz: d368ce137f669d56ec8c72da5c3a106c7acc95e19c8daa0c3371e09cc91c5af79e6d0af0cfd4498ac466aef96c26377c5020647b8132a59fce453574a303217a
|
data/.rubocop.yml
CHANGED
data/CONTEXT.md
CHANGED
|
@@ -23,5 +23,11 @@ keep this current when concepts sharpen.
|
|
|
23
23
|
from ISO/CS editors; also carries reviewer remark threads.
|
|
24
24
|
- **OSD** — ISO Online Standards Development; its XLSX comment exports come
|
|
25
25
|
in resolved and unresolved variants.
|
|
26
|
+
- **Stage Comparison** — how the comment set evolved between two ballots
|
|
27
|
+
(new, withdrawn, repeated, revised; resolved = disposition recorded).
|
|
28
|
+
`BallotDiff`.
|
|
29
|
+
- **GitHub Sync** — reconciliation of the comment YAML with GitHub issues
|
|
30
|
+
(YAML as source of truth): create missing, refresh content per conflict
|
|
31
|
+
policy, close dispositioned issues. `GitHubSync`.
|
|
26
32
|
- **Unique ID** — stage-aware identity of a comment's GitHub issue
|
|
27
33
|
(`[DIS] GB-001` by default), used for duplicate detection.
|
data/README.adoc
CHANGED
|
@@ -234,6 +234,20 @@ issue counts as *open*; no disposition at all counts as *undecided*). Use
|
|
|
234
234
|
`--format yaml` for machine-readable output, and `-o report.md` to write to a
|
|
235
235
|
file — the Markdown pastes directly into a GitHub issue or WG minutes.
|
|
236
236
|
|
|
237
|
+
=== Comparing ballots between stages
|
|
238
|
+
|
|
239
|
+
The same comment ID (`DE-001`) recurs at every ballot stage; comparing two
|
|
240
|
+
ballot sheets shows how the set evolved between CD and DIS:
|
|
241
|
+
|
|
242
|
+
[source,shell]
|
|
243
|
+
----
|
|
244
|
+
commenter diff cd-ballot.yaml dis-ballot.yaml
|
|
245
|
+
----
|
|
246
|
+
|
|
247
|
+
Comments are matched by ID and classified as new, withdrawn, repeated
|
|
248
|
+
(unchanged text), or revised; a comment whose later version carries a
|
|
249
|
+
disposition counts as resolved. Markdown (default) or `--format yaml`.
|
|
250
|
+
|
|
237
251
|
=== Filling DOCX templates from YAML
|
|
238
252
|
|
|
239
253
|
This gem contains a command-line utility to fill a DOCX template with comments
|
|
@@ -464,6 +478,35 @@ The `unique_id` is available as a variable in title and body templates:
|
|
|
464
478
|
|
|
465
479
|
This renders to: `[DIS] GB-001: Clause 5.1 summary (ISO/DIS 2533)`
|
|
466
480
|
|
|
481
|
+
==== Synchronizing with GitHub issues
|
|
482
|
+
|
|
483
|
+
`github-create` is one-shot: it creates missing issues and skips existing
|
|
484
|
+
ones. `github-sync` keeps the YAML and the issues in step, treating the YAML
|
|
485
|
+
as the source of truth:
|
|
486
|
+
|
|
487
|
+
[source,shell]
|
|
488
|
+
----
|
|
489
|
+
commenter github-sync --config github_config.yaml comments.yaml
|
|
490
|
+
----
|
|
491
|
+
|
|
492
|
+
For each comment it creates a missing issue, refreshes issue content from
|
|
493
|
+
the YAML, and reconciles issue state. Two policies govern the parts that
|
|
494
|
+
can collide:
|
|
495
|
+
|
|
496
|
+
`--conflict yaml` (default):: Re-render the issue title and body from the
|
|
497
|
+
YAML when they differ (YAML wins).
|
|
498
|
+
`--conflict github`:: Leave differing issue content untouched (edits made
|
|
499
|
+
on GitHub win).
|
|
500
|
+
`--conflict skip`:: Take no action on differing content; the result is
|
|
501
|
+
reported as a conflict.
|
|
502
|
+
|
|
503
|
+
By default an open issue whose comment has a recorded disposition
|
|
504
|
+
(`observations`) is closed — that is what keeps the
|
|
505
|
+
`github-retrieve` round-trip working, since observations are retrieved from
|
|
506
|
+
closed issues. Disable with `--no-close-on-disposition`.
|
|
507
|
+
|
|
508
|
+
The milestone is resolved once per run and reused for every issue.
|
|
509
|
+
|
|
467
510
|
==== Retrieving observations from GitHub issues
|
|
468
511
|
|
|
469
512
|
===== Basic usage
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commenter
|
|
4
|
+
# Stage-to-stage comparison of two ballots: how the comment set evolved
|
|
5
|
+
# between e.g. CD and DIS — new comments, withdrawn comments, repeated
|
|
6
|
+
# unchanged comments, and revised ones. Comments are matched by ID (member
|
|
7
|
+
# body + number), which repeats across stages.
|
|
8
|
+
module BallotDiff
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def diff(before_sheet, after_sheet)
|
|
12
|
+
before_comments = indexed(before_sheet)
|
|
13
|
+
after_comments = indexed(after_sheet)
|
|
14
|
+
|
|
15
|
+
result = { new: [], withdrawn: [], repeated: [], revised: [], resolved: [] }
|
|
16
|
+
after_comments.each do |id, after|
|
|
17
|
+
before = before_comments[id]
|
|
18
|
+
if before.nil?
|
|
19
|
+
result[:new] << id
|
|
20
|
+
elsif identical?(before, after)
|
|
21
|
+
result[:repeated] << id
|
|
22
|
+
else
|
|
23
|
+
result[:revised] << id
|
|
24
|
+
end
|
|
25
|
+
result[:resolved] << id if dispositioned?(after)
|
|
26
|
+
end
|
|
27
|
+
before_comments.each_key { |id| result[:withdrawn] << id unless after_comments.key?(id) }
|
|
28
|
+
|
|
29
|
+
result.each_value(&:sort!)
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_markdown(before_sheet, after_sheet)
|
|
34
|
+
data = diff(before_sheet, after_sheet)
|
|
35
|
+
lines = []
|
|
36
|
+
lines << "| Category | Count | Comments |"
|
|
37
|
+
lines << "|----------|-------|----------|"
|
|
38
|
+
data.each do |category, ids|
|
|
39
|
+
label = category.to_s.tr("_", " ").capitalize
|
|
40
|
+
listed = ids.length <= 10 ? ids.join(", ") : "#{ids.first(10).join(", ")} …"
|
|
41
|
+
lines << "| #{label} | #{ids.length} | #{listed} |"
|
|
42
|
+
end
|
|
43
|
+
lines << ""
|
|
44
|
+
lines << "Resolved (disposition recorded in the later stage): #{data[:resolved].length}"
|
|
45
|
+
lines.join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def indexed(sheet)
|
|
49
|
+
sheet.comments.each_with_object({}) do |comment, index|
|
|
50
|
+
key = comment.id.to_s
|
|
51
|
+
index[key] = comment unless key.empty?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def identical?(before, after)
|
|
56
|
+
before.comments == after.comments && before.proposed_change == after.proposed_change
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dispositioned?(comment)
|
|
60
|
+
observation = comment.observations.to_s.strip
|
|
61
|
+
!observation.empty? || DispositionStatus.match(comment.resolution_status)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/commenter/cli.rb
CHANGED
|
@@ -65,6 +65,27 @@ module Commenter
|
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
desc "diff BEFORE.yaml AFTER.yaml", "Compare two ballots (e.g. CD vs DIS) by comment ID"
|
|
69
|
+
option :output, type: :string, aliases: :o, desc: "Output file (default: stdout)"
|
|
70
|
+
option :format, type: :string, default: "markdown", enum: %w[markdown yaml], desc: "Report format"
|
|
71
|
+
def diff(before_yaml, after_yaml)
|
|
72
|
+
before_sheet = CommentSheet.from_yaml(File.read(before_yaml))
|
|
73
|
+
after_sheet = CommentSheet.from_yaml(File.read(after_yaml))
|
|
74
|
+
|
|
75
|
+
report = if options[:format] == "yaml"
|
|
76
|
+
BallotDiff.diff(before_sheet, after_sheet).to_yaml
|
|
77
|
+
else
|
|
78
|
+
BallotDiff.to_markdown(before_sheet, after_sheet)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if options[:output]
|
|
82
|
+
File.write(options[:output], report)
|
|
83
|
+
puts "Wrote comparison to #{options[:output]}"
|
|
84
|
+
else
|
|
85
|
+
puts report
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
68
89
|
desc "fill INPUT.yaml", "Fill DOCX template from YAML comments"
|
|
69
90
|
option :output, type: :string, aliases: :o, default: "filled_comments.docx", desc: "Output DOCX file"
|
|
70
91
|
option :template, type: :string, aliases: :t, desc: "Custom template file"
|
|
@@ -160,6 +181,66 @@ module Commenter
|
|
|
160
181
|
exit 1
|
|
161
182
|
end
|
|
162
183
|
|
|
184
|
+
desc "github-sync INPUT.yaml", "Synchronize comments with GitHub issues (YAML is the source of truth)"
|
|
185
|
+
option :config, type: :string, aliases: :c, required: true, desc: "GitHub configuration YAML file"
|
|
186
|
+
option :output, type: :string, aliases: :o, desc: "Output YAML file (default: update original)"
|
|
187
|
+
option :stage, type: :string, desc: "Override approval stage (WD/CD/DIS/FDIS/PRF/PUB)"
|
|
188
|
+
option :milestone, type: :string, desc: "Override milestone name or number"
|
|
189
|
+
option :assignee, type: :string, desc: "Override assignee GitHub handle"
|
|
190
|
+
option :title_template, type: :string, desc: "Custom title template"
|
|
191
|
+
option :body_template, type: :string, desc: "Custom body template"
|
|
192
|
+
option :dry_run, type: :boolean, desc: "Preview sync actions without applying them"
|
|
193
|
+
option :conflict, type: :string, default: "yaml", enum: %w[yaml github skip],
|
|
194
|
+
desc: "Conflict policy: yaml re-renders the issue, " \
|
|
195
|
+
"github keeps the issue content, skip reports and leaves it"
|
|
196
|
+
option :close_on_disposition, type: :boolean, default: true,
|
|
197
|
+
desc: "Close open issues whose comment has a recorded " \
|
|
198
|
+
"disposition"
|
|
199
|
+
def github_sync(input_yaml)
|
|
200
|
+
sync = GitHubSync.new(options[:config], options[:title_template], options[:body_template])
|
|
201
|
+
|
|
202
|
+
sync_options = {
|
|
203
|
+
stage: options[:stage],
|
|
204
|
+
milestone: options[:milestone],
|
|
205
|
+
assignee: options[:assignee],
|
|
206
|
+
conflict: options[:conflict],
|
|
207
|
+
close_on_disposition: options[:close_on_disposition],
|
|
208
|
+
dry_run: options[:dry_run],
|
|
209
|
+
output: options[:output]
|
|
210
|
+
}.compact
|
|
211
|
+
results = sync.sync(input_yaml, sync_options)
|
|
212
|
+
|
|
213
|
+
if options[:dry_run]
|
|
214
|
+
puts "DRY RUN - Planned sync actions:"
|
|
215
|
+
puts "=" * 50
|
|
216
|
+
results.each do |result|
|
|
217
|
+
issue = result[:issue_number] ? "issue ##{result[:issue_number]}" : "no issue yet"
|
|
218
|
+
puts "#{result[:comment_id]} (#{issue}): #{result[:actions].join(", ")}"
|
|
219
|
+
end
|
|
220
|
+
puts "-" * 30
|
|
221
|
+
puts "Based on the issue state last recorded in the YAML; run without --dry-run for a live pass."
|
|
222
|
+
else
|
|
223
|
+
puts "GitHub sync results:"
|
|
224
|
+
puts "=" * 40
|
|
225
|
+
results.each do |result|
|
|
226
|
+
case result[:status]
|
|
227
|
+
when :created then puts "\u2713 #{result[:comment_id]}: Created issue ##{result[:issue_number]} " \
|
|
228
|
+
"(#{result[:issue_url]})"
|
|
229
|
+
when :updated then puts "\u21bb #{result[:comment_id]}: Updated issue ##{result[:issue_number]} " \
|
|
230
|
+
"(#{result[:actions].join(", ")})"
|
|
231
|
+
when :closed then puts "\u2713 #{result[:comment_id]}: Closed issue ##{result[:issue_number]} (disposition recorded)"
|
|
232
|
+
when :updated_and_closed then puts "\u21bb\u2713 #{result[:comment_id]}: Updated and closed issue ##{result[:issue_number]}"
|
|
233
|
+
when :unchanged then puts "- #{result[:comment_id]}: Issue ##{result[:issue_number]} in sync"
|
|
234
|
+
when :error then puts "\u2717 #{result[:comment_id]}: Error - #{result[:message]}"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
puts "Updated YAML file: #{options[:output] || input_yaml}"
|
|
238
|
+
end
|
|
239
|
+
rescue StandardError => e
|
|
240
|
+
puts "Error: #{e.message}"
|
|
241
|
+
exit 1
|
|
242
|
+
end
|
|
243
|
+
|
|
163
244
|
desc "github-retrieve INPUT.yaml", "Retrieve observations from GitHub issues"
|
|
164
245
|
option :config, type: :string, aliases: :c, required: true, desc: "GitHub configuration YAML file"
|
|
165
246
|
option :output, type: :string, aliases: :o, desc: "Output YAML file (default: update original)"
|
|
@@ -250,8 +250,12 @@ module Commenter
|
|
|
250
250
|
puts "[GitHubIssueCreator] Using milestone number: #{milestone_config["number"]}"
|
|
251
251
|
milestone_config["number"]
|
|
252
252
|
elsif milestone_config["name"]
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
# Resolved once per run and memoized: resolving per comment caused
|
|
254
|
+
# duplicated milestone fetches (see 3a06c3d).
|
|
255
|
+
@milestone_number ||= begin
|
|
256
|
+
puts "[GitHubIssueCreator] Using milestone name: #{milestone_config["name"]}"
|
|
257
|
+
resolve_milestone_by_name_or_number(milestone_config["name"])
|
|
258
|
+
end
|
|
255
259
|
end
|
|
256
260
|
end
|
|
257
261
|
|
|
@@ -276,13 +280,14 @@ module Commenter
|
|
|
276
280
|
def update_yaml_with_github_info(yaml_file, comment_sheet, results, options)
|
|
277
281
|
# Update comments with GitHub information
|
|
278
282
|
results.each do |result|
|
|
279
|
-
next unless result[:
|
|
283
|
+
next unless result[:issue_number]
|
|
280
284
|
|
|
281
285
|
comment = comment_sheet.comments.find { |c| c.id == result[:comment_id] }
|
|
282
286
|
next unless comment
|
|
283
287
|
|
|
284
288
|
comment.record_github_issue(issue_number: result[:issue_number], issue_url: result[:issue_url],
|
|
285
|
-
status: "open"
|
|
289
|
+
status: result[:status] == :created ? "open" : comment.github_status,
|
|
290
|
+
created_at: comment.github_created_at || Time.now.utc.iso8601)
|
|
286
291
|
end
|
|
287
292
|
|
|
288
293
|
# Write updated YAML
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commenter
|
|
4
|
+
# Synchronizes the comment YAML with GitHub issues, treating the YAML as
|
|
5
|
+
# the source of truth: comments without an issue get one, issue content is
|
|
6
|
+
# refreshed from the YAML (per the conflict policy), and issues whose
|
|
7
|
+
# comment has a recorded disposition can be closed.
|
|
8
|
+
class GitHubSync < GitHubIssueCreator
|
|
9
|
+
# Returns the sync actions for one comment by comparing the rendered
|
|
10
|
+
# issue content against the issue's current state. Pure — no network —
|
|
11
|
+
# so the conflict matrix is directly specifiable.
|
|
12
|
+
#
|
|
13
|
+
# conflict policy:
|
|
14
|
+
# yaml - re-render title/body over the issue (YAML wins)
|
|
15
|
+
# github - leave differing issue content untouched (GitHub wins)
|
|
16
|
+
# skip - take no action on differing content, report the conflict
|
|
17
|
+
def self.reconcile_actions(issue:, rendered:, disposition:, conflict: "yaml",
|
|
18
|
+
close_on_disposition: true)
|
|
19
|
+
actions = []
|
|
20
|
+
differing = [rendered[:title].to_s.strip, rendered[:body].to_s.strip] !=
|
|
21
|
+
[issue[:title].to_s.strip, issue[:body].to_s.strip]
|
|
22
|
+
|
|
23
|
+
if differing
|
|
24
|
+
case conflict
|
|
25
|
+
when "yaml" then actions << :update_content
|
|
26
|
+
when "github" then actions << :keep_github_content
|
|
27
|
+
when "skip" then actions << :conflict
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
actions << :close if close_on_disposition && issue[:state] == "open" && disposition && !disposition.strip.empty?
|
|
32
|
+
|
|
33
|
+
actions
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def sync(yaml_file, options = {})
|
|
37
|
+
comment_sheet = CommentSheet.from_yaml(File.read(yaml_file))
|
|
38
|
+
comment_sheet.stage = options[:stage] if options[:stage]
|
|
39
|
+
|
|
40
|
+
results = comment_sheet.comments.map do |comment|
|
|
41
|
+
options[:dry_run] ? plan(comment, options) : reconcile(comment, comment_sheet, options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
update_yaml_with_github_info(yaml_file, comment_sheet, results, options) unless options[:dry_run]
|
|
45
|
+
results
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Dry-run preview, offline: comments with a known issue are planned from
|
|
51
|
+
# the last state recorded in the YAML, not from a live fetch.
|
|
52
|
+
def plan(comment, options)
|
|
53
|
+
return { comment_id: comment.id, actions: [:create] } unless comment.has_github_issue?
|
|
54
|
+
|
|
55
|
+
actions = [:reconcile]
|
|
56
|
+
if close_on_disposition?(options) && comment.github_status == "open" &&
|
|
57
|
+
comment.observations && !comment.observations.strip.empty?
|
|
58
|
+
actions << :close
|
|
59
|
+
end
|
|
60
|
+
{ comment_id: comment.id, issue_number: comment.github_issue_number, actions: actions }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reconcile(comment, comment_sheet, options)
|
|
64
|
+
issue = find_existing_issue(comment, comment_sheet)
|
|
65
|
+
return create_issue(comment, comment_sheet, options) unless issue
|
|
66
|
+
|
|
67
|
+
title = @title_template.render(template_variables(comment, comment_sheet))
|
|
68
|
+
body = @body_template.render(template_variables(comment, comment_sheet))
|
|
69
|
+
actions = self.class.reconcile_actions(
|
|
70
|
+
issue: { title: issue.title, body: issue.body, state: issue.state },
|
|
71
|
+
rendered: { title: title, body: body },
|
|
72
|
+
disposition: comment.observations,
|
|
73
|
+
conflict: options[:conflict] || "yaml",
|
|
74
|
+
close_on_disposition: close_on_disposition?(options)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
apply(comment, issue, title, body, actions)
|
|
78
|
+
rescue Octokit::Error => e
|
|
79
|
+
{ comment_id: comment.id, status: :error, message: e.message }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def apply(comment, issue, title, body, actions)
|
|
83
|
+
result = { comment_id: comment.id, issue_number: issue.number, status: :unchanged, actions: actions }
|
|
84
|
+
comment.record_github_issue(issue_number: issue.number, issue_url: issue.html_url, status: issue.state)
|
|
85
|
+
|
|
86
|
+
if actions.include?(:update_content)
|
|
87
|
+
@github_client.update_issue(@repo, issue.number, title: title, body: body)
|
|
88
|
+
result[:status] = :updated
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if actions.include?(:close)
|
|
92
|
+
@github_client.close_issue(@repo, issue.number)
|
|
93
|
+
comment.github.status = "closed"
|
|
94
|
+
comment.github.updated_at = Time.now.utc.iso8601
|
|
95
|
+
result[:status] = actions.include?(:update_content) ? :updated_and_closed : :closed
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
result
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def close_on_disposition?(options)
|
|
102
|
+
options.fetch(:close_on_disposition, true)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/commenter/version.rb
CHANGED
data/lib/commenter.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Commenter
|
|
|
13
13
|
autoload :DispositionStatus, "commenter/disposition_status"
|
|
14
14
|
autoload :Ballot, "commenter/ballot"
|
|
15
15
|
autoload :BallotReport, "commenter/ballot_report"
|
|
16
|
+
autoload :BallotDiff, "commenter/ballot_diff"
|
|
16
17
|
autoload :Comment, "commenter/comment"
|
|
17
18
|
autoload :CommentSheet, "commenter/comment_sheet"
|
|
18
19
|
autoload :GitHubSession, "commenter/github_session"
|
|
@@ -20,4 +21,5 @@ module Commenter
|
|
|
20
21
|
autoload :Filler, "commenter/filler"
|
|
21
22
|
autoload :GitHubIssueCreator, "commenter/github_integration"
|
|
22
23
|
autoload :GitHubIssueRetriever, "commenter/github_integration"
|
|
24
|
+
autoload :GitHubSync, "commenter/github_sync"
|
|
23
25
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Commenter::BallotDiff do
|
|
6
|
+
def sheet(comments)
|
|
7
|
+
Commenter::CommentSheet.new(comments: comments)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def comment(id, text: "Text for #{id}", observations: nil)
|
|
11
|
+
Commenter::Comment.new(id: id, body: id.split("-").first, comments: text, observations: observations)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
let(:cd) do
|
|
15
|
+
sheet([
|
|
16
|
+
comment("DE-001"),
|
|
17
|
+
comment("DE-002", text: "Old wording"),
|
|
18
|
+
comment("US-001")
|
|
19
|
+
])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:dis) do
|
|
23
|
+
sheet([
|
|
24
|
+
comment("DE-001"),
|
|
25
|
+
comment("DE-002", text: "Revised wording"),
|
|
26
|
+
comment("US-001", observations: "Accepted."),
|
|
27
|
+
comment("JP-001")
|
|
28
|
+
])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "classifies new, withdrawn, repeated, and revised comments" do
|
|
32
|
+
result = described_class.diff(cd, dis)
|
|
33
|
+
|
|
34
|
+
expect(result[:new]).to eq(["JP-001"])
|
|
35
|
+
expect(result[:withdrawn]).to eq([]) # US-001 present in both
|
|
36
|
+
expect(result[:repeated]).to eq(%w[DE-001 US-001])
|
|
37
|
+
expect(result[:revised]).to eq(["DE-002"])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "counts comments with a disposition in the later stage as resolved" do
|
|
41
|
+
result = described_class.diff(cd, dis)
|
|
42
|
+
|
|
43
|
+
expect(result[:resolved]).to eq(["US-001"])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "reports withdrawn comments" do
|
|
47
|
+
result = described_class.diff(dis, cd)
|
|
48
|
+
|
|
49
|
+
expect(result[:withdrawn]).to eq(["JP-001"])
|
|
50
|
+
expect(result[:new]).to eq([])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "renders a markdown table" do
|
|
54
|
+
markdown = described_class.to_markdown(cd, dis)
|
|
55
|
+
|
|
56
|
+
expect(markdown).to start_with("| Category | Count | Comments |")
|
|
57
|
+
expect(markdown).to include("| New | 1 | JP-001 |")
|
|
58
|
+
expect(markdown).to include("| Revised | 1 | DE-002 |")
|
|
59
|
+
expect(markdown).to include("Resolved (disposition recorded in the later stage): 1")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Commenter::GitHubSync do
|
|
7
|
+
describe ".reconcile_actions" do
|
|
8
|
+
def actions(**overrides)
|
|
9
|
+
described_class.reconcile_actions(
|
|
10
|
+
issue: { title: "Title", body: "Body", state: "open" },
|
|
11
|
+
rendered: { title: "Title", body: "Body" },
|
|
12
|
+
disposition: nil,
|
|
13
|
+
**overrides
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "reports nothing when the issue is in sync" do
|
|
18
|
+
expect(actions).to eq([])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "re-renders differing content under the yaml policy" do
|
|
22
|
+
expect(actions(conflict: "yaml", issue: { title: "Title", body: "Old body", state: "open" })).to eq([:update_content])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "keeps differing GitHub content under the github policy" do
|
|
26
|
+
expect(actions(conflict: "github", issue: { title: "Title", body: "Edited on GitHub", state: "open" }))
|
|
27
|
+
.to eq([:keep_github_content])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "reports a conflict under the skip policy" do
|
|
31
|
+
expect(actions(conflict: "skip", issue: { title: "Edited on GitHub", body: "Body", state: "open" })).to eq([:conflict])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "closes an open issue whose comment has a disposition" do
|
|
35
|
+
expect(actions(disposition: "Accepted.")).to eq([:close])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "combines content update and close" do
|
|
39
|
+
expect(actions(issue: { title: "Title", body: "Old", state: "open" }, disposition: "Accepted.")).to eq(%i[update_content close])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "does not close a closed issue, an undispositioned comment, or when disabled" do
|
|
43
|
+
expect(actions(disposition: "Accepted.", issue: { title: "Title", body: "Body", state: "closed" })).to eq([])
|
|
44
|
+
expect(actions(disposition: " ")).to eq([])
|
|
45
|
+
expect(actions(disposition: "Accepted.", close_on_disposition: false)).to eq([])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "ignores whitespace when comparing content" do
|
|
49
|
+
expect(actions(issue: { title: "Title", body: " Body ", state: "open" })).to eq([])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "#sync dry run" do
|
|
54
|
+
let(:config_file) do
|
|
55
|
+
file = Tempfile.new(["config", ".yaml"])
|
|
56
|
+
file.write({ "github" => { "repository" => "test-org/test-repo", "token" => "test-token" } }.to_yaml)
|
|
57
|
+
file.close
|
|
58
|
+
file
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def yaml_file(comments)
|
|
62
|
+
file = Tempfile.new(["comments", ".yaml"])
|
|
63
|
+
file.write({ "version" => "2012-03", "stage" => "DIS", "comments" => comments }.to_yaml)
|
|
64
|
+
file.close
|
|
65
|
+
file
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
after { config_file.unlink }
|
|
69
|
+
|
|
70
|
+
it "plans creation for comments without issues and closure for dispositioned open ones" do
|
|
71
|
+
input = yaml_file([
|
|
72
|
+
{ "id" => "US-001", "body" => "US", "comments" => "No issue yet" },
|
|
73
|
+
{ "id" => "US-002", "body" => "US", "comments" => "Resolved", "observations" => "Accepted.",
|
|
74
|
+
"github" => { "issue_number" => 12, "status" => "open" } }
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
sync = described_class.new(config_file.path)
|
|
78
|
+
results = sync.sync(input.path, dry_run: true)
|
|
79
|
+
|
|
80
|
+
expect(results[0]).to eq(comment_id: "US-001", actions: [:create])
|
|
81
|
+
expect(results[1][:issue_number]).to eq(12)
|
|
82
|
+
expect(results[1][:actions]).to include(:close)
|
|
83
|
+
|
|
84
|
+
input.unlink
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: commenter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -152,6 +152,7 @@ files:
|
|
|
152
152
|
- exe/commenter
|
|
153
153
|
- lib/commenter.rb
|
|
154
154
|
- lib/commenter/ballot.rb
|
|
155
|
+
- lib/commenter/ballot_diff.rb
|
|
155
156
|
- lib/commenter/ballot_report.rb
|
|
156
157
|
- lib/commenter/cli.rb
|
|
157
158
|
- lib/commenter/comment.rb
|
|
@@ -163,6 +164,7 @@ files:
|
|
|
163
164
|
- lib/commenter/github_info.rb
|
|
164
165
|
- lib/commenter/github_integration.rb
|
|
165
166
|
- lib/commenter/github_session.rb
|
|
167
|
+
- lib/commenter/github_sync.rb
|
|
166
168
|
- lib/commenter/parser.rb
|
|
167
169
|
- lib/commenter/parser/osd_xlsx_parser.rb
|
|
168
170
|
- lib/commenter/parser/track_change_docx_parser.rb
|
|
@@ -170,6 +172,7 @@ files:
|
|
|
170
172
|
- schema/iso_comment_2012-03.yaml
|
|
171
173
|
- schema/iso_comment_osd.yaml
|
|
172
174
|
- sig/commenter.rbs
|
|
175
|
+
- spec/commenter/ballot_diff_spec.rb
|
|
173
176
|
- spec/commenter/ballot_report_spec.rb
|
|
174
177
|
- spec/commenter/ballot_spec.rb
|
|
175
178
|
- spec/commenter/cli_spec.rb
|
|
@@ -179,6 +182,7 @@ files:
|
|
|
179
182
|
- spec/commenter/disposition_status_spec.rb
|
|
180
183
|
- spec/commenter/filler_spec.rb
|
|
181
184
|
- spec/commenter/github_integration_spec.rb
|
|
185
|
+
- spec/commenter/github_sync_spec.rb
|
|
182
186
|
- spec/commenter/osd_xlsx_parser_spec.rb
|
|
183
187
|
- spec/commenter/track_change_docx_parser_spec.rb
|
|
184
188
|
- spec/commenter_spec.rb
|