You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/10/19 13:37:49 UTC

(maven-dependency-plugin) 02/04: [MDEP-619] Purge only snapshot versions when resolutionFuzziness is artifactId or groupId

This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-dependency-plugin.git

commit 88a176821d876954c233c74e374b9add220e5070
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Tue Oct 13 22:55:51 2026 +0200

    [MDEP-619] Purge only snapshot versions when resolutionFuzziness is artifactId or groupId
    
    With snapshotsOnly the artifactId and groupId fuzziness deleted the whole directory, release versions included. Only the snapshot version directories below it are deleted now.
---
 .../invoker.properties                             |  18 ++
 .../pom.xml                                        |  47 +++++
 .../setup.groovy                                   |  35 ++++
 .../test.properties                                |  20 ++
 .../verify.groovy                                  |  41 +++++
 .../invoker.properties                             |  18 ++
 .../pom.xml                                        |  47 +++++
 .../setup.groovy                                   |  35 ++++
 .../test.properties                                |  20 ++
 .../verify.groovy                                  |  41 +++++
 .../dependency/PurgeLocalRepositoryMojo.java       |  61 +++++--
 .../dependency/PurgeLocalRepositoryMojoTest.java   | 202 +++++++++++++++++++++
 12 files changed, 568 insertions(+), 17 deletions(-)

diff --git a/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/invoker.properties b/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/invoker.properties
new file mode 100644
index 00000000..3c1911e6
--- /dev/null
+++ b/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/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 = ${project.groupId}:${project.artifactId}:${project.version}:purge-local-repository
diff --git a/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/pom.xml b/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/pom.xml
new file mode 100644
index 00000000..85e2f4d9
--- /dev/null
+++ b/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/pom.xml
@@ -0,0 +1,47 @@
+<?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/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.its.dependency</groupId>
+  <artifactId>test</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>Test</name>
+  <description>
+    Test dependency:purge-local-repository with snapshotsOnly and resolutionFuzziness=artifactId (MDEP-619)
+  </description>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.its.dependency</groupId>
+      <artifactId>snap-purged</artifactId>
+      <version>2.0-SNAPSHOT</version>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/setup.groovy b/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/setup.groovy
new file mode 100644
index 00000000..f30cd4e4
--- /dev/null
+++ b/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/setup.groovy
@@ -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.
+ */
+
+String groupPath = "org/apache/maven/its/dependency/"
+
+void createJar( String relativePath )
+{
+    File jar = new File( localRepositoryPath, relativePath )
+    jar.getParentFile().mkdirs()
+    jar.createNewFile()
+}
+
+createJar( groupPath + "snap-purged/1.0-SNAPSHOT/snap-purged-1.0-SNAPSHOT.jar" )
+createJar( groupPath + "snap-purged/2.0-SNAPSHOT/snap-purged-2.0-SNAPSHOT.jar" )
+createJar( groupPath + "snap-purged/1.0/snap-purged-1.0.jar" )
+createJar( groupPath + "sibling/1.0-SNAPSHOT/sibling-1.0-SNAPSHOT.jar" )
+createJar( groupPath + "sibling/1.0/sibling-1.0.jar" )
+
+return true
diff --git a/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/test.properties b/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/test.properties
new file mode 100644
index 00000000..6e1454a5
--- /dev/null
+++ b/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/test.properties
@@ -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.
+
+reResolve = false
+snapshotsOnly = true
+resolutionFuzziness = artifactId
diff --git a/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/verify.groovy b/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/verify.groovy
new file mode 100644
index 00000000..9e054fa0
--- /dev/null
+++ b/src/it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/verify.groovy
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+String groupPath = "org/apache/maven/its/dependency/"
+
+void checkAbsence( String path )
+{
+    File file = new File( localRepositoryPath, path )
+    assert !file.exists() : "Not purged: " + file
+}
+
+void checkPresence( String path )
+{
+    File file = new File( localRepositoryPath, path )
+    assert file.exists() : "Purged although not a snapshot: " + file
+}
+
+// every snapshot of the artifact is deleted, the releases of the artifact and the sibling artifact are kept
+checkAbsence( groupPath + "snap-purged/1.0-SNAPSHOT" )
+checkAbsence( groupPath + "snap-purged/2.0-SNAPSHOT" )
+checkPresence( groupPath + "snap-purged/1.0/snap-purged-1.0.jar" )
+checkPresence( groupPath + "sibling/1.0-SNAPSHOT/sibling-1.0-SNAPSHOT.jar" )
+checkPresence( groupPath + "sibling/1.0/sibling-1.0.jar" )
+
+return true
diff --git a/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/invoker.properties b/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/invoker.properties
new file mode 100644
index 00000000..3c1911e6
--- /dev/null
+++ b/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/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 = ${project.groupId}:${project.artifactId}:${project.version}:purge-local-repository
diff --git a/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/pom.xml b/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/pom.xml
new file mode 100644
index 00000000..b431d0c0
--- /dev/null
+++ b/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/pom.xml
@@ -0,0 +1,47 @@
+<?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/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.its.dependency</groupId>
+  <artifactId>test</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>Test</name>
+  <description>
+    Test dependency:purge-local-repository with snapshotsOnly and resolutionFuzziness=groupId (MDEP-619)
+  </description>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.its.dependency</groupId>
+      <artifactId>snap-purged</artifactId>
+      <version>2.0-SNAPSHOT</version>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/setup.groovy b/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/setup.groovy
new file mode 100644
index 00000000..f30cd4e4
--- /dev/null
+++ b/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/setup.groovy
@@ -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.
+ */
+
+String groupPath = "org/apache/maven/its/dependency/"
+
+void createJar( String relativePath )
+{
+    File jar = new File( localRepositoryPath, relativePath )
+    jar.getParentFile().mkdirs()
+    jar.createNewFile()
+}
+
+createJar( groupPath + "snap-purged/1.0-SNAPSHOT/snap-purged-1.0-SNAPSHOT.jar" )
+createJar( groupPath + "snap-purged/2.0-SNAPSHOT/snap-purged-2.0-SNAPSHOT.jar" )
+createJar( groupPath + "snap-purged/1.0/snap-purged-1.0.jar" )
+createJar( groupPath + "sibling/1.0-SNAPSHOT/sibling-1.0-SNAPSHOT.jar" )
+createJar( groupPath + "sibling/1.0/sibling-1.0.jar" )
+
+return true
diff --git a/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/test.properties b/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/test.properties
new file mode 100644
index 00000000..e98dbcc1
--- /dev/null
+++ b/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/test.properties
@@ -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.
+
+reResolve = false
+snapshotsOnly = true
+resolutionFuzziness = groupId
diff --git a/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/verify.groovy b/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/verify.groovy
new file mode 100644
index 00000000..eefb6dfc
--- /dev/null
+++ b/src/it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/verify.groovy
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+String groupPath = "org/apache/maven/its/dependency/"
+
+void checkAbsence( String path )
+{
+    File file = new File( localRepositoryPath, path )
+    assert !file.exists() : "Not purged: " + file
+}
+
+void checkPresence( String path )
+{
+    File file = new File( localRepositoryPath, path )
+    assert file.exists() : "Purged although not a snapshot: " + file
+}
+
+// the snapshots of every artifact of the group are deleted, all the releases are kept
+checkAbsence( groupPath + "snap-purged/1.0-SNAPSHOT" )
+checkAbsence( groupPath + "snap-purged/2.0-SNAPSHOT" )
+checkAbsence( groupPath + "sibling/1.0-SNAPSHOT" )
+checkPresence( groupPath + "snap-purged/1.0/snap-purged-1.0.jar" )
+checkPresence( groupPath + "sibling/1.0/sibling-1.0.jar" )
+
+return true
diff --git a/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java b/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java
index 8325af29..4f9ccf90 100644
--- a/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java
+++ b/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java
@@ -24,6 +24,7 @@ import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -570,20 +571,20 @@ public class PurgeLocalRepositoryMojo extends AbstractMojo {
         for (Artifact artifact : artifacts) {
             verbose("Purging artifact: " + artifact.getId());
 
-            File deleteTarget = findDeleteTarget(artifact);
-
-            verbose("Deleting: " + deleteTarget);
-
-            if (deleteTarget.isDirectory()) {
-                try {
-                    FileUtils.deleteDirectory(deleteTarget);
-                } catch (IOException e) {
-                    getLog().warn("Unable to purge local repository location: " + deleteTarget, e);
-                }
-            } else {
-                if (!deleteTarget.delete()) {
-                    deleteTarget.deleteOnExit();
-                    getLog().warn("Unable to purge local repository location immediately: " + deleteTarget);
+            for (File deleteTarget : findDeleteTargets(artifact)) {
+                verbose("Deleting: " + deleteTarget);
+
+                if (deleteTarget.isDirectory()) {
+                    try {
+                        FileUtils.deleteDirectory(deleteTarget);
+                    } catch (IOException e) {
+                        getLog().warn("Unable to purge local repository location: " + deleteTarget, e);
+                    }
+                } else {
+                    if (!deleteTarget.delete()) {
+                        deleteTarget.deleteOnExit();
+                        getLog().warn("Unable to purge local repository location immediately: " + deleteTarget);
+                    }
                 }
             }
             artifact.setResolved(false);
@@ -619,7 +620,7 @@ public class PurgeLocalRepositoryMojo extends AbstractMojo {
         }
     }
 
-    private File findDeleteTarget(Artifact artifact) {
+    private List<File> findDeleteTargets(Artifact artifact) {
         // Use localRepository.pathOf() in case artifact.getFile() is not set
         File deleteTarget = new File(localRepository.getBasedir(), localRepository.pathOf(artifact));
 
@@ -632,9 +633,35 @@ public class PurgeLocalRepositoryMojo extends AbstractMojo {
         } else if (VERSION_FUZZINESS.equals(resolutionFuzziness)) {
             // get the version dir.
             deleteTarget = deleteTarget.getParentFile();
+        } else {
+            // it's file fuzziness.
+            return Collections.singletonList(deleteTarget);
+        }
+
+        if (snapshotsOnly && !VERSION_FUZZINESS.equals(resolutionFuzziness)) {
+            // The groupId or artifactId directory also holds release versions, which must be kept.
+            List<File> snapshotDirectories = new ArrayList<>();
+            collectSnapshotVersionDirectories(deleteTarget, snapshotDirectories);
+            return snapshotDirectories;
+        }
+        return Collections.singletonList(deleteTarget);
+    }
+
+    /**
+     * Collects, below the given directory, the version directories of snapshots, without descending into them.
+     */
+    private void collectSnapshotVersionDirectories(File directory, List<File> snapshotDirectories) {
+        File[] children = directory.listFiles(File::isDirectory);
+        if (children == null) {
+            return;
+        }
+        for (File child : children) {
+            if (ArtifactUtils.isSnapshot(child.getName())) {
+                snapshotDirectories.add(child);
+            } else {
+                collectSnapshotVersionDirectories(child, snapshotDirectories);
+            }
         }
-        // else it's file fuzziness.
-        return deleteTarget;
     }
 
     private void verbose(String message) {
diff --git a/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java b/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
new file mode 100644
index 00000000..6f78e1fb
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.dependency;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.apache.maven.artifact.handler.DefaultArtifactHandler;
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.artifact.repository.MavenArtifactRepository;
+import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.MojoExecution.Source;
+import org.apache.maven.plugins.dependency.utils.ResolverUtil;
+import org.apache.maven.project.MavenProject;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.artifact.DefaultArtifactType;
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.util.artifact.DefaultArtifactTypeRegistry;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.apache.maven.api.plugin.testing.MojoExtension.setVariableValueToObject;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@SuppressWarnings("deprecation")
+class PurgeLocalRepositoryMojoTest {
+
+    private static final String GROUP_PATH = "org/example";
+
+    @TempDir
+    Path localRepo;
+
+    private MavenProject project;
+
+    private MavenSession session;
+
+    private ResolverUtil resolverUtil;
+
+    private PurgeLocalRepositoryMojo mojo;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        project = mock(MavenProject.class);
+        session = mock(MavenSession.class);
+        resolverUtil = mock(ResolverUtil.class);
+
+        DefaultArtifactTypeRegistry typeRegistry = new DefaultArtifactTypeRegistry();
+        typeRegistry.add(new DefaultArtifactType("jar", "jar", "", "java"));
+        typeRegistry.add(new DefaultArtifactType("custom", "cext", "", "java"));
+        RepositorySystemSession repositorySession = mock(RepositorySystemSession.class);
+        when(repositorySession.getArtifactTypeRegistry()).thenReturn(typeRegistry);
+        when(session.getRepositorySession()).thenReturn(repositorySession);
+
+        org.apache.maven.artifact.Artifact projectArtifact = new org.apache.maven.artifact.DefaultArtifact(
+                "org.example.project", "project", "1.0", null, "jar", null, new DefaultArtifactHandler("jar"));
+        when(project.getArtifact()).thenReturn(projectArtifact);
+        when(project.getId()).thenReturn("org.example.project:project:jar:1.0");
+        when(project.getArtifactId()).thenReturn("project");
+        when(project.getRemoteProjectRepositories()).thenReturn(Collections.<RemoteRepository>emptyList());
+
+        MojoExecution mojoExecution = mock(MojoExecution.class);
+        when(mojoExecution.getSource()).thenReturn(Source.CLI);
+
+        ArtifactRepository localRepository = new MavenArtifactRepository(
+                "local",
+                localRepo.toUri().toString(),
+                new DefaultRepositoryLayout(),
+                new org.apache.maven.artifact.repository.ArtifactRepositoryPolicy(),
+                new org.apache.maven.artifact.repository.ArtifactRepositoryPolicy());
+
+        mojo = new PurgeLocalRepositoryMojo(project, session, resolverUtil);
+        setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project));
+        setVariableValueToObject(mojo, "mojoExecution", mojoExecution);
+        setVariableValueToObject(mojo, "localRepository", localRepository);
+        setVariableValueToObject(mojo, "reResolve", false);
+        setVariableValueToObject(mojo, "actTransitively", true);
+        setVariableValueToObject(mojo, "resolutionFuzziness", "version");
+    }
+
+    private void resolvesTo(Artifact... artifacts) throws Exception {
+        when(resolverUtil.resolveDependenciesForArtifact(
+                        any(Artifact.class), anyList(), any(), anyList(), any(DependencyFilter.class)))
+                .thenReturn(Arrays.asList(artifacts));
+    }
+
+    private Path file(String relativePath) throws IOException {
+        Path file = localRepo.resolve(relativePath);
+        Files.createDirectories(file.getParent());
+        return Files.write(file, new byte[0]);
+    }
+
+    private Path artifactFile(String artifactId, String version) throws IOException {
+        return file(GROUP_PATH + "/" + artifactId + "/" + version + "/" + artifactId + "-" + version + ".jar");
+    }
+
+    private static Artifact artifact(String artifactId, String version) {
+        return new DefaultArtifact("org.example", artifactId, "jar", version);
+    }
+
+    @Test
+    void artifactIdFuzzinessWithSnapshotsOnlyDeletesOnlySnapshotVersions() throws Exception {
+        Path snapshot = artifactFile("lib", "1.0-SNAPSHOT");
+        Path otherSnapshot = artifactFile("lib", "2.0-SNAPSHOT");
+        Path release = artifactFile("lib", "1.0");
+        Path sibling = artifactFile("sibling", "1.0-SNAPSHOT");
+        setVariableValueToObject(mojo, "resolutionFuzziness", "artifactId");
+        setVariableValueToObject(mojo, "snapshotsOnly", true);
+        resolvesTo(artifact("lib", "1.0-SNAPSHOT"));
+
+        mojo.execute();
+
+        assertThat(snapshot.getParent()).doesNotExist();
+        assertThat(otherSnapshot.getParent()).doesNotExist();
+        assertThat(release).exists();
+        assertThat(sibling).exists();
+    }
+
+    @Test
+    void groupIdFuzzinessWithSnapshotsOnlyDeletesOnlySnapshotVersions() throws Exception {
+        Path snapshot = artifactFile("lib", "1.0-SNAPSHOT");
+        Path release = artifactFile("lib", "1.0");
+        Path siblingSnapshot = artifactFile("sibling", "3.0-SNAPSHOT");
+        Path siblingRelease = artifactFile("sibling", "3.0");
+        Path subGroupSnapshot = file(GROUP_PATH + "/sub/deep/1.0-SNAPSHOT/deep-1.0-SNAPSHOT.jar");
+        Path subGroupRelease = file(GROUP_PATH + "/sub/deep/1.0/deep-1.0.jar");
+        Path otherGroup = file("org/other/lib/1.0-SNAPSHOT/lib-1.0-SNAPSHOT.jar");
+        setVariableValueToObject(mojo, "resolutionFuzziness", "groupId");
+        setVariableValueToObject(mojo, "snapshotsOnly", true);
+        resolvesTo(artifact("lib", "1.0-SNAPSHOT"));
+
+        mojo.execute();
+
+        assertThat(snapshot.getParent()).doesNotExist();
+        assertThat(siblingSnapshot.getParent()).doesNotExist();
+        assertThat(subGroupSnapshot.getParent()).doesNotExist();
+        assertThat(release).exists();
+        assertThat(siblingRelease).exists();
+        assertThat(subGroupRelease).exists();
+        assertThat(otherGroup).exists();
+    }
+
+    @Test
+    void artifactIdFuzzinessWithoutSnapshotsOnlyDeletesWholeArtifactDirectory() throws Exception {
+        Path snapshot = artifactFile("lib", "1.0-SNAPSHOT");
+        Path release = artifactFile("lib", "1.0");
+        Path sibling = artifactFile("sibling", "1.0");
+        setVariableValueToObject(mojo, "resolutionFuzziness", "artifactId");
+        resolvesTo(artifact("lib", "1.0-SNAPSHOT"));
+
+        mojo.execute();
+
+        assertThat(snapshot).doesNotExist();
+        assertThat(release).doesNotExist();
+        assertThat(localRepo.resolve(GROUP_PATH + "/lib")).doesNotExist();
+        assertThat(sibling).exists();
+    }
+
+    @Test
+    void groupIdFuzzinessWithoutSnapshotsOnlyDeletesWholeGroupDirectory() throws Exception {
+        Path lib = artifactFile("lib", "1.0");
+        Path sibling = artifactFile("sibling", "1.0");
+        Path otherGroup = file("org/other/lib/1.0/lib-1.0.jar");
+        setVariableValueToObject(mojo, "resolutionFuzziness", "groupId");
+        resolvesTo(artifact("lib", "1.0"));
+
+        mojo.execute();
+
+        assertThat(lib).doesNotExist();
+        assertThat(sibling).doesNotExist();
+        assertThat(localRepo.resolve(GROUP_PATH)).doesNotExist();
+        assertThat(otherGroup).exists();
+    }
+}