github_changelog_generator 1.15.0.pre.rc → 1.15.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.
Files changed (57) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +1 -1
  3. data/README.md +126 -51
  4. data/bin/git-generate-changelog +1 -1
  5. data/lib/github_changelog_generator.rb +10 -6
  6. data/lib/github_changelog_generator/generator/entry.rb +218 -0
  7. data/lib/github_changelog_generator/generator/generator.rb +96 -119
  8. data/lib/github_changelog_generator/generator/generator_fetcher.rb +140 -21
  9. data/lib/github_changelog_generator/generator/generator_processor.rb +40 -10
  10. data/lib/github_changelog_generator/generator/generator_tags.rb +10 -12
  11. data/lib/github_changelog_generator/generator/section.rb +104 -0
  12. data/lib/github_changelog_generator/octo_fetcher.rb +113 -23
  13. data/lib/github_changelog_generator/options.rb +35 -4
  14. data/lib/github_changelog_generator/parser.rb +88 -49
  15. data/lib/github_changelog_generator/parser_file.rb +6 -2
  16. data/lib/github_changelog_generator/task.rb +2 -3
  17. data/lib/github_changelog_generator/version.rb +1 -1
  18. data/man/git-generate-changelog.1 +125 -51
  19. data/man/git-generate-changelog.1.html +145 -89
  20. data/man/git-generate-changelog.html +19 -7
  21. data/man/git-generate-changelog.md +141 -86
  22. data/spec/files/github-changelog-generator.md +114 -114
  23. data/spec/{install-gem-in-bundler.gemfile → install_gem_in_bundler.gemfile} +2 -0
  24. data/spec/spec_helper.rb +1 -5
  25. data/spec/unit/generator/entry_spec.rb +760 -0
  26. data/spec/unit/generator/generator_processor_spec.rb +9 -2
  27. data/spec/unit/generator/generator_tags_spec.rb +5 -21
  28. data/spec/unit/octo_fetcher_spec.rb +204 -197
  29. data/spec/unit/options_spec.rb +24 -0
  30. data/spec/unit/parse_file_spec.rb +2 -2
  31. data/spec/unit/reader_spec.rb +4 -4
  32. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits/when_API_is_valid/returns_commits.json +1 -0
  33. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits_before/when_API_is_valid/returns_commits.json +1 -1
  34. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid.json +1 -1
  35. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issue_with_proper_key/values.json +1 -1
  36. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues.json +1 -1
  37. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues_with_labels.json +1 -1
  38. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_request_with_proper_key/values.json +1 -1
  39. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_requests_with_labels.json +1 -1
  40. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid.json +1 -1
  41. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_correct_pull_request_keys.json +1 -1
  42. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_pull_requests.json +1 -1
  43. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid.json +1 -1
  44. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid/returns_commit.json +1 -1
  45. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid.json +1 -1
  46. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid/returns_date.json +1 -1
  47. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid.json +1 -1
  48. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid/populates_issues.json +1 -1
  49. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid.json +1 -1
  50. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags.json +1 -1
  51. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags_count.json +1 -1
  52. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided.json +1 -1
  53. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided/should_raise_Unauthorized_error.json +1 -1
  54. metadata +17 -17
  55. data/bin/ghclgen +0 -5
  56. data/lib/github_changelog_generator/generator/generator_generation.rb +0 -181
  57. data/spec/unit/generator/generator_generation_spec.rb +0 -73
@@ -1,20 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "github_changelog_generator/octo_fetcher"
4
- require "github_changelog_generator/generator/generator_generation"
5
4
  require "github_changelog_generator/generator/generator_fetcher"
6
5
  require "github_changelog_generator/generator/generator_processor"
7
6
  require "github_changelog_generator/generator/generator_tags"
7
+ require "github_changelog_generator/generator/entry"
8
+ require "github_changelog_generator/generator/section"
8
9
 
9
10
  module GitHubChangelogGenerator
10
11
  # Default error for ChangelogGenerator
11
12
  class ChangelogGeneratorError < StandardError
12
13
  end
13
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
14
26
  class Generator
15
- attr_accessor :options, :filtered_tags, :github, :tag_section_mapping, :sorted_tags
27
+ attr_accessor :options, :filtered_tags, :tag_section_mapping, :sorted_tags
16
28
 
17
- # A Generator responsible for all logic, related with change log generation from ready-to-parse issues
29
+ # A Generator responsible for all logic, related with changelog generation from ready-to-parse issues
18
30
  #
19
31
  # Example:
20
32
  # generator = GitHubChangelogGenerator::Generator.new
@@ -23,156 +35,121 @@ module GitHubChangelogGenerator
23
35
  @options = options
24
36
  @tag_times_hash = {}
25
37
  @fetcher = GitHubChangelogGenerator::OctoFetcher.new(options)
38
+ @sections = []
26
39
  end
27
40
 
28
- def fetch_issues_and_pr
29
- issues, pull_requests = @fetcher.fetch_closed_issues_and_pr
41
+ # Main function to start changelog generation
42
+ #
43
+ # @return [String] Generated changelog file
44
+ def compound_changelog
45
+ @options.load_custom_ruby_files
46
+ fetch_and_filter_tags
47
+ fetch_issues_and_pr
30
48
 
31
- @pull_requests = options[:pulls] ? get_filtered_pull_requests(pull_requests) : []
49
+ log = ""
50
+ log += @options[:frontmatter] if @options[:frontmatter]
51
+ log += "#{options[:header]}\n\n"
32
52
 
33
- @issues = options[:issues] ? get_filtered_issues(issues) : []
53
+ log += if @options[:unreleased_only]
54
+ generate_entry_between_tags(@filtered_tags[0], nil)
55
+ else
56
+ generate_entries_for_all_tags
57
+ end
34
58
 
35
- fetch_events_for_issues_and_pr
36
- detect_actual_closed_dates(@issues + @pull_requests)
59
+ log += File.read(@options[:base]) if File.file?(@options[:base])
60
+
61
+ credit_line = "\n\n\\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*"
62
+ log.gsub!(/#{credit_line}(\n)?/, "") # Remove old credit lines
63
+ log += "#{credit_line}\n"
64
+
65
+ @log = log
37
66
  end
38
67
 
39
- ENCAPSULATED_CHARACTERS = %w(< > * _ \( \) [ ] #)
68
+ private
40
69
 
41
- # Encapsulate characters to make Markdown look as expected.
42
- #
43
- # @param [String] string
44
- # @return [String] encapsulated input string
45
- def encapsulate_string(string)
46
- string = string.gsub('\\', '\\\\')
70
+ # Generate log only between 2 specified tags
71
+ # @param [String] older_tag all issues before this tag date will be excluded. May be nil, if it's first tag
72
+ # @param [String] newer_tag all issue after this tag will be excluded. May be nil for unreleased section
73
+ def generate_entry_between_tags(older_tag, newer_tag)
74
+ filtered_issues, filtered_pull_requests = filter_issues_for_tags(newer_tag, older_tag)
47
75
 
48
- ENCAPSULATED_CHARACTERS.each do |char|
49
- string = string.gsub(char, "\\#{char}")
76
+ if newer_tag.nil? && filtered_issues.empty? && filtered_pull_requests.empty?
77
+ # do not generate empty unreleased section
78
+ return ""
50
79
  end
51
80
 
52
- string
53
- end
54
-
55
- # Generates log for section with header and body
56
- #
57
- # @param [Array] pull_requests List or PR's in new section
58
- # @param [Array] issues List of issues in new section
59
- # @param [String] newer_tag Name of the newer tag. Could be nil for `Unreleased` section
60
- # @param [Hash, nil] older_tag Older tag, used for the links. Could be nil for last tag.
61
- # @return [String] Ready and parsed section
62
- def create_log_for_tag(pull_requests, issues, newer_tag, older_tag = nil)
63
81
  newer_tag_link, newer_tag_name, newer_tag_time = detect_link_tag_time(newer_tag)
64
82
 
65
- github_site = options[:github_site] || "https://github.com"
66
- project_url = "#{github_site}/#{options[:user]}/#{options[:project]}"
67
-
68
83
  # If the older tag is nil, go back in time from the latest tag and find
69
84
  # the SHA for the first commit.
70
85
  older_tag_name =
71
86
  if older_tag.nil?
72
- @fetcher.commits_before(newer_tag_time).last["sha"]
87
+ @fetcher.oldest_commit["sha"]
73
88
  else
74
89
  older_tag["name"]
75
90
  end
76
91
 
77
- log = generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url)
92
+ Entry.new(options).generate_entry_for_tag(filtered_pull_requests, filtered_issues, newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name)
93
+ end
94
+
95
+ # Filters issues and pull requests based on, respectively, `actual_date`
96
+ # and `merged_at` timestamp fields. `actual_date` is the detected form of
97
+ # `closed_at` based on merge event SHA commit times.
98
+ #
99
+ # @return [Array] filtered issues and pull requests
100
+ def filter_issues_for_tags(newer_tag, older_tag)
101
+ filtered_pull_requests = filter_by_tag(@pull_requests, newer_tag)
102
+ filtered_issues = delete_by_time(@issues, "actual_date", older_tag, newer_tag)
103
+
104
+ newer_tag_name = newer_tag.nil? ? nil : newer_tag["name"]
78
105
 
79
- if options[:issues]
80
- # Generate issues:
81
- log += issues_to_log(issues, pull_requests)
106
+ if options[:filter_issues_by_milestone]
107
+ # delete excess irrelevant issues (according milestones). Issue #22.
108
+ filtered_issues = filter_by_milestone(filtered_issues, newer_tag_name, @issues)
109
+ filtered_pull_requests = filter_by_milestone(filtered_pull_requests, newer_tag_name, @pull_requests)
82
110
  end
111
+ [filtered_issues, filtered_pull_requests]
112
+ end
113
+
114
+ # The full cycle of generation for whole project
115
+ # @return [String] All entries in the changelog
116
+ def generate_entries_for_all_tags
117
+ puts "Generating entry..." if options[:verbose]
118
+
119
+ entries = generate_unreleased_entry
83
120
 
84
- if options[:pulls] && options[:add_pr_wo_labels]
85
- # Generate pull requests:
86
- log += generate_sub_section(pull_requests, options[:merge_prefix])
121
+ @tag_section_mapping.each_pair do |_tag_section, left_right_tags|
122
+ older_tag, newer_tag = left_right_tags
123
+ entries += generate_entry_between_tags(older_tag, newer_tag)
87
124
  end
88
125
 
89
- log
126
+ entries
90
127
  end
91
128
 
92
- # Generate ready-to-paste log from list of issues and pull requests.
93
- #
94
- # @param [Array] issues
95
- # @param [Array] pull_requests
96
- # @return [String] generated log for issues
97
- def issues_to_log(issues, pull_requests)
98
- sections = parse_by_sections(issues, pull_requests)
99
-
100
- log = ""
101
- log += generate_sub_section(sections[:breaking], options[:breaking_prefix])
102
- log += generate_sub_section(sections[:enhancements], options[:enhancement_prefix])
103
- log += generate_sub_section(sections[:bugs], options[:bug_prefix])
104
- log += generate_sub_section(sections[:issues], options[:issue_prefix])
105
- log
129
+ def generate_unreleased_entry
130
+ entry = ""
131
+ if options[:unreleased]
132
+ start_tag = @filtered_tags[0] || @sorted_tags.last
133
+ unreleased_entry = generate_entry_between_tags(start_tag, nil)
134
+ entry += unreleased_entry if unreleased_entry
135
+ end
136
+ entry
106
137
  end
107
138
 
108
- # This method sort issues by types
109
- # (bugs, features, or just closed issues) by labels
139
+ # Fetches @pull_requests and @issues and filters them based on options.
110
140
  #
111
- # @param [Array] issues
112
- # @param [Array] pull_requests
113
- # @return [Hash] Mapping of filtered arrays: (Bugs, Enhancements, Breaking stuff, Issues)
114
- def parse_by_sections(issues, pull_requests)
115
- sections = {
116
- issues: [],
117
- enhancements: [],
118
- bugs: [],
119
- breaking: []
120
- }
121
-
122
- issues.each do |dict|
123
- added = false
124
-
125
- dict["labels"].each do |label|
126
- if options[:bug_labels].include?(label["name"])
127
- sections[:bugs] << dict
128
- added = true
129
- elsif options[:enhancement_labels].include?(label["name"])
130
- sections[:enhancements] << dict
131
- added = true
132
- elsif options[:breaking_labels].include?(label["name"])
133
- sections[:breaking] << dict
134
- added = true
135
- end
136
-
137
- break if added
138
- end
141
+ # @return [Nil] No return.
142
+ def fetch_issues_and_pr
143
+ issues, pull_requests = @fetcher.fetch_closed_issues_and_pr
139
144
 
140
- sections[:issues] << dict unless added
141
- end
145
+ @pull_requests = options[:pulls] ? get_filtered_pull_requests(pull_requests) : []
142
146
 
143
- sort_pull_requests(pull_requests, sections)
144
- end
147
+ @issues = options[:issues] ? get_filtered_issues(issues) : []
145
148
 
146
- # This method iterates through PRs and sorts them into sections
147
- #
148
- # @param [Array] pull_requests
149
- # @param [Hash] sections
150
- # @return [Hash] sections
151
- def sort_pull_requests(pull_requests, sections)
152
- added_pull_requests = []
153
- pull_requests.each do |pr|
154
- added = false
155
-
156
- pr["labels"].each do |label|
157
- if options[:bug_labels].include?(label["name"])
158
- sections[:bugs] << pr
159
- added_pull_requests << pr
160
- added = true
161
- elsif options[:enhancement_labels].include?(label["name"])
162
- sections[:enhancements] << pr
163
- added_pull_requests << pr
164
- added = true
165
- elsif options[:breaking_labels].include?(label["name"])
166
- sections[:breaking] << pr
167
- added_pull_requests << pr
168
- added = true
169
- end
170
-
171
- break if added
172
- end
173
- end
174
- added_pull_requests.each { |p| pull_requests.delete(p) }
175
- sections
149
+ fetch_events_for_issues_and_pr
150
+ detect_actual_closed_dates(@issues + @pull_requests)
151
+ add_first_occurring_tag_to_prs(@sorted_tags, @pull_requests)
152
+ nil
176
153
  end
177
154
  end
178
155
  end
@@ -7,9 +7,7 @@ module GitHubChangelogGenerator
7
7
  # Fetch event for issues and pull requests
8
8
  # @return [Array] array of fetched issues
9
9
  def fetch_events_for_issues_and_pr
10
- if options[:verbose]
11
- print "Fetching events for issues and PR: 0/#{@issues.count + @pull_requests.count}\r"
12
- end
10
+ print "Fetching events for issues and PR: 0/#{@issues.count + @pull_requests.count}\r" if options[:verbose]
13
11
 
14
12
  # Async fetching events:
15
13
  @fetcher.fetch_events_async(@issues + @pull_requests)
@@ -18,34 +16,141 @@ module GitHubChangelogGenerator
18
16
  # Async fetching of all tags dates
19
17
  def fetch_tags_dates(tags)
20
18
  print "Fetching tag dates...\r" if options[:verbose]
21
- # Async fetching tags:
22
- threads = []
23
19
  i = 0
24
- all = tags.count
25
20
  tags.each do |tag|
26
- print " \r"
27
- threads << Thread.new do
28
- get_time_of_tag(tag)
29
- print "Fetching tags dates: #{i + 1}/#{all}\r" if options[:verbose]
30
- i += 1
31
- end
21
+ get_time_of_tag(tag)
22
+ i += 1
32
23
  end
33
- threads.each(&:join)
34
- puts "Fetching tags dates: #{i}" if options[:verbose]
24
+ puts "Fetching tags dates: #{i}/#{tags.count}" if options[:verbose]
35
25
  end
36
26
 
37
27
  # Find correct closed dates, if issues was closed by commits
38
28
  def detect_actual_closed_dates(issues)
39
29
  print "Fetching closed dates for issues...\r" if options[:verbose]
40
30
 
41
- issues.each_slice(MAX_THREAD_NUMBER) do |issues_slice|
42
- threads = []
43
- issues_slice.each do |issue|
44
- threads << Thread.new { find_closed_date_by_commit(issue) }
31
+ i = 0
32
+ issues.each do |issue|
33
+ find_closed_date_by_commit(issue)
34
+ i += 1
35
+ end
36
+ puts "Fetching closed dates for issues: #{i}/#{issues.count}" if options[:verbose]
37
+ end
38
+
39
+ # Adds a key "first_occurring_tag" to each PR with a value of the oldest
40
+ # tag that a PR's merge commit occurs in in the git history. This should
41
+ # indicate the release of each PR by git's history regardless of dates and
42
+ # divergent branches.
43
+ #
44
+ # @param [Array] tags The tags sorted by time, newest to oldest.
45
+ # @param [Array] prs The PRs to discover the tags of.
46
+ # @return [Nil] No return; PRs are updated in-place.
47
+ def add_first_occurring_tag_to_prs(tags, prs)
48
+ total = prs.count
49
+
50
+ prs_left = associate_tagged_prs(tags, prs, total)
51
+ prs_left = associate_release_branch_prs(prs_left, total)
52
+ prs_left = associate_rebase_comment_prs(tags, prs_left, total) if prs_left.any?
53
+ # PRs in prs_left will be untagged, not in release branch, and not
54
+ # rebased. They should not be included in the changelog as they probably
55
+ # have been merged to a branch other than the release branch.
56
+ @pull_requests -= prs_left
57
+ Helper.log.info "Associating PRs with tags: #{total}/#{total}"
58
+ end
59
+
60
+ # Associate merged PRs by the merge SHA contained in each tag. If the
61
+ # merge_commit_sha is not found in any tag's history, skip association.
62
+ #
63
+ # @param [Array] tags The tags sorted by time, newest to oldest.
64
+ # @param [Array] prs The PRs to associate.
65
+ # @return [Array] PRs without their merge_commit_sha in a tag.
66
+ def associate_tagged_prs(tags, prs, total)
67
+ @fetcher.fetch_tag_shas_async(tags)
68
+
69
+ i = 0
70
+ prs.reject do |pr|
71
+ found = false
72
+ # XXX Wish I could use merge_commit_sha, but gcg doesn't currently
73
+ # fetch that. See
74
+ # https://developer.github.com/v3/pulls/#get-a-single-pull-request vs.
75
+ # https://developer.github.com/v3/pulls/#list-pull-requests
76
+ if pr["events"] && (event = pr["events"].find { |e| e["event"] == "merged" })
77
+ # Iterate tags.reverse (oldest to newest) to find first tag of each PR.
78
+ if (oldest_tag = tags.reverse.find { |tag| tag["shas_in_tag"].include?(event["commit_id"]) })
79
+ pr["first_occurring_tag"] = oldest_tag["name"]
80
+ found = true
81
+ i += 1
82
+ print("Associating PRs with tags: #{i}/#{total}\r") if @options[:verbose]
83
+ end
84
+ else
85
+ # Either there were no events or no merged event. Github's api can be
86
+ # weird like that apparently. Check for a rebased comment before erroring.
87
+ no_events_pr = associate_rebase_comment_prs(tags, [pr], total)
88
+ raise StandardError, "No merge sha found for PR #{pr['number']} via the GitHub API" unless no_events_pr.empty?
89
+
90
+ found = true
91
+ i += 1
92
+ print("Associating PRs with tags: #{i}/#{total}\r") if @options[:verbose]
93
+ end
94
+ found
95
+ end
96
+ end
97
+
98
+ # Associate merged PRs by the HEAD of the release branch. If no
99
+ # --release-branch was specified, then the github default branch is used.
100
+ #
101
+ # @param [Array] prs_left PRs not associated with any tag.
102
+ # @param [Integer] total The total number of PRs to associate; used for verbose printing.
103
+ # @return [Array] PRs without their merge_commit_sha in the branch.
104
+ def associate_release_branch_prs(prs_left, total)
105
+ if prs_left.any?
106
+ i = total - prs_left.count
107
+ prs_left.reject do |pr|
108
+ found = false
109
+ if pr["events"] && (event = pr["events"].find { |e| e["event"] == "merged" }) && sha_in_release_branch(event["commit_id"])
110
+ found = true
111
+ i += 1
112
+ print("Associating PRs with tags: #{i}/#{total}\r") if @options[:verbose]
113
+ end
114
+ found
45
115
  end
46
- threads.each(&:join)
116
+ else
117
+ prs_left
118
+ end
119
+ end
120
+
121
+ # Associate merged PRs by the SHA detected in github comments of the form
122
+ # "rebased commit: <sha>". For use when the merge_commit_sha is not in the
123
+ # actual git history due to rebase.
124
+ #
125
+ # @param [Array] tags The tags sorted by time, newest to oldest.
126
+ # @param [Array] prs_left The PRs not yet associated with any tag or branch.
127
+ # @return [Array] PRs without rebase comments.
128
+ def associate_rebase_comment_prs(tags, prs_left, total)
129
+ i = total - prs_left.count
130
+ # Any remaining PRs were not found in the list of tags by their merge
131
+ # commit and not found in any specified release branch. Fallback to
132
+ # rebased commit comment.
133
+ @fetcher.fetch_comments_async(prs_left)
134
+ prs_left.reject do |pr|
135
+ found = false
136
+ if pr["comments"] && (rebased_comment = pr["comments"].reverse.find { |c| c["body"].match(%r{rebased commit: ([0-9a-f]{40})}i) })
137
+ rebased_sha = rebased_comment["body"].match(%r{rebased commit: ([0-9a-f]{40})}i)[1]
138
+ if (oldest_tag = tags.reverse.find { |tag| tag["shas_in_tag"].include?(rebased_sha) })
139
+ pr["first_occurring_tag"] = oldest_tag["name"]
140
+ found = true
141
+ i += 1
142
+ elsif sha_in_release_branch(rebased_sha)
143
+ found = true
144
+ i += 1
145
+ else
146
+ raise StandardError, "PR #{pr['number']} has a rebased SHA comment but that SHA was not found in the release branch or any tags"
147
+ end
148
+ print("Associating PRs with tags: #{i}/#{total}\r") if @options[:verbose]
149
+ else
150
+ 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"
151
+ end
152
+ found
47
153
  end
48
- puts "Fetching closed dates for issues: Done!" if options[:verbose]
49
154
  end
50
155
 
51
156
  # Fill :actual_date parameter of specified issue by closed date of the commit, if it was closed by commit.
@@ -74,7 +179,7 @@ module GitHubChangelogGenerator
74
179
  issue["actual_date"] = issue["closed_at"]
75
180
  else
76
181
  begin
77
- commit = @fetcher.fetch_commit(event)
182
+ commit = @fetcher.fetch_commit(event["commit_id"])
78
183
  issue["actual_date"] = commit["commit"]["author"]["date"]
79
184
 
80
185
  # issue['actual_date'] = commit['author']['date']
@@ -84,5 +189,19 @@ module GitHubChangelogGenerator
84
189
  end
85
190
  end
86
191
  end
192
+
193
+ private
194
+
195
+ # Detect if a sha occurs in the --release-branch. Uses the github repo
196
+ # default branch if not specified.
197
+ #
198
+ # @param [String] sha SHA to check.
199
+ # @return [Boolean] True if SHA is in the branch git history.
200
+ def sha_in_release_branch(sha)
201
+ branch = @options[:release_branch] || @fetcher.default_branch
202
+ commits_in_branch = @fetcher.fetch_compare(@fetcher.oldest_commit["sha"], branch)
203
+ shas_in_branch = commits_in_branch["commits"].collect { |commit| commit["sha"] }
204
+ shas_in_branch.include?(sha)
205
+ end
87
206
  end
88
207
  end