px_github_changelog_generator 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +9 -0
  3. data/README.md +357 -0
  4. data/Rakefile +19 -0
  5. data/bin/git-generate-changelog +5 -0
  6. data/bin/github_changelog_generator +5 -0
  7. data/lib/github_changelog_generator/argv_parser.rb +225 -0
  8. data/lib/github_changelog_generator/file_parser_chooser.rb +27 -0
  9. data/lib/github_changelog_generator/generator/entry.rb +218 -0
  10. data/lib/github_changelog_generator/generator/generator.rb +177 -0
  11. data/lib/github_changelog_generator/generator/generator_fetcher.rb +202 -0
  12. data/lib/github_changelog_generator/generator/generator_processor.rb +237 -0
  13. data/lib/github_changelog_generator/generator/generator_tags.rb +210 -0
  14. data/lib/github_changelog_generator/generator/section.rb +129 -0
  15. data/lib/github_changelog_generator/helper.rb +37 -0
  16. data/lib/github_changelog_generator/octo_fetcher.rb +535 -0
  17. data/lib/github_changelog_generator/options.rb +159 -0
  18. data/lib/github_changelog_generator/parser.rb +89 -0
  19. data/lib/github_changelog_generator/parser_file.rb +101 -0
  20. data/lib/github_changelog_generator/reader.rb +88 -0
  21. data/lib/github_changelog_generator/ssl_certs/cacert.pem +3138 -0
  22. data/lib/github_changelog_generator/task.rb +68 -0
  23. data/lib/github_changelog_generator/version.rb +5 -0
  24. data/lib/github_changelog_generator.rb +49 -0
  25. data/man/git-generate-changelog.1 +393 -0
  26. data/man/git-generate-changelog.1.html +359 -0
  27. data/man/git-generate-changelog.html +270 -0
  28. data/man/git-generate-changelog.md +274 -0
  29. data/spec/files/angular.js.md +9395 -0
  30. data/spec/files/bundler.md +1911 -0
  31. data/spec/files/config_example +5 -0
  32. data/spec/files/github-changelog-generator.md +305 -0
  33. data/spec/github_changelog_generator_spec.rb +32 -0
  34. data/spec/install_gem_in_bundler.gemfile +5 -0
  35. data/spec/spec_helper.rb +74 -0
  36. data/spec/unit/generator/entry_spec.rb +766 -0
  37. data/spec/unit/generator/generator_processor_spec.rb +203 -0
  38. data/spec/unit/generator/generator_spec.rb +47 -0
  39. data/spec/unit/generator/generator_tags_spec.rb +337 -0
  40. data/spec/unit/generator/section_spec.rb +43 -0
  41. data/spec/unit/octo_fetcher_spec.rb +590 -0
  42. data/spec/unit/options_spec.rb +67 -0
  43. data/spec/unit/parser_file_spec.rb +94 -0
  44. data/spec/unit/parser_spec.rb +54 -0
  45. data/spec/unit/reader_spec.rb +120 -0
  46. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits/when_API_is_valid/returns_commits.json +1 -0
  47. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits_before/when_API_is_valid/returns_commits.json +1 -0
  48. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issue_with_proper_key/values.json +1 -0
  49. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues.json +1 -0
  50. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues_with_labels.json +1 -0
  51. 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
  52. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_requests_with_labels.json +1 -0
  53. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid.json +1 -0
  54. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_correct_pull_request_keys.json +1 -0
  55. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_pull_requests.json +1 -0
  56. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid.json +1 -0
  57. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid/returns_commit.json +1 -0
  58. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid.json +1 -0
  59. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid/returns_date.json +1 -0
  60. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid.json +1 -0
  61. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid/populates_issues.json +1 -0
  62. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid.json +1 -0
  63. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags.json +1 -0
  64. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags_count.json +1 -0
  65. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid.json +1 -0
  66. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided/should_raise_Unauthorized_error.json +1 -0
  67. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided.json +1 -0
  68. metadata +250 -0
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitHubChangelogGenerator
4
+ class Generator
5
+ # delete all issues with labels from options[:exclude_labels] array
6
+ # @param [Array] issues
7
+ # @return [Array] filtered array
8
+ def exclude_issues_by_labels(issues)
9
+ return issues if !options[:exclude_labels] || options[:exclude_labels].empty?
10
+
11
+ issues.reject do |issue|
12
+ labels = issue["labels"].map { |l| l["name"] }
13
+ (labels & options[:exclude_labels]).any?
14
+ end
15
+ end
16
+
17
+ # Only include issues without labels if options[:add_issues_wo_labels]
18
+ # @param [Array] issues
19
+ # @return [Array] filtered array
20
+ def exclude_issues_without_labels(issues)
21
+ return issues if issues.empty?
22
+ return issues if issues.first.key?("pull_request") && options[:add_pr_wo_labels]
23
+ return issues if !issues.first.key?("pull_request") && options[:add_issues_wo_labels]
24
+
25
+ issues.reject do |issue|
26
+ issue["labels"].empty?
27
+ end
28
+ end
29
+
30
+ # @return [Array] filtered issues accourding milestone
31
+ def filter_by_milestone(filtered_issues, tag_name, all_issues)
32
+ remove_issues_in_milestones(filtered_issues)
33
+ unless tag_name.nil?
34
+ # add missed issues (according milestones)
35
+ issues_to_add = find_issues_to_add(all_issues, tag_name)
36
+
37
+ filtered_issues |= issues_to_add
38
+ end
39
+ filtered_issues
40
+ end
41
+
42
+ # Add all issues, that should be in that tag, according milestone
43
+ #
44
+ # @param [Array] all_issues
45
+ # @param [String] tag_name
46
+ # @return [Array] issues with milestone #tag_name
47
+ def find_issues_to_add(all_issues, tag_name)
48
+ all_issues.select do |issue|
49
+ if (milestone = issue["milestone"]).nil?
50
+ false
51
+ # check, that this milestone in tag list:
52
+ elsif (tag = find_tag_for_milestone(milestone)).nil?
53
+ false
54
+ else
55
+ tag["name"] == tag_name
56
+ end
57
+ end
58
+ end
59
+
60
+ # @return [Array] array with removed issues, that contain milestones with same name as a tag
61
+ def remove_issues_in_milestones(filtered_issues)
62
+ filtered_issues.select! do |issue|
63
+ # leave issues without milestones
64
+ if (milestone = issue["milestone"]).nil?
65
+ true
66
+ # remove issues of open milestones if option is set
67
+ elsif milestone["state"] == "open"
68
+ @options[:issues_of_open_milestones]
69
+ else
70
+ # check, that this milestone in tag list:
71
+ find_tag_for_milestone(milestone).nil?
72
+ end
73
+ end
74
+ end
75
+
76
+ def find_tag_for_milestone(milestone)
77
+ @filtered_tags.find { |tag| tag["name"] == milestone["title"] }
78
+ end
79
+
80
+ # Method filter issues, that belong only specified tag range
81
+ #
82
+ # @param [Array] issues issues to filter
83
+ # @param [Hash, Nil] newer_tag Tag to find PRs of. May be nil for unreleased section
84
+ # @return [Array] filtered issues
85
+ def filter_by_tag(issues, newer_tag = nil)
86
+ issues.select do |issue|
87
+ issue["first_occurring_tag"] == (newer_tag.nil? ? nil : newer_tag["name"])
88
+ end
89
+ end
90
+
91
+ # Method filter issues, that belong only specified tag range
92
+ # @param [Array] issues issues to filter
93
+ # @param [Symbol] hash_key key of date value default is :actual_date
94
+ # @param [Hash, Nil] older_tag all issues before this tag date will be excluded. May be nil, if it's first tag
95
+ # @param [Hash, Nil] newer_tag all issue after this tag will be excluded. May be nil for unreleased section
96
+ # @return [Array] filtered issues
97
+ def delete_by_time(issues, hash_key = "actual_date", older_tag = nil, newer_tag = nil)
98
+ # in case if not tags specified - return unchanged array
99
+ return issues if older_tag.nil? && newer_tag.nil?
100
+
101
+ older_tag = ensure_older_tag(older_tag, newer_tag)
102
+
103
+ newer_tag_time = newer_tag && get_time_of_tag(newer_tag)
104
+ older_tag_time = older_tag && get_time_of_tag(older_tag)
105
+
106
+ issues.select do |issue|
107
+ if issue[hash_key]
108
+ time = Time.parse(issue[hash_key].to_s).utc
109
+
110
+ tag_in_range_old = tag_newer_old_tag?(older_tag_time, time)
111
+
112
+ tag_in_range_new = tag_older_new_tag?(newer_tag_time, time)
113
+
114
+ tag_in_range = tag_in_range_old && tag_in_range_new
115
+
116
+ tag_in_range
117
+ else
118
+ false
119
+ end
120
+ end
121
+ end
122
+
123
+ def ensure_older_tag(older_tag, newer_tag)
124
+ return older_tag if older_tag
125
+
126
+ idx = sorted_tags.index { |t| t["name"] == newer_tag["name"] }
127
+ # skip if we are already at the oldest element
128
+ return if idx == sorted_tags.size - 1
129
+
130
+ sorted_tags[idx - 1]
131
+ end
132
+
133
+ def tag_older_new_tag?(newer_tag_time, time)
134
+ if newer_tag_time.nil?
135
+ true
136
+ else
137
+ time <= newer_tag_time
138
+ end
139
+ end
140
+
141
+ def tag_newer_old_tag?(older_tag_time, time)
142
+ if older_tag_time.nil?
143
+ true
144
+ else
145
+ time > older_tag_time
146
+ end
147
+ end
148
+
149
+ # Include issues with labels, specified in :include_labels
150
+ # @param [Array] issues to filter
151
+ # @return [Array] filtered array of issues
152
+ def include_issues_by_labels(issues)
153
+ filtered_issues = filter_by_include_labels(issues)
154
+ filter_wo_labels(filtered_issues)
155
+ end
156
+
157
+ # @param [Array] items Issues & PRs to filter when without labels
158
+ # @return [Array] Issues & PRs without labels or empty array if
159
+ # add_issues_wo_labels or add_pr_wo_labels are false
160
+ def filter_wo_labels(items)
161
+ if items.any? && items.first.key?("pull_request")
162
+ return items if options[:add_pr_wo_labels]
163
+ elsif options[:add_issues_wo_labels]
164
+ return items
165
+ end
166
+ # The default is to filter items without labels
167
+ items.select { |item| item["labels"].map { |l| l["name"] }.any? }
168
+ end
169
+
170
+ # @todo Document this
171
+ # @param [Object] issues
172
+ def filter_by_include_labels(issues)
173
+ if options[:include_labels].nil?
174
+ issues
175
+ else
176
+ issues.select do |issue|
177
+ labels = issue["labels"].map { |l| l["name"] } & options[:include_labels]
178
+ labels.any? || issue["labels"].empty?
179
+ end
180
+ end
181
+ end
182
+
183
+ # General filtered function
184
+ #
185
+ # @param [Array] all_issues PRs or issues
186
+ # @return [Array] filtered issues
187
+ def filter_array_by_labels(all_issues)
188
+ filtered_issues = include_issues_by_labels(all_issues)
189
+ filtered_issues = exclude_issues_by_labels(filtered_issues)
190
+ exclude_issues_without_labels(filtered_issues)
191
+ end
192
+
193
+ # Filter issues according labels
194
+ # @return [Array] Filtered issues
195
+ def get_filtered_issues(issues)
196
+ issues = filter_array_by_labels(issues)
197
+ puts "Filtered issues: #{issues.count}" if options[:verbose]
198
+ issues
199
+ end
200
+
201
+ # This method fetches missing params for PR and filter them by specified options
202
+ # It include add all PR's with labels from options[:include_labels] array
203
+ # And exclude all from :exclude_labels array.
204
+ # @return [Array] filtered PR's
205
+ def get_filtered_pull_requests(pull_requests)
206
+ pull_requests = filter_array_by_labels(pull_requests)
207
+ pull_requests = filter_merged_pull_requests(pull_requests)
208
+ puts "Filtered pull requests: #{pull_requests.count}" if options[:verbose]
209
+ pull_requests
210
+ end
211
+
212
+ # This method filter only merged PR and
213
+ # fetch missing required attributes for pull requests
214
+ # :merged_at - is a date, when issue PR was merged.
215
+ # More correct to use merged date, rather than closed date.
216
+ def filter_merged_pull_requests(pull_requests)
217
+ print "Fetching merged dates...\r" if options[:verbose]
218
+ closed_pull_requests = @fetcher.fetch_closed_pull_requests
219
+
220
+ pull_requests.each do |pr|
221
+ fetched_pr = closed_pull_requests.find do |fpr|
222
+ fpr["number"] == pr["number"]
223
+ end
224
+ if fetched_pr
225
+ pr["merged_at"] = fetched_pr["merged_at"]
226
+ closed_pull_requests.delete(fetched_pr)
227
+ end
228
+ end
229
+
230
+ pull_requests.reject! do |pr|
231
+ pr["merged_at"].nil?
232
+ end
233
+
234
+ pull_requests
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitHubChangelogGenerator
4
+ class Generator
5
+ # fetch, filter tags, fetch dates and sort them in time order
6
+ def fetch_and_filter_tags
7
+ since_tag
8
+ due_tag
9
+
10
+ all_tags = @fetcher.fetch_all_tags
11
+ fetch_tags_dates(all_tags) # Creates a Hash @tag_times_hash
12
+ all_sorted_tags = sort_tags_by_date(all_tags)
13
+
14
+ @sorted_tags = filter_included_tags(all_sorted_tags)
15
+ @sorted_tags = filter_excluded_tags(@sorted_tags)
16
+ @filtered_tags = get_filtered_tags(@sorted_tags)
17
+ @tag_section_mapping = build_tag_section_mapping(@filtered_tags, @filtered_tags)
18
+
19
+ @filtered_tags
20
+ end
21
+
22
+ # @param [Array] section_tags are the tags that need a subsection output
23
+ # @param [Array] filtered_tags is the list of filtered tags ordered from newest -> oldest
24
+ # @return [Hash] key is the tag to output, value is an array of [Left Tag, Right Tag]
25
+ # PRs to include in this section will be >= [Left Tag Date] and <= [Right Tag Date]
26
+ # rubocop:disable Style/For - for allows us to be more concise
27
+ def build_tag_section_mapping(section_tags, filtered_tags)
28
+ tag_mapping = {}
29
+ for i in 0..(section_tags.length - 1)
30
+ tag = section_tags[i]
31
+
32
+ # Don't create section header for the "since" tag
33
+ next if since_tag && tag["name"] == since_tag
34
+
35
+ # Don't create a section header for the first tag in between_tags
36
+ next if options[:between_tags] && tag == section_tags.last
37
+
38
+ # Don't create a section header for excluded tags
39
+ next unless filtered_tags.include?(tag)
40
+
41
+ older_tag = section_tags[i + 1]
42
+ tag_mapping[tag] = [older_tag, tag]
43
+ end
44
+ tag_mapping
45
+ end
46
+ # rubocop:enable Style/For
47
+
48
+ # Sort all tags by date, newest to oldest
49
+ def sort_tags_by_date(tags)
50
+ puts "Sorting tags..." if options[:verbose]
51
+ tags.sort_by! do |x|
52
+ get_time_of_tag(x)
53
+ end.reverse!
54
+ end
55
+
56
+ # Returns date for given GitHub Tag hash
57
+ #
58
+ # Memoize the date by tag name.
59
+ #
60
+ # @param [Hash] tag_name
61
+ #
62
+ # @return [Time] time of specified tag
63
+ def get_time_of_tag(tag_name)
64
+ raise ChangelogGeneratorError, "tag_name is nil" if tag_name.nil?
65
+
66
+ name_of_tag = tag_name.fetch("name")
67
+ time_for_tag_name = @tag_times_hash[name_of_tag]
68
+ return time_for_tag_name if time_for_tag_name
69
+
70
+ @fetcher.fetch_date_of_tag(tag_name).tap do |time_string|
71
+ @tag_times_hash[name_of_tag] = time_string
72
+ end
73
+ end
74
+
75
+ # Detect link, name and time for specified tag.
76
+ #
77
+ # @param [Hash] newer_tag newer tag. Can be nil, if it's Unreleased section.
78
+ # @return [Array] link, name and time of the tag
79
+ def detect_link_tag_time(newer_tag)
80
+ # if tag is nil - set current time
81
+ newer_tag_time = newer_tag.nil? ? Time.new.getutc : get_time_of_tag(newer_tag)
82
+
83
+ # if it's future release tag - set this value
84
+ if newer_tag.nil? && options[:future_release]
85
+ newer_tag_name = options[:future_release]
86
+ newer_tag_link = options[:future_release]
87
+ else
88
+ # put unreleased label if there is no name for the tag
89
+ newer_tag_name = newer_tag.nil? ? options[:unreleased_label] : newer_tag["name"]
90
+ newer_tag_link = newer_tag.nil? ? "HEAD" : newer_tag_name
91
+ end
92
+ [newer_tag_link, newer_tag_name, newer_tag_time]
93
+ end
94
+
95
+ # @return [Object] try to find newest tag using #Reader and :base option if specified otherwise returns nil
96
+ def since_tag
97
+ @since_tag ||= options.fetch(:since_tag) { version_of_first_item }
98
+ end
99
+
100
+ def due_tag
101
+ @due_tag ||= options.fetch(:due_tag, nil)
102
+ end
103
+
104
+ def version_of_first_item
105
+ return unless File.file?(options[:base].to_s)
106
+
107
+ sections = GitHubChangelogGenerator::Reader.new.read(options[:base])
108
+ sections.first["version"] if sections && sections.any?
109
+ end
110
+
111
+ # Return tags after filtering tags in lists provided by option: --exclude-tags
112
+ #
113
+ # @return [Array]
114
+ def get_filtered_tags(all_tags)
115
+ filtered_tags = filter_since_tag(all_tags)
116
+ filter_due_tag(filtered_tags)
117
+ end
118
+
119
+ # @param [Array] all_tags all tags
120
+ # @return [Array] filtered tags according :since_tag option
121
+ def filter_since_tag(all_tags)
122
+ return all_tags unless (tag = since_tag)
123
+
124
+ raise ChangelogGeneratorError, "Error: can't find tag #{tag}, specified with --since-tag option." if all_tags.none? { |t| t["name"] == tag }
125
+
126
+ if (idx = all_tags.index { |t| t["name"] == tag })
127
+ all_tags[0..idx]
128
+ else
129
+ []
130
+ end
131
+ end
132
+
133
+ # @param [Array] all_tags all tags
134
+ # @return [Array] filtered tags according :due_tag option
135
+ def filter_due_tag(all_tags)
136
+ return all_tags unless (tag = due_tag)
137
+
138
+ raise ChangelogGeneratorError, "Error: can't find tag #{tag}, specified with --due-tag option." if all_tags.none? { |t| t["name"] == tag }
139
+
140
+ if (idx = all_tags.index { |t| t["name"] == tag }) > 0
141
+ all_tags[(idx + 1)..-1]
142
+ else
143
+ []
144
+ end
145
+ end
146
+
147
+ # @param [Array] all_tags all tags
148
+ # @return [Array] filtered tags according to :include_tags_regex option
149
+ def filter_included_tags(all_tags)
150
+ if options[:include_tags_regex]
151
+ regex = Regexp.new(options[:include_tags_regex])
152
+ all_tags.select { |tag| regex =~ tag["name"] }
153
+ else
154
+ all_tags
155
+ end
156
+ end
157
+
158
+ # @param [Array] all_tags all tags
159
+ # @return [Array] filtered tags according :exclude_tags or :exclude_tags_regex option
160
+ def filter_excluded_tags(all_tags)
161
+ if options[:exclude_tags]
162
+ apply_exclude_tags(all_tags)
163
+ elsif options[:exclude_tags_regex]
164
+ apply_exclude_tags_regex(all_tags)
165
+ else
166
+ all_tags
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ def apply_exclude_tags(all_tags)
173
+ if options[:exclude_tags].is_a?(Regexp)
174
+ filter_tags_with_regex(all_tags, options[:exclude_tags], "--exclude-tags")
175
+ else
176
+ filter_exact_tags(all_tags)
177
+ end
178
+ end
179
+
180
+ def apply_exclude_tags_regex(all_tags)
181
+ regex = Regexp.new(options[:exclude_tags_regex])
182
+ filter_tags_with_regex(all_tags, regex, "--exclude-tags-regex")
183
+ end
184
+
185
+ def filter_tags_with_regex(all_tags, regex, regex_option_name)
186
+ warn_if_nonmatching_regex(all_tags, regex, regex_option_name)
187
+ all_tags.reject { |tag| regex =~ tag["name"] }
188
+ end
189
+
190
+ def filter_exact_tags(all_tags)
191
+ options[:exclude_tags].each do |tag|
192
+ warn_if_tag_not_found(all_tags, tag)
193
+ end
194
+ all_tags.reject { |tag| options[:exclude_tags].include?(tag["name"]) }
195
+ end
196
+
197
+ def warn_if_nonmatching_regex(all_tags, regex, regex_option_name)
198
+ return if all_tags.any? { |t| regex.match?(t["name"]) }
199
+
200
+ Helper.log.warn "Warning: unable to reject any tag, using regex "\
201
+ "#{regex.inspect} in #{regex_option_name} option."
202
+ end
203
+
204
+ def warn_if_tag_not_found(all_tags, tag)
205
+ return if all_tags.any? { |t| t["name"] == tag }
206
+
207
+ Helper.log.warn("Warning: can't find tag #{tag}, specified with --exclude-tags option.")
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitHubChangelogGenerator
4
+ # This class generates the content for a single section of a changelog entry.
5
+ # It turns the tagged issues and PRs into a well-formatted list of changes to
6
+ # be later incorporated into a changelog entry.
7
+ #
8
+ # @see GitHubChangelogGenerator::Entry
9
+ class Section
10
+ # @return [String]
11
+ attr_accessor :name
12
+
13
+ # @return [String] a merge prefix, or an issue prefix
14
+ attr_reader :prefix
15
+
16
+ # @return [Array<Hash>]
17
+ attr_reader :issues
18
+
19
+ # @return [Array<String>]
20
+ attr_reader :labels
21
+
22
+ # @return [Boolean]
23
+ attr_reader :body_only
24
+
25
+ # @return [Options]
26
+ attr_reader :options
27
+
28
+ def initialize(opts = {})
29
+ @name = opts[:name]
30
+ @prefix = opts[:prefix]
31
+ @labels = opts[:labels] || []
32
+ @issues = opts[:issues] || []
33
+ @options = opts[:options] || Options.new({})
34
+ @body_only = opts[:body_only] || false
35
+ @entry = Entry.new(options)
36
+ end
37
+
38
+ # Returns the content of a section.
39
+ #
40
+ # @return [String] Generated section content
41
+ def generate_content
42
+ content = +""
43
+
44
+ if @issues.any?
45
+ content += "#{@prefix}\n\n" unless @options[:simple_list] || @prefix.blank?
46
+ @issues.each do |issue|
47
+ merge_string = get_string_for_issue(issue)
48
+ content += "- " unless @body_only
49
+ content += "#{merge_string}\n"
50
+ end
51
+ content += "\n"
52
+ end
53
+ content
54
+ end
55
+
56
+ private
57
+
58
+ # Parse issue and generate single line formatted issue line.
59
+ #
60
+ # Example output:
61
+ # - Add coveralls integration [\#223](https://github.com/github-changelog-generator/github-changelog-generator/pull/223) (@github-changelog-generator)
62
+ #
63
+ # @param [Hash] issue Fetched issue from GitHub
64
+ # @return [String] Markdown-formatted single issue
65
+ def get_string_for_issue(issue)
66
+ encapsulated_title = encapsulate_string issue["title"]
67
+
68
+ title_with_number = "#{encapsulated_title} [\\##{issue['number']}](#{issue['html_url']})"
69
+ title_with_number = "#{title_with_number}#{@entry.line_labels_for(issue)}" if @options[:issue_line_labels].present?
70
+ line = issue_line_with_user(title_with_number, issue)
71
+ issue_line_with_body(line, issue)
72
+ end
73
+
74
+ def issue_line_with_body(line, issue)
75
+ return normalize_body(issue["body"]) if @body_only && issue["body"].present?
76
+ return line if !@options[:issue_line_body] || issue["body"].blank?
77
+
78
+ # get issue body till first line break
79
+ body_paragraph = body_till_first_break(normalize_body(issue["body"]))
80
+ # remove spaces from beginning of the string
81
+ body_paragraph.rstrip!
82
+ # encapsulate to md
83
+ encapsulated_body = " \n#{encapsulate_string(body_paragraph)}"
84
+
85
+ "**#{line}** #{encapsulated_body}"
86
+ end
87
+
88
+ # Normalize line endings from CRLF to LF
89
+ def normalize_body(body)
90
+ body.gsub(/\r?\n/, "\n")
91
+ end
92
+
93
+ def body_till_first_break(body)
94
+ body.split(/\n/, 2).first
95
+ end
96
+
97
+ def issue_line_with_user(line, issue)
98
+ return line if !@options[:author] || issue["pull_request"].nil?
99
+
100
+ user = issue["user"]
101
+ return "#{line} ({Null user})" unless user
102
+
103
+ if @options[:usernames_as_github_logins]
104
+ "#{line} (@#{user['login']})"
105
+ else
106
+ "#{line} ([#{user['login']}](#{user['html_url']}))"
107
+ end
108
+ end
109
+
110
+ ENCAPSULATED_CHARACTERS = %w(< > * _ \( \) [ ] #)
111
+
112
+ # Encapsulate characters to make Markdown look as expected.
113
+ #
114
+ # @param [String] string
115
+ # @return [String] encapsulated input string
116
+ def encapsulate_string(string)
117
+ string = string.gsub('\\', '\\\\')
118
+
119
+ ENCAPSULATED_CHARACTERS.each do |char|
120
+ # Only replace char with escaped version if it isn't inside backticks (markdown inline code).
121
+ # This relies on each opening '`' being closed (ie an even number in total).
122
+ # A char is *outside* backticks if there is an even number of backticks following it.
123
+ string = string.gsub(%r{#{Regexp.escape(char)}(?=([^`]*`[^`]*`)*[^`]*$)}, "\\#{char}")
124
+ end
125
+
126
+ string
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "rainbow"
5
+
6
+ module GitHubChangelogGenerator
7
+ module Helper
8
+ # @return true if the currently running program is a unit test
9
+ def self.test?
10
+ defined? SpecHelper
11
+ end
12
+
13
+ # :nocov:
14
+ @log ||= if test?
15
+ Logger.new(nil) # don't show any logs when running tests
16
+ else
17
+ Logger.new($stdout)
18
+ end
19
+ @log.formatter = proc do |severity, _datetime, _progname, msg|
20
+ string = "#{msg}\n"
21
+ case severity
22
+ when "DEBUG" then Rainbow(string).magenta
23
+ when "INFO" then Rainbow(string).white
24
+ when "WARN" then Rainbow(string).yellow
25
+ when "ERROR" then Rainbow(string).red
26
+ when "FATAL" then Rainbow(string).red.bright
27
+ else string
28
+ end
29
+ end
30
+ # :nocov:
31
+
32
+ # Logging happens using this method
33
+ class << self
34
+ attr_reader :log
35
+ end
36
+ end
37
+ end