diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala
index d8ccdf62b..0dcbb0d15 100644
--- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala
+++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala
@@ -31,6 +31,8 @@ trait DependencyGraphKeys {
"The header of the dot file. (e.g. to set your preferred node shapes)")
val dependencyDot = TaskKey[File]("dependency-dot",
"Creates a dot file containing the dependency-graph for a project")
+ val dependencyDotString = TaskKey[String]("dependency-dot-string",
+ "Creates a String containing the dependency-graph for a project in dot format")
val moduleGraph = TaskKey[ModuleGraph]("module-graph",
"The dependency graph for a project")
val asciiGraph = TaskKey[String]("dependency-graph-string",
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala
index f1ff27d6c..29c8fc306 100644
--- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala
+++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala
@@ -17,12 +17,12 @@
package net.virtualvoid.sbt.graph
import net.virtualvoid.sbt.graph.backend.{ IvyReport, SbtUpdateReport }
+import net.virtualvoid.sbt.graph.util.IOUtil
import sbt._
import Keys._
import CrossVersion._
-import sbt.complete.DefaultParsers._
import sbt.complete.Parser
import org.apache.ivy.core.resolve.ResolveOptions
@@ -81,17 +81,15 @@ object DependencyGraphSettings {
dependencyGraphMLFile <<= target / "dependencies-%s.graphml".format(config.toString),
dependencyGraphML <<= dependencyGraphMLTask,
dependencyDotFile <<= target / "dependencies-%s.dot".format(config.toString),
- dependencyDot <<= dependencyDotTask,
+ dependencyDotString <<= dependencyDotStringTask,
+ dependencyDot <<= writeToFile(dependencyDotString, dependencyDotFile),
dependencyDotHeader := """digraph "dependency-graph" {
| graph[rankdir="LR"]
- | node [
- | shape="record"
- | ]
| edge [
| arrowtail="none"
| ]""".stripMargin,
dependencyDotNodeLabel := { (organisation: String, name: String, version: String) ⇒
- """<%s
%s
%s>""".format(organisation, name, version)
+ """%s
%s
%s""".format(organisation, name, version)
},
whatDependsOn <<= InputTask(artifactIdParser) { module ⇒
(module, streams, moduleGraph) map { (module, streams, graph) ⇒
@@ -115,14 +113,19 @@ object DependencyGraphSettings {
streams.log.info("Wrote dependency graph to '%s'" format resultFile)
resultFile
}
- def dependencyDotTask =
- (moduleGraph, dependencyDotHeader, dependencyDotNodeLabel, dependencyDotFile, streams).map {
- (graph, dotHead, nodeLabel, outFile, streams) ⇒
-
- val resultFile = rendering.DOT.saveAsDot(graph, dotHead, nodeLabel, outFile)
- streams.log.info("Wrote dependency graph to '%s'" format resultFile)
- resultFile
+ def dependencyDotStringTask =
+ (moduleGraph, dependencyDotHeader, dependencyDotNodeLabel).map {
+ (graph, dotHead, nodeLabel) ⇒ rendering.DOT.dotGraph(graph, dotHead, nodeLabel)
}
+
+ def writeToFile(dataTask: TaskKey[String], fileTask: SettingKey[File]) =
+ (dataTask, fileTask, streams).map { (data, outFile, streams) ⇒
+ IOUtil.writeToFile(data, outFile)
+
+ streams.log.info("Wrote dependency graph to '%s'" format outFile)
+ outFile
+ }
+
def absoluteReportPath = (file: File) ⇒ file.getAbsolutePath
def print(key: TaskKey[String]) =
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala
index 3eb168e5b..181e8b7eb 100644
--- a/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala
+++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala
@@ -14,31 +14,50 @@
* limitations under the License.
*/
-package net.virtualvoid.sbt.graph.rendering
-
-import java.io.File
-
-import net.virtualvoid.sbt.graph.ModuleGraph
+package net.virtualvoid.sbt.graph
+package rendering
object DOT {
- def saveAsDot(graph: ModuleGraph,
- dotHead: String,
- nodeFormation: (String, String, String) ⇒ String,
- outputFile: File): File = {
+ val EvictedStyle = "stroke-dasharray: 5,5"
+
+ def dotGraph(graph: ModuleGraph,
+ dotHead: String,
+ nodeFormation: (String, String, String) ⇒ String): String = {
val nodes = {
- for (n ← graph.nodes)
- yield """ "%s"[label=%s]""".format(n.id.idString,
- nodeFormation(n.id.organisation, n.id.name, n.id.version))
+ for (n ← graph.nodes) yield {
+ val style = if (n.isEvicted) EvictedStyle else ""
+ """ "%s"[labelType="html" label="%s" style="%s"]""".format(n.id.idString,
+ nodeFormation(n.id.organisation, n.id.name, n.id.version),
+ style)
+ }
}.mkString("\n")
+ def originWasEvicted(edge: Edge): Boolean = graph.module(edge._1).isEvicted
+ def targetWasEvicted(edge: Edge): Boolean = graph.module(edge._2).isEvicted
+
+ // add extra edges from evicted to evicted-by module
+ val evictedByEdges: Seq[Edge] =
+ graph.nodes.filter(_.isEvicted).map(m ⇒ Edge(m.id, m.id.copy(version = m.evictedByVersion.get)))
+
+ // remove edges to new evicted-by module which is now replaced by a chain
+ // dependend -> [evicted] -> dependee
+ val evictionTargetEdges =
+ graph.edges.filter(targetWasEvicted).map {
+ case (from, evicted) ⇒ (from, evicted.copy(version = graph.module(evicted).evictedByVersion.get))
+ }.toSet
+
+ val filteredEdges =
+ graph.edges
+ .filterNot(e ⇒ originWasEvicted(e) || evictionTargetEdges(e)) ++ evictedByEdges
+
val edges = {
- for (e ← graph.edges)
- yield """ "%s" -> "%s"""".format(e._1.idString, e._2.idString)
+ for (e ← filteredEdges) yield {
+ val extra = if (graph.module(e._1).isEvicted)
+ s""" [label="Evicted By" style="$EvictedStyle"]""" else ""
+ """ "%s" -> "%s"%s""".format(e._1.idString, e._2.idString, extra)
+ }
}.mkString("\n")
- val dot = "%s\n%s\n%s\n}".format(dotHead, nodes, edges)
-
- sbt.IO.write(outputFile, dot)
- outputFile
+ "%s\n%s\n%s\n}".format(dotHead, nodes, edges)
}
}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/util/IOUtil.scala b/src/main/scala/net/virtualvoid/sbt/graph/util/IOUtil.scala
new file mode 100644
index 000000000..c9b8ebbf5
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/util/IOUtil.scala
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 Johannes Rudolph
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.virtualvoid.sbt.graph.util
+
+import java.io.{ OutputStream, InputStream, FileOutputStream, File }
+import java.nio.charset.Charset
+
+import scala.annotation.tailrec
+
+object IOUtil {
+ val utf8 = Charset.forName("utf8")
+
+ def writeToFile(string: String, file: File): Unit =
+ sbt.IO.write(file, string, utf8)
+
+ def saveResource(resourcePath: String, to: File): Unit = {
+ val is = getClass.getClassLoader.getResourceAsStream(resourcePath)
+ require(is ne null, s"Couldn't load '$resourcePath' from classpath.")
+
+ val fos = new FileOutputStream(to)
+ try copy(is, fos)
+ finally {
+ is.close()
+ fos.close()
+ }
+ }
+
+ def copy(from: InputStream, to: OutputStream): Unit = {
+ val buffer = new Array[Byte](65536)
+
+ @tailrec def rec(): Unit = {
+ val read = from.read(buffer)
+ if (read > 0) {
+ to.write(buffer, 0, read)
+ rec()
+ } else if (read == 0)
+ throw new IllegalStateException("InputStream.read returned 0")
+ }
+ rec()
+ }
+}