Bug 424586 - Report cycles in dependency graph

Considered root artifact of dependency graph when looking out for cycles
diff --git a/aether-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCollector.java b/aether-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCollector.java
index a972a2e..1cb0ceb 100644
--- a/aether-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCollector.java
+++ b/aether-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCollector.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -429,15 +429,19 @@
                 int cycleEntry = args.nodes.find( d.getArtifact() );
                 if ( cycleEntry >= 0 )
                 {
-                    DependencyNode cycleNode = args.nodes.get( cycleEntry );
-                    DefaultDependencyNode child =
-                        createDependencyNode( relocations, preManaged, rangeResult, version, d, descriptorResult,
-                                              cycleNode );
-
-                    node.getChildren().add( child );
                     results.addCycle( args.nodes, cycleEntry, d );
+                    DependencyNode cycleNode = args.nodes.get( cycleEntry );
+                    if ( cycleNode.getDependency() != null )
+                    {
+                        DefaultDependencyNode child =
+                            createDependencyNode( relocations, preManaged, rangeResult, version, d, descriptorResult,
+                                                  cycleNode );
+                        node.getChildren().add( child );
+                        continue;
+                    }
                 }
-                else if ( !descriptorResult.getRelocations().isEmpty() )
+
+                if ( !descriptorResult.getRelocations().isEmpty() )
                 {
                     boolean disableVersionManagementSubsequently =
                         originalArtifact.getGroupId().equals( d.getArtifact().getGroupId() )
diff --git a/aether-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCycle.java b/aether-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCycle.java
index 6f31f88..94cd344 100644
--- a/aether-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCycle.java
+++ b/aether-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCycle.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2013 Sonatype, Inc.
+ * Copyright (c) 2013, 2014 Sonatype, Inc.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -16,6 +16,7 @@
 
 import org.eclipse.aether.graph.Dependency;
 import org.eclipse.aether.graph.DependencyCycle;
+import org.eclipse.aether.graph.DependencyNode;
 import org.eclipse.aether.util.artifact.ArtifactIdUtils;
 
 /**
@@ -31,11 +32,18 @@
 
     public DefaultDependencyCycle( NodeStack nodes, int cycleEntry, Dependency dependency )
     {
-        int offset = ( nodes.get( 0 ).getDependency() == null ) ? 1 : 0;
+        // skip root node unless it actually has a dependency or is considered the cycle entry (due to its label)
+        int offset = ( cycleEntry > 0 && nodes.get( 0 ).getDependency() == null ) ? 1 : 0;
         Dependency[] dependencies = new Dependency[nodes.size() - offset + 1];
         for ( int i = 0, n = dependencies.length - 1; i < n; i++ )
         {
-            dependencies[i] = nodes.get( i + offset ).getDependency();
+            DependencyNode node = nodes.get( i + offset );
+            dependencies[i] = node.getDependency();
+            // when cycle starts at root artifact as opposed to root dependency, synthesize a dependency
+            if ( dependencies[i] == null )
+            {
+                dependencies[i] = new Dependency( node.getArtifact(), null );
+            }
         }
         dependencies[dependencies.length - 1] = dependency;
         this.dependencies = Collections.unmodifiableList( Arrays.asList( dependencies ) );
diff --git a/aether-impl/src/main/java/org/eclipse/aether/internal/impl/NodeStack.java b/aether-impl/src/main/java/org/eclipse/aether/internal/impl/NodeStack.java
index 658364b..a61bfb4 100644
--- a/aether-impl/src/main/java/org/eclipse/aether/internal/impl/NodeStack.java
+++ b/aether-impl/src/main/java/org/eclipse/aether/internal/impl/NodeStack.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -13,7 +13,6 @@
 import java.util.Arrays;
 
 import org.eclipse.aether.artifact.Artifact;
-import org.eclipse.aether.graph.Dependency;
 import org.eclipse.aether.graph.DependencyNode;
 
 /**
@@ -61,13 +60,12 @@
         {
             DependencyNode node = nodes[i];
 
-            Dependency dependency = node.getDependency();
-            if ( dependency == null )
+            Artifact a = node.getArtifact();
+            if ( a == null )
             {
                 break;
             }
 
-            Artifact a = dependency.getArtifact();
             if ( !a.getArtifactId().equals( artifact.getArtifactId() ) )
             {
                 continue;
diff --git a/aether-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDependencyCollectorTest.java b/aether-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDependencyCollectorTest.java
index a714d5b..b79e990 100644
--- a/aether-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDependencyCollectorTest.java
+++ b/aether-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDependencyCollectorTest.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -294,6 +294,29 @@
     }
 
     @Test
+    public void testCyclicProjects_ConsiderLabelOfRootlessGraph()
+        throws Exception
+    {
+        Dependency dep = newDep( "gid:aid:ver", "compile" );
+        CollectRequest request =
+            new CollectRequest().addDependency( dep ).addRepository( repository ).setRootArtifact( dep.getArtifact() );
+        CollectResult result = collector.collectDependencies( session, request );
+        DependencyNode root = result.getRoot();
+        DependencyNode a1 = root.getChildren().get( 0 );
+        assertEquals( "aid", a1.getArtifact().getArtifactId() );
+        assertEquals( "ver", a1.getArtifact().getVersion() );
+        DependencyNode a2 = a1.getChildren().get( 0 );
+        assertEquals( "aid2", a2.getArtifact().getArtifactId() );
+        assertEquals( "ver", a2.getArtifact().getVersion() );
+
+        assertEquals( 1, result.getCycles().size() );
+        DependencyCycle cycle = result.getCycles().get( 0 );
+        assertEquals( Arrays.asList(), cycle.getPrecedingDependencies() );
+        assertEquals( Arrays.asList( new Dependency( dep.getArtifact(), null ), a1.getDependency() ),
+                      cycle.getCyclicDependencies() );
+    }
+
+    @Test
     public void testPartialResultOnError()
         throws IOException
     {