github_changelog_generator 1.15.0.pre.rc → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
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