From 2da147458800dfe9ecc24865aef0e718ac56df57 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 24 Jun 2024 17:05:32 +0200 Subject: [PATCH 01/90] [MCLEAN-123] Switch to Maven 4 and the new api (#20) --- .github/workflows/maven-verify.yml | 4 +- pom.xml | 62 ++-- src/it/settings.xml | 4 + .../apache/maven/plugins/clean/CleanMojo.java | 70 ++-- .../apache/maven/plugins/clean/Cleaner.java | 334 ++++++++++-------- .../apache/maven/plugins/clean/Fileset.java | 6 +- .../examples/delete_additional_files.apt.vm | 2 +- .../maven/plugins/clean/CleanMojoTest.java | 170 +++++---- .../unit/basic-clean-test/plugin-pom.xml | 35 -- .../pom.xml} | 6 +- .../{plugin-pom.xml => pom.xml} | 0 .../{plugin-pom.xml => pom.xml} | 5 +- .../pom.xml} | 2 +- .../pom.xml} | 2 +- .../pom.xml} | 2 +- .../resources/unit/nested-clean-test/pom.xml | 35 ++ 16 files changed, 411 insertions(+), 328 deletions(-) delete mode 100644 src/test/resources/unit/basic-clean-test/plugin-pom.xml rename src/test/resources/unit/{nested-clean-test/plugin-pom.xml => basic-clean-test/pom.xml} (76%) rename src/test/resources/unit/empty-clean-test/{plugin-pom.xml => pom.xml} (100%) rename src/test/resources/unit/fileset-clean-test/{plugin-pom.xml => pom.xml} (88%) rename src/test/resources/unit/{locked-file-test/plugin-pom.xml => invalid-directory-test/pom.xml} (91%) rename src/test/resources/unit/{missing-directory-test/plugin-pom.xml => locked-file-test/pom.xml} (91%) rename src/test/resources/unit/{invalid-directory-test/plugin-pom.xml => missing-directory-test/pom.xml} (91%) create mode 100644 src/test/resources/unit/nested-clean-test/pom.xml diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index ce4b500d..56062b54 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -26,4 +26,6 @@ jobs: name: Verify uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v4 with: - maven4-enabled: true + ff-maven: "4.0.0-beta-3" # Maven version for fail-fast-build + maven-matrix: '[ "4.0.0-beta-3" ]' + jdk-matrix: '[ "17", "21" ]' diff --git a/pom.xml b/pom.xml index 73c3c727..a7ef0bb5 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ under the License. maven-clean-plugin - 3.4.1-SNAPSHOT + 4.0.0-SNAPSHOT maven-plugin Apache Maven Clean Plugin @@ -61,46 +61,44 @@ under the License. - 3.6.3 + 4.0.0-beta-3 + 17 + 3.7.0 + 4.0.0-alpha-3-SNAPSHOT + 4.0.0-SNAPSHOT 2024-06-16T10:25:11Z org.apache.maven - maven-plugin-api + maven-api-core ${mavenVersion} provided org.apache.maven - maven-core + maven-api-di ${mavenVersion} provided - org.apache.maven.resolver - maven-resolver-api - 1.1.1 + org.apache.maven + maven-api-meta + ${mavenVersion} provided + org.codehaus.plexus plexus-utils - - - org.apache.maven.plugin-tools - maven-plugin-annotations - provided - - org.apache.maven.plugin-testing maven-plugin-testing-harness - 4.0.0-alpha-2 + ${version.maven-plugin-testing} test @@ -109,18 +107,42 @@ under the License. test - org.codehaus.plexus - plexus-testing - 1.3.0 + org.apache.maven + maven-core + ${mavenVersion} test - org.codehaus.plexus - plexus-xml + com.google.inject + guice + 6.0.0 + test + + + org.slf4j + slf4j-simple + 2.0.13 test + + + + org.apache.maven.plugins + maven-plugin-plugin + ${version.maven-plugin-tools} + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + run-its diff --git a/src/it/settings.xml b/src/it/settings.xml index c8f77f0b..5617e4e5 100644 --- a/src/it/settings.xml +++ b/src/it/settings.xml @@ -32,9 +32,11 @@ under the License. @localRepositoryUrl@ true + ignore true + ignore @@ -44,9 +46,11 @@ under the License. @localRepositoryUrl@ true + ignore true + ignore diff --git a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java index 4f77c009..44ceb480 100644 --- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java +++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java @@ -18,14 +18,16 @@ */ package org.apache.maven.plugins.clean; -import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; -import org.apache.maven.execution.MavenSession; -import org.apache.maven.plugin.AbstractMojo; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.api.Session; +import org.apache.maven.api.di.Inject; +import org.apache.maven.api.plugin.Log; +import org.apache.maven.api.plugin.MojoException; +import org.apache.maven.api.plugin.annotations.Mojo; +import org.apache.maven.api.plugin.annotations.Parameter; /** * Goal which cleans the build. @@ -43,8 +45,8 @@ * @see org.apache.maven.plugins.clean.Fileset * @since 2.0 */ -@Mojo(name = "clean", threadSafe = true) -public class CleanMojo extends AbstractMojo { +@Mojo(name = "clean") +public class CleanMojo implements org.apache.maven.api.plugin.Mojo { public static final String FAST_MODE_BACKGROUND = "background"; @@ -52,23 +54,26 @@ public class CleanMojo extends AbstractMojo { public static final String FAST_MODE_DEFER = "defer"; + @Inject + private Log logger; + /** * This is where build results go. */ @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true) - private File directory; + private Path directory; /** * This is where compiled classes go. */ @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true, required = true) - private File outputDirectory; + private Path outputDirectory; /** * This is where compiled test classes go. */ @Parameter(defaultValue = "${project.build.testOutputDirectory}", readonly = true, required = true) - private File testOutputDirectory; + private Path testOutputDirectory; /** * This is where the site plugin generates its pages. @@ -76,7 +81,7 @@ public class CleanMojo extends AbstractMojo { * @since 2.1.1 */ @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true, required = true) - private File reportDirectory; + private Path reportDirectory; /** * Sets whether the plugin runs in verbose mode. As of plugin version 2.3, the default value is derived from Maven's @@ -186,11 +191,11 @@ public class CleanMojo extends AbstractMojo { * should usually reside on the same volume. The exact conditions are system dependant though, but if an atomic * move is not supported, the standard deletion mechanism will be used. * - * @see #fast * @since 3.2 + * @see #fast */ @Parameter(property = "maven.clean.fastDir") - private File fastDir; + private Path fastDir; /** * Mode to use when using fast clean. Values are: background to start deletion immediately and @@ -199,35 +204,35 @@ public class CleanMojo extends AbstractMojo { * the actual file deletion should be started in the background when the session ends (this should only be used * when maven is embedded in a long running process). * - * @see #fast * @since 3.2 + * @see #fast */ @Parameter(property = "maven.clean.fastMode", defaultValue = FAST_MODE_BACKGROUND) private String fastMode; - @Parameter(defaultValue = "${session}", readonly = true) - private MavenSession session; + @Inject + private Session session; /** * Deletes file-sets in the following project build directory order: (source) directory, output directory, test * directory, report directory, and then the additional file-sets. * - * @throws MojoExecutionException When a directory failed to get deleted. - * @see org.apache.maven.plugin.Mojo#execute() + * @throws MojoException When a directory failed to get deleted. + * @see org.apache.maven.api.plugin.Mojo#execute() */ - public void execute() throws MojoExecutionException { + public void execute() { if (skip) { getLog().info("Clean is skipped."); return; } String multiModuleProjectDirectory = - session != null ? session.getSystemProperties().getProperty("maven.multiModuleProjectDirectory") : null; - File fastDir; + session != null ? session.getSystemProperties().get("maven.multiModuleProjectDirectory") : null; + Path fastDir; if (fast && this.fastDir != null) { fastDir = this.fastDir; } else if (fast && multiModuleProjectDirectory != null) { - fastDir = new File(multiModuleProjectDirectory, "target/.clean"); + fastDir = Paths.get(multiModuleProjectDirectory, "target/.clean"); } else { fastDir = null; if (fast) { @@ -247,7 +252,7 @@ public void execute() throws MojoExecutionException { Cleaner cleaner = new Cleaner(session, getLog(), isVerbose(), fastDir, fastMode); try { - for (File directoryItem : getDirectories()) { + for (Path directoryItem : getDirectories()) { if (directoryItem != null) { cleaner.delete(directoryItem, null, followSymLinks, failOnError, retryOnError); } @@ -256,7 +261,7 @@ public void execute() throws MojoExecutionException { if (filesets != null) { for (Fileset fileset : filesets) { if (fileset.getDirectory() == null) { - throw new MojoExecutionException("Missing base directory for " + fileset); + throw new MojoException("Missing base directory for " + fileset); } final String[] includes = fileset.getIncludes(); final String[] excludes = fileset.getExcludes(); @@ -273,8 +278,9 @@ public void execute() throws MojoExecutionException { fileset.getDirectory(), selector, fileset.isFollowSymlinks(), failOnError, retryOnError); } } + } catch (IOException e) { - throw new MojoExecutionException("Failed to clean project: " + e.getMessage(), e); + throw new MojoException("Failed to clean project: " + e.getMessage(), e); } } @@ -292,13 +298,17 @@ private boolean isVerbose() { * * @return The directories to clean or an empty array if none, never null. */ - private File[] getDirectories() { - File[] directories; + private Path[] getDirectories() { + Path[] directories; if (excludeDefaultDirectories) { - directories = new File[0]; + directories = new Path[0]; } else { - directories = new File[] {directory, outputDirectory, testOutputDirectory, reportDirectory}; + directories = new Path[] {directory, outputDirectory, testOutputDirectory, reportDirectory}; } return directories; } + + private Log getLog() { + return logger; + } } diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java index f5c25d27..533a2241 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -20,9 +20,6 @@ import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; @@ -30,12 +27,17 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayDeque; import java.util.Deque; - -import org.apache.maven.execution.ExecutionListener; -import org.apache.maven.execution.MavenSession; -import org.apache.maven.plugin.logging.Log; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.apache.maven.api.Event; +import org.apache.maven.api.EventType; +import org.apache.maven.api.Listener; +import org.apache.maven.api.Session; +import org.apache.maven.api.SessionData; +import org.apache.maven.api.plugin.Log; import org.codehaus.plexus.util.Os; -import org.eclipse.aether.SessionData; import static org.apache.maven.plugins.clean.CleanMojo.FAST_MODE_BACKGROUND; import static org.apache.maven.plugins.clean.CleanMojo.FAST_MODE_DEFER; @@ -49,70 +51,93 @@ class Cleaner { private static final boolean ON_WINDOWS = Os.isFamily(Os.FAMILY_WINDOWS); - private static final String LAST_DIRECTORY_TO_DELETE = Cleaner.class.getName() + ".lastDirectoryToDelete"; + private static final SessionData.Key LAST_DIRECTORY_TO_DELETE = + SessionData.key(Path.class, Cleaner.class.getName() + ".lastDirectoryToDelete"); /** * The maven session. This is typically non-null in a real run, but it can be during unit tests. */ - private final MavenSession session; + private final Session session; - private final File fastDir; + private final Logger logDebug; - private final String fastMode; + private final Logger logInfo; + + private final Logger logVerbose; - private final boolean verbose; + private final Logger logWarn; - private Log log; + private final Path fastDir; + + private final String fastMode; /** * Creates a new cleaner. * * @param session The Maven session to be used. - * @param log The logger to use. + * @param log The logger to use, may be null to disable logging. * @param verbose Whether to perform verbose logging. * @param fastDir The explicit configured directory or to be deleted in fast mode. * @param fastMode The fast deletion mode. */ - Cleaner(MavenSession session, final Log log, boolean verbose, File fastDir, String fastMode) { + Cleaner(Session session, Log log, boolean verbose, Path fastDir, String fastMode) { + logDebug = (log == null || !log.isDebugEnabled()) ? null : logger(log::debug, log::debug); + + logInfo = (log == null || !log.isInfoEnabled()) ? null : logger(log::info, log::info); + + logWarn = (log == null || !log.isWarnEnabled()) ? null : logger(log::warn, log::warn); + + logVerbose = verbose ? logInfo : logDebug; + this.session = session; - // This can't be null as the Cleaner gets it from the CleanMojo which gets it from AbstractMojo class, where it - // is never null. - this.log = log; this.fastDir = fastDir; this.fastMode = fastMode; - this.verbose = verbose; + } + + private Logger logger(Consumer l1, BiConsumer l2) { + return new Logger() { + @Override + public void log(CharSequence message) { + l1.accept(message); + } + + @Override + public void log(CharSequence message, Throwable t) { + l2.accept(message, t); + } + }; } /** * Deletes the specified directories and its contents. * - * @param basedir The directory to delete, must not be null. Non-existing directories will be silently - * ignored. - * @param selector The selector used to determine what contents to delete, may be null to delete - * everything. + * @param basedir The directory to delete, must not be null. Non-existing directories will be silently + * ignored. + * @param selector The selector used to determine what contents to delete, may be null to delete + * everything. * @param followSymlinks Whether to follow symlinks. - * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted. - * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. + * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted. + * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. * @throws IOException If a file/directory could not be deleted and failOnError is true. */ public void delete( - File basedir, Selector selector, boolean followSymlinks, boolean failOnError, boolean retryOnError) + Path basedir, Selector selector, boolean followSymlinks, boolean failOnError, boolean retryOnError) throws IOException { - if (!basedir.isDirectory()) { - if (!basedir.exists()) { - if (log.isDebugEnabled()) { - log.debug("Skipping non-existing directory " + basedir); + if (!Files.isDirectory(basedir)) { + if (!Files.exists(basedir)) { + if (logDebug != null) { + logDebug.log("Skipping non-existing directory " + basedir); } return; } throw new IOException("Invalid base directory " + basedir); } - if (log.isInfoEnabled()) { - log.info("Deleting " + basedir + (selector != null ? " (" + selector + ")" : "")); + if (logInfo != null) { + logInfo.log("Deleting " + basedir + (selector != null ? " (" + selector + ")" : "")); } - File file = followSymlinks ? basedir : basedir.getCanonicalFile(); + Path file = followSymlinks ? basedir : getCanonicalPath(basedir); if (selector == null && !followSymlinks && fastDir != null && session != null) { // If anything wrong happens, we'll just use the usual deletion mechanism @@ -124,9 +149,8 @@ public void delete( delete(file, "", selector, followSymlinks, failOnError, retryOnError); } - private boolean fastDelete(File baseDirFile) { - Path baseDir = baseDirFile.toPath(); - Path fastDir = this.fastDir.toPath(); + private boolean fastDelete(Path baseDir) { + Path fastDir = this.fastDir; // Handle the case where we use ${maven.multiModuleProjectDirectory}/target/.clean for example if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) { try { @@ -135,7 +159,7 @@ private boolean fastDelete(File baseDirFile) { try { Files.move(baseDir, tmpDir, StandardCopyOption.REPLACE_EXISTING); if (session != null) { - session.getRepositorySession().getData().set(LAST_DIRECTORY_TO_DELETE, baseDir.toFile()); + session.getData().set(LAST_DIRECTORY_TO_DELETE, baseDir); } baseDir = tmpDir; } catch (IOException e) { @@ -143,8 +167,8 @@ private boolean fastDelete(File baseDirFile) { throw e; } } catch (IOException e) { - if (log.isDebugEnabled()) { - log.debug("Unable to fast delete directory: ", e); + if (logDebug != null) { + logDebug.log("Unable to fast delete directory", e); } return false; } @@ -155,10 +179,10 @@ private boolean fastDelete(File baseDirFile) { Files.createDirectories(fastDir); } } catch (IOException e) { - if (log.isDebugEnabled()) { - log.debug( + if (logDebug != null) { + logDebug.log( "Unable to fast delete directory as the path " + fastDir - + " does not point to a directory or cannot be created: ", + + " does not point to a directory or cannot be created", e); } return false; @@ -172,11 +196,11 @@ private boolean fastDelete(File baseDirFile) { // or any other exception occurs, an exception will be thrown in which case // the method will return false and the usual deletion will be performed. Files.move(baseDir, dstDir, StandardCopyOption.ATOMIC_MOVE); - BackgroundCleaner.delete(this, tmpDir.toFile(), fastMode); + BackgroundCleaner.delete(this, tmpDir, fastMode); return true; } catch (IOException e) { - if (log.isDebugEnabled()) { - log.debug("Unable to fast delete directory: ", e); + if (logDebug != null) { + logDebug.log("Unable to fast delete directory", e); } return false; } @@ -185,20 +209,20 @@ private boolean fastDelete(File baseDirFile) { /** * Deletes the specified file or directory. * - * @param file The file/directory to delete, must not be null. If followSymlinks is - * false, it is assumed that the parent file is canonical. - * @param pathname The relative pathname of the file, using {@link File#separatorChar}, must not be - * null. - * @param selector The selector used to determine what contents to delete, may be null to delete - * everything. + * @param file The file/directory to delete, must not be null. If followSymlinks is + * false, it is assumed that the parent file is canonical. + * @param pathname The relative pathname of the file, using {@link File#separatorChar}, must not be + * null. + * @param selector The selector used to determine what contents to delete, may be null to delete + * everything. * @param followSymlinks Whether to follow symlinks. - * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted. - * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. + * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted. + * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. * @return The result of the cleaning, never null. * @throws IOException If a file/directory could not be deleted and failOnError is true. */ private Result delete( - File file, + Path file, String pathname, Selector selector, boolean followSymlinks, @@ -207,46 +231,43 @@ private Result delete( throws IOException { Result result = new Result(); - boolean isDirectory = file.isDirectory(); + boolean isDirectory = Files.isDirectory(file); if (isDirectory) { if (selector == null || selector.couldHoldSelected(pathname)) { - if (followSymlinks || !isSymbolicLink(file.toPath())) { - File canonical = followSymlinks ? file : file.getCanonicalFile(); - String[] filenames = canonical.list(); - if (filenames != null) { - String prefix = pathname.length() > 0 ? pathname + File.separatorChar : ""; - for (int i = filenames.length - 1; i >= 0; i--) { - String filename = filenames[i]; - File child = new File(canonical, filename); + final boolean isSymlink = isSymbolicLink(file); + Path canonical = followSymlinks ? file : getCanonicalPath(file); + if (followSymlinks || !isSymlink) { + String prefix = !pathname.isEmpty() ? pathname + File.separatorChar : ""; + try (Stream children = Files.list(canonical)) { + for (Path child : children.toList()) { result.update(delete( - child, prefix + filename, selector, followSymlinks, failOnError, retryOnError)); + child, + prefix + child.getFileName(), + selector, + followSymlinks, + failOnError, + retryOnError)); } } - } else if (log.isDebugEnabled()) { - log.debug("Not recursing into symlink " + file); + } else if (logDebug != null) { + logDebug.log("Not recursing into symlink " + file); } - } else if (log.isDebugEnabled()) { - log.debug("Not recursing into directory without included files " + file); + } else if (logDebug != null) { + logDebug.log("Not recursing into directory without included files " + file); } } if (!result.excluded && (selector == null || selector.isSelected(pathname))) { - String logmessage; - if (isDirectory) { - logmessage = "Deleting directory " + file; - } else if (file.exists()) { - logmessage = "Deleting file " + file; - } else { - logmessage = "Deleting dangling symlink " + file; - } - - if (verbose && log.isInfoEnabled()) { - log.info(logmessage); - } else if (log.isDebugEnabled()) { - log.debug(logmessage); + if (logVerbose != null) { + if (isDirectory) { + logVerbose.log("Deleting directory " + file); + } else if (Files.exists(file)) { + logVerbose.log("Deleting file " + file); + } else { + logVerbose.log("Deleting dangling symlink " + file); + } } - result.failures += delete(file, failOnError, retryOnError); } else { result.excluded = true; @@ -255,6 +276,14 @@ private Result delete( return result; } + private static Path getCanonicalPath(Path path) { + try { + return path.toRealPath(); + } catch (IOException e) { + return getCanonicalPath(path.getParent()).resolve(path.getFileName()); + } + } + private boolean isSymbolicLink(Path path) throws IOException { BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); return attrs.isSymbolicLink() @@ -262,19 +291,13 @@ private boolean isSymbolicLink(Path path) throws IOException { || (attrs.isDirectory() && attrs.isOther()); } - /** - * Deletes the specified file, directory. If the path denotes a symlink, only the link is removed, its target is - * left untouched. - * - * @param file The file/directory to delete, must not be null. - * @param failOnError Whether to abort with an exception in case the file/directory could not be deleted. - * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. - * @return 0 if the file was deleted, 1 otherwise. - * @throws IOException If a file/directory could not be deleted and failOnError is true. - */ - private int delete(File file, boolean failOnError, boolean retryOnError) throws IOException { - if (!file.delete()) { - boolean deleted = false; + private int delete(Path file, boolean failOnError, boolean retryOnError) throws IOException { + try { + Files.deleteIfExists(file); + return 0; + } catch (IOException e) { + IOException exception = new IOException("Failed to delete " + file); + exception.addSuppressed(e); if (retryOnError) { if (ON_WINDOWS) { @@ -283,24 +306,27 @@ private int delete(File file, boolean failOnError, boolean retryOnError) throws } final int[] delays = {50, 250, 750}; - for (int i = 0; !deleted && i < delays.length; i++) { + for (int delay : delays) { try { - Thread.sleep(delays[i]); - } catch (InterruptedException e) { - // ignore + Thread.sleep(delay); + } catch (InterruptedException e2) { + exception.addSuppressed(e2); + } + try { + Files.deleteIfExists(file); + return 0; + } catch (IOException e2) { + exception.addSuppressed(e2); } - deleted = file.delete() || !file.exists(); } - } else { - deleted = !file.exists(); } - if (!deleted) { + if (Files.exists(file)) { if (failOnError) { - throw new IOException("Failed to delete " + file); + throw new IOException("Failed to delete " + file, exception); } else { - if (log.isWarnEnabled()) { - log.warn("Failed to delete " + file); + if (logWarn != null) { + logWarn.log("Failed to delete " + file, exception); } return 1; } @@ -322,25 +348,30 @@ public void update(Result result) { } } + private interface Logger { + + void log(CharSequence message); + + void log(CharSequence message, Throwable t); + } + private static class BackgroundCleaner extends Thread { - private static final int NEW = 0; - private static final int RUNNING = 1; - private static final int STOPPED = 2; private static BackgroundCleaner instance; - private final Deque filesToDelete = new ArrayDeque<>(); + + private final Deque filesToDelete = new ArrayDeque<>(); + private final Cleaner cleaner; + private final String fastMode; - private int status = NEW; - private BackgroundCleaner(Cleaner cleaner, File dir, String fastMode) { - super("mvn-background-cleaner"); - this.cleaner = cleaner; - this.fastMode = fastMode; - init(cleaner.fastDir, dir); - } + private static final int NEW = 0; + private static final int RUNNING = 1; + private static final int STOPPED = 2; - public static void delete(Cleaner cleaner, File dir, String fastMode) { + private int status = NEW; + + public static void delete(Cleaner cleaner, Path dir, String fastMode) { synchronized (BackgroundCleaner.class) { if (instance == null || !instance.doDelete(dir)) { instance = new BackgroundCleaner(cleaner, dir, fastMode); @@ -356,9 +387,16 @@ static void sessionEnd() { } } + private BackgroundCleaner(Cleaner cleaner, Path dir, String fastMode) { + super("mvn-background-cleaner"); + this.cleaner = cleaner; + this.fastMode = fastMode; + init(cleaner.fastDir, dir); + } + public void run() { while (true) { - File basedir = pollNext(); + Path basedir = pollNext(); if (basedir == null) { break; } @@ -370,24 +408,25 @@ public void run() { } } - synchronized void init(File fastDir, File dir) { - if (fastDir.isDirectory()) { - File[] children = fastDir.listFiles(); - if (children != null && children.length > 0) { - for (File child : children) { - doDelete(child); + synchronized void init(Path fastDir, Path dir) { + if (Files.isDirectory(fastDir)) { + try { + try (Stream children = Files.list(fastDir)) { + children.forEach(this::doDelete); } + } catch (IOException e) { + throw new RuntimeException(e); } } doDelete(dir); } - synchronized File pollNext() { - File basedir = filesToDelete.poll(); + synchronized Path pollNext() { + Path basedir = filesToDelete.poll(); if (basedir == null) { if (cleaner.session != null) { - SessionData data = cleaner.session.getRepositorySession().getData(); - File lastDir = (File) data.get(LAST_DIRECTORY_TO_DELETE); + SessionData data = cleaner.session.getData(); + Path lastDir = (Path) data.get(LAST_DIRECTORY_TO_DELETE); if (lastDir != null) { data.set(LAST_DIRECTORY_TO_DELETE, null); return lastDir; @@ -399,7 +438,7 @@ synchronized File pollNext() { return basedir; } - synchronized boolean doDelete(File dir) { + synchronized boolean doDelete(Path dir) { if (status == STOPPED) { return false; } @@ -421,15 +460,10 @@ synchronized boolean doDelete(File dir) { * to outlive its main execution. */ private void wrapExecutionListener() { - ExecutionListener executionListener = cleaner.session.getRequest().getExecutionListener(); - if (executionListener == null - || !Proxy.isProxyClass(executionListener.getClass()) - || !(Proxy.getInvocationHandler(executionListener) instanceof SpyInvocationHandler)) { - ExecutionListener listener = (ExecutionListener) Proxy.newProxyInstance( - ExecutionListener.class.getClassLoader(), - new Class[] {ExecutionListener.class}, - new SpyInvocationHandler(executionListener)); - cleaner.session.getRequest().setExecutionListener(listener); + synchronized (CleanerListener.class) { + if (cleaner.session.getListeners().stream().noneMatch(l -> l instanceof CleanerListener)) { + cleaner.session.registerListener(new CleanerListener()); + } } } @@ -440,8 +474,8 @@ synchronized void doSessionEnd() { } if (!FAST_MODE_DEFER.equals(fastMode)) { try { - if (cleaner.log.isInfoEnabled()) { - cleaner.log.info("Waiting for background file deletion"); + if (cleaner.logInfo != null) { + cleaner.logInfo.log("Waiting for background file deletion"); } while (status != STOPPED) { wait(); @@ -454,22 +488,12 @@ synchronized void doSessionEnd() { } } - static class SpyInvocationHandler implements InvocationHandler { - private final ExecutionListener delegate; - - SpyInvocationHandler(ExecutionListener delegate) { - this.delegate = delegate; - } - + static class CleanerListener implements Listener { @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if ("sessionEnded".equals(method.getName())) { + public void onEvent(Event event) { + if (event.getType() == EventType.SESSION_ENDED) { BackgroundCleaner.sessionEnd(); } - if (delegate != null) { - return method.invoke(delegate, args); - } - return null; } } } diff --git a/src/main/java/org/apache/maven/plugins/clean/Fileset.java b/src/main/java/org/apache/maven/plugins/clean/Fileset.java index ecd88b0f..75084115 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Fileset.java +++ b/src/main/java/org/apache/maven/plugins/clean/Fileset.java @@ -18,7 +18,7 @@ */ package org.apache.maven.plugins.clean; -import java.io.File; +import java.nio.file.Path; import java.util.Arrays; /** @@ -32,7 +32,7 @@ */ public class Fileset { - private File directory; + private Path directory; private String[] includes; @@ -45,7 +45,7 @@ public class Fileset { /** * @return {@link #directory} */ - public File getDirectory() { + public Path getDirectory() { return directory; } diff --git a/src/site/apt/examples/delete_additional_files.apt.vm b/src/site/apt/examples/delete_additional_files.apt.vm index 0fa768c5..84ed0797 100644 --- a/src/site/apt/examples/delete_additional_files.apt.vm +++ b/src/site/apt/examples/delete_additional_files.apt.vm @@ -68,7 +68,7 @@ Delete Additional Files Not Exposed to Maven is equivalent to: +-------- - ${project.basedir}/some/relative/path + ${basedir}/some/relative/path +-------- You could also define file set rules in a parent POM. In this case, the clean plugin adds the subproject diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java index 22287d77..5a5d0d45 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java @@ -29,17 +29,17 @@ import java.nio.file.Paths; import java.util.Collections; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.logging.SystemStreamLog; -import org.apache.maven.plugin.testing.junit5.InjectMojo; -import org.apache.maven.plugin.testing.junit5.MojoTest; +import org.apache.maven.api.plugin.MojoException; +import org.apache.maven.api.plugin.testing.Basedir; +import org.apache.maven.api.plugin.testing.InjectMojo; +import org.apache.maven.api.plugin.testing.MojoTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; -import static org.apache.maven.plugin.testing.junit5.MojoExtension.setVariableValueToObject; -import static org.codehaus.plexus.testing.PlexusExtension.getBasedir; +import static org.apache.maven.api.plugin.testing.MojoExtension.getBasedir; +import static org.apache.maven.api.plugin.testing.MojoExtension.setVariableValueToObject; import static org.codehaus.plexus.util.IOUtil.copy; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -49,30 +49,25 @@ /** * Test the clean mojo. - * - * @author Vincent Siveton */ @MojoTest -class CleanMojoTest { +public class CleanMojoTest { + private static final String LOCAL_REPO = "target/local-repo/"; + /** * Tests the simple removal of directories * * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/basic-clean-test/plugin-pom.xml") - void testBasicClean(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/basic-clean-test") + @InjectMojo(goal = "clean") + public void testBasicClean(CleanMojo mojo) throws Exception { mojo.execute(); - assertFalse( - checkExists(getBasedir() + "/target/test-classes/unit/" + "basic-clean-test/buildDirectory"), - "Directory exists"); - assertFalse( - checkExists(getBasedir() + "/target/test-classes/unit/basic-clean-test/" + "buildOutputDirectory"), - "Directory exists"); - assertFalse( - checkExists(getBasedir() + "/target/test-classes/unit/basic-clean-test/" + "buildTestDirectory"), - "Directory exists"); + assertFalse(checkExists(getBasedir() + "/buildDirectory"), "Directory exists"); + assertFalse(checkExists(getBasedir() + "/buildOutputDirectory"), "Directory exists"); + assertFalse(checkExists(getBasedir() + "/buildTestDirectory"), "Directory exists"); } /** @@ -81,13 +76,14 @@ void testBasicClean(CleanMojo mojo) throws Exception { * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/nested-clean-test/plugin-pom.xml") - void testCleanNestedStructure(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/nested-clean-test") + @InjectMojo(goal = "clean") + public void testCleanNestedStructure(CleanMojo mojo) throws Exception { mojo.execute(); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/nested-clean-test/target")); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/nested-clean-test/target/classes")); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/nested-clean-test/target/test-classes")); + assertFalse(checkExists(getBasedir() + "/target")); + assertFalse(checkExists(getBasedir() + "/target/classes")); + assertFalse(checkExists(getBasedir() + "/target/test-classes")); } /** @@ -97,17 +93,15 @@ void testCleanNestedStructure(CleanMojo mojo) throws Exception { * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/empty-clean-test/plugin-pom.xml") - void testCleanEmptyDirectories(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/empty-clean-test") + @InjectMojo(goal = "clean") + public void testCleanEmptyDirectories(CleanMojo mojo) throws Exception { mojo.execute(); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/empty-clean-test/testDirectoryStructure")); - assertTrue(checkExists( - getBasedir() + "/target/test-classes/unit/empty-clean-test/" + "testDirectoryStructure/file.txt")); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/empty-clean-test/" - + "testDirectoryStructure/outputDirectory")); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/empty-clean-test/" - + "testDirectoryStructure/outputDirectory/file.txt")); + assertTrue(checkExists(getBasedir() + "/testDirectoryStructure")); + assertTrue(checkExists(getBasedir() + "/testDirectoryStructure/file.txt")); + assertTrue(checkExists(getBasedir() + "/testDirectoryStructure/outputDirectory")); + assertTrue(checkExists(getBasedir() + "/testDirectoryStructure/outputDirectory/file.txt")); } /** @@ -116,25 +110,24 @@ void testCleanEmptyDirectories(CleanMojo mojo) throws Exception { * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/fileset-clean-test/plugin-pom.xml") - void testFilesetsClean(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/fileset-clean-test") + @InjectMojo(goal = "clean") + public void testFilesetsClean(CleanMojo mojo) throws Exception { mojo.execute(); // fileset 1 - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target")); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/classes")); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/test-classes")); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/subdir")); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/classes/file.txt")); - assertTrue(checkEmpty(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/classes")); - assertFalse(checkEmpty(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/subdir")); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/subdir/file.txt")); + assertTrue(checkExists(getBasedir() + "/target")); + assertTrue(checkExists(getBasedir() + "/target/classes")); + assertFalse(checkExists(getBasedir() + "/target/test-classes")); + assertTrue(checkExists(getBasedir() + "/target/subdir")); + assertFalse(checkExists(getBasedir() + "/target/classes/file.txt")); + assertTrue(checkEmpty(getBasedir() + "/target/classes")); + assertFalse(checkEmpty(getBasedir() + "/target/subdir")); + assertTrue(checkExists(getBasedir() + "/target/subdir/file.txt")); // fileset 2 - assertTrue( - checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/" + "buildOutputDirectory")); - assertFalse(checkExists( - getBasedir() + "/target/test-classes/unit/fileset-clean-test/" + "buildOutputDirectory/file.txt")); + assertTrue(checkExists(getBasedir() + "/" + "buildOutputDirectory")); + assertFalse(checkExists(getBasedir() + "/" + "buildOutputDirectory/file.txt")); } /** @@ -143,9 +136,10 @@ void testFilesetsClean(CleanMojo mojo) throws Exception { * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/invalid-directory-test/plugin-pom.xml") - void testCleanInvalidDirectory(CleanMojo mojo) throws Exception { - assertThrows(MojoExecutionException.class, mojo::execute); + @Basedir("${basedir}/target/test-classes/unit/invalid-directory-test") + @InjectMojo(goal = "clean") + public void testCleanInvalidDirectory(CleanMojo mojo) throws Exception { + assertThrows(MojoException.class, mojo::execute, "Should fail to delete a file treated as a directory"); } /** @@ -154,11 +148,12 @@ void testCleanInvalidDirectory(CleanMojo mojo) throws Exception { * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/missing-directory-test/plugin-pom.xml") - void testMissingDirectory(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/missing-directory-test") + @InjectMojo(goal = "clean") + public void testMissingDirectory(CleanMojo mojo) throws Exception { mojo.execute(); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/missing-directory-test/does-not-exist")); + assertFalse(checkExists(getBasedir() + "/does-not-exist")); } /** @@ -171,14 +166,15 @@ void testMissingDirectory(CleanMojo mojo) throws Exception { */ @Test @EnabledOnOs(OS.WINDOWS) - @InjectMojo(goal = "clean", pom = "classpath:/unit/locked-file-test/plugin-pom.xml") - void testCleanLockedFile(CleanMojo mojo) throws Exception { - File f = new File(getBasedir(), "target/test-classes/unit/locked-file-test/buildDirectory/file.txt"); + @Basedir("${basedir}/target/test-classes/unit/locked-file-test") + @InjectMojo(goal = "clean") + public void testCleanLockedFile(CleanMojo mojo) throws Exception { + File f = new File(getBasedir(), "buildDirectory/file.txt"); try (FileChannel channel = new RandomAccessFile(f, "rw").getChannel(); FileLock ignored = channel.lock()) { mojo.execute(); fail("Should fail to delete a file that is locked"); - } catch (MojoExecutionException expected) { + } catch (MojoException expected) { assertTrue(true); } } @@ -193,29 +189,29 @@ void testCleanLockedFile(CleanMojo mojo) throws Exception { */ @Test @EnabledOnOs(OS.WINDOWS) - @InjectMojo(goal = "clean", pom = "classpath:/unit/locked-file-test/plugin-pom.xml") - void testCleanLockedFileWithNoError(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/locked-file-test") + @InjectMojo(goal = "clean") + public void testCleanLockedFileWithNoError(CleanMojo mojo) throws Exception { setVariableValueToObject(mojo, "failOnError", Boolean.FALSE); assertNotNull(mojo); - File f = new File(getBasedir(), "target/test-classes/unit/locked-file-test/buildDirectory/file.txt"); + File f = new File(getBasedir(), "buildDirectory/file.txt"); try (FileChannel channel = new RandomAccessFile(f, "rw").getChannel(); FileLock ignored = channel.lock()) { mojo.execute(); assertTrue(true); - } catch (MojoExecutionException expected) { + } catch (MojoException expected) { fail("Should display a warning when deleting a file that is locked"); } } /** * Test the followLink option with windows junctions - * * @throws Exception */ @Test @EnabledOnOs(OS.WINDOWS) - void testFollowLinksWithWindowsJunction() throws Exception { + public void testFollowLinksWithWindowsJunction() throws Exception { testSymlink((link, target) -> { Process process = new ProcessBuilder() .directory(link.getParent().toFile()) @@ -233,12 +229,11 @@ void testFollowLinksWithWindowsJunction() throws Exception { /** * Test the followLink option with sym link - * * @throws Exception */ @Test @DisabledOnOs(OS.WINDOWS) - void testFollowLinksWithSymLinkOnPosix() throws Exception { + public void testFollowLinksWithSymLinkOnPosix() throws Exception { testSymlink((link, target) -> { try { Files.createSymbolicLink(link, target); @@ -248,9 +243,13 @@ void testFollowLinksWithSymLinkOnPosix() throws Exception { }); } + @FunctionalInterface + interface LinkCreator { + void createLink(Path link, Path target) throws Exception; + } + private void testSymlink(LinkCreator linkCreator) throws Exception { - // We use the SystemStreamLog() as the AbstractMojo class, because from there the Log is always provided - Cleaner cleaner = new Cleaner(null, new SystemStreamLog(), false, null, null); + Cleaner cleaner = new Cleaner(null, null, false, null, null); Path testDir = Paths.get("target/test-classes/unit/test-dir").toAbsolutePath(); Path dirWithLnk = testDir.resolve("dir"); Path orgDir = testDir.resolve("org-dir"); @@ -263,7 +262,7 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { Files.write(file, Collections.singleton("Hello world")); linkCreator.createLink(jctDir, orgDir); // delete - cleaner.delete(dirWithLnk.toFile(), null, false, true, false); + cleaner.delete(dirWithLnk, null, false, true, false); // verify assertTrue(Files.exists(file)); assertFalse(Files.exists(jctDir)); @@ -276,7 +275,7 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { Files.write(file, Collections.singleton("Hello world")); linkCreator.createLink(jctDir, orgDir); // delete - cleaner.delete(dirWithLnk.toFile(), null, true, true, false); + cleaner.delete(dirWithLnk, null, true, true, false); // verify assertFalse(Files.exists(file)); assertFalse(Files.exists(jctDir)); @@ -284,6 +283,32 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { assertFalse(Files.exists(dirWithLnk)); } + // @Provides + // @Singleton + // private Project createProject() { + // ProjectStub project = new ProjectStub(); + // project.setGroupId("myGroupId"); + // return project; + // } + + // @Provides + // @Singleton + // @SuppressWarnings("unused") + // private InternalSession createSession() { + // InternalSession session = SessionStub.getMockSession(LOCAL_REPO); + // Properties props = new Properties(); + // props.put("basedir", MojoExtension.getBasedir()); + // doReturn(props).when(session).getSystemProperties(); + // return session; + // } + + // @Provides + // @Singleton + // @SuppressWarnings("unused") + // private MojoExecution createMojoExecution() { + // return new MojoExecutionStub("default-clean", "clean"); + // } + /** * @param dir a dir or a file * @return true if a file/dir exists, false otherwise @@ -300,9 +325,4 @@ private boolean checkEmpty(String dir) { File[] files = new File(dir).listFiles(); return files == null || files.length == 0; } - - @FunctionalInterface - interface LinkCreator { - void createLink(Path link, Path target) throws Exception; - } } diff --git a/src/test/resources/unit/basic-clean-test/plugin-pom.xml b/src/test/resources/unit/basic-clean-test/plugin-pom.xml deleted file mode 100644 index 38a6000c..00000000 --- a/src/test/resources/unit/basic-clean-test/plugin-pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - maven-clean-plugin - - ${basedir}/target/test-classes/unit/basic-clean-test/buildDirectory - ${basedir}/target/test-classes/unit/basic-clean-test/buildOutputDirectory - ${basedir}/target/test-classes/unit/basic-clean-test/buildTestDirectory - true - true - - - - - diff --git a/src/test/resources/unit/nested-clean-test/plugin-pom.xml b/src/test/resources/unit/basic-clean-test/pom.xml similarity index 76% rename from src/test/resources/unit/nested-clean-test/plugin-pom.xml rename to src/test/resources/unit/basic-clean-test/pom.xml index 790d14d5..ba901f06 100644 --- a/src/test/resources/unit/nested-clean-test/plugin-pom.xml +++ b/src/test/resources/unit/basic-clean-test/pom.xml @@ -23,9 +23,9 @@ maven-clean-plugin - ${basedir}/target/test-classes/unit/nested-clean-test/target - ${basedir}/target/test-classes/unit/nested-clean-test/target/classes - ${basedir}/target/test-classes/unit/nested-clean-test/target/test-classes + ${project.basedir}/buildDirectory + ${project.basedir}/buildOutputDirectory + ${project.basedir}/buildTestDirectory true true diff --git a/src/test/resources/unit/empty-clean-test/plugin-pom.xml b/src/test/resources/unit/empty-clean-test/pom.xml similarity index 100% rename from src/test/resources/unit/empty-clean-test/plugin-pom.xml rename to src/test/resources/unit/empty-clean-test/pom.xml diff --git a/src/test/resources/unit/fileset-clean-test/plugin-pom.xml b/src/test/resources/unit/fileset-clean-test/pom.xml similarity index 88% rename from src/test/resources/unit/fileset-clean-test/plugin-pom.xml rename to src/test/resources/unit/fileset-clean-test/pom.xml index 5cd3ab34..2457c4ed 100644 --- a/src/test/resources/unit/fileset-clean-test/plugin-pom.xml +++ b/src/test/resources/unit/fileset-clean-test/pom.xml @@ -23,9 +23,10 @@ maven-clean-plugin + true - ${basedir}/target/test-classes/unit/fileset-clean-test/target + ${project.basedir}/target **/file.txt **/test-classes/** @@ -35,7 +36,7 @@ - ${basedir}/target/test-classes/unit/fileset-clean-test/buildOutputDirectory + ${project.basedir}/buildOutputDirectory ** diff --git a/src/test/resources/unit/locked-file-test/plugin-pom.xml b/src/test/resources/unit/invalid-directory-test/pom.xml similarity index 91% rename from src/test/resources/unit/locked-file-test/plugin-pom.xml rename to src/test/resources/unit/invalid-directory-test/pom.xml index 4101091f..5d35e78c 100644 --- a/src/test/resources/unit/locked-file-test/plugin-pom.xml +++ b/src/test/resources/unit/invalid-directory-test/pom.xml @@ -23,7 +23,7 @@ maven-clean-plugin - ${basedir}/target/test-classes/unit/locked-file-test/buildDirectory + ${project.basedir}/this-is-a-file true true diff --git a/src/test/resources/unit/missing-directory-test/plugin-pom.xml b/src/test/resources/unit/locked-file-test/pom.xml similarity index 91% rename from src/test/resources/unit/missing-directory-test/plugin-pom.xml rename to src/test/resources/unit/locked-file-test/pom.xml index 15b3923b..1f2f429a 100644 --- a/src/test/resources/unit/missing-directory-test/plugin-pom.xml +++ b/src/test/resources/unit/locked-file-test/pom.xml @@ -23,7 +23,7 @@ maven-clean-plugin - ${basedir}/target/test-classes/unit/missing-clean-test/does-not-exist + ${project.basedir}/buildDirectory true true diff --git a/src/test/resources/unit/invalid-directory-test/plugin-pom.xml b/src/test/resources/unit/missing-directory-test/pom.xml similarity index 91% rename from src/test/resources/unit/invalid-directory-test/plugin-pom.xml rename to src/test/resources/unit/missing-directory-test/pom.xml index b5621223..ec695d12 100644 --- a/src/test/resources/unit/invalid-directory-test/plugin-pom.xml +++ b/src/test/resources/unit/missing-directory-test/pom.xml @@ -23,7 +23,7 @@ maven-clean-plugin - ${basedir}/target/test-classes/unit/invalid-directory-test/this-is-a-file + ${project.basedir}/does-not-exist true true diff --git a/src/test/resources/unit/nested-clean-test/pom.xml b/src/test/resources/unit/nested-clean-test/pom.xml new file mode 100644 index 00000000..5f9e755c --- /dev/null +++ b/src/test/resources/unit/nested-clean-test/pom.xml @@ -0,0 +1,35 @@ + + + + + + + maven-clean-plugin + + ${project.basedir}/target + ${project.basedir}/target/classes + ${project.basedir}/target/test-classes + true + true + + + + + From 30b89ca3ac0c8e96c4fe70ba88d5fe35c8526ed9 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 26 Jun 2024 07:52:29 +0200 Subject: [PATCH 02/90] Fix license/notice (#48) --- LICENSE | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ NOTICE | 5 ++ 2 files changed, 207 insertions(+) create mode 100644 LICENSE create mode 100644 NOTICE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..28b03487 --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +Apache Maven Clean Plugin +Copyright 2007-2024 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). From b9769ef83ff28b82704d4deb5ea27f422e681b70 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 26 Jun 2024 08:22:49 +0200 Subject: [PATCH 03/90] Build with JDK 17 and 21 in Jenkins (#49) --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index e9f05f7d..5fed9c58 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,4 +17,4 @@ * under the License. */ -asfMavenTlpPlgnBuild() +asfMavenTlpPlgnBuild(jdks:[ "17", "21" ]) From 99ac6665df55a90ac8de00b1b06117f6e6460f76 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 26 Jun 2024 08:40:48 +0200 Subject: [PATCH 04/90] Build with Maven 4.0.x on Jenkins --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 5fed9c58..217636c5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,4 +17,4 @@ * under the License. */ -asfMavenTlpPlgnBuild(jdks:[ "17", "21" ]) +asfMavenTlpPlgnBuild(jdks:[ "17", "21" ], maven: [ "4.0.x" ], siteJdk:[ "17" ], siteMvn:[ "4.0.x" ]) From 8c7ce9aed242acbc985f2201896cd0acfa2ec9ad Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 26 Jun 2024 10:15:28 +0200 Subject: [PATCH 05/90] Clean dependencies --- pom.xml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a7ef0bb5..dc34bcff 100644 --- a/pom.xml +++ b/pom.xml @@ -63,12 +63,25 @@ under the License. 4.0.0-beta-3 17 + 33.2.1-jre + 6.0.0 + 2.0.13 3.7.0 4.0.0-alpha-3-SNAPSHOT 4.0.0-SNAPSHOT 2024-06-16T10:25:11Z + + + + com.google.guava + guava + ${guavaVersion} + + + + org.apache.maven @@ -115,13 +128,13 @@ under the License. com.google.inject guice - 6.0.0 + ${guiceVersion} test org.slf4j slf4j-simple - 2.0.13 + ${slf4jVersion} test From e35167df10c12aaa12ec0da8a9466c2d6c5123b1 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 26 Jun 2024 10:17:24 +0200 Subject: [PATCH 06/90] Code formatting on ITs pom.xml --- pom.xml | 41 ++++++++----- src/it/dangling-symlinks/pom.xml | 6 +- src/it/default/pom.xml | 6 +- src/it/exclude-default-dirs/pom.xml | 6 +- src/it/fast-delete/pom.xml | 58 +++++++++---------- .../file-sets-absolute-paths/child-a/pom.xml | 8 +-- src/it/file-sets-absolute-paths/pom.xml | 6 +- src/it/file-sets-includes-excludes/pom.xml | 6 +- .../file-sets-relative-paths/child-a/pom.xml | 8 +-- .../file-sets-relative-paths/child-b/pom.xml | 8 +-- src/it/file-sets-relative-paths/pom.xml | 6 +- src/it/non-existent-base-dirs/pom.xml | 6 +- src/it/only-test-clean/pom.xml | 7 +-- src/it/special-characters/pom.xml | 6 +- src/it/symlink-dont-follow/pom.xml | 6 +- .../resources/unit/basic-clean-test/pom.xml | 2 +- .../resources/unit/empty-clean-test/pom.xml | 2 +- .../resources/unit/fileset-clean-test/pom.xml | 4 +- .../unit/invalid-directory-test/pom.xml | 2 +- .../resources/unit/locked-file-test/pom.xml | 2 +- .../unit/missing-directory-test/pom.xml | 2 +- .../resources/unit/nested-clean-test/pom.xml | 2 +- 22 files changed, 78 insertions(+), 122 deletions(-) diff --git a/pom.xml b/pom.xml index dc34bcff..c9e5ac37 100644 --- a/pom.xml +++ b/pom.xml @@ -140,20 +140,33 @@ under the License. - - - org.apache.maven.plugins - maven-plugin-plugin - ${version.maven-plugin-tools} - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - + + + + com.diffplug.spotless + spotless-maven-plugin + + + + src/**/*.java + + + + + **/pom.xml + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + diff --git a/src/it/dangling-symlinks/pom.xml b/src/it/dangling-symlinks/pom.xml index 7e185f52..0431c499 100644 --- a/src/it/dangling-symlinks/pom.xml +++ b/src/it/dangling-symlinks/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 test diff --git a/src/it/default/pom.xml b/src/it/default/pom.xml index fa866df4..c6251939 100644 --- a/src/it/default/pom.xml +++ b/src/it/default/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 test diff --git a/src/it/exclude-default-dirs/pom.xml b/src/it/exclude-default-dirs/pom.xml index 210817a9..b33c6043 100644 --- a/src/it/exclude-default-dirs/pom.xml +++ b/src/it/exclude-default-dirs/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 test diff --git a/src/it/fast-delete/pom.xml b/src/it/fast-delete/pom.xml index 92030564..f501863f 100644 --- a/src/it/fast-delete/pom.xml +++ b/src/it/fast-delete/pom.xml @@ -1,5 +1,4 @@ - - - - 4.0.0 - - test - fast-delete - 1.0-SNAPSHOT - - Fast delete - Check that fast delete is invoked. - - - UTF-8 - - - - - - org.apache.maven.plugins - maven-clean-plugin - @project.version@ - - true - ${project.basedir}${file.separator}.fastdir - - - - + + 4.0.0 + + test + fast-delete + 1.0-SNAPSHOT + + Fast delete + Check that fast delete is invoked. + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-clean-plugin + @project.version@ + + true + ${project.basedir}${file.separator}.fastdir + + + + diff --git a/src/it/file-sets-absolute-paths/child-a/pom.xml b/src/it/file-sets-absolute-paths/child-a/pom.xml index ed4536d4..07bfc770 100644 --- a/src/it/file-sets-absolute-paths/child-a/pom.xml +++ b/src/it/file-sets-absolute-paths/child-a/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 @@ -30,7 +26,7 @@ under the License. 1.0-SNAPSHOT - child-a + child-a 1.0-SNAPSHOT Child A diff --git a/src/it/file-sets-absolute-paths/pom.xml b/src/it/file-sets-absolute-paths/pom.xml index dfa87f63..ce9e654c 100644 --- a/src/it/file-sets-absolute-paths/pom.xml +++ b/src/it/file-sets-absolute-paths/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 test diff --git a/src/it/file-sets-includes-excludes/pom.xml b/src/it/file-sets-includes-excludes/pom.xml index cdae0584..dfee54c0 100644 --- a/src/it/file-sets-includes-excludes/pom.xml +++ b/src/it/file-sets-includes-excludes/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 test diff --git a/src/it/file-sets-relative-paths/child-a/pom.xml b/src/it/file-sets-relative-paths/child-a/pom.xml index a1ff584e..53fcca7b 100644 --- a/src/it/file-sets-relative-paths/child-a/pom.xml +++ b/src/it/file-sets-relative-paths/child-a/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 @@ -30,7 +26,7 @@ under the License. 1.0-SNAPSHOT - child-a + child-a 1.0-SNAPSHOT Child A diff --git a/src/it/file-sets-relative-paths/child-b/pom.xml b/src/it/file-sets-relative-paths/child-b/pom.xml index 6821d50c..5795647f 100644 --- a/src/it/file-sets-relative-paths/child-b/pom.xml +++ b/src/it/file-sets-relative-paths/child-b/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 @@ -30,7 +26,7 @@ under the License. 1.0-SNAPSHOT - child-b + child-b 1.0-SNAPSHOT Child B diff --git a/src/it/file-sets-relative-paths/pom.xml b/src/it/file-sets-relative-paths/pom.xml index 4117bdfd..ab708d4d 100644 --- a/src/it/file-sets-relative-paths/pom.xml +++ b/src/it/file-sets-relative-paths/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 test diff --git a/src/it/non-existent-base-dirs/pom.xml b/src/it/non-existent-base-dirs/pom.xml index 16b2242c..e5473574 100644 --- a/src/it/non-existent-base-dirs/pom.xml +++ b/src/it/non-existent-base-dirs/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 test diff --git a/src/it/only-test-clean/pom.xml b/src/it/only-test-clean/pom.xml index 716e8de8..a23365fd 100644 --- a/src/it/only-test-clean/pom.xml +++ b/src/it/only-test-clean/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 test @@ -73,5 +69,4 @@ under the License. - diff --git a/src/it/special-characters/pom.xml b/src/it/special-characters/pom.xml index 35cd8b8b..911e6070 100644 --- a/src/it/special-characters/pom.xml +++ b/src/it/special-characters/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 org.apache.maven.plugins.clean.its diff --git a/src/it/symlink-dont-follow/pom.xml b/src/it/symlink-dont-follow/pom.xml index 88e8c077..de0c2d9a 100644 --- a/src/it/symlink-dont-follow/pom.xml +++ b/src/it/symlink-dont-follow/pom.xml @@ -1,5 +1,4 @@ - - - + 4.0.0 test diff --git a/src/test/resources/unit/basic-clean-test/pom.xml b/src/test/resources/unit/basic-clean-test/pom.xml index ba901f06..6981ce73 100644 --- a/src/test/resources/unit/basic-clean-test/pom.xml +++ b/src/test/resources/unit/basic-clean-test/pom.xml @@ -1,3 +1,4 @@ + - diff --git a/src/test/resources/unit/empty-clean-test/pom.xml b/src/test/resources/unit/empty-clean-test/pom.xml index 3d7000d8..54ae6b06 100644 --- a/src/test/resources/unit/empty-clean-test/pom.xml +++ b/src/test/resources/unit/empty-clean-test/pom.xml @@ -1,3 +1,4 @@ + - diff --git a/src/test/resources/unit/fileset-clean-test/pom.xml b/src/test/resources/unit/fileset-clean-test/pom.xml index 2457c4ed..90c09715 100644 --- a/src/test/resources/unit/fileset-clean-test/pom.xml +++ b/src/test/resources/unit/fileset-clean-test/pom.xml @@ -1,3 +1,4 @@ + - @@ -41,7 +41,7 @@ ** - + diff --git a/src/test/resources/unit/invalid-directory-test/pom.xml b/src/test/resources/unit/invalid-directory-test/pom.xml index 5d35e78c..a278e722 100644 --- a/src/test/resources/unit/invalid-directory-test/pom.xml +++ b/src/test/resources/unit/invalid-directory-test/pom.xml @@ -1,3 +1,4 @@ + - diff --git a/src/test/resources/unit/locked-file-test/pom.xml b/src/test/resources/unit/locked-file-test/pom.xml index 1f2f429a..a677da40 100644 --- a/src/test/resources/unit/locked-file-test/pom.xml +++ b/src/test/resources/unit/locked-file-test/pom.xml @@ -1,3 +1,4 @@ + - diff --git a/src/test/resources/unit/missing-directory-test/pom.xml b/src/test/resources/unit/missing-directory-test/pom.xml index ec695d12..bcf3a242 100644 --- a/src/test/resources/unit/missing-directory-test/pom.xml +++ b/src/test/resources/unit/missing-directory-test/pom.xml @@ -1,3 +1,4 @@ + - diff --git a/src/test/resources/unit/nested-clean-test/pom.xml b/src/test/resources/unit/nested-clean-test/pom.xml index 5f9e755c..77f95d37 100644 --- a/src/test/resources/unit/nested-clean-test/pom.xml +++ b/src/test/resources/unit/nested-clean-test/pom.xml @@ -1,3 +1,4 @@ + - From bbdee584e46b6603755eeebda9cdf2ae2438b616 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 26 Jun 2024 10:17:48 +0200 Subject: [PATCH 07/90] Upgrade to m-plugin-testing and m-plugin-tools 4.0.0-beta-1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c9e5ac37..c0b6f3da 100644 --- a/pom.xml +++ b/pom.xml @@ -67,8 +67,8 @@ under the License. 6.0.0 2.0.13 3.7.0 - 4.0.0-alpha-3-SNAPSHOT - 4.0.0-SNAPSHOT + 4.0.0-beta-1 + 4.0.0-beta-1 2024-06-16T10:25:11Z From 99986b8c0517c8170b708369916962eed7e13a93 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 26 Jun 2024 10:19:17 +0200 Subject: [PATCH 08/90] [maven-release-plugin] prepare release maven-clean-plugin-4.0.0-beta-1 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index c0b6f3da..818858fa 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ under the License. maven-clean-plugin - 4.0.0-SNAPSHOT + 4.0.0-beta-1 maven-plugin Apache Maven Clean Plugin @@ -42,7 +42,7 @@ under the License. scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git - HEAD + maven-clean-plugin-4.0.0-beta-1 https://github.com/apache/maven-clean-plugin/tree/${project.scm.tag} @@ -69,7 +69,7 @@ under the License. 3.7.0 4.0.0-beta-1 4.0.0-beta-1 - 2024-06-16T10:25:11Z + 2024-06-26T08:19:11Z From 648ca78783d0cfa90df30ad94858fb7f1c0ae4b5 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 26 Jun 2024 10:19:25 +0200 Subject: [PATCH 09/90] [maven-release-plugin] prepare for next development iteration --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 818858fa..a716836b 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ under the License. maven-clean-plugin - 4.0.0-beta-1 + 4.0.0-beta-2-SNAPSHOT maven-plugin Apache Maven Clean Plugin @@ -42,7 +42,7 @@ under the License. scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git - maven-clean-plugin-4.0.0-beta-1 + HEAD https://github.com/apache/maven-clean-plugin/tree/${project.scm.tag} @@ -69,7 +69,7 @@ under the License. 3.7.0 4.0.0-beta-1 4.0.0-beta-1 - 2024-06-26T08:19:11Z + 2024-06-26T08:19:25Z From 87a7adc5b088e437de1343b23a54528fcbfd790a Mon Sep 17 00:00:00 2001 From: Michael Osipov Date: Wed, 3 Jul 2024 20:38:41 +0200 Subject: [PATCH 10/90] Inherit site skin from parent --- src/site/site.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/site/site.xml b/src/site/site.xml index d776c3d8..b14b745b 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -21,11 +21,6 @@ under the License. - - org.apache.maven.skins - maven-fluido-skin - 1.9 - From d4f554fa40e5513cd5e22f685764948ac6efed7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Boutemy?= Date: Mon, 19 Aug 2024 02:03:32 +0200 Subject: [PATCH 11/90] use new Reproducible Central badge endpoint --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e3a867a..97d2634a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Contributing to [Apache Maven Clean Plugin](https://maven.apache.org/plugins/mav [![ASF Jira](https://img.shields.io/endpoint?url=https%3A%2F%2Fmaven.apache.org%2Fbadges%2Fasf_jira-MCLEAN.json)][jira] [![Apache License, Version 2.0, January 2004](https://img.shields.io/github/license/apache/maven.svg?label=License)][license] [![Maven Central](https://img.shields.io/maven-central/v/org.apache.maven.plugins/maven-clean-plugin.svg?label=Maven%20Central)](https://search.maven.org/artifact/org.apache.maven.plugins/maven-clean-plugin) -[![Reproducible Builds](https://img.shields.io/badge/Reproducible_Builds-ok-green?labelColor=blue)](https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/org/apache/maven/plugins/maven-clean-plugin/README.md) +[![Reproducible Builds](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jvm-repo-rebuild/reproducible-central/master/content/org/apache/maven/plugins/maven-clean-plugin/badge.json)](https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/org/apache/maven/plugins/maven-clean-plugin/README.md) [![Jenkins Status](https://img.shields.io/jenkins/s/https/ci-maven.apache.org/job/Maven/job/maven-box/job/maven-clean-plugin/job/master.svg?)][build] [![Jenkins tests](https://img.shields.io/jenkins/t/https/ci-maven.apache.org/job/Maven/job/maven-box/job/maven-clean-plugin/job/master.svg?)][test-results] From 50593b37aef6c9cebc920a74473a9a007bcfcda2 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Fri, 8 Nov 2024 20:10:30 +0100 Subject: [PATCH 12/90] Drop comment from jira integration --- .asf.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.asf.yaml b/.asf.yaml index 853ac712..29922675 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -34,4 +34,4 @@ notifications: commits: commits@maven.apache.org issues: issues@maven.apache.org pullrequests: issues@maven.apache.org - jira_options: link label comment + jira_options: link label From 68d4eca4c4607be0be8f8961216138ee013cda42 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 14 Nov 2024 08:49:52 +0100 Subject: [PATCH 13/90] Upgrade to beta-5 (#58) --- .github/workflows/maven-verify.yml | 4 ++-- pom.xml | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 56062b54..ff3f00c3 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -26,6 +26,6 @@ jobs: name: Verify uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v4 with: - ff-maven: "4.0.0-beta-3" # Maven version for fail-fast-build - maven-matrix: '[ "4.0.0-beta-3" ]' + ff-maven: "4.0.0-beta-5" # Maven version for fail-fast-build + maven-matrix: '[ "4.0.0-beta-5" ]' jdk-matrix: '[ "17", "21" ]' diff --git a/pom.xml b/pom.xml index a716836b..2a93992d 100644 --- a/pom.xml +++ b/pom.xml @@ -61,14 +61,16 @@ under the License. - 4.0.0-beta-3 + 4.0.0-beta-5 17 33.2.1-jre 6.0.0 2.0.13 3.7.0 - 4.0.0-beta-1 + 4.0.0-beta-2 4.0.0-beta-1 + 4.0.0-M16 + 2.0.0-M11 2024-06-26T08:19:25Z @@ -119,6 +121,12 @@ under the License. junit-jupiter-api test + + org.apache.maven + maven-api-impl + ${mavenVersion} + test + org.apache.maven maven-core From 155e5dc678ef4dde961582952a5d5dc44a1d6caf Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Wed, 27 Nov 2024 19:35:43 +0000 Subject: [PATCH 14/90] Clean up assertions in CleanMojoTest (#61) * Clean up assertions --- .../org/apache/maven/plugins/clean/CleanMojoTest.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java index 5a5d0d45..882a672d 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java @@ -45,14 +45,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; /** * Test the clean mojo. */ @MojoTest public class CleanMojoTest { - private static final String LOCAL_REPO = "target/local-repo/"; /** * Tests the simple removal of directories @@ -172,10 +170,7 @@ public void testCleanLockedFile(CleanMojo mojo) throws Exception { File f = new File(getBasedir(), "buildDirectory/file.txt"); try (FileChannel channel = new RandomAccessFile(f, "rw").getChannel(); FileLock ignored = channel.lock()) { - mojo.execute(); - fail("Should fail to delete a file that is locked"); - } catch (MojoException expected) { - assertTrue(true); + assertThrows(MojoException.class, () -> mojo.execute()); } } @@ -199,9 +194,6 @@ public void testCleanLockedFileWithNoError(CleanMojo mojo) throws Exception { try (FileChannel channel = new RandomAccessFile(f, "rw").getChannel(); FileLock ignored = channel.lock()) { mojo.execute(); - assertTrue(true); - } catch (MojoException expected) { - fail("Should display a warning when deleting a file that is locked"); } } From 95cc132fd7334158f73df6b2ec5503ef5b36a549 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:14:19 +0100 Subject: [PATCH 15/90] Bump org.apache.maven.plugins:maven-plugins from 42 to 43 (#52) Bumps [org.apache.maven.plugins:maven-plugins](https://github.com/apache/maven-parent) from 42 to 43. - [Release notes](https://github.com/apache/maven-parent/releases) - [Commits](https://github.com/apache/maven-parent/commits) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-plugins dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2a93992d..423e55b9 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.maven.plugins maven-plugins - 42 + 43 From 04f274953fdf3224ec780d6f5eb47f428fc43b9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:15:22 +0100 Subject: [PATCH 16/90] Bump org.slf4j:slf4j-simple from 2.0.13 to 2.0.16 (#55) Bumps org.slf4j:slf4j-simple from 2.0.13 to 2.0.16. --- updated-dependencies: - dependency-name: org.slf4j:slf4j-simple dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 423e55b9..510597b4 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ under the License. 17 33.2.1-jre 6.0.0 - 2.0.13 + 2.0.16 3.7.0 4.0.0-beta-2 4.0.0-beta-1 From 4a9ad244c13c75995c4e63619fb72744ac81b03e Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Wed, 8 Jan 2025 21:24:26 +0100 Subject: [PATCH 17/90] (doc) Delete branch on merge --- .asf.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.asf.yaml b/.asf.yaml index 29922675..d9b63d32 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -1,13 +1,13 @@ -# +# # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You 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 -# +# # http://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. @@ -30,6 +30,8 @@ github: rebase: true autolink_jira: - MCLEAN + del_branch_on_merge: true + notifications: commits: commits@maven.apache.org issues: issues@maven.apache.org From c17d0921ae832092207aa02c08c405177ef1fe11 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Fri, 10 Jan 2025 19:36:15 +0100 Subject: [PATCH 18/90] Disable jenkins build As we have a beta version on master we need a newer version of Maven which is not installed on jenkins --- Jenkinsfile => Jenkinsfile.disabled | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Jenkinsfile => Jenkinsfile.disabled (100%) diff --git a/Jenkinsfile b/Jenkinsfile.disabled similarity index 100% rename from Jenkinsfile rename to Jenkinsfile.disabled From 33078f9860e8a4e0a9932fc66fb482f9a2a7dca2 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Thu, 9 Jan 2025 23:51:50 +0100 Subject: [PATCH 19/90] Add Release Drafter --- .github/release-drafter-3.x.yml | 22 ++++++++++++++++++++++ .github/release-drafter.yml | 22 ++++++++++++++++++++++ .github/workflows/release-drafter.yml | 27 +++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 .github/release-drafter-3.x.yml create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/release-drafter.yml diff --git a/.github/release-drafter-3.x.yml b/.github/release-drafter-3.x.yml new file mode 100644 index 00000000..083cd490 --- /dev/null +++ b/.github/release-drafter-3.x.yml @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +_extends: maven-gh-actions-shared +tag-template: maven-clean-plugin-$NEXT_MINOR_VERSION +version-template: 3.$MINOR.$PATCH +commitish: maven-clean-plugin-3.x +filter-by-commitish: true diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..7a69eeb7 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +_extends: maven-gh-actions-shared +tag-template: maven-clean-plugin-$NEXT_MINOR_VERSION +version-template: 4.$MINOR.$PATCH +commitish: master +filter-by-commitish: true diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..1695d359 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,27 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +name: Release Drafter +on: + push: + branches: + - master + workflow_dispatch: + +jobs: + update_release_draft: + uses: apache/maven-gh-actions-shared/.github/workflows/release-drafter.yml@v4 From d0dc919a6c9fd8848c7324de73cad151d77dcc37 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Fri, 10 Jan 2025 21:36:58 +0100 Subject: [PATCH 20/90] Fix config in release-drafter-3.x.yml --- .github/release-drafter-3.x.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release-drafter-3.x.yml b/.github/release-drafter-3.x.yml index 083cd490..a9448105 100644 --- a/.github/release-drafter-3.x.yml +++ b/.github/release-drafter-3.x.yml @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -_extends: maven-gh-actions-shared +_extends: maven-gh-actions-shared:.github/release-drafter.yml tag-template: maven-clean-plugin-$NEXT_MINOR_VERSION version-template: 3.$MINOR.$PATCH commitish: maven-clean-plugin-3.x From 1c6f59c632c8352068df6e847426962bc83a6cf6 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Fri, 10 Jan 2025 22:02:35 +0100 Subject: [PATCH 21/90] Add prerelease identifier to release-drafter --- .github/release-drafter.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 7a69eeb7..99916821 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -16,7 +16,8 @@ # under the License. _extends: maven-gh-actions-shared -tag-template: maven-clean-plugin-$NEXT_MINOR_VERSION +tag-template: maven-clean-plugin-$NEXT_PATCH_VERSION version-template: 4.$MINOR.$PATCH commitish: master filter-by-commitish: true +prerelease-identifier: 'beta' From 391c739080f8dae583f7bf2f5caba3ee7e0fc1c1 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sun, 19 Jan 2025 16:50:23 +0100 Subject: [PATCH 22/90] Bump Maven in CI to 4.0 rc2 (#66) --- .github/workflows/maven-verify.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index ff3f00c3..2bc0ff1e 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -26,6 +26,6 @@ jobs: name: Verify uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v4 with: - ff-maven: "4.0.0-beta-5" # Maven version for fail-fast-build - maven-matrix: '[ "4.0.0-beta-5" ]' + ff-maven: "4.0.0-rc-2" # Maven version for fail-fast-build + maven-matrix: '[ "4.0.0-rc-2" ]' jdk-matrix: '[ "17", "21" ]' From f79f1e420f27c77e67e64c8ad99a50aaf6ea81d0 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Sun, 19 Jan 2025 18:13:55 +0000 Subject: [PATCH 23/90] [MCLEAN-127] Use Files.createSymbolicLink instead of shelling out (#65) * Use Files.createSymbolicLink instead of shelling out --- .../{setup.bsh => setup.groovy} | 10 ++-- .../{verify.bsh => verify.groovy} | 0 .../{setup.bsh => setup.groovy} | 26 +++++---- .../{verify.bsh => verify.groovy} | 19 +++---- .../org/apache/maven/plugins/clean/Utils.java | 53 ------------------- 5 files changed, 29 insertions(+), 79 deletions(-) rename src/it/dangling-symlinks/{setup.bsh => setup.groovy} (84%) rename src/it/dangling-symlinks/{verify.bsh => verify.groovy} (100%) rename src/it/symlink-dont-follow/{setup.bsh => setup.groovy} (68%) rename src/it/symlink-dont-follow/{verify.bsh => verify.groovy} (79%) delete mode 100644 src/test/java/org/apache/maven/plugins/clean/Utils.java diff --git a/src/it/dangling-symlinks/setup.bsh b/src/it/dangling-symlinks/setup.groovy similarity index 84% rename from src/it/dangling-symlinks/setup.bsh rename to src/it/dangling-symlinks/setup.groovy index 4dc56675..81623d14 100644 --- a/src/it/dangling-symlinks/setup.bsh +++ b/src/it/dangling-symlinks/setup.groovy @@ -18,6 +18,7 @@ */ import java.io.*; +import java.nio.file.*; import java.util.*; import java.util.jar.*; import java.util.regex.*; @@ -30,17 +31,18 @@ try File target = new File( targetDir, "link-target.txt" ); System.out.println( "Creating symlink " + link + " -> " + target ); - if ( !Utils.createSymlink( target, link ) || !link.exists() ) + Files.createSymbolicLink( link.toPath(), target.toPath() ); + if ( !link.exists() ) { - System.out.println( "FAILURE, platform does not support symlinks, skipping test." ); + System.out.println( "Platform does not support symlinks, skipping test." ); } System.out.println( "Deleting symlink target " + target ); target.delete(); } -catch( Throwable t ) +catch( Exception ex ) { - t.printStackTrace(); + ex.printStackTrace(); return false; } diff --git a/src/it/dangling-symlinks/verify.bsh b/src/it/dangling-symlinks/verify.groovy similarity index 100% rename from src/it/dangling-symlinks/verify.bsh rename to src/it/dangling-symlinks/verify.groovy diff --git a/src/it/symlink-dont-follow/setup.bsh b/src/it/symlink-dont-follow/setup.groovy similarity index 68% rename from src/it/symlink-dont-follow/setup.bsh rename to src/it/symlink-dont-follow/setup.groovy index 50bec7fe..7ae9bc55 100644 --- a/src/it/symlink-dont-follow/setup.bsh +++ b/src/it/symlink-dont-follow/setup.groovy @@ -18,27 +18,31 @@ */ import java.io.*; +import java.nio.file.*; import java.util.*; import java.util.jar.*; import java.util.regex.*; import org.apache.maven.plugins.clean.*; -String[][] pairs = -{ - { "ext/file.txt", "target/link.txt" }, - { "ext/dir", "target/link" }, - { "ext/file.txt", "target2/link.txt" }, - { "ext/dir", "target2/link" }, -}; +def pairs = +[ + [ "ext/file.txt", "target/link.txt" ], + [ "ext/dir", "target/link" ], + [ "ext/file.txt", "target2/link.txt" ], + [ "ext/dir", "target2/link" ], +]; -for ( String[] pair : pairs ) +for ( pair : pairs ) { File target = new File( basedir, pair[0] ); File link = new File( basedir, pair[1] ); - System.out.println( "Creating symlink " + link + " -> " + target ); - if ( !Utils.createSymlink( target, link ) || !link.exists() ) + println "Creating symlink " + link + " -> " + target; + Path targetPath = target.toPath(); + Path linkPath = link.toPath(); + Files.createSymbolicLink( linkPath, targetPath ); + if ( !link.exists() ) { - System.out.println( "FAILURE, platform does not support symlinks, skipping test." ); + println "Platform does not support symlinks, skipping test."; return; } } diff --git a/src/it/symlink-dont-follow/verify.bsh b/src/it/symlink-dont-follow/verify.groovy similarity index 79% rename from src/it/symlink-dont-follow/verify.bsh rename to src/it/symlink-dont-follow/verify.groovy index d8e4c7a6..dae47c9d 100644 --- a/src/it/symlink-dont-follow/verify.bsh +++ b/src/it/symlink-dont-follow/verify.groovy @@ -18,43 +18,40 @@ */ import java.io.*; -import java.util.*; -import java.util.jar.*; -import java.util.regex.*; -String[] expected = { +def expected = [ "ext", "ext/file.txt", "ext/dir/file.txt", -}; +]; for ( String path : expected ) { File file = new File( basedir, path ); - System.out.println( "Checking for existence of " + file ); + println "Checking for existence of " + file; if ( !file.exists() ) { - System.out.println( "FAILURE!" ); + println "FAILURE!"; return false; } } -String[] unexpected = { +def unexpected = [ "target/link.txt", "target/link", "target", "target2/link.txt", "target2/link", "target2", -}; +]; for ( String path : unexpected ) { File file = new File( basedir, path ); - System.out.println( "Checking for absence of " + file ); + println "Checking for absence of " + file; if ( file.exists() ) { - System.out.println( "FAILURE!" ); + println "FAILURE!"; return false; } } diff --git a/src/test/java/org/apache/maven/plugins/clean/Utils.java b/src/test/java/org/apache/maven/plugins/clean/Utils.java deleted file mode 100644 index f96226d9..00000000 --- a/src/test/java/org/apache/maven/plugins/clean/Utils.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 - * - * http://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.apache.maven.plugins.clean; - -import java.io.File; - -import org.codehaus.plexus.util.cli.CommandLineUtils; -import org.codehaus.plexus.util.cli.Commandline; - -/** - * Testing helpers for the IT scripts. - * - * @author Benjamin Bentmann - */ -public class Utils { - - /** - * Creates a symbolic link. - * - * @param target The target (file or directory) of the link, must not be null. - * @param link The path to the link, must not be null. - * @return true if the symlink could be created, false otherwise. - */ - public static boolean createSymlink(File target, File link) { - try { - Commandline cli = new Commandline(); - cli.setExecutable("ln"); - cli.createArg().setValue("-s"); - cli.createArg().setFile(target); - cli.createArg().setFile(link); - int code = CommandLineUtils.executeCommandLine(cli, System.out::println, System.err::println); - return 0 == code; - } catch (Exception e) { - return false; - } - } -} From 9056ffd80a1f735ce604cd42941a46849f401551 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Wed, 22 Jan 2025 00:55:19 +0100 Subject: [PATCH 24/90] Updated Dependabot setup to monitor 3.x branch --- .github/dependabot.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d5588b49..e4940818 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,15 +16,19 @@ # version: 2 updates: + - package-ecosystem: "maven" directory: "/" schedule: interval: "daily" - ignore: - # Ignore Maven Core updates - - dependency-name: "org.apache.maven:*" + - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "daily" + target-branch: "maven-clean-plugin-3.x" From 5f46be489c65e1b5be7b5b7600f85d8a9996a52c Mon Sep 17 00:00:00 2001 From: Peter De Maeyer Date: Sun, 19 Jan 2025 16:55:46 +0100 Subject: [PATCH 25/90] Bump maven-site-plugin to 3.21.0 and Skin to 2.0.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 510597b4..adad51b9 100644 --- a/pom.xml +++ b/pom.xml @@ -69,8 +69,8 @@ under the License. 3.7.0 4.0.0-beta-2 4.0.0-beta-1 - 4.0.0-M16 - 2.0.0-M11 + 3.21.0 + 2.0.1 2024-06-26T08:19:25Z From 63f2ac75eb9fbd9c7db5dc1a417fa8c8495d029f Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Tue, 28 Jan 2025 19:12:01 +0100 Subject: [PATCH 26/90] Drop prerelease identifier from release-drafter --- .github/release-drafter.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 99916821..582b0beb 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -20,4 +20,3 @@ tag-template: maven-clean-plugin-$NEXT_PATCH_VERSION version-template: 4.$MINOR.$PATCH commitish: master filter-by-commitish: true -prerelease-identifier: 'beta' From f4e99f579a8366a4a0290a2f7904c1ad8a660a2f Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Tue, 28 Jan 2025 19:33:15 +0100 Subject: [PATCH 27/90] Add PR Automation --- .github/workflows/pr-automation.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/pr-automation.yml diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml new file mode 100644 index 00000000..e9d66988 --- /dev/null +++ b/.github/workflows/pr-automation.yml @@ -0,0 +1,29 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +name: PR Automation +on: + pull_request_target: + types: + - closed + - unlabeled + - review_requested + +jobs: + pr-automation: + name: PR Automation + uses: apache/maven-gh-actions-shared/.github/workflows/pr-automation.yml@v4 From 8cf010733a9274ff88f77aa85e82200f9ed6d153 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Wed, 29 Jan 2025 13:23:17 +0100 Subject: [PATCH 28/90] Use resolved versions in release drafter --- .github/release-drafter-3.x.yml | 6 ++++-- .github/release-drafter.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/release-drafter-3.x.yml b/.github/release-drafter-3.x.yml index a9448105..44537beb 100644 --- a/.github/release-drafter-3.x.yml +++ b/.github/release-drafter-3.x.yml @@ -16,7 +16,9 @@ # under the License. _extends: maven-gh-actions-shared:.github/release-drafter.yml -tag-template: maven-clean-plugin-$NEXT_MINOR_VERSION -version-template: 3.$MINOR.$PATCH + +name-template: $RESOLVED_VERSION +tag-template: maven-clean-plugin-$RESOLVED_VERSION + commitish: maven-clean-plugin-3.x filter-by-commitish: true diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 582b0beb..a219c03f 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -16,7 +16,9 @@ # under the License. _extends: maven-gh-actions-shared -tag-template: maven-clean-plugin-$NEXT_PATCH_VERSION -version-template: 4.$MINOR.$PATCH + +name-template: $RESOLVED_VERSION +tag-template: maven-clean-plugin-$RESOLVED_VERSION + commitish: master filter-by-commitish: true From 2d7979c92d431fa9d30cbce56440a6b7161730a1 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Wed, 29 Jan 2025 15:05:09 +0100 Subject: [PATCH 29/90] Use COMPLETE for version-template in release drafter --- .github/release-drafter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index a219c03f..ad85203e 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -19,6 +19,7 @@ _extends: maven-gh-actions-shared name-template: $RESOLVED_VERSION tag-template: maven-clean-plugin-$RESOLVED_VERSION +version-template: $COMPLETE commitish: master filter-by-commitish: true From ba920a15c46aa6a0703687f864d0914f0a6b6751 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sat, 1 Feb 2025 15:17:22 +0100 Subject: [PATCH 30/90] Use global release-drafter configuration --- .github/release-drafter.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index ad85203e..8922af47 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -17,9 +17,4 @@ _extends: maven-gh-actions-shared -name-template: $RESOLVED_VERSION tag-template: maven-clean-plugin-$RESOLVED_VERSION -version-template: $COMPLETE - -commitish: master -filter-by-commitish: true From 906067111461312eb0698a778cc492b75acc10e5 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sat, 1 Feb 2025 17:54:29 +0100 Subject: [PATCH 31/90] Drop unused release-drafter configuration --- .github/release-drafter-3.x.yml | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 .github/release-drafter-3.x.yml diff --git a/.github/release-drafter-3.x.yml b/.github/release-drafter-3.x.yml deleted file mode 100644 index 44537beb..00000000 --- a/.github/release-drafter-3.x.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. - -_extends: maven-gh-actions-shared:.github/release-drafter.yml - -name-template: $RESOLVED_VERSION -tag-template: maven-clean-plugin-$RESOLVED_VERSION - -commitish: maven-clean-plugin-3.x -filter-by-commitish: true From 3cc5d607b14f0e351ee83c4895889184a11b5a1a Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sun, 2 Feb 2025 00:36:31 +0100 Subject: [PATCH 32/90] Add configuration for pre-release in master --- .github/release-drafter-3.x.yml | 20 ++++++++++++++++++++ .github/release-drafter.yml | 3 +++ 2 files changed, 23 insertions(+) create mode 100644 .github/release-drafter-3.x.yml diff --git a/.github/release-drafter-3.x.yml b/.github/release-drafter-3.x.yml new file mode 100644 index 00000000..ec320ddc --- /dev/null +++ b/.github/release-drafter-3.x.yml @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +_extends: maven-gh-actions-shared:.github/release-drafter.yml + +tag-template: maven-clean-plugin-$RESOLVED_VERSION diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 8922af47..ef6c4f1d 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -18,3 +18,6 @@ _extends: maven-gh-actions-shared tag-template: maven-clean-plugin-$RESOLVED_VERSION + +include-pre-releases: true +prerelease: true From 282cf6131d1c2c50bbb772d127df70c90e19ffc2 Mon Sep 17 00:00:00 2001 From: Peter De Maeyer Date: Sat, 9 Nov 2024 16:19:42 +0100 Subject: [PATCH 33/90] [MCLEAN-124] Leverage Files.delete(Path) API to provide more accurate reason in case of failure --- pom.xml | 6 + .../apache/maven/plugins/clean/Cleaner.java | 93 +++++++----- .../maven/plugins/clean/CleanerTest.java | 140 ++++++++++++++++++ 3 files changed, 199 insertions(+), 40 deletions(-) create mode 100644 src/test/java/org/apache/maven/plugins/clean/CleanerTest.java diff --git a/pom.xml b/pom.xml index adad51b9..3dde41f2 100644 --- a/pom.xml +++ b/pom.xml @@ -139,6 +139,12 @@ under the License. ${guiceVersion} test + + org.mockito + mockito-core + 4.11.0 + test + org.slf4j slf4j-simple diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java index 533a2241..70d6832a 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -74,11 +74,11 @@ class Cleaner { /** * Creates a new cleaner. * - * @param session The Maven session to be used. - * @param log The logger to use, may be null to disable logging. - * @param verbose Whether to perform verbose logging. - * @param fastDir The explicit configured directory or to be deleted in fast mode. - * @param fastMode The fast deletion mode. + * @param session the Maven session to be used + * @param log the logger to use, may be null to disable logging + * @param verbose whether to perform verbose logging + * @param fastDir the explicit configured directory or to be deleted in fast mode + * @param fastMode the fast deletion mode */ Cleaner(Session session, Log log, boolean verbose, Path fastDir, String fastMode) { logDebug = (log == null || !log.isDebugEnabled()) ? null : logger(log::debug, log::debug); @@ -111,14 +111,14 @@ public void log(CharSequence message, Throwable t) { /** * Deletes the specified directories and its contents. * - * @param basedir The directory to delete, must not be null. Non-existing directories will be silently - * ignored. - * @param selector The selector used to determine what contents to delete, may be null to delete - * everything. - * @param followSymlinks Whether to follow symlinks. - * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted. - * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. - * @throws IOException If a file/directory could not be deleted and failOnError is true. + * @param basedir the directory to delete, must not be null. Non-existing directories will be silently + * ignored + * @param selector the selector used to determine what contents to delete, may be null to delete + * everything + * @param followSymlinks whether to follow symlinks + * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted + * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed + * @throws IOException if a file/directory could not be deleted and failOnError is true */ public void delete( Path basedir, Selector selector, boolean followSymlinks, boolean failOnError, boolean retryOnError) @@ -209,17 +209,17 @@ private boolean fastDelete(Path baseDir) { /** * Deletes the specified file or directory. * - * @param file The file/directory to delete, must not be null. If followSymlinks is - * false, it is assumed that the parent file is canonical. - * @param pathname The relative pathname of the file, using {@link File#separatorChar}, must not be - * null. - * @param selector The selector used to determine what contents to delete, may be null to delete - * everything. - * @param followSymlinks Whether to follow symlinks. - * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted. - * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. - * @return The result of the cleaning, never null. - * @throws IOException If a file/directory could not be deleted and failOnError is true. + * @param file the file/directory to delete, must not be null. If followSymlinks is + * false, it is assumed that the parent file is canonical + * @param pathname the relative pathname of the file, using {@link File#separatorChar}, must not be + * null + * @param selector the selector used to determine what contents to delete, may be null to delete + * everything + * @param followSymlinks whether to follow symlinks + * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted + * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed + * @return The result of the cleaning, never null + * @throws IOException if a file/directory could not be deleted and failOnError is true */ private Result delete( Path file, @@ -291,13 +291,19 @@ private boolean isSymbolicLink(Path path) throws IOException { || (attrs.isDirectory() && attrs.isOther()); } + /** + * Deletes the specified file or directory. If the path denotes a symlink, only the link is removed. Its target is + * left untouched. + * + * @param file the file/directory to delete, must not be null + * @param failOnError whether to abort with an exception if the file/directory could not be deleted + * @param retryOnError whether to undertake additional delete attempts if the first attempt failed + * @return 0 if the file was deleted, 1 otherwise + * @throws IOException if a file/directory could not be deleted and failOnError is true + */ private int delete(Path file, boolean failOnError, boolean retryOnError) throws IOException { - try { - Files.deleteIfExists(file); - return 0; - } catch (IOException e) { - IOException exception = new IOException("Failed to delete " + file); - exception.addSuppressed(e); + IOException failure = delete(file); + if (failure != null) { if (retryOnError) { if (ON_WINDOWS) { @@ -309,24 +315,22 @@ private int delete(Path file, boolean failOnError, boolean retryOnError) throws for (int delay : delays) { try { Thread.sleep(delay); - } catch (InterruptedException e2) { - exception.addSuppressed(e2); + } catch (InterruptedException e) { + throw new IOException(e); } - try { - Files.deleteIfExists(file); - return 0; - } catch (IOException e2) { - exception.addSuppressed(e2); + failure = delete(file); + if (failure == null) { + break; } } } if (Files.exists(file)) { if (failOnError) { - throw new IOException("Failed to delete " + file, exception); + throw new IOException("Failed to delete " + file, failure); } else { if (logWarn != null) { - logWarn.log("Failed to delete " + file, exception); + logWarn.log("Failed to delete " + file, failure); } return 1; } @@ -336,6 +340,15 @@ private int delete(Path file, boolean failOnError, boolean retryOnError) throws return 0; } + private static IOException delete(Path file) { + try { + Files.deleteIfExists(file); + } catch (IOException e) { + return e; + } + return null; + } + private static class Result { private int failures; @@ -426,7 +439,7 @@ synchronized Path pollNext() { if (basedir == null) { if (cleaner.session != null) { SessionData data = cleaner.session.getData(); - Path lastDir = (Path) data.get(LAST_DIRECTORY_TO_DELETE); + Path lastDir = data.get(LAST_DIRECTORY_TO_DELETE); if (lastDir != null) { data.set(LAST_DIRECTORY_TO_DELETE, null); return lastDir; diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java new file mode 100644 index 00000000..798ee3a5 --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.maven.plugins.clean; + +import java.io.IOException; +import java.nio.file.AccessDeniedException; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; + +import org.apache.maven.api.plugin.Log; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; + +import static java.nio.file.Files.createDirectory; +import static java.nio.file.Files.createFile; +import static java.nio.file.Files.exists; +import static java.nio.file.Files.setPosixFilePermissions; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class CleanerTest { + + private final Log log = mock(Log.class); + + @Test + @DisabledOnOs(OS.WINDOWS) + void deleteSucceedsDeeply(@TempDir Path tempDir) throws Exception { + final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath(); + final Path file = createFile(basedir.resolve("file")); + final Cleaner cleaner = new Cleaner(null, log, false, null, null); + cleaner.delete(basedir, null, false, true, false); + assertFalse(exists(basedir)); + assertFalse(exists(file)); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Exception { + when(log.isWarnEnabled()).thenReturn(true); + final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath(); + createFile(basedir.resolve("file")); + // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException. + final Set permissions = PosixFilePermissions.fromString("rw-rw-r--"); + setPosixFilePermissions(basedir, permissions); + final Cleaner cleaner = new Cleaner(null, log, false, null, null); + final IOException exception = + assertThrows(IOException.class, () -> cleaner.delete(basedir, null, false, true, false)); + verify(log, never()).warn(any(CharSequence.class), any(Throwable.class)); + assertEquals("Failed to delete " + basedir, exception.getMessage()); + final DirectoryNotEmptyException cause = + assertInstanceOf(DirectoryNotEmptyException.class, exception.getCause()); + assertEquals(basedir.toString(), cause.getMessage()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void deleteFailsAfterRetryWhenNoPermission(@TempDir Path tempDir) throws Exception { + final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath(); + createFile(basedir.resolve("file")); + // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException. + final Set permissions = PosixFilePermissions.fromString("rw-rw-r--"); + setPosixFilePermissions(basedir, permissions); + final Cleaner cleaner = new Cleaner(null, log, false, null, null); + final IOException exception = + assertThrows(IOException.class, () -> cleaner.delete(basedir, null, false, true, true)); + assertEquals("Failed to delete " + basedir, exception.getMessage()); + final DirectoryNotEmptyException cause = + assertInstanceOf(DirectoryNotEmptyException.class, exception.getCause()); + assertEquals(basedir.toString(), cause.getMessage()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void deleteLogsWarningWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Exception { + when(log.isWarnEnabled()).thenReturn(true); + final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath(); + final Path file = createFile(basedir.resolve("file")); + // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException. + final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x"); + setPosixFilePermissions(basedir, permissions); + final Cleaner cleaner = new Cleaner(null, log, false, null, null); + assertDoesNotThrow(() -> cleaner.delete(basedir, null, false, false, false)); + verify(log, times(2)).warn(any(CharSequence.class), any(Throwable.class)); + InOrder inOrder = inOrder(log); + ArgumentCaptor cause1 = ArgumentCaptor.forClass(AccessDeniedException.class); + inOrder.verify(log).warn(eq("Failed to delete " + file), cause1.capture()); + assertEquals(file.toString(), cause1.getValue().getMessage()); + ArgumentCaptor cause2 = ArgumentCaptor.forClass(DirectoryNotEmptyException.class); + inOrder.verify(log).warn(eq("Failed to delete " + basedir), cause2.capture()); + assertEquals(basedir.toString(), cause2.getValue().getMessage()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void deleteDoesNotLogAnythingWhenNoPermissionAndWarnDisabled(@TempDir Path tempDir) throws Exception { + when(log.isWarnEnabled()).thenReturn(false); + final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath(); + createFile(basedir.resolve("file")); + // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException. + final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x"); + setPosixFilePermissions(basedir, permissions); + final Cleaner cleaner = new Cleaner(null, log, false, null, null); + assertDoesNotThrow(() -> cleaner.delete(basedir, null, false, false, false)); + verify(log, never()).warn(any(CharSequence.class), any(Throwable.class)); + } +} From 393458a7e1dcc0d51bffb267b533a30788cd3212 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Thu, 30 Jan 2025 19:37:01 +0100 Subject: [PATCH 34/90] Enable GitHub Issues --- .asf.yaml | 2 ++ .github/ISSUE_TEMPLATE/BUG.yml | 48 ++++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/FEATURE.yml | 35 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 30 +++++++++++++++++++ .github/pull_request_template.md | 32 ++++++++------------ README.md | 18 ----------- pom.xml | 4 +-- 7 files changed, 130 insertions(+), 39 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/BUG.yml create mode 100644 .github/ISSUE_TEMPLATE/FEATURE.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.asf.yaml b/.asf.yaml index d9b63d32..9ad5d1e3 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -31,6 +31,8 @@ github: autolink_jira: - MCLEAN del_branch_on_merge: true + features: + issues: true notifications: commits: commits@maven.apache.org diff --git a/.github/ISSUE_TEMPLATE/BUG.yml b/.github/ISSUE_TEMPLATE/BUG.yml new file mode 100644 index 00000000..d49e4ceb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG.yml @@ -0,0 +1,48 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 +# +# http://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. +# + +# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema + +name: Bug Report +description: File a bug report +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report. + + Simple fixes in single PRs do not require issues. + + **Do you use the latest project version?** + + - type: input + id: version + attributes: + label: Affected version + validations: + required: true + + - type: textarea + id: massage + attributes: + label: Bug description + validations: + required: true + + diff --git a/.github/ISSUE_TEMPLATE/FEATURE.yml b/.github/ISSUE_TEMPLATE/FEATURE.yml new file mode 100644 index 00000000..343183d5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE.yml @@ -0,0 +1,35 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 +# +# http://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. +# + +# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema + +name: Feature request +description: File a proposal for new feature, improvement +labels: ["enhancement"] + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this new feature, improvement proposal. + + - type: textarea + id: massage + attributes: + label: New feature, improvement proposal + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..a83ed529 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,30 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 +# +# http://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. +# + +# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser + +blank_issues_enabled: false + +contact_links: + + - name: Project Mailing Lists + url: https://maven.apache.org/mailing-lists.html + about: Please ask a question or discuss here + + - name: Old JIRA Issues + url: https://issues.apache.org/jira/projects/MCLEAN + about: Please search old JIRA issues diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 24cfaca0..bafdc3ad 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,29 +1,23 @@ -Following this checklist to help us incorporate your +Following this checklist to help us incorporate your contribution quickly and easily: - - [ ] Make sure there is a [JIRA issue](https://issues.apache.org/jira/browse/MCLEAN) filed - for the change (usually before you start working on it). Trivial changes like typos do not - require a JIRA issue. Your pull request should address just this issue, without - pulling in other changes. - - [ ] Each commit in the pull request should have a meaningful subject line and body. - - [ ] Format the pull request title like `[MCLEAN-XXX] - Fixes bug in ApproximateQuantiles`, - where you replace `MCLEAN-XXX` with the appropriate JIRA issue. Best practice - is to use the JIRA issue title in the pull request title and in the first line of the - commit message. - - [ ] Write a pull request description that is detailed enough to understand what the pull request does, how, and why. - - [ ] Run `mvn clean verify` to make sure basic checks pass. A more thorough check will - be performed on your pull request automatically. - - [ ] You have run the integration tests successfully (`mvn -Prun-its clean verify`). +- [ ] Your pull request should address just one issue, without pulling in other changes. +- [ ] Write a pull request description that is detailed enough to understand what the pull request does, how, and why. +- [ ] Each commit in the pull request should have a meaningful subject line and body. + Note that commits might be squashed by a maintainer on merge. +- [ ] Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied. + This may not always be possible but is a best-practice. +- [ ] Run `mvn clean verify` to make sure basic checks pass. + A more thorough check will be performed on your pull request automatically. +- [ ] You have run the integration tests successfully (`mvn -Prun-its clean verify`). If your pull request is about ~20 lines of code you don't need to sign an [Individual Contributor License Agreement](https://www.apache.org/licenses/icla.pdf) if you are unsure please ask on the developers list. -To make clear that you license your contribution under +To make clear that you license your contribution under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/LICENSE-2.0) you have to acknowledge this by using the following check-box. - - [ ] I hereby declare this contribution to be licenced under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/LICENSE-2.0) - - - [ ] In any other case, please file an [Apache Individual Contributor License Agreement](https://www.apache.org/licenses/icla.pdf). - +- [ ] I hereby declare this contribution to be licenced under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/LICENSE-2.0) +- [ ] In any other case, please file an [Apache Individual Contributor License Agreement](https://www.apache.org/licenses/icla.pdf). diff --git a/README.md b/README.md index 97d2634a..024dfaa8 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,12 @@ Contributing to [Apache Maven Clean Plugin](https://maven.apache.org/plugins/maven-clean-plugin/) ====================== -[![ASF Jira](https://img.shields.io/endpoint?url=https%3A%2F%2Fmaven.apache.org%2Fbadges%2Fasf_jira-MCLEAN.json)][jira] [![Apache License, Version 2.0, January 2004](https://img.shields.io/github/license/apache/maven.svg?label=License)][license] [![Maven Central](https://img.shields.io/maven-central/v/org.apache.maven.plugins/maven-clean-plugin.svg?label=Maven%20Central)](https://search.maven.org/artifact/org.apache.maven.plugins/maven-clean-plugin) [![Reproducible Builds](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jvm-repo-rebuild/reproducible-central/master/content/org/apache/maven/plugins/maven-clean-plugin/badge.json)](https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/org/apache/maven/plugins/maven-clean-plugin/README.md) [![Jenkins Status](https://img.shields.io/jenkins/s/https/ci-maven.apache.org/job/Maven/job/maven-box/job/maven-clean-plugin/job/master.svg?)][build] [![Jenkins tests](https://img.shields.io/jenkins/t/https/ci-maven.apache.org/job/Maven/job/maven-box/job/maven-clean-plugin/job/master.svg?)][test-results] - You have found a bug or you have an idea for a cool new feature? Contributing code is a great way to give something back to the open source community. Before you dig right into the code, there are a few guidelines that we need @@ -34,7 +32,6 @@ things. Getting Started --------------- -+ Make sure you have a [JIRA account](https://issues.apache.org/jira/). + Make sure you have a [GitHub account](https://github.com/signup/free). + If you're planning to implement a new feature, it makes sense to discuss your changes on the [dev list][ml-list] first. @@ -60,37 +57,22 @@ There are some guidelines which will make applying PRs easier for us: + Create minimal diffs - disable on save actions like reformat source code or organize imports. If you feel the source code should be reformatted, create a separate PR for this change. + Check for unnecessary whitespace with `git diff --check` before committing. -+ Make sure your commit messages are in the proper format. Your commit message should contain the key of the JIRA issue. -``` -[MCLEAN-XXX] - Subject of the JIRA Ticket - Optional supplemental description. -``` + Make sure you have added the necessary tests (JUnit/IT) for your changes. + Run all the tests with `mvn -Prun-its verify` to assure nothing else was accidentally broken. + Submit a pull request to the repository in the Apache organization. -+ Update your JIRA ticket and include a link to the pull request in the ticket. If you plan to contribute on a regular basis, please consider filing a [contributor license agreement][cla]. -Making Trivial Changes ----------------------- - -For changes of a trivial nature to comments and documentation, it is not always -necessary to create a new ticket in JIRA. In this case, it is appropriate to -start the first line of a commit with '(doc)' instead of a ticket number. - Additional Resources -------------------- + [Contributing patches](https://maven.apache.org/guides/development/guide-maven-development.html#Creating_and_submitting_a_patch) -+ [Apache Maven Clean JIRA project page][jira] + [Contributor License Agreement][cla] + [General GitHub documentation](https://help.github.com/) + [GitHub pull request documentation](https://help.github.com/send-pull-requests/) + [Apache Maven Twitter Account](https://twitter.com/ASFMavenProject) + #Maven IRC channel on freenode.org -[jira]: https://issues.apache.org/jira/projects/MCLEAN/ [license]: https://www.apache.org/licenses/LICENSE-2.0 [ml-list]: https://maven.apache.org/mailing-lists.html [code-style]: https://maven.apache.org/developers/conventions/code.html diff --git a/pom.xml b/pom.xml index 3dde41f2..83672f7b 100644 --- a/pom.xml +++ b/pom.xml @@ -46,8 +46,8 @@ under the License. https://github.com/apache/maven-clean-plugin/tree/${project.scm.tag} - JIRA - https://issues.apache.org/jira/browse/MCLEAN + GitHub + https://github.com/apache/maven-clean-plugin/issues Jenkins From b0b6505f17bfa9783f58de6151eb5a6edf051e64 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sun, 9 Feb 2025 11:39:21 +0100 Subject: [PATCH 35/90] Add badge for 3.x version --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 024dfaa8..c3034b4e 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Contributing to [Apache Maven Clean Plugin](https://maven.apache.org/plugins/mav ====================== [![Apache License, Version 2.0, January 2004](https://img.shields.io/github/license/apache/maven.svg?label=License)][license] +[![Maven Central](https://img.shields.io/maven-central/v/org.apache.maven.plugins/maven-clean-plugin.svg?label=Maven%20Central&versionPrefix=3.)](https://search.maven.org/artifact/org.apache.maven.plugins/maven-clean-plugin) [![Maven Central](https://img.shields.io/maven-central/v/org.apache.maven.plugins/maven-clean-plugin.svg?label=Maven%20Central)](https://search.maven.org/artifact/org.apache.maven.plugins/maven-clean-plugin) [![Reproducible Builds](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jvm-repo-rebuild/reproducible-central/master/content/org/apache/maven/plugins/maven-clean-plugin/badge.json)](https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/org/apache/maven/plugins/maven-clean-plugin/README.md) [![Jenkins Status](https://img.shields.io/jenkins/s/https/ci-maven.apache.org/job/Maven/job/maven-box/job/maven-clean-plugin/job/master.svg?)][build] From 68dc77722c2d8e876dd44c0c06dabffa9099e04c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 05:46:39 +0000 Subject: [PATCH 36/90] Bump org.mockito:mockito-core from 4.11.0 to 5.15.2 Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 4.11.0 to 5.15.2. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.11.0...v5.15.2) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 83672f7b..cb88205b 100644 --- a/pom.xml +++ b/pom.xml @@ -142,7 +142,7 @@ under the License. org.mockito mockito-core - 4.11.0 + 5.15.2 test From ccc3f166a2608c704e0ec6c6cf9c5de2de7a60c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCnger?= Date: Tue, 11 Feb 2025 17:24:02 +0100 Subject: [PATCH 37/90] [MNGSITE-529] Rename "Goals" to "Plugin Documentation" --- src/site/site.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/site.xml b/src/site/site.xml index b14b745b..4c033804 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -24,7 +24,7 @@ under the License. - + From 5f7f19525402d97683826581ec299f2545abcefa Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Tue, 11 Feb 2025 17:44:16 +0100 Subject: [PATCH 38/90] PR Automation only on close event --- .github/workflows/pr-automation.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml index e9d66988..53075957 100644 --- a/.github/workflows/pr-automation.yml +++ b/.github/workflows/pr-automation.yml @@ -20,8 +20,6 @@ on: pull_request_target: types: - closed - - unlabeled - - review_requested jobs: pr-automation: From 73cb7471356eec2945d7f131cba0ab967bd5184b Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 11 Feb 2025 18:04:37 +0100 Subject: [PATCH 39/90] Upgrade to Maven 4.0.0-rc-2 (#90) * Upgrade to Maven 4.0.0-rc-2 and maven-plugin-testing-4.0.0-beta-3 * Upgrade maven version used in CI --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index cb88205b..8c9854bc 100644 --- a/pom.xml +++ b/pom.xml @@ -61,13 +61,13 @@ under the License. - 4.0.0-beta-5 + 4.0.0-rc-2 17 33.2.1-jre 6.0.0 2.0.16 3.7.0 - 4.0.0-beta-2 + 4.0.0-beta-3 4.0.0-beta-1 3.21.0 2.0.1 @@ -99,7 +99,7 @@ under the License. org.apache.maven - maven-api-meta + maven-api-annotations ${mavenVersion} provided @@ -123,7 +123,7 @@ under the License. org.apache.maven - maven-api-impl + maven-impl ${mavenVersion} test From 051ab19675f859867a9ccc3307902ab2571c98bf Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Tue, 11 Feb 2025 18:19:13 +0100 Subject: [PATCH 40/90] Simplify GH action --- .github/workflows/maven-verify.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 2bc0ff1e..7a9f071a 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -26,6 +26,5 @@ jobs: name: Verify uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v4 with: - ff-maven: "4.0.0-rc-2" # Maven version for fail-fast-build - maven-matrix: '[ "4.0.0-rc-2" ]' - jdk-matrix: '[ "17", "21" ]' + maven4-build: true + maven4-version: '4.0.0-rc-2' # the same as used in project From 4f0b0905465d358ea6fe93451a36781745efc6d7 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Tue, 11 Feb 2025 18:54:04 +0100 Subject: [PATCH 41/90] Publish documentation to LATEST-4.x --- pom.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8c9854bc..cebe6978 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ under the License. apache.website - scm:svn:https://svn.apache.org/repos/asf/maven/website/components/${maven.site.path} + scm:svn:https://svn.apache.org/repos/asf/maven/website/components/${maven4x.site.path} @@ -72,6 +72,9 @@ under the License. 3.21.0 2.0.1 2024-06-26T08:19:25Z + + + plugins-archives/${project.artifactId}-LATEST-4.x From 189df6694101444001fc18ba6dd0f0be1edc3267 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sat, 15 Feb 2025 09:21:55 +0100 Subject: [PATCH 42/90] Add Stale action --- .github/workflows/stale.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..35fd7939 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,28 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +name: Stale + +on: + schedule: + - cron: '16 4 * * *' + issue_comment: + types: [ 'created' ] + +jobs: + stale: + uses: 'apache/maven-gh-actions-shared/.github/workflows/stale.yml@v4' From 1b80df52038ea84f529a5de53fcb8d0e43c6b2dd Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sat, 15 Feb 2025 11:57:18 +0100 Subject: [PATCH 43/90] Add header to release-drafter about Maven 4 requirements --- .github/release-drafter.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index ef6c4f1d..194be6ac 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -21,3 +21,7 @@ tag-template: maven-clean-plugin-$RESOLVED_VERSION include-pre-releases: true prerelease: true + +header: | + > [!WARNING] + > This plugin is a Maven 4 plugin and requires Maven 4.x to run. From 1eb6ea5986421650cd19c9d6ac0ab5107510f405 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Feb 2025 12:48:09 +0100 Subject: [PATCH 44/90] [MCLEAN-130] Bump com.google.guava:guava from 33.2.1-jre to 33.4.0-jre (#67) Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.2.1-jre to 33.4.0-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cebe6978..d0b03fca 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ under the License. 4.0.0-rc-2 17 - 33.2.1-jre + 33.4.0-jre 6.0.0 2.0.16 3.7.0 From bcad805b60dff1f9f1acc7eea89b003ab08dd482 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sat, 15 Feb 2025 12:53:59 +0100 Subject: [PATCH 45/90] Override maven.site.path property for 4.x publishing As property maven.site.path is used in many other place we have to override it --- pom.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d0b03fca..a3793a66 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ under the License. apache.website - scm:svn:https://svn.apache.org/repos/asf/maven/website/components/${maven4x.site.path} + scm:svn:https://svn.apache.org/repos/asf/maven/website/components/${maven.site.path} @@ -73,8 +73,10 @@ under the License. 2.0.1 2024-06-26T08:19:25Z - + plugins-archives/${project.artifactId}-LATEST-4.x + + ${maven4x.site.path} From eeaa3a16185aca5608e0a5ecbc16096120931b80 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sat, 15 Feb 2025 19:29:14 +0100 Subject: [PATCH 46/90] [maven-release-plugin] prepare release maven-clean-plugin-4.0.0-beta-2 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index a3793a66..1b6c1442 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ under the License. maven-clean-plugin - 4.0.0-beta-2-SNAPSHOT + 4.0.0-beta-2 maven-plugin Apache Maven Clean Plugin @@ -42,7 +42,7 @@ under the License. scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git - HEAD + maven-clean-plugin-4.0.0-beta-2 https://github.com/apache/maven-clean-plugin/tree/${project.scm.tag} @@ -71,7 +71,7 @@ under the License. 4.0.0-beta-1 3.21.0 2.0.1 - 2024-06-26T08:19:25Z + 2025-02-15T18:29:07Z plugins-archives/${project.artifactId}-LATEST-4.x From 4d58bc6bc1b04b573afe2c1e83f96792b50388ff Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sat, 15 Feb 2025 19:29:35 +0100 Subject: [PATCH 47/90] [maven-release-plugin] prepare for next development iteration --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 1b6c1442..784a8392 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ under the License. maven-clean-plugin - 4.0.0-beta-2 + 4.0.0-beta-3-SNAPSHOT maven-plugin Apache Maven Clean Plugin @@ -42,7 +42,7 @@ under the License. scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git - maven-clean-plugin-4.0.0-beta-2 + HEAD https://github.com/apache/maven-clean-plugin/tree/${project.scm.tag} @@ -71,7 +71,7 @@ under the License. 4.0.0-beta-1 3.21.0 2.0.1 - 2025-02-15T18:29:07Z + 2025-02-15T18:29:35Z plugins-archives/${project.artifactId}-LATEST-4.x From 12ac3d395ba6f52d18323701c5b441ec6f3f7969 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Mon, 17 Feb 2025 13:54:52 +0000 Subject: [PATCH 48/90] Delete commented code (#103) --- .../maven/plugins/clean/CleanMojoTest.java | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java index 882a672d..02e8bf6c 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java @@ -86,7 +86,7 @@ public void testCleanNestedStructure(CleanMojo mojo) throws Exception { /** * Tests that no exception is thrown when all internal variables are empty and that it doesn't - * just remove whats there + * just remove what's there * * @throws Exception in case of an error. */ @@ -275,32 +275,6 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { assertFalse(Files.exists(dirWithLnk)); } - // @Provides - // @Singleton - // private Project createProject() { - // ProjectStub project = new ProjectStub(); - // project.setGroupId("myGroupId"); - // return project; - // } - - // @Provides - // @Singleton - // @SuppressWarnings("unused") - // private InternalSession createSession() { - // InternalSession session = SessionStub.getMockSession(LOCAL_REPO); - // Properties props = new Properties(); - // props.put("basedir", MojoExtension.getBasedir()); - // doReturn(props).when(session).getSystemProperties(); - // return session; - // } - - // @Provides - // @Singleton - // @SuppressWarnings("unused") - // private MojoExecution createMojoExecution() { - // return new MojoExecutionStub("default-clean", "clean"); - // } - /** * @param dir a dir or a file * @return true if a file/dir exists, false otherwise From 85f7a3db59b8520bdccb75a9217590d085973563 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Tue, 18 Feb 2025 14:15:23 +0000 Subject: [PATCH 49/90] Remove unused constructor (#106) --- .../java/org/apache/maven/plugins/clean/GlobSelector.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java b/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java index 2d2f99d6..2b851065 100644 --- a/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java +++ b/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java @@ -37,10 +37,6 @@ class GlobSelector implements Selector { private final String str; - GlobSelector(String[] includes, String[] excludes) { - this(includes, excludes, false); - } - GlobSelector(String[] includes, String[] excludes, boolean useDefaultExcludes) { this.str = "includes = " + toString(includes) + ", excludes = " + toString(excludes); this.includes = normalizePatterns(includes); From f52fc9e57b4727bfcf3a9ca52ce83379e1c15c25 Mon Sep 17 00:00:00 2001 From: Peter De Maeyer Date: Wed, 19 Feb 2025 10:04:05 +0100 Subject: [PATCH 50/90] [MCLEAN-110, MCLEAN-124, MCLEAN-126, ] Merge 3.x to 4.x (#86) --- src/it/dangling-symlinks/setup.groovy | 18 +++++++----------- src/it/symlink-dont-follow/setup.groovy | 18 ++++++------------ .../apache/maven/plugins/clean/CleanMojo.java | 6 +++--- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/it/dangling-symlinks/setup.groovy b/src/it/dangling-symlinks/setup.groovy index 81623d14..be18cfd5 100644 --- a/src/it/dangling-symlinks/setup.groovy +++ b/src/it/dangling-symlinks/setup.groovy @@ -17,28 +17,24 @@ * under the License. */ -import java.io.*; import java.nio.file.*; -import java.util.*; -import java.util.jar.*; -import java.util.regex.*; -import org.apache.maven.plugins.clean.*; +import java.nio.file.attribute.*; try { - File targetDir = new File( basedir, "target" ); - File link = new File( targetDir, "link" ); - File target = new File( targetDir, "link-target.txt" ); + Path targetDir = basedir.toPath().resolve( "target" ); + Path link = targetDir.resolve( "link" ); + Path target = targetDir.resolve( "link-target.txt" ); System.out.println( "Creating symlink " + link + " -> " + target ); - Files.createSymbolicLink( link.toPath(), target.toPath() ); - if ( !link.exists() ) + Files.createSymbolicLink( link, target, new FileAttribute[0] ); + if ( !Files.exists( link, new LinkOption[0] ) ) { System.out.println( "Platform does not support symlinks, skipping test." ); } System.out.println( "Deleting symlink target " + target ); - target.delete(); + Files.delete( target ); } catch( Exception ex ) { diff --git a/src/it/symlink-dont-follow/setup.groovy b/src/it/symlink-dont-follow/setup.groovy index 7ae9bc55..37fac7e6 100644 --- a/src/it/symlink-dont-follow/setup.groovy +++ b/src/it/symlink-dont-follow/setup.groovy @@ -17,12 +17,8 @@ * under the License. */ -import java.io.*; import java.nio.file.*; -import java.util.*; -import java.util.jar.*; -import java.util.regex.*; -import org.apache.maven.plugins.clean.*; +import java.nio.file.attribute.*; def pairs = [ @@ -34,13 +30,11 @@ def pairs = for ( pair : pairs ) { - File target = new File( basedir, pair[0] ); - File link = new File( basedir, pair[1] ); - println "Creating symlink " + link + " -> " + target; - Path targetPath = target.toPath(); - Path linkPath = link.toPath(); - Files.createSymbolicLink( linkPath, targetPath ); - if ( !link.exists() ) + Path target = basedir.toPath().resolve( pair[0] ); + Path link = basedir.toPath().resolve( pair[1] ); + System.out.println( "Creating symlink " + link + " -> " + target ); + Files.createSymbolicLink( link, target, new FileAttribute[0] ); + if ( !Files.exists( link, new LinkOption[0] ) ) { println "Platform does not support symlinks, skipping test."; return; diff --git a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java index 44ceb480..e3a20048 100644 --- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java +++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java @@ -188,7 +188,7 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { * ${maven.multiModuleProjectDirectory}/target/.clean directory will be used. If the * ${build.directory} has been modified, you'll have to adjust this property explicitly. * In order for fast clean to work correctly, this directory and the various directories that will be deleted - * should usually reside on the same volume. The exact conditions are system dependant though, but if an atomic + * should usually reside on the same volume. The exact conditions are system-dependent though, but if an atomic * move is not supported, the standard deletion mechanism will be used. * * @since 3.2 @@ -201,8 +201,8 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { * Mode to use when using fast clean. Values are: background to start deletion immediately and * waiting for all files to be deleted when the session ends, at-end to indicate that the actual * deletion should be performed synchronously when the session ends, or defer to specify that - * the actual file deletion should be started in the background when the session ends (this should only be used - * when maven is embedded in a long running process). + * the actual file deletion should be started in the background when the session ends. This should only be used + * when maven is embedded in a long-running process. * * @since 3.2 * @see #fast From 153344705e919580b7f891510488bb2098445072 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Wed, 26 Feb 2025 13:13:23 +0100 Subject: [PATCH 51/90] Temporarily disable issue notifications for jira-to-github migration (#108) --- .asf.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.asf.yaml b/.asf.yaml index 9ad5d1e3..3f844bc0 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -36,6 +36,6 @@ github: notifications: commits: commits@maven.apache.org - issues: issues@maven.apache.org + #issues: issues@maven.apache.org pullrequests: issues@maven.apache.org jira_options: link label From 15dde124d608a614b52c735b1a6d2067c7b122cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:38:51 +0100 Subject: [PATCH 52/90] Bump org.mockito:mockito-core from 5.15.2 to 5.16.0 Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.15.2 to 5.16.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.15.2...v5.16.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 784a8392..62057651 100644 --- a/pom.xml +++ b/pom.xml @@ -147,7 +147,7 @@ under the License. org.mockito mockito-core - 5.15.2 + 5.16.0 test From b1def686b96ee70720e2ecf7f84d94bd7b88a19f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:46:18 +0100 Subject: [PATCH 53/90] Bump org.slf4j:slf4j-simple from 2.0.16 to 2.0.17 Bumps org.slf4j:slf4j-simple from 2.0.16 to 2.0.17. --- updated-dependencies: - dependency-name: org.slf4j:slf4j-simple dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 62057651..81ca3790 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ under the License. 17 33.4.0-jre 6.0.0 - 2.0.16 + 2.0.17 3.7.0 4.0.0-beta-3 4.0.0-beta-1 From d3f3554dbf8093ee00d4695660f09163f514c6fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 18:08:55 +0100 Subject: [PATCH 54/90] Bump org.mockito:mockito-core from 5.16.0 to 5.16.1 Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.16.0 to 5.16.1. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.16.0...v5.16.1) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 81ca3790..a07021be 100644 --- a/pom.xml +++ b/pom.xml @@ -147,7 +147,7 @@ under the License. org.mockito mockito-core - 5.16.0 + 5.16.1 test From 554aaa8d419007d71de69d2b4fc075d42938ee63 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Fri, 21 Mar 2025 20:19:51 +0100 Subject: [PATCH 55/90] Re-enable issue notifications (#232) --- .asf.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.asf.yaml b/.asf.yaml index 3f844bc0..9ad5d1e3 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -36,6 +36,6 @@ github: notifications: commits: commits@maven.apache.org - #issues: issues@maven.apache.org + issues: issues@maven.apache.org pullrequests: issues@maven.apache.org jira_options: link label From 4533b548da30c2a83e49f9b627bce26899ce7979 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 06:35:50 +0100 Subject: [PATCH 56/90] Bump com.google.guava:guava from 33.4.0-jre to 33.4.6-jre Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.4.0-jre to 33.4.6-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a07021be..cfa4b17c 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ under the License. 4.0.0-rc-2 17 - 33.4.0-jre + 33.4.6-jre 6.0.0 2.0.17 3.7.0 From b99c9db3ab2d85eda2a3ffd4601be8752612ca64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 05:59:00 +0000 Subject: [PATCH 57/90] Bump org.apache.maven.plugins:maven-plugins from 43 to 44 Bumps [org.apache.maven.plugins:maven-plugins](https://github.com/apache/maven-parent) from 43 to 44. - [Release notes](https://github.com/apache/maven-parent/releases) - [Commits](https://github.com/apache/maven-parent/commits/v44) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-plugins dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cfa4b17c..47c7c904 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.maven.plugins maven-plugins - 43 + 44 From ecbd5db22d1ba50d14ce70d63dfaada770e78ae3 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sun, 30 Mar 2025 11:50:16 +0200 Subject: [PATCH 58/90] Remove an unused dependency, add missing `@Override` annotations, avoid an usage of `/` path separator. --- pom.xml | 10 ---------- .../java/org/apache/maven/plugins/clean/CleanMojo.java | 4 ++-- .../java/org/apache/maven/plugins/clean/Cleaner.java | 1 + .../java/org/apache/maven/plugins/clean/Fileset.java | 1 + .../org/apache/maven/plugins/clean/GlobSelector.java | 3 +++ 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index 47c7c904..0c465d1a 100644 --- a/pom.xml +++ b/pom.xml @@ -79,16 +79,6 @@ under the License. ${maven4x.site.path} - - - - com.google.guava - guava - ${guavaVersion} - - - - org.apache.maven diff --git a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java index e3a20048..67d3fe71 100644 --- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java +++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.nio.file.Path; -import java.nio.file.Paths; import org.apache.maven.api.Session; import org.apache.maven.api.di.Inject; @@ -220,6 +219,7 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { * @throws MojoException When a directory failed to get deleted. * @see org.apache.maven.api.plugin.Mojo#execute() */ + @Override public void execute() { if (skip) { getLog().info("Clean is skipped."); @@ -232,7 +232,7 @@ public void execute() { if (fast && this.fastDir != null) { fastDir = this.fastDir; } else if (fast && multiModuleProjectDirectory != null) { - fastDir = Paths.get(multiModuleProjectDirectory, "target/.clean"); + fastDir = Path.of(multiModuleProjectDirectory, "target", ".clean"); } else { fastDir = null; if (fast) { diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java index 70d6832a..f19a9b50 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -407,6 +407,7 @@ private BackgroundCleaner(Cleaner cleaner, Path dir, String fastMode) { init(cleaner.fastDir, dir); } + @Override public void run() { while (true) { Path basedir = pollNext(); diff --git a/src/main/java/org/apache/maven/plugins/clean/Fileset.java b/src/main/java/org/apache/maven/plugins/clean/Fileset.java index 75084115..90d15c53 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Fileset.java +++ b/src/main/java/org/apache/maven/plugins/clean/Fileset.java @@ -87,6 +87,7 @@ public boolean isUseDefaultExcludes() { * [included files], excluded: [excluded files])" * @see java.lang.Object#toString() */ + @Override public String toString() { return "file set: " + getDirectory() + " (included: " + Arrays.asList(getIncludes()) + ", excluded: " + Arrays.asList(getExcludes()) + ")"; diff --git a/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java b/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java index 2b851065..3d61071f 100644 --- a/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java +++ b/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java @@ -90,6 +90,7 @@ private static String normalizePattern(String pattern) { return normalized; } + @Override public boolean isSelected(String pathname) { return (includes.length <= 0 || isMatched(pathname, includes)) && (excludes.length <= 0 || !isMatched(pathname, excludes)); @@ -104,6 +105,7 @@ private static boolean isMatched(String pathname, String[] patterns) { return false; } + @Override public boolean couldHoldSelected(String pathname) { for (String include : includes) { if (SelectorUtils.matchPatternStart(include, pathname)) { @@ -113,6 +115,7 @@ public boolean couldHoldSelected(String pathname) { return includes.length <= 0; } + @Override public String toString() { return str; } From 39b9ee1fb8429f672d4d53ef7e1e20a0b9299c21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:13:29 +0200 Subject: [PATCH 59/90] Bump mavenVersion from 4.0.0-rc-2 to 4.0.0-rc-3 (#234) * Bump mavenVersion from 4.0.0-rc-2 to 4.0.0-rc-3 Bumps `mavenVersion` from 4.0.0-rc-2 to 4.0.0-rc-3. Updates `org.apache.maven:maven-api-core` from 4.0.0-rc-2 to 4.0.0-rc-3 - [Release notes](https://github.com/apache/maven/releases) - [Commits](https://github.com/apache/maven/compare/maven-4.0.0-rc-2...maven-4.0.0-rc-3) Updates `org.apache.maven:maven-api-di` from 4.0.0-rc-2 to 4.0.0-rc-3 - [Release notes](https://github.com/apache/maven/releases) - [Commits](https://github.com/apache/maven/compare/maven-4.0.0-rc-2...maven-4.0.0-rc-3) Updates `org.apache.maven:maven-api-annotations` from 4.0.0-rc-2 to 4.0.0-rc-3 - [Release notes](https://github.com/apache/maven/releases) - [Commits](https://github.com/apache/maven/compare/maven-4.0.0-rc-2...maven-4.0.0-rc-3) Updates `org.apache.maven:maven-impl` from 4.0.0-rc-2 to 4.0.0-rc-3 - [Release notes](https://github.com/apache/maven/releases) - [Commits](https://github.com/apache/maven/compare/maven-4.0.0-rc-2...maven-4.0.0-rc-3) Updates `org.apache.maven:maven-core` from 4.0.0-rc-2 to 4.0.0-rc-3 - [Release notes](https://github.com/apache/maven/releases) - [Commits](https://github.com/apache/maven/compare/maven-4.0.0-rc-2...maven-4.0.0-rc-3) --- updated-dependencies: - dependency-name: org.apache.maven:maven-api-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.maven:maven-api-di dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.maven:maven-api-annotations dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.maven:maven-impl dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: org.apache.maven:maven-core dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump org.apache.maven.plugin-testing:maven-plugin-testing-harness Bumps [org.apache.maven.plugin-testing:maven-plugin-testing-harness](https://github.com/apache/maven-plugin-testing) from 4.0.0-beta-3 to 4.0.0-beta-4. - [Release notes](https://github.com/apache/maven-plugin-testing/releases) - [Commits](https://github.com/apache/maven-plugin-testing/compare/maven-plugin-testing-4.0.0-beta-3...v4.0.0-beta-4) --- updated-dependencies: - dependency-name: org.apache.maven.plugin-testing:maven-plugin-testing-harness dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Build and test with Maven 4.0.0-rc-3 too --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guillaume Nodet --- .github/workflows/maven-verify.yml | 2 +- pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 7a9f071a..a6464063 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -27,4 +27,4 @@ jobs: uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v4 with: maven4-build: true - maven4-version: '4.0.0-rc-2' # the same as used in project + maven4-version: '4.0.0-rc-3' # the same as used in project diff --git a/pom.xml b/pom.xml index 0c465d1a..aa6bda38 100644 --- a/pom.xml +++ b/pom.xml @@ -61,13 +61,13 @@ under the License. - 4.0.0-rc-2 + 4.0.0-rc-3 17 33.4.6-jre 6.0.0 2.0.17 3.7.0 - 4.0.0-beta-3 + 4.0.0-beta-4 4.0.0-beta-1 3.21.0 2.0.1 From de0af5283a04da2fa69a4698681a4574084ac1de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 06:13:21 +0000 Subject: [PATCH 60/90] Bump org.mockito:mockito-core from 5.16.1 to 5.17.0 Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.16.1 to 5.17.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.16.1...v5.17.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.17.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aa6bda38..5b024dff 100644 --- a/pom.xml +++ b/pom.xml @@ -137,7 +137,7 @@ under the License. org.mockito mockito-core - 5.16.1 + 5.17.0 test From ea9325b2cfc119a88ac5957fe29cd806d4695e5e Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Mon, 28 Apr 2025 21:21:06 +0200 Subject: [PATCH 61/90] Update release-drafter, README and PR template after parent 44 --- .github/pull_request_template.md | 4 ++-- .github/release-drafter-3.x.yml | 2 -- .github/release-drafter.yml | 2 -- README.md | 5 +++-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index bafdc3ad..0456e309 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,9 +7,9 @@ contribution quickly and easily: Note that commits might be squashed by a maintainer on merge. - [ ] Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied. This may not always be possible but is a best-practice. -- [ ] Run `mvn clean verify` to make sure basic checks pass. +- [ ] Run `mvn verify` to make sure basic checks pass. A more thorough check will be performed on your pull request automatically. -- [ ] You have run the integration tests successfully (`mvn -Prun-its clean verify`). +- [ ] You have run the integration tests successfully (`mvn -Prun-its verify`). If your pull request is about ~20 lines of code you don't need to sign an [Individual Contributor License Agreement](https://www.apache.org/licenses/icla.pdf) if you are unsure diff --git a/.github/release-drafter-3.x.yml b/.github/release-drafter-3.x.yml index ec320ddc..7438f111 100644 --- a/.github/release-drafter-3.x.yml +++ b/.github/release-drafter-3.x.yml @@ -16,5 +16,3 @@ # under the License. _extends: maven-gh-actions-shared:.github/release-drafter.yml - -tag-template: maven-clean-plugin-$RESOLVED_VERSION diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 194be6ac..6d3b73cf 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -17,8 +17,6 @@ _extends: maven-gh-actions-shared -tag-template: maven-clean-plugin-$RESOLVED_VERSION - include-pre-releases: true prerelease: true diff --git a/README.md b/README.md index c3034b4e..3dc38fa8 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,9 @@ Additional Resources + [Contributor License Agreement][cla] + [General GitHub documentation](https://help.github.com/) + [GitHub pull request documentation](https://help.github.com/send-pull-requests/) -+ [Apache Maven Twitter Account](https://twitter.com/ASFMavenProject) -+ #Maven IRC channel on freenode.org ++ [Apache Maven X Account](https://x.com/ASFMavenProject) ++ [Apache Maven Bluesky Account](https://bsky.app/profile/maven.apache.org) ++ [Apache Maven Mastodon Account](https://mastodon.social/deck/@ASFMavenProject@fosstodon.org) [license]: https://www.apache.org/licenses/LICENSE-2.0 [ml-list]: https://maven.apache.org/mailing-lists.html From e734c70d14f6127b816acb4397d8fc54cd7b5f07 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Mon, 28 Apr 2025 21:25:15 +0200 Subject: [PATCH 62/90] Restore jenkins build --- Jenkinsfile.disabled => Jenkinsfile | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Jenkinsfile.disabled => Jenkinsfile (100%) diff --git a/Jenkinsfile.disabled b/Jenkinsfile similarity index 100% rename from Jenkinsfile.disabled rename to Jenkinsfile From 1620e1ed57260e84f7af76db07b29853bb6ccf8a Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sat, 3 May 2025 23:38:30 +0200 Subject: [PATCH 63/90] Replace Plexus dependency by more extensive usage of `java.nio` (#243) Remove the dependency to Plexus, replaced by more reliance on `java.nio`. This commit contains the following work items: * Replacement of Plexus includes/excludes filters by `java.nio.file.PathMatcher`. * Use `java.nio.file.FileVisitor` for walking over files and directory trees. * Changes in some logging messages and exceptions. * The `IOException` throws by Java is no longer wrapped in another `IOException`. Pull request: https://github.com/apache/maven-clean-plugin/pull/243 --- pom.xml | 5 - .../apache/maven/plugins/clean/CleanMojo.java | 33 +- .../apache/maven/plugins/clean/Cleaner.java | 493 ++++++++++-------- .../apache/maven/plugins/clean/Fileset.java | 61 ++- .../maven/plugins/clean/GlobSelector.java | 122 ----- .../apache/maven/plugins/clean/Selector.java | 397 +++++++++++++- .../maven/plugins/clean/CleanMojoTest.java | 16 +- .../maven/plugins/clean/CleanerTest.java | 43 +- 8 files changed, 749 insertions(+), 421 deletions(-) delete mode 100644 src/main/java/org/apache/maven/plugins/clean/GlobSelector.java diff --git a/pom.xml b/pom.xml index 5b024dff..06c45def 100644 --- a/pom.xml +++ b/pom.xml @@ -99,11 +99,6 @@ under the License. provided - - org.codehaus.plexus - plexus-utils - - org.apache.maven.plugin-testing diff --git a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java index 67d3fe71..7f60747b 100644 --- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java +++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java @@ -222,7 +222,7 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { @Override public void execute() { if (skip) { - getLog().info("Clean is skipped."); + logger.info("Clean is skipped."); return; } @@ -236,7 +236,7 @@ public void execute() { } else { fastDir = null; if (fast) { - getLog().warn("Fast clean requires maven 3.3.1 or newer, " + logger.warn("Fast clean requires maven 3.3.1 or newer, " + "or an explicit directory to be specified with the 'fastDir' configuration of " + "this plugin, or the 'maven.clean.fastDir' user property to be set."); } @@ -248,37 +248,22 @@ public void execute() { throw new IllegalArgumentException("Illegal value '" + fastMode + "' for fastMode. Allowed values are '" + FAST_MODE_BACKGROUND + "', '" + FAST_MODE_AT_END + "' and '" + FAST_MODE_DEFER + "'."); } - - Cleaner cleaner = new Cleaner(session, getLog(), isVerbose(), fastDir, fastMode); - + final var cleaner = + new Cleaner(session, logger, isVerbose(), fastDir, fastMode, followSymLinks, failOnError, retryOnError); try { for (Path directoryItem : getDirectories()) { if (directoryItem != null) { - cleaner.delete(directoryItem, null, followSymLinks, failOnError, retryOnError); + cleaner.delete(directoryItem); } } - if (filesets != null) { for (Fileset fileset : filesets) { if (fileset.getDirectory() == null) { throw new MojoException("Missing base directory for " + fileset); } - final String[] includes = fileset.getIncludes(); - final String[] excludes = fileset.getExcludes(); - final boolean useDefaultExcludes = fileset.isUseDefaultExcludes(); - final GlobSelector selector; - if ((includes != null && includes.length != 0) - || (excludes != null && excludes.length != 0) - || useDefaultExcludes) { - selector = new GlobSelector(includes, excludes, useDefaultExcludes); - } else { - selector = null; - } - cleaner.delete( - fileset.getDirectory(), selector, fileset.isFollowSymlinks(), failOnError, retryOnError); + cleaner.delete(fileset); } } - } catch (IOException e) { throw new MojoException("Failed to clean project: " + e.getMessage(), e); } @@ -290,7 +275,7 @@ public void execute() { * @return true if verbose output is enabled, false otherwise. */ private boolean isVerbose() { - return (verbose != null) ? verbose : getLog().isDebugEnabled(); + return (verbose != null) ? verbose : logger.isDebugEnabled(); } /** @@ -307,8 +292,4 @@ private Path[] getDirectories() { } return directories; } - - private Log getLog() { - return logger; - } } diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java index f19a9b50..0a399ac2 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -20,15 +20,17 @@ import java.io.File; import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; import java.nio.file.Files; -import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayDeque; +import java.util.BitSet; import java.util.Deque; -import java.util.function.BiConsumer; -import java.util.function.Consumer; +import java.util.EnumSet; import java.util.stream.Stream; import org.apache.maven.api.Event; @@ -36,121 +38,175 @@ import org.apache.maven.api.Listener; import org.apache.maven.api.Session; import org.apache.maven.api.SessionData; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.plugin.Log; -import org.codehaus.plexus.util.Os; - -import static org.apache.maven.plugins.clean.CleanMojo.FAST_MODE_BACKGROUND; -import static org.apache.maven.plugins.clean.CleanMojo.FAST_MODE_DEFER; /** * Cleans directories. * * @author Benjamin Bentmann + * @author Martin Desruisseaux */ -class Cleaner { - - private static final boolean ON_WINDOWS = Os.isFamily(Os.FAMILY_WINDOWS); +final class Cleaner implements FileVisitor { + /** + * Whether the host operating system is from the Windows family. + */ + private static final boolean ON_WINDOWS = (File.separatorChar == '\\'); private static final SessionData.Key LAST_DIRECTORY_TO_DELETE = SessionData.key(Path.class, Cleaner.class.getName() + ".lastDirectoryToDelete"); /** - * The maven session. This is typically non-null in a real run, but it can be during unit tests. + * The maven session. This is typically non-null in a real run, but it can be during unit tests. */ + @Nonnull private final Session session; - private final Logger logDebug; - - private final Logger logInfo; + /** + * The logger where to send information or warning messages. + */ + @Nonnull + private final Log logger; - private final Logger logVerbose; + /** + * Whether to send to the logger some information that would normally be at the "debug" level. + * Those information include the files or directories that are deleted. + */ + private final boolean verbose; - private final Logger logWarn; + /** + * Whether to list the files or directories that are deleted. This is a combination of {@link #verbose} + * with {@link #logger} configuration, and is stored because frequently requested. + */ + private final boolean listDeletedFiles; + @Nonnull private final Path fastDir; + @Nonnull private final String fastMode; + @Nullable + private Selector selector; + + /** + * Whether the base directory is excluded from the set of directories to delete. + * This is usually {@code false}, unless explicitly excluded by specifying an + * empty string in the excludes. + */ + private boolean isBaseDirectoryExcluded; + + private boolean followSymlinks; + + private final boolean failOnError; + + private final boolean retryOnError; + + /** + * Number of files that we failed to delete. + * This is incremented only if {@link #failOnError} is {@code false}, otherwise exceptions are thrown. + */ + private int failureCount; + + /** + * Whether each directory level contains at least one excluded file. + * This is used for determining whether the directory can be deleted. + * A bit is used for each directory level, with {@link #currentDepth} + * telling which bit is for the current directory. + */ + private final BitSet nonEmptyDirectoryLevels; + + /** + * 0 for the base directory, and incremented for each subdirectory. + */ + private int currentDepth; + /** * Creates a new cleaner. * * @param session the Maven session to be used - * @param log the logger to use, may be null to disable logging + * @param logger the logger to use * @param verbose whether to perform verbose logging * @param fastDir the explicit configured directory or to be deleted in fast mode * @param fastMode the fast deletion mode + * @param followSymlinks whether to follow symlinks + * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted + * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed */ - Cleaner(Session session, Log log, boolean verbose, Path fastDir, String fastMode) { - logDebug = (log == null || !log.isDebugEnabled()) ? null : logger(log::debug, log::debug); - - logInfo = (log == null || !log.isInfoEnabled()) ? null : logger(log::info, log::info); - - logWarn = (log == null || !log.isWarnEnabled()) ? null : logger(log::warn, log::warn); - - logVerbose = verbose ? logInfo : logDebug; - + @SuppressWarnings("checkstyle:ParameterNumber") + Cleaner( + @Nonnull Session session, + @Nonnull Log logger, + boolean verbose, + @Nonnull Path fastDir, + @Nonnull String fastMode, + boolean followSymlinks, + boolean failOnError, + boolean retryOnError) { this.session = session; + this.logger = logger; + this.verbose = verbose; this.fastDir = fastDir; this.fastMode = fastMode; + this.followSymlinks = followSymlinks; + this.failOnError = failOnError; + this.retryOnError = retryOnError; + listDeletedFiles = verbose ? logger.isInfoEnabled() : logger.isDebugEnabled(); + nonEmptyDirectoryLevels = new BitSet(); } - private Logger logger(Consumer l1, BiConsumer l2) { - return new Logger() { - @Override - public void log(CharSequence message) { - l1.accept(message); - } - - @Override - public void log(CharSequence message, Throwable t) { - l2.accept(message, t); - } - }; + /** + * Deletes the specified fileset. + * + * @param fileset the fileset to delete, must not be {@code null} + * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true} + */ + public void delete(@Nonnull Fileset fileset) throws IOException { + selector = new Selector(fileset); + if (selector.isEmpty()) { + selector = null; + } + isBaseDirectoryExcluded = fileset.isBaseDirectoryExcluded(); + followSymlinks = fileset.isFollowSymlinks(); + delete(fileset.getDirectory()); } /** - * Deletes the specified directories and its contents. + * Deletes the specified directory and its contents. + * Non-existing directories will be silently ignored. * - * @param basedir the directory to delete, must not be null. Non-existing directories will be silently - * ignored - * @param selector the selector used to determine what contents to delete, may be null to delete - * everything - * @param followSymlinks whether to follow symlinks - * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted - * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed - * @throws IOException if a file/directory could not be deleted and failOnError is true + * @param basedir the directory to delete, must not be {@code null} + * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true} */ - public void delete( - Path basedir, Selector selector, boolean followSymlinks, boolean failOnError, boolean retryOnError) - throws IOException { + public void delete(@Nonnull Path basedir) throws IOException { if (!Files.isDirectory(basedir)) { - if (!Files.exists(basedir)) { - if (logDebug != null) { - logDebug.log("Skipping non-existing directory " + basedir); + if (Files.notExists(basedir)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping non-existing directory " + basedir); } return; } throw new IOException("Invalid base directory " + basedir); } - - if (logInfo != null) { - logInfo.log("Deleting " + basedir + (selector != null ? " (" + selector + ")" : "")); + if (logger.isInfoEnabled()) { + logger.info("Deleting " + basedir + (selector != null ? " (" + selector + ")" : "")); + } + var options = EnumSet.noneOf(FileVisitOption.class); + if (followSymlinks) { + options.add(FileVisitOption.FOLLOW_LINKS); + basedir = getCanonicalPath(basedir, null); } - - Path file = followSymlinks ? basedir : getCanonicalPath(basedir); - if (selector == null && !followSymlinks && fastDir != null && session != null) { // If anything wrong happens, we'll just use the usual deletion mechanism - if (fastDelete(file)) { + if (fastDelete(basedir)) { return; } } - - delete(file, "", selector, followSymlinks, failOnError, retryOnError); + Files.walkFileTree(basedir, options, Integer.MAX_VALUE, this); } private boolean fastDelete(Path baseDir) { - Path fastDir = this.fastDir; // Handle the case where we use ${maven.multiModuleProjectDirectory}/target/.clean for example if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) { try { @@ -167,9 +223,7 @@ private boolean fastDelete(Path baseDir) { throw e; } } catch (IOException e) { - if (logDebug != null) { - logDebug.log("Unable to fast delete directory", e); - } + logger.debug("Unable to fast delete directory", e); return false; } } @@ -179,15 +233,12 @@ private boolean fastDelete(Path baseDir) { Files.createDirectories(fastDir); } } catch (IOException e) { - if (logDebug != null) { - logDebug.log( - "Unable to fast delete directory as the path " + fastDir - + " does not point to a directory or cannot be created", - e); - } + logger.debug( + "Unable to fast delete directory as the path " + fastDir + + " does not point to a directory or cannot be created", + e); return false; } - try { Path tmpDir = Files.createTempDirectory(fastDir, ""); Path dstDir = tmpDir.resolve(baseDir.getFileName()); @@ -199,175 +250,198 @@ private boolean fastDelete(Path baseDir) { BackgroundCleaner.delete(this, tmpDir, fastMode); return true; } catch (IOException e) { - if (logDebug != null) { - logDebug.log("Unable to fast delete directory", e); - } + logger.debug("Unable to fast delete directory", e); return false; } } /** - * Deletes the specified file or directory. - * - * @param file the file/directory to delete, must not be null. If followSymlinks is - * false, it is assumed that the parent file is canonical - * @param pathname the relative pathname of the file, using {@link File#separatorChar}, must not be - * null - * @param selector the selector used to determine what contents to delete, may be null to delete - * everything - * @param followSymlinks whether to follow symlinks - * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted - * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed - * @return The result of the cleaning, never null - * @throws IOException if a file/directory could not be deleted and failOnError is true + * Invoked for a directory before entries in the directory are visited. + * Determines if the given directory should be scanned for files to delete. */ - private Result delete( - Path file, - String pathname, - Selector selector, - boolean followSymlinks, - boolean failOnError, - boolean retryOnError) - throws IOException { - Result result = new Result(); - - boolean isDirectory = Files.isDirectory(file); - - if (isDirectory) { - if (selector == null || selector.couldHoldSelected(pathname)) { - final boolean isSymlink = isSymbolicLink(file); - Path canonical = followSymlinks ? file : getCanonicalPath(file); - if (followSymlinks || !isSymlink) { - String prefix = !pathname.isEmpty() ? pathname + File.separatorChar : ""; - try (Stream children = Files.list(canonical)) { - for (Path child : children.toList()) { - result.update(delete( - child, - prefix + child.getFileName(), - selector, - followSymlinks, - failOnError, - retryOnError)); - } - } - } else if (logDebug != null) { - logDebug.log("Not recursing into symlink " + file); - } - } else if (logDebug != null) { - logDebug.log("Not recursing into directory without included files " + file); - } + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (ON_WINDOWS && !followSymlinks && attrs.isOther()) { + /* + * MCLEAN-93: NTFS junctions have isDirectory() and isOther() attributes set. + * If not following symbolic links, then it should be handled as a file. + */ + visitFile(dir, attrs); + return FileVisitResult.SKIP_SUBTREE; + } + if (selector == null || selector.couldHoldSelected(dir)) { + nonEmptyDirectoryLevels.clear(++currentDepth); + return FileVisitResult.CONTINUE; + } else { + nonEmptyDirectoryLevels.set(currentDepth); // Remember that this directory is not empty. + logger.debug("Not recursing into directory without included files " + dir); + return FileVisitResult.SKIP_SUBTREE; } + } - if (!result.excluded && (selector == null || selector.isSelected(pathname))) { - if (logVerbose != null) { - if (isDirectory) { - logVerbose.log("Deleting directory " + file); - } else if (Files.exists(file)) { - logVerbose.log("Deleting file " + file); - } else { - logVerbose.log("Deleting dangling symlink " + file); - } + /** + * Invoked for a file in a directory. + * Deletes that file, unless the file is excluded by the selector. + */ + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if ((selector == null || selector.matches(file)) && tryDelete(file)) { + if (listDeletedFiles) { + logDelete(file, attrs); } - result.failures += delete(file, failOnError, retryOnError); } else { - result.excluded = true; + nonEmptyDirectoryLevels.set(currentDepth); // Remember that the directory will not be empty. } + return FileVisitResult.CONTINUE; + } - return result; + /** + * Invoked for a file that could not be visited. + */ + @Override + public FileVisitResult visitFileFailed(Path file, IOException failure) throws IOException { + if (logger.isWarnEnabled()) { + logger.warn("Failed to visit " + file, failure); + } + failureCount++; + nonEmptyDirectoryLevels.set(currentDepth); // Remember that the directory will not be empty. + if (failOnError) { + throw failure; + } + return FileVisitResult.CONTINUE; } - private static Path getCanonicalPath(Path path) { + /** + * Invoked for a directory after all files and sub-trees have been visited. + * If the directory is not empty, then this method deletes it. + */ + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException failure) throws IOException { + boolean canDelete = !nonEmptyDirectoryLevels.get(currentDepth--); // False if the directory is not empty. + if (failure != null) { + if (logger.isWarnEnabled()) { + logger.warn("Error in directory " + dir, failure); + } + failureCount++; + canDelete = false; + } else { + canDelete &= (currentDepth != 0 || !isBaseDirectoryExcluded); + if (canDelete && selector != null) { + canDelete = selector.matches(dir); + } + } + if (canDelete) { + if (tryDelete(dir) && listDeletedFiles) { + logDelete(dir, null); + } + } else { + nonEmptyDirectoryLevels.set(currentDepth); // Remember that the parent of `dir` is not empty. + } + if (failure != null && failOnError) { + throw failure; + } + return FileVisitResult.CONTINUE; + } + + /** + * Returns the real path of the given file. If the real path cannot be obtained, + * this method tries to get the real path of the parent and to append the rest of + * the filename. + * + * @param path the path to get as a canonical path + * @param mainError should be {@code null} (reserved to recursive calls of this method) + * @return the real path of the given path + * @throws IOException if the canonical path cannot be obtained + */ + private static Path getCanonicalPath(final Path path, IOException mainError) throws IOException { try { return path.toRealPath(); } catch (IOException e) { - return getCanonicalPath(path.getParent()).resolve(path.getFileName()); + if (mainError == null) { + mainError = e; + } else { + mainError.addSuppressed(e); + } + final Path parent = path.getParent(); + if (parent != null) { + return getCanonicalPath(parent, mainError).resolve(path.getFileName()); + } + throw e; } } - private boolean isSymbolicLink(Path path) throws IOException { - BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); - return attrs.isSymbolicLink() - // MCLEAN-93: NTFS junctions have isDirectory() and isOther() attributes set - || (attrs.isDirectory() && attrs.isOther()); - } - /** - * Deletes the specified file or directory. If the path denotes a symlink, only the link is removed. Its target is - * left untouched. + * Deletes the specified file or directory. + * If the path denotes a symlink, only the link is removed. Its target is left untouched. + * This method returns {@code true} if the file has been deleted, or {@code false} if the + * file does not exist or if an {@link IOException} occurred but {@link #failOnError} is + * {@code false}. * - * @param file the file/directory to delete, must not be null - * @param failOnError whether to abort with an exception if the file/directory could not be deleted - * @param retryOnError whether to undertake additional delete attempts if the first attempt failed - * @return 0 if the file was deleted, 1 otherwise - * @throws IOException if a file/directory could not be deleted and failOnError is true + * @param file the file/directory to delete, must not be {@code null} + * @return whether the file has been deleted + * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true} */ - private int delete(Path file, boolean failOnError, boolean retryOnError) throws IOException { - IOException failure = delete(file); - if (failure != null) { - + @SuppressWarnings("SleepWhileInLoop") + private boolean tryDelete(final Path file) throws IOException { + try { + return Files.deleteIfExists(file); + } catch (IOException failure) { if (retryOnError) { if (ON_WINDOWS) { // try to release any locks held by non-closed files System.gc(); } - - final int[] delays = {50, 250, 750}; - for (int delay : delays) { + for (int delay : new int[] {50, 250, 750}) { try { Thread.sleep(delay); } catch (InterruptedException e) { - throw new IOException(e); + failure.addSuppressed(e); + throw failure; } - failure = delete(file); - if (failure == null) { - break; + try { + return Files.deleteIfExists(file); + } catch (IOException again) { + again.addSuppressed(failure); + failure = again; } } } - - if (Files.exists(file)) { - if (failOnError) { - throw new IOException("Failed to delete " + file, failure); - } else { - if (logWarn != null) { - logWarn.log("Failed to delete " + file, failure); - } - return 1; - } + if (logger.isWarnEnabled()) { + logger.warn("Failed to delete " + file, failure); } + failureCount++; + if (failOnError) { + throw failure; + } + return false; } - - return 0; } - private static IOException delete(Path file) { - try { - Files.deleteIfExists(file); - } catch (IOException e) { - return e; + /** + * Reports that a file, directory or symbolic link has been deleted. This method should be invoked only + * when {@link #tryDelete(Path)} returned {@code true} and {@link #listDeletedFiles} is {@code true}. + * + *

If {@code attrs} is {@code null}, then the file is assumed a directory. This arbitrary rule + * is an implementation convenience specific to the context in which we invoke this method.

+ */ + private void logDelete(final Path file, final BasicFileAttributes attrs) { + String message; + if (attrs == null || attrs.isDirectory()) { + message = "Deleted directory " + file; + } else if (attrs.isRegularFile()) { + message = "Deleted file " + file; + } else if (attrs.isSymbolicLink()) { + message = "Deleted dangling symlink " + file; + } else { + message = "Deleted " + file; } - return null; - } - - private static class Result { - - private int failures; - - private boolean excluded; - - public void update(Result result) { - failures += result.failures; - excluded |= result.excluded; + if (verbose) { + logger.info(message); + } else { + logger.debug(message); } } - private interface Logger { - - void log(CharSequence message); - - void log(CharSequence message, Throwable t); - } - private static class BackgroundCleaner extends Thread { private static BackgroundCleaner instance; @@ -409,13 +483,14 @@ private BackgroundCleaner(Cleaner cleaner, Path dir, String fastMode) { @Override public void run() { - while (true) { - Path basedir = pollNext(); - if (basedir == null) { - break; - } + var options = EnumSet.noneOf(FileVisitOption.class); + if (cleaner.followSymlinks) { + options.add(FileVisitOption.FOLLOW_LINKS); + } + Path basedir; + while ((basedir = pollNext()) != null) { try { - cleaner.delete(basedir, "", null, false, false, true); + Files.walkFileTree(basedir, options, Integer.MAX_VALUE, cleaner); } catch (IOException e) { // do not display errors } @@ -457,7 +532,7 @@ synchronized boolean doDelete(Path dir) { return false; } filesToDelete.add(dir); - if (status == NEW && FAST_MODE_BACKGROUND.equals(fastMode)) { + if (status == NEW && CleanMojo.FAST_MODE_BACKGROUND.equals(fastMode)) { status = RUNNING; notifyAll(); start(); @@ -486,11 +561,9 @@ synchronized void doSessionEnd() { if (status == NEW) { start(); } - if (!FAST_MODE_DEFER.equals(fastMode)) { + if (!CleanMojo.FAST_MODE_DEFER.equals(fastMode)) { try { - if (cleaner.logInfo != null) { - cleaner.logInfo.log("Waiting for background file deletion"); - } + cleaner.logger.info("Waiting for background file deletion"); while (status != STOPPED) { wait(); } diff --git a/src/main/java/org/apache/maven/plugins/clean/Fileset.java b/src/main/java/org/apache/maven/plugins/clean/Fileset.java index 90d15c53..2842ec6a 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Fileset.java +++ b/src/main/java/org/apache/maven/plugins/clean/Fileset.java @@ -19,11 +19,10 @@ package org.apache.maven.plugins.clean; import java.nio.file.Path; -import java.util.Arrays; /** * Customizes the string representation of - * org.apache.maven.shared.model.fileset.FileSet to return the + * {@code org.apache.maven.shared.model.fileset.FileSet} to return the * included and excluded files from the file-set's directory. Specifically, * "file-set: [directory] (included: [included files], * excluded: [excluded files])" @@ -43,53 +42,87 @@ public class Fileset { private boolean useDefaultExcludes; /** - * @return {@link #directory} + * {@return the base directory}. */ public Path getDirectory() { return directory; } /** - * @return {@link #includes} + * {@return the patterns of the file to include, or an empty array if unspecified}. */ public String[] getIncludes() { return (includes != null) ? includes : new String[0]; } /** - * @return {@link #excludes} + * {@return the patterns of the file to exclude, or an empty array if unspecified}. */ public String[] getExcludes() { return (excludes != null) ? excludes : new String[0]; } /** - * @return {@link #followSymlinks} + * {@return whether the base directory is excluded from the fileset}. + * This is {@code false} by default. The base directory can be excluded + * explicitly if the exclude patterns contains an empty string. + */ + public boolean isBaseDirectoryExcluded() { + if (excludes != null) { + for (String pattern : excludes) { + if (pattern == null || pattern.isEmpty()) { + return true; + } + } + } + return false; + } + + /** + * {@return whether to follow symbolic links}. */ public boolean isFollowSymlinks() { return followSymlinks; } /** - * @return {@link #useDefaultExcludes} + * {@return whether to use a default set of excludes}. */ public boolean isUseDefaultExcludes() { return useDefaultExcludes; } /** - * Retrieves the included and excluded files from this file-set's directory. - * Specifically, "file-set: [directory] (included: - * [included files], excluded: [excluded files])" + * Appends the elements of the given array in the given buffer. + * This is a helper method for {@link #toString()} implementations. * - * @return The included and excluded files from this file-set's directory. + * @param buffer the buffer where to add the elements + * @param label label identifying the array of elements to add + * @param patterns the elements to append, or {@code null} if none + */ + static void append(StringBuilder buffer, String label, String[] patterns) { + buffer.append(label).append(": ["); + if (patterns != null) { + for (int i = 0; i < patterns.length; i++) { + if (i != 0) { + buffer.append(", "); + } + buffer.append(patterns[i]); + } + } + buffer.append(']'); + } + + /** + * {@return a string representation of the included and excluded files from this file-set's directory}. * Specifically, "file-set: [directory] (included: * [included files], excluded: [excluded files])" - * @see java.lang.Object#toString() */ @Override public String toString() { - return "file set: " + getDirectory() + " (included: " + Arrays.asList(getIncludes()) + ", excluded: " - + Arrays.asList(getExcludes()) + ")"; + var buffer = new StringBuilder("file set: ").append(getDirectory()); + append(buffer.append(" ("), "included", getIncludes()); + append(buffer.append(", "), "excluded", getExcludes()); + return buffer.append(')').toString(); } } diff --git a/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java b/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java deleted file mode 100644 index 3d61071f..00000000 --- a/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 - * - * http://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.apache.maven.plugins.clean; - -import java.io.File; -import java.util.Arrays; - -import org.codehaus.plexus.util.DirectoryScanner; -import org.codehaus.plexus.util.SelectorUtils; - -/** - * Selects paths based on Ant-like glob patterns. - * - * @author Benjamin Bentmann - */ -class GlobSelector implements Selector { - - private final String[] includes; - - private final String[] excludes; - - private final String str; - - GlobSelector(String[] includes, String[] excludes, boolean useDefaultExcludes) { - this.str = "includes = " + toString(includes) + ", excludes = " + toString(excludes); - this.includes = normalizePatterns(includes); - this.excludes = normalizePatterns(addDefaultExcludes(excludes, useDefaultExcludes)); - } - - private static String toString(String[] patterns) { - return (patterns == null) ? "[]" : Arrays.asList(patterns).toString(); - } - - private static String[] addDefaultExcludes(String[] excludes, boolean useDefaultExcludes) { - String[] defaults = DirectoryScanner.DEFAULTEXCLUDES; - if (!useDefaultExcludes) { - return excludes; - } else if (excludes == null || excludes.length <= 0) { - return defaults; - } else { - String[] patterns = new String[excludes.length + defaults.length]; - System.arraycopy(excludes, 0, patterns, 0, excludes.length); - System.arraycopy(defaults, 0, patterns, excludes.length, defaults.length); - return patterns; - } - } - - private static String[] normalizePatterns(String[] patterns) { - String[] normalized; - - if (patterns != null) { - normalized = new String[patterns.length]; - for (int i = patterns.length - 1; i >= 0; i--) { - normalized[i] = normalizePattern(patterns[i]); - } - } else { - normalized = new String[0]; - } - - return normalized; - } - - private static String normalizePattern(String pattern) { - if (pattern == null) { - return ""; - } - - String normalized = pattern.replace((File.separatorChar == '/') ? '\\' : '/', File.separatorChar); - - if (normalized.endsWith(File.separator)) { - normalized += "**"; - } - - return normalized; - } - - @Override - public boolean isSelected(String pathname) { - return (includes.length <= 0 || isMatched(pathname, includes)) - && (excludes.length <= 0 || !isMatched(pathname, excludes)); - } - - private static boolean isMatched(String pathname, String[] patterns) { - for (String pattern : patterns) { - if (SelectorUtils.matchPath(pattern, pathname)) { - return true; - } - } - return false; - } - - @Override - public boolean couldHoldSelected(String pathname) { - for (String include : includes) { - if (SelectorUtils.matchPatternStart(include, pathname)) { - return true; - } - } - return includes.length <= 0; - } - - @Override - public String toString() { - return str; - } -} diff --git a/src/main/java/org/apache/maven/plugins/clean/Selector.java b/src/main/java/org/apache/maven/plugins/clean/Selector.java index 93a06953..ee840489 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Selector.java +++ b/src/main/java/org/apache/maven/plugins/clean/Selector.java @@ -18,28 +18,405 @@ */ package org.apache.maven.plugins.clean; +import java.io.File; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.LinkedHashSet; +import java.util.Set; + /** - * Determines whether a path is selected for deletion. The pathnames used for method parameters will be relative to some - * base directory and use {@link java.io.File#separatorChar} as separator. + * Determines whether a path is selected for deletion. + * The pathnames used for method parameters will be relative to some base directory + * and use {@code '/'} as separator, regardless the hosting operating system. + * + *

Syntax

+ * If a pattern contains the {@code ':'} character, then that pattern is given verbatim to + * {@link FileSystem#getPathMatcher(String)}, which will interpret the part before {@code ':'} + * as the syntax (usually {@code "glob"} or {@code "regex"}). If a pattern does not contain the + * {@code ':'} character, then the syntax defaults to a reproduction of the Maven 3 behavior. + * This is implemented as the {@code "glob"} syntax with the following modifications: + * + *
    + *
  • The platform-specific separator ({@code '\\'} on Windows) is replaced by {@code '/'}. + * Note that it means that the backslash cannot be used for escaping characters.
  • + *
  • Trailing {@code "/"} is completed as {@code "/**"}.
  • + *
  • The {@code "**"} wildcard means "0 or more directories" instead of "1 or more directories". + * This is implemented by adding variants of the pattern without the {@code "**"} wildcard.
  • + *
+ * + * If above changes are not desired, put an explicit {@code "glob:"} prefix before the patterns. + * Note that putting such prefix is recommended anyway for better performances. * * @author Benjamin Bentmann + * @author Martin Desruisseaux */ -interface Selector { +final class Selector implements PathMatcher { + /** + * Patterns which should be excluded by default, like SCM files. + * + *

Source: this list is copied from {@code plexus-utils-4.0.2} (released in + * September 23, 2024), class {@code org.codehaus.plexus.util.AbstractScanner}.

+ */ + private static final String[] DEFAULT_EXCLUDES = { + // Miscellaneous typical temporary files + "**/*~", + "**/#*#", + "**/.#*", + "**/%*%", + "**/._*", + + // CVS + "**/CVS", + "**/CVS/**", + "**/.cvsignore", + + // RCS + "**/RCS", + "**/RCS/**", + + // SCCS + "**/SCCS", + "**/SCCS/**", + + // Visual SourceSafe + "**/vssver.scc", + + // MKS + "**/project.pj", + + // Subversion + "**/.svn", + "**/.svn/**", + + // Arch + "**/.arch-ids", + "**/.arch-ids/**", + + // Bazaar + "**/.bzr", + "**/.bzr/**", + + // SurroundSCM + "**/.MySCMServerInfo", + + // Mac + "**/.DS_Store", + + // Serena Dimensions Version 10 + "**/.metadata", + "**/.metadata/**", + + // Mercurial + "**/.hg", + "**/.hg/**", + + // git + "**/.git", + "**/.git/**", + "**/.gitignore", + + // BitKeeper + "**/BitKeeper", + "**/BitKeeper/**", + "**/ChangeSet", + "**/ChangeSet/**", + + // darcs + "**/_darcs", + "**/_darcs/**", + "**/.darcsrepo", + "**/.darcsrepo/**", + "**/-darcs-backup*", + "**/.darcs-temp-mail" + }; + + /** + * String representation of the normalized include filters. + * This is kept only for {@link #toString()} implementation. + */ + private final String[] includePatterns; + + /** + * String representation of the normalized exclude filters. + * This is kept only for {@link #toString()} implementation. + */ + private final String[] excludePatterns; + + /** + * The matcher for includes. The length of this array is equal to {@link #includePatterns} array length. + */ + private final PathMatcher[] includes; + + /** + * The matcher for excludes. The length of this array is equal to {@link #excludePatterns} array length. + */ + private final PathMatcher[] excludes; + + /** + * The matcher for all directories to include. This array includes the parents of all those directories, + * because they need to be accepted before we can walk to the sub-directories. + * This is an optimization for skipping whole directories when possible. + */ + private final PathMatcher[] dirIncludes; + + /** + * The matcher for directories to exclude. This array does not include the parent directories, + * since they may contain other sub-trees that need to be included. + * This is an optimization for skipping whole directories when possible. + */ + private final PathMatcher[] dirExcludes; + + /** + * The base directory. All files will be relativized to that directory before to be matched. + */ + private final Path baseDirectory; + + /** + * Creates a new selector from the given file seT. + * + * @param fs the user-specified configuration + */ + Selector(Fileset fs) { + includePatterns = normalizePatterns(fs.getIncludes(), false); + excludePatterns = normalizePatterns(addDefaultExcludes(fs.getExcludes(), fs.isUseDefaultExcludes()), true); + baseDirectory = fs.getDirectory(); + FileSystem system = baseDirectory.getFileSystem(); + includes = matchers(system, includePatterns); + excludes = matchers(system, excludePatterns); + dirIncludes = matchers(system, directoryPatterns(includePatterns, false)); + dirExcludes = matchers(system, directoryPatterns(excludePatterns, true)); + } + + /** + * Returns the given array of excludes, optionally expanded with a default set of excludes. + * + * @param excludes the user-specified excludes. + * @param useDefaultExcludes whether to expand user exclude with the set of default excludes + * @return the potentially expanded set of excludes to use + */ + private static String[] addDefaultExcludes(final String[] excludes, final boolean useDefaultExcludes) { + if (!useDefaultExcludes) { + return excludes; + } + String[] defaults = DEFAULT_EXCLUDES; + if (excludes == null || excludes.length == 0) { + return defaults; + } else { + String[] patterns = new String[excludes.length + defaults.length]; + System.arraycopy(excludes, 0, patterns, 0, excludes.length); + System.arraycopy(defaults, 0, patterns, excludes.length, defaults.length); + return patterns; + } + } + + /** + * Returns the given array of patterns with path separator normalized to {@code '/'}. + * Null or empty patterns are ignored, and duplications are removed. + * + * @param patterns the patterns to normalize + * @param excludes whether the patterns are exclude patterns + * @return normalized patterns without null, empty or duplicated patterns + */ + private static String[] normalizePatterns(final String[] patterns, final boolean excludes) { + if (patterns == null) { + return new String[0]; + } + // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19. + final var normalized = new LinkedHashSet(patterns.length); + for (String pattern : patterns) { + if (pattern != null && !pattern.isEmpty()) { + final boolean useMavenSyntax = pattern.indexOf(':') < 0; + if (useMavenSyntax) { + pattern = pattern.replace(File.separatorChar, '/'); + if (pattern.endsWith("/")) { + pattern += "**"; + } + // Following are okay only when "**" means "0 or more directories". + while (pattern.endsWith("/**/**")) { + pattern = pattern.substring(0, pattern.length() - 3); + } + while (pattern.startsWith("**/**/")) { + pattern = pattern.substring(3); + } + pattern = pattern.replace("/**/**/", "/**/"); + } + normalized.add(pattern); + /* + * If the pattern starts or ends with "**", Java GLOB expects a directory level at + * that location while Maven seems to consider that "**" can mean "no directory". + * Add another pattern for reproducing this effect. + */ + if (useMavenSyntax) { + addPatternsWithOneDirRemoved(normalized, pattern, 0); + } + } + } + return simplify(normalized, excludes); + } + + /** + * Adds all variants of the given pattern with {@code **} removed. + * This is used for simulating the Maven behavior where {@code "**} may match zero directory. + * Tests suggest that we need an explicit GLOB pattern with no {@code "**"} for matching an absence of directory. + * + * @param patterns where to add the derived patterns + * @param pattern the pattern for which to add derived forms + * @param end should be 0 (reserved for recursive invocations of this method) + */ + private static void addPatternsWithOneDirRemoved(final Set patterns, final String pattern, int end) { + final int length = pattern.length(); + int start; + while ((start = pattern.indexOf("**", end)) >= 0) { + end = start + 2; // 2 is the length of "**". + if (end < length) { + if (pattern.charAt(end) != '/') { + continue; + } + if (start == 0) { + end++; // Ommit the leading slash if there is nothing before it. + } + } + if (start > 0) { + if (pattern.charAt(--start) != '/') { + continue; + } + } + String reduced = pattern.substring(0, start) + pattern.substring(end); + patterns.add(reduced); + addPatternsWithOneDirRemoved(patterns, reduced, start); + } + } + + /** + * Applies some heuristic rules for simplifying the set of patterns, + * then returns the patterns as an array. + * + * @param patterns the patterns to simplify and return asarray + * @param excludes whether the patterns are exclude patterns + * @return the set content as an array, after simplification + */ + private static String[] simplify(Set patterns, boolean excludes) { + /* + * If the "**" pattern is present, it makes all other patterns useless. + * In the case of include patterns, an empty set means to include everything. + */ + if (patterns.remove("**")) { + patterns.clear(); + if (excludes) { + patterns.add("**"); + } + } + return patterns.toArray(String[]::new); + } + + /** + * Eventually adds the parent directory of the given patterns, without duplicated values. + * The patterns given to this method should have been normalized. + * + * @param patterns the normalized include or exclude patterns + * @param excludes whether the patterns are exclude patterns + * @return pattens of directories to include or exclude + */ + private static String[] directoryPatterns(final String[] patterns, final boolean excludes) { + // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19. + final var directories = new LinkedHashSet(patterns.length); + for (String pattern : patterns) { + int s = pattern.indexOf(':'); + if (s < 0 || pattern.startsWith("glob:")) { + if (excludes) { + if (pattern.endsWith("/**")) { + directories.add(pattern.substring(0, pattern.length() - 3)); + } + } else { + if (pattern.regionMatches(++s, "**/", 0, 3)) { + s = pattern.indexOf('/', s + 3); + if (s < 0) { + return new String[0]; // Pattern is "**", so we need to accept everything. + } + directories.add(pattern.substring(0, s)); + } + } + } + } + return simplify(directories, excludes); + } + + /** + * Creates the path matchers for the given patterns. + * If no syntax is specified, the default is {@code glob}. + */ + private static PathMatcher[] matchers(final FileSystem fs, final String[] patterns) { + final var matchers = new PathMatcher[patterns.length]; + for (int i = 0; i < patterns.length; i++) { + String pattern = patterns[i]; + if (pattern.indexOf(':') < 0) { + pattern = "glob:" + pattern; + } + matchers[i] = fs.getPathMatcher(pattern); + } + return matchers; + } + + /** + * {@return whether there is no include or exclude filters}. + * In such case, this {@code Selector} instance should be ignored. + */ + boolean isEmpty() { + return (includes.length | excludes.length) == 0; + } /** * Determines whether a path is selected for deletion. + * This is true if the given file matches an include pattern and no exclude pattern. * - * @param pathname The pathname to test, must not be null. - * @return true if the given path is selected for deletion, false otherwise. + * @param pathname The pathname to test, must not be {@code null} + * @return {@code true} if the given path is selected for deletion, {@code false} otherwise */ - boolean isSelected(String pathname); + @Override + public boolean matches(Path path) { + path = baseDirectory.relativize(path); + return (includes.length == 0 || isMatched(path, includes)) + && (excludes.length == 0 || !isMatched(path, excludes)); + } + + /** + * {@return whether the given file matches according one of the given matchers}. + */ + private static boolean isMatched(Path path, PathMatcher[] matchers) { + for (PathMatcher matcher : matchers) { + if (matcher.matches(path)) { + return true; + } + } + return false; + } /** * Determines whether a directory could contain selected paths. * - * @param pathname The directory pathname to test, must not be null. - * @return true if the given directory might contain selected paths, false if the - * directory will definitively not contain selected paths.. + * @param directory the directory pathname to test, must not be {@code null} + * @return {@code true} if the given directory might contain selected paths, {@code false} if the + * directory will definitively not contain selected paths + */ + public boolean couldHoldSelected(Path directory) { + if (baseDirectory.equals(directory)) { + return true; + } + directory = baseDirectory.relativize(directory); + return (dirIncludes.length == 0 || isMatched(directory, dirIncludes)) + && (dirExcludes.length == 0 || !isMatched(directory, dirExcludes)); + } + + /** + * {@return a string representation for logging purposes}. + * This string representation is reported logged when {@link Cleaner} is executed. */ - boolean couldHoldSelected(String pathname); + @Override + public String toString() { + var buffer = new StringBuilder(); + Fileset.append(buffer, "includes", includePatterns); + Fileset.append(buffer.append(", "), "excludes", excludePatterns); + return buffer.toString(); + } } diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java index 02e8bf6c..670133cc 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java @@ -29,6 +29,7 @@ import java.nio.file.Paths; import java.util.Collections; +import org.apache.maven.api.plugin.Log; import org.apache.maven.api.plugin.MojoException; import org.apache.maven.api.plugin.testing.Basedir; import org.apache.maven.api.plugin.testing.InjectMojo; @@ -40,11 +41,11 @@ import static org.apache.maven.api.plugin.testing.MojoExtension.getBasedir; import static org.apache.maven.api.plugin.testing.MojoExtension.setVariableValueToObject; -import static org.codehaus.plexus.util.IOUtil.copy; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; /** * Test the clean mojo. @@ -52,6 +53,8 @@ @MojoTest public class CleanMojoTest { + private final Log log = mock(Log.class); + /** * Tests the simple removal of directories * @@ -211,8 +214,8 @@ public void testFollowLinksWithWindowsJunction() throws Exception { .start(); process.waitFor(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - copy(process.getInputStream(), baos); - copy(process.getErrorStream(), baos); + process.getInputStream().transferTo(baos); + process.getErrorStream().transferTo(baos); if (!Files.exists(link)) { throw new IOException("Unable to create junction: " + baos); } @@ -241,7 +244,7 @@ interface LinkCreator { } private void testSymlink(LinkCreator linkCreator) throws Exception { - Cleaner cleaner = new Cleaner(null, null, false, null, null); + Cleaner cleaner = new Cleaner(null, log, false, null, null, false, true, false); Path testDir = Paths.get("target/test-classes/unit/test-dir").toAbsolutePath(); Path dirWithLnk = testDir.resolve("dir"); Path orgDir = testDir.resolve("org-dir"); @@ -254,7 +257,7 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { Files.write(file, Collections.singleton("Hello world")); linkCreator.createLink(jctDir, orgDir); // delete - cleaner.delete(dirWithLnk, null, false, true, false); + cleaner.delete(dirWithLnk); // verify assertTrue(Files.exists(file)); assertFalse(Files.exists(jctDir)); @@ -267,7 +270,8 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { Files.write(file, Collections.singleton("Hello world")); linkCreator.createLink(jctDir, orgDir); // delete - cleaner.delete(dirWithLnk, null, true, true, false); + cleaner = new Cleaner(null, log, false, null, null, true, true, false); + cleaner.delete(dirWithLnk); // verify assertFalse(Files.exists(file)); assertFalse(Files.exists(jctDir)); diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java index 798ee3a5..c4e85673 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java @@ -18,9 +18,7 @@ */ package org.apache.maven.plugins.clean; -import java.io.IOException; import java.nio.file.AccessDeniedException; -import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; @@ -41,8 +39,8 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; @@ -61,8 +59,8 @@ class CleanerTest { void deleteSucceedsDeeply(@TempDir Path tempDir) throws Exception { final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath(); final Path file = createFile(basedir.resolve("file")); - final Cleaner cleaner = new Cleaner(null, log, false, null, null); - cleaner.delete(basedir, null, false, true, false); + final var cleaner = new Cleaner(null, log, false, null, null, false, true, false); + cleaner.delete(basedir); assertFalse(exists(basedir)); assertFalse(exists(file)); } @@ -76,14 +74,10 @@ void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Excep // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException. final Set permissions = PosixFilePermissions.fromString("rw-rw-r--"); setPosixFilePermissions(basedir, permissions); - final Cleaner cleaner = new Cleaner(null, log, false, null, null); - final IOException exception = - assertThrows(IOException.class, () -> cleaner.delete(basedir, null, false, true, false)); - verify(log, never()).warn(any(CharSequence.class), any(Throwable.class)); - assertEquals("Failed to delete " + basedir, exception.getMessage()); - final DirectoryNotEmptyException cause = - assertInstanceOf(DirectoryNotEmptyException.class, exception.getCause()); - assertEquals(basedir.toString(), cause.getMessage()); + final var cleaner = new Cleaner(null, log, false, null, null, false, true, false); + final var exception = assertThrows(AccessDeniedException.class, () -> cleaner.delete(basedir)); + verify(log, times(1)).warn(any(CharSequence.class), any(Throwable.class)); + assertTrue(exception.getMessage().contains(basedir.toString())); } @Test @@ -94,13 +88,9 @@ void deleteFailsAfterRetryWhenNoPermission(@TempDir Path tempDir) throws Excepti // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException. final Set permissions = PosixFilePermissions.fromString("rw-rw-r--"); setPosixFilePermissions(basedir, permissions); - final Cleaner cleaner = new Cleaner(null, log, false, null, null); - final IOException exception = - assertThrows(IOException.class, () -> cleaner.delete(basedir, null, false, true, true)); - assertEquals("Failed to delete " + basedir, exception.getMessage()); - final DirectoryNotEmptyException cause = - assertInstanceOf(DirectoryNotEmptyException.class, exception.getCause()); - assertEquals(basedir.toString(), cause.getMessage()); + final var cleaner = new Cleaner(null, log, false, null, null, false, true, true); + final var exception = assertThrows(AccessDeniedException.class, () -> cleaner.delete(basedir)); + assertTrue(exception.getMessage().contains(basedir.toString())); } @Test @@ -112,16 +102,13 @@ void deleteLogsWarningWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException. final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x"); setPosixFilePermissions(basedir, permissions); - final Cleaner cleaner = new Cleaner(null, log, false, null, null); - assertDoesNotThrow(() -> cleaner.delete(basedir, null, false, false, false)); - verify(log, times(2)).warn(any(CharSequence.class), any(Throwable.class)); + final var cleaner = new Cleaner(null, log, false, null, null, false, false, false); + assertDoesNotThrow(() -> cleaner.delete(basedir)); + verify(log, times(1)).warn(any(CharSequence.class), any(Throwable.class)); InOrder inOrder = inOrder(log); ArgumentCaptor cause1 = ArgumentCaptor.forClass(AccessDeniedException.class); inOrder.verify(log).warn(eq("Failed to delete " + file), cause1.capture()); assertEquals(file.toString(), cause1.getValue().getMessage()); - ArgumentCaptor cause2 = ArgumentCaptor.forClass(DirectoryNotEmptyException.class); - inOrder.verify(log).warn(eq("Failed to delete " + basedir), cause2.capture()); - assertEquals(basedir.toString(), cause2.getValue().getMessage()); } @Test @@ -133,8 +120,8 @@ void deleteDoesNotLogAnythingWhenNoPermissionAndWarnDisabled(@TempDir Path tempD // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException. final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x"); setPosixFilePermissions(basedir, permissions); - final Cleaner cleaner = new Cleaner(null, log, false, null, null); - assertDoesNotThrow(() -> cleaner.delete(basedir, null, false, false, false)); + final var cleaner = new Cleaner(null, log, false, null, null, false, false, false); + assertDoesNotThrow(() -> cleaner.delete(basedir)); verify(log, never()).warn(any(CharSequence.class), any(Throwable.class)); } } From 345b37f927541eb3a3b7b469e2f7444466f4b24e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 05:58:51 +0000 Subject: [PATCH 64/90] Bump org.mockito:mockito-core from 5.17.0 to 5.18.0 Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.17.0 to 5.18.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.17.0...v5.18.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.18.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 06c45def..81293c10 100644 --- a/pom.xml +++ b/pom.xml @@ -132,7 +132,7 @@ under the License. org.mockito mockito-core - 5.17.0 + 5.18.0 test From 1ae6f51fe9b296ba97c9b48f6dea321587253577 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sat, 21 Jun 2025 13:23:23 +0200 Subject: [PATCH 65/90] Configuration parameter for deleting read-only files (#250) This commit contains: * Javadoc adjustments for uniformization before the addition of a new parameter. * Replacements of ... by {@code ...}. * Replacements of
by

...

* Addition of a `force` configuration option for deleting read-only files and directories. * If a file has been deleted concurrently, report that fact as a log instead of considering the delete operation as a failure. --- src/it/read-only/pom.xml | 48 +++++ src/it/read-only/setup.bsh | 37 ++++ .../target/read-only-dir/read-only.properties | 16 ++ .../target/writable-dir/writable.properties | 16 ++ src/it/read-only/verify.bsh | 26 +++ .../apache/maven/plugins/clean/CleanMojo.java | 87 +++++---- .../apache/maven/plugins/clean/Cleaner.java | 165 +++++++++++++++--- .../maven/plugins/clean/CleanMojoTest.java | 4 +- .../maven/plugins/clean/CleanerTest.java | 10 +- 9 files changed, 341 insertions(+), 68 deletions(-) create mode 100644 src/it/read-only/pom.xml create mode 100644 src/it/read-only/setup.bsh create mode 100644 src/it/read-only/target/read-only-dir/read-only.properties create mode 100644 src/it/read-only/target/writable-dir/writable.properties create mode 100644 src/it/read-only/verify.bsh diff --git a/src/it/read-only/pom.xml b/src/it/read-only/pom.xml new file mode 100644 index 00000000..a3c0e832 --- /dev/null +++ b/src/it/read-only/pom.xml @@ -0,0 +1,48 @@ + + + + 4.0.0 + + test + read-only + 1.0-SNAPSHOT + + Test for clean + Check for proper deletion (or not) of read-only files. + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-clean-plugin + @pom.version@ + + true + false + + + + + + diff --git a/src/it/read-only/setup.bsh b/src/it/read-only/setup.bsh new file mode 100644 index 00000000..799b4118 --- /dev/null +++ b/src/it/read-only/setup.bsh @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +import java.io.File; + +if (!new File(basedir, "target/read-only-dir/read-only.properties").setWritable(false)) { + System.out.println("Cannot change file permission."); + return false; +} +if (File.separatorChar == '/') { + // Directory permission can be changed only on Unix, not on Windows. + if (!new File(basedir, "target/read-only-dir").setWritable(false)) { + System.out.println("Cannot change directory permission."); + return false; + } +} +if (!new File(basedir, "target/writable-dir/writable.properties").canWrite()) { + System.out.println("Expected a writable file."); + return false; +} +return true; diff --git a/src/it/read-only/target/read-only-dir/read-only.properties b/src/it/read-only/target/read-only-dir/read-only.properties new file mode 100644 index 00000000..13a83393 --- /dev/null +++ b/src/it/read-only/target/read-only-dir/read-only.properties @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. diff --git a/src/it/read-only/target/writable-dir/writable.properties b/src/it/read-only/target/writable-dir/writable.properties new file mode 100644 index 00000000..13a83393 --- /dev/null +++ b/src/it/read-only/target/writable-dir/writable.properties @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. diff --git a/src/it/read-only/verify.bsh b/src/it/read-only/verify.bsh new file mode 100644 index 00000000..ed830cca --- /dev/null +++ b/src/it/read-only/verify.bsh @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +import java.io.File; + +if (new File(basedir, "target").exists()) { + System.out.println("target should have been deleted."); + return false; +} +return true; diff --git a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java index 7f60747b..dc531d83 100644 --- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java +++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java @@ -30,18 +30,17 @@ /** * Goal which cleans the build. + * This attempts to clean a project's working directory of the files that were generated at build-time. + * By default, it discovers and deletes the directories configured in {@code project.build.directory}, + * {@code project.build.outputDirectory}, {@code project.build.testOutputDirectory}, and + * {@code project.reporting.outputDirectory}. + * *

- * This attempts to clean a project's working directory of the files that were generated at build-time. By default, it - * discovers and deletes the directories configured in project.build.directory, - * project.build.outputDirectory, project.build.testOutputDirectory, and - * project.reporting.outputDirectory. - *

- *

- * Files outside the default may also be included in the deletion by configuring the filesets tag. + * Files outside the default may also be included in the deletion by configuring the {@code filesets} tag. *

* * @author Emmanuel Venisse - * @see org.apache.maven.plugins.clean.Fileset + * @see Fileset * @since 2.0 */ @Mojo(name = "clean") @@ -84,9 +83,10 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { /** * Sets whether the plugin runs in verbose mode. As of plugin version 2.3, the default value is derived from Maven's - * global debug flag (compare command line switch -X).
- * Starting with 3.0.0 the property has been renamed from clean.verbose to - * maven.clean.verbose. + * global debug flag (compare command line switch {@code -X}). + * + *

Starting with 3.0.0 the property has been renamed from {@code clean.verbose} to + * {@code maven.clean.verbose}.

* * @since 2.1 */ @@ -119,11 +119,10 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { /** * Sets whether the plugin should follow symbolic links while deleting files from the default output directories of - * the project. Not following symlinks requires more IO operations and heap memory, regardless whether symlinks are - * actually present. So projects with a huge output directory that knowingly does not contain symlinks can improve - * performance by setting this parameter to true.
- * Starting with 3.0.0 the property has been renamed from clean.followSymLinks to - * maven.clean.followSymLinks. + * the project. + * + *

Starting with 3.0.0 the property has been renamed from {@code clean.followSymLinks} to + * {@code maven.clean.followSymLinks}.

* * @since 2.1 */ @@ -131,9 +130,18 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { private boolean followSymLinks; /** - * Disables the plugin execution.
- * Starting with 3.0.0 the property has been renamed from clean.skip to - * maven.clean.skip. + * Whether to force the deletion of read-only files. + * + * @since 3.4.2 + */ + @Parameter(property = "maven.clean.force", defaultValue = "false") + private boolean force; + + /** + * Disables the plugin execution. + * + *

Starting with 3.0.0 the property has been renamed from {@code clean.skip} to + * {@code maven.clean.skip}.

* * @since 2.2 */ @@ -159,10 +167,11 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { private boolean retryOnError; /** - * Disables the deletion of the default output directories configured for a project. If set to true, - * only the files/directories selected via the parameter {@link #filesets} will be deleted.
- * Starting with 3.0.0 the property has been renamed from clean.excludeDefaultDirectories to - * maven.clean.excludeDefaultDirectories. + * Disables the deletion of the default output directories configured for a project. If set to {@code true}, + * only the files/directories selected via the parameter {@link #filesets} will be deleted. + * + *

Starting with 3.0.0 the property has been renamed from {@code clean.excludeDefaultDirectories} + * to {@code maven.clean.excludeDefaultDirectories}.

* * @since 2.3 */ @@ -170,8 +179,8 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { private boolean excludeDefaultDirectories; /** - * Enables fast clean if possible. If set to true, when the plugin is executed, a directory to - * be deleted will be atomically moved inside the maven.clean.fastDir directory and a thread will + * Enables fast clean if possible. If set to {@code true}, when the plugin is executed, a directory to + * be deleted will be atomically moved inside the {@code maven.clean.fastDir} directory and a thread will * be launched to delete the needed files in the background. When the build is completed, maven will wait * until all the files have been deleted. If any problem occurs during the atomic move of the directories, * the plugin will default to the traditional deletion mechanism. @@ -182,10 +191,10 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { private boolean fast; /** - * When fast clean is specified, the fastDir property will be used as the location where directories + * When fast clean is specified, the {@code fastDir} property will be used as the location where directories * to be deleted will be moved prior to background deletion. If not specified, the - * ${maven.multiModuleProjectDirectory}/target/.clean directory will be used. If the - * ${build.directory} has been modified, you'll have to adjust this property explicitly. + * {@code ${maven.multiModuleProjectDirectory}/target/.clean} directory will be used. + * If the {@code ${build.directory}} has been modified, you'll have to adjust this property explicitly. * In order for fast clean to work correctly, this directory and the various directories that will be deleted * should usually reside on the same volume. The exact conditions are system-dependent though, but if an atomic * move is not supported, the standard deletion mechanism will be used. @@ -197,11 +206,11 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { private Path fastDir; /** - * Mode to use when using fast clean. Values are: background to start deletion immediately and - * waiting for all files to be deleted when the session ends, at-end to indicate that the actual - * deletion should be performed synchronously when the session ends, or defer to specify that - * the actual file deletion should be started in the background when the session ends. This should only be used - * when maven is embedded in a long-running process. + * Mode to use when using fast clean. Values are: {@code background} to start deletion immediately and + * waiting for all files to be deleted when the session ends, {@code at-end} to indicate that the actual + * deletion should be performed synchronously when the session ends, or {@code defer} to specify that + * the actual file deletion should be started in the background when the session ends. + * This should only be used when maven is embedded in a long-running process. * * @since 3.2 * @see #fast @@ -228,7 +237,9 @@ public void execute() { String multiModuleProjectDirectory = session != null ? session.getSystemProperties().get("maven.multiModuleProjectDirectory") : null; - Path fastDir; + + @SuppressWarnings("LocalVariableHidesMemberVariable") + final Path fastDir; if (fast && this.fastDir != null) { fastDir = this.fastDir; } else if (fast && multiModuleProjectDirectory != null) { @@ -248,8 +259,8 @@ public void execute() { throw new IllegalArgumentException("Illegal value '" + fastMode + "' for fastMode. Allowed values are '" + FAST_MODE_BACKGROUND + "', '" + FAST_MODE_AT_END + "' and '" + FAST_MODE_DEFER + "'."); } - final var cleaner = - new Cleaner(session, logger, isVerbose(), fastDir, fastMode, followSymLinks, failOnError, retryOnError); + final var cleaner = new Cleaner( + session, logger, isVerbose(), fastDir, fastMode, followSymLinks, force, failOnError, retryOnError); try { for (Path directoryItem : getDirectories()) { if (directoryItem != null) { @@ -272,7 +283,7 @@ public void execute() { /** * Indicates whether verbose output is enabled. * - * @return true if verbose output is enabled, false otherwise. + * @return {@code true} if verbose output is enabled, {@code false} otherwise. */ private boolean isVerbose() { return (verbose != null) ? verbose : logger.isDebugEnabled(); @@ -281,7 +292,7 @@ private boolean isVerbose() { /** * Gets the directories to clean (if any). The returned array may contain null entries. * - * @return The directories to clean or an empty array if none, never null. + * @return The directories to clean or an empty array if none, never {@code null}. */ private Path[] getDirectories() { Path[] directories; diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java index 0a399ac2..5715981a 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.AccessDeniedException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; @@ -27,10 +28,17 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.DosFileAttributeView; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayDeque; import java.util.BitSet; import java.util.Deque; import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import org.apache.maven.api.Event; @@ -81,6 +89,16 @@ final class Cleaner implements FileVisitor { */ private final boolean listDeletedFiles; + /** + * Whether the last call to {@code tryDelete(Path)} really deleted the file. + * If this flag is {@code false} after {@link #tryDelete(Path)} returned {@code true}, + * then the file has been deleted by some concurrent process before this cleaner plugin + * tried to delete the file. This flag is used only for reporting this fact in the logs. + * + * @see #logDelete(Path, BasicFileAttributes) + */ + private boolean reallyDeletedLastFile; + @Nonnull private final Path fastDir; @@ -99,10 +117,31 @@ final class Cleaner implements FileVisitor { private boolean followSymlinks; + /** + * Whether to force the deletion of read-only files. Note that on Linux, + * {@link Files#delete(Path)} and {@link Files#deleteIfExists(Path)} delete read-only files + * but throw {@link AccessDeniedException} if the directory containing the file is read-only. + */ + private final boolean force; + + /** + * Whether the build stops if there are I/O errors. + */ private final boolean failOnError; + /** + * Whether the plugin should undertake additional attempts (after a short delay) to delete a file + * if the first attempt failed. This is meant to help deleting files that are temporarily locked + * by third-party tools like virus scanners or search indexing. + */ private final boolean retryOnError; + /** + * The delays (in milliseconds) if {@link #retryOnError} is {@code true}. + * The length of this array is the maximal number of new attempts. + */ + private static final int[] RETRY_DELAYS = new int[] {50, 250, 750}; + /** * Number of files that we failed to delete. * This is incremented only if {@link #failOnError} is {@code false}, otherwise exceptions are thrown. @@ -131,6 +170,7 @@ final class Cleaner implements FileVisitor { * @param fastDir the explicit configured directory or to be deleted in fast mode * @param fastMode the fast deletion mode * @param followSymlinks whether to follow symlinks + * @param force whether to force the deletion of read-only files * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed */ @@ -142,6 +182,7 @@ final class Cleaner implements FileVisitor { @Nonnull Path fastDir, @Nonnull String fastMode, boolean followSymlinks, + boolean force, boolean failOnError, boolean retryOnError) { this.session = session; @@ -150,6 +191,7 @@ final class Cleaner implements FileVisitor { this.fastDir = fastDir; this.fastMode = fastMode; this.followSymlinks = followSymlinks; + this.force = force; this.failOnError = failOnError; this.retryOnError = retryOnError; listDeletedFiles = verbose ? logger.isInfoEnabled() : logger.isDebugEnabled(); @@ -210,7 +252,7 @@ private boolean fastDelete(Path baseDir) { // Handle the case where we use ${maven.multiModuleProjectDirectory}/target/.clean for example if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) { try { - String prefix = baseDir.getFileName().toString() + "."; + String prefix = baseDir.getFileName().toString() + '.'; Path tmpDir = Files.createTempDirectory(baseDir.getParent(), prefix); try { Files.move(baseDir, tmpDir, StandardCopyOption.REPLACE_EXISTING); @@ -330,8 +372,8 @@ public FileVisitResult postVisitDirectory(Path dir, IOException failure) throws canDelete = selector.matches(dir); } } - if (canDelete) { - if (tryDelete(dir) && listDeletedFiles) { + if (canDelete && tryDelete(dir)) { + if (listDeletedFiles) { logDelete(dir, null); } } else { @@ -370,6 +412,42 @@ private static Path getCanonicalPath(final Path path, IOException mainError) thr } } + /** + * Makes the given file or directory writable. + * If the file is already writable, then this method tries to make the parent directory writable. + * + * @param file the path to the file or directory to make writable, or {@code null} if none + * @param currentDepth 0 for the base directory, and decremented for each parent directory + * @return the root path which has been made writable, or {@code null} if none + */ + private static Path setWritable(Path file, int currentDepth) throws IOException { + while (file != null) { + PosixFileAttributeView posix = Files.getFileAttributeView(file, PosixFileAttributeView.class); + if (posix != null) { + EnumSet permissions = + EnumSet.copyOf(posix.readAttributes().permissions()); + if (permissions.add(PosixFilePermission.OWNER_WRITE)) { + posix.setPermissions(permissions); + return file; + } + } else { + DosFileAttributeView dos = Files.getFileAttributeView(file, DosFileAttributeView.class); + if (dos == null) { + return null; // Unknown type of file attributes. + } + // No need to update the parent directory because DOS read-only attribute does not apply to folders. + dos.setReadOnly(false); + return file; + } + // File was already writable. Maybe it is the parent directory which was not writable. + if (--currentDepth < 0) { + break; + } + file = file.getParent(); + } + return null; + } + /** * Deletes the specified file or directory. * If the path denotes a symlink, only the link is removed. Its target is left untouched. @@ -377,35 +455,63 @@ private static Path getCanonicalPath(final Path path, IOException mainError) thr * file does not exist or if an {@link IOException} occurred but {@link #failOnError} is * {@code false}. * + *

Auxiliary information as side-effect

+ * This method sets the {@link #reallyDeletedLastFile} flag to whether this method really deleted the file. + * If that flag is {@code false} after this method returned {@code true}, then the file has been deleted by + * some concurrent process before this method tried to deleted the file. + * That flag is used for logging purpose only. + * * @param file the file/directory to delete, must not be {@code null} - * @return whether the file has been deleted + * @return whether the file has been deleted or did not existed anymore by the time this method is invoked * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true} */ @SuppressWarnings("SleepWhileInLoop") private boolean tryDelete(final Path file) throws IOException { try { - return Files.deleteIfExists(file); + reallyDeletedLastFile = Files.deleteIfExists(file); + return true; } catch (IOException failure) { - if (retryOnError) { - if (ON_WINDOWS) { - // try to release any locks held by non-closed files - System.gc(); + boolean tryWritable = force && failure instanceof AccessDeniedException; + if (tryWritable || retryOnError) { + final Set madeWritable; // Safety against never-ending loops. + if (force) { + madeWritable = new HashSet<>(); + madeWritable.add(null); // For having `add(null)` to return `false`. + } else { + madeWritable = null; } - for (int delay : new int[] {50, 250, 750}) { - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - failure.addSuppressed(e); - throw failure; + final var alreadyReported = new HashMap, Set>(); // For avoiding repetition. + isNewError(alreadyReported, failure); + int delayIndex = 0; + while (delayIndex < RETRY_DELAYS.length) { + if (tryWritable) { + tryWritable = madeWritable.add(setWritable(file, currentDepth)); + // `true` if we successfully changed permission, in which case we will skip the delay. + } + if (!tryWritable) { + if (ON_WINDOWS) { + // Try to release any locks held by non-closed files. + System.gc(); + } + try { + Thread.sleep(RETRY_DELAYS[delayIndex++]); + } catch (InterruptedException e) { + failure.addSuppressed(e); + throw failure; + } } try { - return Files.deleteIfExists(file); + reallyDeletedLastFile = Files.deleteIfExists(file); + return true; } catch (IOException again) { - again.addSuppressed(failure); - failure = again; + tryWritable = force && failure instanceof AccessDeniedException; + if (isNewError(alreadyReported, again)) { + failure.addSuppressed(again); + } } } } + reallyDeletedLastFile = false; // As a matter of principle, but not really needed. if (logger.isWarnEnabled()) { logger.warn("Failed to delete " + file, failure); } @@ -417,6 +523,13 @@ private boolean tryDelete(final Path file) throws IOException { } } + /** + * Returns {@code true} if the given exception has not been reported before. + */ + private static boolean isNewError(Map, Set> reported, Exception e) { + return reported.computeIfAbsent(e.getClass(), (key) -> new HashSet<>()).add(e.getMessage()); + } + /** * Reports that a file, directory or symbolic link has been deleted. This method should be invoked only * when {@link #tryDelete(Path)} returned {@code true} and {@link #listDeletedFiles} is {@code true}. @@ -427,15 +540,21 @@ private boolean tryDelete(final Path file) throws IOException { private void logDelete(final Path file, final BasicFileAttributes attrs) { String message; if (attrs == null || attrs.isDirectory()) { - message = "Deleted directory " + file; + message = "Deleted directory "; } else if (attrs.isRegularFile()) { - message = "Deleted file " + file; + message = "Deleted file "; } else if (attrs.isSymbolicLink()) { - message = "Deleted dangling symlink " + file; + message = "Deleted dangling symlink "; } else { - message = "Deleted " + file; + message = "Deleted "; + } + boolean info = verbose; + if (!reallyDeletedLastFile) { + info = true; + message = "Another process deleted concurrently" + message.substring(message.indexOf(' ')); } - if (verbose) { + message += file; + if (info) { logger.info(message); } else { logger.debug(message); diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java index 670133cc..967edcea 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java @@ -244,7 +244,7 @@ interface LinkCreator { } private void testSymlink(LinkCreator linkCreator) throws Exception { - Cleaner cleaner = new Cleaner(null, log, false, null, null, false, true, false); + Cleaner cleaner = new Cleaner(null, log, false, null, null, false, false, true, false); Path testDir = Paths.get("target/test-classes/unit/test-dir").toAbsolutePath(); Path dirWithLnk = testDir.resolve("dir"); Path orgDir = testDir.resolve("org-dir"); @@ -270,7 +270,7 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { Files.write(file, Collections.singleton("Hello world")); linkCreator.createLink(jctDir, orgDir); // delete - cleaner = new Cleaner(null, log, false, null, null, true, true, false); + cleaner = new Cleaner(null, log, false, null, null, true, false, true, false); cleaner.delete(dirWithLnk); // verify assertFalse(Files.exists(file)); diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java index c4e85673..8de70d18 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java @@ -59,7 +59,7 @@ class CleanerTest { void deleteSucceedsDeeply(@TempDir Path tempDir) throws Exception { final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath(); final Path file = createFile(basedir.resolve("file")); - final var cleaner = new Cleaner(null, log, false, null, null, false, true, false); + final var cleaner = new Cleaner(null, log, false, null, null, false, false, true, false); cleaner.delete(basedir); assertFalse(exists(basedir)); assertFalse(exists(file)); @@ -74,7 +74,7 @@ void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Excep // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException. final Set permissions = PosixFilePermissions.fromString("rw-rw-r--"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, log, false, null, null, false, true, false); + final var cleaner = new Cleaner(null, log, false, null, null, false, false, true, false); final var exception = assertThrows(AccessDeniedException.class, () -> cleaner.delete(basedir)); verify(log, times(1)).warn(any(CharSequence.class), any(Throwable.class)); assertTrue(exception.getMessage().contains(basedir.toString())); @@ -88,7 +88,7 @@ void deleteFailsAfterRetryWhenNoPermission(@TempDir Path tempDir) throws Excepti // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException. final Set permissions = PosixFilePermissions.fromString("rw-rw-r--"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, log, false, null, null, false, true, true); + final var cleaner = new Cleaner(null, log, false, null, null, false, false, true, true); final var exception = assertThrows(AccessDeniedException.class, () -> cleaner.delete(basedir)); assertTrue(exception.getMessage().contains(basedir.toString())); } @@ -102,7 +102,7 @@ void deleteLogsWarningWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException. final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, log, false, null, null, false, false, false); + final var cleaner = new Cleaner(null, log, false, null, null, false, false, false, false); assertDoesNotThrow(() -> cleaner.delete(basedir)); verify(log, times(1)).warn(any(CharSequence.class), any(Throwable.class)); InOrder inOrder = inOrder(log); @@ -120,7 +120,7 @@ void deleteDoesNotLogAnythingWhenNoPermissionAndWarnDisabled(@TempDir Path tempD // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException. final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, log, false, null, null, false, false, false); + final var cleaner = new Cleaner(null, log, false, null, null, false, false, false, false); assertDoesNotThrow(() -> cleaner.delete(basedir)); verify(log, never()).warn(any(CharSequence.class), any(Throwable.class)); } From 0dfff4f9875bbd27277c05d7a747bc2be7aeb216 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 13:25:34 +0200 Subject: [PATCH 66/90] Bump org.apache.maven.plugins:maven-plugins from 44 to 45 (#260) Bumps [org.apache.maven.plugins:maven-plugins](https://github.com/apache/maven-parent) from 44 to 45. - [Release notes](https://github.com/apache/maven-parent/releases) - [Commits](https://github.com/apache/maven-parent/commits) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-plugins dependency-version: '45' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 81293c10..85aaad33 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.maven.plugins maven-plugins - 44 + 45 From 7f8febccf4debca0e2c869f981e5584c479856ef Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sat, 12 Jul 2025 11:52:32 +0200 Subject: [PATCH 67/90] Upgrade Maven core dependency to 4.0.0-rc-4. (#265) --- .github/workflows/maven-verify.yml | 2 +- pom.xml | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index a6464063..1e682104 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -27,4 +27,4 @@ jobs: uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v4 with: maven4-build: true - maven4-version: '4.0.0-rc-3' # the same as used in project + maven4-version: '4.0.0-rc-4' # the same as used in project diff --git a/pom.xml b/pom.xml index 85aaad33..6b32cc05 100644 --- a/pom.xml +++ b/pom.xml @@ -61,12 +61,12 @@ under the License. - 4.0.0-rc-3 + 4.0.0-rc-4 17 33.4.6-jre 6.0.0 2.0.17 - 3.7.0 + 3.9.1 4.0.0-beta-4 4.0.0-beta-1 3.21.0 @@ -98,6 +98,12 @@ under the License. ${mavenVersion} provided
+ + org.apache.maven + maven-xml + ${mavenVersion} + provided + From 8400a22ac2f0aaa18d027ec976caf1ce5862aab3 Mon Sep 17 00:00:00 2001 From: Sandra Parsick Date: Thu, 14 Aug 2025 07:36:33 +0200 Subject: [PATCH 68/90] feat: enable prevent branch protection rules (#268) also: - refactor deprecated config - remove unnecessary jira config Signed-off-by: Sandra Parsick --- .asf.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.asf.yaml b/.asf.yaml index 9ad5d1e3..42e1f6f7 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -30,7 +30,11 @@ github: rebase: true autolink_jira: - MCLEAN - del_branch_on_merge: true + protected_branches: + master: { } + maven-clean-plugin-3.x: { } + pull_requests: + del_branch_on_merge: true features: issues: true @@ -38,4 +42,3 @@ notifications: commits: commits@maven.apache.org issues: issues@maven.apache.org pullrequests: issues@maven.apache.org - jira_options: link label From b02b578b7291a4b436d74efe53af8d474ae238ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:41:00 +0000 Subject: [PATCH 69/90] Bump org.mockito:mockito-core from 5.18.0 to 5.19.0 Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.18.0 to 5.19.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.18.0...v5.19.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.19.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6b32cc05..7d906984 100644 --- a/pom.xml +++ b/pom.xml @@ -138,7 +138,7 @@ under the License. org.mockito mockito-core - 5.18.0 + 5.19.0 test From 1c02fc43508c40eb6bf3de132189f8c9a8461c51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 05:09:05 +0000 Subject: [PATCH 70/90] Bump org.mockito:mockito-core from 5.19.0 to 5.20.0 Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.19.0 to 5.20.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.19.0...v5.20.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.20.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7d906984..d4fcc5b6 100644 --- a/pom.xml +++ b/pom.xml @@ -138,7 +138,7 @@ under the License. org.mockito mockito-core - 5.19.0 + 5.20.0 test From ed9cab9ae9fd1b237aa6f0b66c9c3501882d6308 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Tue, 8 Jul 2025 16:03:33 +0200 Subject: [PATCH 71/90] Rename `Selector` as `PathSelector` for consistency with the class in Maven core. This is in anticipation for possible replacement by a shared implementation. --- src/main/java/org/apache/maven/plugins/clean/Cleaner.java | 4 ++-- .../plugins/clean/{Selector.java => PathSelector.java} | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/main/java/org/apache/maven/plugins/clean/{Selector.java => PathSelector.java} (99%) diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java index 5715981a..6dd973fc 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -106,7 +106,7 @@ final class Cleaner implements FileVisitor { private final String fastMode; @Nullable - private Selector selector; + private PathSelector selector; /** * Whether the base directory is excluded from the set of directories to delete. @@ -205,7 +205,7 @@ final class Cleaner implements FileVisitor { * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true} */ public void delete(@Nonnull Fileset fileset) throws IOException { - selector = new Selector(fileset); + selector = new PathSelector(fileset); if (selector.isEmpty()) { selector = null; } diff --git a/src/main/java/org/apache/maven/plugins/clean/Selector.java b/src/main/java/org/apache/maven/plugins/clean/PathSelector.java similarity index 99% rename from src/main/java/org/apache/maven/plugins/clean/Selector.java rename to src/main/java/org/apache/maven/plugins/clean/PathSelector.java index ee840489..0a80ba11 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Selector.java +++ b/src/main/java/org/apache/maven/plugins/clean/PathSelector.java @@ -51,7 +51,7 @@ * @author Benjamin Bentmann * @author Martin Desruisseaux */ -final class Selector implements PathMatcher { +final class PathSelector implements PathMatcher { /** * Patterns which should be excluded by default, like SCM files. * @@ -177,7 +177,7 @@ final class Selector implements PathMatcher { * * @param fs the user-specified configuration */ - Selector(Fileset fs) { + PathSelector(Fileset fs) { includePatterns = normalizePatterns(fs.getIncludes(), false); excludePatterns = normalizePatterns(addDefaultExcludes(fs.getExcludes(), fs.isUseDefaultExcludes()), true); baseDirectory = fs.getDirectory(); @@ -360,7 +360,7 @@ private static PathMatcher[] matchers(final FileSystem fs, final String[] patter /** * {@return whether there is no include or exclude filters}. - * In such case, this {@code Selector} instance should be ignored. + * In such case, this {@code PathSelector} instance should be ignored. */ boolean isEmpty() { return (includes.length | excludes.length) == 0; From d1ecf6e32cb638c498f3e8ffaa8e7921ad990424 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Tue, 8 Jul 2025 16:11:05 +0200 Subject: [PATCH 72/90] Replace `PathSelector` by a modified copy of the class in Maven core. The intend is to prepare the replacement by a shared implementation. --- .../apache/maven/plugins/clean/Cleaner.java | 11 +- .../apache/maven/plugins/clean/Fileset.java | 7 +- .../maven/plugins/clean/PathSelector.java | 508 +++++++++++++----- 3 files changed, 379 insertions(+), 147 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java index 6dd973fc..2ad8276e 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -32,6 +32,7 @@ import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayDeque; +import java.util.Arrays; import java.util.BitSet; import java.util.Deque; import java.util.EnumSet; @@ -105,6 +106,10 @@ final class Cleaner implements FileVisitor { @Nonnull private final String fastMode; + /** + * Combination of includes and excludes path matchers. + * A {@code null} value means to include everything. + */ @Nullable private PathSelector selector; @@ -205,7 +210,11 @@ final class Cleaner implements FileVisitor { * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true} */ public void delete(@Nonnull Fileset fileset) throws IOException { - selector = new PathSelector(fileset); + selector = new PathSelector( + fileset.getDirectory(), + Arrays.asList(fileset.getIncludes()), + Arrays.asList(fileset.getExcludes()), + fileset.isUseDefaultExcludes()); if (selector.isEmpty()) { selector = null; } diff --git a/src/main/java/org/apache/maven/plugins/clean/Fileset.java b/src/main/java/org/apache/maven/plugins/clean/Fileset.java index 2842ec6a..de497f8c 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Fileset.java +++ b/src/main/java/org/apache/maven/plugins/clean/Fileset.java @@ -41,6 +41,11 @@ public class Fileset { private boolean useDefaultExcludes; + /** + * Creates an initially empty file set. + */ + public Fileset() {} + /** * {@return the base directory}. */ @@ -96,7 +101,7 @@ public boolean isUseDefaultExcludes() { * Appends the elements of the given array in the given buffer. * This is a helper method for {@link #toString()} implementations. * - * @param buffer the buffer where to add the elements + * @param buffer the buffer to add the elements to * @param label label identifying the array of elements to add * @param patterns the elements to append, or {@code null} if none */ diff --git a/src/main/java/org/apache/maven/plugins/clean/PathSelector.java b/src/main/java/org/apache/maven/plugins/clean/PathSelector.java index 0a80ba11..2d810829 100644 --- a/src/main/java/org/apache/maven/plugins/clean/PathSelector.java +++ b/src/main/java/org/apache/maven/plugins/clean/PathSelector.java @@ -22,19 +22,26 @@ import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; import java.util.Set; /** - * Determines whether a path is selected for deletion. + * Determines whether a path is selected according to include/exclude patterns. * The pathnames used for method parameters will be relative to some base directory - * and use {@code '/'} as separator, regardless the hosting operating system. + * and use {@code '/'} as separator, regardless of the hosting operating system. * - *

Syntax

- * If a pattern contains the {@code ':'} character, then that pattern is given verbatim to - * {@link FileSystem#getPathMatcher(String)}, which will interpret the part before {@code ':'} - * as the syntax (usually {@code "glob"} or {@code "regex"}). If a pattern does not contain the - * {@code ':'} character, then the syntax defaults to a reproduction of the Maven 3 behavior. + *

Syntax

+ * If a pattern contains the {@code ':'} character and the prefix before is longer than 1 character, + * then that pattern is given verbatim to {@link FileSystem#getPathMatcher(String)}, which interprets + * the part before {@code ':'} as the syntax (usually {@code "glob"} or {@code "regex"}). + * If a pattern does not contain the {@code ':'} character, or if the prefix is one character long + * (interpreted as a Windows drive), then the syntax defaults to a reproduction of the Maven 3 behavior. * This is implemented as the {@code "glob"} syntax with the following modifications: * *
    @@ -43,13 +50,17 @@ *
  • Trailing {@code "/"} is completed as {@code "/**"}.
  • *
  • The {@code "**"} wildcard means "0 or more directories" instead of "1 or more directories". * This is implemented by adding variants of the pattern without the {@code "**"} wildcard.
  • + *
  • Bracket characters [ ] and { } are escaped.
  • + *
  • On Unix only, the escape character {@code '\\'} is itself escaped.
  • *
* - * If above changes are not desired, put an explicit {@code "glob:"} prefix before the patterns. - * Note that putting such prefix is recommended anyway for better performances. + * If above changes are not desired, put an explicit {@code "glob:"} prefix before the pattern. + * Note that putting such a prefix is recommended anyway for better performances. * * @author Benjamin Bentmann * @author Martin Desruisseaux + * + * @see java.nio.file.FileSystem#getPathMatcher(String) */ final class PathSelector implements PathMatcher { /** @@ -58,93 +69,126 @@ final class PathSelector implements PathMatcher { *

Source: this list is copied from {@code plexus-utils-4.0.2} (released in * September 23, 2024), class {@code org.codehaus.plexus.util.AbstractScanner}.

*/ - private static final String[] DEFAULT_EXCLUDES = { - // Miscellaneous typical temporary files - "**/*~", - "**/#*#", - "**/.#*", - "**/%*%", - "**/._*", - - // CVS - "**/CVS", - "**/CVS/**", - "**/.cvsignore", - - // RCS - "**/RCS", - "**/RCS/**", - - // SCCS - "**/SCCS", - "**/SCCS/**", - - // Visual SourceSafe - "**/vssver.scc", - - // MKS - "**/project.pj", - - // Subversion - "**/.svn", - "**/.svn/**", - - // Arch - "**/.arch-ids", - "**/.arch-ids/**", - - // Bazaar - "**/.bzr", - "**/.bzr/**", - - // SurroundSCM - "**/.MySCMServerInfo", - - // Mac - "**/.DS_Store", - - // Serena Dimensions Version 10 - "**/.metadata", - "**/.metadata/**", - - // Mercurial - "**/.hg", - "**/.hg/**", - - // git - "**/.git", - "**/.git/**", - "**/.gitignore", - - // BitKeeper - "**/BitKeeper", - "**/BitKeeper/**", - "**/ChangeSet", - "**/ChangeSet/**", - - // darcs - "**/_darcs", - "**/_darcs/**", - "**/.darcsrepo", - "**/.darcsrepo/**", - "**/-darcs-backup*", - "**/.darcs-temp-mail" - }; + private static final List DEFAULT_EXCLUDES = List.of( + // Miscellaneous typical temporary files + "**/*~", + "**/#*#", + "**/.#*", + "**/%*%", + "**/._*", + + // CVS + "**/CVS", + "**/CVS/**", + "**/.cvsignore", + + // RCS + "**/RCS", + "**/RCS/**", + + // SCCS + "**/SCCS", + "**/SCCS/**", + + // Visual SourceSafe + "**/vssver.scc", + + // MKS + "**/project.pj", + + // Subversion + "**/.svn", + "**/.svn/**", + + // Arch + "**/.arch-ids", + "**/.arch-ids/**", + + // Bazaar + "**/.bzr", + "**/.bzr/**", + + // SurroundSCM + "**/.MySCMServerInfo", + + // Mac + "**/.DS_Store", + + // Serena Dimensions Version 10 + "**/.metadata", + "**/.metadata/**", + + // Mercurial + "**/.hg", + "**/.hg/**", + + // git + "**/.git", + "**/.git/**", + "**/.gitignore", + + // BitKeeper + "**/BitKeeper", + "**/BitKeeper/**", + "**/ChangeSet", + "**/ChangeSet/**", + + // darcs + "**/_darcs", + "**/_darcs/**", + "**/.darcsrepo", + "**/.darcsrepo/**", + "**/-darcs-backup*", + "**/.darcs-temp-mail"); + + /** + * Maximum number of characters of the prefix before {@code ':'} for handling as a Maven syntax. + */ + private static final int MAVEN_SYNTAX_THRESHOLD = 1; /** - * String representation of the normalized include filters. - * This is kept only for {@link #toString()} implementation. + * The default syntax to use if none was specified. Note that when this default syntax is applied, + * the user-provided pattern get some changes as documented in class Javadoc. + */ + private static final String DEFAULT_SYNTAX = "glob:"; + + /** + * Characters having a special meaning in the glob syntax. + * + * @see FileSystem#getPathMatcher(String) + */ + private static final String SPECIAL_CHARACTERS = "*?[]{}\\"; + + /** + * A path matcher which accepts all files. + * + * @see #simplify() + */ + private static final PathMatcher INCLUDES_ALL = (path) -> true; + + /** + * String representations of the normalized include filters. + * Each pattern shall be prefixed by its syntax, which is {@value #DEFAULT_SYNTAX} by default. + * An empty array means to include all files. + * + * @see #toString() */ private final String[] includePatterns; /** - * String representation of the normalized exclude filters. - * This is kept only for {@link #toString()} implementation. + * String representations of the normalized exclude filters. + * Each pattern shall be prefixed by its syntax. If no syntax is specified, + * the default is a Maven 3 syntax similar, but not identical, to {@value #DEFAULT_SYNTAX}. + * This array may be longer or shorter than the user-supplied excludes, depending on whether + * default excludes have been added and whether some unnecessary excludes have been omitted. + * + * @see #toString() */ private final String[] excludePatterns; /** * The matcher for includes. The length of this array is equal to {@link #includePatterns} array length. + * An empty array means to include all files. */ private final PathMatcher[] includes; @@ -157,12 +201,13 @@ final class PathSelector implements PathMatcher { * The matcher for all directories to include. This array includes the parents of all those directories, * because they need to be accepted before we can walk to the sub-directories. * This is an optimization for skipping whole directories when possible. + * An empty array means to include all directories. */ private final PathMatcher[] dirIncludes; /** * The matcher for directories to exclude. This array does not include the parent directories, - * since they may contain other sub-trees that need to be included. + * because they may contain other sub-trees that need to be included. * This is an optimization for skipping whole directories when possible. */ private final PathMatcher[] dirExcludes; @@ -173,41 +218,184 @@ final class PathSelector implements PathMatcher { private final Path baseDirectory; /** - * Creates a new selector from the given file seT. + * Whether paths must be relativized before to be given to a matcher. If {@code true}, then every paths + * will be made relative to {@link #baseDirectory} for allowing patterns like {@code "foo/bar/*.java"} + * to work. As a slight optimization, we can skip this step if all patterns start with {@code "**"}. + */ + private final boolean needRelativize; + + /** + * Creates a new selector from the given includes and excludes. * - * @param fs the user-specified configuration + * @param directory the base directory of the files to filter + * @param includes the patterns of the files to include, or null or empty for including all files + * @param excludes the patterns of the files to exclude, or null or empty for no exclusion + * @param useDefaultExcludes whether to augment the excludes with a default set of SCM patterns */ - PathSelector(Fileset fs) { - includePatterns = normalizePatterns(fs.getIncludes(), false); - excludePatterns = normalizePatterns(addDefaultExcludes(fs.getExcludes(), fs.isUseDefaultExcludes()), true); - baseDirectory = fs.getDirectory(); - FileSystem system = baseDirectory.getFileSystem(); - includes = matchers(system, includePatterns); - excludes = matchers(system, excludePatterns); - dirIncludes = matchers(system, directoryPatterns(includePatterns, false)); - dirExcludes = matchers(system, directoryPatterns(excludePatterns, true)); + PathSelector(Path directory, Collection includes, Collection excludes, boolean useDefaultExcludes) { + includePatterns = normalizePatterns(includes, false); + excludePatterns = normalizePatterns(effectiveExcludes(excludes, includePatterns, useDefaultExcludes), true); + baseDirectory = directory; + FileSystem fs = directory.getFileSystem(); + this.includes = matchers(fs, includePatterns); + this.excludes = matchers(fs, excludePatterns); + dirIncludes = matchers(fs, directoryPatterns(includePatterns, false)); + dirExcludes = matchers(fs, directoryPatterns(excludePatterns, true)); + needRelativize = needRelativize(includePatterns) || needRelativize(excludePatterns); } /** - * Returns the given array of excludes, optionally expanded with a default set of excludes. + * Returns the given array of excludes, optionally expanded with a default set of excludes, + * then with unnecessary excludes omitted. An unnecessary exclude is an exclude which will never + * match a file because there is no include which would accept a file that could match the exclude. + * For example, if the only include is {@code "*.java"}, then the "**/project.pj", + * "**/.DS_Store" and other excludes will never match a file and can be omitted. + * Because the list of {@linkplain #DEFAULT_EXCLUDES default excludes} contains many elements, + * removing unnecessary excludes can reduce a lot the number of matches tested on each source file. + * + *

Implementation note

+ * The removal of unnecessary excludes is done on a best effort basis. The current implementation + * compares only the prefixes and suffixes of each pattern, keeping the pattern in case of doubt. + * This is not bad, but it does not remove all unnecessary patterns. It would be possible to do + * better in the future if benchmarking suggests that it would be worth the effort. * - * @param excludes the user-specified excludes. + * @param excludes the user-specified excludes, potentially not yet converted to glob syntax + * @param includes the include patterns converted to glob syntax * @param useDefaultExcludes whether to expand user exclude with the set of default excludes - * @return the potentially expanded set of excludes to use + * @return the potentially expanded or reduced set of excludes to use */ - private static String[] addDefaultExcludes(final String[] excludes, final boolean useDefaultExcludes) { - if (!useDefaultExcludes) { + private static Collection effectiveExcludes( + Collection excludes, final String[] includes, final boolean useDefaultExcludes) { + if (excludes == null || excludes.isEmpty()) { + if (useDefaultExcludes) { + excludes = new ArrayList<>(DEFAULT_EXCLUDES); + } else { + return List.of(); + } + } else { + excludes = new ArrayList<>(excludes); + excludes.removeIf(Objects::isNull); + if (useDefaultExcludes) { + excludes.addAll(DEFAULT_EXCLUDES); + } + } + if (includes.length == 0) { return excludes; } - String[] defaults = DEFAULT_EXCLUDES; - if (excludes == null || excludes.length == 0) { - return defaults; - } else { - String[] patterns = new String[excludes.length + defaults.length]; - System.arraycopy(excludes, 0, patterns, 0, excludes.length); - System.arraycopy(defaults, 0, patterns, excludes.length, defaults.length); - return patterns; + /* + * Get the prefixes and suffixes of all includes, stopping at the first special character. + * Redundant prefixes and suffixes are omitted. + */ + var prefixes = new String[includes.length]; + var suffixes = new String[includes.length]; + for (int i = 0; i < includes.length; i++) { + String include = includes[i]; + if (!include.startsWith(DEFAULT_SYNTAX)) { + return excludes; // Do not filter if at least one pattern is too complicated. + } + include = include.substring(DEFAULT_SYNTAX.length()); + prefixes[i] = prefixOrSuffix(include, false); + suffixes[i] = prefixOrSuffix(include, true); + } + prefixes = sortByLength(prefixes, false); + suffixes = sortByLength(suffixes, true); + /* + * Keep only the exclude which start with one of the prefixes and end with one of the suffixes. + * Note that a prefix or suffix may be the empty string, which match everything. + */ + final Iterator it = excludes.iterator(); + nextExclude: + while (it.hasNext()) { + final String exclude = it.next(); + final int s = exclude.indexOf(':'); + if (s <= MAVEN_SYNTAX_THRESHOLD || exclude.startsWith(DEFAULT_SYNTAX)) { + if (cannotMatch(exclude, prefixes, false) || cannotMatch(exclude, suffixes, true)) { + it.remove(); + } + } + } + return excludes; + } + + /** + * Returns the maximal amount of ordinary characters at the beginning or end of the given pattern. + * The prefix or suffix stops at the first {@linkplain #SPECIAL_CHARACTERS special character}. + * + * @param include the pattern for which to get a prefix or suffix without special character + * @param suffix {@code false} if a prefix is desired, or {@code true} if a suffix is desired + */ + private static String prefixOrSuffix(final String include, boolean suffix) { + int s = suffix ? -1 : include.length(); + for (int i = SPECIAL_CHARACTERS.length(); --i >= 0; ) { + char c = SPECIAL_CHARACTERS.charAt(i); + if (suffix) { + s = Math.max(s, include.lastIndexOf(c)); + } else { + int p = include.indexOf(c); + if (p >= 0 && p < s) { + s = p; + } + } } + return suffix ? include.substring(s + 1) : include.substring(0, s); + } + + /** + * Returns {@code true} if the given exclude cannot match any include patterns. + * In case of doubt, returns {@code false}. + * + * @param exclude the exclude pattern to test + * @param fragments the prefixes or suffixes (fragments without special characters) of the includes + * @param suffix {@code false} if the specified fragments are prefixes, {@code true} if they are suffixes + * @return {@code true} if it is certain that the exclude pattern cannot match, or {@code false} in case of doubt + */ + private static boolean cannotMatch(String exclude, final String[] fragments, final boolean suffix) { + exclude = prefixOrSuffix(exclude, suffix); + for (String fragment : fragments) { + int fg = fragment.length(); + int ex = exclude.length(); + int length = Math.min(fg, ex); + if (suffix) { + fg -= length; + ex -= length; + } else { + fg = 0; + ex = 0; + } + if (exclude.regionMatches(ex, fragment, fg, length)) { + return false; + } + } + return true; + } + + /** + * Sorts the given patterns by their length. The main intent is to have the empty string first, + * while will cause the loops testing for prefixes and suffixes to stop almost immediately. + * Short prefixes or suffixes are also more likely to be matched. + * + * @param fragments the fragments to sort in-place + * @param suffix {@code false} if the specified fragments are prefixes, {@code true} if they are suffixes + * @return the given array, or a smaller array if some fragments were discarded because redundant + */ + private static String[] sortByLength(final String[] fragments, final boolean suffix) { + Arrays.sort(fragments, (s1, s2) -> s1.length() - s2.length()); + int count = 0; + /* + * Simplify the array of prefixes or suffixes by removing all redundant elements. + * An element is redundant if there is a shorter prefix or suffix with the same characters. + */ + nextBase: + for (String fragment : fragments) { + for (int i = count; --i >= 0; ) { + String base = fragments[i]; + if (suffix ? fragment.endsWith(base) : fragment.startsWith(base)) { + continue nextBase; // Skip this fragment + } + } + fragments[count++] = fragment; + } + return (fragments.length == count) ? fragments : Arrays.copyOf(fragments, count); } /** @@ -218,16 +406,15 @@ private static String[] addDefaultExcludes(final String[] excludes, final boolea * @param excludes whether the patterns are exclude patterns * @return normalized patterns without null, empty or duplicated patterns */ - private static String[] normalizePatterns(final String[] patterns, final boolean excludes) { - if (patterns == null) { + private static String[] normalizePatterns(final Collection patterns, final boolean excludes) { + if (patterns == null || patterns.isEmpty()) { return new String[0]; } // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19. - final var normalized = new LinkedHashSet(patterns.length); + final var normalized = new LinkedHashSet(patterns.size()); for (String pattern : patterns) { if (pattern != null && !pattern.isEmpty()) { - final boolean useMavenSyntax = pattern.indexOf(':') < 0; - if (useMavenSyntax) { + if (pattern.indexOf(':') <= MAVEN_SYNTAX_THRESHOLD) { pattern = pattern.replace(File.separatorChar, '/'); if (pattern.endsWith("/")) { pattern += "**"; @@ -240,15 +427,20 @@ private static String[] normalizePatterns(final String[] patterns, final boolean pattern = pattern.substring(3); } pattern = pattern.replace("/**/**/", "/**/"); - } - normalized.add(pattern); - /* - * If the pattern starts or ends with "**", Java GLOB expects a directory level at - * that location while Maven seems to consider that "**" can mean "no directory". - * Add another pattern for reproducing this effect. - */ - if (useMavenSyntax) { + pattern = pattern.replace("\\", "\\\\") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("{", "\\{") + .replace("}", "\\}"); + normalized.add(DEFAULT_SYNTAX + pattern); + /* + * If the pattern starts or ends with "**", Java GLOB expects a directory level at + * that location while Maven seems to consider that "**" can mean "no directory". + * Add another pattern for reproducing this effect. + */ addPatternsWithOneDirRemoved(normalized, pattern, 0); + } else { + normalized.add(pattern); } } } @@ -261,7 +453,7 @@ private static String[] normalizePatterns(final String[] patterns, final boolean * Tests suggest that we need an explicit GLOB pattern with no {@code "**"} for matching an absence of directory. * * @param patterns where to add the derived patterns - * @param pattern the pattern for which to add derived forms + * @param pattern the pattern for which to add derived forms, without the "glob:" syntax prefix * @param end should be 0 (reserved for recursive invocations of this method) */ private static void addPatternsWithOneDirRemoved(final Set patterns, final String pattern, int end) { @@ -277,13 +469,11 @@ private static void addPatternsWithOneDirRemoved(final Set patterns, fin end++; // Ommit the leading slash if there is nothing before it. } } - if (start > 0) { - if (pattern.charAt(--start) != '/') { - continue; - } + if (start > 0 && pattern.charAt(--start) != '/') { + continue; } String reduced = pattern.substring(0, start) + pattern.substring(end); - patterns.add(reduced); + patterns.add(DEFAULT_SYNTAX + reduced); addPatternsWithOneDirRemoved(patterns, reduced, start); } } @@ -292,7 +482,7 @@ private static void addPatternsWithOneDirRemoved(final Set patterns, fin * Applies some heuristic rules for simplifying the set of patterns, * then returns the patterns as an array. * - * @param patterns the patterns to simplify and return asarray + * @param patterns the patterns to simplify and return as an array * @param excludes whether the patterns are exclude patterns * @return the set content as an array, after simplification */ @@ -322,13 +512,13 @@ private static String[] directoryPatterns(final String[] patterns, final boolean // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19. final var directories = new LinkedHashSet(patterns.length); for (String pattern : patterns) { - int s = pattern.indexOf(':'); - if (s < 0 || pattern.startsWith("glob:")) { + if (pattern.startsWith(DEFAULT_SYNTAX)) { if (excludes) { if (pattern.endsWith("/**")) { directories.add(pattern.substring(0, pattern.length() - 3)); } } else { + int s = pattern.indexOf(':'); if (pattern.regionMatches(++s, "**/", 0, 3)) { s = pattern.indexOf('/', s + 3); if (s < 0) { @@ -342,18 +532,29 @@ private static String[] directoryPatterns(final String[] patterns, final boolean return simplify(directories, excludes); } + /** + * Returns {@code true} if at least one pattern requires path to be relativized before to be matched. + * + * @param patterns include or exclude patterns + * @return whether at least one pattern require relativization + */ + private static boolean needRelativize(String[] patterns) { + for (String pattern : patterns) { + if (!pattern.startsWith(DEFAULT_SYNTAX + "**/")) { + return true; + } + } + return false; + } + /** * Creates the path matchers for the given patterns. - * If no syntax is specified, the default is {@code glob}. + * The syntax (usually {@value #DEFAULT_SYNTAX}) must be specified for each pattern. */ private static PathMatcher[] matchers(final FileSystem fs, final String[] patterns) { final var matchers = new PathMatcher[patterns.length]; for (int i = 0; i < patterns.length; i++) { - String pattern = patterns[i]; - if (pattern.indexOf(':') < 0) { - pattern = "glob:" + pattern; - } - matchers[i] = fs.getPathMatcher(pattern); + matchers[i] = fs.getPathMatcher(patterns[i]); } return matchers; } @@ -362,26 +563,44 @@ private static PathMatcher[] matchers(final FileSystem fs, final String[] patter * {@return whether there is no include or exclude filters}. * In such case, this {@code PathSelector} instance should be ignored. */ - boolean isEmpty() { + public boolean isEmpty() { return (includes.length | excludes.length) == 0; } /** - * Determines whether a path is selected for deletion. + * {@return a potentially simpler matcher equivalent to this matcher}. + */ + @SuppressWarnings("checkstyle:MissingSwitchDefault") + public PathMatcher simplify() { + if (!needRelativize && excludes.length == 0) { + switch (includes.length) { + case 0: + return INCLUDES_ALL; + case 1: + return includes[0]; + } + } + return this; + } + + /** + * Determines whether a path is selected. * This is true if the given file matches an include pattern and no exclude pattern. * - * @param pathname The pathname to test, must not be {@code null} - * @return {@code true} if the given path is selected for deletion, {@code false} otherwise + * @param path the pathname to test, must not be {@code null} + * @return {@code true} if the given path is selected, {@code false} otherwise */ @Override public boolean matches(Path path) { - path = baseDirectory.relativize(path); + if (needRelativize) { + path = baseDirectory.relativize(path); + } return (includes.length == 0 || isMatched(path, includes)) && (excludes.length == 0 || !isMatched(path, excludes)); } /** - * {@return whether the given file matches according one of the given matchers}. + * {@return whether the given file matches according to one of the given matchers}. */ private static boolean isMatched(Path path, PathMatcher[] matchers) { for (PathMatcher matcher : matchers) { @@ -410,7 +629,6 @@ public boolean couldHoldSelected(Path directory) { /** * {@return a string representation for logging purposes}. - * This string representation is reported logged when {@link Cleaner} is executed. */ @Override public String toString() { From 25c53b6543b5f01ea64e60356016456b47dd1bc2 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sun, 26 Oct 2025 18:13:13 +0100 Subject: [PATCH 73/90] Delete also the directories specified in `` (#276) In the list of directories to delete, include the values specified in the `` child of `` elements. Remove the `reportDirectory` field because its value (read-only) was identical to `outputDirectory` (also read-only). --- .../outside-target/README.md | 20 ++++ src/it/sources-targetPath/pom.xml | 46 ++++++++ src/it/sources-targetPath/setup.bsh | 23 ++++ .../target/test-classes/README.md | 19 +++ src/it/sources-targetPath/verify.bsh | 22 ++++ .../apache/maven/plugins/clean/CleanMojo.java | 111 ++++++++++++++---- 6 files changed, 215 insertions(+), 26 deletions(-) create mode 100644 src/it/sources-targetPath/outside-target/README.md create mode 100644 src/it/sources-targetPath/pom.xml create mode 100644 src/it/sources-targetPath/setup.bsh create mode 100644 src/it/sources-targetPath/target/test-classes/README.md create mode 100644 src/it/sources-targetPath/verify.bsh diff --git a/src/it/sources-targetPath/outside-target/README.md b/src/it/sources-targetPath/outside-target/README.md new file mode 100644 index 00000000..47afc419 --- /dev/null +++ b/src/it/sources-targetPath/outside-target/README.md @@ -0,0 +1,20 @@ + + +The purpose of this file is to verify that the directory specified by `` is deleted. +This verification requires a directory outside the `target` directory, otherwise its deletion could +not be distinguished from the usual deletion of `${maven.build.directory}`. diff --git a/src/it/sources-targetPath/pom.xml b/src/it/sources-targetPath/pom.xml new file mode 100644 index 00000000..3ef54a0d --- /dev/null +++ b/src/it/sources-targetPath/pom.xml @@ -0,0 +1,46 @@ + + + + 4.1.0 + + test + sources-targetPath + 1.0-SNAPSHOT + + Test <targetPath> cleaning + Check for proper cleaning of output files in directories specified by <targetPath>. + + + + + org.apache.maven.plugins + maven-clean-plugin + @pom.version@ + + + + + + ${project.basedir}/outside-target + + + + + diff --git a/src/it/sources-targetPath/setup.bsh b/src/it/sources-targetPath/setup.bsh new file mode 100644 index 00000000..a9fc1366 --- /dev/null +++ b/src/it/sources-targetPath/setup.bsh @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +import java.io.File; + +return new File(basedir, "target").exists() && new File(basedir, "outside-target").exists(); +// The opposite condition is tested in "verify.bsh". diff --git a/src/it/sources-targetPath/target/test-classes/README.md b/src/it/sources-targetPath/target/test-classes/README.md new file mode 100644 index 00000000..8b4a9af3 --- /dev/null +++ b/src/it/sources-targetPath/target/test-classes/README.md @@ -0,0 +1,19 @@ + + +The purpose of this file is to verify that the directory +specified by `` is still deleted. diff --git a/src/it/sources-targetPath/verify.bsh b/src/it/sources-targetPath/verify.bsh new file mode 100644 index 00000000..5022ea2a --- /dev/null +++ b/src/it/sources-targetPath/verify.bsh @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +import java.io.File; + +return !(new File(basedir, "target").exists() || new File(basedir, "outside-target").exists()); diff --git a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java index dc531d83..090e8042 100644 --- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java +++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java @@ -20,13 +20,21 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.apache.maven.api.Project; +import org.apache.maven.api.ProjectScope; import org.apache.maven.api.Session; import org.apache.maven.api.di.Inject; +import org.apache.maven.api.model.Build; import org.apache.maven.api.plugin.Log; import org.apache.maven.api.plugin.MojoException; import org.apache.maven.api.plugin.annotations.Mojo; import org.apache.maven.api.plugin.annotations.Parameter; +import org.apache.maven.api.services.ProjectManager; /** * Goal which cleans the build. @@ -52,35 +60,32 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { public static final String FAST_MODE_DEFER = "defer"; + /** + * The logger where to send information about what the plugin is doing. + */ @Inject private Log logger; /** - * This is where build results go. + * The directory where build results went. */ @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true) private Path directory; /** - * This is where compiled classes go. + * The directory where compiled main classes went. This is usually a sub-directory + * of {@link #directory}, but could be configured by the user to another location. */ @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true, required = true) private Path outputDirectory; /** - * This is where compiled test classes go. + * The directory where compiled test classes went. This is usually a sub-directory + * of {@link #directory}, but could be configured by the user to another location. */ @Parameter(defaultValue = "${project.build.testOutputDirectory}", readonly = true, required = true) private Path testOutputDirectory; - /** - * This is where the site plugin generates its pages. - * - * @since 2.1.1 - */ - @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true, required = true) - private Path reportDirectory; - /** * Sets whether the plugin runs in verbose mode. As of plugin version 2.3, the default value is derived from Maven's * global debug flag (compare command line switch {@code -X}). @@ -218,15 +223,34 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { @Parameter(property = "maven.clean.fastMode", defaultValue = FAST_MODE_BACKGROUND) private String fastMode; + /** + * The current session. May be null during some tests. + */ @Inject private Session session; /** - * Deletes file-sets in the following project build directory order: (source) directory, output directory, test - * directory, report directory, and then the additional file-sets. + * The current project instance. + */ + @Inject + private Project project; + + /** + * Deletes build directories and file-sets. + * Directories are deleted in the following order: * - * @throws MojoException When a directory failed to get deleted. - * @see org.apache.maven.api.plugin.Mojo#execute() + *
    + *
  1. Build directory ({@code ${project.build.directory}})
  2. + *
  3. Main classes directory ({@code ${project.build.outputDirectory}})
  4. + *
  5. Test classes directory ({@code ${project.build.testOutputDirectory}})
  6. + *
  7. Directories specified in the {@code } child of {@code } elements
  8. + *
  9. Additional file-sets
  10. + *
+ * + * Redundant directories are omitted. For example, the main classes directory will be skipped + * in the usual case where it is a sub-directory of the build directory. + * + * @throws MojoException if a directory cannot be deleted */ @Override public void execute() { @@ -263,9 +287,7 @@ public void execute() { session, logger, isVerbose(), fastDir, fastMode, followSymLinks, force, failOnError, retryOnError); try { for (Path directoryItem : getDirectories()) { - if (directoryItem != null) { - cleaner.delete(directoryItem); - } + cleaner.delete(directoryItem); } if (filesets != null) { for (Fileset fileset : filesets) { @@ -290,16 +312,53 @@ private boolean isVerbose() { } /** - * Gets the directories to clean (if any). The returned array may contain null entries. - * - * @return The directories to clean or an empty array if none, never {@code null}. + * {@return the default directories to clean, or an empty list if none} + * The list includes the directories specified in both the Maven 3 and Maven 4 ways. + * Null items and redundant directories (children of other items) are omitted. */ - private Path[] getDirectories() { - Path[] directories; + private List getDirectories() { if (excludeDefaultDirectories) { - directories = new Path[0]; - } else { - directories = new Path[] {directory, outputDirectory, testOutputDirectory, reportDirectory}; + return List.of(); + } + + // Directories declared in the Maven 3 way. + var directories = new ArrayList(Arrays.asList(directory, outputDirectory, testOutputDirectory)); + + // Directories declared in the Maven 4 way, with elements. + if (project != null && session != null) { + ProjectManager projectManager = session.getService(ProjectManager.class); + if (projectManager != null) { + final Build build = project.getBuild(); + projectManager.getSourceRoots(project).forEach((source) -> { + source.targetPath().ifPresent((targetPath) -> { + // TODO: we can replace by `Project.getOutputDirectory(scope)` if MVN-11322 is merged. + String base; + ProjectScope scope = source.scope(); + if (scope == ProjectScope.MAIN) { + base = build.getOutputDirectory(); + } else if (scope == ProjectScope.TEST) { + base = build.getTestOutputDirectory(); + } else { + base = build.getDirectory(); + } + Path dir = project.getBasedir().resolve(base); + directories.add(dir.resolve(targetPath)); + }); + }); + } + } + /* + * Remove children of included parents. In the common case where the first element in the list is + * the parent of all other directories, these loops will do only one iteration over all elements. + */ + directories.removeIf(Objects::isNull); + for (int i = 0; i < directories.size(); i++) { + final Path prefix = directories.get(i); + for (int j = directories.size(); --j >= 0; ) { + if (j != i && directories.get(j).startsWith(prefix)) { + directories.remove(j); // Keep only the base directories. + } + } } return directories; } From 9c120700ea6782f0886f63fbea7f71e32bf75d66 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Fri, 7 Nov 2025 08:47:06 +0100 Subject: [PATCH 74/90] JUnit Jupiter best practices (#279) Co-authored-by: Moderne --- .../maven/plugins/clean/CleanMojoTest.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java index 967edcea..3f0f7cd4 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java @@ -51,7 +51,7 @@ * Test the clean mojo. */ @MojoTest -public class CleanMojoTest { +class CleanMojoTest { private final Log log = mock(Log.class); @@ -63,7 +63,7 @@ public class CleanMojoTest { @Test @Basedir("${basedir}/target/test-classes/unit/basic-clean-test") @InjectMojo(goal = "clean") - public void testBasicClean(CleanMojo mojo) throws Exception { + void basicClean(CleanMojo mojo) throws Exception { mojo.execute(); assertFalse(checkExists(getBasedir() + "/buildDirectory"), "Directory exists"); @@ -79,7 +79,7 @@ public void testBasicClean(CleanMojo mojo) throws Exception { @Test @Basedir("${basedir}/target/test-classes/unit/nested-clean-test") @InjectMojo(goal = "clean") - public void testCleanNestedStructure(CleanMojo mojo) throws Exception { + void cleanNestedStructure(CleanMojo mojo) throws Exception { mojo.execute(); assertFalse(checkExists(getBasedir() + "/target")); @@ -96,7 +96,7 @@ public void testCleanNestedStructure(CleanMojo mojo) throws Exception { @Test @Basedir("${basedir}/target/test-classes/unit/empty-clean-test") @InjectMojo(goal = "clean") - public void testCleanEmptyDirectories(CleanMojo mojo) throws Exception { + void cleanEmptyDirectories(CleanMojo mojo) throws Exception { mojo.execute(); assertTrue(checkExists(getBasedir() + "/testDirectoryStructure")); @@ -113,7 +113,7 @@ public void testCleanEmptyDirectories(CleanMojo mojo) throws Exception { @Test @Basedir("${basedir}/target/test-classes/unit/fileset-clean-test") @InjectMojo(goal = "clean") - public void testFilesetsClean(CleanMojo mojo) throws Exception { + void filesetsClean(CleanMojo mojo) throws Exception { mojo.execute(); // fileset 1 @@ -139,7 +139,7 @@ public void testFilesetsClean(CleanMojo mojo) throws Exception { @Test @Basedir("${basedir}/target/test-classes/unit/invalid-directory-test") @InjectMojo(goal = "clean") - public void testCleanInvalidDirectory(CleanMojo mojo) throws Exception { + void cleanInvalidDirectory(CleanMojo mojo) throws Exception { assertThrows(MojoException.class, mojo::execute, "Should fail to delete a file treated as a directory"); } @@ -151,7 +151,7 @@ public void testCleanInvalidDirectory(CleanMojo mojo) throws Exception { @Test @Basedir("${basedir}/target/test-classes/unit/missing-directory-test") @InjectMojo(goal = "clean") - public void testMissingDirectory(CleanMojo mojo) throws Exception { + void missingDirectory(CleanMojo mojo) throws Exception { mojo.execute(); assertFalse(checkExists(getBasedir() + "/does-not-exist")); @@ -169,7 +169,7 @@ public void testMissingDirectory(CleanMojo mojo) throws Exception { @EnabledOnOs(OS.WINDOWS) @Basedir("${basedir}/target/test-classes/unit/locked-file-test") @InjectMojo(goal = "clean") - public void testCleanLockedFile(CleanMojo mojo) throws Exception { + void cleanLockedFile(CleanMojo mojo) throws Exception { File f = new File(getBasedir(), "buildDirectory/file.txt"); try (FileChannel channel = new RandomAccessFile(f, "rw").getChannel(); FileLock ignored = channel.lock()) { @@ -189,7 +189,7 @@ public void testCleanLockedFile(CleanMojo mojo) throws Exception { @EnabledOnOs(OS.WINDOWS) @Basedir("${basedir}/target/test-classes/unit/locked-file-test") @InjectMojo(goal = "clean") - public void testCleanLockedFileWithNoError(CleanMojo mojo) throws Exception { + void cleanLockedFileWithNoError(CleanMojo mojo) throws Exception { setVariableValueToObject(mojo, "failOnError", Boolean.FALSE); assertNotNull(mojo); @@ -206,7 +206,7 @@ public void testCleanLockedFileWithNoError(CleanMojo mojo) throws Exception { */ @Test @EnabledOnOs(OS.WINDOWS) - public void testFollowLinksWithWindowsJunction() throws Exception { + void followLinksWithWindowsJunction() throws Exception { testSymlink((link, target) -> { Process process = new ProcessBuilder() .directory(link.getParent().toFile()) @@ -228,7 +228,7 @@ public void testFollowLinksWithWindowsJunction() throws Exception { */ @Test @DisabledOnOs(OS.WINDOWS) - public void testFollowLinksWithSymLinkOnPosix() throws Exception { + void followLinksWithSymLinkOnPosix() throws Exception { testSymlink((link, target) -> { try { Files.createSymbolicLink(link, target); From 5d1147d06315bde15a53a9d05b4bbf1bb4ff8d29 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sat, 15 Nov 2025 12:37:35 +0100 Subject: [PATCH 75/90] Upgrade from Maven 4.0.0-rc-4 to 4.0.0-rc-5 (#283) --- .github/workflows/maven-verify.yml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 1e682104..eca8804b 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -27,4 +27,4 @@ jobs: uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v4 with: maven4-build: true - maven4-version: '4.0.0-rc-4' # the same as used in project + maven4-version: '4.0.0-rc-5' # the same as used in project diff --git a/pom.xml b/pom.xml index d4fcc5b6..7edef7da 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ under the License. - 4.0.0-rc-4 + 4.0.0-rc-5 17 33.4.6-jre 6.0.0 From 00107cb437b722cef2ffe4d9b57619bc35504c6f Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Thu, 16 Oct 2025 20:28:38 +0200 Subject: [PATCH 76/90] Replace the `PathSelector` class by the implementation provided by Maven core. --- .../apache/maven/plugins/clean/CleanMojo.java | 18 +- .../apache/maven/plugins/clean/Cleaner.java | 112 ++- .../apache/maven/plugins/clean/Fileset.java | 53 +- .../maven/plugins/clean/PathSelector.java | 640 ------------------ .../maven/plugins/clean/CleanMojoTest.java | 66 +- .../maven/plugins/clean/CleanerTest.java | 21 +- 6 files changed, 185 insertions(+), 725 deletions(-) delete mode 100644 src/main/java/org/apache/maven/plugins/clean/PathSelector.java diff --git a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java index 090e8042..9ac88c4e 100644 --- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java +++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java @@ -34,6 +34,7 @@ import org.apache.maven.api.plugin.MojoException; import org.apache.maven.api.plugin.annotations.Mojo; import org.apache.maven.api.plugin.annotations.Parameter; +import org.apache.maven.api.services.PathMatcherFactory; import org.apache.maven.api.services.ProjectManager; /** @@ -235,6 +236,12 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { @Inject private Project project; + /** + * The service to use for creating include and exclude filters. + */ + @Inject + private PathMatcherFactory matcherFactory; + /** * Deletes build directories and file-sets. * Directories are deleted in the following order: @@ -284,7 +291,16 @@ public void execute() { + FAST_MODE_BACKGROUND + "', '" + FAST_MODE_AT_END + "' and '" + FAST_MODE_DEFER + "'."); } final var cleaner = new Cleaner( - session, logger, isVerbose(), fastDir, fastMode, followSymLinks, force, failOnError, retryOnError); + session, + matcherFactory, + logger, + isVerbose(), + fastDir, + fastMode, + followSymLinks, + force, + failOnError, + retryOnError); try { for (Path directoryItem : getDirectories()) { cleaner.delete(directoryItem); diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java index 2ad8276e..2cfce014 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -25,14 +25,15 @@ import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; +import java.nio.file.NotDirectoryException; import java.nio.file.Path; +import java.nio.file.PathMatcher; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.DosFileAttributeView; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayDeque; -import java.util.Arrays; import java.util.BitSet; import java.util.Deque; import java.util.EnumSet; @@ -50,6 +51,7 @@ import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.plugin.Log; +import org.apache.maven.api.services.PathMatcherFactory; /** * Cleans directories. @@ -107,11 +109,23 @@ final class Cleaner implements FileVisitor { private final String fastMode; /** - * Combination of includes and excludes path matchers. - * A {@code null} value means to include everything. + * The service to use for creating include and exclude filters. + * Used for setting a value to {@link #fileMatcher} and {@link #directoryMatcher}. */ - @Nullable - private PathSelector selector; + @Nonnull + private final PathMatcherFactory matcherFactory; + + /** + * Combination of includes and excludes path matchers applied on files. + */ + @Nonnull + private PathMatcher fileMatcher; + + /** + * Combination of includes and excludes path matchers applied on directories. + */ + @Nonnull + private PathMatcher directoryMatcher; /** * Whether the base directory is excluded from the set of directories to delete. @@ -120,6 +134,14 @@ final class Cleaner implements FileVisitor { */ private boolean isBaseDirectoryExcluded; + /** + * Whether to follow symbolic links while deleting files from the directories. + * This value is specified by the MOJO plugin configuration, + * but can be overridden by {@link Fileset}. + * + * @see CleanMojo#followSymLinks + * @see Fileset#followSymlinks + */ private boolean followSymlinks; /** @@ -168,29 +190,35 @@ final class Cleaner implements FileVisitor { /** * Creates a new cleaner. + * By default, the cleaner has no include or exclude filters, + * does not exclude the base directory and does not follow symbolic links. + * These properties can be modified by {@link #delete(Fileset)}. * - * @param session the Maven session to be used - * @param logger the logger to use - * @param verbose whether to perform verbose logging - * @param fastDir the explicit configured directory or to be deleted in fast mode - * @param fastMode the fast deletion mode - * @param followSymlinks whether to follow symlinks - * @param force whether to force the deletion of read-only files - * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted - * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed + * @param session the Maven session to be used + * @param matcherFactory the service to use for creating include and exclude filters. + * @param logger the logger to use + * @param verbose whether to perform verbose logging + * @param fastDir the explicit configured directory or to be deleted in fast mode + * @param fastMode the fast deletion mode + * @param followSymlinks whether to follow symlinks + * @param force whether to force the deletion of read-only files + * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted + * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed */ @SuppressWarnings("checkstyle:ParameterNumber") Cleaner( - @Nonnull Session session, + @Nullable Session session, + @Nonnull PathMatcherFactory matcherFactory, @Nonnull Log logger, boolean verbose, - @Nonnull Path fastDir, + @Nullable Path fastDir, @Nonnull String fastMode, boolean followSymlinks, boolean force, boolean failOnError, boolean retryOnError) { this.session = session; + this.matcherFactory = matcherFactory; this.logger = logger; this.verbose = verbose; this.fastDir = fastDir; @@ -201,32 +229,38 @@ final class Cleaner implements FileVisitor { this.retryOnError = retryOnError; listDeletedFiles = verbose ? logger.isInfoEnabled() : logger.isDebugEnabled(); nonEmptyDirectoryLevels = new BitSet(); + fileMatcher = matcherFactory.includesAll(); + directoryMatcher = fileMatcher; } /** * Deletes the specified fileset. + * This method modifies the include and exclude filters, + * whether to exclude the base directory and whether to follow symbolic links. * - * @param fileset the fileset to delete, must not be {@code null} - * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true} + * @param fileset the fileset to delete + * @throws IOException if a file/directory could not be deleted and {@link #failOnError} is {@code true} */ public void delete(@Nonnull Fileset fileset) throws IOException { - selector = new PathSelector( - fileset.getDirectory(), - Arrays.asList(fileset.getIncludes()), - Arrays.asList(fileset.getExcludes()), - fileset.isUseDefaultExcludes()); - if (selector.isEmpty()) { - selector = null; - } + fileMatcher = matcherFactory.createPathMatcher( + fileset.getDirectory(), fileset.getIncludes(), fileset.getExcludes(), fileset.useDefaultExcludes()); + directoryMatcher = matcherFactory.deriveDirectoryMatcher(fileMatcher); isBaseDirectoryExcluded = fileset.isBaseDirectoryExcluded(); - followSymlinks = fileset.isFollowSymlinks(); + followSymlinks = fileset.followSymlinks(); delete(fileset.getDirectory()); } /** - * Deletes the specified directory and its contents. + * Deletes the specified directory and its contents using the current configuration. * Non-existing directories will be silently ignored. * + *

Configuration

+ * The behavior of this method depends on the {@code Cleaner} configuration. + * Some configuration can be modified by calls to {@link #delete(Fileset)}. + * Therefore, for deleting files with the default configuration (no include + * or exclude filters, not following symbolic links), this method should be + * invoked first. + * * @param basedir the directory to delete, must not be {@code null} * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true} */ @@ -238,17 +272,17 @@ public void delete(@Nonnull Path basedir) throws IOException { } return; } - throw new IOException("Invalid base directory " + basedir); + throw new NotDirectoryException("Invalid base directory " + basedir); } if (logger.isInfoEnabled()) { - logger.info("Deleting " + basedir + (selector != null ? " (" + selector + ")" : "")); + logger.info("Deleting " + basedir + (isClearAll() ? "" : " (" + fileMatcher + ')')); } var options = EnumSet.noneOf(FileVisitOption.class); if (followSymlinks) { options.add(FileVisitOption.FOLLOW_LINKS); basedir = getCanonicalPath(basedir, null); } - if (selector == null && !followSymlinks && fastDir != null && session != null) { + if (isClearAll() && !followSymlinks && fastDir != null && session != null) { // If anything wrong happens, we'll just use the usual deletion mechanism if (fastDelete(basedir)) { return; @@ -257,6 +291,14 @@ public void delete(@Nonnull Path basedir) throws IOException { Files.walkFileTree(basedir, options, Integer.MAX_VALUE, this); } + /** + * {@return whether {@link #fileMatcher} matches all files}. + * This is a required condition for allowing the use of {@link #fastDelete(Path)}. + */ + private boolean isClearAll() { + return fileMatcher == matcherFactory.includesAll(); + } + private boolean fastDelete(Path baseDir) { // Handle the case where we use ${maven.multiModuleProjectDirectory}/target/.clean for example if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) { @@ -320,7 +362,7 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th visitFile(dir, attrs); return FileVisitResult.SKIP_SUBTREE; } - if (selector == null || selector.couldHoldSelected(dir)) { + if (directoryMatcher.matches(dir)) { nonEmptyDirectoryLevels.clear(++currentDepth); return FileVisitResult.CONTINUE; } else { @@ -336,7 +378,7 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th */ @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - if ((selector == null || selector.matches(file)) && tryDelete(file)) { + if (fileMatcher.matches(file) && tryDelete(file)) { if (listDeletedFiles) { logDelete(file, attrs); } @@ -377,8 +419,8 @@ public FileVisitResult postVisitDirectory(Path dir, IOException failure) throws canDelete = false; } else { canDelete &= (currentDepth != 0 || !isBaseDirectoryExcluded); - if (canDelete && selector != null) { - canDelete = selector.matches(dir); + if (canDelete) { + canDelete = fileMatcher.matches(dir); } } if (canDelete && tryDelete(dir)) { diff --git a/src/main/java/org/apache/maven/plugins/clean/Fileset.java b/src/main/java/org/apache/maven/plugins/clean/Fileset.java index de497f8c..b55dcc46 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Fileset.java +++ b/src/main/java/org/apache/maven/plugins/clean/Fileset.java @@ -19,10 +19,12 @@ package org.apache.maven.plugins.clean; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; /** * Customizes the string representation of - * {@code org.apache.maven.shared.model.fileset.FileSet} to return the + * {@code org.apache.maven.api.model.FileSet} to return the * included and excluded files from the file-set's directory. Specifically, * "file-set: [directory] (included: [included files], * excluded: [excluded files])" @@ -54,17 +56,37 @@ public Path getDirectory() { } /** - * {@return the patterns of the file to include, or an empty array if unspecified}. + * {@return the patterns of the file to include, or an empty list if unspecified}. */ - public String[] getIncludes() { - return (includes != null) ? includes : new String[0]; + public List getIncludes() { + return listWithoutNull(includes); } /** - * {@return the patterns of the file to exclude, or an empty array if unspecified}. + * {@return the patterns of the file to exclude, or an empty list if unspecified}. */ - public String[] getExcludes() { - return (excludes != null) ? excludes : new String[0]; + public List getExcludes() { + return listWithoutNull(excludes); + } + + /** + * {@return the content of the given array without null elements}. + * The existence of null elements has been observed in practice, + * not sure where they come from. + * + * @param patterns the {@link #includes} or {@link #excludes} array, or {@code null} if none + */ + private static List listWithoutNull(String[] patterns) { + if (patterns == null) { + return List.of(); + } + var list = new ArrayList(patterns.length); + for (String pattern : patterns) { + if (pattern != null) { + list.add(pattern); + } + } + return list; } /** @@ -86,14 +108,14 @@ public boolean isBaseDirectoryExcluded() { /** * {@return whether to follow symbolic links}. */ - public boolean isFollowSymlinks() { + public boolean followSymlinks() { return followSymlinks; } /** * {@return whether to use a default set of excludes}. */ - public boolean isUseDefaultExcludes() { + public boolean useDefaultExcludes() { return useDefaultExcludes; } @@ -105,15 +127,12 @@ public boolean isUseDefaultExcludes() { * @param label label identifying the array of elements to add * @param patterns the elements to append, or {@code null} if none */ - static void append(StringBuilder buffer, String label, String[] patterns) { + private static void append(StringBuilder buffer, String label, List patterns) { buffer.append(label).append(": ["); - if (patterns != null) { - for (int i = 0; i < patterns.length; i++) { - if (i != 0) { - buffer.append(", "); - } - buffer.append(patterns[i]); - } + String separator = ""; + for (String pattern : patterns) { + buffer.append(separator).append(pattern); + separator = ", "; } buffer.append(']'); } diff --git a/src/main/java/org/apache/maven/plugins/clean/PathSelector.java b/src/main/java/org/apache/maven/plugins/clean/PathSelector.java deleted file mode 100644 index 2d810829..00000000 --- a/src/main/java/org/apache/maven/plugins/clean/PathSelector.java +++ /dev/null @@ -1,640 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 - * - * http://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.apache.maven.plugins.clean; - -import java.io.File; -import java.nio.file.FileSystem; -import java.nio.file.Path; -import java.nio.file.PathMatcher; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -/** - * Determines whether a path is selected according to include/exclude patterns. - * The pathnames used for method parameters will be relative to some base directory - * and use {@code '/'} as separator, regardless of the hosting operating system. - * - *

Syntax

- * If a pattern contains the {@code ':'} character and the prefix before is longer than 1 character, - * then that pattern is given verbatim to {@link FileSystem#getPathMatcher(String)}, which interprets - * the part before {@code ':'} as the syntax (usually {@code "glob"} or {@code "regex"}). - * If a pattern does not contain the {@code ':'} character, or if the prefix is one character long - * (interpreted as a Windows drive), then the syntax defaults to a reproduction of the Maven 3 behavior. - * This is implemented as the {@code "glob"} syntax with the following modifications: - * - *
    - *
  • The platform-specific separator ({@code '\\'} on Windows) is replaced by {@code '/'}. - * Note that it means that the backslash cannot be used for escaping characters.
  • - *
  • Trailing {@code "/"} is completed as {@code "/**"}.
  • - *
  • The {@code "**"} wildcard means "0 or more directories" instead of "1 or more directories". - * This is implemented by adding variants of the pattern without the {@code "**"} wildcard.
  • - *
  • Bracket characters [ ] and { } are escaped.
  • - *
  • On Unix only, the escape character {@code '\\'} is itself escaped.
  • - *
- * - * If above changes are not desired, put an explicit {@code "glob:"} prefix before the pattern. - * Note that putting such a prefix is recommended anyway for better performances. - * - * @author Benjamin Bentmann - * @author Martin Desruisseaux - * - * @see java.nio.file.FileSystem#getPathMatcher(String) - */ -final class PathSelector implements PathMatcher { - /** - * Patterns which should be excluded by default, like SCM files. - * - *

Source: this list is copied from {@code plexus-utils-4.0.2} (released in - * September 23, 2024), class {@code org.codehaus.plexus.util.AbstractScanner}.

- */ - private static final List DEFAULT_EXCLUDES = List.of( - // Miscellaneous typical temporary files - "**/*~", - "**/#*#", - "**/.#*", - "**/%*%", - "**/._*", - - // CVS - "**/CVS", - "**/CVS/**", - "**/.cvsignore", - - // RCS - "**/RCS", - "**/RCS/**", - - // SCCS - "**/SCCS", - "**/SCCS/**", - - // Visual SourceSafe - "**/vssver.scc", - - // MKS - "**/project.pj", - - // Subversion - "**/.svn", - "**/.svn/**", - - // Arch - "**/.arch-ids", - "**/.arch-ids/**", - - // Bazaar - "**/.bzr", - "**/.bzr/**", - - // SurroundSCM - "**/.MySCMServerInfo", - - // Mac - "**/.DS_Store", - - // Serena Dimensions Version 10 - "**/.metadata", - "**/.metadata/**", - - // Mercurial - "**/.hg", - "**/.hg/**", - - // git - "**/.git", - "**/.git/**", - "**/.gitignore", - - // BitKeeper - "**/BitKeeper", - "**/BitKeeper/**", - "**/ChangeSet", - "**/ChangeSet/**", - - // darcs - "**/_darcs", - "**/_darcs/**", - "**/.darcsrepo", - "**/.darcsrepo/**", - "**/-darcs-backup*", - "**/.darcs-temp-mail"); - - /** - * Maximum number of characters of the prefix before {@code ':'} for handling as a Maven syntax. - */ - private static final int MAVEN_SYNTAX_THRESHOLD = 1; - - /** - * The default syntax to use if none was specified. Note that when this default syntax is applied, - * the user-provided pattern get some changes as documented in class Javadoc. - */ - private static final String DEFAULT_SYNTAX = "glob:"; - - /** - * Characters having a special meaning in the glob syntax. - * - * @see FileSystem#getPathMatcher(String) - */ - private static final String SPECIAL_CHARACTERS = "*?[]{}\\"; - - /** - * A path matcher which accepts all files. - * - * @see #simplify() - */ - private static final PathMatcher INCLUDES_ALL = (path) -> true; - - /** - * String representations of the normalized include filters. - * Each pattern shall be prefixed by its syntax, which is {@value #DEFAULT_SYNTAX} by default. - * An empty array means to include all files. - * - * @see #toString() - */ - private final String[] includePatterns; - - /** - * String representations of the normalized exclude filters. - * Each pattern shall be prefixed by its syntax. If no syntax is specified, - * the default is a Maven 3 syntax similar, but not identical, to {@value #DEFAULT_SYNTAX}. - * This array may be longer or shorter than the user-supplied excludes, depending on whether - * default excludes have been added and whether some unnecessary excludes have been omitted. - * - * @see #toString() - */ - private final String[] excludePatterns; - - /** - * The matcher for includes. The length of this array is equal to {@link #includePatterns} array length. - * An empty array means to include all files. - */ - private final PathMatcher[] includes; - - /** - * The matcher for excludes. The length of this array is equal to {@link #excludePatterns} array length. - */ - private final PathMatcher[] excludes; - - /** - * The matcher for all directories to include. This array includes the parents of all those directories, - * because they need to be accepted before we can walk to the sub-directories. - * This is an optimization for skipping whole directories when possible. - * An empty array means to include all directories. - */ - private final PathMatcher[] dirIncludes; - - /** - * The matcher for directories to exclude. This array does not include the parent directories, - * because they may contain other sub-trees that need to be included. - * This is an optimization for skipping whole directories when possible. - */ - private final PathMatcher[] dirExcludes; - - /** - * The base directory. All files will be relativized to that directory before to be matched. - */ - private final Path baseDirectory; - - /** - * Whether paths must be relativized before to be given to a matcher. If {@code true}, then every paths - * will be made relative to {@link #baseDirectory} for allowing patterns like {@code "foo/bar/*.java"} - * to work. As a slight optimization, we can skip this step if all patterns start with {@code "**"}. - */ - private final boolean needRelativize; - - /** - * Creates a new selector from the given includes and excludes. - * - * @param directory the base directory of the files to filter - * @param includes the patterns of the files to include, or null or empty for including all files - * @param excludes the patterns of the files to exclude, or null or empty for no exclusion - * @param useDefaultExcludes whether to augment the excludes with a default set of SCM patterns - */ - PathSelector(Path directory, Collection includes, Collection excludes, boolean useDefaultExcludes) { - includePatterns = normalizePatterns(includes, false); - excludePatterns = normalizePatterns(effectiveExcludes(excludes, includePatterns, useDefaultExcludes), true); - baseDirectory = directory; - FileSystem fs = directory.getFileSystem(); - this.includes = matchers(fs, includePatterns); - this.excludes = matchers(fs, excludePatterns); - dirIncludes = matchers(fs, directoryPatterns(includePatterns, false)); - dirExcludes = matchers(fs, directoryPatterns(excludePatterns, true)); - needRelativize = needRelativize(includePatterns) || needRelativize(excludePatterns); - } - - /** - * Returns the given array of excludes, optionally expanded with a default set of excludes, - * then with unnecessary excludes omitted. An unnecessary exclude is an exclude which will never - * match a file because there is no include which would accept a file that could match the exclude. - * For example, if the only include is {@code "*.java"}, then the "**/project.pj", - * "**/.DS_Store" and other excludes will never match a file and can be omitted. - * Because the list of {@linkplain #DEFAULT_EXCLUDES default excludes} contains many elements, - * removing unnecessary excludes can reduce a lot the number of matches tested on each source file. - * - *

Implementation note

- * The removal of unnecessary excludes is done on a best effort basis. The current implementation - * compares only the prefixes and suffixes of each pattern, keeping the pattern in case of doubt. - * This is not bad, but it does not remove all unnecessary patterns. It would be possible to do - * better in the future if benchmarking suggests that it would be worth the effort. - * - * @param excludes the user-specified excludes, potentially not yet converted to glob syntax - * @param includes the include patterns converted to glob syntax - * @param useDefaultExcludes whether to expand user exclude with the set of default excludes - * @return the potentially expanded or reduced set of excludes to use - */ - private static Collection effectiveExcludes( - Collection excludes, final String[] includes, final boolean useDefaultExcludes) { - if (excludes == null || excludes.isEmpty()) { - if (useDefaultExcludes) { - excludes = new ArrayList<>(DEFAULT_EXCLUDES); - } else { - return List.of(); - } - } else { - excludes = new ArrayList<>(excludes); - excludes.removeIf(Objects::isNull); - if (useDefaultExcludes) { - excludes.addAll(DEFAULT_EXCLUDES); - } - } - if (includes.length == 0) { - return excludes; - } - /* - * Get the prefixes and suffixes of all includes, stopping at the first special character. - * Redundant prefixes and suffixes are omitted. - */ - var prefixes = new String[includes.length]; - var suffixes = new String[includes.length]; - for (int i = 0; i < includes.length; i++) { - String include = includes[i]; - if (!include.startsWith(DEFAULT_SYNTAX)) { - return excludes; // Do not filter if at least one pattern is too complicated. - } - include = include.substring(DEFAULT_SYNTAX.length()); - prefixes[i] = prefixOrSuffix(include, false); - suffixes[i] = prefixOrSuffix(include, true); - } - prefixes = sortByLength(prefixes, false); - suffixes = sortByLength(suffixes, true); - /* - * Keep only the exclude which start with one of the prefixes and end with one of the suffixes. - * Note that a prefix or suffix may be the empty string, which match everything. - */ - final Iterator it = excludes.iterator(); - nextExclude: - while (it.hasNext()) { - final String exclude = it.next(); - final int s = exclude.indexOf(':'); - if (s <= MAVEN_SYNTAX_THRESHOLD || exclude.startsWith(DEFAULT_SYNTAX)) { - if (cannotMatch(exclude, prefixes, false) || cannotMatch(exclude, suffixes, true)) { - it.remove(); - } - } - } - return excludes; - } - - /** - * Returns the maximal amount of ordinary characters at the beginning or end of the given pattern. - * The prefix or suffix stops at the first {@linkplain #SPECIAL_CHARACTERS special character}. - * - * @param include the pattern for which to get a prefix or suffix without special character - * @param suffix {@code false} if a prefix is desired, or {@code true} if a suffix is desired - */ - private static String prefixOrSuffix(final String include, boolean suffix) { - int s = suffix ? -1 : include.length(); - for (int i = SPECIAL_CHARACTERS.length(); --i >= 0; ) { - char c = SPECIAL_CHARACTERS.charAt(i); - if (suffix) { - s = Math.max(s, include.lastIndexOf(c)); - } else { - int p = include.indexOf(c); - if (p >= 0 && p < s) { - s = p; - } - } - } - return suffix ? include.substring(s + 1) : include.substring(0, s); - } - - /** - * Returns {@code true} if the given exclude cannot match any include patterns. - * In case of doubt, returns {@code false}. - * - * @param exclude the exclude pattern to test - * @param fragments the prefixes or suffixes (fragments without special characters) of the includes - * @param suffix {@code false} if the specified fragments are prefixes, {@code true} if they are suffixes - * @return {@code true} if it is certain that the exclude pattern cannot match, or {@code false} in case of doubt - */ - private static boolean cannotMatch(String exclude, final String[] fragments, final boolean suffix) { - exclude = prefixOrSuffix(exclude, suffix); - for (String fragment : fragments) { - int fg = fragment.length(); - int ex = exclude.length(); - int length = Math.min(fg, ex); - if (suffix) { - fg -= length; - ex -= length; - } else { - fg = 0; - ex = 0; - } - if (exclude.regionMatches(ex, fragment, fg, length)) { - return false; - } - } - return true; - } - - /** - * Sorts the given patterns by their length. The main intent is to have the empty string first, - * while will cause the loops testing for prefixes and suffixes to stop almost immediately. - * Short prefixes or suffixes are also more likely to be matched. - * - * @param fragments the fragments to sort in-place - * @param suffix {@code false} if the specified fragments are prefixes, {@code true} if they are suffixes - * @return the given array, or a smaller array if some fragments were discarded because redundant - */ - private static String[] sortByLength(final String[] fragments, final boolean suffix) { - Arrays.sort(fragments, (s1, s2) -> s1.length() - s2.length()); - int count = 0; - /* - * Simplify the array of prefixes or suffixes by removing all redundant elements. - * An element is redundant if there is a shorter prefix or suffix with the same characters. - */ - nextBase: - for (String fragment : fragments) { - for (int i = count; --i >= 0; ) { - String base = fragments[i]; - if (suffix ? fragment.endsWith(base) : fragment.startsWith(base)) { - continue nextBase; // Skip this fragment - } - } - fragments[count++] = fragment; - } - return (fragments.length == count) ? fragments : Arrays.copyOf(fragments, count); - } - - /** - * Returns the given array of patterns with path separator normalized to {@code '/'}. - * Null or empty patterns are ignored, and duplications are removed. - * - * @param patterns the patterns to normalize - * @param excludes whether the patterns are exclude patterns - * @return normalized patterns without null, empty or duplicated patterns - */ - private static String[] normalizePatterns(final Collection patterns, final boolean excludes) { - if (patterns == null || patterns.isEmpty()) { - return new String[0]; - } - // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19. - final var normalized = new LinkedHashSet(patterns.size()); - for (String pattern : patterns) { - if (pattern != null && !pattern.isEmpty()) { - if (pattern.indexOf(':') <= MAVEN_SYNTAX_THRESHOLD) { - pattern = pattern.replace(File.separatorChar, '/'); - if (pattern.endsWith("/")) { - pattern += "**"; - } - // Following are okay only when "**" means "0 or more directories". - while (pattern.endsWith("/**/**")) { - pattern = pattern.substring(0, pattern.length() - 3); - } - while (pattern.startsWith("**/**/")) { - pattern = pattern.substring(3); - } - pattern = pattern.replace("/**/**/", "/**/"); - pattern = pattern.replace("\\", "\\\\") - .replace("[", "\\[") - .replace("]", "\\]") - .replace("{", "\\{") - .replace("}", "\\}"); - normalized.add(DEFAULT_SYNTAX + pattern); - /* - * If the pattern starts or ends with "**", Java GLOB expects a directory level at - * that location while Maven seems to consider that "**" can mean "no directory". - * Add another pattern for reproducing this effect. - */ - addPatternsWithOneDirRemoved(normalized, pattern, 0); - } else { - normalized.add(pattern); - } - } - } - return simplify(normalized, excludes); - } - - /** - * Adds all variants of the given pattern with {@code **} removed. - * This is used for simulating the Maven behavior where {@code "**} may match zero directory. - * Tests suggest that we need an explicit GLOB pattern with no {@code "**"} for matching an absence of directory. - * - * @param patterns where to add the derived patterns - * @param pattern the pattern for which to add derived forms, without the "glob:" syntax prefix - * @param end should be 0 (reserved for recursive invocations of this method) - */ - private static void addPatternsWithOneDirRemoved(final Set patterns, final String pattern, int end) { - final int length = pattern.length(); - int start; - while ((start = pattern.indexOf("**", end)) >= 0) { - end = start + 2; // 2 is the length of "**". - if (end < length) { - if (pattern.charAt(end) != '/') { - continue; - } - if (start == 0) { - end++; // Ommit the leading slash if there is nothing before it. - } - } - if (start > 0 && pattern.charAt(--start) != '/') { - continue; - } - String reduced = pattern.substring(0, start) + pattern.substring(end); - patterns.add(DEFAULT_SYNTAX + reduced); - addPatternsWithOneDirRemoved(patterns, reduced, start); - } - } - - /** - * Applies some heuristic rules for simplifying the set of patterns, - * then returns the patterns as an array. - * - * @param patterns the patterns to simplify and return as an array - * @param excludes whether the patterns are exclude patterns - * @return the set content as an array, after simplification - */ - private static String[] simplify(Set patterns, boolean excludes) { - /* - * If the "**" pattern is present, it makes all other patterns useless. - * In the case of include patterns, an empty set means to include everything. - */ - if (patterns.remove("**")) { - patterns.clear(); - if (excludes) { - patterns.add("**"); - } - } - return patterns.toArray(String[]::new); - } - - /** - * Eventually adds the parent directory of the given patterns, without duplicated values. - * The patterns given to this method should have been normalized. - * - * @param patterns the normalized include or exclude patterns - * @param excludes whether the patterns are exclude patterns - * @return pattens of directories to include or exclude - */ - private static String[] directoryPatterns(final String[] patterns, final boolean excludes) { - // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19. - final var directories = new LinkedHashSet(patterns.length); - for (String pattern : patterns) { - if (pattern.startsWith(DEFAULT_SYNTAX)) { - if (excludes) { - if (pattern.endsWith("/**")) { - directories.add(pattern.substring(0, pattern.length() - 3)); - } - } else { - int s = pattern.indexOf(':'); - if (pattern.regionMatches(++s, "**/", 0, 3)) { - s = pattern.indexOf('/', s + 3); - if (s < 0) { - return new String[0]; // Pattern is "**", so we need to accept everything. - } - directories.add(pattern.substring(0, s)); - } - } - } - } - return simplify(directories, excludes); - } - - /** - * Returns {@code true} if at least one pattern requires path to be relativized before to be matched. - * - * @param patterns include or exclude patterns - * @return whether at least one pattern require relativization - */ - private static boolean needRelativize(String[] patterns) { - for (String pattern : patterns) { - if (!pattern.startsWith(DEFAULT_SYNTAX + "**/")) { - return true; - } - } - return false; - } - - /** - * Creates the path matchers for the given patterns. - * The syntax (usually {@value #DEFAULT_SYNTAX}) must be specified for each pattern. - */ - private static PathMatcher[] matchers(final FileSystem fs, final String[] patterns) { - final var matchers = new PathMatcher[patterns.length]; - for (int i = 0; i < patterns.length; i++) { - matchers[i] = fs.getPathMatcher(patterns[i]); - } - return matchers; - } - - /** - * {@return whether there is no include or exclude filters}. - * In such case, this {@code PathSelector} instance should be ignored. - */ - public boolean isEmpty() { - return (includes.length | excludes.length) == 0; - } - - /** - * {@return a potentially simpler matcher equivalent to this matcher}. - */ - @SuppressWarnings("checkstyle:MissingSwitchDefault") - public PathMatcher simplify() { - if (!needRelativize && excludes.length == 0) { - switch (includes.length) { - case 0: - return INCLUDES_ALL; - case 1: - return includes[0]; - } - } - return this; - } - - /** - * Determines whether a path is selected. - * This is true if the given file matches an include pattern and no exclude pattern. - * - * @param path the pathname to test, must not be {@code null} - * @return {@code true} if the given path is selected, {@code false} otherwise - */ - @Override - public boolean matches(Path path) { - if (needRelativize) { - path = baseDirectory.relativize(path); - } - return (includes.length == 0 || isMatched(path, includes)) - && (excludes.length == 0 || !isMatched(path, excludes)); - } - - /** - * {@return whether the given file matches according to one of the given matchers}. - */ - private static boolean isMatched(Path path, PathMatcher[] matchers) { - for (PathMatcher matcher : matchers) { - if (matcher.matches(path)) { - return true; - } - } - return false; - } - - /** - * Determines whether a directory could contain selected paths. - * - * @param directory the directory pathname to test, must not be {@code null} - * @return {@code true} if the given directory might contain selected paths, {@code false} if the - * directory will definitively not contain selected paths - */ - public boolean couldHoldSelected(Path directory) { - if (baseDirectory.equals(directory)) { - return true; - } - directory = baseDirectory.relativize(directory); - return (dirIncludes.length == 0 || isMatched(directory, dirIncludes)) - && (dirExcludes.length == 0 || !isMatched(directory, dirExcludes)); - } - - /** - * {@return a string representation for logging purposes}. - */ - @Override - public String toString() { - var buffer = new StringBuilder(); - Fileset.append(buffer, "includes", includePatterns); - Fileset.append(buffer.append(", "), "excludes", excludePatterns); - return buffer.toString(); - } -} diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java index 3f0f7cd4..c7935dea 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java @@ -34,6 +34,8 @@ import org.apache.maven.api.plugin.testing.Basedir; import org.apache.maven.api.plugin.testing.InjectMojo; import org.apache.maven.api.plugin.testing.MojoTest; +import org.apache.maven.api.services.PathMatcherFactory; +import org.apache.maven.impl.DefaultPathMatcherFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.EnabledOnOs; @@ -55,6 +57,15 @@ class CleanMojoTest { private final Log log = mock(Log.class); + /** + * The factory to use for creating patch matcher. + * The actual implementation is used rather than a mock because filtering is an important part + * of this plugin and is tedious to test. Therefore, it is hard to guarantee that the tests of + * {@code PathMatcherFactory} in Maven core are sufficient, and we want this plugin to test it + * more. + */ + private final PathMatcherFactory matcherFactory = new DefaultPathMatcherFactory(); + /** * Tests the simple removal of directories * @@ -66,9 +77,9 @@ class CleanMojoTest { void basicClean(CleanMojo mojo) throws Exception { mojo.execute(); - assertFalse(checkExists(getBasedir() + "/buildDirectory"), "Directory exists"); - assertFalse(checkExists(getBasedir() + "/buildOutputDirectory"), "Directory exists"); - assertFalse(checkExists(getBasedir() + "/buildTestDirectory"), "Directory exists"); + assertFalse(checkExists("buildDirectory"), "Directory exists"); + assertFalse(checkExists("buildOutputDirectory"), "Directory exists"); + assertFalse(checkExists("buildTestDirectory"), "Directory exists"); } /** @@ -82,9 +93,9 @@ void basicClean(CleanMojo mojo) throws Exception { void cleanNestedStructure(CleanMojo mojo) throws Exception { mojo.execute(); - assertFalse(checkExists(getBasedir() + "/target")); - assertFalse(checkExists(getBasedir() + "/target/classes")); - assertFalse(checkExists(getBasedir() + "/target/test-classes")); + assertFalse(checkExists("target")); + assertFalse(checkExists("target/classes")); + assertFalse(checkExists("target/test-classes")); } /** @@ -99,10 +110,10 @@ void cleanNestedStructure(CleanMojo mojo) throws Exception { void cleanEmptyDirectories(CleanMojo mojo) throws Exception { mojo.execute(); - assertTrue(checkExists(getBasedir() + "/testDirectoryStructure")); - assertTrue(checkExists(getBasedir() + "/testDirectoryStructure/file.txt")); - assertTrue(checkExists(getBasedir() + "/testDirectoryStructure/outputDirectory")); - assertTrue(checkExists(getBasedir() + "/testDirectoryStructure/outputDirectory/file.txt")); + assertTrue(checkExists("testDirectoryStructure")); + assertTrue(checkExists("testDirectoryStructure/file.txt")); + assertTrue(checkExists("testDirectoryStructure/outputDirectory")); + assertTrue(checkExists("testDirectoryStructure/outputDirectory/file.txt")); } /** @@ -117,18 +128,18 @@ void filesetsClean(CleanMojo mojo) throws Exception { mojo.execute(); // fileset 1 - assertTrue(checkExists(getBasedir() + "/target")); - assertTrue(checkExists(getBasedir() + "/target/classes")); - assertFalse(checkExists(getBasedir() + "/target/test-classes")); - assertTrue(checkExists(getBasedir() + "/target/subdir")); - assertFalse(checkExists(getBasedir() + "/target/classes/file.txt")); - assertTrue(checkEmpty(getBasedir() + "/target/classes")); - assertFalse(checkEmpty(getBasedir() + "/target/subdir")); - assertTrue(checkExists(getBasedir() + "/target/subdir/file.txt")); + assertTrue(checkExists("target")); + assertTrue(checkExists("target/classes")); + assertFalse(checkExists("target/test-classes")); + assertTrue(checkExists("target/subdir")); + assertFalse(checkExists("target/classes/file.txt")); + assertTrue(checkEmpty("target/classes")); + assertFalse(checkEmpty("target/subdir")); + assertTrue(checkExists("target/subdir/file.txt")); // fileset 2 - assertTrue(checkExists(getBasedir() + "/" + "buildOutputDirectory")); - assertFalse(checkExists(getBasedir() + "/" + "buildOutputDirectory/file.txt")); + assertTrue(checkExists("buildOutputDirectory")); + assertFalse(checkExists("buildOutputDirectory/file.txt")); } /** @@ -154,7 +165,7 @@ void cleanInvalidDirectory(CleanMojo mojo) throws Exception { void missingDirectory(CleanMojo mojo) throws Exception { mojo.execute(); - assertFalse(checkExists(getBasedir() + "/does-not-exist")); + assertFalse(checkExists("does-not-exist")); } /** @@ -244,7 +255,7 @@ interface LinkCreator { } private void testSymlink(LinkCreator linkCreator) throws Exception { - Cleaner cleaner = new Cleaner(null, log, false, null, null, false, false, true, false); + Cleaner cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, true, false); Path testDir = Paths.get("target/test-classes/unit/test-dir").toAbsolutePath(); Path dirWithLnk = testDir.resolve("dir"); Path orgDir = testDir.resolve("org-dir"); @@ -270,7 +281,7 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { Files.write(file, Collections.singleton("Hello world")); linkCreator.createLink(jctDir, orgDir); // delete - cleaner = new Cleaner(null, log, false, null, null, true, false, true, false); + cleaner = new Cleaner(null, matcherFactory, log, false, null, null, true, false, true, false); cleaner.delete(dirWithLnk); // verify assertFalse(Files.exists(file)); @@ -283,16 +294,17 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { * @param dir a dir or a file * @return true if a file/dir exists, false otherwise */ - private boolean checkExists(String dir) { - return new File(new File(dir).getAbsolutePath()).exists(); + private static boolean checkExists(String dir) { + return Files.exists(Path.of(getBasedir(), dir)); } /** * @param dir a directory * @return true if a dir is empty, false otherwise */ - private boolean checkEmpty(String dir) { - File[] files = new File(dir).listFiles(); + private static boolean checkEmpty(String dir) { + Path path = Path.of(getBasedir(), dir); + File[] files = path.toFile().listFiles(); return files == null || files.length == 0; } } diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java index 8de70d18..8c84cee3 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java @@ -25,6 +25,8 @@ import java.util.Set; import org.apache.maven.api.plugin.Log; +import org.apache.maven.api.services.PathMatcherFactory; +import org.apache.maven.impl.DefaultPathMatcherFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; @@ -54,12 +56,21 @@ class CleanerTest { private final Log log = mock(Log.class); + /** + * The factory to use for creating patch matcher. + * The actual implementation is used rather than a mock because filtering is an important part + * of this plugin and is tedious to test. Therefore, it is hard to guarantee that the tests of + * {@code PathMatcherFactory} in Maven core are sufficient, and we want this plugin to test it + * more. + */ + private final PathMatcherFactory matcherFactory = new DefaultPathMatcherFactory(); + @Test @DisabledOnOs(OS.WINDOWS) void deleteSucceedsDeeply(@TempDir Path tempDir) throws Exception { final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath(); final Path file = createFile(basedir.resolve("file")); - final var cleaner = new Cleaner(null, log, false, null, null, false, false, true, false); + final var cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, true, false); cleaner.delete(basedir); assertFalse(exists(basedir)); assertFalse(exists(file)); @@ -74,7 +85,7 @@ void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Excep // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException. final Set permissions = PosixFilePermissions.fromString("rw-rw-r--"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, log, false, null, null, false, false, true, false); + final var cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, true, false); final var exception = assertThrows(AccessDeniedException.class, () -> cleaner.delete(basedir)); verify(log, times(1)).warn(any(CharSequence.class), any(Throwable.class)); assertTrue(exception.getMessage().contains(basedir.toString())); @@ -88,7 +99,7 @@ void deleteFailsAfterRetryWhenNoPermission(@TempDir Path tempDir) throws Excepti // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException. final Set permissions = PosixFilePermissions.fromString("rw-rw-r--"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, log, false, null, null, false, false, true, true); + final var cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, true, true); final var exception = assertThrows(AccessDeniedException.class, () -> cleaner.delete(basedir)); assertTrue(exception.getMessage().contains(basedir.toString())); } @@ -102,7 +113,7 @@ void deleteLogsWarningWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException. final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, log, false, null, null, false, false, false, false); + final var cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, false, false); assertDoesNotThrow(() -> cleaner.delete(basedir)); verify(log, times(1)).warn(any(CharSequence.class), any(Throwable.class)); InOrder inOrder = inOrder(log); @@ -120,7 +131,7 @@ void deleteDoesNotLogAnythingWhenNoPermissionAndWarnDisabled(@TempDir Path tempD // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException. final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, log, false, null, null, false, false, false, false); + final var cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, false, false); assertDoesNotThrow(() -> cleaner.delete(basedir)); verify(log, never()).warn(any(CharSequence.class), any(Throwable.class)); } From 90ab0cf0fd6720ea8b1a5ce3fe515ca3ab1ab827 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sun, 26 Oct 2025 18:34:37 +0100 Subject: [PATCH 77/90] Replace the resolution of `targetPath` by the method now available in Maven core. --- .../org/apache/maven/plugins/clean/CleanMojo.java | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java index 9ac88c4e..2bea405d 100644 --- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java +++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java @@ -26,10 +26,8 @@ import java.util.Objects; import org.apache.maven.api.Project; -import org.apache.maven.api.ProjectScope; import org.apache.maven.api.Session; import org.apache.maven.api.di.Inject; -import org.apache.maven.api.model.Build; import org.apache.maven.api.plugin.Log; import org.apache.maven.api.plugin.MojoException; import org.apache.maven.api.plugin.annotations.Mojo; @@ -344,20 +342,9 @@ private List getDirectories() { if (project != null && session != null) { ProjectManager projectManager = session.getService(ProjectManager.class); if (projectManager != null) { - final Build build = project.getBuild(); projectManager.getSourceRoots(project).forEach((source) -> { source.targetPath().ifPresent((targetPath) -> { - // TODO: we can replace by `Project.getOutputDirectory(scope)` if MVN-11322 is merged. - String base; - ProjectScope scope = source.scope(); - if (scope == ProjectScope.MAIN) { - base = build.getOutputDirectory(); - } else if (scope == ProjectScope.TEST) { - base = build.getTestOutputDirectory(); - } else { - base = build.getDirectory(); - } - Path dir = project.getBasedir().resolve(base); + Path dir = project.getOutputDirectory(source.scope()); directories.add(dir.resolve(targetPath)); }); }); From 99080f3d1768d323aa26449001a8e0f4585b6e15 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Wed, 26 Nov 2025 20:01:11 +0100 Subject: [PATCH 78/90] Refactor the code of the background thread used in "fast delete" mode (#286) - Move the `FAST_MODE_*` string constants to an enumeration. - Move the code for background execution in a separated `BakgroundCleaner` subclass. - Use `java.util.concurrent.ExecutorService` instead of a thread with our own lifecycle management. - Add explanation about why a directory may be moved twice (same algorithm as before, just explained). - Reduce the number of directory levels by one (no need for a directory which will always contain exactly one directory). - Consolidation of exception handling, including the addition of error reporting at the end. - Add integration test for "fast-delete" using a non-default directory and complete existing test. --- src/it/fast-delete-default/invoker.properties | 18 + src/it/fast-delete-default/pom.xml | 48 +++ src/it/fast-delete-default/target/dummy.txt | 16 + src/it/fast-delete-default/verify.groovy | 27 ++ src/it/fast-delete/pom.xml | 3 +- src/it/fast-delete/target/dummy.txt | 16 + src/it/fast-delete/verify.groovy | 11 + .../plugins/clean/BackgroundCleaner.java | 342 ++++++++++++++++++ .../apache/maven/plugins/clean/CleanMojo.java | 83 ++--- .../apache/maven/plugins/clean/Cleaner.java | 318 ++++------------ .../apache/maven/plugins/clean/FastMode.java | 75 ++++ .../maven/plugins/clean/CleanMojoTest.java | 4 +- .../maven/plugins/clean/CleanerTest.java | 10 +- 13 files changed, 671 insertions(+), 300 deletions(-) create mode 100644 src/it/fast-delete-default/invoker.properties create mode 100644 src/it/fast-delete-default/pom.xml create mode 100644 src/it/fast-delete-default/target/dummy.txt create mode 100644 src/it/fast-delete-default/verify.groovy create mode 100644 src/it/fast-delete/target/dummy.txt create mode 100644 src/main/java/org/apache/maven/plugins/clean/BackgroundCleaner.java create mode 100644 src/main/java/org/apache/maven/plugins/clean/FastMode.java diff --git a/src/it/fast-delete-default/invoker.properties b/src/it/fast-delete-default/invoker.properties new file mode 100644 index 00000000..808c49e5 --- /dev/null +++ b/src/it/fast-delete-default/invoker.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +invoker.goals = install clean -Dorg.slf4j.simpleLogger.showThreadName=true -X -e diff --git a/src/it/fast-delete-default/pom.xml b/src/it/fast-delete-default/pom.xml new file mode 100644 index 00000000..2cbc9965 --- /dev/null +++ b/src/it/fast-delete-default/pom.xml @@ -0,0 +1,48 @@ + + + + 4.0.0 + + test + fast-delete + 1.0-SNAPSHOT + + Fast delete + Check fast delete with default configuration. The default directory is `target/.clean`, + which implies additional work as it is a directory inside the directory to delete. + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-clean-plugin + @project.version@ + + true + + + + + + diff --git a/src/it/fast-delete-default/target/dummy.txt b/src/it/fast-delete-default/target/dummy.txt new file mode 100644 index 00000000..978b68af --- /dev/null +++ b/src/it/fast-delete-default/target/dummy.txt @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. diff --git a/src/it/fast-delete-default/verify.groovy b/src/it/fast-delete-default/verify.groovy new file mode 100644 index 00000000..173271e5 --- /dev/null +++ b/src/it/fast-delete-default/verify.groovy @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +if ( new File( basedir, "target" ).exists() ) +{ + System.out.println( "FAILURE: 'target' has not been deleted." ); + return false; +} + +File buildLog = new File(basedir, 'build.log') +return buildLog.text.contains('mvn-background-cleaner') diff --git a/src/it/fast-delete/pom.xml b/src/it/fast-delete/pom.xml index f501863f..658cf286 100644 --- a/src/it/fast-delete/pom.xml +++ b/src/it/fast-delete/pom.xml @@ -25,7 +25,8 @@ under the License. 1.0-SNAPSHOT Fast delete - Check that fast delete is invoked. + Check that fast delete is invoked. For making this test simpler, + this test uses a `.fastdir` directory outside the `target` directory. UTF-8 diff --git a/src/it/fast-delete/target/dummy.txt b/src/it/fast-delete/target/dummy.txt new file mode 100644 index 00000000..978b68af --- /dev/null +++ b/src/it/fast-delete/target/dummy.txt @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. diff --git a/src/it/fast-delete/verify.groovy b/src/it/fast-delete/verify.groovy index 59f5940a..f25ed803 100644 --- a/src/it/fast-delete/verify.groovy +++ b/src/it/fast-delete/verify.groovy @@ -17,5 +17,16 @@ * under the License. */ +if ( new File( basedir, "target" ).exists() ) +{ + System.out.println( "FAILURE: 'target' has not been deleted." ); + return false; +} +if ( new File( basedir, ".fastdir" ).exists() ) +{ + System.out.println( "FAILURE: '.fastdir' has not been deleted." ); + return false; +} + File buildLog = new File(basedir, 'build.log') return buildLog.text.contains('mvn-background-cleaner') diff --git a/src/main/java/org/apache/maven/plugins/clean/BackgroundCleaner.java b/src/main/java/org/apache/maven/plugins/clean/BackgroundCleaner.java new file mode 100644 index 00000000..f180a43f --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/clean/BackgroundCleaner.java @@ -0,0 +1,342 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.maven.plugins.clean; + +import java.io.IOException; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.maven.api.Event; +import org.apache.maven.api.EventType; +import org.apache.maven.api.Listener; +import org.apache.maven.api.Session; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.plugin.Log; +import org.apache.maven.api.services.PathMatcherFactory; + +/** + * A cleaner potentially executed by background threads. + * + *

Limitations

+ * This class can be used for deleting {@link Path} only, not {@link Fileset}, because this class cannot handle + * the case where only a subset of the files should be deleted. It cannot handle following symbolic links neither. + * + * @author Benjamin Bentmann + * @author Martin Desruisseaux + */ +final class BackgroundCleaner extends Cleaner implements Listener, Runnable { + /** + * The maven session. + */ + @Nonnull + private final Session session; + + /** + * The directory where to temporarily move the files to delete. + */ + @Nonnull + private final Path fastDir; + + /** + * Mode to use when using fast clean. Values are: + * {@code background} to start deletion immediately and waiting for all files to be deleted when the session ends, + * {@code at-end} to indicate that the actual deletion should be performed synchronously when the session ends, or + * {@code defer} to specify that the actual file deletion should be started in the background when the session ends. + */ + @Nonnull + private final FastMode fastMode; + + /** + * The executor to use for deleting files in background threads. + */ + @Nonnull + private final ExecutorService executor; + + /** + * Files to delete at the end of the session instead of in background thread. + * This is unused ({@code null}) for {@link FastMode#BACKGROUND}. + */ + private final List filesToDeleteAtEnd; + + /** + * Directories to delete last, after the executor has been shutdown, and only if they are empty. + * This is the directory that contains the temporary directories where the files to delete have been moved. + * We can obviously not delete that directory before all deletion tasks in the background thread finished. + * The directory is deleted only if empty because other plugins (e.g. compiler plugin) may have wrote new + * files after the clean. + */ + @Nonnull + private final Set directoriesToDeleteIfEmpty; + + /** + * Errors that occurred during the deletion. + */ + private IOException errors; + + /** + * Whether at least one deletion task has been queued. + */ + private boolean started; + + /** + * Whether to disable the deletion of files in background threads. + * This is used for avoiding to repeat the same warning many times + * when the {@link #fastDir} directory does not exist. + */ + private boolean disabled; + + /** + * Creates a new cleaner to be executed in a background thread. + * + * @param session the Maven session to be used + * @param matcherFactory the service to use for creating include and exclude filters. + * @param logger the logger to use + * @param verbose whether to perform verbose logging + * @param fastDir the explicit configured directory or to be deleted in fast mode + * @param fastMode the fast deletion mode + * @param followSymlinks whether to follow symlinks + * @param force whether to force the deletion of read-only files + * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted + * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed + */ + @SuppressWarnings("checkstyle:ParameterNumber") + BackgroundCleaner( + @Nonnull Session session, + @Nonnull PathMatcherFactory matcherFactory, + @Nonnull Log logger, + boolean verbose, + @Nonnull Path fastDir, + @Nonnull FastMode fastMode, + boolean followSymlinks, + boolean force, + boolean failOnError, + boolean retryOnError) { + super(matcherFactory, logger, verbose, followSymlinks, force, failOnError, retryOnError); + this.session = session; + this.fastDir = fastDir; + this.fastMode = fastMode; + filesToDeleteAtEnd = (fastMode != FastMode.BACKGROUND) ? new ArrayList<>() : null; + directoriesToDeleteIfEmpty = new LinkedHashSet<>(); // Will need to delete in order. + executor = Executors.newSingleThreadExecutor((task) -> new Thread(task, "mvn-background-cleaner")); + } + + /** + * Returns an error message to show to user if the fast delete failed. + */ + @Override + String fastDeleteError(IOException e) { + disabled = true; + var message = new StringBuilder("Unable to fast delete directory"); + if (!Files.isDirectory(fastDir)) { + message.append(" as the path ") + .append(fastDir) + .append(" does not point to a directory or cannot be created"); + } + return message.append(". Fallback to immediate mode.").toString(); + } + + /** + * Deletes the specified directory and its contents in a background thread. + * + * @param basedir the directory to delete, must not be {@code null} + * @return whether this method was able to register the background task + * @throws IOException if an error occurred while preparing the task before execution in a background thread + */ + @Override + boolean fastDelete(Path baseDir) throws IOException { + if (disabled) { + return false; + } + final Path parent = baseDir.getParent(); + if (parent == null) { + return false; + } + if (!started) { + started = true; + session.registerListener(this); + } + /* + * The default directory is `${maven.multiModuleProjectDirectory}/target/.clean`. + * This is fine when cleaning a multi-project, in which case this directory will + * be shared by all sub-projects and should not interfere with any sub-project. + * However, when cleaning a single project, that default directory may be inside + * the `target` directory to delete. In such case, we need a 3 steps process: + * + * 1) The `target` directory is renamed to temporary name inside the same parent directory. + * 2) A new `target` directory is created with a `.clean` sub-folder (after this `if` block). + * 3) The directory at 1 is moved to 2 as if it was the target directory of a sub-project. + * + * Note that we have to use `toAbsolutePath()` instead of `toRealPath()` + * because `fastDir` may not exist yet. + */ + directoriesToDeleteIfEmpty.add(fastDir); // Should be before `baseDir`. + if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) { + String prefix = baseDir.getFileName().toString() + '-'; + Path tmpDir = Files.createTempDirectory(parent, prefix); + + // After `baseDir` has been moved, it will be implicitly recreated by `createDirectories(fastDir)` below. + // Register for another deletion, but after `fastDir` for giving a chance to `baseDir` to become empty. + directoriesToDeleteIfEmpty.add(baseDir); + try { + baseDir = Files.move(baseDir, tmpDir, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + try { + Files.delete(tmpDir); + } catch (IOException s) { + e.addSuppressed(s); + } + throw e; + } + } + /* + * Create a temporary directory inside `fastDir` and all parent directories if needed. + * The prefix is the name of parent directory, which is usually the sub-project name. + * It allows to recognize the target directory when all of them are moved to the same + * `${maven.multiModuleProjectDirectory}/target/.clean` directory. + */ + String prefix = parent.getFileName().toString() + '-'; + Path tmpDir = Files.createTempDirectory(Files.createDirectories(fastDir), prefix); + /* + * Note: a previous version used `ATOMIC_MOVE` standard option instead of `REPLACE_EXISTING` in order + * to increse the chances to have an exception if the path leads to a directory on another mountpoint. + * However, `ATOMIC_MOVE` causes the `REPLACE_EXISTING` option to be ignored and it is implementation + * specific if the existing `tmpDir` is replaced or if the method fails by throwing an `IOException`. + * For avoiding this risk, it should be okay to not use the atomic move given that the `Files.move(…)` + * Javadoc specifies: + * + * > When invoked to move a directory that is not empty then the directory is moved if it does not + * > require moving the entries in the directory. For example, renaming a directory on the same + * > `FileStore` will usually not require moving the entries in the directory. When moving a directory + * > requires that its entries be moved then this method fails (by throwing an `IOException`). + * + * If an exception occurs, the usual deletion will be performed. + */ + try { + final Path dir = Files.move(baseDir, tmpDir, StandardCopyOption.REPLACE_EXISTING); + if (filesToDeleteAtEnd != null) { + filesToDeleteAtEnd.add(dir); + } else { + executor.submit(() -> deleteSilently(dir)); + } + } catch (IOException | RuntimeException e) { + try { + Files.delete(tmpDir); + } catch (IOException s) { + e.addSuppressed(s); + } + throw e; + } + return true; + } + + /** + * Deletes the given directory without logging messages and without throwing {@link IOException}. + * The exceptions are stored for reporting after the end of the session. + * + *

Thread safety

+ * Contrarily to most other methods in {@code BackgroundCleaner}, this method is + * thread-safe because it uses a copy of this cleaner for walking in the file tree. + */ + private void deleteSilently(final Path dir) { + try { + Files.walkFileTree(dir, Set.of(), Integer.MAX_VALUE, new Cleaner(this)); + } catch (IOException e) { + errorOccurred(e); + } + } + + /** + * Stores the given error for later reporting. This method can be invoked from any thread. + */ + private synchronized void errorOccurred(IOException e) { + if (errors == null) { + errors = e; + } else { + errors.addSuppressed(e); + } + } + + /** + * Invoked at the end of the session for waiting the completion of background tasks. + * There's no clean API to do that properly as this is a very unusual use case for a + * plugin to outlive its main execution. + */ + @Override + public void onEvent(Event event) { + if (event.getType() != EventType.SESSION_ENDED) { + return; + } + session.unregisterListener(this); + if (filesToDeleteAtEnd != null) { + filesToDeleteAtEnd.forEach((dir) -> executor.submit(() -> deleteSilently(dir))); + } + if (fastMode == FastMode.DEFER) { + executor.submit(this); + executor.shutdown(); + return; + } + executor.shutdown(); + try { + // Wait for a short time for logging only if it takes longer. + if (!executor.awaitTermination(2, TimeUnit.SECONDS)) { + logger.info("Waiting for background file deletion."); + if (!executor.awaitTermination(1, TimeUnit.HOURS)) { + logger.warn("Timeout while waiting for background file deletion." + + " Some directories may not have been deleted."); + } + } + } catch (InterruptedException e) { + // Someone decided that we waited long enough. + logger.warn(e); + } + run(); + } + + /** + * Invoked after most other executor tasks are finished. It should be the very last task, + * but it may not be really last if a timeout occurred while waiting for the completion of + * other tasks, or if the wait has been interrupted, or if using a multi-threaded executor + * with {@link FastMode#DEFER}. + */ + @Override + public synchronized void run() { + for (Path dir : directoriesToDeleteIfEmpty) { + try { + Files.deleteIfExists(dir); + } catch (DirectoryNotEmptyException e) { + // Ignore as per method contract. Maybe another plugin started to write its output. + } catch (IOException e) { + errorOccurred(e); + } + } + if (errors != null) { + logger.warn("Errors during background file deletion.", errors); + errors = null; + } + } +} diff --git a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java index 2bea405d..9d2daf7d 100644 --- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java +++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java @@ -47,18 +47,12 @@ *

* * @author Emmanuel Venisse + * @author Martin Desruisseaux * @see Fileset * @since 2.0 */ @Mojo(name = "clean") public class CleanMojo implements org.apache.maven.api.plugin.Mojo { - - public static final String FAST_MODE_BACKGROUND = "background"; - - public static final String FAST_MODE_AT_END = "at-end"; - - public static final String FAST_MODE_DEFER = "defer"; - /** * The logger where to send information about what the plugin is doing. */ @@ -189,19 +183,23 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { * until all the files have been deleted. If any problem occurs during the atomic move of the directories, * the plugin will default to the traditional deletion mechanism. * + *

Note that for small projects with few files to delete, the "fast" clean tends to be actually slower. + * It is also more at risk that errors occurring during the deletion of a file get unnoticed, or are noticed + * late in the build process. This option should be used only when it has been verified to be worth.

+ * * @since 3.2 */ @Parameter(property = "maven.clean.fast", defaultValue = "false") private boolean fast; /** - * When fast clean is specified, the {@code fastDir} property will be used as the location where directories - * to be deleted will be moved prior to background deletion. If not specified, the - * {@code ${maven.multiModuleProjectDirectory}/target/.clean} directory will be used. + * When fast clean is enabled, + * the location where directories to be deleted will be moved prior to background deletion. + * If not specified, the {@code ${maven.multiModuleProjectDirectory}/target/.clean} directory will be used. * If the {@code ${build.directory}} has been modified, you'll have to adjust this property explicitly. * In order for fast clean to work correctly, this directory and the various directories that will be deleted * should usually reside on the same volume. The exact conditions are system-dependent though, but if an atomic - * move is not supported, the standard deletion mechanism will be used. + * move is not supported, the immediate deletion mechanism will be used. * * @since 3.2 * @see #fast @@ -210,16 +208,16 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo { private Path fastDir; /** - * Mode to use when using fast clean. Values are: {@code background} to start deletion immediately and - * waiting for all files to be deleted when the session ends, {@code at-end} to indicate that the actual - * deletion should be performed synchronously when the session ends, or {@code defer} to specify that - * the actual file deletion should be started in the background when the session ends. + * Mode to use when using fast clean. Values are: + * {@code background} to start deletion immediately and waiting for all files to be deleted when the session ends, + * {@code at-end} to indicate that the actual deletion should be performed synchronously when the session ends, or + * {@code defer} to specify that the actual file deletion should be started in the background when the session ends. * This should only be used when maven is embedded in a long-running process. * * @since 3.2 * @see #fast */ - @Parameter(property = "maven.clean.fastMode", defaultValue = FAST_MODE_BACKGROUND) + @Parameter(property = "maven.clean.fastMode", defaultValue = "background") private String fastMode; /** @@ -263,42 +261,27 @@ public void execute() { logger.info("Clean is skipped."); return; } - - String multiModuleProjectDirectory = - session != null ? session.getSystemProperties().get("maven.multiModuleProjectDirectory") : null; - - @SuppressWarnings("LocalVariableHidesMemberVariable") - final Path fastDir; - if (fast && this.fastDir != null) { - fastDir = this.fastDir; - } else if (fast && multiModuleProjectDirectory != null) { - fastDir = Path.of(multiModuleProjectDirectory, "target", ".clean"); - } else { - fastDir = null; - if (fast) { - logger.warn("Fast clean requires maven 3.3.1 or newer, " - + "or an explicit directory to be specified with the 'fastDir' configuration of " - + "this plugin, or the 'maven.clean.fastDir' user property to be set."); + Cleaner cleaner; + if (fast && session != null) { + Path tmpDir = fastDir; + if (tmpDir == null) { + tmpDir = session.getRootDirectory().resolve("target").resolve(".clean"); } + cleaner = new BackgroundCleaner( + session, + matcherFactory, + logger, + isVerbose(), + tmpDir, + FastMode.caseInsensitiveValueOf(fastMode), + followSymLinks, + force, + failOnError, + retryOnError); + } else { + cleaner = + new Cleaner(matcherFactory, logger, isVerbose(), followSymLinks, force, failOnError, retryOnError); } - if (fast - && !FAST_MODE_BACKGROUND.equals(fastMode) - && !FAST_MODE_AT_END.equals(fastMode) - && !FAST_MODE_DEFER.equals(fastMode)) { - throw new IllegalArgumentException("Illegal value '" + fastMode + "' for fastMode. Allowed values are '" - + FAST_MODE_BACKGROUND + "', '" + FAST_MODE_AT_END + "' and '" + FAST_MODE_DEFER + "'."); - } - final var cleaner = new Cleaner( - session, - matcherFactory, - logger, - isVerbose(), - fastDir, - fastMode, - followSymLinks, - force, - failOnError, - retryOnError); try { for (Path directoryItem : getDirectories()) { cleaner.delete(directoryItem); diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java index 2cfce014..bcdff203 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -28,57 +28,44 @@ import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.PathMatcher; -import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.DosFileAttributeView; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; -import java.util.ArrayDeque; import java.util.BitSet; -import java.util.Deque; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.stream.Stream; -import org.apache.maven.api.Event; -import org.apache.maven.api.EventType; -import org.apache.maven.api.Listener; -import org.apache.maven.api.Session; -import org.apache.maven.api.SessionData; import org.apache.maven.api.annotations.Nonnull; -import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.plugin.Log; import org.apache.maven.api.services.PathMatcherFactory; /** - * Cleans directories. + * Cleans directories. This base class deletes the files in the current thread. + * The {@link BackgroundCleaner} subclass adds the capability to run in background. + * + *

Limitations

+ * This class is not thread-safe: each instance shall be be executed in only one thread at a time. + * All directories specified as {@link Path} shall be deleted before directories specified as {@link Fileset}, + * because the {@link Path} deletions rely on default values that are modified by {@link Fileset} deletions. * * @author Benjamin Bentmann * @author Martin Desruisseaux */ -final class Cleaner implements FileVisitor { +class Cleaner implements FileVisitor { /** * Whether the host operating system is from the Windows family. */ private static final boolean ON_WINDOWS = (File.separatorChar == '\\'); - private static final SessionData.Key LAST_DIRECTORY_TO_DELETE = - SessionData.key(Path.class, Cleaner.class.getName() + ".lastDirectoryToDelete"); - - /** - * The maven session. This is typically non-null in a real run, but it can be during unit tests. - */ - @Nonnull - private final Session session; - /** * The logger where to send information or warning messages. */ @Nonnull - private final Log logger; + protected final Log logger; /** * Whether to send to the logger some information that would normally be at the "debug" level. @@ -102,12 +89,6 @@ final class Cleaner implements FileVisitor { */ private boolean reallyDeletedLastFile; - @Nonnull - private final Path fastDir; - - @Nonnull - private final String fastMode; - /** * The service to use for creating include and exclude filters. * Used for setting a value to {@link #fileMatcher} and {@link #directoryMatcher}. @@ -181,7 +162,7 @@ final class Cleaner implements FileVisitor { * A bit is used for each directory level, with {@link #currentDepth} * telling which bit is for the current directory. */ - private final BitSet nonEmptyDirectoryLevels; + private final BitSet nonEmptyDirectoryLevels = new BitSet(); /** * 0 for the base directory, and incremented for each subdirectory. @@ -194,54 +175,72 @@ final class Cleaner implements FileVisitor { * does not exclude the base directory and does not follow symbolic links. * These properties can be modified by {@link #delete(Fileset)}. * - * @param session the Maven session to be used * @param matcherFactory the service to use for creating include and exclude filters. * @param logger the logger to use * @param verbose whether to perform verbose logging - * @param fastDir the explicit configured directory or to be deleted in fast mode - * @param fastMode the fast deletion mode * @param followSymlinks whether to follow symlinks * @param force whether to force the deletion of read-only files * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed */ - @SuppressWarnings("checkstyle:ParameterNumber") Cleaner( - @Nullable Session session, @Nonnull PathMatcherFactory matcherFactory, @Nonnull Log logger, boolean verbose, - @Nullable Path fastDir, - @Nonnull String fastMode, boolean followSymlinks, boolean force, boolean failOnError, boolean retryOnError) { - this.session = session; this.matcherFactory = matcherFactory; this.logger = logger; this.verbose = verbose; - this.fastDir = fastDir; - this.fastMode = fastMode; this.followSymlinks = followSymlinks; this.force = force; this.failOnError = failOnError; this.retryOnError = retryOnError; listDeletedFiles = verbose ? logger.isInfoEnabled() : logger.isDebugEnabled(); - nonEmptyDirectoryLevels = new BitSet(); fileMatcher = matcherFactory.includesAll(); directoryMatcher = fileMatcher; } /** - * Deletes the specified fileset. + * Creates a new cleaner with the configuration of the given cleaner as it was a construction time. + * An exception is the {@link #followSymlinks} flag which is set to {@code false}. This constructor + * is invoked by {@link BackgroundCleaner} before to invoke {@link #delete(Path)} at a moment which + * is potentially after some {@link #delete(Fileset)} executions. Because {@link BackgroundCleaner} + * can be used only when {@link #followSymlinks} is {@code false}, we know that this flag can be + * cleared unconditionally. + * + * @param other the cleaner from which to copy the configuration + */ + Cleaner(Cleaner other) { + // Copy only final fields. + matcherFactory = other.matcherFactory; + logger = other.logger; + verbose = other.verbose; + force = other.force; + failOnError = other.failOnError; + retryOnError = other.retryOnError; + listDeletedFiles = other.listDeletedFiles; + + // Non-final fields. + fileMatcher = matcherFactory.includesAll(); + directoryMatcher = fileMatcher; + } + + /** + * Deletes the specified fileset in the current thread. * This method modifies the include and exclude filters, * whether to exclude the base directory and whether to follow symbolic links. * + *

Configuration

+ * {@link Fileset} deletions should be done after all {@link Path} deletions, + * because this method modifies this {@code Cleaner} configuration. + * * @param fileset the fileset to delete * @throws IOException if a file/directory could not be deleted and {@link #failOnError} is {@code true} */ - public void delete(@Nonnull Fileset fileset) throws IOException { + public final void delete(@Nonnull Fileset fileset) throws IOException { fileMatcher = matcherFactory.createPathMatcher( fileset.getDirectory(), fileset.getIncludes(), fileset.getExcludes(), fileset.useDefaultExcludes()); directoryMatcher = matcherFactory.deriveDirectoryMatcher(fileMatcher); @@ -252,7 +251,7 @@ public void delete(@Nonnull Fileset fileset) throws IOException { /** * Deletes the specified directory and its contents using the current configuration. - * Non-existing directories will be silently ignored. + * Non-existing directories will be ignored with a warning logged at the debug level. * *

Configuration

* The behavior of this method depends on the {@code Cleaner} configuration. @@ -264,28 +263,35 @@ public void delete(@Nonnull Fileset fileset) throws IOException { * @param basedir the directory to delete, must not be {@code null} * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true} */ - public void delete(@Nonnull Path basedir) throws IOException { + public final void delete(@Nonnull Path basedir) throws IOException { if (!Files.isDirectory(basedir)) { if (Files.notExists(basedir)) { - if (logger.isDebugEnabled()) { - logger.debug("Skipping non-existing directory " + basedir); - } + logger.debug("Skipping non-existing directory \"" + basedir + "\"."); return; } - throw new NotDirectoryException("Invalid base directory " + basedir); + throw new NotDirectoryException("Invalid base directory \"" + basedir + "\"."); } if (logger.isInfoEnabled()) { - logger.info("Deleting " + basedir + (isClearAll() ? "" : " (" + fileMatcher + ')')); + StringBuilder message = + new StringBuilder("Deleting \"").append(basedir).append('"'); + if (!isClearAll()) { + message.append(" (").append(fileMatcher).append(')'); + } + logger.info(message.append('.').toString()); } var options = EnumSet.noneOf(FileVisitOption.class); if (followSymlinks) { options.add(FileVisitOption.FOLLOW_LINKS); basedir = getCanonicalPath(basedir, null); } - if (isClearAll() && !followSymlinks && fastDir != null && session != null) { + if (isClearAll() && !followSymlinks) { // If anything wrong happens, we'll just use the usual deletion mechanism - if (fastDelete(basedir)) { - return; + try { + if (fastDelete(basedir)) { + return; + } + } catch (IOException e) { + logger.debug(fastDeleteError(e), e); } } Files.walkFileTree(basedir, options, Integer.MAX_VALUE, this); @@ -296,56 +302,26 @@ public void delete(@Nonnull Path basedir) throws IOException { * This is a required condition for allowing the use of {@link #fastDelete(Path)}. */ private boolean isClearAll() { - return fileMatcher == matcherFactory.includesAll(); + return matcherFactory.isIncludesAll(fileMatcher); } - private boolean fastDelete(Path baseDir) { - // Handle the case where we use ${maven.multiModuleProjectDirectory}/target/.clean for example - if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) { - try { - String prefix = baseDir.getFileName().toString() + '.'; - Path tmpDir = Files.createTempDirectory(baseDir.getParent(), prefix); - try { - Files.move(baseDir, tmpDir, StandardCopyOption.REPLACE_EXISTING); - if (session != null) { - session.getData().set(LAST_DIRECTORY_TO_DELETE, baseDir); - } - baseDir = tmpDir; - } catch (IOException e) { - Files.delete(tmpDir); - throw e; - } - } catch (IOException e) { - logger.debug("Unable to fast delete directory", e); - return false; - } - } - // Create fastDir and the needed parents if needed - try { - if (!Files.isDirectory(fastDir)) { - Files.createDirectories(fastDir); - } - } catch (IOException e) { - logger.debug( - "Unable to fast delete directory as the path " + fastDir - + " does not point to a directory or cannot be created", - e); - return false; - } - try { - Path tmpDir = Files.createTempDirectory(fastDir, ""); - Path dstDir = tmpDir.resolve(baseDir.getFileName()); - // Note that by specifying the ATOMIC_MOVE, we expect an exception to be thrown - // if the path leads to a directory on another mountpoint. If this is the case - // or any other exception occurs, an exception will be thrown in which case - // the method will return false and the usual deletion will be performed. - Files.move(baseDir, dstDir, StandardCopyOption.ATOMIC_MOVE); - BackgroundCleaner.delete(this, tmpDir, fastMode); - return true; - } catch (IOException e) { - logger.debug("Unable to fast delete directory", e); - return false; - } + /** + * Deletes the specified directory and its contents in a background thread. + * The default implementation returns {@code false}. + * + * @param basedir the directory to delete, must not be {@code null} + * @return whether this method was able to register the background task + * @throws IOException if an error occurred while preparing the task before execution in a background thread + */ + boolean fastDelete(Path baseDir) throws IOException { + return false; + } + + /** + * Returns an error message to show to user if the fast delete failed. + */ + String fastDeleteError(IOException e) { + return e.toString(); } /** @@ -611,146 +587,4 @@ private void logDelete(final Path file, final BasicFileAttributes attrs) { logger.debug(message); } } - - private static class BackgroundCleaner extends Thread { - - private static BackgroundCleaner instance; - - private final Deque filesToDelete = new ArrayDeque<>(); - - private final Cleaner cleaner; - - private final String fastMode; - - private static final int NEW = 0; - private static final int RUNNING = 1; - private static final int STOPPED = 2; - - private int status = NEW; - - public static void delete(Cleaner cleaner, Path dir, String fastMode) { - synchronized (BackgroundCleaner.class) { - if (instance == null || !instance.doDelete(dir)) { - instance = new BackgroundCleaner(cleaner, dir, fastMode); - } - } - } - - static void sessionEnd() { - synchronized (BackgroundCleaner.class) { - if (instance != null) { - instance.doSessionEnd(); - } - } - } - - private BackgroundCleaner(Cleaner cleaner, Path dir, String fastMode) { - super("mvn-background-cleaner"); - this.cleaner = cleaner; - this.fastMode = fastMode; - init(cleaner.fastDir, dir); - } - - @Override - public void run() { - var options = EnumSet.noneOf(FileVisitOption.class); - if (cleaner.followSymlinks) { - options.add(FileVisitOption.FOLLOW_LINKS); - } - Path basedir; - while ((basedir = pollNext()) != null) { - try { - Files.walkFileTree(basedir, options, Integer.MAX_VALUE, cleaner); - } catch (IOException e) { - // do not display errors - } - } - } - - synchronized void init(Path fastDir, Path dir) { - if (Files.isDirectory(fastDir)) { - try { - try (Stream children = Files.list(fastDir)) { - children.forEach(this::doDelete); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - doDelete(dir); - } - - synchronized Path pollNext() { - Path basedir = filesToDelete.poll(); - if (basedir == null) { - if (cleaner.session != null) { - SessionData data = cleaner.session.getData(); - Path lastDir = data.get(LAST_DIRECTORY_TO_DELETE); - if (lastDir != null) { - data.set(LAST_DIRECTORY_TO_DELETE, null); - return lastDir; - } - } - status = STOPPED; - notifyAll(); - } - return basedir; - } - - synchronized boolean doDelete(Path dir) { - if (status == STOPPED) { - return false; - } - filesToDelete.add(dir); - if (status == NEW && CleanMojo.FAST_MODE_BACKGROUND.equals(fastMode)) { - status = RUNNING; - notifyAll(); - start(); - } - wrapExecutionListener(); - return true; - } - - /** - * If this has not been done already, we wrap the ExecutionListener inside a proxy - * which simply delegates call to the previous listener. When the session ends, it will - * also call {@link BackgroundCleaner#sessionEnd()}. - * There's no clean API to do that properly as this is a very unusual use case for a plugin - * to outlive its main execution. - */ - private void wrapExecutionListener() { - synchronized (CleanerListener.class) { - if (cleaner.session.getListeners().stream().noneMatch(l -> l instanceof CleanerListener)) { - cleaner.session.registerListener(new CleanerListener()); - } - } - } - - synchronized void doSessionEnd() { - if (status != STOPPED) { - if (status == NEW) { - start(); - } - if (!CleanMojo.FAST_MODE_DEFER.equals(fastMode)) { - try { - cleaner.logger.info("Waiting for background file deletion"); - while (status != STOPPED) { - wait(); - } - } catch (InterruptedException e) { - // ignore - } - } - } - } - } - - static class CleanerListener implements Listener { - @Override - public void onEvent(Event event) { - if (event.getType() == EventType.SESSION_ENDED) { - BackgroundCleaner.sessionEnd(); - } - } - } } diff --git a/src/main/java/org/apache/maven/plugins/clean/FastMode.java b/src/main/java/org/apache/maven/plugins/clean/FastMode.java new file mode 100644 index 00000000..86445e11 --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/clean/FastMode.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.maven.plugins.clean; + +import java.util.Locale; + +/** + * Specifies when to delete files when fast clean is used. + * This enumeration defines the value values for {@code maven.clean.fastMode}, + * in upper case and with {@code '-'} replaced by {@code '_'}. + */ +enum FastMode { + /** + * Start deletion immediately and wait for all files to be deleted when the session ends. + * This is the default mode when the deletion of files in a background thread is enabled. + */ + BACKGROUND, + + /** + * Actual deletion should be performed synchronously when the session ends. + * The plugin waits for all files to be deleted when the session end. + */ + AT_END, + + /** + * Actual file deletion should be started in the background when the session ends. + * The plugin does not wait for files to be deleted. + */ + DEFER; + + /** + * {@return the enumeration value for the given configuration option} + * + * @param option the configuration option, case insensitive + * @throws IllegalArgumentException if the given option is invalid + */ + public static FastMode caseInsensitiveValueOf(String option) { + try { + return valueOf(option.trim().toUpperCase(Locale.US).replace('-', '_')); + } catch (NullPointerException | IllegalArgumentException e) { + StringBuilder sb = + new StringBuilder("Illegal value '").append(option).append("' for fastMode. Allowed values are '"); + FastMode[] values = values(); + int last = values.length; + for (int i = 0; i <= last; i++) { + sb.append(values[i]).append(i < last ? "', '" : "' and '"); + } + throw new IllegalArgumentException(sb.append("'.").toString(), e); + } + } + + /** + * {@return the lower-case variant of this enumeration value} + */ + @Override + public String toString() { + return name().replace('_', '-').toLowerCase(Locale.US); + } +} diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java index c7935dea..a6a138e7 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java @@ -255,7 +255,7 @@ interface LinkCreator { } private void testSymlink(LinkCreator linkCreator) throws Exception { - Cleaner cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, true, false); + Cleaner cleaner = new Cleaner(matcherFactory, log, false, false, false, true, false); Path testDir = Paths.get("target/test-classes/unit/test-dir").toAbsolutePath(); Path dirWithLnk = testDir.resolve("dir"); Path orgDir = testDir.resolve("org-dir"); @@ -281,7 +281,7 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { Files.write(file, Collections.singleton("Hello world")); linkCreator.createLink(jctDir, orgDir); // delete - cleaner = new Cleaner(null, matcherFactory, log, false, null, null, true, false, true, false); + cleaner = new Cleaner(matcherFactory, log, false, true, false, true, false); cleaner.delete(dirWithLnk); // verify assertFalse(Files.exists(file)); diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java index 8c84cee3..22ac91fc 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java @@ -70,7 +70,7 @@ class CleanerTest { void deleteSucceedsDeeply(@TempDir Path tempDir) throws Exception { final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath(); final Path file = createFile(basedir.resolve("file")); - final var cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, true, false); + final var cleaner = new Cleaner(matcherFactory, log, false, false, false, true, false); cleaner.delete(basedir); assertFalse(exists(basedir)); assertFalse(exists(file)); @@ -85,7 +85,7 @@ void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Excep // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException. final Set permissions = PosixFilePermissions.fromString("rw-rw-r--"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, true, false); + final var cleaner = new Cleaner(matcherFactory, log, false, false, false, true, false); final var exception = assertThrows(AccessDeniedException.class, () -> cleaner.delete(basedir)); verify(log, times(1)).warn(any(CharSequence.class), any(Throwable.class)); assertTrue(exception.getMessage().contains(basedir.toString())); @@ -99,7 +99,7 @@ void deleteFailsAfterRetryWhenNoPermission(@TempDir Path tempDir) throws Excepti // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException. final Set permissions = PosixFilePermissions.fromString("rw-rw-r--"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, true, true); + final var cleaner = new Cleaner(matcherFactory, log, false, false, false, true, true); final var exception = assertThrows(AccessDeniedException.class, () -> cleaner.delete(basedir)); assertTrue(exception.getMessage().contains(basedir.toString())); } @@ -113,7 +113,7 @@ void deleteLogsWarningWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException. final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, false, false); + final var cleaner = new Cleaner(matcherFactory, log, false, false, false, false, false); assertDoesNotThrow(() -> cleaner.delete(basedir)); verify(log, times(1)).warn(any(CharSequence.class), any(Throwable.class)); InOrder inOrder = inOrder(log); @@ -131,7 +131,7 @@ void deleteDoesNotLogAnythingWhenNoPermissionAndWarnDisabled(@TempDir Path tempD // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException. final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x"); setPosixFilePermissions(basedir, permissions); - final var cleaner = new Cleaner(null, matcherFactory, log, false, null, null, false, false, false, false); + final var cleaner = new Cleaner(matcherFactory, log, false, false, false, false, false); assertDoesNotThrow(() -> cleaner.delete(basedir)); verify(log, never()).warn(any(CharSequence.class), any(Throwable.class)); } From d4baff82141f61818b2cbf38b283d27e82f75998 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:19:31 +0100 Subject: [PATCH 79/90] Bump org.mockito:mockito-core from 5.20.0 to 5.21.0 (#288) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.20.0 to 5.21.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.20.0...v5.21.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.21.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7edef7da..cb78b5c9 100644 --- a/pom.xml +++ b/pom.xml @@ -138,7 +138,7 @@ under the License. org.mockito mockito-core - 5.20.0 + 5.21.0 test From b5698670a449e1316b44ee885bfdcc35ee9b97bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 06:34:42 +0100 Subject: [PATCH 80/90] Bump org.apache.maven.plugins:maven-plugins from 45 to 46 (#291) Bumps [org.apache.maven.plugins:maven-plugins](https://github.com/apache/maven-parent) from 45 to 46. - [Release notes](https://github.com/apache/maven-parent/releases) - [Commits](https://github.com/apache/maven-parent/commits) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-plugins dependency-version: '46' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cb78b5c9..098c9683 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.maven.plugins maven-plugins - 45 + 46 From 18df3426ff59b18c9bd45204e0087ebfe070b4a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:45:42 +0100 Subject: [PATCH 81/90] Bump org.apache.maven.plugins:maven-plugins from 46 to 47 (#294) Bumps [org.apache.maven.plugins:maven-plugins](https://github.com/apache/maven-parent) from 46 to 47. - [Release notes](https://github.com/apache/maven-parent/releases) - [Commits](https://github.com/apache/maven-parent/commits) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-plugins dependency-version: '47' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 098c9683..1a712448 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.maven.plugins maven-plugins - 46 + 47 From f29258f233fc8d7d315b2668d15fd01692c1dcc7 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sat, 14 Feb 2026 15:26:31 +0100 Subject: [PATCH 82/90] Mockito improvements (#298) -- Co-authored-by: Moderne --- .../java/org/apache/maven/plugins/clean/CleanerTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java index 22ac91fc..df02e83b 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java @@ -48,7 +48,6 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -87,7 +86,7 @@ void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Excep setPosixFilePermissions(basedir, permissions); final var cleaner = new Cleaner(matcherFactory, log, false, false, false, true, false); final var exception = assertThrows(AccessDeniedException.class, () -> cleaner.delete(basedir)); - verify(log, times(1)).warn(any(CharSequence.class), any(Throwable.class)); + verify(log).warn(any(CharSequence.class), any(Throwable.class)); assertTrue(exception.getMessage().contains(basedir.toString())); } @@ -115,7 +114,7 @@ void deleteLogsWarningWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws setPosixFilePermissions(basedir, permissions); final var cleaner = new Cleaner(matcherFactory, log, false, false, false, false, false); assertDoesNotThrow(() -> cleaner.delete(basedir)); - verify(log, times(1)).warn(any(CharSequence.class), any(Throwable.class)); + verify(log).warn(any(CharSequence.class), any(Throwable.class)); InOrder inOrder = inOrder(log); ArgumentCaptor cause1 = ArgumentCaptor.forClass(AccessDeniedException.class); inOrder.verify(log).warn(eq("Failed to delete " + file), cause1.capture()); From 82b0c653878f1eb0aa5a0ce193ba2b6828409ab1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:26:55 +0100 Subject: [PATCH 83/90] Bump org.mockito:mockito-core from 5.21.0 to 5.23.0 (#303) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.21.0 to 5.23.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.21.0...v5.23.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.23.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1a712448..bc9de135 100644 --- a/pom.xml +++ b/pom.xml @@ -138,7 +138,7 @@ under the License. org.mockito mockito-core - 5.21.0 + 5.23.0 test From bba1733e469621f63c601b4990f6735453d5d9db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 22:13:02 +0200 Subject: [PATCH 84/90] Bump org.apache.maven.plugins:maven-plugins from 47 to 48 (#306) Bumps [org.apache.maven.plugins:maven-plugins](https://github.com/apache/maven-parent) from 47 to 48. - [Release notes](https://github.com/apache/maven-parent/releases) - [Commits](https://github.com/apache/maven-parent/commits) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-plugins dependency-version: '48' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bc9de135..f0e5d530 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.maven.plugins maven-plugins - 47 + 48 From 978727570dcfadc418f456e351d63435b469a503 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 22:14:02 +0200 Subject: [PATCH 85/90] Bump org.slf4j:slf4j-simple from 2.0.17 to 2.0.18 (#308) Bumps org.slf4j:slf4j-simple from 2.0.17 to 2.0.18. --- updated-dependencies: - dependency-name: org.slf4j:slf4j-simple dependency-version: 2.0.18 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f0e5d530..bbe66c9b 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ under the License. 17 33.4.6-jre 6.0.0 - 2.0.17 + 2.0.18 3.9.1 4.0.0-beta-4 4.0.0-beta-1 From 250f5f69c862a29577fd2f5be40c086a77a8e75a Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Mon, 22 Jun 2026 23:06:53 +0200 Subject: [PATCH 86/90] Try release-drafter 7.4.0 --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 1695d359..01a0c7b3 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -24,4 +24,4 @@ on: jobs: update_release_draft: - uses: apache/maven-gh-actions-shared/.github/workflows/release-drafter.yml@v4 + uses: apache/maven-gh-actions-shared/.github/workflows/release-drafter.yml@dependabot/github_actions/v4/release-drafter/release-drafter-7.4.0 From d776151f11b511436c7f0722dc0dcd8b7f1c0163 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Mon, 22 Jun 2026 23:17:01 +0200 Subject: [PATCH 87/90] Set custom tag-template in release-drafter configuration --- .github/release-drafter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 6d3b73cf..fe965e16 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -16,6 +16,7 @@ # under the License. _extends: maven-gh-actions-shared +tag-template: maven-clean-plugin-$RESOLVED_VERSION include-pre-releases: true prerelease: true From 24a282d21e5b480ff92caeda0c901d7a276ed413 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Thu, 25 Jun 2026 18:46:42 +0200 Subject: [PATCH 88/90] Try release-drafter 7.5.1 --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 01a0c7b3..745a8c36 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -24,4 +24,4 @@ on: jobs: update_release_draft: - uses: apache/maven-gh-actions-shared/.github/workflows/release-drafter.yml@dependabot/github_actions/v4/release-drafter/release-drafter-7.4.0 + uses: apache/maven-gh-actions-shared/.github/workflows/release-drafter.yml@dependabot/github_actions/v4/release-drafter/release-drafter-7.5.1 From c5a9cdbf9722db9e9140972eb57664c00e1d79c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:12:35 +0200 Subject: [PATCH 89/90] Bump org.apache.maven.plugins:maven-plugins from 48 to 49 (#313) Bumps [org.apache.maven.plugins:maven-plugins](https://github.com/apache/maven-parent) from 48 to 49. - [Release notes](https://github.com/apache/maven-parent/releases) - [Commits](https://github.com/apache/maven-parent/commits) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-plugins dependency-version: '49' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bbe66c9b..234fcd79 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.maven.plugins maven-plugins - 48 + 49 From eaae1b8f269c15ad59b21095bf5a32f8271ce132 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Wed, 8 Jul 2026 13:09:38 +0200 Subject: [PATCH 90/90] Add AGENTS.md + SECURITY.md security-model pointer for scanner discoverability Generated-by: Claude Code --- AGENTS.md | 32 ++++++++++++++++++++++++++++++++ SECURITY.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 AGENTS.md create mode 100644 SECURITY.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..50786196 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ + + +# Agent Guide for maven-clean-plugin + +This file is read by automated agents (security scanners, code +analyzers, AI assistants) operating on this repository. + +## Security + +Security model: [SECURITY.md](./SECURITY.md) + +Agents that scan this repository should consult `SECURITY.md` and the +threat model it links before reporting issues. + +Points at the Apache Maven family umbrella security model. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..a44c459a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,32 @@ + + +# Security Policy + +## Reporting a Vulnerability + +`apache/maven-clean-plugin` follows the [Apache Software Foundation security process](https://www.apache.org/security/). Please report suspected +vulnerabilities privately to `security@apache.org`; do not open public +GitHub issues or pull requests for security reports. + +## Threat Model + +What the project treats as in scope and out of scope, the security +properties it provides and disclaims, the adversary model, and how +findings are triaged are documented in .