diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflictsStrategies.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflictsStrategies.java index 1f5555a9f..870860d31 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflictsStrategies.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflictsStrategies.java @@ -54,16 +54,36 @@ public static void main(String[] args) throws Exception { System.out.println("------------------------------------------------------------"); System.out.println(GetDependencyHierarchyWithConflictsStrategies.class.getSimpleName()); - runItWithStrategy(args, ConfigurableVersionSelector.NEAREST_SELECTION_STRATEGY); - runItWithStrategy(args, ConfigurableVersionSelector.HIGHEST_SELECTION_STRATEGY); + CollectRequest collectRequest; + + // okttp cleanly shows difference between nearest/closest + collectRequest = new CollectRequest(); + collectRequest.setRootArtifact(new DefaultArtifact("demo:demo:1.0")); + collectRequest.setDependencies( + List.of(new Dependency(new DefaultArtifact("com.squareup.okhttp3:okhttp:jar:4.12.0"), "compile"))); + runItWithStrategy(args, ConfigurableVersionSelector.NEAREST_SELECTION_STRATEGY, collectRequest); + runItWithStrategy(args, ConfigurableVersionSelector.HIGHEST_SELECTION_STRATEGY, collectRequest); + + // MENFORCER-408 inspired + collectRequest = new CollectRequest(); + collectRequest.setRootArtifact(new DefaultArtifact("demo:demo:1.0")); + collectRequest.setDependencies(List.of( + new Dependency(new DefaultArtifact("org.seleniumhq.selenium:selenium-java:jar:3.0.1"), "test"))); + collectRequest.setManagedDependencies(List.of( + new Dependency(new DefaultArtifact("org.seleniumhq.selenium:selenium-java:jar:3.0.1"), "test"), + new Dependency(new DefaultArtifact("org.seleniumhq.selenium:selenium-remote-driver:jar:3.0.1"), "test"), + new Dependency(new DefaultArtifact("com.codeborne:phantomjsdriver:jar:1.3.0"), "test"))); + runItWithStrategy(args, ConfigurableVersionSelector.NEAREST_SELECTION_STRATEGY, collectRequest); + runItWithStrategy(args, ConfigurableVersionSelector.HIGHEST_SELECTION_STRATEGY, collectRequest); } - private static void runItWithStrategy(String[] args, String selectionStrategy) throws Exception { + private static void runItWithStrategy(String[] args, String selectionStrategy, CollectRequest collectRequest) + throws Exception { System.out.println(); System.out.println(selectionStrategy); try (RepositorySystem system = Booter.newRepositorySystem(Booter.selectFactory(args))) { SessionBuilder sessionBuilder = Booter.newRepositorySystemSession(system); - sessionBuilder.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, ConflictResolver.Verbosity.FULL); + sessionBuilder.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, ConflictResolver.Verbosity.STANDARD); sessionBuilder.setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true); sessionBuilder.setConfigProperty( ConfigurableVersionSelector.CONFIG_PROP_SELECTION_STRATEGY, selectionStrategy); @@ -81,10 +101,6 @@ private static void runItWithStrategy(String[] args, String selectionStrategy) t .setTransferListener(null) .build()) { - CollectRequest collectRequest = new CollectRequest(); - collectRequest.setRootArtifact(new DefaultArtifact("demo:demo:1.0")); - collectRequest.setDependencies(List.of( - new Dependency(new DefaultArtifact("com.squareup.okhttp3:okhttp:jar:4.12.0"), "compile"))); collectRequest.setRepositories(Booter.newRepositories(system, session)); CollectResult result = system.collectDependencies(session, collectRequest); diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java index f297f9fb4..9e4dea0bf 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java @@ -806,16 +806,6 @@ private static final class ConflictItem extends ConflictResolver.ConflictItem { // bit field of OPTIONAL_FALSE and OPTIONAL_TRUE int optionalities; - /** - * Bit flag indicating whether one or more paths consider the dependency non-optional. - */ - public static final int OPTIONAL_FALSE = 0x01; - - /** - * Bit flag indicating whether one or more paths consider the dependency optional. - */ - public static final int OPTIONAL_TRUE = 0x02; - private ConflictItem(DependencyNode parent, DependencyNode node, String scope, boolean optional) { if (parent != null) { this.parent = parent.getChildren(); diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java index bc059dc83..15c73c563 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java @@ -151,26 +151,31 @@ public class ConflictResolver implements DependencyGraphTransformer { */ public enum Verbosity { /** - * Verbosity level to be used in all "common" resolving use cases (ie. dependencies to build class path). The + * Verbosity level to be used in all "common" resolving use cases (ie dependencies to build class path). The * {@link ConflictResolver} in this mode will trim down the graph to the barest minimum: will not leave * any conflicting node in place, hence no conflicts will be present in transformed graph either. */ NONE, /** - * Verbosity level to be used in "analyze" resolving use cases (ie. dependency convergence calculations). The - * {@link ConflictResolver} in this mode will remove any redundant collected nodes, in turn it will leave one - * with recorded conflicting information. This mode corresponds to "classic verbose" mode when + * Verbosity level to be used in "analyze" resolving use cases (ie dependency convergence calculations). The + * {@link ConflictResolver} in this mode will remove any redundant collected nodes and cycles, in turn it will + * leave one with recorded conflicting information. This mode corresponds to "classic verbose" mode when * {@link #CONFIG_PROP_VERBOSE} was set to {@code true}. Obviously, the resulting dependency tree is not * suitable for artifact resolution unless a filter is employed to exclude the duplicate dependencies. */ STANDARD, /** - * Verbosity level to be used in "analyze" resolving use cases (ie. dependency convergence calculations). The - * {@link ConflictResolver} in this mode will not remove any collected node, in turn it will record on all - * eliminated nodes the conflicting information. Obviously, the resulting dependency tree is not suitable - * for artifact resolution unless a filter is employed to exclude the duplicate dependencies. + * Verbosity level to be used in "analyze" resolving use cases (ie dependency convergence calculations). The + * {@link ConflictResolver} in this mode will not remove any collected node nor cycle, in turn it will record + * on all eliminated nodes the conflicting information. Obviously, the resulting dependency tree is not suitable + * for artifact resolution unless a filter is employed to exclude the duplicate dependencies and possible cycles. + * Because of left in cycles, user of this verbosity level should ensure that graph post-processing does not + * contain elements that would explode on them. In other words, session should be modified with proper + * graph transformers. + * + * @see RepositorySystemSession#getDependencyGraphTransformer() */ FULL } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java index 960451241..5ff78ffa8 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java @@ -22,18 +22,20 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +import org.eclipse.aether.ConfigurationProperties; import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.collection.DependencyGraphTransformationContext; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.util.ConfigUtils; import org.eclipse.aether.util.artifact.ArtifactIdUtils; import static java.util.Objects.requireNonNull; @@ -90,6 +92,23 @@ * @since 2.0.11 */ public final class PathConflictResolver extends ConflictResolver { + /** + * This implementation of conflict resolver is able to show more precise information regarding cycles in standard + * verbose mode. But, to make it really drop-in-replacement, we "tame down" this information. Still, users needing it + * may want to enable this for easier cycle detection, but in that case this conflict resolver will provide "extra nodes" + * not present on "standard verbosity level" with "classic" conflict resolver, that may lead to IT issues down the + * stream. Hence, the default is to provide as much information as much verbose "classic" does. + * + * @since 2.0.12 + * @configurationSource {@link RepositorySystemSession#getConfigProperties()} + * @configurationType {@link java.lang.Boolean} + * @configurationDefaultValue {@link #DEFAULT_SHOW_CYCLES_IN_STANDARD_VERBOSITY} + */ + public static final String CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY = ConfigurationProperties.PREFIX_AETHER + + "conflictResolver." + ConflictResolver.PATH_CONFLICT_RESOLVER + ".showCyclesInStandardVerbosity"; + + public static final boolean DEFAULT_SHOW_CYCLES_IN_STANDARD_VERBOSITY = false; + private final ConflictResolver.VersionSelector versionSelector; private final ConflictResolver.ScopeSelector scopeSelector; private final ConflictResolver.ScopeDeriver scopeDeriver; @@ -145,28 +164,22 @@ public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransfo throw new RepositoryException("conflict groups have not been identified"); } - Map> cyclicPredecessors = new HashMap<>(); - for (Collection cycle : conflictIdCycles) { - for (String conflictId : cycle) { - Collection predecessors = cyclicPredecessors.computeIfAbsent(conflictId, k -> new HashSet<>()); - predecessors.addAll(cycle); - } - } - State state = new State( ConflictResolver.getVerbosity(context.getSession()), + ConfigUtils.getBoolean( + context.getSession(), + DEFAULT_SHOW_CYCLES_IN_STANDARD_VERBOSITY, + CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY), versionSelector.getInstance(node, context), scopeSelector.getInstance(node, context), scopeDeriver.getInstance(node, context), optionalitySelector.getInstance(node, context), - sortedConflictIds, - conflictIds, - cyclicPredecessors); + conflictIds); state.build(node); // loop over topographically sorted conflictIds - for (String conflictId : state.sortedConflictIds) { + for (String conflictId : sortedConflictIds) { // paths in given conflict group to consider List paths = state.partitions.get(conflictId); if (paths.isEmpty()) { @@ -203,9 +216,11 @@ public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransfo // loop over considered paths and apply selection results; note: node may remove itself from iterated list for (Path path : new ArrayList<>(paths)) { - // apply selected inherited properties scope/optional to all (winner carries version; others are losers) - path.scope = ctx.scope; - path.optional = ctx.optional; + // apply selected properties scope/optional to winner (winner carries version; others are losers) + if (path == winnerPath) { + path.scope = ctx.scope; + path.optional = ctx.optional; + } // reset children as inheritance may be affected by this node scope/optionality change path.children.forEach(c -> c.pull(0)); @@ -236,6 +251,12 @@ private static class State { */ private final ConflictResolver.Verbosity verbosity; + /** + * Whether to show nodes entering cycles, for easier identification. If this is enabled, this implementation + * of conflict resolver will show more data than classic. + */ + private final boolean showCyclesInStandardVerbosity; + /** * The {@link ConflictResolver.VersionSelector} to use. */ @@ -256,23 +277,11 @@ private static class State { */ private final ConflictResolver.OptionalitySelector optionalitySelector; - /** - * Topologically sorted conflictIds from {@link ConflictIdSorter}. - */ - private final List sortedConflictIds; - /** * The node to conflictId mapping from {@link ConflictMarker}. */ private final Map conflictIds; - /** - * The map of conflictIds which could apply to ancestors of nodes with the key conflict id, used to avoid - * recursion early on. This is basically a superset of the key set of resolvedIds, the additional ids account - * for cyclic dependencies. From {@link ConflictIdSorter}. - */ - private final Map> cyclicPredecessors; - /** * A mapping from conflictId to paths represented as {@link Path}s that exist for each conflictId. In other * words all paths to each {@link DependencyNode} that are member of same conflictId group. @@ -287,33 +296,31 @@ private static class State { @SuppressWarnings("checkstyle:ParameterNumber") private State( ConflictResolver.Verbosity verbosity, + boolean showCyclesInStandardVerbosity, ConflictResolver.VersionSelector versionSelector, ConflictResolver.ScopeSelector scopeSelector, ConflictResolver.ScopeDeriver scopeDeriver, ConflictResolver.OptionalitySelector optionalitySelector, - List sortedConflictIds, - Map conflictIds, - Map> cyclicPredecessors) { + Map conflictIds) { this.verbosity = verbosity; + this.showCyclesInStandardVerbosity = showCyclesInStandardVerbosity; this.versionSelector = versionSelector; this.scopeSelector = scopeSelector; this.scopeDeriver = scopeDeriver; this.optionalitySelector = optionalitySelector; - this.sortedConflictIds = sortedConflictIds; this.conflictIds = conflictIds; - this.cyclicPredecessors = cyclicPredecessors; this.partitions = new HashMap<>(); this.resolvedIds = new HashMap<>(); } /** * Consumes the dirty graph and builds internal structures out of {@link Path} instances that is always a - * tree. + * tree. As a side effect, {@link #partitions} are being filled up as well, that combined with topo + * sorted conflictIds can serve as a starting to point to walk the graph. */ - private Path build(DependencyNode node) throws RepositoryException { - Path root = new Path(this, node, null); + private void build(DependencyNode node) throws RepositoryException { + Path root = new Path(this, node, null, false); gatherCRNodes(root); - return root; } /** @@ -325,7 +332,9 @@ private void gatherCRNodes(Path node) throws RepositoryException { // add children; we will get back those really added (not causing cycles) List added = node.addChildren(children); for (Path child : added) { - gatherCRNodes(child); + if (!child.cycle) { + gatherCRNodes(child); + } } } } @@ -364,16 +373,18 @@ private static class Path { private DependencyNode dn; private final String conflictId; private final Path parent; + private final boolean cycle; private final int depth; private final List children; private String scope; private boolean optional; - private Path(State state, DependencyNode dn, Path parent) { + private Path(State state, DependencyNode dn, Path parent, boolean cycle) { this.state = state; this.dn = dn; this.conflictId = state.conflictIds.get(dn); this.parent = parent; + this.cycle = cycle; this.depth = parent != null ? parent.depth + 1 : 0; this.children = new ArrayList<>(); pull(0); @@ -471,70 +482,105 @@ private void push(int levels) { boolean markLoser = false; switch (state.verbosity) { case NONE: - // remove this dn + // remove loser dn this.parent.children.remove(this); this.parent.dn.setChildren(new ArrayList<>(this.parent.dn.getChildren())); this.parent.dn.getChildren().remove(this.dn); this.children.clear(); break; case STANDARD: - // leave this dn; remove children String artifactId = ArtifactIdUtils.toId(this.dn.getArtifact()); String winnerArtifactId = ArtifactIdUtils.toId(winner.dn.getArtifact()); - if (!Objects.equals(artifactId, winnerArtifactId) - && relatedSiblingsCount(this.dn.getArtifact(), this.parent) > 1) { + // is redundant if: + // - is not same as winner, and has related siblings (version range) + // - same instance of DN is direct dependency on path leading here + boolean isRedundant = (!Objects.equals(artifactId, winnerArtifactId) + && relatedSiblingsCount(this.dn.getArtifact(), this.parent) > 1); + if (!this.state.showCyclesInStandardVerbosity) { + isRedundant = isRedundant + || this.parent.isDirectDependencyOnPathToRoot(this.dn.getArtifact()); + } + if (isRedundant) { + // is redundant dn; remove dn this.parent.children.remove(this); this.parent.dn.setChildren(new ArrayList<>(this.parent.dn.getChildren())); this.parent.dn.getChildren().remove(this.dn); this.children.clear(); } else { + // copy loser dn; without children + DependencyNode dnCopy = new DefaultDependencyNode(this.dn); + dnCopy.setChildren(Collections.emptyList()); + + // swap it out in DN graph; in case of cycles this may happen more than once + int idx = this.parent.dn.getChildren().indexOf(this.dn); + if (idx >= 0) { + this.parent.dn.getChildren().set(idx, dnCopy); + } + this.dn = dnCopy; + this.children.clear(); - this.dn.setChildren(Collections.emptyList()); markLoser = true; } break; case FULL: - // leave all in place (even cycles) + // copy loser dn; with children + DependencyNode dnCopy = new DefaultDependencyNode(this.dn); + dnCopy.setChildren(new ArrayList<>(this.dn.getChildren())); + + // swap it out in DN graph; in case of cycles this may happen more than once + int idx = this.parent.dn.getChildren().indexOf(this.dn); + if (idx >= 0) { + this.parent.dn.getChildren().set(idx, dnCopy); + } + this.dn = dnCopy; + markLoser = true; break; default: throw new IllegalArgumentException("Unknown " + state.verbosity); } if (markLoser) { - // copy dn - DependencyNode dnCopy = new DefaultDependencyNode(this.dn); - dnCopy.setData(ConflictResolver.NODE_DATA_WINNER, winner.dn); - dnCopy.setData( + this.dn.setData(ConflictResolver.NODE_DATA_WINNER, winner.dn); + this.dn.setData( ConflictResolver.NODE_DATA_ORIGINAL_SCOPE, this.dn.getDependency().getScope()); - dnCopy.setData( + this.dn.setData( ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY, this.dn.getDependency().getOptional()); - dnCopy.setScope(this.scope); - dnCopy.setOptional(this.optional); - if (ConflictResolver.Verbosity.FULL != state.verbosity) { - dnCopy.getChildren().clear(); - } - - // swap it out in DN graph - this.parent - .dn - .getChildren() - .set(this.parent.dn.getChildren().indexOf(this.dn), dnCopy); - this.dn = dnCopy; + this.dn.setScope(this.scope); + this.dn.setOptional(this.optional); } } } - if (!this.children.isEmpty()) { - int newLevels = levels - 1; - if (newLevels >= 0) { - // child may remove itself from iterated list - for (Path child : new ArrayList<>(children)) { - child.push(newLevels); - } + + int newLevels = levels - 1; + if (newLevels >= 0 && !this.children.isEmpty()) { + // child may remove itself from iterated list + for (Path child : new ArrayList<>(children)) { + child.push(newLevels); } - } else if (!this.dn.getChildren().isEmpty()) { - this.dn.setChildren(Collections.emptyList()); + } + } + + /** + * Returns {@code true} if given artifactId is direct dependency on the path leading from this toward root. + * For some reason "classic" conflict resolver removes these. + *

+ * Note: this check and use of this method is ONLY present to make this conflict resolver produce SAME output + * as {@link ClassicConflictResolver} does, but IMHO this rule here is very arbitrary, moreover, in "standard" + * (where it is only used) verbosity it in facts HIDES the trace of possible cycles. + * + * @see #CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY + */ + private boolean isDirectDependencyOnPathToRoot(Artifact artifact) { + if (this.depth == 1 + && ArtifactIdUtils.toVersionlessId(this.dn.getArtifact()) + .equals(ArtifactIdUtils.toVersionlessId(artifact))) { + return true; + } else if (this.parent != null) { + return parent.isDirectDependencyOnPathToRoot(artifact); + } else { + return false; } } @@ -567,19 +613,15 @@ private void moveOutOfScope() { * Method will return really added ones, as this class avoids cycles. */ private List addChildren(List children) throws RepositoryException { - Collection thisCyclicPredecessors = - this.state.cyclicPredecessors.getOrDefault(this.conflictId, Collections.emptySet()); - ArrayList added = new ArrayList<>(children.size()); for (DependencyNode child : children) { String childConflictId = this.state.conflictIds.get(child); - if (!this.state.partitions.containsKey(childConflictId) - || !thisCyclicPredecessors.contains(childConflictId)) { - Path c = new Path(this.state, child, this); - this.children.add(c); - c.derive(0, false); - added.add(c); - } + boolean cycle = this.state.partitions.getOrDefault(childConflictId, Collections.emptyList()).stream() + .anyMatch(p -> p.dn.equals(child)); + Path c = new Path(this.state, child, this, cycle); + this.children.add(c); + c.derive(0, false); + added.add(c); } return added; } @@ -684,16 +726,6 @@ private static final class ConflictItem extends ConflictResolver.ConflictItem { private final String scope; private final int optionalities; - /** - * Bit flag indicating whether one or more paths consider the dependency non-optional. - */ - public static final int OPTIONAL_FALSE = 0x01; - - /** - * Bit flag indicating whether one or more paths consider the dependency optional. - */ - public static final int OPTIONAL_TRUE = 0x02; - private ConflictItem(Path path) { this.path = path; if (path.parent != null) { diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java index 5ed1bd352..9d8fca505 100644 --- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java @@ -449,6 +449,79 @@ void derivedOptionalStatusChange(ConflictResolver conflictResolver) throws Repos assertSame(jazNode, barNode.getChildren().get(0)); } + @ParameterizedTest + @MethodSource("conflictResolverSource") + void winnerCycleRemoved(ConflictResolver conflictResolver) throws RepositoryException { + // reproducer for MENFORCER-408: + // - foo1 and foo2 are different instances of same dep (foo2 was managed into foo1 during collection) + // - foo2 parent baz has more than one child + // - bug was: baz on being declared winner left foo2 in place (and hence cycle as well) + // Layout: + // root -> foo1 -> bar + // \---> baz -> foo2 + // \---> bam + + // another issue: "classic" CR does NOT leave all cycles in place when verbosity is FULL + // the "path" CR does obey FULL verbosity + for (ConflictResolver.Verbosity verbosity : ConflictResolver.Verbosity.values()) { + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode foo1 = makeDependencyNode("some-group", "foo", "1.0"); + DependencyNode foo2 = makeDependencyNode("some-group", "foo", "1.0"); + DependencyNode bar = makeDependencyNode("some-group", "bar", "1.0"); + DependencyNode baz = makeDependencyNode("some-group", "baz", "1.0"); + DependencyNode bam = makeDependencyNode("some-group", "bam", "1.0"); + root.setChildren(mutableList(foo1)); + foo1.setChildren(mutableList(bar, baz)); + baz.setChildren(mutableList(foo2, bam)); + foo2.setChildren(foo1.getChildren()); + + boolean cyclesLeftInPlace = verbosity == ConflictResolver.Verbosity.FULL; + setVerbosity(verbosity); + session.setConfigProperty( + PathConflictResolver.CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY, String.valueOf(true)); + DependencyNode transformed = transform(conflictResolver, root); + System.out.println("CR=" + conflictResolver.getClass().getSimpleName() + "; verbosity=" + verbosity.name()); + if (!cyclesLeftInPlace) { + transformed.accept(DUMPER_SOUT); + } + + assertSame(transformed, root); + assertEquals(1, transformed.getChildren().size()); + assertSame(foo1, transformed.getChildren().get(0)); + assertEquals(2, foo1.getChildren().size()); + assertSame(bar, foo1.getChildren().get(0)); + assertSame(baz, foo1.getChildren().get(1)); + assertEquals(0, bar.getChildren().size()); + if (conflictResolver.getClass().equals(PathConflictResolver.class)) { + switch (verbosity) { + case NONE: + assertEquals(1, baz.getChildren().size()); + assertSame(bam, baz.getChildren().get(0)); + break; + case STANDARD: + assertEquals(2, baz.getChildren().size()); + assertConflictedButSameAsOriginal( + foo2, baz.getChildren().get(0)); // was copied; not same + assertEquals(0, baz.getChildren().get(0).getChildren().size()); // cycle removed + assertSame(bam, baz.getChildren().get(1)); + break; + case FULL: + assertEquals(2, baz.getChildren().size()); + assertConflictedButSameAsOriginal( + foo2, baz.getChildren().get(0)); // was copied; not same + assertEquals(2, baz.getChildren().get(0).getChildren().size()); // cycle remains + assertSame(bam, baz.getChildren().get(1)); + break; + } + } else if (conflictResolver.getClass().equals(ClassicConflictResolver.class)) { + assertEquals(1, baz.getChildren().size()); + assertSame(bam, baz.getChildren().get(0)); + } else { + fail("Unknown conflict resolver"); + } + } + } + private static DependencyNode makeDependencyNode(String groupId, String artifactId, String version) { return makeDependencyNode(groupId, artifactId, version, "compile"); } diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index e87b91fde..2ef1f2e5a 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -41,6 +41,7 @@ To modify this file, edit the template and regenerate. | `"aether.chainedLocalRepository.ignoreTailAvailability"` | `Boolean` | When using chained local repository, should be the artifact availability ignored in tail. | `true` | 1.9.2 | No | Session Configuration | | `"aether.checksums.omitChecksumsForExtensions"` | `String` | Comma-separated list of extensions with leading dot (example ".asc") that should have checksums omitted. These are applied to sub-artifacts only. Note: to achieve 1.7.x aether.checksums.forSignature=true behaviour, pass empty string as value for this property. | `".asc,.sigstore,.sigstore.json"` | | No | Session Configuration | | `"aether.conflictResolver.impl"` | `String` | The name of the conflict resolver implementation to use: "path" (default) or "classic" (same as Maven 3). | `"path"` | 2.0.11 | No | Session Configuration | +| `"aether.conflictResolver.path.showCyclesInStandardVerbosity"` | `Boolean` | This implementation of conflict resolver is able to show more precise information regarding cycles in standard verbose mode. But, to make it really drop-in-replacement, we "tame down" this information. Still, users needing it may want to enable this for easier cycle detection, but in that case this conflict resolver will provide "extra nodes" not present on "standard verbosity level" with "classic" conflict resolver, that may lead to IT issues down the stream. Hence, the default is to provide as much information as much verbose "classic" does. | `false` | 2.0.12 | No | Session Configuration | | `"aether.conflictResolver.verbose"` | `Object` | The key in the repository session's org.eclipse.aether.RepositorySystemSession#getConfigProperties() configuration properties used to store a Boolean flag controlling the transformer's verbose mode. Accepted values are Boolean types, String type (where "true" would be interpreted as true ) or Verbosity enum instances. | `"NONE"` | | No | Session Configuration | | `"aether.conflictResolver.versionSelector.selectionStrategy"` | `String` | The name of the version selection strategy to use in conflict resolution: "nearest" (default) or "highest". | `"nearest"` | 2.0.11 | No | Session Configuration | | `"aether.connector.basic.downstreamThreads"` | `Integer` | Number of threads in basic connector for downloading. | `5` | 2.0.0 | Yes | Session Configuration |