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:47 UTC

(maven-dependency-plugin) branch master updated (14c46007 -> 42350c08)

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

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


    from 14c46007 [MDEP-458] Assert on the Overwriting warning in the IT
     new 8cc5d386 [MDEP-631] Include provided and test dependencies when purging
     new 88a17682 [MDEP-619] Purge only snapshot versions when resolutionFuzziness is artifactId or groupId
     new 7afd046b [MDEP-574] Match include and exclude patterns on the artifact type
     new 42350c08 [MDEP-574] Fall back to the jar type when a node has no type

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../invoker.properties                             |   0
 .../pom.xml                                        |  12 +-
 .../setup.groovy}                                  |  24 +-
 .../test.properties                                |   3 +-
 .../verify.groovy                                  |  11 +-
 .../invoker.properties                             |   0
 .../pom.xml                                        |   6 +-
 .../setup.groovy}                                  |  31 ++-
 .../test.properties                                |   1 +
 .../verify.groovy                                  |  29 ++-
 .../invoker.properties                             |   0
 .../pom.xml                                        |   6 +-
 .../setup.groovy}                                  |  31 ++-
 .../test.properties                                |   1 +
 .../verify.groovy                                  |  29 ++-
 .../projects/purge-local-repository/verify.groovy  |   6 +-
 .../dependency/PurgeLocalRepositoryMojo.java       | 120 +++++++--
 .../plugins/dependency/utils/ResolverUtil.java     |   6 +-
 .../examples/purging-local-repository.md.vm        |   8 +-
 .../dependency/PurgeLocalRepositoryMojoTest.java   | 285 +++++++++++++++++++++
 .../plugins/dependency/utils/ResolverUtilTest.java |   9 +-
 21 files changed, 499 insertions(+), 119 deletions(-)
 copy src/it/projects/{purge-local-repository-multi-module => purge-local-repository-include-custom-type}/invoker.properties (100%)
 copy src/it/projects/{purge-local-repository => purge-local-repository-include-custom-type}/pom.xml (86%)
 copy src/it/projects/{mdep-1650-scriptable-output-null-classifier/verify.groovy => purge-local-repository-include-custom-type/setup.groovy} (64%)
 copy src/it/projects/{purge-local-repository-include => purge-local-repository-include-custom-type}/test.properties (90%)
 copy src/it/projects/{copy-maven-coordinates => purge-local-repository-include-custom-type}/verify.groovy (60%)
 copy src/it/projects/{purge-local-repository-multi-module => purge-local-repository-snapshots-only-artifactid-fuzziness}/invoker.properties (100%)
 copy src/it/projects/{purge-local-repository-version-range => purge-local-repository-snapshots-only-artifactid-fuzziness}/pom.xml (89%)
 copy src/{test/java/org/apache/maven/plugins/dependency/testUtils/stubs/StubMarkerFile.java => it/projects/purge-local-repository-snapshots-only-artifactid-fuzziness/setup.groovy} (61%)
 copy src/it/projects/{purge-local-repository-snapshots-only => purge-local-repository-snapshots-only-artifactid-fuzziness}/test.properties (96%)
 copy src/it/projects/{mdep-1650-scriptable-output-null-classifier => purge-local-repository-snapshots-only-artifactid-fuzziness}/verify.groovy (51%)
 copy src/it/projects/{purge-local-repository-multi-module => purge-local-repository-snapshots-only-groupid-fuzziness}/invoker.properties (100%)
 copy src/it/projects/{purge-local-repository-version-range => purge-local-repository-snapshots-only-groupid-fuzziness}/pom.xml (89%)
 copy src/{test/java/org/apache/maven/plugins/dependency/testUtils/stubs/StubMarkerFile.java => it/projects/purge-local-repository-snapshots-only-groupid-fuzziness/setup.groovy} (61%)
 copy src/it/projects/{purge-local-repository-snapshots-only => purge-local-repository-snapshots-only-groupid-fuzziness}/test.properties (96%)
 copy src/it/projects/{mdep-1650-scriptable-output-null-classifier => purge-local-repository-snapshots-only-groupid-fuzziness}/verify.groovy (53%)
 create mode 100644 src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java


(maven-dependency-plugin) 01/04: [MDEP-631] Include provided and test dependencies when purging

Posted by sl...@apache.org.
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 8cc5d386c53b13ea51ebf1ca8dafd9b4ff13febc
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Tue Oct 13 22:50:14 2026 +0200

    [MDEP-631] Include provided and test dependencies when purging
    
    The collect root was a Dependency, so the session's DependencySelector treated the project's direct dependencies as transitive and dropped provided and test ones. Only PurgeLocalRepositoryMojo calls the five-argument overload; GoOfflineMojo uses the four-argument one, which already sets a root artifact.
---
 src/it/projects/purge-local-repository/verify.groovy             | 6 +++---
 .../org/apache/maven/plugins/dependency/utils/ResolverUtil.java  | 6 ++++--
 .../apache/maven/plugins/dependency/utils/ResolverUtilTest.java  | 9 +++------
 3 files changed, 10 insertions(+), 11 deletions(-)

diff --git a/src/it/projects/purge-local-repository/verify.groovy b/src/it/projects/purge-local-repository/verify.groovy
index 6ffc8f1f..6895f8e2 100644
--- a/src/it/projects/purge-local-repository/verify.groovy
+++ b/src/it/projects/purge-local-repository/verify.groovy
@@ -30,10 +30,10 @@ void checkFilePresence( String path )
 }
 
 checkFileAbsence( "org/apache/maven/its/dependency/purged/1.0/purged-1.0.jar" )
-checkFilePresence( "org/apache/maven/its/dependency/not-purged-test/1.0/not-purged-test-1.0.jar" )
-checkFilePresence( "org/apache/maven/its/dependency/not-purged-provided/1.0/not-purged-provided-1.0.jar" )
+checkFileAbsence( "org/apache/maven/its/dependency/not-purged-test/1.0/not-purged-test-1.0.jar" )
+checkFileAbsence( "org/apache/maven/its/dependency/not-purged-provided/1.0/not-purged-provided-1.0.jar" )
 checkFilePresence( "org/apache/maven/its/dependency/not-purged-optional/1.0/not-purged-optional-1.0.jar" )
 
 String buildLog = new File( basedir, "build.log" ).getText( "UTF-8" )
-assert buildLog.contains( 'Deleting 1 transitive dependency for project test from ' )
+assert buildLog.contains( 'Deleting 3 transitive dependencies for project test from ' )
 assert buildLog.contains( 'with artifact version resolution fuzziness' )
diff --git a/src/main/java/org/apache/maven/plugins/dependency/utils/ResolverUtil.java b/src/main/java/org/apache/maven/plugins/dependency/utils/ResolverUtil.java
index 52e71ad3..d92cd9fd 100644
--- a/src/main/java/org/apache/maven/plugins/dependency/utils/ResolverUtil.java
+++ b/src/main/java/org/apache/maven/plugins/dependency/utils/ResolverUtil.java
@@ -330,9 +330,11 @@ public class ResolverUtil {
             List<RemoteRepository> remoteProjectRepositories,
             DependencyFilter dependencyFilter)
             throws DependencyResolutionException {
+        // The root is set as an artifact rather than as a dependency: a root dependency makes the session's
+        // DependencySelector treat the project's direct dependencies as transitive and drop provided and test ones.
         CollectRequest collectRequest =
-                new CollectRequest(new Dependency(rootArtifact, null), dependencies, remoteProjectRepositories);
-        collectRequest.setManagedDependencies(managedDependencies);
+                new CollectRequest(dependencies, managedDependencies, remoteProjectRepositories);
+        collectRequest.setRootArtifact(rootArtifact);
         return resolveDependencies(collectRequest, dependencyFilter);
     }
 
diff --git a/src/test/java/org/apache/maven/plugins/dependency/utils/ResolverUtilTest.java b/src/test/java/org/apache/maven/plugins/dependency/utils/ResolverUtilTest.java
index 4b6f5e2c..e421ae5f 100644
--- a/src/test/java/org/apache/maven/plugins/dependency/utils/ResolverUtilTest.java
+++ b/src/test/java/org/apache/maven/plugins/dependency/utils/ResolverUtilTest.java
@@ -341,7 +341,7 @@ class ResolverUtilTest {
     }
 
     @Test
-    void resolveDependenciesForArtifactWithFilter() throws Exception {
+    void resolveDependenciesForArtifactWithFilterKeepsDirectDependenciesOfAllScopes() throws Exception {
         Artifact rootArtifact = new DefaultArtifact("org.apache.maven.plugins", "artifact", "jar", "1.0");
         DependencyResult dependencyResult = new DependencyResult(new DependencyRequest());
         when(sessionProvider.get()).thenReturn(mavenSession);
@@ -360,11 +360,8 @@ class ResolverUtilTest {
                 .resolveDependencies(
                         eq(repositorySystemSession),
                         argThat(request -> request.getFilter() == dependencyFilter
-                                && request.getCollectRequest().getRootArtifact() == null
-                                && request.getCollectRequest()
-                                        .getRoot()
-                                        .getArtifact()
-                                        .equals(rootArtifact)));
+                                && request.getCollectRequest().getRoot() == null
+                                && request.getCollectRequest().getRootArtifact().equals(rootArtifact)));
     }
 
     @Test


(maven-dependency-plugin) 03/04: [MDEP-574] Match include and exclude patterns on the artifact type

Posted by sl...@apache.org.
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 7afd046b7504e4b5be4ac385a35e9aad1b8802b0
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Tue Oct 13 23:05:01 2026 +0200

    [MDEP-574] Match include and exclude patterns on the artifact type
    
    The Resolver pattern filters compare the third element with the file extension only, so a pattern naming a type such as test-jar never matched. Keep matching the extension, so existing patterns such as groupId:artifactId:jar still work.
---
 .../invoker.properties                             | 18 ++++++
 .../pom.xml                                        | 59 +++++++++++++++++
 .../setup.groovy                                   | 34 ++++++++++
 .../test.properties                                | 20 ++++++
 .../verify.groovy                                  | 27 ++++++++
 .../dependency/PurgeLocalRepositoryMojo.java       | 58 ++++++++++++++++-
 .../examples/purging-local-repository.md.vm        |  8 ++-
 .../dependency/PurgeLocalRepositoryMojoTest.java   | 73 ++++++++++++++++++++++
 8 files changed, 294 insertions(+), 3 deletions(-)

diff --git a/src/it/projects/purge-local-repository-include-custom-type/invoker.properties b/src/it/projects/purge-local-repository-include-custom-type/invoker.properties
new file mode 100644
index 00000000..3c1911e6
--- /dev/null
+++ b/src/it/projects/purge-local-repository-include-custom-type/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-include-custom-type/pom.xml b/src/it/projects/purge-local-repository-include-custom-type/pom.xml
new file mode 100644
index 00000000..0d40246e
--- /dev/null
+++ b/src/it/projects/purge-local-repository-include-custom-type/pom.xml
@@ -0,0 +1,59 @@
+<?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 an include pattern that names an artifact type (MDEP-574)
+  </description>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.its.dependency</groupId>
+      <artifactId>typed</artifactId>
+      <version>1.0</version>
+      <type>test-jar</type>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.its.dependency</groupId>
+      <artifactId>typed</artifactId>
+      <version>1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.its.dependency</groupId>
+      <artifactId>untouched</artifactId>
+      <version>1.0</version>
+      <type>test-jar</type>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/src/it/projects/purge-local-repository-include-custom-type/setup.groovy b/src/it/projects/purge-local-repository-include-custom-type/setup.groovy
new file mode 100644
index 00000000..3ccff669
--- /dev/null
+++ b/src/it/projects/purge-local-repository-include-custom-type/setup.groovy
@@ -0,0 +1,34 @@
+/*
+ * 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 createFile( String relativePath )
+{
+    File file = new File( localRepositoryPath, relativePath )
+    file.getParentFile().mkdirs()
+    file.createNewFile()
+}
+
+// the type test-jar has the file extension jar and the classifier tests
+createFile( groupPath + "typed/1.0/typed-1.0-tests.jar" )
+createFile( groupPath + "typed/1.0/typed-1.0.jar" )
+createFile( groupPath + "untouched/1.0/untouched-1.0-tests.jar" )
+
+return true
diff --git a/src/it/projects/purge-local-repository-include-custom-type/test.properties b/src/it/projects/purge-local-repository-include-custom-type/test.properties
new file mode 100644
index 00000000..daa19f69
--- /dev/null
+++ b/src/it/projects/purge-local-repository-include-custom-type/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
+include = org.apache.maven.its.dependency:typed:test-jar
+resolutionFuzziness = file
diff --git a/src/it/projects/purge-local-repository-include-custom-type/verify.groovy b/src/it/projects/purge-local-repository-include-custom-type/verify.groovy
new file mode 100644
index 00000000..f1ea36ad
--- /dev/null
+++ b/src/it/projects/purge-local-repository-include-custom-type/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.
+ */
+
+String groupPath = "org/apache/maven/its/dependency/"
+
+// only the test-jar of the included artifactId is purged: the plain jar has the extension jar, but not the type test-jar
+assert !new File( localRepositoryPath, groupPath + "typed/1.0/typed-1.0-tests.jar" ).exists() : "test-jar was not purged"
+assert new File( localRepositoryPath, groupPath + "typed/1.0/typed-1.0.jar" ).exists() : "jar was purged"
+assert new File( localRepositoryPath, groupPath + "untouched/1.0/untouched-1.0-tests.jar" ).exists() : "not included artifact was purged"
+
+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 4f9ccf90..0047bf0e 100644
--- a/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java
+++ b/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java
@@ -60,8 +60,12 @@ import org.apache.maven.shared.artifact.filter.resolve.transform.EclipseAetherFi
 import org.apache.maven.shared.utils.logging.MessageBuilder;
 import org.apache.maven.shared.utils.logging.MessageUtils;
 import org.codehaus.plexus.util.FileUtils;
+import org.eclipse.aether.artifact.ArtifactType;
 import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.DefaultDependencyNode;
 import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
 import org.eclipse.aether.resolution.ArtifactDescriptorException;
 import org.eclipse.aether.resolution.DependencyResolutionException;
 
@@ -267,6 +271,56 @@ public class PurgeLocalRepositoryMojo extends AbstractMojo {
         }
     }
 
+    /**
+     * Applies an include or exclude pattern filter to an artifact's type as well as to its file extension. The
+     * pattern filters of the Resolver match the third element of {@code groupId:artifactId:extension:version} against
+     * the file extension only, so a pattern naming a type such as {@code test-jar} would never match.
+     */
+    private class TypeOrExtensionFilter extends AbstractFilter {
+        private final DependencyFilter patternFilter;
+
+        private final boolean exclusion;
+
+        /**
+         * @param patternFilter a {@link PatternInclusionsFilter} or {@link PatternExclusionsFilter}
+         * @param exclusion whether the pattern filter is an exclusion, which accepts the artifacts that match none
+         */
+        TypeOrExtensionFilter(TransformableFilter patternFilter, boolean exclusion) {
+            this.patternFilter = patternFilter.transform(new EclipseAetherFilterTransformer());
+            this.exclusion = exclusion;
+        }
+
+        @Override
+        public boolean accept(Node node, List<Node> parents) {
+            Dependency dependency = node.getDependency();
+            if (dependency == null) {
+                return false;
+            }
+
+            String type = dependency.getType();
+            ArtifactType artifactType =
+                    session.getRepositorySession().getArtifactTypeRegistry().get(type);
+            String extension = artifactType != null ? artifactType.getExtension() : type;
+
+            Set<String> candidates = new LinkedHashSet<>(Arrays.asList(type, extension));
+            for (String candidate : candidates) {
+                org.eclipse.aether.artifact.Artifact artifact = new DefaultArtifact(
+                        dependency.getGroupId(),
+                        dependency.getArtifactId(),
+                        dependency.getClassifier(),
+                        candidate,
+                        dependency.getVersion());
+                DependencyNode candidateNode = new DefaultDependencyNode(
+                        new org.eclipse.aether.graph.Dependency(artifact, dependency.getScope()));
+                // an include is satisfied by one match, an exclude only by the absence of any match
+                if (patternFilter.accept(candidateNode, Collections.emptyList()) != exclusion) {
+                    return !exclusion;
+                }
+            }
+            return exclusion;
+        }
+    }
+
     /**
      * Includes only snapshot artifacts.
      */
@@ -444,14 +498,14 @@ public class PurgeLocalRepositoryMojo extends AbstractMojo {
             this.includes = parseIncludes(this.include);
         }
         if (this.includes != null) {
-            subFilters.add(new PatternInclusionsFilter(includes));
+            subFilters.add(new TypeOrExtensionFilter(new PatternInclusionsFilter(includes), false));
         }
 
         if (!(this.exclude == null || this.exclude.isEmpty())) {
             this.excludes = parseIncludes(this.exclude);
         }
         if (this.excludes != null) {
-            subFilters.add(new PatternExclusionsFilter(excludes));
+            subFilters.add(new TypeOrExtensionFilter(new PatternExclusionsFilter(excludes), true));
         }
 
         if (!actTransitively) {
diff --git a/src/site/markdown/examples/purging-local-repository.md.vm b/src/site/markdown/examples/purging-local-repository.md.vm
index 399c1d78..23ff2799 100644
--- a/src/site/markdown/examples/purging-local-repository.md.vm
+++ b/src/site/markdown/examples/purging-local-repository.md.vm
@@ -85,6 +85,12 @@ Specific dependencies from the current project dependency tree can be included o
 </project>
 ```
 
+Each include or exclude entry is a pattern of the form `groupId[:artifactId[:type[:version]]]`. Trailing elements can be left out, and `*` matches any sequence of characters in an element. The `type` element matches either the dependency's type, such as `test-jar` or `ejb-client`, or the file extension of its artifact, such as `jar`. For example, `org.example:lib:test-jar` selects the test jar of `org.example:lib` without selecting its main jar.
+
+```shell
+mvn dependency:purge-local-repository -Dinclude=org.example:lib:test-jar
+```
+
 The includes/excludes configuration can also be passed from the CLI using the "include" or "exclude" parameter.
 
 ```shell
@@ -109,7 +115,7 @@ mvn dependency:purge-local-repository -DmanualInclude=org.apache:apache
 Other purge configuration
 -------------------------
 
-To purge all the dependencies of a given groupId, use the `resolutionFuzziness` configuration parameter.
+To purge all the dependencies of a given groupId, use the `resolutionFuzziness` configuration parameter. Combined with `snapshotsOnly`, the `artifactId` and `groupId` values delete only the snapshot versions below the artifactId or groupId directory and keep its release versions.
 
 If neither `includes` nor `excludes` are specified, all the dependencies of the current project are purged.
 
diff --git a/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java b/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
index 6f78e1fb..4f84e53f 100644
--- a/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
+++ b/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
@@ -37,18 +37,22 @@ 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.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
 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 org.mockito.ArgumentCaptor;
 
 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.verify;
 import static org.mockito.Mockito.when;
 
 @SuppressWarnings("deprecation")
@@ -199,4 +203,73 @@ class PurgeLocalRepositoryMojoTest {
         assertThat(localRepo.resolve(GROUP_PATH)).doesNotExist();
         assertThat(otherGroup).exists();
     }
+
+    @Test
+    void includeMatchesCustomArtifactType() throws Exception {
+        DependencyFilter filter = purgeFilterFor("include", "org.example:custom-lib:custom:1.0");
+
+        assertThat(filter.accept(node("custom-lib", "1.0", "custom"), Collections.emptyList()))
+                .as("type")
+                .isTrue();
+        assertThat(filter.accept(node("other-lib", "1.0", "custom"), Collections.emptyList()))
+                .as("other artifactId")
+                .isFalse();
+        assertThat(filter.accept(node("custom-lib", "1.0", "jar"), Collections.emptyList()))
+                .as("other type")
+                .isFalse();
+    }
+
+    @Test
+    void includeStillMatchesFileExtension() throws Exception {
+        DependencyFilter filter = purgeFilterFor("include", "org.example:custom-lib:cext:1.0");
+
+        assertThat(filter.accept(node("custom-lib", "1.0", "custom"), Collections.emptyList()))
+                .isTrue();
+    }
+
+    @Test
+    void includeSupportsWildcardsAroundCustomArtifactType() throws Exception {
+        DependencyFilter filter = purgeFilterFor("include", "org.example:custom-*:custom");
+
+        assertThat(filter.accept(node("custom-lib", "1.0", "custom"), Collections.emptyList()))
+                .isTrue();
+        assertThat(filter.accept(node("other-lib", "1.0", "custom"), Collections.emptyList()))
+                .isFalse();
+    }
+
+    @Test
+    void excludeMatchesCustomArtifactType() throws Exception {
+        DependencyFilter filter = purgeFilterFor("exclude", "org.example:custom-lib:custom");
+
+        assertThat(filter.accept(node("custom-lib", "1.0", "custom"), Collections.emptyList()))
+                .isFalse();
+        assertThat(filter.accept(node("custom-lib", "1.0", "jar"), Collections.emptyList()))
+                .isTrue();
+        assertThat(filter.accept(node("other-lib", "1.0", "custom"), Collections.emptyList()))
+                .isTrue();
+    }
+
+    private DependencyFilter purgeFilterFor(String parameter, String pattern) throws Exception {
+        setVariableValueToObject(mojo, parameter, pattern);
+        resolvesTo();
+
+        mojo.execute();
+
+        ArgumentCaptor<DependencyFilter> filter = ArgumentCaptor.forClass(DependencyFilter.class);
+        verify(resolverUtil)
+                .resolveDependenciesForArtifact(any(Artifact.class), anyList(), any(), anyList(), filter.capture());
+        return filter.getValue();
+    }
+
+    private static DefaultDependencyNode node(String artifactId, String version, String type) {
+        String extension = "custom".equals(type) ? "cext" : type;
+        Artifact artifact = new DefaultArtifact(
+                "org.example",
+                artifactId,
+                "",
+                extension,
+                version,
+                new DefaultArtifactType(type, extension, "", "java"));
+        return new DefaultDependencyNode(new Dependency(artifact, "compile"));
+    }
 }


(maven-dependency-plugin) 04/04: [MDEP-574] Fall back to the jar type when a node has no type

Posted by sl...@apache.org.
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 42350c08719482df2f8c97a8ba14768c1488d62c
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Tue Oct 13 23:57:10 2026 +0200

    [MDEP-574] Fall back to the jar type when a node has no type
---
 .../maven/plugins/dependency/PurgeLocalRepositoryMojo.java     |  3 ++-
 .../maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java | 10 ++++++++++
 2 files changed, 12 insertions(+), 1 deletion(-)

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 0047bf0e..67ac4cb3 100644
--- a/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java
+++ b/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java
@@ -297,7 +297,8 @@ public class PurgeLocalRepositoryMojo extends AbstractMojo {
                 return false;
             }
 
-            String type = dependency.getType();
+            // a node without a type property falls back to the default type and extension
+            String type = dependency.getType() != null ? dependency.getType() : "jar";
             ArtifactType artifactType =
                     session.getRepositorySession().getArtifactTypeRegistry().get(type);
             String extension = artifactType != null ? artifactType.getExtension() : type;
diff --git a/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java b/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
index 4f84e53f..57503444 100644
--- a/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
+++ b/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
@@ -237,6 +237,16 @@ class PurgeLocalRepositoryMojoTest {
                 .isFalse();
     }
 
+    @Test
+    void nodeWithoutTypePropertyFallsBackToTheExtension() throws Exception {
+        DependencyFilter filter = purgeFilterFor("include", "org.example:lib:jar");
+        // a plain artifact has no "type" property
+        DefaultDependencyNode untyped =
+                new DefaultDependencyNode(new Dependency(new DefaultArtifact("org.example:lib:jar:1.0"), "compile"));
+
+        assertThat(filter.accept(untyped, Collections.emptyList())).isTrue();
+    }
+
     @Test
     void excludeMatchesCustomArtifactType() throws Exception {
         DependencyFilter filter = purgeFilterFor("exclude", "org.example:custom-lib:custom");


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

Posted by sl...@apache.org.
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();
+    }
+}