From 2da147458800dfe9ecc24865aef0e718ac56df57 Mon Sep 17 00:00:00 2001
From: Guillaume Nodet <gnodet@gmail.com>
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.
   </parent>
 
   <artifactId>maven-clean-plugin</artifactId>
-  <version>3.4.1-SNAPSHOT</version>
+  <version>4.0.0-SNAPSHOT</version>
   <packaging>maven-plugin</packaging>
 
   <name>Apache Maven Clean Plugin</name>
@@ -61,46 +61,44 @@ under the License.
   </distributionManagement>
 
   <properties>
-    <mavenVersion>3.6.3</mavenVersion>
+    <mavenVersion>4.0.0-beta-3</mavenVersion>
+    <javaVersion>17</javaVersion>
+    <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
+    <version.maven-plugin-testing>4.0.0-alpha-3-SNAPSHOT</version.maven-plugin-testing>
+    <version.maven-plugin-tools>4.0.0-SNAPSHOT</version.maven-plugin-tools>
     <project.build.outputTimestamp>2024-06-16T10:25:11Z</project.build.outputTimestamp>
   </properties>
 
   <dependencies>
     <dependency>
       <groupId>org.apache.maven</groupId>
-      <artifactId>maven-plugin-api</artifactId>
+      <artifactId>maven-api-core</artifactId>
       <version>${mavenVersion}</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.maven</groupId>
-      <artifactId>maven-core</artifactId>
+      <artifactId>maven-api-di</artifactId>
       <version>${mavenVersion}</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
-      <groupId>org.apache.maven.resolver</groupId>
-      <artifactId>maven-resolver-api</artifactId>
-      <version>1.1.1</version>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-api-meta</artifactId>
+      <version>${mavenVersion}</version>
       <scope>provided</scope>
     </dependency>
+
     <dependency>
       <groupId>org.codehaus.plexus</groupId>
       <artifactId>plexus-utils</artifactId>
     </dependency>
 
-    <!-- dependencies to annotations -->
-    <dependency>
-      <groupId>org.apache.maven.plugin-tools</groupId>
-      <artifactId>maven-plugin-annotations</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
     <!-- Test -->
     <dependency>
       <groupId>org.apache.maven.plugin-testing</groupId>
       <artifactId>maven-plugin-testing-harness</artifactId>
-      <version>4.0.0-alpha-2</version>
+      <version>${version.maven-plugin-testing}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
@@ -109,18 +107,42 @@ under the License.
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>org.codehaus.plexus</groupId>
-      <artifactId>plexus-testing</artifactId>
-      <version>1.3.0</version>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-core</artifactId>
+      <version>${mavenVersion}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>org.codehaus.plexus</groupId>
-      <artifactId>plexus-xml</artifactId>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+      <version>6.0.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <version>2.0.13</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
 
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-plugin-plugin</artifactId>
+        <version>${version.maven-plugin-tools}</version>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <redirectTestOutputToFile>true</redirectTestOutputToFile>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
   <profiles>
     <profile>
       <id>run-its</id>
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.
           <url>@localRepositoryUrl@</url>
           <releases>
             <enabled>true</enabled>
+            <checksumPolicy>ignore</checksumPolicy>
           </releases>
           <snapshots>
             <enabled>true</enabled>
+            <checksumPolicy>ignore</checksumPolicy>
           </snapshots>
         </repository>
       </repositories>
@@ -44,9 +46,11 @@ under the License.
           <url>@localRepositoryUrl@</url>
           <releases>
             <enabled>true</enabled>
+            <checksumPolicy>ignore</checksumPolicy>
           </releases>
           <snapshots>
             <enabled>true</enabled>
+            <checksumPolicy>ignore</checksumPolicy>
           </snapshots>
         </pluginRepository>
       </pluginRepositories>
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: <code>background</code> 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 <code>null</code>.
      */
-    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<Path> 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 <code>null</code> 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<CharSequence> l1, BiConsumer<CharSequence, Throwable> 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 <code>null</code>. Non-existing directories will be silently
-     *                       ignored.
-     * @param selector       The selector used to determine what contents to delete, may be <code>null</code> to delete
-     *                       everything.
+     * @param basedir The directory to delete, must not be <code>null</code>. Non-existing directories will be silently
+     *            ignored.
+     * @param selector The selector used to determine what contents to delete, may be <code>null</code> 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 <code>failOnError</code> is <code>true</code>.
      */
     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 <code>null</code>. If <code>followSymlinks</code> is
-     *                       <code>false</code>, it is assumed that the parent file is canonical.
-     * @param pathname       The relative pathname of the file, using {@link File#separatorChar}, must not be
-     *                       <code>null</code>.
-     * @param selector       The selector used to determine what contents to delete, may be <code>null</code> to delete
-     *                       everything.
+     * @param file The file/directory to delete, must not be <code>null</code>. If <code>followSymlinks</code> is
+     *            <code>false</code>, it is assumed that the parent file is canonical.
+     * @param pathname The relative pathname of the file, using {@link File#separatorChar}, must not be
+     *            <code>null</code>.
+     * @param selector The selector used to determine what contents to delete, may be <code>null</code> 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 <code>null</code>.
      * @throws IOException If a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>.
      */
     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<Path> 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 <code>null</code>.
-     * @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 <code>0</code> if the file was deleted, <code>1</code> otherwise.
-     * @throws IOException If a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>.
-     */
-    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<File> filesToDelete = new ArrayDeque<>();
+
+        private final Deque<Path> 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<Path> 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:
 
 +--------
-  <directory>${project.basedir}/some/relative/path</directory>
+  <directory>${basedir}/some/relative/path</directory>
 +--------
 
  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 <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
  */
 @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 @@
-<!--
-    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.
--->
-
-<project>
-  <build>
-    <plugins>
-      <plugin>
-        <artifactId>maven-clean-plugin</artifactId>
-        <configuration>
-          <directory>${basedir}/target/test-classes/unit/basic-clean-test/buildDirectory</directory>
-          <outputDirectory>${basedir}/target/test-classes/unit/basic-clean-test/buildOutputDirectory</outputDirectory>
-          <testOutputDirectory>${basedir}/target/test-classes/unit/basic-clean-test/buildTestDirectory</testOutputDirectory>
-          <verbose>true</verbose>
-          <failOnError>true</failOnError>
-        </configuration>
-      </plugin>
-    </plugins>
-  </build>
-</project>
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 @@
       <plugin>
         <artifactId>maven-clean-plugin</artifactId>
         <configuration>
-          <directory>${basedir}/target/test-classes/unit/nested-clean-test/target</directory>
-          <outputDirectory>${basedir}/target/test-classes/unit/nested-clean-test/target/classes</outputDirectory>
-          <testOutputDirectory>${basedir}/target/test-classes/unit/nested-clean-test/target/test-classes</testOutputDirectory>
+          <directory>${project.basedir}/buildDirectory</directory>
+          <outputDirectory>${project.basedir}/buildOutputDirectory</outputDirectory>
+          <testOutputDirectory>${project.basedir}/buildTestDirectory</testOutputDirectory>
           <verbose>true</verbose>
           <failOnError>true</failOnError>
         </configuration>
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 @@
       <plugin>
         <artifactId>maven-clean-plugin</artifactId>
         <configuration>
+          <excludeDefaultDirectories>true</excludeDefaultDirectories>
           <filesets>
             <fileset>
-              <directory>${basedir}/target/test-classes/unit/fileset-clean-test/target</directory>
+              <directory>${project.basedir}/target</directory>
               <includes>
                 <include>**/file.txt</include>
                 <include>**/test-classes/**</include>
@@ -35,7 +36,7 @@
               </excludes>
             </fileset>
             <fileset>
-              <directory>${basedir}/target/test-classes/unit/fileset-clean-test/buildOutputDirectory</directory>
+              <directory>${project.basedir}/buildOutputDirectory</directory>
               <includes>
                 <include>**</include>
               </includes>
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 @@
       <plugin>
         <artifactId>maven-clean-plugin</artifactId>
         <configuration>
-          <directory>${basedir}/target/test-classes/unit/locked-file-test/buildDirectory</directory>
+          <directory>${project.basedir}/this-is-a-file</directory>
           <verbose>true</verbose>
           <failOnError>true</failOnError>
         </configuration>
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 @@
       <plugin>
         <artifactId>maven-clean-plugin</artifactId>
         <configuration>
-          <directory>${basedir}/target/test-classes/unit/missing-clean-test/does-not-exist</directory>
+          <directory>${project.basedir}/buildDirectory</directory>
           <verbose>true</verbose>
           <failOnError>true</failOnError>
         </configuration>
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 @@
       <plugin>
         <artifactId>maven-clean-plugin</artifactId>
         <configuration>
-          <directory>${basedir}/target/test-classes/unit/invalid-directory-test/this-is-a-file</directory>
+          <directory>${project.basedir}/does-not-exist</directory>
           <verbose>true</verbose>
           <failOnError>true</failOnError>
         </configuration>
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 @@
+<!--
+    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.
+-->
+
+<project>
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-clean-plugin</artifactId>
+        <configuration>
+          <directory>${project.basedir}/target</directory>
+          <outputDirectory>${project.basedir}/target/classes</outputDirectory>
+          <testOutputDirectory>${project.basedir}/target/test-classes</testOutputDirectory>
+          <verbose>true</verbose>
+          <failOnError>true</failOnError>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>

From 30b89ca3ac0c8e96c4fe70ba88d5fe35c8526ed9 Mon Sep 17 00:00:00 2001
From: Guillaume Nodet <gnodet@gmail.com>
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 <gnodet@gmail.com>
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 <gnodet@gmail.com>
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 <gnodet@gmail.com>
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.
   <properties>
     <mavenVersion>4.0.0-beta-3</mavenVersion>
     <javaVersion>17</javaVersion>
+    <guavaVersion>33.2.1-jre</guavaVersion>
+    <guiceVersion>6.0.0</guiceVersion>
+    <slf4jVersion>2.0.13</slf4jVersion>
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
     <version.maven-plugin-testing>4.0.0-alpha-3-SNAPSHOT</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-SNAPSHOT</version.maven-plugin-tools>
     <project.build.outputTimestamp>2024-06-16T10:25:11Z</project.build.outputTimestamp>
   </properties>
 
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava</artifactId>
+        <version>${guavaVersion}</version>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
   <dependencies>
     <dependency>
       <groupId>org.apache.maven</groupId>
@@ -115,13 +128,13 @@ under the License.
     <dependency>
       <groupId>com.google.inject</groupId>
       <artifactId>guice</artifactId>
-      <version>6.0.0</version>
+      <version>${guiceVersion}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-simple</artifactId>
-      <version>2.0.13</version>
+      <version>${slf4jVersion}</version>
       <scope>test</scope>
     </dependency>
   </dependencies>

From e35167df10c12aaa12ec0da8a9466c2d6c5123b1 Mon Sep 17 00:00:00 2001
From: Guillaume Nodet <gnodet@gmail.com>
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.
   </dependencies>
 
   <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-plugin-plugin</artifactId>
-        <version>${version.maven-plugin-tools}</version>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-surefire-plugin</artifactId>
-        <configuration>
-          <redirectTestOutputToFile>true</redirectTestOutputToFile>
-        </configuration>
-      </plugin>
-    </plugins>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>com.diffplug.spotless</groupId>
+          <artifactId>spotless-maven-plugin</artifactId>
+          <configuration>
+            <java>
+              <includes>
+                <include>src/**/*.java</include>
+              </includes>
+            </java>
+            <pom>
+              <includes>
+                <include>**/pom.xml</include>
+              </includes>
+            </pom>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <configuration>
+            <redirectTestOutputToFile>true</redirectTestOutputToFile>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
   </build>
 
   <profiles>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>test</groupId>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>test</groupId>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>test</groupId>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,35 +17,32 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-	<modelVersion>4.0.0</modelVersion>
-
-	<groupId>test</groupId>
-	<artifactId>fast-delete</artifactId>
-	<version>1.0-SNAPSHOT</version>
-
-	<name>Fast delete</name>
-	<description>Check that fast delete is invoked.</description>
-
-	<properties>
-		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-	</properties>
-
-	<build>
-		<plugins>
-			<plugin>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-clean-plugin</artifactId>
-				<version>@project.version@</version>
-				<configuration>
-					<fast>true</fast>
-					<fastDir>${project.basedir}${file.separator}.fastdir</fastDir>
-				</configuration>
-			</plugin>
-		</plugins>
-	</build>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>test</groupId>
+  <artifactId>fast-delete</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>Fast delete</name>
+  <description>Check that fast delete is invoked.</description>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-clean-plugin</artifactId>
+        <version>@project.version@</version>
+        <configuration>
+          <fast>true</fast>
+          <fastDir>${project.basedir}${file.separator}.fastdir</fastDir>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
 
 </project>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <parent>
@@ -30,7 +26,7 @@ under the License.
     <version>1.0-SNAPSHOT</version>
   </parent>
 
-    <artifactId>child-a</artifactId>
+  <artifactId>child-a</artifactId>
   <version>1.0-SNAPSHOT</version>
 
   <name>Child A</name>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>test</groupId>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>test</groupId>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <parent>
@@ -30,7 +26,7 @@ under the License.
     <version>1.0-SNAPSHOT</version>
   </parent>
 
-    <artifactId>child-a</artifactId>
+  <artifactId>child-a</artifactId>
   <version>1.0-SNAPSHOT</version>
 
   <name>Child A</name>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <parent>
@@ -30,7 +26,7 @@ under the License.
     <version>1.0-SNAPSHOT</version>
   </parent>
 
-    <artifactId>child-b</artifactId>
+  <artifactId>child-b</artifactId>
   <version>1.0-SNAPSHOT</version>
 
   <name>Child B</name>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>test</groupId>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>test</groupId>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>test</groupId>
@@ -73,5 +69,4 @@ under the License.
     </profile>
   </profiles>
 
-
 </project>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>org.apache.maven.plugins.clean.its</groupId>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,10 +17,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>test</groupId>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
     Licensed to the Apache Software Foundation (ASF) under one
     or more contributor license agreements.  See the NOTICE file
@@ -16,7 +17,6 @@
     specific language governing permissions and limitations
     under the License.
 -->
-
 <project>
   <build>
     <plugins>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
     Licensed to the Apache Software Foundation (ASF) under one
     or more contributor license agreements.  See the NOTICE file
@@ -16,7 +17,6 @@
     specific language governing permissions and limitations
     under the License.
 -->
-
 <project>
   <build>
     <plugins>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
     Licensed to the Apache Software Foundation (ASF) under one
     or more contributor license agreements.  See the NOTICE file
@@ -16,7 +17,6 @@
     specific language governing permissions and limitations
     under the License.
 -->
-
 <project>
   <build>
     <plugins>
@@ -41,7 +41,7 @@
                 <include>**</include>
               </includes>
               <excludes>
-                <exclude></exclude>
+                <exclude />
               </excludes>
             </fileset>
           </filesets>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
     Licensed to the Apache Software Foundation (ASF) under one
     or more contributor license agreements.  See the NOTICE file
@@ -16,7 +17,6 @@
     specific language governing permissions and limitations
     under the License.
 -->
-
 <project>
   <build>
     <plugins>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
     Licensed to the Apache Software Foundation (ASF) under one
     or more contributor license agreements.  See the NOTICE file
@@ -16,7 +17,6 @@
     specific language governing permissions and limitations
     under the License.
 -->
-
 <project>
   <build>
     <plugins>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
     Licensed to the Apache Software Foundation (ASF) under one
     or more contributor license agreements.  See the NOTICE file
@@ -16,7 +17,6 @@
     specific language governing permissions and limitations
     under the License.
 -->
-
 <project>
   <build>
     <plugins>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
     Licensed to the Apache Software Foundation (ASF) under one
     or more contributor license agreements.  See the NOTICE file
@@ -16,7 +17,6 @@
     specific language governing permissions and limitations
     under the License.
 -->
-
 <project>
   <build>
     <plugins>

From bbdee584e46b6603755eeebda9cdf2ae2438b616 Mon Sep 17 00:00:00 2001
From: Guillaume Nodet <gnodet@gmail.com>
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.
     <guiceVersion>6.0.0</guiceVersion>
     <slf4jVersion>2.0.13</slf4jVersion>
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
-    <version.maven-plugin-testing>4.0.0-alpha-3-SNAPSHOT</version.maven-plugin-testing>
-    <version.maven-plugin-tools>4.0.0-SNAPSHOT</version.maven-plugin-tools>
+    <version.maven-plugin-testing>4.0.0-beta-1</version.maven-plugin-testing>
+    <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>
     <project.build.outputTimestamp>2024-06-16T10:25:11Z</project.build.outputTimestamp>
   </properties>
 

From 99986b8c0517c8170b708369916962eed7e13a93 Mon Sep 17 00:00:00 2001
From: Guillaume Nodet <gnodet@gmail.com>
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.
   </parent>
 
   <artifactId>maven-clean-plugin</artifactId>
-  <version>4.0.0-SNAPSHOT</version>
+  <version>4.0.0-beta-1</version>
   <packaging>maven-plugin</packaging>
 
   <name>Apache Maven Clean Plugin</name>
@@ -42,7 +42,7 @@ under the License.
   <scm>
     <connection>scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git</connection>
     <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git</developerConnection>
-    <tag>HEAD</tag>
+    <tag>maven-clean-plugin-4.0.0-beta-1</tag>
     <url>https://github.com/apache/maven-clean-plugin/tree/${project.scm.tag}</url>
   </scm>
   <issueManagement>
@@ -69,7 +69,7 @@ under the License.
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
     <version.maven-plugin-testing>4.0.0-beta-1</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>
-    <project.build.outputTimestamp>2024-06-16T10:25:11Z</project.build.outputTimestamp>
+    <project.build.outputTimestamp>2024-06-26T08:19:11Z</project.build.outputTimestamp>
   </properties>
 
   <dependencyManagement>

From 648ca78783d0cfa90df30ad94858fb7f1c0ae4b5 Mon Sep 17 00:00:00 2001
From: Guillaume Nodet <gnodet@gmail.com>
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.
   </parent>
 
   <artifactId>maven-clean-plugin</artifactId>
-  <version>4.0.0-beta-1</version>
+  <version>4.0.0-beta-2-SNAPSHOT</version>
   <packaging>maven-plugin</packaging>
 
   <name>Apache Maven Clean Plugin</name>
@@ -42,7 +42,7 @@ under the License.
   <scm>
     <connection>scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git</connection>
     <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git</developerConnection>
-    <tag>maven-clean-plugin-4.0.0-beta-1</tag>
+    <tag>HEAD</tag>
     <url>https://github.com/apache/maven-clean-plugin/tree/${project.scm.tag}</url>
   </scm>
   <issueManagement>
@@ -69,7 +69,7 @@ under the License.
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
     <version.maven-plugin-testing>4.0.0-beta-1</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>
-    <project.build.outputTimestamp>2024-06-26T08:19:11Z</project.build.outputTimestamp>
+    <project.build.outputTimestamp>2024-06-26T08:19:25Z</project.build.outputTimestamp>
   </properties>
 
   <dependencyManagement>

From 87a7adc5b088e437de1343b23a54528fcbfd790a Mon Sep 17 00:00:00 2001
From: Michael Osipov <michaelo@apache.org>
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.
 
 <project xmlns="http://maven.apache.org/DECORATION/1.8.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/DECORATION/1.8.0 https://maven.apache.org/xsd/decoration-1.8.0.xsd">
-  <skin>
-    <groupId>org.apache.maven.skins</groupId>
-    <artifactId>maven-fluido-skin</artifactId>
-    <version>1.9</version>
-  </skin>
   <body>
     <menu name="Overview">
       <item name="Introduction" href="index.html"/>

From d4f554fa40e5513cd5e22f685764948ac6efed7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Herv=C3=A9=20Boutemy?= <hboutemy@apache.org>
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 <s.jaranowski@gmail.com>
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 <gnodet@gmail.com>
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.
   </distributionManagement>
 
   <properties>
-    <mavenVersion>4.0.0-beta-3</mavenVersion>
+    <mavenVersion>4.0.0-beta-5</mavenVersion>
     <javaVersion>17</javaVersion>
     <guavaVersion>33.2.1-jre</guavaVersion>
     <guiceVersion>6.0.0</guiceVersion>
     <slf4jVersion>2.0.13</slf4jVersion>
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
-    <version.maven-plugin-testing>4.0.0-beta-1</version.maven-plugin-testing>
+    <version.maven-plugin-testing>4.0.0-beta-2</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>
+    <version.maven-site-plugin>4.0.0-M16</version.maven-site-plugin>
+    <version.maven-fluido-skin>2.0.0-M11</version.maven-fluido-skin>
     <project.build.outputTimestamp>2024-06-26T08:19:25Z</project.build.outputTimestamp>
   </properties>
 
@@ -119,6 +121,12 @@ under the License.
       <artifactId>junit-jupiter-api</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-api-impl</artifactId>
+      <version>${mavenVersion}</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.maven</groupId>
       <artifactId>maven-core</artifactId>

From 155e5dc678ef4dde961582952a5d5dc44a1d6caf Mon Sep 17 00:00:00 2001
From: Elliotte Rusty Harold <elharo@users.noreply.github.com>
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] <support@github.com>
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.
   <parent>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-plugins</artifactId>
-    <version>42</version>
+    <version>43</version>
     <relativePath />
   </parent>
 

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] <support@github.com>
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.
     <javaVersion>17</javaVersion>
     <guavaVersion>33.2.1-jre</guavaVersion>
     <guiceVersion>6.0.0</guiceVersion>
-    <slf4jVersion>2.0.13</slf4jVersion>
+    <slf4jVersion>2.0.16</slf4jVersion>
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
     <version.maven-plugin-testing>4.0.0-beta-2</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>

From 4a9ad244c13c75995c4e63619fb72744ac81b03e Mon Sep 17 00:00:00 2001
From: Sylwester Lachiewicz <slachiewicz@apache.org>
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 <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <slachiewicz@apache.org>
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 <elharo@users.noreply.github.com>
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 <code>null</code>.
-     * @param link The path to the link, must not be <code>null</code>.
-     * @return <code>true</code> if the symlink could be created, <code>false</code> 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 <slachiewicz@apache.org>
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 <peter.de.maeyer@gmail.com>
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.
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
     <version.maven-plugin-testing>4.0.0-beta-2</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>
-    <version.maven-site-plugin>4.0.0-M16</version.maven-site-plugin>
-    <version.maven-fluido-skin>2.0.0-M11</version.maven-fluido-skin>
+    <version.maven-site-plugin>3.21.0</version.maven-site-plugin>
+    <version.maven-fluido-skin>2.0.1</version.maven-fluido-skin>
     <project.build.outputTimestamp>2024-06-26T08:19:25Z</project.build.outputTimestamp>
   </properties>
 

From 63f2ac75eb9fbd9c7db5dc1a417fa8c8495d029f Mon Sep 17 00:00:00 2001
From: Slawomir Jaranowski <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <peter.de.maeyer@gmail.com>
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.
       <version>${guiceVersion}</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <version>4.11.0</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-simple</artifactId>
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 <code>null</code> 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 <code>null</code> 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 <code>null</code>. Non-existing directories will be silently
-     *            ignored.
-     * @param selector The selector used to determine what contents to delete, may be <code>null</code> 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 <code>failOnError</code> is <code>true</code>.
+     * @param basedir the directory to delete, must not be <code>null</code>. Non-existing directories will be silently
+     *            ignored
+     * @param selector the selector used to determine what contents to delete, may be <code>null</code> 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 <code>failOnError</code> is <code>true</code>
      */
     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 <code>null</code>. If <code>followSymlinks</code> is
-     *            <code>false</code>, it is assumed that the parent file is canonical.
-     * @param pathname The relative pathname of the file, using {@link File#separatorChar}, must not be
-     *            <code>null</code>.
-     * @param selector The selector used to determine what contents to delete, may be <code>null</code> 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 <code>null</code>.
-     * @throws IOException If a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>.
+     * @param file the file/directory to delete, must not be <code>null</code>. If <code>followSymlinks</code> is
+     *            <code>false</code>, it is assumed that the parent file is canonical
+     * @param pathname the relative pathname of the file, using {@link File#separatorChar}, must not be
+     *            <code>null</code>
+     * @param selector the selector used to determine what contents to delete, may be <code>null</code> 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 <code>null</code>
+     * @throws IOException if a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>
      */
     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 <code>null</code>
+     * @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 <code>0</code> if the file was deleted, <code>1</code> otherwise
+     * @throws IOException if a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>
+     */
     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<PosixFilePermission> 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<PosixFilePermission> 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<PosixFilePermission> 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<AccessDeniedException> cause1 = ArgumentCaptor.forClass(AccessDeniedException.class);
+        inOrder.verify(log).warn(eq("Failed to delete " + file), cause1.capture());
+        assertEquals(file.toString(), cause1.getValue().getMessage());
+        ArgumentCaptor<DirectoryNotEmptyException> 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<PosixFilePermission> 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 <s.jaranowski@gmail.com>
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.
     <url>https://github.com/apache/maven-clean-plugin/tree/${project.scm.tag}</url>
   </scm>
   <issueManagement>
-    <system>JIRA</system>
-    <url>https://issues.apache.org/jira/browse/MCLEAN</url>
+    <system>GitHub</system>
+    <url>https://github.com/apache/maven-clean-plugin/issues</url>
   </issueManagement>
   <ciManagement>
     <system>Jenkins</system>

From b0b6505f17bfa9783f58de6151eb5a6edf051e64 Mon Sep 17 00:00:00 2001
From: Slawomir Jaranowski <s.jaranowski@gmail.com>
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] <support@github.com>
---
 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.
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <version>4.11.0</version>
+      <version>5.15.2</version>
       <scope>test</scope>
     </dependency>
     <dependency>

From ccc3f166a2608c704e0ec6c6cf9c5de2de7a60c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matthias=20B=C3=BCnger?= <Bukama@users.noreply.github.com>
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.
   <body>
     <menu name="Overview">
       <item name="Introduction" href="index.html"/>
-      <item name="Goals" href="plugin-info.html"/>
+      <item name="Plugin Documentation" href="plugin-info.html"/>
       <item name="Usage" href="usage.html"/>
       <item name="FAQ" href="faq.html"/>
       <!-- According to https://issues.apache.org/jira/browse/MNGSITE-152 -->

From 5f7f19525402d97683826581ec299f2545abcefa Mon Sep 17 00:00:00 2001
From: Slawomir Jaranowski <s.jaranowski@gmail.com>
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 <gnodet@gmail.com>
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.
   </distributionManagement>
 
   <properties>
-    <mavenVersion>4.0.0-beta-5</mavenVersion>
+    <mavenVersion>4.0.0-rc-2</mavenVersion>
     <javaVersion>17</javaVersion>
     <guavaVersion>33.2.1-jre</guavaVersion>
     <guiceVersion>6.0.0</guiceVersion>
     <slf4jVersion>2.0.16</slf4jVersion>
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
-    <version.maven-plugin-testing>4.0.0-beta-2</version.maven-plugin-testing>
+    <version.maven-plugin-testing>4.0.0-beta-3</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>
     <version.maven-site-plugin>3.21.0</version.maven-site-plugin>
     <version.maven-fluido-skin>2.0.1</version.maven-fluido-skin>
@@ -99,7 +99,7 @@ under the License.
     </dependency>
     <dependency>
       <groupId>org.apache.maven</groupId>
-      <artifactId>maven-api-meta</artifactId>
+      <artifactId>maven-api-annotations</artifactId>
       <version>${mavenVersion}</version>
       <scope>provided</scope>
     </dependency>
@@ -123,7 +123,7 @@ under the License.
     </dependency>
     <dependency>
       <groupId>org.apache.maven</groupId>
-      <artifactId>maven-api-impl</artifactId>
+      <artifactId>maven-impl</artifactId>
       <version>${mavenVersion}</version>
       <scope>test</scope>
     </dependency>

From 051ab19675f859867a9ccc3307902ab2571c98bf Mon Sep 17 00:00:00 2001
From: Slawomir Jaranowski <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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.
   <distributionManagement>
     <site>
       <id>apache.website</id>
-      <url>scm:svn:https://svn.apache.org/repos/asf/maven/website/components/${maven.site.path}</url>
+      <url>scm:svn:https://svn.apache.org/repos/asf/maven/website/components/${maven4x.site.path}</url>
     </site>
   </distributionManagement>
 
@@ -72,6 +72,9 @@ under the License.
     <version.maven-site-plugin>3.21.0</version.maven-site-plugin>
     <version.maven-fluido-skin>2.0.1</version.maven-fluido-skin>
     <project.build.outputTimestamp>2024-06-26T08:19:25Z</project.build.outputTimestamp>
+
+    <!-- TODO remove with next parent -->
+    <maven4x.site.path>plugins-archives/${project.artifactId}-LATEST-4.x</maven4x.site.path>
   </properties>
 
   <dependencyManagement>

From 189df6694101444001fc18ba6dd0f0be1edc3267 Mon Sep 17 00:00:00 2001
From: Slawomir Jaranowski <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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] <support@github.com>
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.
   <properties>
     <mavenVersion>4.0.0-rc-2</mavenVersion>
     <javaVersion>17</javaVersion>
-    <guavaVersion>33.2.1-jre</guavaVersion>
+    <guavaVersion>33.4.0-jre</guavaVersion>
     <guiceVersion>6.0.0</guiceVersion>
     <slf4jVersion>2.0.16</slf4jVersion>
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>

From bcad805b60dff1f9f1acc7eea89b003ab08dd482 Mon Sep 17 00:00:00 2001
From: Slawomir Jaranowski <s.jaranowski@gmail.com>
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.
   <distributionManagement>
     <site>
       <id>apache.website</id>
-      <url>scm:svn:https://svn.apache.org/repos/asf/maven/website/components/${maven4x.site.path}</url>
+      <url>scm:svn:https://svn.apache.org/repos/asf/maven/website/components/${maven.site.path}</url>
     </site>
   </distributionManagement>
 
@@ -73,8 +73,10 @@ under the License.
     <version.maven-fluido-skin>2.0.1</version.maven-fluido-skin>
     <project.build.outputTimestamp>2024-06-26T08:19:25Z</project.build.outputTimestamp>
 
-    <!-- TODO remove with next parent -->
+    <!-- TODO remove property maven4x.site.path with next parent -->
     <maven4x.site.path>plugins-archives/${project.artifactId}-LATEST-4.x</maven4x.site.path>
+    <!-- 4.x publish -->
+    <maven.site.path>${maven4x.site.path}</maven.site.path>
   </properties>
 
   <dependencyManagement>

From eeaa3a16185aca5608e0a5ecbc16096120931b80 Mon Sep 17 00:00:00 2001
From: Slawomir Jaranowski <s.jaranowski@gmail.com>
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.
   </parent>
 
   <artifactId>maven-clean-plugin</artifactId>
-  <version>4.0.0-beta-2-SNAPSHOT</version>
+  <version>4.0.0-beta-2</version>
   <packaging>maven-plugin</packaging>
 
   <name>Apache Maven Clean Plugin</name>
@@ -42,7 +42,7 @@ under the License.
   <scm>
     <connection>scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git</connection>
     <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git</developerConnection>
-    <tag>HEAD</tag>
+    <tag>maven-clean-plugin-4.0.0-beta-2</tag>
     <url>https://github.com/apache/maven-clean-plugin/tree/${project.scm.tag}</url>
   </scm>
   <issueManagement>
@@ -71,7 +71,7 @@ under the License.
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>
     <version.maven-site-plugin>3.21.0</version.maven-site-plugin>
     <version.maven-fluido-skin>2.0.1</version.maven-fluido-skin>
-    <project.build.outputTimestamp>2024-06-26T08:19:25Z</project.build.outputTimestamp>
+    <project.build.outputTimestamp>2025-02-15T18:29:07Z</project.build.outputTimestamp>
 
     <!-- TODO remove property maven4x.site.path with next parent -->
     <maven4x.site.path>plugins-archives/${project.artifactId}-LATEST-4.x</maven4x.site.path>

From 4d58bc6bc1b04b573afe2c1e83f96792b50388ff Mon Sep 17 00:00:00 2001
From: Slawomir Jaranowski <s.jaranowski@gmail.com>
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.
   </parent>
 
   <artifactId>maven-clean-plugin</artifactId>
-  <version>4.0.0-beta-2</version>
+  <version>4.0.0-beta-3-SNAPSHOT</version>
   <packaging>maven-plugin</packaging>
 
   <name>Apache Maven Clean Plugin</name>
@@ -42,7 +42,7 @@ under the License.
   <scm>
     <connection>scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git</connection>
     <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/maven-clean-plugin.git</developerConnection>
-    <tag>maven-clean-plugin-4.0.0-beta-2</tag>
+    <tag>HEAD</tag>
     <url>https://github.com/apache/maven-clean-plugin/tree/${project.scm.tag}</url>
   </scm>
   <issueManagement>
@@ -71,7 +71,7 @@ under the License.
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>
     <version.maven-site-plugin>3.21.0</version.maven-site-plugin>
     <version.maven-fluido-skin>2.0.1</version.maven-fluido-skin>
-    <project.build.outputTimestamp>2025-02-15T18:29:07Z</project.build.outputTimestamp>
+    <project.build.outputTimestamp>2025-02-15T18:29:35Z</project.build.outputTimestamp>
 
     <!-- TODO remove property maven4x.site.path with next parent -->
     <maven4x.site.path>plugins-archives/${project.artifactId}-LATEST-4.x</maven4x.site.path>

From 12ac3d395ba6f52d18323701c5b441ec6f3f7969 Mon Sep 17 00:00:00 2001
From: Elliotte Rusty Harold <elharo@users.noreply.github.com>
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 <elharo@users.noreply.github.com>
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 <peter.de.maeyer@gmail.com>
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 {
      * <code>${maven.multiModuleProjectDirectory}/target/.clean</code> directory will be used.  If the
      * <code>${build.directory}</code> 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: <code>background</code> to start deletion immediately and
      * waiting for all files to be deleted when the session ends, <code>at-end</code> to indicate that the actual
      * deletion should be performed synchronously when the session ends, or <code>defer</code> 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 <bmarwell@apache.org>
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] <support@github.com>
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.
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <version>5.15.2</version>
+      <version>5.16.0</version>
       <scope>test</scope>
     </dependency>
     <dependency>

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] <support@github.com>
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.
     <javaVersion>17</javaVersion>
     <guavaVersion>33.4.0-jre</guavaVersion>
     <guiceVersion>6.0.0</guiceVersion>
-    <slf4jVersion>2.0.16</slf4jVersion>
+    <slf4jVersion>2.0.17</slf4jVersion>
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
     <version.maven-plugin-testing>4.0.0-beta-3</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>

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] <support@github.com>
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.
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <version>5.16.0</version>
+      <version>5.16.1</version>
       <scope>test</scope>
     </dependency>
     <dependency>

From 554aaa8d419007d71de69d2b4fc075d42938ee63 Mon Sep 17 00:00:00 2001
From: Benjamin Marwell <bmarwell@apache.org>
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] <support@github.com>
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.
   <properties>
     <mavenVersion>4.0.0-rc-2</mavenVersion>
     <javaVersion>17</javaVersion>
-    <guavaVersion>33.4.0-jre</guavaVersion>
+    <guavaVersion>33.4.6-jre</guavaVersion>
     <guiceVersion>6.0.0</guiceVersion>
     <slf4jVersion>2.0.17</slf4jVersion>
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>

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] <support@github.com>
---
 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.
   <parent>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-plugins</artifactId>
-    <version>43</version>
+    <version>44</version>
     <relativePath />
   </parent>
 

From ecbd5db22d1ba50d14ce70d63dfaada770e78ae3 Mon Sep 17 00:00:00 2001
From: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
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.
     <maven.site.path>${maven4x.site.path}</maven.site.path>
   </properties>
 
-  <dependencyManagement>
-    <dependencies>
-      <dependency>
-        <groupId>com.google.guava</groupId>
-        <artifactId>guava</artifactId>
-        <version>${guavaVersion}</version>
-      </dependency>
-    </dependencies>
-  </dependencyManagement>
-
   <dependencies>
     <dependency>
       <groupId>org.apache.maven</groupId>
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() {
      * <I>[included files]</I>, excluded: <I>[excluded files]</I>)"</code>
      * @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] <support@github.com>

* 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] <support@github.com>

* Build and test with Maven 4.0.0-rc-3 too

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Guillaume Nodet <gnodet@gmail.com>
---
 .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.
   </distributionManagement>
 
   <properties>
-    <mavenVersion>4.0.0-rc-2</mavenVersion>
+    <mavenVersion>4.0.0-rc-3</mavenVersion>
     <javaVersion>17</javaVersion>
     <guavaVersion>33.4.6-jre</guavaVersion>
     <guiceVersion>6.0.0</guiceVersion>
     <slf4jVersion>2.0.17</slf4jVersion>
     <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
-    <version.maven-plugin-testing>4.0.0-beta-3</version.maven-plugin-testing>
+    <version.maven-plugin-testing>4.0.0-beta-4</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>
     <version.maven-site-plugin>3.21.0</version.maven-site-plugin>
     <version.maven-fluido-skin>2.0.1</version.maven-fluido-skin>

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] <support@github.com>
---
 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.
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <version>5.16.1</version>
+      <version>5.17.0</version>
       <scope>test</scope>
     </dependency>
     <dependency>

From ea9325b2cfc119a88ac5957fe29cd806d4695e5e Mon Sep 17 00:00:00 2001
From: Slawomir Jaranowski <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <martin.desruisseaux@geomatys.com>
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.
       <scope>provided</scope>
     </dependency>
 
-    <dependency>
-      <groupId>org.codehaus.plexus</groupId>
-      <artifactId>plexus-utils</artifactId>
-    </dependency>
-
     <!-- Test -->
     <dependency>
       <groupId>org.apache.maven.plugin-testing</groupId>
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 <code>true</code> if verbose output is enabled, <code>false</code> 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<Path> {
+    /**
+     * Whether the host operating system is from the Windows family.
+     */
+    private static final boolean ON_WINDOWS = (File.separatorChar == '\\');
 
     private static final SessionData.Key<Path> 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 <code>null</code> 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<CharSequence> l1, BiConsumer<CharSequence, Throwable> 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 <code>null</code>. Non-existing directories will be silently
-     *            ignored
-     * @param selector the selector used to determine what contents to delete, may be <code>null</code> 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 <code>failOnError</code> is <code>true</code>
+     * @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 <code>null</code>. If <code>followSymlinks</code> is
-     *            <code>false</code>, it is assumed that the parent file is canonical
-     * @param pathname the relative pathname of the file, using {@link File#separatorChar}, must not be
-     *            <code>null</code>
-     * @param selector the selector used to determine what contents to delete, may be <code>null</code> 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 <code>null</code>
-     * @throws IOException if a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>
+     * 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<Path> 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 <code>null</code>
-     * @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 <code>0</code> if the file was deleted, <code>1</code> otherwise
-     * @throws IOException if a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>
+     * @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}.
+     *
+     * <p>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.</p>
+     */
+    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
- * <code>org.apache.maven.shared.model.fileset.FileSet</code> 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,
  * <code>"file-set: <I>[directory]</I> (included: <I>[included files]</I>,
  * excluded: <I>[excluded files]</I>)"</code>
@@ -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, <code>"file-set: <I>[directory]</I> (included:
-     * <I>[included files]</I>, excluded: <I>[excluded files]</I>)"</code>
+     * 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, <code>"file-set: <I>[directory]</I> (included:
      * <I>[included files]</I>, excluded: <I>[excluded files]</I>)"</code>
-     * @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.
+ *
+ * <h4>Syntax</h4>
+ * 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:
+ *
+ * <ul>
+ *   <li>The platform-specific separator ({@code '\\'} on Windows) is replaced by {@code '/'}.
+ *       Note that it means that the backslash cannot be used for escaping characters.</li>
+ *   <li>Trailing {@code "/"} is completed as {@code "/**"}.</li>
+ *   <li>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.</li>
+ * </ul>
+ *
+ * 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 <abbr>SCM</abbr> files.
+     *
+     * <p><b>Source:</b> this list is copied from {@code plexus-utils-4.0.2} (released in
+     * September 23, 2024), class {@code org.codehaus.plexus.util.AbstractScanner}.</p>
+     */
+    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 <em>not</em> 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<String>(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<String> 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<String> 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<String>(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 <code>null</code>.
-     * @return <code>true</code> if the given path is selected for deletion, <code>false</code> 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 <code>null</code>.
-     * @return <code>true</code> if the given directory might contain selected paths, <code>false</code> 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<PosixFilePermission> 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<PosixFilePermission> 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<PosixFilePermission> 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<AccessDeniedException> cause1 = ArgumentCaptor.forClass(AccessDeniedException.class);
         inOrder.verify(log).warn(eq("Failed to delete " + file), cause1.capture());
         assertEquals(file.toString(), cause1.getValue().getMessage());
-        ArgumentCaptor<DirectoryNotEmptyException> 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<PosixFilePermission> 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] <support@github.com>
---
 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.
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <version>5.17.0</version>
+      <version>5.18.0</version>
       <scope>test</scope>
     </dependency>
     <dependency>

From 1ae6f51fe9b296ba97c9b48f6dea321587253577 Mon Sep 17 00:00:00 2001
From: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
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 <code>...</code> by {@code ...}.
* Replacements of </br> by <p>...</p>
* 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>test</groupId>
+  <artifactId>read-only</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>Test for clean</name>
+  <description>Check for proper deletion (or not) of read-only files.</description>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-clean-plugin</artifactId>
+        <version>@pom.version@</version>
+        <configuration>
+          <force>true</force>
+          <retryOnError>false</retryOnError>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
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}.
+ *
  * <p>
- * 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>,
- * <code>project.build.outputDirectory</code>, <code>project.build.testOutputDirectory</code>, and
- * <code>project.reporting.outputDirectory</code>.
- * </p>
- * <p>
- * Files outside the default may also be included in the deletion by configuring the <code>filesets</code> tag.
+ * Files outside the default may also be included in the deletion by configuring the {@code filesets} tag.
  * </p>
  *
  * @author <a href="mailto:evenisse@maven.org">Emmanuel Venisse</a>
- * @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 <code>-X</code>). <br/>
-     * Starting with <b>3.0.0</b> the property has been renamed from <code>clean.verbose</code> to
-     * <code>maven.clean.verbose</code>.
+     * global debug flag (compare command line switch {@code -X}).
+     *
+     * <p>Starting with <b>3.0.0</b> the property has been renamed from {@code clean.verbose} to
+     * {@code maven.clean.verbose}.</p>
      *
      * @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 <code>true</code>. <br/>
-     * Starting with <code>3.0.0</code> the property has been renamed from <code>clean.followSymLinks</code> to
-     * <code>maven.clean.followSymLinks</code>.
+     * the project.
+     *
+     * <p>Starting with <b>3.0.0</b> the property has been renamed from {@code clean.followSymLinks} to
+     * {@code maven.clean.followSymLinks}.</p>
      *
      * @since 2.1
      */
@@ -131,9 +130,18 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo {
     private boolean followSymLinks;
 
     /**
-     * Disables the plugin execution. <br/>
-     * Starting with <code>3.0.0</code> the property has been renamed from <code>clean.skip</code> to
-     * <code>maven.clean.skip</code>.
+     * 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.
+     *
+     * <p>Starting with <b>3.0.0</b> the property has been renamed from {@code clean.skip} to
+     * {@code maven.clean.skip}.</p>
      *
      * @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 <code>true</code>,
-     * only the files/directories selected via the parameter {@link #filesets} will be deleted. <br/>
-     * Starting with <b>3.0.0</b> the property has been renamed from <code>clean.excludeDefaultDirectories</code> to
-     * <code>maven.clean.excludeDefaultDirectories</code>.
+     * 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.
+     *
+     * <p>Starting with <b>3.0.0</b> the property has been renamed from {@code clean.excludeDefaultDirectories}
+     * to {@code maven.clean.excludeDefaultDirectories}.</p>
      *
      * @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 <code>true</code>, when the plugin is executed, a directory to
-     * be deleted will be atomically moved inside the <code>maven.clean.fastDir</code> 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 <code>fastDir</code> 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
-     * <code>${maven.multiModuleProjectDirectory}/target/.clean</code> directory will be used.  If the
-     * <code>${build.directory}</code> 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: <code>background</code> to start deletion immediately and
-     * waiting for all files to be deleted when the session ends, <code>at-end</code> to indicate that the actual
-     * deletion should be performed synchronously when the session ends, or <code>defer</code> 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 <code>true</code> if verbose output is enabled, <code>false</code> 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 <code>null</code>.
+     * @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<Path> {
      */
     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<Path> {
 
     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<Path> {
      * @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<Path> {
             @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<Path> {
         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<PosixFilePermission> 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}.
      *
+     * <h4>Auxiliary information as side-effect</h4>
+     * 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<Path> 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<Class<?>, Set<String>>(); // 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<Class<?>, Set<String>> 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<PosixFilePermission> 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<PosixFilePermission> 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<PosixFilePermission> 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<PosixFilePermission> 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] <support@github.com>
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.
   <parent>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-plugins</artifactId>
-    <version>44</version>
+    <version>45</version>
     <relativePath />
   </parent>
 

From 7f8febccf4debca0e2c869f981e5584c479856ef Mon Sep 17 00:00:00 2001
From: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
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.
   </distributionManagement>
 
   <properties>
-    <mavenVersion>4.0.0-rc-3</mavenVersion>
+    <mavenVersion>4.0.0-rc-4</mavenVersion>
     <javaVersion>17</javaVersion>
     <guavaVersion>33.4.6-jre</guavaVersion>
     <guiceVersion>6.0.0</guiceVersion>
     <slf4jVersion>2.0.17</slf4jVersion>
-    <version.maven-invoker-plugin>3.7.0</version.maven-invoker-plugin>
+    <version.maven-invoker-plugin>3.9.1</version.maven-invoker-plugin>
     <version.maven-plugin-testing>4.0.0-beta-4</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>
     <version.maven-site-plugin>3.21.0</version.maven-site-plugin>
@@ -98,6 +98,12 @@ under the License.
       <version>${mavenVersion}</version>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-xml</artifactId>
+      <version>${mavenVersion}</version>
+      <scope>provided</scope>
+    </dependency>
 
     <!-- Test -->
     <dependency>

From 8400a22ac2f0aaa18d027ec976caf1ce5862aab3 Mon Sep 17 00:00:00 2001
From: Sandra Parsick <sparsick@users.noreply.github.com>
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 <sandra@parsick.dev>
---
 .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] <support@github.com>
---
 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.
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <version>5.18.0</version>
+      <version>5.19.0</version>
       <scope>test</scope>
     </dependency>
     <dependency>

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] <support@github.com>
---
 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.
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <version>5.19.0</version>
+      <version>5.20.0</version>
       <scope>test</scope>
     </dependency>
     <dependency>

From ed9cab9ae9fd1b237aa6f0b66c9c3501882d6308 Mon Sep 17 00:00:00 2001
From: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
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<Path> {
     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<Path> {
      * @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 <abbr>SCM</abbr> 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 <martin.desruisseaux@geomatys.com>
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<Path> {
     @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<Path> {
      * @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.
  *
- * <h4>Syntax</h4>
- * 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.
+ * <h2>Syntax</h2>
+ * 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:
  *
  * <ul>
@@ -43,13 +50,17 @@
  *   <li>Trailing {@code "/"} is completed as {@code "/**"}.</li>
  *   <li>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.</li>
+ *   <li>Bracket characters [ ] and { } are escaped.</li>
+ *   <li>On Unix only, the escape character {@code '\\'} is itself escaped.</li>
  * </ul>
  *
- * 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 {
      * <p><b>Source:</b> this list is copied from {@code plexus-utils-4.0.2} (released in
      * September 23, 2024), class {@code org.codehaus.plexus.util.AbstractScanner}.</p>
      */
-    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<String> 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 <em>not</em> 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 <abbr>SCM</abbr> 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<String> includes, Collection<String> 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 <code>"**&sol;project.pj"</code>,
+     * <code>"**&sol;.DS_Store"</code> 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.
+     *
+     * <h4>Implementation note</h4>
+     * 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<String> effectiveExcludes(
+            Collection<String> 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<String> 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<String> 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<String>(patterns.length);
+        final var normalized = new LinkedHashSet<String>(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<String> patterns, final String pattern, int end) {
@@ -277,13 +469,11 @@ private static void addPatternsWithOneDirRemoved(final Set<String> 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<String> 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<String>(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 <martin.desruisseaux@geomatys.com>
Date: Sun, 26 Oct 2025 18:13:13 +0100
Subject: [PATCH 73/90] Delete also the directories specified in `<targetPath>`
 (#276)

In the list of directories to delete, include the values specified in the `<targetPath>` child of `<source>` 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 @@
+<!---
+ 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.
+-->
+
+The purpose of this file is to verify that the directory specified by `<targetPath>` 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
+  <modelVersion>4.1.0</modelVersion>
+
+  <groupId>test</groupId>
+  <artifactId>sources-targetPath</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>Test &lt;targetPath&gt; cleaning</name>
+  <description>Check for proper cleaning of output files in directories specified by &lt;targetPath&gt;.</description>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-clean-plugin</artifactId>
+        <version>@pom.version@</version>
+      </plugin>
+    </plugins>
+
+    <sources>
+      <source>
+        <targetPath>${project.basedir}/outside-target</targetPath>
+      </source>
+    </sources>
+  </build>
+
+</project>
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 @@
+<!---
+ 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.
+-->
+
+The purpose of this file is to verify that the directory
+specified by `<testOutputDirectory>` 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()
+     * <ol>
+     *   <li>Build directory ({@code ${project.build.directory}})</li>
+     *   <li>Main classes directory ({@code ${project.build.outputDirectory}})</li>
+     *   <li>Test classes directory ({@code ${project.build.testOutputDirectory}})</li>
+     *   <li>Directories specified in the {@code <targetPath>} child of {@code <source>} elements</li>
+     *   <li>Additional file-sets</li>
+     * </ol>
+     *
+     * 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<Path> 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<Path>(Arrays.asList(directory, outputDirectory, testOutputDirectory));
+
+        // Directories declared in the Maven 4 way, with <source> 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 <slachiewicz@apache.org>
Date: Fri, 7 Nov 2025 08:47:06 +0100
Subject: [PATCH 74/90] JUnit Jupiter best practices (#279)

Co-authored-by: Moderne <team@moderne.io>
---
 .../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 <martin.desruisseaux@geomatys.com>
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.
   </distributionManagement>
 
   <properties>
-    <mavenVersion>4.0.0-rc-4</mavenVersion>
+    <mavenVersion>4.0.0-rc-5</mavenVersion>
     <javaVersion>17</javaVersion>
     <guavaVersion>33.4.6-jre</guavaVersion>
     <guiceVersion>6.0.0</guiceVersion>

From 00107cb437b722cef2ffe4d9b57619bc35504c6f Mon Sep 17 00:00:00 2001
From: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
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<Path> {
     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<Path> {
      */
     private boolean isBaseDirectoryExcluded;
 
+    /**
+     * Whether to follow symbolic links while deleting files from the directories.
+     * This value is specified by the <abbr>MOJO</abbr> 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<Path> {
 
     /**
      * 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<Path> {
         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.
      *
+     * <h4>Configuration</h4>
+     * 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,
  * <code>"file-set: <I>[directory]</I> (included: <I>[included files]</I>,
  * excluded: <I>[excluded files]</I>)"</code>
@@ -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<String> 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<String> 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<String> listWithoutNull(String[] patterns) {
+        if (patterns == null) {
+            return List.of();
+        }
+        var list = new ArrayList<String>(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<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]);
-            }
+        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.
- *
- * <h2>Syntax</h2>
- * 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:
- *
- * <ul>
- *   <li>The platform-specific separator ({@code '\\'} on Windows) is replaced by {@code '/'}.
- *       Note that it means that the backslash cannot be used for escaping characters.</li>
- *   <li>Trailing {@code "/"} is completed as {@code "/**"}.</li>
- *   <li>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.</li>
- *   <li>Bracket characters [ ] and { } are escaped.</li>
- *   <li>On Unix only, the escape character {@code '\\'} is itself escaped.</li>
- * </ul>
- *
- * 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 <abbr>SCM</abbr> files.
-     *
-     * <p><b>Source:</b> this list is copied from {@code plexus-utils-4.0.2} (released in
-     * September 23, 2024), class {@code org.codehaus.plexus.util.AbstractScanner}.</p>
-     */
-    private static final List<String> 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 <em>not</em> 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 <abbr>SCM</abbr> patterns
-     */
-    PathSelector(Path directory, Collection<String> includes, Collection<String> 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 <code>"**&sol;project.pj"</code>,
-     * <code>"**&sol;.DS_Store"</code> 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.
-     *
-     * <h4>Implementation note</h4>
-     * 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<String> effectiveExcludes(
-            Collection<String> 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<String> 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<String> 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<String>(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<String> 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<String> 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<String>(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<PosixFilePermission> 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<PosixFilePermission> 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<PosixFilePermission> 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<PosixFilePermission> 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 <martin.desruisseaux@geomatys.com>
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<Path> 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 <martin.desruisseaux@geomatys.com>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>test</groupId>
+  <artifactId>fast-delete</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>Fast delete</name>
+  <description>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.</description>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-clean-plugin</artifactId>
+        <version>@project.version@</version>
+        <configuration>
+          <fast>true</fast>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
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.
   <version>1.0-SNAPSHOT</version>
 
   <name>Fast delete</name>
-  <description>Check that fast delete is invoked.</description>
+  <description>Check that fast delete is invoked. For making this test simpler,
+    this test uses a `.fastdir` directory outside the `target` directory.</description>
 
   <properties>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
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.
+ *
+ * <h4>Limitations</h4>
+ * 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<Path> 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<Path> 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.
+     *
+     * <h4>Thread safety</h4>
+     * 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 @@
  * </p>
  *
  * @author <a href="mailto:evenisse@maven.org">Emmanuel Venisse</a>
+ * @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.
      *
+     * <p>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.</p>
+     *
      * @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.
+ *
+ * <h4>Limitations</h4>
+ * 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<Path> {
+class Cleaner implements FileVisitor<Path> {
     /**
      * Whether the host operating system is from the Windows family.
      */
     private static final boolean ON_WINDOWS = (File.separatorChar == '\\');
 
-    private static final SessionData.Key<Path> 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<Path> {
      */
     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<Path> {
      * 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<Path> {
      * 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.
      *
+     * <h4>Configuration</h4>
+     * {@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.
      *
      * <h4>Configuration</h4>
      * 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<Path> 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<Path> 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<PosixFilePermission> 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<PosixFilePermission> 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<PosixFilePermission> 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<PosixFilePermission> 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] <support@github.com>
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.
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <version>5.20.0</version>
+      <version>5.21.0</version>
       <scope>test</scope>
     </dependency>
     <dependency>

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] <support@github.com>
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.
   <parent>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-plugins</artifactId>
-    <version>45</version>
+    <version>46</version>
     <relativePath />
   </parent>
 

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] <support@github.com>
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.
   <parent>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-plugins</artifactId>
-    <version>46</version>
+    <version>47</version>
     <relativePath />
   </parent>
 

From f29258f233fc8d7d315b2668d15fd01692c1dcc7 Mon Sep 17 00:00:00 2001
From: Sylwester Lachiewicz <slachiewicz@apache.org>
Date: Sat, 14 Feb 2026 15:26:31 +0100
Subject: [PATCH 82/90] Mockito improvements (#298)

--

Co-authored-by: Moderne <team@moderne.io>
---
 .../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<AccessDeniedException> 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] <support@github.com>
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.
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <version>5.21.0</version>
+      <version>5.23.0</version>
       <scope>test</scope>
     </dependency>
     <dependency>

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] <support@github.com>
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.
   <parent>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-plugins</artifactId>
-    <version>47</version>
+    <version>48</version>
     <relativePath />
   </parent>
 

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] <support@github.com>
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.
     <javaVersion>17</javaVersion>
     <guavaVersion>33.4.6-jre</guavaVersion>
     <guiceVersion>6.0.0</guiceVersion>
-    <slf4jVersion>2.0.17</slf4jVersion>
+    <slf4jVersion>2.0.18</slf4jVersion>
     <version.maven-invoker-plugin>3.9.1</version.maven-invoker-plugin>
     <version.maven-plugin-testing>4.0.0-beta-4</version.maven-plugin-testing>
     <version.maven-plugin-tools>4.0.0-beta-1</version.maven-plugin-tools>

From 250f5f69c862a29577fd2f5be40c086a77a8e75a Mon Sep 17 00:00:00 2001
From: Slawomir Jaranowski <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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 <s.jaranowski@gmail.com>
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] <support@github.com>
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.
   <parent>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-plugins</artifactId>
-    <version>48</version>
+    <version>49</version>
     <relativePath />
   </parent>
 

From eaae1b8f269c15ad59b21095bf5a32f8271ce132 Mon Sep 17 00:00:00 2001
From: Jarek Potiuk <jarek@potiuk.com>
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 @@
+<!--
+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.
+-->
+
+# 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 @@
+<!--
+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.
+-->
+
+# 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 <https://github.com/apache/maven/blob/master/THREAT_MODEL.md>.
