github_changelog_generator 1.15.0.pre.beta → 1.16.1

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 (62) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +1 -1
  3. data/README.md +332 -285
  4. data/Rakefile +1 -1
  5. data/bin/git-generate-changelog +1 -1
  6. data/lib/github_changelog_generator.rb +10 -6
  7. data/lib/github_changelog_generator/generator/entry.rb +218 -0
  8. data/lib/github_changelog_generator/generator/generator.rb +124 -125
  9. data/lib/github_changelog_generator/generator/generator_fetcher.rb +139 -23
  10. data/lib/github_changelog_generator/generator/generator_processor.rb +59 -27
  11. data/lib/github_changelog_generator/generator/generator_tags.rb +25 -21
  12. data/lib/github_changelog_generator/generator/section.rb +124 -0
  13. data/lib/github_changelog_generator/helper.rb +1 -1
  14. data/lib/github_changelog_generator/octo_fetcher.rb +233 -96
  15. data/lib/github_changelog_generator/options.rb +74 -2
  16. data/lib/github_changelog_generator/parser.rb +118 -74
  17. data/lib/github_changelog_generator/parser_file.rb +7 -3
  18. data/lib/github_changelog_generator/reader.rb +2 -2
  19. data/lib/github_changelog_generator/task.rb +4 -3
  20. data/lib/github_changelog_generator/version.rb +1 -1
  21. data/man/git-generate-changelog.1 +144 -45
  22. data/man/git-generate-changelog.1.html +157 -84
  23. data/man/git-generate-changelog.html +19 -7
  24. data/man/git-generate-changelog.md +151 -84
  25. data/spec/files/github-changelog-generator.md +114 -114
  26. data/spec/{install-gem-in-bundler.gemfile → install_gem_in_bundler.gemfile} +2 -0
  27. data/spec/spec_helper.rb +2 -6
  28. data/spec/unit/generator/entry_spec.rb +766 -0
  29. data/spec/unit/generator/generator_processor_spec.rb +103 -41
  30. data/spec/unit/generator/generator_spec.rb +47 -0
  31. data/spec/unit/generator/generator_tags_spec.rb +51 -24
  32. data/spec/unit/generator/section_spec.rb +34 -0
  33. data/spec/unit/octo_fetcher_spec.rb +247 -197
  34. data/spec/unit/options_spec.rb +24 -0
  35. data/spec/unit/parse_file_spec.rb +2 -2
  36. data/spec/unit/reader_spec.rb +4 -4
  37. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits/when_API_is_valid/returns_commits.json +1 -0
  38. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits_before/when_API_is_valid/returns_commits.json +1 -1
  39. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid.json +1 -1
  40. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issue_with_proper_key/values.json +1 -1
  41. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues.json +1 -1
  42. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues_with_labels.json +1 -1
  43. 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
  44. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_requests_with_labels.json +1 -1
  45. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid.json +1 -1
  46. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_correct_pull_request_keys.json +1 -1
  47. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_pull_requests.json +1 -1
  48. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid.json +1 -1
  49. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid/returns_commit.json +1 -1
  50. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid.json +1 -1
  51. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid/returns_date.json +1 -1
  52. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid.json +1 -1
  53. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid/populates_issues.json +1 -1
  54. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid.json +1 -1
  55. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags.json +1 -1
  56. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags_count.json +1 -1
  57. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided.json +1 -1
  58. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided/should_raise_Unauthorized_error.json +1 -1
  59. metadata +71 -38
  60. data/bin/ghclgen +0 -5
  61. data/lib/github_changelog_generator/generator/generator_generation.rb +0 -180
  62. data/spec/unit/generator/generator_generation_spec.rb +0 -73
data/Rakefile CHANGED
@@ -13,7 +13,7 @@ RSpec::Core::RakeTask.new
13
13
 
14
14
  desc "When releasing the gem, re-fetch latest cacert.pem from curl.haxx.se. Developer task."
15
15
  task :update_ssl_ca_file do
16
- `pushd lib/github_changelog_generator/ssl_certs && curl --remote-name --time-cond cacert.pem https://curl.haxx.se/ca/cacert.pem && popd`
16
+ `pushd lib/github_changelog_generator/ssl_certs && curl --remote-name --time-cond cacert.pem https://curl.se/ca/cacert.pem && popd`
17
17
  end
18
18
 
19
19
  task default: %i[rubocop spec]
@@ -1,4 +1,4 @@
1
- #! /usr/bin/env ruby
1
+ #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "../lib/github_changelog_generator"
@@ -22,22 +22,26 @@ require "github_changelog_generator/reader"
22
22
  module GitHubChangelogGenerator
23
23
  # Main class and entry point for this script.
24
24
  class ChangelogGenerator
25
- # Class, responsible for whole change log generation cycle
25
+ # Class, responsible for whole changelog generation cycle
26
26
  # @return initialised instance of ChangelogGenerator
27
27
  def initialize
28
28
  @options = Parser.parse_options
29
29
  @generator = Generator.new @options
30
30
  end
31
31
 
32
- # The entry point of this script to generate change log
32
+ # The entry point of this script to generate changelog
33
33
  # @raise (ChangelogGeneratorError) Is thrown when one of specified tags was not found in list of tags.
34
34
  def run
35
35
  log = @generator.compound_changelog
36
36
 
37
- output_filename = @options[:output].to_s
38
- File.open(output_filename, "wb") { |file| file.write(log) }
39
- puts "Done!"
40
- puts "Generated log placed in #{Dir.pwd}/#{output_filename}"
37
+ if @options.write_to_file?
38
+ output_filename = @options[:output].to_s
39
+ File.open(output_filename, "wb") { |file| file.write(log) }
40
+ puts "Done!"
41
+ puts "Generated log placed in #{Dir.pwd}/#{output_filename}"
42
+ else
43
+ puts log
44
+ end
41
45
  end
42
46
  end
43
47
  end
@@ -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
+ # controled 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
@@ -1,20 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../octo_fetcher"
4
- require_relative "generator_generation"
5
- require_relative "generator_fetcher"
6
- require_relative "generator_processor"
7
- require_relative "generator_tags"
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"
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
+ CREDIT_LINE = <<~CREDIT
30
+ \\* *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
18
36
  #
19
37
  # Example:
20
38
  # generator = GitHubChangelogGenerator::Generator.new
@@ -23,156 +41,137 @@ module GitHubChangelogGenerator
23
41
  @options = options
24
42
  @tag_times_hash = {}
25
43
  @fetcher = GitHubChangelogGenerator::OctoFetcher.new(options)
44
+ @sections = []
26
45
  end
27
46
 
28
- def fetch_issues_and_pr
29
- issues, pull_requests = @fetcher.fetch_closed_issues_and_pr
30
-
31
- @pull_requests = options[:pulls] ? get_filtered_pull_requests(pull_requests) : []
32
-
33
- @issues = options[:issues] ? get_filtered_issues(issues) : []
34
-
35
- fetch_events_for_issues_and_pr
36
- detect_actual_closed_dates(@issues + @pull_requests)
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
37
67
  end
38
68
 
39
- ENCAPSULATED_CHARACTERS = %w(< > * _ \( \) [ ] #)
69
+ private
40
70
 
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('\\', '\\\\')
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)
47
76
 
48
- ENCAPSULATED_CHARACTERS.each do |char|
49
- string = string.gsub(char, "\\#{char}")
77
+ if newer_tag.nil? && filtered_issues.empty? && filtered_pull_requests.empty?
78
+ # do not generate empty unreleased section
79
+ return ""
50
80
  end
51
81
 
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
82
  newer_tag_link, newer_tag_name, newer_tag_time = detect_link_tag_time(newer_tag)
64
83
 
65
- github_site = options[:github_site] || "https://github.com"
66
- project_url = "#{github_site}/#{options[:user]}/#{options[:project]}"
67
-
68
84
  # If the older tag is nil, go back in time from the latest tag and find
69
85
  # the SHA for the first commit.
70
86
  older_tag_name =
71
87
  if older_tag.nil?
72
- @fetcher.commits_before(newer_tag_time).last["sha"]
88
+ @fetcher.oldest_commit["sha"]
73
89
  else
74
90
  older_tag["name"]
75
91
  end
76
92
 
77
- log = generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url)
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)
78
104
 
79
- if options[:issues]
80
- # Generate issues:
81
- log += issues_to_log(issues, pull_requests)
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)
82
111
  end
112
+ [filtered_issues, filtered_pull_requests]
113
+ end
83
114
 
84
- if options[:pulls] && options[:add_pr_wo_labels]
85
- # Generate pull requests:
86
- log += generate_sub_section(pull_requests, options[:merge_prefix])
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)
87
125
  end
88
126
 
89
- log
127
+ entries
90
128
  end
91
129
 
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
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
106
138
  end
107
139
 
108
- # This method sort issues by types
109
- # (bugs, features, or just closed issues) by labels
140
+ # Fetches @pull_requests and @issues and filters them based on options.
110
141
  #
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
142
+ # @return [Nil] No return.
143
+ def fetch_issues_and_pr
144
+ issues, pull_requests = @fetcher.fetch_closed_issues_and_pr
139
145
 
140
- sections[:issues] << dict unless added
141
- end
146
+ @pull_requests = options[:pulls] ? get_filtered_pull_requests(pull_requests) : []
142
147
 
143
- sort_pull_requests(pull_requests, sections)
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
144
154
  end
145
155
 
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
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
176
175
  end
177
176
  end
178
177
  end