diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala
index 1d8ef8a8a..a33f347f2 100644
--- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala
+++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala
@@ -31,7 +31,7 @@ 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 dpendency-graph for a project")
- val moduleGraph = TaskKey[IvyGraphMLDependencies.ModuleGraph]("module-graph",
+ val moduleGraph = TaskKey[ModuleGraph]("module-graph",
"The dependency graph for a project")
val asciiGraph = TaskKey[String]("dependency-graph-string",
"Returns a string containing the ascii representation of the dependency graph for a project")
@@ -54,7 +54,7 @@ trait DependencyGraphKeys {
"Aggregates and shows information about the licenses of dependencies")
// internal
- private[graph] val moduleGraphStore = TaskKey[IvyGraphMLDependencies.ModuleGraph]("module-graph-store", "The stored module-graph from the last run")
+ private[graph] val moduleGraphStore = TaskKey[ModuleGraph]("module-graph-store", "The stored module-graph from the last run")
private[graph] val whatDependsOn = InputKey[Unit]("what-depends-on", "Shows information about what depends on the given module")
}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala
index f943e2d96..9f8bffce5 100644
--- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala
+++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala
@@ -26,8 +26,6 @@ import sbt.complete.Parser
import org.apache.ivy.core.resolve.ResolveOptions
-import IvyGraphMLDependencies.ModuleGraph
-
object DependencyGraphSettings {
import DependencyGraphKeys._
import ModuleGraphProtocol._
@@ -54,23 +52,23 @@ object DependencyGraphSettings {
def ivyReportForConfig(config: Configuration) = inConfig(config)(Seq(
ivyReport <<= ivyReportFunction map (_(config.toString)) dependsOn(ignoreMissingUpdate),
- moduleGraph <<= ivyReport map (absoluteReportPath.andThen(IvyGraphMLDependencies.graph)),
+ moduleGraph <<= ivyReport map (absoluteReportPath.andThen(frontend.IvyReport.fromReportFile)),
moduleGraph <<= (scalaVersion, moduleGraph, filterScalaLibrary) map { (scalaV, graph, filter) =>
if (filter)
- IvyGraphMLDependencies.ignoreScalaLibrary(scalaV, graph)
+ GraphTransformations.ignoreScalaLibrary(scalaV, graph)
else
graph
},
moduleGraphStore <<= moduleGraph storeAs moduleGraphStore triggeredBy moduleGraph,
- asciiGraph <<= moduleGraph map IvyGraphMLDependencies.asciiGraph,
+ asciiGraph <<= moduleGraph map rendering.AsciiGraph.asciiGraph,
dependencyGraph <<= InputTask(shouldForceParser) { force =>
(force, moduleGraph, streams) map { (force, graph, streams) =>
if (force || graph.nodes.size < 15) {
- streams.log.info(IvyGraphMLDependencies.asciiGraph(graph))
+ streams.log.info(rendering.AsciiGraph.asciiGraph(graph))
streams.log.info("\n\n")
streams.log.info("Note: The old tree layout is still available by using `dependency-tree`")
} else {
- streams.log.info(IvyGraphMLDependencies.asciiTree(graph))
+ streams.log.info(rendering.AsciiTree.asciiTree(graph))
if (!force) {
streams.log.info("\n")
@@ -79,7 +77,7 @@ object DependencyGraphSettings {
}
}
},
- asciiTree <<= moduleGraph map IvyGraphMLDependencies.asciiTree,
+ asciiTree <<= moduleGraph map rendering.AsciiTree.asciiTree,
dependencyTree <<= print(asciiTree),
dependencyGraphMLFile <<= target / "dependencies-%s.graphml".format(config.toString),
dependencyGraphML <<= dependencyGraphMLTask,
@@ -98,7 +96,7 @@ object DependencyGraphSettings {
},
whatDependsOn <<= InputTask(artifactIdParser) { module =>
(module, streams, moduleGraph) map { (module, streams, graph) =>
- streams.log.info(IvyGraphMLDependencies.asciiTree(IvyGraphMLDependencies.reverseGraphStartingAt(graph, module)))
+ streams.log.info(rendering.AsciiTree.asciiTree(GraphTransformations.reverseGraphStartingAt(graph, module)))
}
},
licenseInfo <<= (moduleGraph, streams) map showLicenseInfo
@@ -109,7 +107,7 @@ object DependencyGraphSettings {
def dependencyGraphMLTask =
(moduleGraph, dependencyGraphMLFile, streams) map { (graph, resultFile, streams) =>
- IvyGraphMLDependencies.saveAsGraphML(graph, resultFile.getAbsolutePath)
+ rendering.GraphML.saveAsGraphML(graph, resultFile.getAbsolutePath)
streams.log.info("Wrote dependency graph to '%s'" format resultFile)
resultFile
}
@@ -117,7 +115,7 @@ object DependencyGraphSettings {
(moduleGraph, dependencyDotHeader, dependencyDotNodeLabel, dependencyDotFile, streams).map {
(graph, dotHead, nodeLabel, outFile, streams) =>
- val resultFile = IvyGraphMLDependencies.saveAsDot(graph, dotHead, nodeLabel, outFile)
+ val resultFile = rendering.DOT.saveAsDot(graph, dotHead, nodeLabel, outFile)
streams.log.info("Wrote dependency graph to '%s'" format resultFile)
resultFile
}
@@ -143,8 +141,6 @@ object DependencyGraphSettings {
(Space ~> token("--force")).?.map(_.isDefined)
}
- import IvyGraphMLDependencies.ModuleId
-
val artifactIdParser: Initialize[State => Parser[ModuleId]] =
resolvedScoped { ctx => (state: State) =>
val graph = loadFromContext(moduleGraphStore, ctx, state) getOrElse ModuleGraph(Nil, Nil)
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/GraphTransformations.scala b/src/main/scala/net/virtualvoid/sbt/graph/GraphTransformations.scala
new file mode 100644
index 000000000..dc4e976c2
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/GraphTransformations.scala
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2011, 2012 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
+
+object GraphTransformations {
+ def reverseGraphStartingAt(graph: ModuleGraph, root: ModuleId): ModuleGraph = {
+ val deps = graph.reverseDependencyMap
+
+ def visit(module: ModuleId, visited: Set[ModuleId]): Seq[(ModuleId, ModuleId)] =
+ if (visited(module))
+ Nil
+ else
+ deps.get(module) match {
+ case Some(deps) =>
+ deps.flatMap { to =>
+ (module, to.id) +: visit(to.id, visited + module)
+ }
+ case None => Nil
+ }
+
+ val edges = visit(root, Set.empty)
+ val nodes = edges.foldLeft(Set.empty[ModuleId])((set, edge) => set + edge._1 + edge._2).map(graph.module)
+ ModuleGraph(nodes.toSeq, edges)
+ }
+
+ def ignoreScalaLibrary(scalaVersion: String, graph: ModuleGraph): ModuleGraph = {
+ def isScalaLibrary(m: Module) = isScalaLibraryId(m.id)
+ def isScalaLibraryId(id: ModuleId) = id.organisation == "org.scala-lang" && id.name == "scala-library"
+
+ def dependsOnScalaLibrary(m: Module): Boolean =
+ graph.dependencyMap(m.id).exists(isScalaLibrary)
+
+ def addScalaLibraryAnnotation(m: Module): Module = {
+ if (dependsOnScalaLibrary(m))
+ m.copy(extraInfo = m.extraInfo + " [S]")
+ else
+ m
+ }
+
+ val newNodes = graph.nodes.map(addScalaLibraryAnnotation).filterNot(isScalaLibrary)
+ val newEdges = graph.edges.filterNot(e => isScalaLibraryId(e._2))
+ ModuleGraph(newNodes, newEdges)
+ }
+}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala b/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala
deleted file mode 100644
index fd84b63f9..000000000
--- a/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Copyright 2011, 2012 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
-
-import xml.parsing.ConstructingParser
-import java.io.File
-import collection.mutable.HashMap
-import collection.mutable.MultiMap
-import collection.mutable.{Set => MSet}
-import sbt.ConsoleLogger
-import xml.{NodeSeq, Document, XML, Node}
-import com.github.mdr.ascii.layout
-import layout._
-import sbinary.{Format, DefaultProtocol}
-
-object IvyGraphMLDependencies extends App {
- case class ModuleId(organisation: String,
- name: String,
- version: String) {
- def idString: String = organisation+":"+name+":"+version
- }
- case class Module(id: ModuleId,
- license: Option[String] = None,
- extraInfo: String = "",
- evictedByVersion: Option[String] = None,
- error: Option[String] = None) {
- def hadError: Boolean = error.isDefined
- def isUsed: Boolean = !evictedByVersion.isDefined
- }
-
- type Edge = (ModuleId, ModuleId)
-
- case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) {
- lazy val modules: Map[ModuleId, Module] =
- nodes.map(n => (n.id, n)).toMap
-
- def module(id: ModuleId): Module = modules(id)
-
- lazy val dependencyMap: Map[ModuleId, Seq[Module]] =
- createMap(identity)
-
- lazy val reverseDependencyMap: Map[ModuleId, Seq[Module]] =
- createMap { case (a, b) => (b, a) }
-
- def createMap(bindingFor: ((ModuleId, ModuleId)) => (ModuleId, ModuleId)): Map[ModuleId, Seq[Module]] = {
- val m = new HashMap[ModuleId, MSet[Module]] with MultiMap[ModuleId, Module]
- edges.foreach { entry =>
- val (f, t) = bindingFor(entry)
- m.addBinding(f, module(t))
- }
- m.toMap.mapValues(_.toSeq.sortBy(_.id.idString)).withDefaultValue(Nil)
- }
- }
-
- def graph(ivyReportFile: String): ModuleGraph =
- buildGraph(buildDoc(ivyReportFile))
-
- def buildGraph(doc: Document): ModuleGraph = {
- def edgesForModule(id: ModuleId, revision: NodeSeq): Seq[Edge] =
- for {
- caller <- revision \ "caller"
- callerModule = moduleIdFromElement(caller, caller.attribute("callerrev").get.text)
- } yield (moduleIdFromElement(caller, caller.attribute("callerrev").get.text), id)
-
- val moduleEdges: Seq[(Module, Seq[Edge])] = for {
- mod <- doc \ "dependencies" \ "module"
- revision <- mod \ "revision"
- rev = revision.attribute("name").get.text
- moduleId = moduleIdFromElement(mod, rev)
- module = Module(moduleId,
- (revision \ "license").headOption.flatMap(_.attribute("name")).map(_.text),
- evictedByVersion = (revision \ "evicted-by").headOption.flatMap(_.attribute("rev").map(_.text)),
- error = revision.attribute("error").map(_.text))
- } yield (module, edgesForModule(moduleId, revision))
-
- val (nodes, edges) = moduleEdges.unzip
-
- val info = (doc \ "info").head
- def infoAttr(name: String): String =
- info.attribute(name).getOrElse(throw new IllegalArgumentException("Missing attribute "+name)).text
- val rootModule = Module(ModuleId(infoAttr("organisation"), infoAttr("module"), infoAttr("revision")))
-
- ModuleGraph(rootModule +: nodes, edges.flatten)
- }
-
- def reverseGraphStartingAt(graph: ModuleGraph, root: ModuleId): ModuleGraph = {
- val deps = graph.reverseDependencyMap
-
- def visit(module: ModuleId, visited: Set[ModuleId]): Seq[(ModuleId, ModuleId)] =
- if (visited(module))
- Nil
- else
- deps.get(module) match {
- case Some(deps) =>
- deps.flatMap { to =>
- (module, to.id) +: visit(to.id, visited + module)
- }
- case None => Nil
- }
-
- val edges = visit(root, Set.empty)
- val nodes = edges.foldLeft(Set.empty[ModuleId])((set, edge) => set + edge._1 + edge._2).map(graph.module)
- ModuleGraph(nodes.toSeq, edges)
- }
-
- def ignoreScalaLibrary(scalaVersion: String, graph: ModuleGraph): ModuleGraph = {
- def isScalaLibrary(m: Module) = isScalaLibraryId(m.id)
- def isScalaLibraryId(id: ModuleId) = id.organisation == "org.scala-lang" && id.name == "scala-library"
-
- def dependsOnScalaLibrary(m: Module): Boolean =
- graph.dependencyMap(m.id).exists(isScalaLibrary)
-
- def addScalaLibraryAnnotation(m: Module): Module = {
- if (dependsOnScalaLibrary(m))
- m.copy(extraInfo = m.extraInfo + " [S]")
- else
- m
- }
-
- val newNodes = graph.nodes.map(addScalaLibraryAnnotation).filterNot(isScalaLibrary)
- val newEdges = graph.edges.filterNot(e => isScalaLibraryId(e._2))
- ModuleGraph(newNodes, newEdges)
- }
-
- def asciiGraph(graph: ModuleGraph): String =
- Layouter.renderGraph(buildAsciiGraph(graph))
-
- def asciiTree(graph: ModuleGraph): String = {
- val deps = graph.dependencyMap
-
- // there should only be one root node (the project itself)
- val roots = graph.nodes.filter(n => !graph.edges.exists(_._2 == n.id)).sortBy(_.id.idString)
- roots.map { root =>
- util.AsciiTreeLayout.toAscii[Module](root, node => deps.getOrElse(node.id, Seq.empty[Module]), displayModule)
- }.mkString("\n")
- }
-
- def displayModule(module: Module): String =
- red(module.id.idString +
- module.extraInfo +
- module.error.map(" (error: "+_+")").getOrElse("") +
- module.evictedByVersion.map(_ formatted " (evicted by: %s)").getOrElse(""), module.hadError)
-
- private def buildAsciiGraph(moduleGraph: ModuleGraph): layout.Graph[String] = {
- def renderVertex(module: Module): String =
- module.id.name + module.extraInfo + "\n" +
- module.id.organisation + "\n" +
- module.id.version +
- module.error.map("\nerror: "+_).getOrElse("") +
- module.evictedByVersion.map(_ formatted "\nevicted by: %s").getOrElse("")
-
- val vertices = moduleGraph.nodes.map(renderVertex).toList
- val edges = moduleGraph.edges.toList.map { case (from, to) ⇒ (renderVertex(moduleGraph.module(from)), renderVertex(moduleGraph.module(to))) }
- layout.Graph(vertices, edges)
- }
-
- def saveAsGraphML(graph: ModuleGraph, outputFile: String) {
- val nodesXml =
- for (n <- graph.nodes)
- yield
-
-
- {n.id.idString}
-
-
-
- val edgesXml =
- for (e <- graph.edges)
- yield
-
- val xml =
-
-
-
- {nodesXml}
- {edgesXml}
-
-
-
- XML.save(outputFile, xml)
- }
- def saveAsDot(graph: ModuleGraph,
- dotHead: String,
- nodeFormation: (String, String, String) => String,
- outputFile: File): File = {
- val nodes = {
- for (n <- graph.nodes)
- yield
- """ "%s"[label=%s]""".format(n.id.idString,
- nodeFormation(n.id.organisation, n.id.name, n.id.version))
- }.mkString("\n")
-
- val edges = {
- for ( e <- graph.edges)
- yield
- """ "%s" -> "%s"""".format(e._1.idString, e._2.idString)
- }.mkString("\n")
-
- val dot = "%s\n%s\n%s\n}".format(dotHead, nodes, edges)
-
- sbt.IO.write(outputFile, dot)
- outputFile
- }
-
- def moduleIdFromElement(element: Node, version: String): ModuleId =
- ModuleId(element.attribute("organisation").get.text, element.attribute("name").get.text, version)
-
- private def buildDoc(ivyReportFile: String) = ConstructingParser.fromSource(io.Source.fromFile(ivyReportFile), false).document
-
- def red(str: String, doRed: Boolean): String =
- if (ConsoleLogger.formatEnabled && doRed)
- Console.RED + str + Console.RESET
- else
- str
-
- def die(msg: String): Nothing = {
- println(msg)
- sys.exit(1)
- }
- def usage: String =
- "Usage: "
-
- val reportFile = args.lift(0).filter(f => new File(f).exists).getOrElse(die(usage))
- val outputFile = args.lift(1).getOrElse(die(usage))
- saveAsGraphML(graph(reportFile), outputFile)
-}
-
-object ModuleGraphProtocol extends DefaultProtocol {
- import IvyGraphMLDependencies._
-
- implicit def seqFormat[T: Format]: Format[Seq[T]] = wrap[Seq[T], List[T]](_.toList, _.toSeq)
- implicit val ModuleIdFormat: Format[ModuleId] = asProduct3(ModuleId)(ModuleId.unapply(_).get)
- implicit val ModuleFormat: Format[Module] = asProduct5(Module)(Module.unapply(_).get)
- implicit val ModuleGraphFormat: Format[ModuleGraph] = asProduct2(ModuleGraph)(ModuleGraph.unapply(_).get)
-}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/Main.scala b/src/main/scala/net/virtualvoid/sbt/graph/Main.scala
new file mode 100644
index 000000000..497258260
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/Main.scala
@@ -0,0 +1,33 @@
+/*
+ * 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
+
+import java.io.File
+
+object Main extends App {
+ def die(msg: String): Nothing = {
+ println(msg)
+ sys.exit(1)
+ }
+ def usage: String =
+ "Usage: "
+
+ val reportFile = args.lift(0).filter(f => new File(f).exists).getOrElse(die(usage))
+ val outputFile = args.lift(1).getOrElse(die(usage))
+ val graph = frontend.IvyReport.fromReportFile(reportFile)
+ rendering.GraphML.saveAsGraphML(graph, outputFile)
+}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/frontend/IvyReport.scala b/src/main/scala/net/virtualvoid/sbt/graph/frontend/IvyReport.scala
new file mode 100644
index 000000000..94c9b61e9
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/frontend/IvyReport.scala
@@ -0,0 +1,61 @@
+/*
+ * 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.frontend
+
+import net.virtualvoid.sbt.graph._
+
+import scala.xml.{NodeSeq, Document, Node}
+import scala.xml.parsing.ConstructingParser
+
+object IvyReport {
+ def fromReportFile(ivyReportFile: String): ModuleGraph =
+ fromReportXML(loadXML(ivyReportFile))
+
+ def fromReportXML(doc: Document): ModuleGraph = {
+ def edgesForModule(id: ModuleId, revision: NodeSeq): Seq[Edge] =
+ for {
+ caller <- revision \ "caller"
+ callerModule = moduleIdFromElement(caller, caller.attribute("callerrev").get.text)
+ } yield (moduleIdFromElement(caller, caller.attribute("callerrev").get.text), id)
+
+ val moduleEdges: Seq[(Module, Seq[Edge])] = for {
+ mod <- doc \ "dependencies" \ "module"
+ revision <- mod \ "revision"
+ rev = revision.attribute("name").get.text
+ moduleId = moduleIdFromElement(mod, rev)
+ module = Module(moduleId,
+ (revision \ "license").headOption.flatMap(_.attribute("name")).map(_.text),
+ evictedByVersion = (revision \ "evicted-by").headOption.flatMap(_.attribute("rev").map(_.text)),
+ error = revision.attribute("error").map(_.text))
+ } yield (module, edgesForModule(moduleId, revision))
+
+ val (nodes, edges) = moduleEdges.unzip
+
+ val info = (doc \ "info").head
+ def infoAttr(name: String): String =
+ info.attribute(name).getOrElse(throw new IllegalArgumentException("Missing attribute "+name)).text
+ val rootModule = Module(ModuleId(infoAttr("organisation"), infoAttr("module"), infoAttr("revision")))
+
+ ModuleGraph(rootModule +: nodes, edges.flatten)
+ }
+
+ private def moduleIdFromElement(element: Node, version: String): ModuleId =
+ ModuleId(element.attribute("organisation").get.text, element.attribute("name").get.text, version)
+
+ private def loadXML(ivyReportFile: String) =
+ ConstructingParser.fromSource(io.Source.fromFile(ivyReportFile), preserveWS = false).document()
+}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/model.scala b/src/main/scala/net/virtualvoid/sbt/graph/model.scala
new file mode 100644
index 000000000..9af79d129
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/model.scala
@@ -0,0 +1,63 @@
+/*
+ * 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
+
+import scala.collection.mutable.{MultiMap, HashMap, Set}
+
+case class ModuleId(organisation: String,
+ name: String,
+ version: String) {
+ def idString: String = organisation+":"+name+":"+version
+}
+case class Module(id: ModuleId,
+ license: Option[String] = None,
+ extraInfo: String = "",
+ evictedByVersion: Option[String] = None,
+ error: Option[String] = None) {
+ def hadError: Boolean = error.isDefined
+ def isUsed: Boolean = !evictedByVersion.isDefined
+}
+
+case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) {
+ lazy val modules: Map[ModuleId, Module] =
+ nodes.map(n => (n.id, n)).toMap
+
+ def module(id: ModuleId): Module = modules(id)
+
+ lazy val dependencyMap: Map[ModuleId, Seq[Module]] =
+ createMap(identity)
+
+ lazy val reverseDependencyMap: Map[ModuleId, Seq[Module]] =
+ createMap { case (a, b) => (b, a) }
+
+ def createMap(bindingFor: ((ModuleId, ModuleId)) => (ModuleId, ModuleId)): Map[ModuleId, Seq[Module]] = {
+ val m = new HashMap[ModuleId, Set[Module]] with MultiMap[ModuleId, Module]
+ edges.foreach { entry =>
+ val (f, t) = bindingFor(entry)
+ m.addBinding(f, module(t))
+ }
+ m.toMap.mapValues(_.toSeq.sortBy(_.id.idString)).withDefaultValue(Nil)
+ }
+}
+
+import sbinary.{Format, DefaultProtocol}
+object ModuleGraphProtocol extends DefaultProtocol {
+ implicit def seqFormat[T: Format]: Format[Seq[T]] = wrap[Seq[T], List[T]](_.toList, _.toSeq)
+ implicit val ModuleIdFormat: Format[ModuleId] = asProduct3(ModuleId)(ModuleId.unapply(_).get)
+ implicit val ModuleFormat: Format[Module] = asProduct5(Module)(Module.unapply(_).get)
+ implicit val ModuleGraphFormat: Format[ModuleGraph] = asProduct2(ModuleGraph.apply _)(ModuleGraph.unapply(_).get)
+}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/package.scala b/src/main/scala/net/virtualvoid/sbt/graph/package.scala
new file mode 100644
index 000000000..1fe4e5fc1
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/package.scala
@@ -0,0 +1,21 @@
+/*
+ * 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
+
+package object graph {
+ type Edge = (ModuleId, ModuleId)
+}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiGraph.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiGraph.scala
new file mode 100644
index 000000000..f4b21ea95
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiGraph.scala
@@ -0,0 +1,38 @@
+/*
+ * 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
+package rendering
+
+import com.github.mdr.ascii.layout._
+
+object AsciiGraph {
+ def asciiGraph(graph: ModuleGraph): String =
+ Layouter.renderGraph(buildAsciiGraph(graph))
+
+ private def buildAsciiGraph(moduleGraph: ModuleGraph): Graph[String] = {
+ def renderVertex(module: Module): String =
+ module.id.name + module.extraInfo + "\n" +
+ module.id.organisation + "\n" +
+ module.id.version +
+ module.error.map("\nerror: "+_).getOrElse("") +
+ module.evictedByVersion.map(_ formatted "\nevicted by: %s").getOrElse("")
+
+ val vertices = moduleGraph.nodes.map(renderVertex).toList
+ val edges = moduleGraph.edges.toList.map { case (from, to) ⇒ (renderVertex(moduleGraph.module(from)), renderVertex(moduleGraph.module(to))) }
+ Graph(vertices, edges)
+ }
+}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala
new file mode 100644
index 000000000..d406a310e
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala
@@ -0,0 +1,39 @@
+/*
+ * 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
+package rendering
+
+import util.AsciiTreeLayout
+import util.ConsoleUtils._
+
+object AsciiTree {
+ def asciiTree(graph: ModuleGraph): String = {
+ val deps = graph.dependencyMap
+
+ // there should only be one root node (the project itself)
+ val roots = graph.nodes.filter(n => !graph.edges.exists(_._2 == n.id)).sortBy(_.id.idString)
+ roots.map { root =>
+ AsciiTreeLayout.toAscii[Module](root, node => deps.getOrElse(node.id, Seq.empty[Module]), displayModule)
+ }.mkString("\n")
+ }
+
+ def displayModule(module: Module): String =
+ red(module.id.idString +
+ module.extraInfo +
+ module.error.map(" (error: "+_+")").getOrElse("") +
+ module.evictedByVersion.map(_ formatted " (evicted by: %s)").getOrElse(""), module.hadError)
+}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala
new file mode 100644
index 000000000..e7cb2c3f5
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala
@@ -0,0 +1,46 @@
+/*
+ * 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.rendering
+
+import java.io.File
+
+import net.virtualvoid.sbt.graph.ModuleGraph
+
+object DOT {
+ def saveAsDot(graph: ModuleGraph,
+ dotHead: String,
+ nodeFormation: (String, String, String) => String,
+ outputFile: File): File = {
+ val nodes = {
+ for (n <- graph.nodes)
+ yield
+ """ "%s"[label=%s]""".format(n.id.idString,
+ nodeFormation(n.id.organisation, n.id.name, n.id.version))
+ }.mkString("\n")
+
+ val edges = {
+ for ( e <- graph.edges)
+ yield
+ """ "%s" -> "%s"""".format(e._1.idString, e._2.idString)
+ }.mkString("\n")
+
+ val dot = "%s\n%s\n%s\n}".format(dotHead, nodes, edges)
+
+ sbt.IO.write(outputFile, dot)
+ outputFile
+ }
+}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/GraphML.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/GraphML.scala
new file mode 100644
index 000000000..48bf1ce6b
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/GraphML.scala
@@ -0,0 +1,52 @@
+/*
+ * 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.rendering
+
+import net.virtualvoid.sbt.graph.ModuleGraph
+
+import scala.xml.XML
+
+object GraphML {
+ def saveAsGraphML(graph: ModuleGraph, outputFile: String) {
+ val nodesXml =
+ for (n <- graph.nodes)
+ yield
+
+
+ {n.id.idString}
+
+
+
+ val edgesXml =
+ for (e <- graph.edges)
+ yield
+
+ val xml =
+
+
+
+ {nodesXml}
+ {edgesXml}
+
+
+
+ XML.save(outputFile, xml)
+ }
+}
diff --git a/src/main/scala/net/virtualvoid/sbt/graph/util/ConsoleUtils.scala b/src/main/scala/net/virtualvoid/sbt/graph/util/ConsoleUtils.scala
new file mode 100644
index 000000000..ceba7da65
--- /dev/null
+++ b/src/main/scala/net/virtualvoid/sbt/graph/util/ConsoleUtils.scala
@@ -0,0 +1,27 @@
+/*
+ * 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 sbt.ConsoleLogger
+
+object ConsoleUtils {
+ def red(str: String, doRed: Boolean): String =
+ if (ConsoleLogger.formatEnabled && doRed)
+ Console.RED + str + Console.RESET
+ else
+ str
+}
diff --git a/src/main/scala/sbt/SbtAccess.scala b/src/main/scala/sbt/SbtAccess.scala
index eb9113300..a75a57c31 100644
--- a/src/main/scala/sbt/SbtAccess.scala
+++ b/src/main/scala/sbt/SbtAccess.scala
@@ -16,6 +16,7 @@
package sbt
+/** Accessors to private[sbt] symbols. */
object SbtAccess {
val unmanagedScalaInstanceOnly = Defaults.unmanagedScalaInstanceOnly