diff --git a/README.md b/README.md index e4ad588..42951fd 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ the notes of version [0.8.2](https://github.com/jrudolph/sbt-dependency-graph/tr ## Main Tasks - * `dependencyTree`: Shows an ASCII tree representation of the project's dependencies + * `dependencyTree`: Shows an ASCII tree representation of the project's dependencies (see [below](#dependencyTree-filtering) for examples filtering the output) * `dependencyBrowseGraph`: Opens a browser window with a visualization of the dependency graph (courtesy of graphlib-dot + dagre-d3). * `dependencyList`: Shows a flat list of all transitive dependencies on the sbt console (sorted by organization and name) * `whatDependsOn `: Find out what depends on an artifact. Shows a reverse dependency @@ -40,6 +40,43 @@ All tasks can be scoped to a configuration to get the report for a specific conf for example, prints the dependencies in the `test` configuration. If you don't specify any configuration, `compile` is assumed as usual. +### `dependencyTree` filtering +The `dependencyTree` task supports filtering with inclusion/exclusion rules: + +- exclusion rules are prefixed by `-` +- inclusion rules are the default (or can be prefixed by `+`) + +Dependencies are "preserved" iff: +- they match at least one inclusion rule (or no inclusion rules are provided), and +- they match no exclusion rules (including when none are provided) + +They are then displayed if they are preserved *or at least one of their transitive dependencies is preserved*. + +This mimics the behavior of [Maven dependency:tree](https://maven.apache.org/plugins/maven-dependency-plugin/tree-mojo.html)'s `includes` and `excludes` parameters. + +#### Examples + +Inclusions/Exclusions can be partial-matched against any part of a dependency's Maven coordinate: + +``` +dependencyTree -foo // exclude deps that contain "foo" in the group, name, or version +dependencyTree foo // include deps that contain "foo" in the group, name, or version +``` + +Or they can be fully-matched against specific parts of the coordinate: + +``` +dependencyTree -:foo* // exclude deps whose name starts with "foo" +dependencyTree -*foo*::*bar // exclude deps whose group contains "foo" and version ends with "bar" +``` + +Inclusions and exclusions can be combined and repeated: +``` +dependencyTree foo bar -baz // include only deps that contain "foo" or "bar" and not "baz" +``` + +In all cases, the full paths to dependencies that match the query are displayed (which can mean that dependencies are displayed even though they would have been excluded in their own right, because they form part of a chain to a dependency that was not excluded). + ## Configuration settings * `filterScalaLibrary`: Defines if the scala library should be excluded from the output of the dependency-* functions. diff --git a/build.sbt b/build.sbt index 411b401..ed5a2a4 100644 --- a/build.sbt +++ b/build.sbt @@ -27,4 +27,5 @@ scalacOptions ++= Seq( "-unchecked" ) -ScalariformSupport.formatSettings \ No newline at end of file +ScalariformSupport.formatSettings + diff --git a/project/ScalariformSupport.scala b/project/ScalariformSupport.scala index 665a9e6..95af53f 100644 --- a/project/ScalariformSupport.scala +++ b/project/ScalariformSupport.scala @@ -16,5 +16,6 @@ object ScalariformSupport { .setPreference(AlignParameters, true) .setPreference(AlignSingleLineCaseStatements, true) .setPreference(DoubleIndentClassDeclaration, true) + .setPreference(PreserveDanglingCloseParenthesis, true) } diff --git a/src/main/scala-sbt-0.13/net/virtualvoid/sbt/graph/rendering/AsciiGraph.scala b/src/main/scala-sbt-0.13/net/virtualvoid/sbt/graph/rendering/AsciiGraph.scala index 701fdce..057256f 100644 --- a/src/main/scala-sbt-0.13/net/virtualvoid/sbt/graph/rendering/AsciiGraph.scala +++ b/src/main/scala-sbt-0.13/net/virtualvoid/sbt/graph/rendering/AsciiGraph.scala @@ -19,6 +19,7 @@ package rendering import com.github.mdr.ascii.layout._ import net.virtualvoid.sbt.graph.DependencyGraphKeys._ +import net.virtualvoid.sbt.graph.model.{ Module, ModuleGraph } import sbt.Keys._ object AsciiGraph { @@ -49,7 +50,7 @@ object AsciiGraph { log.info("Note: The old tree layout is still available by using `dependency-tree`") } - log.info(rendering.AsciiTree.asciiTree(moduleGraph.value)) + log.info(AsciiTree(moduleGraph.value)) if (!force) { log.info("\n") diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala index ace01d1..fe4f40a 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala @@ -16,6 +16,7 @@ package net.virtualvoid.sbt.graph +import net.virtualvoid.sbt.graph.model.ModuleGraph import sbt._ trait DependencyGraphKeys { @@ -49,9 +50,9 @@ trait DependencyGraphKeys { "Returns a string containing the ascii representation of the dependency graph for a project") val dependencyGraph = InputKey[Unit]("dependency-graph", "Prints the ascii graph to the console") - val asciiTree = TaskKey[String]("dependency-tree-string", + val asciiTree = InputKey[String]("dependency-tree-string", "Returns a string containing an ascii tree representation of the dependency graph for a project") - val dependencyTree = TaskKey[Unit]("dependency-tree", + val dependencyTree = InputKey[Unit]("dependency-tree", "Prints an ascii tree of all the dependencies to the console") val dependencyList = TaskKey[Unit]("dependency-list", "Prints a list of all dependencies to the console") @@ -74,4 +75,4 @@ trait DependencyGraphKeys { private[graph] val crossProjectId = SettingKey[ModuleID]("dependency-graph-cross-project-id") } -object DependencyGraphKeys extends DependencyGraphKeys \ No newline at end of file +object DependencyGraphKeys extends DependencyGraphKeys diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala index 2bf7a13..195bc96 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala @@ -16,22 +16,23 @@ package net.virtualvoid.sbt.graph -import scala.language.reflectiveCalls - -import sbt._ -import Keys._ -import sbt.complete.Parser +import net.virtualvoid.sbt.graph.GraphTransformations.reverseGraphStartingAt import net.virtualvoid.sbt.graph.backend.{ IvyReport, SbtUpdateReport } -import net.virtualvoid.sbt.graph.rendering.{ AsciiGraph, DagreHTML } +import net.virtualvoid.sbt.graph.model.{ FilterRule, ModuleGraph, ModuleId } +import net.virtualvoid.sbt.graph.rendering.{ AsciiGraph, AsciiTree, DagreHTML } import net.virtualvoid.sbt.graph.util.IOUtil -import internal.librarymanagement._ -import librarymanagement._ +import sbt.Keys._ +import sbt._ +import sbt.complete.Parser import sbt.dependencygraph.DependencyGraphSbtCompat import sbt.dependencygraph.DependencyGraphSbtCompat.Implicits._ +import sbt.internal.librarymanagement._ + +import scala.language.reflectiveCalls object DependencyGraphSettings { import DependencyGraphKeys._ - import ModuleGraphProtocol._ + import net.virtualvoid.sbt.graph.model.ModuleGraphProtocol._ def graphSettings = baseSettings ++ reportSettings @@ -48,7 +49,17 @@ object DependencyGraphSettings { ivyReport := { Def.task { ivyReportFunction.value.apply(config.toString) } dependsOn (ignoreMissingUpdate) }.value, crossProjectId := sbt.CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(projectID.value), moduleGraphSbt := - ignoreMissingUpdate.value.configuration(configuration.value).map(report ⇒ SbtUpdateReport.fromConfigurationReport(report, crossProjectId.value)).getOrElse(ModuleGraph.empty), + ignoreMissingUpdate + .value + .configuration(configuration.value) + .map( + report ⇒ + SbtUpdateReport.fromConfigurationReport( + report, + crossProjectId.value + ) + ) + .getOrElse(ModuleGraph.empty), moduleGraphIvyReport := IvyReport.fromReportFile(absoluteReportPath(ivyReport.value)), moduleGraph := { sbtVersion.value match { @@ -65,8 +76,11 @@ object DependencyGraphSettings { else moduleGraph }, moduleGraphStore := (moduleGraph storeAs moduleGraphStore triggeredBy moduleGraph).value, - asciiTree := rendering.AsciiTree.asciiTree(moduleGraph.value), - dependencyTree := print(asciiTree).value, + asciiTree := AsciiTree( + moduleGraph.value, + filterRulesParser.parsed: _* + ), + dependencyTree := streams.value.log.info(asciiTree.evaluated), dependencyGraphMLFile := { target.value / "dependencies-%s.graphml".format(config.toString) }, dependencyGraphML := dependencyGraphMLTask.value, dependencyDotFile := { target.value / "dependencies-%s.dot".format(config.toString) }, @@ -92,8 +106,17 @@ object DependencyGraphSettings { """%s
%s
%s""".format(organisation, name, version) }, whatDependsOn := { - val module = artifactIdParser.parsed - streams.value.log.info(rendering.AsciiTree.asciiTree(GraphTransformations.reverseGraphStartingAt(moduleGraph.value, module))) + streams + .value + .log + .info( + AsciiTree( + reverseGraphStartingAt( + moduleGraph.value, + artifactIdParser.parsed + ) + ) + ) }, licenseInfo := showLicenseInfo(moduleGraph.value, streams.value)) ++ AsciiGraph.asciiGraphSetttings) @@ -105,7 +128,7 @@ object DependencyGraphSettings { (config: String) ⇒ { val org = projectID.organization val name = crossName(ivyModule) - file(s"${crossTarget}/resolution-cache/reports/$org-$name-$config.xml") + file(s"$crossTarget/resolution-cache/reports/$org-$name-$config.xml") } } @@ -151,29 +174,57 @@ object DependencyGraphSettings { }.mkString("\n\n") streams.log.info(output) } - - import Project._ val shouldForceParser: State ⇒ Parser[Boolean] = { (state: State) ⇒ import sbt.complete.DefaultParsers._ (Space ~> token("--force")).?.map(_.isDefined) } + val filterRulesParser: Def.Initialize[State ⇒ Parser[Seq[FilterRule]]] = + resolvedScoped { ctx ⇒ + (state: State) ⇒ + import sbt.complete.DefaultParsers._ + (Space ~> token(StringBasic, "filter")).*.map { + _.map(FilterRule(_)) + } + } + val artifactIdParser: Def.Initialize[State ⇒ Parser[ModuleId]] = resolvedScoped { ctx ⇒ (state: State) ⇒ val graph = loadFromContext(moduleGraphStore, ctx, state) getOrElse ModuleGraph(Nil, Nil) import sbt.complete.DefaultParsers._ - graph.nodes.map(_.id).map { - case id @ ModuleId(org, name, version) ⇒ - (Space ~ token(org) ~ token(Space ~ name) ~ token(Space ~ version)).map(_ ⇒ id) - }.reduceOption(_ | _).getOrElse { - (Space ~> token(StringBasic, "organization") ~ Space ~ token(StringBasic, "module") ~ Space ~ token(StringBasic, "version")).map { - case ((((org, _), mod), _), version) ⇒ - ModuleId(org, mod, version) + graph + .nodes + .map(_.id) + .map { + case id @ ModuleId(org, name, version) ⇒ + ( + Space ~ + token(org) ~ + token(Space ~ name) ~ + token(Space ~ version)) + .map(_ ⇒ id) + } + .reduceOption(_ | _) + .getOrElse { + ( + Space ~> + token(StringBasic, "organization") ~ Space ~ + token(StringBasic, "module") ~ Space ~ + token(StringBasic, "version")) + .map { + case ( + ( + ((org, _), mod), + _ + ), + version + ) ⇒ + ModuleId(org, mod, version) + } } - } } // This is to support 0.13.8's InlineConfigurationWithExcludes while not forcing 0.13.8 diff --git a/src/main/scala/net/virtualvoid/sbt/graph/GraphTransformations.scala b/src/main/scala/net/virtualvoid/sbt/graph/GraphTransformations.scala index c5c4e8a..a1f12d4 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/GraphTransformations.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/GraphTransformations.scala @@ -16,6 +16,8 @@ package net.virtualvoid.sbt.graph +import net.virtualvoid.sbt.graph.model.{ Module, ModuleGraph, ModuleId } + object GraphTransformations { def reverseGraphStartingAt(graph: ModuleGraph, root: ModuleId): ModuleGraph = { val deps = graph.reverseDependencyMap diff --git a/src/main/scala/net/virtualvoid/sbt/graph/backend/IvyReport.scala b/src/main/scala/net/virtualvoid/sbt/graph/backend/IvyReport.scala index 129dde6..3f7c824 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/backend/IvyReport.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/backend/IvyReport.scala @@ -17,7 +17,9 @@ package net.virtualvoid.sbt.graph package backend -import scala.xml.{ NodeSeq, Document, Node } +import net.virtualvoid.sbt.graph.model.{ Module, ModuleGraph, ModuleId } + +import scala.xml.{ Document, Node, NodeSeq } import scala.xml.parsing.ConstructingParser object IvyReport { diff --git a/src/main/scala/net/virtualvoid/sbt/graph/backend/SbtUpdateReport.scala b/src/main/scala/net/virtualvoid/sbt/graph/backend/SbtUpdateReport.scala index 3c8af89..fa496ba 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/backend/SbtUpdateReport.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/backend/SbtUpdateReport.scala @@ -17,9 +17,10 @@ package net.virtualvoid.sbt.graph package backend +import net.virtualvoid.sbt.graph.model.{ Module, ModuleGraph, ModuleId } + import scala.language.implicitConversions import scala.language.reflectiveCalls - import sbt._ object SbtUpdateReport { diff --git a/src/main/scala/net/virtualvoid/sbt/graph/model/FilterRule.scala b/src/main/scala/net/virtualvoid/sbt/graph/model/FilterRule.scala new file mode 100644 index 0000000..fb5ea70 --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/model/FilterRule.scala @@ -0,0 +1,131 @@ +package net.virtualvoid.sbt.graph.model + +import scala.language.implicitConversions +import scala.util.matching.Regex + +/** + * Interface for [[Include]] and [[Exclude]] rules for filtering the output of the + * [[net.virtualvoid.sbt.graph.DependencyGraphKeys.dependencyTree dependencyTree]] task + */ +sealed trait FilterRule { + def pattern: ModuleIdPattern + def apply(node: ModuleId): Boolean = pattern(node) +} + +object FilterRule { + implicit def apply(s: String): FilterRule = + if (s.startsWith("-")) + Exclude(s.substring(1)) + else if (s.startsWith("+")) + Include(s.substring(1)) + else + Include(s) +} + +/** + * If a [[ModuleId]] matches [[pattern]], include it in results + */ +case class Include(pattern: ModuleIdPattern) extends FilterRule +object Include { + def apply(pattern: String): Include = Include(ModuleIdPattern(pattern)) +} + +/** + * If a [[ModuleId]] matches [[pattern]], exclude it from results + */ +case class Exclude(pattern: ModuleIdPattern) extends FilterRule +object Exclude { + def apply(pattern: String): Exclude = Exclude(ModuleIdPattern(pattern)) +} + +/** + * Interface for patterns that can be matched against [[ModuleId]]s, either based on patterns that fully match one or + * more of the (group, artifact, version) segments ([[PerSegmentPatterns]]), or that partially match any of them + * ([[AnySegmentPattern]]) + */ +sealed trait ModuleIdPattern { + /** + * Does this [[ModuleId]] match this pattern? + */ + def apply(moduleId: ModuleId): Boolean +} + +object ModuleIdPattern { + + /** + * Build a [[Regex]] from a [[String]] specifying a module-id pattern: + * + * - escape "dot" (`.`) characters + * - replace "glob"/stars with `.*` + * + * This is more ergonomic than PCRE for Maven coordinates, since literal dots are used frequently, and other + * regex-special-chars (e.g. `[`/`]`, `{`/`}`, `+`) are not. + */ + def regex(s: String): Regex = + if (s.isEmpty) + any + else + s + .replace(".", "\\.") + .replace("*", ".*") + .r + + private val any = ".*".r + + /** + * Generate a [[ModuleIdPattern]] from a loose Maven-coordinate-style specification: + * + * - "foo": coordinates whose group, artifact, or version contains "foo" + * - "foo:": coordinates whose group is exactly "foo" + * - "foo*": coordinates whose group begins with "foo" + * - "*foo": coordinates whose group ends with "foo" + * - "*foo*": coordinates whose group contains "foo" + * - ":foo", ":foo:": coordinates whose name is "foo" + * - ":foo*": coordinates whose name begins with "foo" + * - "::*foo": coordinates whose version ends with "foo" + * - "foo*::*bar*": coordinates whose group begins with "foo" and version contains "bar" + */ + def apply(s: String): ModuleIdPattern = + s.split(":", -1) match { + // encodes a partial-match against any segment + case Array(_) ⇒ AnySegmentPattern(regex(s)) + + // encode full-matches against all segments with non-empty patterns + case Array(group, name) ⇒ PerSegmentPatterns(regex(group), regex(name), any) + case Array(group, name, version) ⇒ PerSegmentPatterns(regex(group), regex(name), regex(version)) + } +} + +/** + * Match [[ModuleId]]s whose group, name, and version each fully-match the corresponding provided [[Regex]] + */ +case class PerSegmentPatterns(group: Regex, + name: Regex, + version: Regex) + extends ModuleIdPattern { + def apply(moduleId: ModuleId): Boolean = + moduleId match { + case ModuleId(group(), name(), version()) ⇒ + true + case _ ⇒ + false + } +} + +/** + * Match [[ModuleId]]s where at least one of their {group, name, version} partially-match [[regex]] + */ +case class AnySegmentPattern(regex: Regex) + extends ModuleIdPattern { + def apply(m: ModuleId): Boolean = + Seq( + m.organisation, + m.name, + m.version + ) + .exists( + regex + .findFirstIn(_) + .isDefined + ) +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/model.scala b/src/main/scala/net/virtualvoid/sbt/graph/model/ModuleGraph.scala similarity index 53% rename from src/main/scala/net/virtualvoid/sbt/graph/model.scala rename to src/main/scala/net/virtualvoid/sbt/graph/model/ModuleGraph.scala index 96ce952..bf7fc99 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/model.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/model/ModuleGraph.scala @@ -14,12 +14,15 @@ * limitations under the License. */ -package net.virtualvoid.sbt.graph +package net.virtualvoid.sbt.graph.model import java.io.File +import net.virtualvoid.sbt.graph.model.ModuleGraph.DepMap +import net.virtualvoid.sbt.graph.{ Edge, ModuleGraphProtocolCompat } import sbinary.Format +import scala.collection.mutable import scala.collection.mutable.{ HashMap, MultiMap, Set } case class ModuleId(organisation: String, @@ -40,6 +43,8 @@ case class Module(id: ModuleId, object ModuleGraph { val empty = ModuleGraph(Seq.empty, Seq.empty) + + type DepMap = Map[ModuleId, Seq[Module]] } case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) { @@ -48,13 +53,13 @@ case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) { def module(id: ModuleId): Module = modules(id) - lazy val dependencyMap: Map[ModuleId, Seq[Module]] = + lazy val dependencyMap: DepMap = createMap(identity) - lazy val reverseDependencyMap: Map[ModuleId, Seq[Module]] = + lazy val reverseDependencyMap: DepMap = createMap { case (a, b) ⇒ (b, a) } - def createMap(bindingFor: ((ModuleId, ModuleId)) ⇒ (ModuleId, ModuleId)): Map[ModuleId, Seq[Module]] = { + def createMap(bindingFor: ((ModuleId, ModuleId)) ⇒ (ModuleId, ModuleId)): DepMap = { val m = new HashMap[ModuleId, Set[Module]] with MultiMap[ModuleId, Module] edges.foreach { entry ⇒ val (f, t) = bindingFor(entry) @@ -65,6 +70,64 @@ case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) { def roots: Seq[Module] = nodes.filter(n ⇒ !edges.exists(_._2 == n.id)).sortBy(_.id.idString) + + def filter(rules: FilterRule*): DepMap = { + val map = mutable.Map[ModuleId, Option[Seq[Module]]]() + + // Are any include- (resp. exclude-) rules present in `rules`? + var (hasIncludes, hasExcludes) = (false, false) + rules foreach { + case _: Include ⇒ hasIncludes = true + case _: Exclude ⇒ hasExcludes = true + } + + // Does a given ID satisfy at least one include (if any are present?) + def matchesInclude(id: ModuleId): Boolean = + !hasIncludes || + rules.exists { + case inc: Include if inc(id) ⇒ true + case _ ⇒ false + } + + // Is a given ID excluded? + def matchesExcludes(id: ModuleId): Boolean = + !hasExcludes || + !rules.exists { + case exc: Exclude if exc(id) ⇒ true + case _ ⇒ false + } + + // Keep an ID iff it satisfies at least one include (if any have been provided), and is not excluded + def keep(id: ModuleId): Boolean = matchesInclude(id) && matchesExcludes(id) + + def filtered(id: ModuleId): Option[Seq[Module]] = + map.getOrElseUpdate( + id, + dependencyMap(id) + .filter { + dep ⇒ + filtered(dep.id).isDefined + } match { + case Seq() if !keep(id) ⇒ + // none of this module's deps passed all rules (or depend on anything that does), nor did this module + // itself; drop it + None + case filteredDeps ⇒ + // Keep this module and its valid deps (which may be empty + Some(filteredDeps) + }) + + dependencyMap foreach { + case (id, _) ⇒ filtered(id) + } + + map + .flatMap { + case (id, Some(deps)) ⇒ Some(id → deps) + case _ ⇒ None + } + .toMap + } } object ModuleGraphProtocol extends ModuleGraphProtocolCompat { @@ -73,5 +136,5 @@ object ModuleGraphProtocol extends ModuleGraphProtocolCompat { 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] = asProduct6(Module)(Module.unapply(_).get) - implicit val ModuleGraphFormat: Format[ModuleGraph] = asProduct2(ModuleGraph.apply _)(ModuleGraph.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 index 41921bc..bc30a7b 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/package.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/package.scala @@ -16,6 +16,8 @@ package net.virtualvoid.sbt +import net.virtualvoid.sbt.graph.model.ModuleId + package object graph { type Edge = (ModuleId, ModuleId) def Edge(from: ModuleId, to: ModuleId): Edge = from -> to diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala index c2670e6..9eb508a 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala @@ -17,23 +17,36 @@ package net.virtualvoid.sbt.graph package rendering -import util.AsciiTreeLayout +import net.virtualvoid.sbt.graph.model.{ FilterRule, Module, ModuleGraph } +import util.AsciiTreeLayout.toAscii import util.ConsoleUtils._ object AsciiTree { - def asciiTree(graph: ModuleGraph): String = { - val deps = graph.dependencyMap + def apply(graph: ModuleGraph, filterRules: FilterRule*): String = { + val deps = graph.filter(filterRules: _*) // there should only be one root node (the project itself) - val roots = graph.roots - roots.map { root ⇒ - AsciiTreeLayout.toAscii[Module](root, node ⇒ deps.getOrElse(node.id, Seq.empty[Module]), displayModule) - }.mkString("\n") + graph + .roots + .map { + root ⇒ + 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) + 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 index 368cf9e..4ab366f 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala @@ -17,6 +17,8 @@ package net.virtualvoid.sbt.graph package rendering +import net.virtualvoid.sbt.graph.model.ModuleGraph + object DOT { val EvictedStyle = "stroke-dasharray: 5,5" diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/FlatList.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/FlatList.scala index eb26cff..3cef700 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/rendering/FlatList.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/FlatList.scala @@ -17,6 +17,8 @@ package net.virtualvoid.sbt.graph package rendering +import net.virtualvoid.sbt.graph.model.{ Module, ModuleGraph } + object FlatList { def render(graph: ModuleGraph, display: Module ⇒ String): String = graph.modules.values.toSeq diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/GraphML.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/GraphML.scala index 436a90c..771da06 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/rendering/GraphML.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/GraphML.scala @@ -16,7 +16,7 @@ package net.virtualvoid.sbt.graph.rendering -import net.virtualvoid.sbt.graph.ModuleGraph +import net.virtualvoid.sbt.graph.model.ModuleGraph import scala.xml.XML diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/Statistics.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/Statistics.scala index 5a82e92..2d8e270 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/rendering/Statistics.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/Statistics.scala @@ -17,6 +17,8 @@ package net.virtualvoid.sbt.graph package rendering +import net.virtualvoid.sbt.graph.model.{ ModuleGraph, ModuleId } + object Statistics { def renderModuleStatsList(graph: ModuleGraph): String = { case class ModuleStats( diff --git a/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/build.sbt b/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/build.sbt index 1d004c5..5c27f63 100644 --- a/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/build.sbt +++ b/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/build.sbt @@ -5,14 +5,13 @@ libraryDependencies ++= Seq( "ch.qos.logback" % "logback-classic" % "1.0.7" ) -TaskKey[Unit]("check") := { +InputKey[Unit]("check") := { val report = (ivyReport in Test).value - val graph = (asciiTree in Test).value + val graph = (asciiTree in Test).evaluated def sanitize(str: String): String = str.split('\n').drop(1).map(_.trim).mkString("\n") val expectedGraph = """default:default-e95e05_2.9.2:0.1-SNAPSHOT [S] | +-ch.qos.logback:logback-classic:1.0.7 - | | +-ch.qos.logback:logback-core:1.0.7 | | +-org.slf4j:slf4j-api:1.6.6 (evicted by: 1.7.2) | | +-org.slf4j:slf4j-api:1.7.2 | | diff --git a/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/test b/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/test index a5912a3..44ebf33 100644 --- a/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/test +++ b/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/test @@ -1 +1 @@ -> check \ No newline at end of file +> check -qos diff --git a/src/sbt-test/sbt-dependency-graph/intervalRangedVersions/build.sbt b/src/sbt-test/sbt-dependency-graph/intervalRangedVersions/build.sbt index b417c9e..8fcf325 100644 --- a/src/sbt-test/sbt-dependency-graph/intervalRangedVersions/build.sbt +++ b/src/sbt-test/sbt-dependency-graph/intervalRangedVersions/build.sbt @@ -6,9 +6,9 @@ libraryDependencies ++= Seq( "com.codahale" % "jerkson_2.9.1" % "0.5.0" ) -TaskKey[Unit]("check") := { +InputKey[Unit]("check") := { val report = (ivyReport in Test).value - val graph = (asciiTree in Test).value + val graph = (asciiTree in Test).evaluated def sanitize(str: String): String = str.split('\n').drop(1).map(_.trim).mkString("\n") val expectedGraph = diff --git a/src/sbt-test/sbt-dependency-graph/showMissingUpdates/build.sbt b/src/sbt-test/sbt-dependency-graph/showMissingUpdates/build.sbt index da6ad1a..0729ffe 100644 --- a/src/sbt-test/sbt-dependency-graph/showMissingUpdates/build.sbt +++ b/src/sbt-test/sbt-dependency-graph/showMissingUpdates/build.sbt @@ -3,9 +3,9 @@ scalaVersion := "2.9.2" libraryDependencies += "at.blub" % "blib" % "1.2.3" % "test" -TaskKey[Unit]("check") := { +InputKey[Unit]("check") := { val report = (ivyReport in Test).value - val graph = (asciiTree in Test).value + val graph = (asciiTree in Test).evaluated def sanitize(str: String): String = str.split('\n').drop(1).mkString("\n") val expectedGraph = diff --git a/src/test/scala/net/virtualvoid/sbt/graph/model/ModuleGraphTest.scala b/src/test/scala/net/virtualvoid/sbt/graph/model/ModuleGraphTest.scala new file mode 100644 index 0000000..8323dbb --- /dev/null +++ b/src/test/scala/net/virtualvoid/sbt/graph/model/ModuleGraphTest.scala @@ -0,0 +1,177 @@ +package net.virtualvoid.sbt.graph.model + +import net.virtualvoid.sbt.graph.Edge +import net.virtualvoid.sbt.graph.rendering.AsciiTree +import org.specs2.mutable.Specification + +class ModuleGraphTest extends Specification { + + // Sample nodes with mostly-distinct group-, name-, and version-strings + val abc: Module = ("ABC", "abc", "1.1.1") + val de: Module = ("DE", "de", "2.2.2") + + val fff: Module = ("FGH", "fff", "3.1.1") + val ggg: Module = ("FGH", "ggg", "3.2.2") + val hhh: Module = ("FGH", "hhh", "3.3.3") + + val ijk: Module = ("IJK", "ijk", "4.4.4") + val lmn: Module = ("LMN", "lmn", "5.5.5") + val opq: Module = ("OPQ", "opq", "6.6.6") + val rst: Module = ("RST", "rst", "7.7.7") + val uvw: Module = ("UVW", "uvw", "8.8.8") + val xyz: Module = ("XYZ", "xyz", "9.9.9") + + val nodes = Seq(abc, de, fff, ggg, hhh, ijk, lmn, opq, rst, uvw, xyz) + + val edges = + Seq[Edge]( + abc → de, + abc → fff, + abc → ggg, + + de → ijk, + de → lmn, + + fff → opq, + + ijk → rst, + ijk → uvw, + + opq → hhh, + + uvw → xyz + ) + + // Will test many inclusion/exclusion rules on this graph + val graph = ModuleGraph(nodes, edges) + + "ModuleGraph" should { + + "show full tree" in { + check()( + """ABC:abc:1.1.1 + | +-DE:de:2.2.2 + | | +-IJK:ijk:4.4.4 + | | | +-RST:rst:7.7.7 + | | | +-UVW:uvw:8.8.8 + | | | +-XYZ:xyz:9.9.9 + | | | + | | +-LMN:lmn:5.5.5 + | | + | +-FGH:fff:3.1.1 + | | +-OPQ:opq:6.6.6 + | | +-FGH:hhh:3.3.3 + | | + | +-FGH:ggg:3.2.2 + | """ + ) + } + + "handle single include" in { + check( + "FGH" // "ggg" and "hhh" (including "fff") ancestries + )( + """ABC:abc:1.1.1 + | +-FGH:fff:3.1.1 + | | +-OPQ:opq:6.6.6 + | | +-FGH:hhh:3.3.3 + | | + | +-FGH:ggg:3.2.2 + | """ + ) + } + + "handle multiple/partial includes" in { + check( + "FG", // "ggg" and "hhh" (including "fff") ancestries + "+5.5" // "lmn" (and parent "de") + )( + """ABC:abc:1.1.1 + | +-DE:de:2.2.2 + | | +-LMN:lmn:5.5.5 + | | + | +-FGH:fff:3.1.1 + | | +-OPQ:opq:6.6.6 + | | +-FGH:hhh:3.3.3 + | | + | +-FGH:ggg:3.2.2 + | """ + ) + } + + "exclude all" in { + check( + "-*" + )( + "ABC:abc:1.1.1" // root (populated with current project) is always preserved + ) + } + + "exclude nested" in { + check( + "-FGH" // only excludes "ggg"; other "FGH"-org nodes have non-excluded descendents + )( + """ABC:abc:1.1.1 + | +-DE:de:2.2.2 + | | +-IJK:ijk:4.4.4 + | | | +-RST:rst:7.7.7 + | | | +-UVW:uvw:8.8.8 + | | | +-XYZ:xyz:9.9.9 + | | | + | | +-LMN:lmn:5.5.5 + | | + | +-FGH:fff:3.1.1 + | +-OPQ:opq:6.6.6 + | """ + ) + } + + "exclude multiple by segments" in { + check( + "-FGH:", // excludes "ggg", which has no non-excluded descendents + "-::4*", // fails to exclude "ijk", which has non-excluded descendents + "-::9*" // excludes "xyz" + )( + """ABC:abc:1.1.1 + | +-DE:de:2.2.2 + | | +-IJK:ijk:4.4.4 + | | | +-RST:rst:7.7.7 + | | | +-UVW:uvw:8.8.8 + | | | + | | +-LMN:lmn:5.5.5 + | | + | +-FGH:fff:3.1.1 + | +-OPQ:opq:6.6.6 + | """ + ) + } + + "includes and excludes" in { + check( + "+:*j*", // include "ijk" (and "de" parent) + ":*o*", // include "opq" (and "fff" parent) + "-de", // attempt to exclude "de" (parent of included "ijk"): no-op + "-O*:" // exclude "opq" (also excluding parent "fff") + )( + """ABC:abc:1.1.1 + | +-DE:de:2.2.2 + | +-IJK:ijk:4.4.4 + | """ + ) + } + } + + /** + * Convenience conversions for specifying test-cases + */ + implicit def moduleToId(m: Module): ModuleId = m.id + implicit def modulesToIds(t: (Module, Module)): (ModuleId, ModuleId) = (t._1, t._2) + implicit def tripleToModule(t: (String, String, String)): Module = Module(ModuleId(t._1, t._2, t._3)) + + /** + * Test-case helper; takes filter rules and verifies expected ASCII-output for [[graph]] + */ + def check(filters: String*)(expected: String) = { + AsciiTree(graph, filters.map(r ⇒ r: FilterRule): _*) === expected.stripMargin + } +}