From 1c4ef5efe8524c215d51e4fb4cb187b897305a27 Mon Sep 17 00:00:00 2001 From: Hunter Haugen Date: Fri, 26 Jan 2018 16:12:25 -0800 Subject: [PATCH 1/2] Fix unlabeled, mixed labels, and unmapped labels handling - Issues/PRs filtered by --exclude-labels/--include-labels should never be added to sections. - Issues/PRs with labels mapping to multiple sections should map to default sections order of breaking, then enhancement, then fix. - Issues/PRs with labels mapping to multiple sections should map to default sections then the first-matching section in --add-sections. - Issues/PRs with labels mapping to multiple sections should map to the first-matching section in --configure-sections. - Issues/PRs with some mapped labels and some unmapped labels should respect the mapping rather than behaving like unmapped issues. - Issues/PRs with only unmapped labels should be added to the --merge-prefix/--issue-prefix section. - Issues/PRs with no labels should be added to the --merge-prefix/--issue-prefix section unless --add-pr-wo-labels/--add-issues-wo-labels is false. - Filter both PRs and issues equally. --- .../generator/entry.rb | 191 +++++----- .../generator/generator.rb | 2 +- .../generator/generator_processor.rb | 27 +- .../generator/section.rb | 4 +- spec/unit/generator/entry_spec.rb | 325 ++++++++++++------ .../generator/generator_processor_spec.rb | 2 +- spec/unit/generator/generator_tags_spec.rb | 2 +- spec/unit/parse_file_spec.rb | 2 +- 8 files changed, 336 insertions(+), 219 deletions(-) diff --git a/lib/github_changelog_generator/generator/entry.rb b/lib/github_changelog_generator/generator/entry.rb index cbc3e06fb..e292cc3fb 100644 --- a/lib/github_changelog_generator/generator/entry.rb +++ b/lib/github_changelog_generator/generator/entry.rb @@ -25,26 +25,24 @@ def initialize(options = Options.new({})) # @param [String] newer_tag_name Name of the newer tag. Could be nil for `Unreleased` section. # @param [String] newer_tag_link Name of the newer tag. Could be "HEAD" for `Unreleased` section. # @param [Time] newer_tag_time Time of the newer tag - # @param [Hash, nil] older_tag Older tag, used for the links. Could be nil for last tag. - # @return [String] Ready and parsed section - def create_entry_for_tag(pull_requests, issues, newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name) # rubocop:disable Metrics/ParameterLists + # @param [Hash, nil] older_tag_name Older tag, used for the links. Could be nil for last tag. + # @return [String] Ready and parsed section content. + def generate_entry_for_tag(pull_requests, issues, newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name) # rubocop:disable Metrics/ParameterLists github_site = @options[:github_site] || "https://github.com" project_url = "#{github_site}/#{@options[:user]}/#{@options[:project]}" - set_sections_and_maps + create_sections @content = generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url) - @content += generate_body(pull_requests, issues) - @content end private - # Creates section objects and the label and section maps needed for - # sorting - def set_sections_and_maps + # Creates section objects for this entry. + # @return [Nil] + def create_sections @sections = if @options.configure_sections? parse_sections(@options[:configure_sections]) elsif @options.add_sections? @@ -52,15 +50,14 @@ def set_sections_and_maps else default_sections end - - @lmap = label_map - @smap = section_map + nil end - # Turns a string from the commandline into an array of Section objects + # Turns the argument from the commandline of --configure-sections or + # --add-sections into an array of Section objects. # - # @param [String, Hash] either string or hash describing sections - # @return [Array] array of Section objects + # @param [String, Hash] sections_desc Either string or hash describing sections + # @return [Array] Parsed section objects. def parse_sections(sections_desc) require "json" @@ -77,34 +74,14 @@ def parse_sections(sections_desc) end end - # Creates a hash map of labels => section objects + # Generates header text for an entry. # - # @return [Hash] map of labels => section objects - def label_map - @sections.each_with_object({}) do |section_obj, memo| - section_obj.labels.each do |label| - memo[label] = section_obj.name - end - end - end - - # Creates a hash map of 'section name' => section object - # - # @return [Hash] map of 'section name' => section object - def section_map - @sections.each_with_object({}) do |section, memo| - memo[section.name] = section - end - end - - # It generates header text for an entry with specific parameters. - # - # @param [String] newer_tag_name - name of newer tag - # @param [String] newer_tag_link - used for links. Could be same as #newer_tag_name or some specific value, like HEAD - # @param [Time] newer_tag_time - time, when newer tag created - # @param [String] older_tag_name - tag name, used for links. - # @param [String] project_url - url for current project. - # @return [String] - Header text for a changelog entry. + # @param [String] newer_tag_name The name of a newer tag + # @param [String] newer_tag_link Used for URL generation. Could be same as #newer_tag_name or some specific value, like HEAD + # @param [Time] newer_tag_time Time when the newer tag was created + # @param [String] older_tag_name The name of an older tag; used for URLs. + # @param [String] project_url URL for the current project. + # @return [String] Header text content. def generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url) header = "" @@ -135,94 +112,92 @@ def generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_na # # @param [Array] pull_requests # @param [Array] issues - # @returns [String] ready-to-go tag body + # @return [String] Content generated from sections of sorted issues & PRs. def generate_body(pull_requests, issues) - body = "" - body += main_sections_to_log(pull_requests, issues) - body += merged_section_to_log(pull_requests) if @options[:pulls] && @options[:add_pr_wo_labels] - body + sort_into_sections(pull_requests, issues) + @sections.map(&:generate_content).join end - # Generates main sections for a tag + # Default sections to used when --configure-sections is not set. # - # @param [Array] pull_requests - # @param [Array] issues - # @return [string] ready-to-go sub-sections - def main_sections_to_log(pull_requests, issues) - if @options[:issues] - sections_to_log = parse_by_sections(pull_requests, issues) - - sections_to_log.map(&:generate_content).join - end - end - - # Generates section for prs with no labels (for a tag) - # - # @param [Array] pull_requests - # @return [string] ready-to-go sub-section - def merged_section_to_log(pull_requests) - merged = Section.new(name: "merged", prefix: @options[:merge_prefix], labels: [], issues: pull_requests, options: @options) - @sections << merged unless @sections.find { |section| section.name == "merged" } - merged.generate_content - end - - # Set of default sections for backwards-compatibility/defaults - # - # @return [Array] array of Section objects + # @return [Array] Section objects. def default_sections [ Section.new(name: "breaking", prefix: @options[:breaking_prefix], labels: @options[:breaking_labels], options: @options), Section.new(name: "enhancements", prefix: @options[:enhancement_prefix], labels: @options[:enhancement_labels], options: @options), - Section.new(name: "bugs", prefix: @options[:bug_prefix], labels: @options[:bug_labels], options: @options), - Section.new(name: "issues", prefix: @options[:issue_prefix], labels: @options[:issue_labels], options: @options) + Section.new(name: "bugs", prefix: @options[:bug_prefix], labels: @options[:bug_labels], options: @options) ] end - # This method sorts issues by types - # (bugs, features, or just closed issues) by labels + # Sorts issues and PRs into entry sections by labels and lack of labels. # # @param [Array] pull_requests # @param [Array] issues - # @return [Hash] Mapping of filtered arrays: (Bugs, Enhancements, Breaking stuff, Issues) - def parse_by_sections(pull_requests, issues) - issues.each do |dict| - added = false - - dict["labels"].each do |label| - break if @lmap[label["name"]].nil? - @smap[@lmap[label["name"]]].issues << dict - added = true + # @return [Nil] + def sort_into_sections(pull_requests, issues) + if @options[:issues] + unmapped_issues = sort_labeled_issues(issues) + add_unmapped_section(unmapped_issues) + end + if @options[:pulls] + unmapped_pull_requests = sort_labeled_issues(pull_requests) + add_unmapped_section(unmapped_pull_requests) + end + nil + end - break if added - end - if @smap["issues"] - @sections.find { |sect| sect.name == "issues" }.issues << dict unless added + # Iterates through sections and sorts labeled issues into them based on + # the label mapping. Returns any unmapped or unlabeled issues. + # + # @param [Array] issues Issues or pull requests. + # @return [Array] Issues that were not mapped into any sections. + def sort_labeled_issues(issues) + sorted_issues = [] + issues.each do |issue| + label_names = issue["labels"].collect { |l| l["name"] } + + # Add PRs in the order of the @sections array. This will either be the + # default sections followed by any --add-sections sections in + # user-defined order, or --configure-sections in user-defined order. + # Ignore the order of the issue labels from github which cannot be + # controled by the user. + @sections.each do |section| + unless (section.labels & label_names).empty? + section.issues << issue + sorted_issues << issue + break + end end end - sort_pull_requests(pull_requests) + issues - sorted_issues end - # This method iterates through PRs and sorts them into sections + # Creates a section for issues/PRs with no labels or no mapped labels. # - # @param [Array] pull_requests - # @param [Hash] sections - # @return [Hash] sections - def sort_pull_requests(pull_requests) - added_pull_requests = [] - pull_requests.each do |pr| - added = false - - pr["labels"].each do |label| - break if @lmap[label["name"]].nil? - @smap[@lmap[label["name"]]].issues << pr - added_pull_requests << pr - added = true - - break if added + # @param [Array] issues + # @return [Nil] + def add_unmapped_section(issues) + unless issues.empty? + # Distinguish between issues and pull requests + if issues.first.key?("pull_request") + name = "merged" + prefix = @options[:merge_prefix] + add_wo_labels = @options[:add_pr_wo_labels] + else + name = "issues" + prefix = @options[:issue_prefix] + add_wo_labels = @options[:add_issues_wo_labels] end + add_issues = if add_wo_labels + issues + else + # Only add unmapped issues + issues.select { |issue| issue["labels"].any? } + end + merged = Section.new(name: name, prefix: prefix, labels: [], issues: add_issues, options: @options) unless add_issues.empty? + @sections << merged end - added_pull_requests.each { |req| pull_requests.delete(req) } - @sections + nil end def line_labels_for(issue) diff --git a/lib/github_changelog_generator/generator/generator.rb b/lib/github_changelog_generator/generator/generator.rb index dbd48d247..75189ce5a 100644 --- a/lib/github_changelog_generator/generator/generator.rb +++ b/lib/github_changelog_generator/generator/generator.rb @@ -89,7 +89,7 @@ def generate_entry_between_tags(older_tag, newer_tag) older_tag["name"] end - Entry.new(options).create_entry_for_tag(filtered_pull_requests, filtered_issues, newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name) + Entry.new(options).generate_entry_for_tag(filtered_pull_requests, filtered_issues, newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name) end # Filters issues and pull requests based on, respectively, `closed_at` and `merged_at` diff --git a/lib/github_changelog_generator/generator/generator_processor.rb b/lib/github_changelog_generator/generator/generator_processor.rb index 744cc9309..44a4c1e46 100644 --- a/lib/github_changelog_generator/generator/generator_processor.rb +++ b/lib/github_changelog_generator/generator/generator_processor.rb @@ -2,7 +2,7 @@ module GitHubChangelogGenerator class Generator - # delete all labels with labels from options[:exclude_labels] array + # delete all issues with labels from options[:exclude_labels] array # @param [Array] issues # @return [Array] filtered array def exclude_issues_by_labels(issues) @@ -14,6 +14,19 @@ def exclude_issues_by_labels(issues) end end + # Only include issues without labels if options[:add_issues_wo_labels] + # @param [Array] issues + # @return [Array] filtered array + def exclude_issues_without_labels(issues) + return issues if issues.empty? + return issues if issues.first.key?("pull_request") && options[:add_pr_wo_labels] + return issues if !issues.first.key?("pull_request") && options[:add_issues_wo_labels] + + issues.reject do |issue| + issue["labels"].empty? + end + end + # @return [Array] filtered issues accourding milestone def filter_by_milestone(filtered_issues, tag_name, all_issues) remove_issues_in_milestones(filtered_issues) @@ -130,15 +143,18 @@ def include_issues_by_labels(issues) filtered_issues end - # @return [Array] issues without labels or empty array if add_issues_wo_labels is false + # @param [Array] issues Issues & PRs to filter when without labels + # @return [Array] Issues & PRs without labels or empty array if + # add_issues_wo_labels or add_pr_wo_labels are false def filter_wo_labels(issues) - if options[:add_issues_wo_labels] + if (!issues.empty? && issues.first.key?("pull_requests") && options[:add_pr_wo_labels]) || options[:add_issues_wo_labels] issues else issues.select { |issue| issue["labels"].map { |l| l["name"] }.any? } end end + # @todo Document this def filter_by_include_labels(issues) if options[:include_labels].nil? issues @@ -152,11 +168,12 @@ def filter_by_include_labels(issues) # General filtered function # - # @param [Array] all_issues + # @param [Array] all_issues PRs or issues # @return [Array] filtered issues def filter_array_by_labels(all_issues) filtered_issues = include_issues_by_labels(all_issues) - exclude_issues_by_labels(filtered_issues) + filtered_issues = exclude_issues_by_labels(filtered_issues) + exclude_issues_without_labels(filtered_issues) end # Filter issues according labels diff --git a/lib/github_changelog_generator/generator/section.rb b/lib/github_changelog_generator/generator/section.rb index 71c7ed342..13850cdce 100644 --- a/lib/github_changelog_generator/generator/section.rb +++ b/lib/github_changelog_generator/generator/section.rb @@ -15,8 +15,8 @@ def initialize(opts = {}) @options = opts[:options] || Options.new({}) end - # @param [Array] issues List of issues on sub-section - # @param [String] prefix Name of sub-section + # Returns the content of a section. + # # @return [String] Generate section content def generate_content content = "" diff --git a/spec/unit/generator/entry_spec.rb b/spec/unit/generator/entry_spec.rb index 45716fe05..4ea0971c9 100644 --- a/spec/unit/generator/entry_spec.rb +++ b/spec/unit/generator/entry_spec.rb @@ -24,7 +24,8 @@ def pr(title, labels, number = "1", user = { "login" => "user" }) "labels" => labels.map { |l| label(l) }, "number" => number, "html_url" => "https://github.com/owner/repo/pull/#{number}", - "user" => user.merge("html_url" => "https://github.com/#{user['login']}") + "user" => user.merge("html_url" => "https://github.com/#{user['login']}"), + "merged_at" => Time.now.utc } end @@ -36,71 +37,179 @@ def default_sections %w[enhancements bugs breaking issues] end - describe "#create_entry_for_tag" do - let(:options) do - Parser.default_options.merge( - user: "owner", - project: "repo", - bug_labels: ["bug"], - enhancement_labels: ["enhancement"], - breaking_labels: ["breaking"] - ) - end + # Default to no issues or PRs. + let(:issues) { [] } + let(:pull_requests) { [] } + + # Default to standard options minus verbose to avoid output during testing. + let(:options) do + Parser.default_options.merge(verbose: false) + end + + # Mock out fake github fetching for the issues/pull_requests lets and then + # expose filtering from the GitHubChangelogGenerator::Generator class + # instance for end-to-end entry testing. + let(:generator) do + fake_fetcher = instance_double( + "fetcher", + fetch_closed_issues_and_pr: [issues, pull_requests], + fetch_closed_pull_requests: [], + fetch_events_async: issues + pull_requests + ) + allow(GitHubChangelogGenerator::OctoFetcher).to receive(:new).and_return(fake_fetcher) + generator = GitHubChangelogGenerator::Generator.new(options) + generator.send(:fetch_issues_and_pr) + generator + end + let(:filtered_issues) do + generator.instance_variable_get :@issues + end + let(:filtered_pull_requests) do + generator.instance_variable_get :@pull_requests + end + let(:entry_sections) do + subject.send(:create_sections) + # In normal usage, the entry generation would have received filtered + # issues and pull requests so replicate that here for ease of testing. + subject.send(:sort_into_sections, filtered_pull_requests, filtered_issues) + subject.instance_variable_get :@sections + end + describe "#generate_entry_for_tag" do let(:issues) do [ issue("no labels", [], "5", "login" => "user1"), issue("enhancement", ["enhancement"], "6", "login" => "user2"), issue("bug", ["bug"], "7", "login" => "user1"), issue("breaking", ["breaking"], "8", "login" => "user5"), - issue("all the labels", %w[enhancement bug breaking], "9", "login" => "user9") + issue("all the labels", %w[enhancement bug breaking], "9", "login" => "user9"), + issue("all the labels different order", %w[breaking enhancement bug], "10", "login" => "user5"), + issue("some unmapped labels", %w[tests-fail bug], "11", "login" => "user5"), + issue("no mapped labels", %w[docs maintenance], "12", "login" => "user5") ] end let(:pull_requests) do [ - pr("no labels", [], "10", "login" => "user1"), - pr("enhancement", ["enhancement"], "11", "login" => "user5"), - pr("bug", ["bug"], "12", "login" => "user5"), - pr("breaking", ["breaking"], "13", "login" => "user5"), - pr("all the labels", %w[enhancement bug breaking], "14", "login" => "user5") + pr("no labels", [], "20", "login" => "user1"), + pr("enhancement", ["enhancement"], "21", "login" => "user5"), + pr("bug", ["bug"], "22", "login" => "user5"), + pr("breaking", ["breaking"], "23", "login" => "user5"), + pr("all the labels", %w[enhancement bug breaking], "24", "login" => "user5"), + pr("all the labels different order", %w[breaking enhancement bug], "25", "login" => "user5"), + pr("some unmapped labels", %w[tests-fail bug], "26", "login" => "user5"), + pr("no mapped labels", %w[docs maintenance], "27", "login" => "user5") ] end subject { described_class.new(options) } - it "generates a header and body" do - changelog = <<-CHANGELOG.gsub(/^ {8}/, "") - ## [1.0.1](https://github.com/owner/repo/tree/1.0.1) (2017-12-04) + describe "include issues without labels" do + let(:options) do + Parser.default_options.merge( + user: "owner", + project: "repo", + bug_labels: ["bug"], + enhancement_labels: ["enhancement"], + breaking_labels: ["breaking"], + verbose: false + ) + end + + it "generates a header and body" do + changelog = <<-CHANGELOG.gsub(/^ {10}/, "") + ## [1.0.1](https://github.com/owner/repo/tree/1.0.1) (2017-12-04) - [Full Changelog](https://github.com/owner/repo/compare/1.0.0...1.0.1) + [Full Changelog](https://github.com/owner/repo/compare/1.0.0...1.0.1) - **Breaking changes:** + **Breaking changes:** - - issue breaking [\\#8](https://github.com/owner/repo/issue/8) - - pr breaking [\\#13](https://github.com/owner/repo/pull/13) ([user5](https://github.com/user5)) + - issue breaking [\\#8](https://github.com/owner/repo/issue/8) + - issue all the labels [\\#9](https://github.com/owner/repo/issue/9) + - issue all the labels different order [\\#10](https://github.com/owner/repo/issue/10) + - pr breaking [\\#23](https://github.com/owner/repo/pull/23) ([user5](https://github.com/user5)) + - pr all the labels [\\#24](https://github.com/owner/repo/pull/24) ([user5](https://github.com/user5)) + - pr all the labels different order [\\#25](https://github.com/owner/repo/pull/25) ([user5](https://github.com/user5)) - **Implemented enhancements:** + **Implemented enhancements:** - - issue enhancement [\\#6](https://github.com/owner/repo/issue/6) - - issue all the labels [\\#9](https://github.com/owner/repo/issue/9) - - pr enhancement [\\#11](https://github.com/owner/repo/pull/11) ([user5](https://github.com/user5)) - - pr all the labels [\\#14](https://github.com/owner/repo/pull/14) ([user5](https://github.com/user5)) + - issue enhancement [\\#6](https://github.com/owner/repo/issue/6) + - pr enhancement [\\#21](https://github.com/owner/repo/pull/21) ([user5](https://github.com/user5)) - **Fixed bugs:** + **Fixed bugs:** - - issue bug [\\#7](https://github.com/owner/repo/issue/7) - - pr bug [\\#12](https://github.com/owner/repo/pull/12) ([user5](https://github.com/user5)) + - issue bug [\\#7](https://github.com/owner/repo/issue/7) + - issue some unmapped labels [\\#11](https://github.com/owner/repo/issue/11) + - pr bug [\\#22](https://github.com/owner/repo/pull/22) ([user5](https://github.com/user5)) + - pr some unmapped labels [\\#26](https://github.com/owner/repo/pull/26) ([user5](https://github.com/user5)) - **Closed issues:** + **Closed issues:** - - issue no labels [\\#5](https://github.com/owner/repo/issue/5) + - issue no labels [\\#5](https://github.com/owner/repo/issue/5) + - issue no mapped labels [\\#12](https://github.com/owner/repo/issue/12) - **Merged pull requests:** + **Merged pull requests:** - - pr no labels [\\#10](https://github.com/owner/repo/pull/10) ([user1](https://github.com/user1)) + - pr no labels [\\#20](https://github.com/owner/repo/pull/20) ([user1](https://github.com/user1)) + - pr no mapped labels [\\#27](https://github.com/owner/repo/pull/27) ([user5](https://github.com/user5)) - CHANGELOG - expect(subject.create_entry_for_tag(pull_requests, issues, "1.0.1", "1.0.1", Time.new(2017, 12, 4), "1.0.0")).to eq(changelog) + CHANGELOG + + expect(subject.generate_entry_for_tag(pull_requests, issues, "1.0.1", "1.0.1", Time.new(2017, 12, 4).utc, "1.0.0")).to eq(changelog) + end + end + describe "exclude issues without labels" do + let(:options) do + Parser.default_options.merge( + user: "owner", + project: "repo", + bug_labels: ["bug"], + enhancement_labels: ["enhancement"], + breaking_labels: ["breaking"], + add_pr_wo_labels: false, + add_issues_wo_labels: false, + verbose: false + ) + end + + it "generates a header and body" do + changelog = <<-CHANGELOG.gsub(/^ {10}/, "") + ## [1.0.1](https://github.com/owner/repo/tree/1.0.1) (2017-12-04) + + [Full Changelog](https://github.com/owner/repo/compare/1.0.0...1.0.1) + + **Breaking changes:** + + - issue breaking [\\#8](https://github.com/owner/repo/issue/8) + - issue all the labels [\\#9](https://github.com/owner/repo/issue/9) + - issue all the labels different order [\\#10](https://github.com/owner/repo/issue/10) + - pr breaking [\\#23](https://github.com/owner/repo/pull/23) ([user5](https://github.com/user5)) + - pr all the labels [\\#24](https://github.com/owner/repo/pull/24) ([user5](https://github.com/user5)) + - pr all the labels different order [\\#25](https://github.com/owner/repo/pull/25) ([user5](https://github.com/user5)) + + **Implemented enhancements:** + + - issue enhancement [\\#6](https://github.com/owner/repo/issue/6) + - pr enhancement [\\#21](https://github.com/owner/repo/pull/21) ([user5](https://github.com/user5)) + + **Fixed bugs:** + + - issue bug [\\#7](https://github.com/owner/repo/issue/7) + - issue some unmapped labels [\\#11](https://github.com/owner/repo/issue/11) + - pr bug [\\#22](https://github.com/owner/repo/pull/22) ([user5](https://github.com/user5)) + - pr some unmapped labels [\\#26](https://github.com/owner/repo/pull/26) ([user5](https://github.com/user5)) + + **Closed issues:** + + - issue no mapped labels [\\#12](https://github.com/owner/repo/issue/12) + + **Merged pull requests:** + + - pr no mapped labels [\\#27](https://github.com/owner/repo/pull/27) ([user5](https://github.com/user5)) + + CHANGELOG + + expect(subject.generate_entry_for_tag(pull_requests, issues, "1.0.1", "1.0.1", Time.new(2017, 12, 4).utc, "1.0.0")).to eq(changelog) + end end end describe "#parse_sections" do @@ -184,14 +293,15 @@ def default_sections end end - describe "#parse_by_sections" do + describe "#sort_into_sections" do context "default sections" do let(:options) do - { + Parser.default_options.merge( bug_labels: ["bug"], enhancement_labels: ["enhancement"], - breaking_labels: ["breaking"] - } + breaking_labels: ["breaking"], + verbose: false + ) end let(:issues) do @@ -200,7 +310,11 @@ def default_sections issue("enhancement", ["enhancement"]), issue("bug", ["bug"]), issue("breaking", ["breaking"]), - issue("all the labels", %w[enhancement bug breaking]) + issue("all the labels", %w[enhancement bug breaking]), + issue("some unmapped labels", %w[tests-fail bug]), + issue("no mapped labels", %w[docs maintenance]), + issue("excluded label", %w[wontfix]), + issue("excluded and included label", %w[breaking wontfix]) ] end @@ -210,43 +324,45 @@ def default_sections pr("enhancement", ["enhancement"]), pr("bug", ["bug"]), pr("breaking", ["breaking"]), - pr("all the labels", %w[enhancement bug breaking]) + pr("all the labels", %w[enhancement bug breaking]), + pr("some unmapped labels", %w[tests-fail bug], "15", "login" => "user5"), + pr("no mapped labels", %w[docs maintenance], "16", "login" => "user5"), + pr("excluded label", %w[wontfix]), + pr("excluded and included label", %w[breaking wontfix]) ] end subject { described_class.new(options) } - before do - subject.send(:set_sections_and_maps) - @arr = subject.send(:parse_by_sections, pull_requests, issues) - end - - it "returns 4 sections" do - expect(@arr.size).to eq 4 + it "returns 5 sections" do + expect(entry_sections.size).to eq 5 end it "returns default sections" do - default_sections.each { |default_section| expect(@arr.select { |section| section.name == default_section }.size).to eq 1 } + default_sections.each { |default_section| expect(entry_sections.select { |section| section.name == default_section }.size).to eq 1 } end it "assigns issues to the correct sections" do - breaking_section = @arr.select { |section| section.name == "breaking" }[0] - enhancement_section = @arr.select { |section| section.name == "enhancements" }[0] - issue_section = @arr.select { |section| section.name == "issues" }[0] - bug_section = @arr.select { |section| section.name == "bugs" }[0] - - expect(titles_for(breaking_section.issues)).to eq(["issue breaking", "pr breaking"]) - expect(titles_for(enhancement_section.issues)).to eq(["issue enhancement", "issue all the labels", "pr enhancement", "pr all the labels"]) - expect(titles_for(issue_section.issues)).to eq(["issue no labels"]) - expect(titles_for(bug_section.issues)).to eq(["issue bug", "pr bug"]) - expect(titles_for(pull_requests)).to eq(["pr no labels"]) + breaking_section = entry_sections.select { |section| section.name == "breaking" }[0] + enhancement_section = entry_sections.select { |section| section.name == "enhancements" }[0] + issue_section = entry_sections.select { |section| section.name == "issues" }[0] + bug_section = entry_sections.select { |section| section.name == "bugs" }[0] + merged_section = entry_sections.select { |section| section.name == "merged" }[0] + + expect(titles_for(breaking_section.issues)).to eq(["issue breaking", "issue all the labels", "pr breaking", "pr all the labels"]) + expect(titles_for(enhancement_section.issues)).to eq(["issue enhancement", "pr enhancement"]) + expect(titles_for(issue_section.issues)).to eq(["issue no labels", "issue no mapped labels"]) + expect(titles_for(bug_section.issues)).to eq(["issue bug", "issue some unmapped labels", "pr bug", "pr some unmapped labels"]) + expect(titles_for(merged_section.issues)).to eq(["pr no labels", "pr no mapped labels"]) end end - context "configure sections" do + context "configure sections and exclude labels" do let(:options) do - { - configure_sections: "{ \"foo\": { \"prefix\": \"foofix\", \"labels\": [\"test1\", \"test2\"]}, \"bar\": { \"prefix\": \"barfix\", \"labels\": [\"test3\", \"test4\"]}}" - } + Parser.default_options.merge( + configure_sections: "{ \"foo\": { \"prefix\": \"foofix\", \"labels\": [\"test1\", \"test2\"]}, \"bar\": { \"prefix\": \"barfix\", \"labels\": [\"test3\", \"test4\"]}}", + exclude_labels: ["excluded"], + verbose: false + ) end let(:issues) do @@ -255,7 +371,9 @@ def default_sections issue("test1", ["test1"]), issue("test3", ["test3"]), issue("test4", ["test4"]), - issue("all the labels", %w[test1 test2 test3 test4]) + issue("all the labels", %w[test4 test2 test3 test1]), + issue("some excluded labels", %w[excluded test2]), + issue("excluded labels", %w[excluded again]) ] end @@ -265,45 +383,46 @@ def default_sections pr("test1", ["test1"]), pr("test3", ["test3"]), pr("test4", ["test4"]), - pr("all the labels", %w[test1 test2 test3 test4]) + pr("all the labels", %w[test4 test2 test3 test1]), + pr("some excluded labels", %w[excluded test2]), + pr("excluded labels", %w[excluded again]) ] end subject { described_class.new(options) } - before do - subject.send(:set_sections_and_maps) - @arr = subject.send(:parse_by_sections, pull_requests, issues) - end - - it "returns 2 sections" do - expect(@arr.size).to eq 2 + it "returns 4 sections" do + expect(entry_sections.size).to eq 4 end it "returns only configured sections" do - expect(@arr.select { |section| section.name == "foo" }.size).to eq 1 - expect(@arr.select { |section| section.name == "bar" }.size).to eq 1 + expect(entry_sections.select { |section| section.name == "foo" }.size).to eq 1 + expect(entry_sections.select { |section| section.name == "bar" }.size).to eq 1 end it "assigns issues to the correct sections" do - foo_section = @arr.select { |section| section.name == "foo" }[0] - bar_section = @arr.select { |section| section.name == "bar" }[0] + foo_section = entry_sections.select { |section| section.name == "foo" }[0] + bar_section = entry_sections.select { |section| section.name == "bar" }[0] + issue_section = entry_sections.select { |section| section.name == "issues" }[0] + merged_section = entry_sections.select { |section| section.name == "merged" }[0] aggregate_failures "checks all sections" do expect(titles_for(foo_section.issues)).to eq(["issue test1", "issue all the labels", "pr test1", "pr all the labels"]) expect(titles_for(bar_section.issues)).to eq(["issue test3", "issue test4", "pr test3", "pr test4"]) - expect(titles_for(pull_requests)).to eq(["pr no labels"]) + expect(titles_for(merged_section.issues)).to eq(["pr no labels"]) + expect(titles_for(issue_section.issues)).to eq(["issue no labels"]) end end end context "add sections" do let(:options) do - { + Parser.default_options.merge( bug_labels: ["bug"], enhancement_labels: ["enhancement"], breaking_labels: ["breaking"], - add_sections: "{ \"foo\": { \"prefix\": \"foofix\", \"labels\": [\"test1\", \"test2\"]}}" - } + add_sections: "{ \"foo\": { \"prefix\": \"foofix\", \"labels\": [\"test1\", \"test2\"]}}", + verbose: false + ) end let(:issues) do @@ -311,7 +430,10 @@ def default_sections issue("no labels", []), issue("test1", ["test1"]), issue("bugaboo", ["bug"]), - issue("all the labels", %w[test1 test2 enhancement bug]) + issue("all the labels", %w[test1 test2 breaking enhancement bug]), + issue("default labels first", %w[enhancement bug test1 test2]), + issue("some excluded labels", %w[wontfix breaking]), + issue("excluded labels", %w[wontfix again]) ] end @@ -320,39 +442,42 @@ def default_sections pr("no labels", []), pr("test1", ["test1"]), pr("enhance", ["enhancement"]), - pr("all the labels", %w[test1 test2 enhancement bug]) + pr("all the labels", %w[test1 test2 breaking enhancement bug]), + pr("default labels first", %w[enhancement bug test1 test2]), + pr("some excluded labels", %w[wontfix breaking]), + pr("excluded labels", %w[wontfix again]) ] end subject { described_class.new(options) } - before do - subject.send(:set_sections_and_maps) - @arr = subject.send(:parse_by_sections, pull_requests, issues) - end - - it "returns 5 sections" do - expect(@arr.size).to eq 5 + it "returns 6 sections" do + expect(entry_sections.size).to eq 6 end it "returns default sections" do - default_sections.each { |default_section| expect(@arr.select { |section| section.name == default_section }.size).to eq 1 } + default_sections.each { |default_section| expect(entry_sections.select { |section| section.name == default_section }.size).to eq 1 } end it "returns added section" do - expect(@arr.select { |section| section.name == "foo" }.size).to eq 1 + expect(entry_sections.select { |section| section.name == "foo" }.size).to eq 1 end it "assigns issues to the correct sections" do - foo_section = @arr.select { |section| section.name == "foo" }[0] - enhancement_section = @arr.select { |section| section.name == "enhancements" }[0] - bug_section = @arr.select { |section| section.name == "bugs" }[0] + foo_section = entry_sections.select { |section| section.name == "foo" }[0] + breaking_section = entry_sections.select { |section| section.name == "breaking" }[0] + enhancement_section = entry_sections.select { |section| section.name == "enhancements" }[0] + bug_section = entry_sections.select { |section| section.name == "bugs" }[0] + issue_section = entry_sections.select { |section| section.name == "issues" }[0] + merged_section = entry_sections.select { |section| section.name == "merged" }[0] aggregate_failures "checks all sections" do - expect(titles_for(foo_section.issues)).to eq(["issue test1", "issue all the labels", "pr test1", "pr all the labels"]) - expect(titles_for(enhancement_section.issues)).to eq(["pr enhance"]) + expect(titles_for(breaking_section.issues)).to eq(["issue all the labels", "pr all the labels"]) + expect(titles_for(enhancement_section.issues)).to eq(["issue default labels first", "pr enhance", "pr default labels first"]) expect(titles_for(bug_section.issues)).to eq(["issue bugaboo"]) - expect(titles_for(pull_requests)).to eq(["pr no labels"]) + expect(titles_for(foo_section.issues)).to eq(["issue test1", "pr test1"]) + expect(titles_for(issue_section.issues)).to eq(["issue no labels"]) + expect(titles_for(merged_section.issues)).to eq(["pr no labels"]) end end end diff --git a/spec/unit/generator/generator_processor_spec.rb b/spec/unit/generator/generator_processor_spec.rb index be0a5e6e4..90f93c907 100644 --- a/spec/unit/generator/generator_processor_spec.rb +++ b/spec/unit/generator/generator_processor_spec.rb @@ -2,7 +2,7 @@ module GitHubChangelogGenerator describe Generator do - let(:default_options) { GitHubChangelogGenerator::Parser.default_options } + let(:default_options) { GitHubChangelogGenerator::Parser.default_options.merge(verbose: false) } let(:options) { {} } let(:generator) { described_class.new(default_options.merge(options)) } diff --git a/spec/unit/generator/generator_tags_spec.rb b/spec/unit/generator/generator_tags_spec.rb index d9dc6b46c..1d9dee452 100644 --- a/spec/unit/generator/generator_tags_spec.rb +++ b/spec/unit/generator/generator_tags_spec.rb @@ -17,7 +17,7 @@ def tags_from_strings(tags_strings) let(:all_tags) { tags_from_strings(%w[8 7 6 5 4 3 2 1]) } let(:sorted_tags) { all_tags } - let(:default_options) { GitHubChangelogGenerator::Parser.default_options } + let(:default_options) { GitHubChangelogGenerator::Parser.default_options.merge(verbose: false) } let(:options) { {} } let(:generator) { described_class.new(default_options.merge(options)) } diff --git a/spec/unit/parse_file_spec.rb b/spec/unit/parse_file_spec.rb index 82d1809b2..955ead12a 100644 --- a/spec/unit/parse_file_spec.rb +++ b/spec/unit/parse_file_spec.rb @@ -36,7 +36,7 @@ end context "when override default values" do - let(:default_options) { GitHubChangelogGenerator::Parser.default_options } + let(:default_options) { GitHubChangelogGenerator::Parser.default_options.merge(verbose: false) } let(:options) { {}.merge(default_options) } let(:options_before_change) { options.dup } let(:file) { StringIO.new("unreleased_label=staging\nunreleased=false\nheader==== Changelog ===") } From 69ca16b38a16f85685f1cb67918f4cd73756b06f Mon Sep 17 00:00:00 2001 From: Hunter Haugen Date: Fri, 23 Feb 2018 10:42:07 -0800 Subject: [PATCH 2/2] Don't let --include-labels exclude unlabeled issues That behavior should be configured by --add-issues-wo-labels and --add-pr-wo-labels --- .../generator/generator_processor.rb | 2 +- lib/github_changelog_generator/parser.rb | 4 +- spec/unit/generator/entry_spec.rb | 58 +++++++++++++++++++ .../generator/generator_processor_spec.rb | 9 ++- 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/lib/github_changelog_generator/generator/generator_processor.rb b/lib/github_changelog_generator/generator/generator_processor.rb index 44a4c1e46..b51c0cbc4 100644 --- a/lib/github_changelog_generator/generator/generator_processor.rb +++ b/lib/github_changelog_generator/generator/generator_processor.rb @@ -161,7 +161,7 @@ def filter_by_include_labels(issues) else issues.select do |issue| labels = issue["labels"].map { |l| l["name"] } & options[:include_labels] - labels.any? + labels.any? || issue["labels"].empty? end end end diff --git a/lib/github_changelog_generator/parser.rb b/lib/github_changelog_generator/parser.rb index 51efda47e..f983057e8 100755 --- a/lib/github_changelog_generator/parser.rb +++ b/lib/github_changelog_generator/parser.rb @@ -116,10 +116,10 @@ def self.setup_parser(options) opts.on("--[no-]compare-link", "Include compare link (Full Changelog) between older version and newer version. Default is true") do |v| options[:compare_link] = v end - opts.on("--include-labels x,y,z", Array, "Only issues with the specified labels will be included in the changelog.") do |list| + opts.on("--include-labels x,y,z", Array, "Of the labeled issues, only include the ones with the given labels.") do |list| options[:include_labels] = list end - opts.on("--exclude-labels x,y,z", Array, "Issues with the specified labels will be always excluded from changelog. Default is 'duplicate,question,invalid,wontfix'") do |list| + opts.on("--exclude-labels x,y,z", Array, "Issues with the specified labels will be excluded from changelog. Default is 'duplicate,question,invalid,wontfix'") do |list| options[:exclude_labels] = list end opts.on("--bug-labels x,y,z", Array, 'Issues with the specified labels will be always added to "Fixed bugs" section. Default is \'bug,Bug\'') do |list| diff --git a/spec/unit/generator/entry_spec.rb b/spec/unit/generator/entry_spec.rb index 4ea0971c9..7017d7fcd 100644 --- a/spec/unit/generator/entry_spec.rb +++ b/spec/unit/generator/entry_spec.rb @@ -356,6 +356,64 @@ def default_sections expect(titles_for(merged_section.issues)).to eq(["pr no labels", "pr no mapped labels"]) end end + context "configure sections and include labels" do + let(:options) do + Parser.default_options.merge( + configure_sections: "{ \"foo\": { \"prefix\": \"foofix\", \"labels\": [\"test1\", \"test2\"]}, \"bar\": { \"prefix\": \"barfix\", \"labels\": [\"test3\", \"test4\"]}}", + include_labels: %w[test1 test2 test3 test4], + verbose: false + ) + end + + let(:issues) do + [ + issue("no labels", []), + issue("test1", ["test1"]), + issue("test3", ["test3"]), + issue("test4", ["test4"]), + issue("all the labels", %w[test4 test2 test3 test1]), + issue("some included labels", %w[unincluded test2]), + issue("no included labels", %w[unincluded again]) + ] + end + + let(:pull_requests) do + [ + pr("no labels", []), + pr("test1", ["test1"]), + pr("test3", ["test3"]), + pr("test4", ["test4"]), + pr("all the labels", %w[test4 test2 test3 test1]), + pr("some included labels", %w[unincluded test2]), + pr("no included labels", %w[unincluded again]) + ] + end + + subject { described_class.new(options) } + + it "returns 4 sections" do + expect(entry_sections.size).to eq 4 + end + + it "returns only configured sections" do + expect(entry_sections.select { |section| section.name == "foo" }.size).to eq 1 + expect(entry_sections.select { |section| section.name == "bar" }.size).to eq 1 + end + + it "assigns issues to the correct sections" do + foo_section = entry_sections.select { |section| section.name == "foo" }[0] + bar_section = entry_sections.select { |section| section.name == "bar" }[0] + issue_section = entry_sections.select { |section| section.name == "issues" }[0] + merged_section = entry_sections.select { |section| section.name == "merged" }[0] + + aggregate_failures "checks all sections" do + expect(titles_for(foo_section.issues)).to eq(["issue test1", "issue all the labels", "issue some included labels", "pr test1", "pr all the labels", "pr some included labels"]) + expect(titles_for(bar_section.issues)).to eq(["issue test3", "issue test4", "pr test3", "pr test4"]) + expect(titles_for(merged_section.issues)).to eq(["pr no labels"]) + expect(titles_for(issue_section.issues)).to eq(["issue no labels"]) + end + end + end context "configure sections and exclude labels" do let(:options) do Parser.default_options.merge( diff --git a/spec/unit/generator/generator_processor_spec.rb b/spec/unit/generator/generator_processor_spec.rb index 90f93c907..c12d5cfb3 100644 --- a/spec/unit/generator/generator_processor_spec.rb +++ b/spec/unit/generator/generator_processor_spec.rb @@ -67,11 +67,18 @@ module GitHubChangelogGenerator it { is_expected.to eq(expected_issues) } end + + context "with 'include_labels'" do + let(:options) { { add_issues_wo_labels: false, include_labels: %w[GOOD] } } + let(:expected_issues) { [good_issue] } + + it { is_expected.to eq(expected_issues) } + end end context "when 'include_labels' is specified" do let(:options) { { include_labels: %w[GOOD] } } - let(:expected_issues) { [good_issue] } + let(:expected_issues) { [good_issue, unlabeled_issue] } it { is_expected.to eq(expected_issues) } end