diff --git a/src/main/scala-sbt-0.11/Graph.scala b/src/main/scala-sbt-0.11/Graph.scala new file mode 100644 index 000000000..ce4b8da7f --- /dev/null +++ b/src/main/scala-sbt-0.11/Graph.scala @@ -0,0 +1,43 @@ +/* sbt -- Simple Build Tool + * Copyright 2011 Mark Harrah, Eugene Yokota + * + * Copied from sbt 0.12 source code + */ +package sbt + +object Graph +{ + // [info] foo + // [info] +-bar + // [info] | +-baz + // [info] | + // [info] +-quux + def toAscii[A](top: A, children: A => Seq[A], display: A => String): String = { + val maxColumn = jline.Terminal.getTerminal.getTerminalWidth - 8 + val twoSpaces = " " + " " // prevent accidentally being converted into a tab + def limitLine(s: String): String = + if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".." + else s + def insertBar(s: String, at: Int): String = + s.slice(0, at) + + (s(at).toString match { + case " " => "|" + case x => x + }) + + s.slice(at + 1, s.length) + def toAsciiLines(node: A, level: Int): Vector[String] = { + val line = limitLine((twoSpaces * level) + (if (level == 0) "" else "+-") + display(node)) + val cs = Vector(children(node): _*) + val childLines = cs map {toAsciiLines(_, level + 1)} + val withBar = childLines.zipWithIndex flatMap { + case (lines, pos) if pos < (cs.size - 1) => lines map {insertBar(_, 2 * (level + 1))} + case (lines, pos) => + if (lines.last.trim != "") lines ++ Vector(twoSpaces * (level + 1)) + else lines + } + line +: withBar + } + + toAsciiLines(top, 0).mkString("\n") + } +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala b/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala index def166018..95dc2b1f5 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala @@ -18,24 +18,54 @@ package net.virtualvoid.sbt.graph import xml.parsing.ConstructingParser import java.io.File -import xml.{XML, Node} +import collection.mutable.HashMap +import collection.mutable.MultiMap +import collection.mutable.{Set => MSet} +import sbt.Graph +import xml.{Document, XML, Node} object IvyGraphMLDependencies extends App { - case class Module(organisation: String, name: String) { - def id: String = organisation+"."+name + case class Module(organisation: String, name: String, version: String) { + def id: String = organisation+":"+name+":"+version } - def transform(ivyReportFile: String, outputFile: String) { - val doc = ConstructingParser.fromSource(io.Source.fromFile(ivyReportFile), false).document - val edges = - for (mod <- doc \ "dependencies" \ "module"; - depModule = nodeFromElement(mod); - caller <- mod \ "revision" \ "caller"; - callerModule = nodeFromElement(caller)) - yield (callerModule, depModule) + case class ModuleGraph(nodes: Seq[Module], edges: Seq[(Module, Module)]) + + def buildGraph(doc: Document): ModuleGraph = { + val edges = for { + mod <- doc \ "dependencies" \ "module" + caller <- mod \ "revision" \ "caller" + callerModule = nodeFromElement(caller, caller.attribute("callerrev").get.text) + depModule = nodeFromElement(mod, caller.attribute("rev").get.text) + } yield (callerModule, depModule) val nodes = edges.flatMap(e => Seq(e._1, e._2)).distinct + ModuleGraph(nodes, edges) + } + + + def ascii(ivyReportFile: String): String = { + val doc = buildDoc(ivyReportFile) + val graph = buildGraph(doc) + import graph._ + val deps = { + val m = new HashMap[Module, MSet[Module]] with MultiMap[Module, Module] + edges.foreach { case (from, to) => m.addBinding(from, to) } + m.toMap.mapValues(_.toSeq.sortBy(_.id)) + } + // there should only be one root node (the project itself) + val roots = nodes.filter(n => !edges.exists(_._2 == n)).sortBy(_.id) + roots.map(root => + Graph.toAscii[Module](root, node => deps.getOrElse(node, Seq.empty[Module]), _.id) + ).mkString("\n") + } + + def transform(ivyReportFile: String, outputFile: String) { + val doc = buildDoc(ivyReportFile) + val graph = buildGraph(doc) + import graph._ + val nodesXml = for (n <- nodes) yield @@ -63,8 +93,10 @@ object IvyGraphMLDependencies extends App { XML.save(outputFile, xml) } - def nodeFromElement(element: Node): Module = - Module(element.attribute("organisation").get.text, element.attribute("name").get.text) + private def nodeFromElement(element: Node, version: String): Module = + Module(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 die(msg: String): Nothing = { println(msg) diff --git a/src/main/scala/net/virtualvoid/sbt/graph/Plugin.scala b/src/main/scala/net/virtualvoid/sbt/graph/Plugin.scala old mode 100644 new mode 100755 index 3c213d15b..d11d59973 --- a/src/main/scala/net/virtualvoid/sbt/graph/Plugin.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/Plugin.scala @@ -22,6 +22,10 @@ import Keys._ object Plugin extends sbt.Plugin { val dependencyGraphTask = TaskKey[File]("dependency-graph", "Creates a graphml file containing the dependency-graph for a project") + val asciiGraph = TaskKey[String]("ascii-graph", + "Returns a string containing the ascii representation of the dependency graph for a project") + val printAsciiGraph = TaskKey[Unit]("print-ascii-graph", + "Prints the ascii graph to the console") val ivyReportF = TaskKey[String => File]("ivy-report-function", "A function which returns the file containing the ivy report from the ivy cache for a given configuration") val ivyReport = InputKey[File]("ivy-report", @@ -46,7 +50,21 @@ object Plugin extends sbt.Plugin { streams.log.info("Wrote dependency graph to '%s'" format resultFile) resultFile } - ) + ) ++ Seq(Compile, Test, Runtime, Provided, Optional).flatMap(asciiGraphSettings) + + def asciiGraphSettings(config: Configuration) = + seq( + asciiGraph in config <<= asciiGraphTask(config), + printAsciiGraph in config <<= printAsciiGraphTask(config) + ) + + def asciiGraphTask(conf: Configuration) = (ivyReportF in conf) map { (report) => + IvyGraphMLDependencies.ascii(report(conf.name).getAbsolutePath) + } dependsOn(deliverLocal) + + def printAsciiGraphTask(conf: Configuration) = (asciiGraph in conf, streams in conf) map { (graph, streams) => + streams.log.info(graph) + } def crossName(ivyModule: IvySbt#Module) = ivyModule.moduleSettings match {