diff --git a/README.adoc b/README.adoc index 4a7d344..3970d4e 100644 --- a/README.adoc +++ b/README.adoc @@ -30,6 +30,8 @@ $ java -jar github-changelog-generator.jar NOTE: By default `` refers to the milestone title. If you want to use milestone ID, you should set the `changelog.milestone-reference` property to `id`. + + === Customizing Sections By default the changelog will contain the following sections: @@ -62,6 +64,36 @@ changelog: labels: ["fix"] ---- + + +==== Showing issues in multiple sections +Unless otherwise configured, issues will only appear in the first matching section. +For example, if you have an issue labeled with `enhancement` and `documentation` then it will only appear in the "New Features" section. + +If you want an issue to appear in multiple sections, you can use the `group` property. +Groups allow you to create logical groupings of related sections. +An issue may only appear once in any given group. + +For example, you might define the following: + +[source,yaml] +---- +changelog: + sections: + - title: "Highlights" + labels: ["noteworthy"] + group: "highlights" + - title: "Enhancements" + labels: ["new"] + - title: "Bugs" + labels: ["fix"] +---- + +This will create two distinct groups, "highlights" and "default" (which is used if no `group` property is specified). +An issue labeled with `new` and `noteworthy` will appear in both the "Highlights" and "Enhancements" section. + + + == License This project is Open Source software released under the https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license]. diff --git a/src/main/java/io/spring/githubchangeloggenerator/ApplicationProperties.java b/src/main/java/io/spring/githubchangeloggenerator/ApplicationProperties.java index c8bcd43..aecad49 100644 --- a/src/main/java/io/spring/githubchangeloggenerator/ApplicationProperties.java +++ b/src/main/java/io/spring/githubchangeloggenerator/ApplicationProperties.java @@ -51,18 +51,12 @@ public class ApplicationProperties { */ private final List
sections; - /** - * Issue structure within the changelog. - */ - private final Issues issues; - public ApplicationProperties(Repository repository, @DefaultValue("title") MilestoneReference milestoneReference, - List
sections, Issues issues) { + List
sections) { Assert.notNull(repository, "Repository must not be null"); this.repository = repository; this.milestoneReference = milestoneReference; this.sections = sections; - this.issues = issues; } public Repository getRepository() { @@ -77,27 +71,30 @@ public List
getSections() { return this.sections; } - public Issues getIssues() { - return this.issues; - } - /** * Properties for a single changelog section. */ public static class Section { /** - * The title of the section. + * Title of the section. */ private final String title; /** - * The labels used to identify if an issue is for the section. + * Group used to bound the contained issues. Issues appear in the first section of + * each group. + */ + private final String group; + + /** + * Labels used to identify if an issue is for the section. */ private final List labels; - public Section(String title, String... labels) { + public Section(String title, @DefaultValue("default") String group, String... labels) { this.title = title; + this.group = (group != null) ? group : "default"; this.labels = Arrays.asList(labels); } @@ -105,28 +102,12 @@ public String getTitle() { return this.title; } - public List getLabels() { - return this.labels; - } - - } - - /** - * Properties relating to issue structure within the release notes. - */ - public static class Issues { - - /** - * Whether an issue can appear in multiple sections. - */ - private final Boolean allowInMultipleSections; - - public Issues(@DefaultValue("false") Boolean allowInMultipleSections) { - this.allowInMultipleSections = allowInMultipleSections; + public String getGroup() { + return this.group; } - public Boolean getAllowInMultipleSections() { - return this.allowInMultipleSections; + public List getLabels() { + return this.labels; } } diff --git a/src/main/java/io/spring/githubchangeloggenerator/ChangelogSection.java b/src/main/java/io/spring/githubchangeloggenerator/ChangelogSection.java index e4eb249..b5769ce 100644 --- a/src/main/java/io/spring/githubchangeloggenerator/ChangelogSection.java +++ b/src/main/java/io/spring/githubchangeloggenerator/ChangelogSection.java @@ -34,19 +34,26 @@ class ChangelogSection { private final String title; + private final String group; + private final List labels; - ChangelogSection(String title, String... labels) { - this(title, Arrays.asList(labels)); + ChangelogSection(String title, String group, String... labels) { + this(title, group, Arrays.asList(labels)); } - ChangelogSection(String title, List labels) { + ChangelogSection(String title, String group, List labels) { Assert.hasText(title, "Title must not be empty"); Assert.isTrue(!CollectionUtils.isEmpty(labels), "Labels must not be empty"); this.title = title; + this.group = group; this.labels = labels; } + String getGroup() { + return this.group; + } + boolean isMatchFor(Issue issue) { for (String candidate : this.labels) { for (Label label : issue.getLabels()) { diff --git a/src/main/java/io/spring/githubchangeloggenerator/ChangelogSections.java b/src/main/java/io/spring/githubchangeloggenerator/ChangelogSections.java index a32f825..2f4f1a4 100644 --- a/src/main/java/io/spring/githubchangeloggenerator/ChangelogSections.java +++ b/src/main/java/io/spring/githubchangeloggenerator/ChangelogSections.java @@ -19,8 +19,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.stream.Collectors; @@ -47,16 +49,13 @@ class ChangelogSections { } private static void add(List sections, String title, String... labels) { - sections.add(new ChangelogSection(title, labels)); + sections.add(new ChangelogSection(title, null, labels)); } private final List sections; - private final Boolean allowInMultipleSections; - ChangelogSections(ApplicationProperties properties) { this.sections = adapt(properties.getSections()); - this.allowInMultipleSections = properties.getIssues().getAllowInMultipleSections(); } private List adapt(List propertySections) { @@ -67,41 +66,31 @@ private List adapt(List propert } private ChangelogSection adapt(ApplicationProperties.Section propertySection) { - return new ChangelogSection(propertySection.getTitle(), propertySection.getLabels()); + return new ChangelogSection(propertySection.getTitle(), propertySection.getGroup(), + propertySection.getLabels()); } Map> collate(List issues) { SortedMap> collated = new TreeMap<>(Comparator.comparing(this.sections::indexOf)); for (Issue issue : issues) { - List sections = (this.allowInMultipleSections) ? getAllMatchingSections(issue) - : Collections.singletonList(getSection(issue)); + List sections = getSections(issue); for (ChangelogSection section : sections) { - if (section != null) { - collated.computeIfAbsent(section, (key) -> new ArrayList<>()); - collated.get(section).add(issue); - } + collated.computeIfAbsent(section, (key) -> new ArrayList<>()); + collated.get(section).add(issue); } } return collated; } - private ChangelogSection getSection(Issue issue) { - for (ChangelogSection section : this.sections) { - if (section.isMatchFor(issue)) { - return section; - } - } - return null; - } - - private List getAllMatchingSections(Issue issue) { - List sections = new ArrayList<>(); + private List getSections(Issue issue) { + List result = new ArrayList<>(); + Set groupClaimes = new HashSet<>(); for (ChangelogSection section : this.sections) { - if (section.isMatchFor(issue)) { - sections.add(section); + if (section.isMatchFor(issue) && groupClaimes.add(section.getGroup())) { + result.add(section); } } - return sections; + return result; } } diff --git a/src/main/java/io/spring/githubchangeloggenerator/github/payload/Issue.java b/src/main/java/io/spring/githubchangeloggenerator/github/payload/Issue.java index 68d264b..07f6cf7 100644 --- a/src/main/java/io/spring/githubchangeloggenerator/github/payload/Issue.java +++ b/src/main/java/io/spring/githubchangeloggenerator/github/payload/Issue.java @@ -75,4 +75,9 @@ public PullRequest getPullRequest() { return this.pullRequest; } + @Override + public String toString() { + return this.title; + } + } diff --git a/src/test/java/io/spring/githubchangeloggenerator/ApplicationPropertiesTests.java b/src/test/java/io/spring/githubchangeloggenerator/ApplicationPropertiesTests.java index 9c7a7d5..507e2a3 100644 --- a/src/test/java/io/spring/githubchangeloggenerator/ApplicationPropertiesTests.java +++ b/src/test/java/io/spring/githubchangeloggenerator/ApplicationPropertiesTests.java @@ -51,10 +51,10 @@ public void loadYaml() throws Exception { assertThat(sections).hasSize(2); assertThat(sections.get(0).getTitle()).isEqualTo(":star: New Features"); assertThat(sections.get(0).getLabels()).containsExactly("enhancement"); + assertThat(sections.get(0).getGroup()).isEqualTo("default"); assertThat(sections.get(1).getTitle()).isEqualTo("Bugs"); assertThat(sections.get(1).getLabels()).containsExactly("bug"); - Boolean allowInMultipleSections = properties.getIssues().getAllowInMultipleSections(); - assertThat(allowInMultipleSections).isTrue(); + assertThat(sections.get(1).getGroup()).isEqualTo("test"); } } diff --git a/src/test/java/io/spring/githubchangeloggenerator/ChangelogGeneratorTests.java b/src/test/java/io/spring/githubchangeloggenerator/ChangelogGeneratorTests.java index 1d876ad..83aeffc 100644 --- a/src/test/java/io/spring/githubchangeloggenerator/ChangelogGeneratorTests.java +++ b/src/test/java/io/spring/githubchangeloggenerator/ChangelogGeneratorTests.java @@ -24,7 +24,6 @@ import java.util.Collections; import java.util.List; -import io.spring.githubchangeloggenerator.ApplicationProperties.Issues; import io.spring.githubchangeloggenerator.github.payload.Issue; import io.spring.githubchangeloggenerator.github.payload.Label; import io.spring.githubchangeloggenerator.github.payload.PullRequest; @@ -160,7 +159,7 @@ public void whenEscapedUserMentionIsInIssueTitleItIsNotEscapedAgain() throws IOE private void setupGenerator(MilestoneReference id) { this.generator = new ChangelogGenerator(this.service, - new ApplicationProperties(REPO, id, Collections.emptyList(), new Issues(false))); + new ApplicationProperties(REPO, id, Collections.emptyList())); } private User createUser(String contributor12, String s) { diff --git a/src/test/java/io/spring/githubchangeloggenerator/ChangelogSectionsTests.java b/src/test/java/io/spring/githubchangeloggenerator/ChangelogSectionsTests.java index f084723..02a280a 100644 --- a/src/test/java/io/spring/githubchangeloggenerator/ChangelogSectionsTests.java +++ b/src/test/java/io/spring/githubchangeloggenerator/ChangelogSectionsTests.java @@ -22,7 +22,6 @@ import java.util.Map; import java.util.stream.Collectors; -import io.spring.githubchangeloggenerator.ApplicationProperties.Issues; import io.spring.githubchangeloggenerator.github.payload.Issue; import io.spring.githubchangeloggenerator.github.payload.Label; import io.spring.githubchangeloggenerator.github.service.Repository; @@ -34,116 +33,113 @@ * Tests for {@link ChangelogSections}. * * @author Eleftheria Stein + * @author Phillip Webb */ public class ChangelogSectionsTests { private static final Repository REPO = Repository.of("org/name"); @Test - public void whenNoCustomSectionsThenDefaultSectionsUsed() { - Issue enhancement = new Issue("1", "Enhancement", null, Collections.singletonList(new Label("enhancement")), - "url1", null); - Issue bug = new Issue("2", "Bug", null, Collections.singletonList(new Label("bug")), "url2", null); - Issue documentation = new Issue("3", "Documentation Change", null, - Collections.singletonList(new Label("documentation")), "url3", null); - Issue dependencyUpgrade = new Issue("4", "Dependency Upgrade", null, - Collections.singletonList(new Label("dependency-upgrade")), "url4", null); - List issues = Arrays.asList(enhancement, bug, documentation, dependencyUpgrade); - ApplicationProperties properties = newApplicationProperties(); + public void collateWhenNoCustomSectionsUsesDefaultSections() { + Issue enhancement = createIssue("1", "enhancement"); + Issue bug = createIssue("2", "bug"); + Issue documentation = createIssue("3", "documentation"); + Issue dependencyUpgrade = createIssue("4", "dependency-upgrade"); + ApplicationProperties properties = new ApplicationProperties(REPO, MilestoneReference.TITLE, null); ChangelogSections sections = new ChangelogSections(properties); - Map> collated = sections.collate(issues); - Map> titlesToIssues = getSectionNameToIssuesMap(collated); - assertThat(titlesToIssues.keySet()).containsExactlyInAnyOrder(":star: New Features", ":beetle: Bug Fixes", + Map> collated = sections + .collate(Arrays.asList(enhancement, bug, documentation, dependencyUpgrade)); + Map> bySection = getBySection(collated); + assertThat(bySection).containsOnlyKeys(":star: New Features", ":beetle: Bug Fixes", ":notebook_with_decorative_cover: Documentation", ":hammer: Dependency Upgrades"); - assertThat(titlesToIssues.get(":star: New Features")).containsExactly(enhancement); - assertThat(titlesToIssues.get(":beetle: Bug Fixes")).containsExactly(bug); - assertThat(titlesToIssues.get(":notebook_with_decorative_cover: Documentation")).containsExactly(documentation); - assertThat(titlesToIssues.get(":hammer: Dependency Upgrades")).containsExactly(dependencyUpgrade); + assertThat(bySection.get(":star: New Features")).containsExactly(enhancement); + assertThat(bySection.get(":beetle: Bug Fixes")).containsExactly(bug); + assertThat(bySection.get(":notebook_with_decorative_cover: Documentation")).containsExactly(documentation); + assertThat(bySection.get(":hammer: Dependency Upgrades")).containsExactly(dependencyUpgrade); } @Test - public void whenCustomSectionsThenUsed() { + public void collateWhenHasCustomSectionsUsesDefinedSections() { ApplicationProperties.Section breaksPassivitySection = new ApplicationProperties.Section(":rewind: Non-passive", - "breaks-passivity"); - ApplicationProperties.Section bugsSection = new ApplicationProperties.Section(":beetle: Bug Fixes", "bug"); + null, "breaks-passivity"); + ApplicationProperties.Section bugsSection = new ApplicationProperties.Section(":beetle: Bug Fixes", null, + "bug"); List customSections = Arrays.asList(breaksPassivitySection, bugsSection); - ApplicationProperties properties = new ApplicationProperties(REPO, MilestoneReference.TITLE, customSections, - new Issues(false)); - + ApplicationProperties properties = new ApplicationProperties(REPO, MilestoneReference.TITLE, customSections); ChangelogSections sections = new ChangelogSections(properties); - Issue bug = new Issue("1", "Bug", null, Collections.singletonList(new Label("bug")), "url1", null); - Issue nonPassive = new Issue("2", "Non-passive change", null, - Collections.singletonList(new Label("breaks-passivity")), "url2", null); - List issues = Arrays.asList(bug, nonPassive); - - Map> collated = sections.collate(issues); - List sectionTitles = collated.keySet().stream().map(ChangelogSection::toString) - .collect(Collectors.toList()); - assertThat(sectionTitles).containsExactlyInAnyOrder(":beetle: Bug Fixes", ":rewind: Non-passive"); + Issue bug = createIssue("1", "bug"); + Issue nonPassive = createIssue("1", "breaks-passivity"); + Map> collated = sections.collate(Arrays.asList(bug, nonPassive)); + Map> bySection = getBySection(collated); + assertThat(bySection).containsOnlyKeys(":beetle: Bug Fixes", ":rewind: Non-passive"); } @Test - public void whenNoIssuesInSectionThenSectionExcluded() { - Issue bug = new Issue("1", "Bug", null, Collections.singletonList(new Label("bug")), "url1", null); - List issues = Collections.singletonList(bug); - ApplicationProperties properties = newApplicationProperties(); + public void collateWhenNoIssuesInSectionExcludesSection() { + Issue bug = createIssue("1", "bug"); + ApplicationProperties properties = new ApplicationProperties(REPO, MilestoneReference.TITLE, null); ChangelogSections sections = new ChangelogSections(properties); - Map> collated = sections.collate(issues); - Map> titlesToIssues = getSectionNameToIssuesMap(collated); - assertThat(titlesToIssues.keySet()).containsExactly(":beetle: Bug Fixes"); + Map> collated = sections.collate(Collections.singletonList(bug)); + Map> bySection = getBySection(collated); + assertThat(bySection.keySet()).containsExactly(":beetle: Bug Fixes"); } @Test - public void whenIssueDoesNotMatchAnySectionLabelThenIssueExcluded() { - Issue bug = new Issue("1", "Bug", null, Collections.singletonList(new Label("bug")), "url1", null); - Issue nonPassive = new Issue("2", "Non-passive change", null, - Collections.singletonList(new Label("non-passive")), "url2", null); - List issues = Arrays.asList(bug, nonPassive); - ApplicationProperties properties = newApplicationProperties(); + public void collateWhenIssueDoesNotMatchAnySectionLabelThenExcludesIssue() { + Issue bug = createIssue("1", "bug"); + Issue nonPassive = createIssue("2", "non-passive"); + ApplicationProperties properties = new ApplicationProperties(REPO, MilestoneReference.TITLE, null); ChangelogSections sections = new ChangelogSections(properties); - Map> collated = sections.collate(issues); - Map> titlesToIssues = getSectionNameToIssuesMap(collated); - assertThat(titlesToIssues.keySet()).containsExactly(":beetle: Bug Fixes"); - assertThat(titlesToIssues.get(":beetle: Bug Fixes")).containsExactly(bug); + Map> collated = sections.collate(Arrays.asList(bug, nonPassive)); + Map> bySection = getBySection(collated); + assertThat(bySection).containsOnlyKeys(":beetle: Bug Fixes"); + assertThat(bySection.get(":beetle: Bug Fixes")).containsExactly(bug); } @Test - public void byDefaultIssueDoesNotAppearInMultipleSections() { - Issue bugAndDocumentation = new Issue("1", "Bug", null, - Arrays.asList(new Label("bug"), new Label("documentation")), "url1", null); - List issues = Collections.singletonList(bugAndDocumentation); - ApplicationProperties properties = newApplicationProperties(); + public void collateWithDefaultsDoesNotAddIssueToMultipleSections() { + Issue bug = createIssue("1", "bug"); + Issue highlight = createIssue("2", "highlight"); + Issue bugAndHighlight = createIssue("3", "bug", "highlight"); + ApplicationProperties.Section bugs = new ApplicationProperties.Section("Bugs", null, "bug"); + ApplicationProperties.Section highlights = new ApplicationProperties.Section("Highlights", null, "highlight"); + List customSections = Arrays.asList(bugs, highlights); + ApplicationProperties properties = new ApplicationProperties(REPO, MilestoneReference.TITLE, customSections); ChangelogSections sections = new ChangelogSections(properties); - Map> collated = sections.collate(issues); - Map> titlesToIssues = getSectionNameToIssuesMap(collated); - assertThat(titlesToIssues.keySet()).containsExactly(":beetle: Bug Fixes"); - assertThat(titlesToIssues.get(":beetle: Bug Fixes")).containsExactly(bugAndDocumentation); + Map> collated = sections.collate(Arrays.asList(bug, highlight, bugAndHighlight)); + Map> bySection = getBySection(collated); + assertThat(bySection).containsOnlyKeys("Bugs", "Highlights"); + assertThat(bySection.get("Bugs")).containsExactly(bug, bugAndHighlight); + assertThat(bySection.get("Highlights")).containsExactly(highlight); } @Test - public void whenAllowInMultipleSectionsIssueAppearsInAllMatchingSections() { - Issue bugAndDocumentation = new Issue("1", "Bug", null, - Arrays.asList(new Label("bug"), new Label("documentation")), "url1", null); - List issueList = Collections.singletonList(bugAndDocumentation); - ApplicationProperties properties = new ApplicationProperties(REPO, MilestoneReference.TITLE, null, - new Issues(true)); + public void collateWithGroupsAddsIssuePerGroup() { + Issue bug = createIssue("1", "bug"); + Issue highlight = createIssue("2", "highlight"); + Issue bugAndHighlight = createIssue("3", "bug", "highlight"); + ApplicationProperties.Section bugs = new ApplicationProperties.Section("Bugs", null, "bug"); + ApplicationProperties.Section highlights = new ApplicationProperties.Section("Highlights", "highlights", + "highlight"); + List customSections = Arrays.asList(bugs, highlights); + ApplicationProperties properties = new ApplicationProperties(REPO, MilestoneReference.TITLE, customSections); ChangelogSections sections = new ChangelogSections(properties); - Map> collated = sections.collate(issueList); - Map> titlesToIssues = getSectionNameToIssuesMap(collated); - assertThat(titlesToIssues.keySet()).containsExactlyInAnyOrder(":beetle: Bug Fixes", - ":notebook_with_decorative_cover: Documentation"); - assertThat(titlesToIssues.get(":beetle: Bug Fixes")).containsExactly(bugAndDocumentation); - assertThat(titlesToIssues.get(":notebook_with_decorative_cover: Documentation")) - .containsExactly(bugAndDocumentation); + Map> collated = sections.collate(Arrays.asList(bug, highlight, bugAndHighlight)); + Map> bySection = getBySection(collated); + assertThat(bySection).containsOnlyKeys("Bugs", "Highlights"); + assertThat(bySection.get("Bugs")).containsExactly(bug, bugAndHighlight); + assertThat(bySection.get("Highlights")).containsExactly(highlight, bugAndHighlight); } - private Map> getSectionNameToIssuesMap(Map> collatedIssues) { - return collatedIssues.entrySet().stream() - .collect(Collectors.toMap((entry) -> entry.getKey().toString(), (entry) -> entry.getValue())); + private Issue createIssue(String number, String... labels) { + return new Issue(number, "I am #" + number, null, + Arrays.stream(labels).map(Label::new).collect(Collectors.toList()), "https://example.com/" + number, + null); } - private ApplicationProperties newApplicationProperties() { - return new ApplicationProperties(REPO, MilestoneReference.TITLE, null, new Issues(false)); + private Map> getBySection(Map> collatedIssues) { + return collatedIssues.entrySet().stream() + .collect(Collectors.toMap((entry) -> entry.getKey().toString(), (entry) -> entry.getValue())); } } diff --git a/src/test/resources/io/spring/githubchangeloggenerator/test-application.yml b/src/test/resources/io/spring/githubchangeloggenerator/test-application.yml index 8146292..d7f099f 100644 --- a/src/test/resources/io/spring/githubchangeloggenerator/test-application.yml +++ b/src/test/resources/io/spring/githubchangeloggenerator/test-application.yml @@ -5,5 +5,6 @@ changelog: labels: ["enhancement"] - title: "Bugs" labels: ["bug"] + group: "test" issues: allow-in-multiple-sections: true