px_github_changelog_generator 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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