From edbf73f8ddd2dcb5bd29d3b8698b4099114838f7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 10 Mar 2022 11:30:24 +0100 Subject: [PATCH] Add support to update license headers. Closes #204 --- .../release/infra/DependencyOperations.java | 42 +--- .../infra/InfrastructureOperations.java | 5 +- .../release/infra/LicenseHeaderCommands.java | 216 ++++++++++++++++++ .../data/release/issues/TicketOperations.java | 119 ++++++++++ 4 files changed, 344 insertions(+), 38 deletions(-) create mode 100644 release-tools/src/main/java/org/springframework/data/release/infra/LicenseHeaderCommands.java create mode 100644 release-tools/src/main/java/org/springframework/data/release/issues/TicketOperations.java diff --git a/release-tools/src/main/java/org/springframework/data/release/infra/DependencyOperations.java b/release-tools/src/main/java/org/springframework/data/release/infra/DependencyOperations.java index d81e1c9..0a7c916 100644 --- a/release-tools/src/main/java/org/springframework/data/release/infra/DependencyOperations.java +++ b/release-tools/src/main/java/org/springframework/data/release/infra/DependencyOperations.java @@ -49,6 +49,7 @@ import org.springframework.data.release.git.GitOperations; import org.springframework.data.release.io.Workspace; import org.springframework.data.release.issues.IssueTracker; import org.springframework.data.release.issues.Ticket; +import org.springframework.data.release.issues.TicketOperations; import org.springframework.data.release.issues.Tickets; import org.springframework.data.release.model.Iteration; import org.springframework.data.release.model.ModuleIteration; @@ -59,7 +60,6 @@ import org.springframework.data.release.utils.ExecutionUtils; import org.springframework.data.release.utils.Logger; import org.springframework.data.util.Streamable; import org.springframework.http.ResponseEntity; -import org.springframework.plugin.core.PluginRegistry; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.client.RestOperations; @@ -87,7 +87,7 @@ public class DependencyOperations { private final ProjectionFactory projectionFactory; private final GitOperations gitOperations; private final Workspace workspace; - private final PluginRegistry tracker; + private final TicketOperations tickets; private final ExecutorService executor; private final RestOperations restOperations; private final Logger logger; @@ -261,37 +261,12 @@ public class DependencyOperations { */ public Tickets getOrCreateUpgradeTickets(ModuleIteration module, DependencyVersions dependencyVersions) { - Project project = module.getProject(); - - IssueTracker tracker = this.tracker.getRequiredPluginFor(project); - Tickets tickets = tracker.getTicketsFor(module); - - List upgradeTickets = new ArrayList<>(); - + List summaries = new ArrayList<>(); dependencyVersions.forEach((dependency, dependencyVersion) -> { - - String upgradeTicketSummary = getUpgradeTicketSummary(dependency, dependencyVersion); - Optional upgradeTicket = getDependencyUpgradeTicket(tickets, upgradeTicketSummary); - - if (upgradeTicket.isPresent()) { - logger.log(project, "Found upgrade ticket %s", upgradeTicket.get()); - upgradeTicket.ifPresent(it -> { - tracker.assignTicketToMe(project, it); - upgradeTickets.add(it); - }); - } else { - - logger.log(module, "Creating upgrade ticket for %s", upgradeTicketSummary); - Ticket ticket = tracker.createTicket(module, upgradeTicketSummary, IssueTracker.TicketType.DependencyUpgrade, - true); - upgradeTickets.add(ticket); - } + summaries.add(getUpgradeTicketSummary(dependency, dependencyVersion)); }); - // flush cache - tracker.reset(); - - return new Tickets(upgradeTickets); + return tickets.getOrCreateTicketsWithSummary(module, IssueTracker.TicketType.DependencyUpgrade, summaries); } /** @@ -353,12 +328,7 @@ public class DependencyOperations { } public void closeUpgradeTickets(ModuleIteration module, Tickets tickets) { - - IssueTracker tracker = this.tracker.getRequiredPluginFor(module.getProject()); - - for (Ticket ticket : tickets) { - tracker.closeTicket(module, ticket); - } + this.tickets.closeTickets(module, tickets); } public DependencyVersions getDependencyUpgradesToApply(Project project, DependencyVersions dependencyVersions) { diff --git a/release-tools/src/main/java/org/springframework/data/release/infra/InfrastructureOperations.java b/release-tools/src/main/java/org/springframework/data/release/infra/InfrastructureOperations.java index 0dcdae2..2e8a9c7 100644 --- a/release-tools/src/main/java/org/springframework/data/release/infra/InfrastructureOperations.java +++ b/release-tools/src/main/java/org/springframework/data/release/infra/InfrastructureOperations.java @@ -31,6 +31,7 @@ import java.util.concurrent.ExecutorService; import org.apache.commons.io.FileUtils; +import org.springframework.data.release.TimedCommand; import org.springframework.data.release.git.Branch; import org.springframework.data.release.git.GitOperations; import org.springframework.data.release.io.Workspace; @@ -52,7 +53,7 @@ import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) -public class InfrastructureOperations { +public class InfrastructureOperations extends TimedCommand { public static final String CI_PROPERTIES = "ci/pipeline.properties"; @@ -97,7 +98,7 @@ public class InfrastructureOperations { git.add(module.getProject(), CI_PROPERTIES); git.commit(module, "Update CI properties.", Optional.empty(), false); - // git.push(iteration); + git.push(module); }); } diff --git a/release-tools/src/main/java/org/springframework/data/release/infra/LicenseHeaderCommands.java b/release-tools/src/main/java/org/springframework/data/release/infra/LicenseHeaderCommands.java new file mode 100644 index 0000000..343c206 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/infra/LicenseHeaderCommands.java @@ -0,0 +1,216 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.release.infra; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.AbstractFileFilter; +import org.apache.commons.io.filefilter.IOFileFilter; +import org.apache.commons.io.filefilter.NameFileFilter; +import org.apache.commons.io.filefilter.NotFileFilter; + +import org.springframework.data.release.CliComponent; +import org.springframework.data.release.TimedCommand; +import org.springframework.data.release.git.GitOperations; +import org.springframework.data.release.io.Workspace; +import org.springframework.data.release.issues.IssueTracker; +import org.springframework.data.release.issues.Ticket; +import org.springframework.data.release.issues.TicketOperations; +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.data.release.model.Project; +import org.springframework.data.release.model.Projects; +import org.springframework.data.release.model.TrainIteration; +import org.springframework.data.release.utils.ExecutionUtils; +import org.springframework.data.release.utils.Logger; +import org.springframework.data.util.Streamable; +import org.springframework.shell.core.annotation.CliCommand; +import org.springframework.shell.core.annotation.CliOption; +import org.springframework.util.AntPathMatcher; + +/** + * @author Mark Paluch + */ +@CliComponent +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class LicenseHeaderCommands extends TimedCommand { + + GitOperations git; + + Workspace workspace; + + Executor executor; + + Logger logger; + + TicketOperations tickets; + + List filePatterns = Arrays.asList("pom.xml", "**/*.java", "**/*.kt", "**/*.adoc"); + + /** + * Process all files matching {@link #filePatterns} and update the Apache license header year range, extending to + * {@code year}. Rewrites single-year and year-range formats. + * + * @param iteration + * @param year + */ + @CliCommand(value = "update license-headers") + public void updateLicenseHeaders(@CliOption(key = "", mandatory = true) TrainIteration iteration, + @CliOption(key = "year", mandatory = true) int year, + @CliOption(key = "project", mandatory = false) String projectName) { + + git.prepare(iteration); + + Streamable modules = iteration; + + if (projectName != null) { + Project project = Projects.requiredByName(projectName); + modules = modules.filter(it -> it.getProject().equals(project)); + } + + ExecutionUtils.run(executor, modules, module -> { + + String summary = String.format("Extend license header copyright years to %d", year); + + int updated = replaceInFiles(module.getProject(), content -> { + + String contentToUse = content; + + contentToUse = contentToUse.replaceAll("(C) ([\\d]{4})-([\\d]{4})", "(C) $1-" + year); + + contentToUse = contentToUse.replaceAll("Copyright ([\\d]{4}) the original author or authors", + "Copyright $1-" + year + " the original author or authors"); + + contentToUse = contentToUse.replaceAll("Copyright ([\\d]{4})-([\\d]{4}) the original author or authors", + "Copyright $1-" + year + " the original author or authors"); + + return contentToUse; + }); + + if (updated > 0) { + commitAndPushWithTicket(module, summary); + } + }); + } + + private void commitAndPushWithTicket(ModuleIteration module, String ticketSummary) throws InterruptedException { + + Ticket ticket = tickets.getOrCreateTicketsWithSummary(module, IssueTracker.TicketType.Task, ticketSummary); + git.commit(module, ticket, ticketSummary, Optional.empty(), true); + + try { + git.push(module); + } catch (Exception e) { + logger.warn(module, e); + } + + TimeUnit.SECONDS.sleep(1); + + tickets.closeTicket(module, ticket); + } + + /** + * Replace content in files by applying {@link Function contentFunction} and return the number of updated files. + * + * @param project + * @param contentFunction + * @return + */ + private int replaceInFiles(Project project, Function contentFunction) { + + File projectDirectory = workspace.getProjectDirectory(project); + IOFileFilter fileFilter = new AntPathFileFilter(projectDirectory, filePatterns); + + int files = 0; + int modified = 0; + Iterator fileIterator = FileUtils.iterateFiles(projectDirectory, fileFilter, + new NotFileFilter(new NameFileFilter(".git"))); + + while (fileIterator.hasNext()) { + + File file = fileIterator.next(); + files++; + + try { + if (doReplace(file, contentFunction)) { + modified++; + } + } catch (IOException e) { + throw new IllegalStateException(String.format("Cannot modify contents of %s", file), e); + } + } + + logger.log(project, "Found %s files, updated %s files", files, modified); + + return modified; + } + + private boolean doReplace(File file, Function modifyFunction) throws IOException { + + String content = FileUtils.readFileToString(file, StandardCharsets.UTF_8); + String modified = modifyFunction.apply(content); + + if (!content.equals(modified)) { + + FileUtils.write(file, modified, StandardCharsets.UTF_8); + return true; + } + + return false; + } + + private static class AntPathFileFilter extends AbstractFileFilter { + + private final URI projectDirectory; + private final List filePatterns; + + public AntPathFileFilter(File basePath, List filePatterns) { + this.projectDirectory = basePath.toURI(); + this.filePatterns = filePatterns; + } + + @Override + public boolean accept(File file) { + + String relativePath = projectDirectory.relativize(file.toURI()).getPath(); + + AntPathMatcher matcher = new AntPathMatcher(); + for (String pattern : filePatterns) { + + if (matcher.match(pattern, relativePath)) { + return true; + } + } + + return false; + } + } +} diff --git a/release-tools/src/main/java/org/springframework/data/release/issues/TicketOperations.java b/release-tools/src/main/java/org/springframework/data/release/issues/TicketOperations.java new file mode 100644 index 0000000..4f5753e --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/issues/TicketOperations.java @@ -0,0 +1,119 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.release.issues; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.data.release.model.Project; +import org.springframework.data.release.utils.Logger; +import org.springframework.plugin.core.PluginRegistry; +import org.springframework.stereotype.Component; + +/** + * @author Mark Paluch + */ +@Component +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class TicketOperations { + + Logger logger; + + PluginRegistry tracker; + + /** + * Create or look up ticket with a particular summary. + * + * @param module + * @param summary + * @return + */ + public Ticket getOrCreateTicketsWithSummary(ModuleIteration module, IssueTracker.TicketType ticketType, + String summary) { + return getOrCreateTicketsWithSummary(module, ticketType, Collections.singletonList(summary)).getTickets().get(0); + } + + /** + * Create or look up tickets with a particular summary. + * + * @param module + * @param ticketType + * @param summary + * @return + */ + public Tickets getOrCreateTicketsWithSummary(ModuleIteration module, IssueTracker.TicketType ticketType, + List summary) { + + Project project = module.getProject(); + + IssueTracker tracker = this.tracker.getRequiredPluginFor(project); + Tickets tickets = tracker.getTicketsFor(module); + List results = new ArrayList<>(); + + for (String s : summary) { + + Optional upgradeTicket = findBySummary(tickets, s); + + if (upgradeTicket.isPresent()) { + logger.log(project, "Found ticket %s", upgradeTicket.get()); + upgradeTicket.ifPresent(it -> { + tracker.assignTicketToMe(project, it); + results.add(it); + }); + } else { + + logger.log(module, "Creating ticket for %s", summary); + Ticket ticket = tracker.createTicket(module, s, ticketType, true); + results.add(ticket); + } + } + + return new Tickets(results); + } + + private Optional findBySummary(Tickets tickets, String summary) { + + List result = tickets.filter(it -> it.getSummary().equals(summary)).toList(); + + if (result.size() > 1) { + throw new IllegalStateException("Multiple tickets found: " + result); + } + + return Optional.ofNullable(result.isEmpty() ? null : result.get(0)); + } + + public void closeTicket(ModuleIteration module, Ticket ticket) { + closeTickets(module, new Tickets(Collections.singletonList(ticket))); + } + + public void closeTickets(ModuleIteration module, Tickets tickets) { + + IssueTracker tracker = this.tracker.getRequiredPluginFor(module.getProject()); + + for (Ticket ticket : tickets) { + tracker.closeTicket(module, ticket); + } + } + +}