px_github_changelog_generator 0.0.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 +7 -0
- data/LICENSE +9 -0
- data/README.md +357 -0
- data/Rakefile +19 -0
- data/bin/git-generate-changelog +5 -0
- data/bin/github_changelog_generator +5 -0
- data/lib/github_changelog_generator/argv_parser.rb +225 -0
- data/lib/github_changelog_generator/file_parser_chooser.rb +27 -0
- data/lib/github_changelog_generator/generator/entry.rb +218 -0
- data/lib/github_changelog_generator/generator/generator.rb +177 -0
- data/lib/github_changelog_generator/generator/generator_fetcher.rb +202 -0
- data/lib/github_changelog_generator/generator/generator_processor.rb +237 -0
- data/lib/github_changelog_generator/generator/generator_tags.rb +210 -0
- data/lib/github_changelog_generator/generator/section.rb +129 -0
- data/lib/github_changelog_generator/helper.rb +37 -0
- data/lib/github_changelog_generator/octo_fetcher.rb +535 -0
- data/lib/github_changelog_generator/options.rb +159 -0
- data/lib/github_changelog_generator/parser.rb +89 -0
- data/lib/github_changelog_generator/parser_file.rb +101 -0
- data/lib/github_changelog_generator/reader.rb +88 -0
- data/lib/github_changelog_generator/ssl_certs/cacert.pem +3138 -0
- data/lib/github_changelog_generator/task.rb +68 -0
- data/lib/github_changelog_generator/version.rb +5 -0
- data/lib/github_changelog_generator.rb +49 -0
- data/man/git-generate-changelog.1 +393 -0
- data/man/git-generate-changelog.1.html +359 -0
- data/man/git-generate-changelog.html +270 -0
- data/man/git-generate-changelog.md +274 -0
- data/spec/files/angular.js.md +9395 -0
- data/spec/files/bundler.md +1911 -0
- data/spec/files/config_example +5 -0
- data/spec/files/github-changelog-generator.md +305 -0
- data/spec/github_changelog_generator_spec.rb +32 -0
- data/spec/install_gem_in_bundler.gemfile +5 -0
- data/spec/spec_helper.rb +74 -0
- data/spec/unit/generator/entry_spec.rb +766 -0
- data/spec/unit/generator/generator_processor_spec.rb +203 -0
- data/spec/unit/generator/generator_spec.rb +47 -0
- data/spec/unit/generator/generator_tags_spec.rb +337 -0
- data/spec/unit/generator/section_spec.rb +43 -0
- data/spec/unit/octo_fetcher_spec.rb +590 -0
- data/spec/unit/options_spec.rb +67 -0
- data/spec/unit/parser_file_spec.rb +94 -0
- data/spec/unit/parser_spec.rb +54 -0
- data/spec/unit/reader_spec.rb +120 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits/when_API_is_valid/returns_commits.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits_before/when_API_is_valid/returns_commits.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issue_with_proper_key/values.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues_with_labels.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_request_with_proper_key/values.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_requests_with_labels.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_correct_pull_request_keys.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_pull_requests.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid/returns_commit.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid/returns_date.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid/populates_issues.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags_count.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided/should_raise_Unauthorized_error.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided.json +1 -0
- metadata +250 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "github_changelog_generator/generator/section"
|
|
4
|
+
|
|
5
|
+
module GitHubChangelogGenerator
|
|
6
|
+
# This class generates the content for a single changelog entry. An entry is
|
|
7
|
+
# generally either for a specific tagged release or the collection of
|
|
8
|
+
# unreleased changes.
|
|
9
|
+
#
|
|
10
|
+
# An entry is comprised of header text followed by a series of sections
|
|
11
|
+
# relating to the entry.
|
|
12
|
+
#
|
|
13
|
+
# @see GitHubChangelogGenerator::Generator
|
|
14
|
+
# @see GitHubChangelogGenerator::Section
|
|
15
|
+
class Entry
|
|
16
|
+
attr_reader :content
|
|
17
|
+
|
|
18
|
+
def initialize(options = Options.new({}))
|
|
19
|
+
@content = ""
|
|
20
|
+
@options = Options.new(options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Generates log entry with header and body
|
|
24
|
+
#
|
|
25
|
+
# @param [Array] pull_requests List or PR's in new section
|
|
26
|
+
# @param [Array] issues List of issues in new section
|
|
27
|
+
# @param [String] newer_tag_name Name of the newer tag. Could be nil for `Unreleased` section.
|
|
28
|
+
# @param [String] newer_tag_link Name of the newer tag. Could be "HEAD" for `Unreleased` section.
|
|
29
|
+
# @param [Time] newer_tag_time Time of the newer tag
|
|
30
|
+
# @param [Hash, nil] older_tag_name Older tag, used for the links. Could be nil for last tag.
|
|
31
|
+
# @return [String] Ready and parsed section content.
|
|
32
|
+
def generate_entry_for_tag(pull_requests, issues, newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name) # rubocop:disable Metrics/ParameterLists
|
|
33
|
+
github_site = @options[:github_site] || "https://github.com"
|
|
34
|
+
project_url = "#{github_site}/#{@options[:user]}/#{@options[:project]}"
|
|
35
|
+
|
|
36
|
+
create_sections
|
|
37
|
+
|
|
38
|
+
@content = generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url)
|
|
39
|
+
@content += generate_body(pull_requests, issues)
|
|
40
|
+
@content
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def line_labels_for(issue)
|
|
44
|
+
labels = if @options[:issue_line_labels] == ["ALL"]
|
|
45
|
+
issue["labels"]
|
|
46
|
+
else
|
|
47
|
+
issue["labels"].select { |label| @options[:issue_line_labels].include?(label["name"]) }
|
|
48
|
+
end
|
|
49
|
+
labels.map { |label| " \[[#{label['name']}](#{label['url'].sub('api.github.com/repos', 'github.com')})\]" }.join("")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Creates section objects for this entry.
|
|
55
|
+
# @return [Nil]
|
|
56
|
+
def create_sections
|
|
57
|
+
@sections = if @options.configure_sections?
|
|
58
|
+
parse_sections(@options[:configure_sections])
|
|
59
|
+
elsif @options.add_sections?
|
|
60
|
+
default_sections.concat parse_sections(@options[:add_sections])
|
|
61
|
+
else
|
|
62
|
+
default_sections
|
|
63
|
+
end
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Turns the argument from the commandline of --configure-sections or
|
|
68
|
+
# --add-sections into an array of Section objects.
|
|
69
|
+
#
|
|
70
|
+
# @param [String, Hash] sections_desc Either string or hash describing sections
|
|
71
|
+
# @return [Array] Parsed section objects.
|
|
72
|
+
def parse_sections(sections_desc)
|
|
73
|
+
require "json"
|
|
74
|
+
|
|
75
|
+
sections_desc = sections_desc.to_json if sections_desc.class == Hash
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
sections_json = JSON.parse(sections_desc)
|
|
79
|
+
rescue JSON::ParserError => e
|
|
80
|
+
raise "There was a problem parsing your JSON string for sections: #{e}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sections_json.collect do |name, v|
|
|
84
|
+
Section.new(name: name.to_s, prefix: v["prefix"], labels: v["labels"], body_only: v["body_only"], options: @options)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Generates header text for an entry.
|
|
89
|
+
#
|
|
90
|
+
# @param [String] newer_tag_name The name of a newer tag
|
|
91
|
+
# @param [String] newer_tag_link Used for URL generation. Could be same as #newer_tag_name or some specific value, like HEAD
|
|
92
|
+
# @param [Time] newer_tag_time Time when the newer tag was created
|
|
93
|
+
# @param [String] older_tag_name The name of an older tag; used for URLs.
|
|
94
|
+
# @param [String] project_url URL for the current project.
|
|
95
|
+
# @return [String] Header text content.
|
|
96
|
+
def generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url)
|
|
97
|
+
header = ""
|
|
98
|
+
|
|
99
|
+
# Generate date string:
|
|
100
|
+
time_string = newer_tag_time.strftime(@options[:date_format])
|
|
101
|
+
|
|
102
|
+
# Generate tag name and link
|
|
103
|
+
release_url = if @options[:release_url]
|
|
104
|
+
format(@options[:release_url], newer_tag_link)
|
|
105
|
+
else
|
|
106
|
+
"#{project_url}/tree/#{newer_tag_link}"
|
|
107
|
+
end
|
|
108
|
+
header += if newer_tag_name.equal?(@options[:unreleased_label])
|
|
109
|
+
"## [#{newer_tag_name}](#{release_url})\n\n"
|
|
110
|
+
else
|
|
111
|
+
"## [#{newer_tag_name}](#{release_url}) (#{time_string})\n\n"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if @options[:compare_link] && older_tag_name
|
|
115
|
+
# Generate compare link
|
|
116
|
+
header += "[Full Changelog](#{project_url}/compare/#{older_tag_name}...#{newer_tag_link})\n\n"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
header
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Generates complete body text for a tag (without a header)
|
|
123
|
+
#
|
|
124
|
+
# @param [Array] pull_requests
|
|
125
|
+
# @param [Array] issues
|
|
126
|
+
# @return [String] Content generated from sections of sorted issues & PRs.
|
|
127
|
+
def generate_body(pull_requests, issues)
|
|
128
|
+
sort_into_sections(pull_requests, issues)
|
|
129
|
+
@sections.map(&:generate_content).join
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Default sections to used when --configure-sections is not set.
|
|
133
|
+
#
|
|
134
|
+
# @return [Array] Section objects.
|
|
135
|
+
def default_sections
|
|
136
|
+
[
|
|
137
|
+
Section.new(name: "summary", prefix: @options[:summary_prefix], labels: @options[:summary_labels], options: @options, body_only: true),
|
|
138
|
+
Section.new(name: "breaking", prefix: @options[:breaking_prefix], labels: @options[:breaking_labels], options: @options),
|
|
139
|
+
Section.new(name: "enhancements", prefix: @options[:enhancement_prefix], labels: @options[:enhancement_labels], options: @options),
|
|
140
|
+
Section.new(name: "bugs", prefix: @options[:bug_prefix], labels: @options[:bug_labels], options: @options),
|
|
141
|
+
Section.new(name: "deprecated", prefix: @options[:deprecated_prefix], labels: @options[:deprecated_labels], options: @options),
|
|
142
|
+
Section.new(name: "removed", prefix: @options[:removed_prefix], labels: @options[:removed_labels], options: @options),
|
|
143
|
+
Section.new(name: "security", prefix: @options[:security_prefix], labels: @options[:security_labels], options: @options)
|
|
144
|
+
]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Sorts issues and PRs into entry sections by labels and lack of labels.
|
|
148
|
+
#
|
|
149
|
+
# @param [Array] pull_requests
|
|
150
|
+
# @param [Array] issues
|
|
151
|
+
# @return [Nil]
|
|
152
|
+
def sort_into_sections(pull_requests, issues)
|
|
153
|
+
if @options[:issues]
|
|
154
|
+
unmapped_issues = sort_labeled_issues(issues)
|
|
155
|
+
add_unmapped_section(unmapped_issues)
|
|
156
|
+
end
|
|
157
|
+
if @options[:pulls]
|
|
158
|
+
unmapped_pull_requests = sort_labeled_issues(pull_requests)
|
|
159
|
+
add_unmapped_section(unmapped_pull_requests)
|
|
160
|
+
end
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Iterates through sections and sorts labeled issues into them based on
|
|
165
|
+
# the label mapping. Returns any unmapped or unlabeled issues.
|
|
166
|
+
#
|
|
167
|
+
# @param [Array] issues Issues or pull requests.
|
|
168
|
+
# @return [Array] Issues that were not mapped into any sections.
|
|
169
|
+
def sort_labeled_issues(issues)
|
|
170
|
+
sorted_issues = []
|
|
171
|
+
issues.each do |issue|
|
|
172
|
+
label_names = issue["labels"].collect { |l| l["name"] }
|
|
173
|
+
|
|
174
|
+
# Add PRs in the order of the @sections array. This will either be the
|
|
175
|
+
# default sections followed by any --add-sections sections in
|
|
176
|
+
# user-defined order, or --configure-sections in user-defined order.
|
|
177
|
+
# Ignore the order of the issue labels from github which cannot be
|
|
178
|
+
# controlled by the user.
|
|
179
|
+
@sections.each do |section|
|
|
180
|
+
unless (section.labels & label_names).empty?
|
|
181
|
+
section.issues << issue
|
|
182
|
+
sorted_issues << issue
|
|
183
|
+
break
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
issues - sorted_issues
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Creates a section for issues/PRs with no labels or no mapped labels.
|
|
191
|
+
#
|
|
192
|
+
# @param [Array] issues
|
|
193
|
+
# @return [Nil]
|
|
194
|
+
def add_unmapped_section(issues)
|
|
195
|
+
unless issues.empty?
|
|
196
|
+
# Distinguish between issues and pull requests
|
|
197
|
+
if issues.first.key?("pull_request")
|
|
198
|
+
name = "merged"
|
|
199
|
+
prefix = @options[:merge_prefix]
|
|
200
|
+
add_wo_labels = @options[:add_pr_wo_labels]
|
|
201
|
+
else
|
|
202
|
+
name = "issues"
|
|
203
|
+
prefix = @options[:issue_prefix]
|
|
204
|
+
add_wo_labels = @options[:add_issues_wo_labels]
|
|
205
|
+
end
|
|
206
|
+
add_issues = if add_wo_labels
|
|
207
|
+
issues
|
|
208
|
+
else
|
|
209
|
+
# Only add unmapped issues
|
|
210
|
+
issues.select { |issue| issue["labels"].any? }
|
|
211
|
+
end
|
|
212
|
+
merged = Section.new(name: name, prefix: prefix, labels: [], issues: add_issues, options: @options) unless add_issues.empty?
|
|
213
|
+
@sections << merged
|
|
214
|
+
end
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "github_changelog_generator/octo_fetcher"
|
|
4
|
+
require "github_changelog_generator/generator/generator_fetcher"
|
|
5
|
+
require "github_changelog_generator/generator/generator_processor"
|
|
6
|
+
require "github_changelog_generator/generator/generator_tags"
|
|
7
|
+
require "github_changelog_generator/generator/entry"
|
|
8
|
+
require "github_changelog_generator/generator/section"
|
|
9
|
+
|
|
10
|
+
module GitHubChangelogGenerator
|
|
11
|
+
# Default error for ChangelogGenerator
|
|
12
|
+
class ChangelogGeneratorError < StandardError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# This class is the high-level code for gathering issues and PRs for a github
|
|
16
|
+
# repository and generating a CHANGELOG.md file. A changelog is made up of a
|
|
17
|
+
# series of "entries" of all tagged releases, plus an extra entry for the
|
|
18
|
+
# unreleased changes. Entries are made up of various organizational
|
|
19
|
+
# "sections," and sections contain the github issues and PRs.
|
|
20
|
+
#
|
|
21
|
+
# So the changelog contains entries, entries contain sections, and sections
|
|
22
|
+
# contain issues and PRs.
|
|
23
|
+
#
|
|
24
|
+
# @see GitHubChangelogGenerator::Entry
|
|
25
|
+
# @see GitHubChangelogGenerator::Section
|
|
26
|
+
class Generator
|
|
27
|
+
attr_accessor :options, :filtered_tags, :tag_section_mapping, :sorted_tags
|
|
28
|
+
|
|
29
|
+
CREDIT_LINE = <<~CREDIT
|
|
30
|
+
\\* *[Test] This Changelog was automatically generated \
|
|
31
|
+
by [github_changelog_generator]\
|
|
32
|
+
(https://github.com/github-changelog-generator/github-changelog-generator)*
|
|
33
|
+
CREDIT
|
|
34
|
+
|
|
35
|
+
# A Generator responsible for all logic, related with changelog generation from ready-to-parse issues
|
|
36
|
+
#
|
|
37
|
+
# Example:
|
|
38
|
+
# generator = GitHubChangelogGenerator::Generator.new
|
|
39
|
+
# content = generator.compound_changelog
|
|
40
|
+
def initialize(options = {})
|
|
41
|
+
@options = options
|
|
42
|
+
@tag_times_hash = {}
|
|
43
|
+
@fetcher = GitHubChangelogGenerator::OctoFetcher.new(options)
|
|
44
|
+
@sections = []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Main function to start changelog generation
|
|
48
|
+
#
|
|
49
|
+
# @return [String] Generated changelog file
|
|
50
|
+
def compound_changelog
|
|
51
|
+
@options.load_custom_ruby_files
|
|
52
|
+
|
|
53
|
+
Sync do
|
|
54
|
+
fetch_and_filter_tags
|
|
55
|
+
fetch_issues_and_pr
|
|
56
|
+
|
|
57
|
+
log = if @options[:unreleased_only]
|
|
58
|
+
generate_entry_between_tags(@filtered_tags[0], nil)
|
|
59
|
+
else
|
|
60
|
+
generate_entries_for_all_tags
|
|
61
|
+
end
|
|
62
|
+
log += File.read(@options[:base]) if File.file?(@options[:base])
|
|
63
|
+
log = remove_old_fixed_string(log)
|
|
64
|
+
log = insert_fixed_string(log)
|
|
65
|
+
@log = log
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Generate log only between 2 specified tags
|
|
72
|
+
# @param [String] older_tag all issues before this tag date will be excluded. May be nil, if it's first tag
|
|
73
|
+
# @param [String] newer_tag all issue after this tag will be excluded. May be nil for unreleased section
|
|
74
|
+
def generate_entry_between_tags(older_tag, newer_tag)
|
|
75
|
+
filtered_issues, filtered_pull_requests = filter_issues_for_tags(newer_tag, older_tag)
|
|
76
|
+
|
|
77
|
+
if newer_tag.nil? && filtered_issues.empty? && filtered_pull_requests.empty?
|
|
78
|
+
# do not generate empty unreleased section
|
|
79
|
+
return +""
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
newer_tag_link, newer_tag_name, newer_tag_time = detect_link_tag_time(newer_tag)
|
|
83
|
+
|
|
84
|
+
# If the older tag is nil, go back in time from the latest tag and find
|
|
85
|
+
# the SHA for the first commit.
|
|
86
|
+
older_tag_name =
|
|
87
|
+
if older_tag.nil?
|
|
88
|
+
@fetcher.oldest_commit["sha"]
|
|
89
|
+
else
|
|
90
|
+
older_tag["name"]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
Entry.new(options).generate_entry_for_tag(filtered_pull_requests, filtered_issues, newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Filters issues and pull requests based on, respectively, `actual_date`
|
|
97
|
+
# and `merged_at` timestamp fields. `actual_date` is the detected form of
|
|
98
|
+
# `closed_at` based on merge event SHA commit times.
|
|
99
|
+
#
|
|
100
|
+
# @return [Array] filtered issues and pull requests
|
|
101
|
+
def filter_issues_for_tags(newer_tag, older_tag)
|
|
102
|
+
filtered_pull_requests = filter_by_tag(@pull_requests, newer_tag)
|
|
103
|
+
filtered_issues = delete_by_time(@issues, "actual_date", older_tag, newer_tag)
|
|
104
|
+
|
|
105
|
+
newer_tag_name = newer_tag.nil? ? nil : newer_tag["name"]
|
|
106
|
+
|
|
107
|
+
if options[:filter_issues_by_milestone]
|
|
108
|
+
# delete excess irrelevant issues (according milestones). Issue #22.
|
|
109
|
+
filtered_issues = filter_by_milestone(filtered_issues, newer_tag_name, @issues)
|
|
110
|
+
filtered_pull_requests = filter_by_milestone(filtered_pull_requests, newer_tag_name, @pull_requests)
|
|
111
|
+
end
|
|
112
|
+
[filtered_issues, filtered_pull_requests]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The full cycle of generation for whole project
|
|
116
|
+
# @return [String] All entries in the changelog
|
|
117
|
+
def generate_entries_for_all_tags
|
|
118
|
+
puts "Generating entry..." if options[:verbose]
|
|
119
|
+
|
|
120
|
+
entries = generate_unreleased_entry
|
|
121
|
+
|
|
122
|
+
@tag_section_mapping.each_pair do |_tag_section, left_right_tags|
|
|
123
|
+
older_tag, newer_tag = left_right_tags
|
|
124
|
+
entries += generate_entry_between_tags(older_tag, newer_tag)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
entries
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def generate_unreleased_entry
|
|
131
|
+
entry = +""
|
|
132
|
+
if options[:unreleased]
|
|
133
|
+
start_tag = @filtered_tags[0] || @sorted_tags.last
|
|
134
|
+
unreleased_entry = generate_entry_between_tags(start_tag, nil)
|
|
135
|
+
entry += unreleased_entry if unreleased_entry
|
|
136
|
+
end
|
|
137
|
+
entry
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Fetches @pull_requests and @issues and filters them based on options.
|
|
141
|
+
#
|
|
142
|
+
# @return [Nil] No return.
|
|
143
|
+
def fetch_issues_and_pr
|
|
144
|
+
issues, pull_requests = @fetcher.fetch_closed_issues_and_pr
|
|
145
|
+
|
|
146
|
+
@pull_requests = options[:pulls] ? get_filtered_pull_requests(pull_requests) : []
|
|
147
|
+
|
|
148
|
+
@issues = options[:issues] ? get_filtered_issues(issues) : []
|
|
149
|
+
|
|
150
|
+
fetch_events_for_issues_and_pr
|
|
151
|
+
detect_actual_closed_dates(@issues + @pull_requests)
|
|
152
|
+
add_first_occurring_tag_to_prs(@sorted_tags, @pull_requests)
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Remove the previously assigned fixed message.
|
|
157
|
+
# @param log [String] Old lines are fixed
|
|
158
|
+
def remove_old_fixed_string(log)
|
|
159
|
+
log.gsub!(/#{Regexp.escape(@options[:frontmatter])}/, "") if @options[:frontmatter]
|
|
160
|
+
log.gsub!(/#{Regexp.escape(@options[:header])}\n{,2}/, "")
|
|
161
|
+
log.gsub!(/\n{,2}#{Regexp.escape(CREDIT_LINE)}/, "") # Remove old credit lines
|
|
162
|
+
log
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Add template messages to given string. Previously added
|
|
166
|
+
# messages of the same wording are removed.
|
|
167
|
+
# @param log [String]
|
|
168
|
+
def insert_fixed_string(log)
|
|
169
|
+
ins = +""
|
|
170
|
+
ins += @options[:frontmatter] if @options[:frontmatter]
|
|
171
|
+
ins += "#{@options[:header]}\n\n"
|
|
172
|
+
log.insert(0, ins)
|
|
173
|
+
log += "\n\n#{CREDIT_LINE}"
|
|
174
|
+
log
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitHubChangelogGenerator
|
|
4
|
+
class Generator
|
|
5
|
+
# Fetch event for issues and pull requests
|
|
6
|
+
# @return [Array] array of fetched issues
|
|
7
|
+
def fetch_events_for_issues_and_pr
|
|
8
|
+
print "Fetching events for issues and PR: 0/#{@issues.count + @pull_requests.count}\r" if options[:verbose]
|
|
9
|
+
|
|
10
|
+
# Async fetching events:
|
|
11
|
+
@fetcher.fetch_events_async(@issues + @pull_requests)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Async fetching of all tags dates
|
|
15
|
+
def fetch_tags_dates(tags)
|
|
16
|
+
print "Fetching tag dates...\r" if options[:verbose]
|
|
17
|
+
i = 0
|
|
18
|
+
tags.each do |tag|
|
|
19
|
+
get_time_of_tag(tag)
|
|
20
|
+
i += 1
|
|
21
|
+
end
|
|
22
|
+
puts "Fetching tags dates: #{i}/#{tags.count}" if options[:verbose]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Find correct closed dates, if issues was closed by commits
|
|
26
|
+
def detect_actual_closed_dates(issues)
|
|
27
|
+
print "Fetching closed dates for issues...\r" if options[:verbose]
|
|
28
|
+
|
|
29
|
+
i = 0
|
|
30
|
+
issues.each do |issue|
|
|
31
|
+
find_closed_date_by_commit(issue)
|
|
32
|
+
i += 1
|
|
33
|
+
end
|
|
34
|
+
puts "Fetching closed dates for issues: #{i}/#{issues.count}" if options[:verbose]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Adds a key "first_occurring_tag" to each PR with a value of the oldest
|
|
38
|
+
# tag that a PR's merge commit occurs in in the git history. This should
|
|
39
|
+
# indicate the release of each PR by git's history regardless of dates and
|
|
40
|
+
# divergent branches.
|
|
41
|
+
#
|
|
42
|
+
# @param [Array] tags The tags sorted by time, newest to oldest.
|
|
43
|
+
# @param [Array] prs The PRs to discover the tags of.
|
|
44
|
+
# @return [Nil] No return; PRs are updated in-place.
|
|
45
|
+
def add_first_occurring_tag_to_prs(tags, prs)
|
|
46
|
+
total = prs.count
|
|
47
|
+
|
|
48
|
+
prs_left = associate_tagged_prs(tags, prs, total)
|
|
49
|
+
prs_left = associate_release_branch_prs(prs_left, total)
|
|
50
|
+
prs_left = associate_rebase_comment_prs(tags, prs_left, total) if prs_left.any?
|
|
51
|
+
# PRs in prs_left will be untagged, not in release branch, and not
|
|
52
|
+
# rebased. They should not be included in the changelog as they probably
|
|
53
|
+
# have been merged to a branch other than the release branch.
|
|
54
|
+
@pull_requests -= prs_left
|
|
55
|
+
Helper.log.info "Associating PRs with tags: #{total}/#{total}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Associate merged PRs by the merge SHA contained in each tag. If the
|
|
59
|
+
# merge_commit_sha is not found in any tag's history, skip association.
|
|
60
|
+
#
|
|
61
|
+
# @param [Array] tags The tags sorted by time, newest to oldest.
|
|
62
|
+
# @param [Array] prs The PRs to associate.
|
|
63
|
+
# @return [Array] PRs without their merge_commit_sha in a tag.
|
|
64
|
+
def associate_tagged_prs(tags, prs, total)
|
|
65
|
+
@fetcher.fetch_tag_shas(tags)
|
|
66
|
+
|
|
67
|
+
i = 0
|
|
68
|
+
prs.reject do |pr|
|
|
69
|
+
found = false
|
|
70
|
+
# XXX Wish I could use merge_commit_sha, but gcg doesn't currently
|
|
71
|
+
# fetch that. See
|
|
72
|
+
# https://developer.github.com/v3/pulls/#get-a-single-pull-request vs.
|
|
73
|
+
# https://developer.github.com/v3/pulls/#list-pull-requests
|
|
74
|
+
if pr["events"] && (event = pr["events"].find { |e| e["event"] == "merged" })
|
|
75
|
+
# Iterate tags.reverse (oldest to newest) to find first tag of each PR.
|
|
76
|
+
if (oldest_tag = tags.reverse.find { |tag| tag["shas_in_tag"].include?(event["commit_id"]) })
|
|
77
|
+
pr["first_occurring_tag"] = oldest_tag["name"]
|
|
78
|
+
found = true
|
|
79
|
+
i += 1
|
|
80
|
+
print("Associating PRs with tags: #{i}/#{total}\r") if @options[:verbose]
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
# Either there were no events or no merged event. GitHub's api can be
|
|
84
|
+
# weird like that apparently. Check for a rebased comment before erroring.
|
|
85
|
+
no_events_pr = associate_rebase_comment_prs(tags, [pr], total)
|
|
86
|
+
raise StandardError, "No merge sha found for PR #{pr['number']} via the GitHub API" unless no_events_pr.empty?
|
|
87
|
+
|
|
88
|
+
found = true
|
|
89
|
+
i += 1
|
|
90
|
+
print("Associating PRs with tags: #{i}/#{total}\r") if @options[:verbose]
|
|
91
|
+
end
|
|
92
|
+
found
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Associate merged PRs by the HEAD of the release branch. If no
|
|
97
|
+
# --release-branch was specified, then the github default branch is used.
|
|
98
|
+
#
|
|
99
|
+
# @param [Array] prs_left PRs not associated with any tag.
|
|
100
|
+
# @param [Integer] total The total number of PRs to associate; used for verbose printing.
|
|
101
|
+
# @return [Array] PRs without their merge_commit_sha in the branch.
|
|
102
|
+
def associate_release_branch_prs(prs_left, total)
|
|
103
|
+
if prs_left.any?
|
|
104
|
+
i = total - prs_left.count
|
|
105
|
+
prs_left.reject do |pr|
|
|
106
|
+
found = false
|
|
107
|
+
if pr["events"] && (event = pr["events"].find { |e| e["event"] == "merged" }) && sha_in_release_branch?(event["commit_id"])
|
|
108
|
+
found = true
|
|
109
|
+
i += 1
|
|
110
|
+
print("Associating PRs with tags: #{i}/#{total}\r") if @options[:verbose]
|
|
111
|
+
end
|
|
112
|
+
found
|
|
113
|
+
end
|
|
114
|
+
else
|
|
115
|
+
prs_left
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Associate merged PRs by the SHA detected in github comments of the form
|
|
120
|
+
# "rebased commit: <sha>". For use when the merge_commit_sha is not in the
|
|
121
|
+
# actual git history due to rebase.
|
|
122
|
+
#
|
|
123
|
+
# @param [Array] tags The tags sorted by time, newest to oldest.
|
|
124
|
+
# @param [Array] prs_left The PRs not yet associated with any tag or branch.
|
|
125
|
+
# @return [Array] PRs without rebase comments.
|
|
126
|
+
def associate_rebase_comment_prs(tags, prs_left, total)
|
|
127
|
+
i = total - prs_left.count
|
|
128
|
+
# Any remaining PRs were not found in the list of tags by their merge
|
|
129
|
+
# commit and not found in any specified release branch. Fallback to
|
|
130
|
+
# rebased commit comment.
|
|
131
|
+
@fetcher.fetch_comments_async(prs_left)
|
|
132
|
+
prs_left.reject do |pr|
|
|
133
|
+
found = false
|
|
134
|
+
if pr["comments"] && (rebased_comment = pr["comments"].reverse.find { |c| c["body"].match(%r{rebased commit: ([0-9a-f]{40})}i) })
|
|
135
|
+
rebased_sha = rebased_comment["body"].match(%r{rebased commit: ([0-9a-f]{40})}i)[1]
|
|
136
|
+
if (oldest_tag = tags.reverse.find { |tag| tag["shas_in_tag"].include?(rebased_sha) })
|
|
137
|
+
pr["first_occurring_tag"] = oldest_tag["name"]
|
|
138
|
+
found = true
|
|
139
|
+
i += 1
|
|
140
|
+
elsif sha_in_release_branch?(rebased_sha)
|
|
141
|
+
found = true
|
|
142
|
+
i += 1
|
|
143
|
+
else
|
|
144
|
+
raise StandardError, "PR #{pr['number']} has a rebased SHA comment but that SHA was not found in the release branch or any tags"
|
|
145
|
+
end
|
|
146
|
+
print("Associating PRs with tags: #{i}/#{total}\r") if @options[:verbose]
|
|
147
|
+
else
|
|
148
|
+
puts "Warning: PR #{pr['number']} merge commit was not found in the release branch or tagged git history and no rebased SHA comment was found"
|
|
149
|
+
end
|
|
150
|
+
found
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Fill :actual_date parameter of specified issue by closed date of the commit, if it was closed by commit.
|
|
155
|
+
# @param [Hash] issue
|
|
156
|
+
def find_closed_date_by_commit(issue)
|
|
157
|
+
return if issue["events"].nil?
|
|
158
|
+
|
|
159
|
+
# if it's PR -> then find "merged event", in case of usual issue -> found closed date
|
|
160
|
+
compare_string = issue["merged_at"].nil? ? "closed" : "merged"
|
|
161
|
+
# reverse! - to find latest closed event. (event goes in date order)
|
|
162
|
+
issue["events"].reverse!.each do |event|
|
|
163
|
+
if event["event"] == compare_string
|
|
164
|
+
set_date_from_event(event, issue)
|
|
165
|
+
break
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
# TODO: assert issues, that remain without 'actual_date' hash for some reason.
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Set closed date from this issue
|
|
172
|
+
#
|
|
173
|
+
# @param [Hash] event
|
|
174
|
+
# @param [Hash] issue
|
|
175
|
+
def set_date_from_event(event, issue)
|
|
176
|
+
if event["commit_id"].nil?
|
|
177
|
+
issue["actual_date"] = issue["closed_at"]
|
|
178
|
+
return
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
commit = @fetcher.fetch_commit(event["commit_id"])
|
|
182
|
+
issue["actual_date"] = commit["commit"]["author"]["date"]
|
|
183
|
+
|
|
184
|
+
# issue['actual_date'] = commit['author']['date']
|
|
185
|
+
rescue StandardError
|
|
186
|
+
puts "Warning: Can't fetch commit #{event['commit_id']}. It is probably referenced from another repo."
|
|
187
|
+
issue["actual_date"] = issue["closed_at"]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
# Detect if a sha occurs in the --release-branch. Uses the github repo
|
|
193
|
+
# default branch if not specified.
|
|
194
|
+
#
|
|
195
|
+
# @param [String] sha SHA to check.
|
|
196
|
+
# @return [Boolean] True if SHA is in the branch git history.
|
|
197
|
+
def sha_in_release_branch?(sha)
|
|
198
|
+
branch = @options[:release_branch] || @fetcher.default_branch
|
|
199
|
+
@fetcher.commits_in_branch(branch).include?(sha)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|