Skip to content
This repository was archived by the owner on Apr 8, 2021. It is now read-only.

Commit 9cc4bd7

Browse files
committed
filter dependency-tree with inclusion/exclusion rules
1 parent 94eae27 commit 9cc4bd7

File tree

10 files changed

+364
-17
lines changed

10 files changed

+364
-17
lines changed

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ the notes of version [0.8.2](https://github.com/jrudolph/sbt-dependency-graph/tr
2121

2222
## Main Tasks
2323

24-
* `dependencyTree`: Shows an ASCII tree representation of the project's dependencies
24+
* `dependencyTree`: Shows an ASCII tree representation of the project's dependencies (see [below](#dependencyTree-filtering) for examples filtering the output)
2525
* `dependencyBrowseGraph`: Opens a browser window with a visualization of the dependency graph (courtesy of graphlib-dot + dagre-d3).
2626
* `dependencyList`: Shows a flat list of all transitive dependencies on the sbt console (sorted by organization and name)
2727
* `whatDependsOn <organization> <module> <revision>`: 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
4040
for example, prints the dependencies in the `test` configuration. If you don't specify any configuration, `compile` is
4141
assumed as usual.
4242

43+
### `dependencyTree` filtering
44+
The `dependencyTree` task supports filtering with inclusion/exclusion rules:
45+
46+
- exclusion rules are prefixed by `-`
47+
- inclusion rules are the default (or can be prefixed by `+`)
48+
49+
Dependencies are "preserved" iff:
50+
- they match at least one inclusion rule (or no inclusion rules are provided), and
51+
- they match no exclusion rules (including when none are provided)
52+
53+
They are then displayed if they are preserved *or at least one of their transitive dependencies is preserved*.
54+
55+
This mimics the behavior of [Maven dependency:tree](https://maven.apache.org/plugins/maven-dependency-plugin/tree-mojo.html)'s `includes` and `excludes` parameters.
56+
57+
#### Examples
58+
59+
Inclusions/Exclusions can be partial-matched against any part of a dependency's Maven coordinate:
60+
61+
```
62+
dependencyTree -foo // exclude deps that contain "foo" in the group, name, or version
63+
dependencyTree foo // include deps that contain "foo" in the group, name, or version
64+
```
65+
66+
Or they can be fully-matched against specific parts of the coordinate:
67+
68+
```
69+
dependencyTree -:foo* // exclude deps whose name starts with "foo"
70+
dependencyTree -*foo*::*bar // exclude deps whose group contains "foo" and version ends with "bar"
71+
```
72+
73+
Inclusions and exclusions can be combined and repeated:
74+
```
75+
dependencyTree foo bar -baz // include only deps that contain "foo" or "bar" and not "baz"
76+
```
77+
78+
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).
79+
4380
## Configuration settings
4481

4582
* `filterScalaLibrary`: Defines if the scala library should be excluded from the output of the dependency-* functions.

src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ trait DependencyGraphKeys {
5050
"Returns a string containing the ascii representation of the dependency graph for a project")
5151
val dependencyGraph = InputKey[Unit]("dependency-graph",
5252
"Prints the ascii graph to the console")
53-
val asciiTree = TaskKey[String]("dependency-tree-string",
53+
val asciiTree = InputKey[String]("dependency-tree-string",
5454
"Returns a string containing an ascii tree representation of the dependency graph for a project")
55-
val dependencyTree = TaskKey[Unit]("dependency-tree",
55+
val dependencyTree = InputKey[Unit]("dependency-tree",
5656
"Prints an ascii tree of all the dependencies to the console")
5757
val dependencyList = TaskKey[Unit]("dependency-list",
5858
"Prints a list of all dependencies to the console")

src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package net.virtualvoid.sbt.graph
1818

1919
import net.virtualvoid.sbt.graph.GraphTransformations.reverseGraphStartingAt
2020
import net.virtualvoid.sbt.graph.backend.{ IvyReport, SbtUpdateReport }
21-
import net.virtualvoid.sbt.graph.model.{ ModuleGraph, ModuleId }
21+
import net.virtualvoid.sbt.graph.model.{ FilterRule, ModuleGraph, ModuleId }
2222
import net.virtualvoid.sbt.graph.rendering.{ AsciiGraph, AsciiTree, DagreHTML }
2323
import net.virtualvoid.sbt.graph.util.IOUtil
2424
import sbt.Keys._
@@ -76,8 +76,11 @@ object DependencyGraphSettings {
7676
else moduleGraph
7777
},
7878
moduleGraphStore := (moduleGraph storeAs moduleGraphStore triggeredBy moduleGraph).value,
79-
asciiTree := AsciiTree(moduleGraph.value),
80-
dependencyTree := print(asciiTree).value,
79+
asciiTree := AsciiTree(
80+
moduleGraph.value,
81+
filterRulesParser.parsed: _*
82+
),
83+
dependencyTree := streams.value.log.info(asciiTree.evaluated),
8184
dependencyGraphMLFile := { target.value / "dependencies-%s.graphml".format(config.toString) },
8285
dependencyGraphML := dependencyGraphMLTask.value,
8386
dependencyDotFile := { target.value / "dependencies-%s.dot".format(config.toString) },
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package net.virtualvoid.sbt.graph.model
2+
3+
import scala.language.implicitConversions
4+
import scala.util.matching.Regex
5+
6+
/**
7+
* Interface for [[Include]] and [[Exclude]] rules for filtering the output of the
8+
* [[net.virtualvoid.sbt.graph.DependencyGraphKeys.dependencyTree dependencyTree]] task
9+
*/
10+
sealed trait FilterRule {
11+
def pattern: ModuleIdPattern
12+
def apply(node: ModuleId): Boolean = pattern(node)
13+
}
14+
15+
object FilterRule {
16+
implicit def apply(s: String): FilterRule =
17+
if (s.startsWith("-"))
18+
Exclude(s.substring(1))
19+
else if (s.startsWith("+"))
20+
Include(s.substring(1))
21+
else
22+
Include(s)
23+
}
24+
25+
/**
26+
* If a [[ModuleId]] matches [[pattern]], include it in results
27+
*/
28+
case class Include(pattern: ModuleIdPattern) extends FilterRule
29+
object Include {
30+
def apply(pattern: String): Include = Include(ModuleIdPattern(pattern))
31+
}
32+
33+
/**
34+
* If a [[ModuleId]] matches [[pattern]], exclude it from results
35+
*/
36+
case class Exclude(pattern: ModuleIdPattern) extends FilterRule
37+
object Exclude {
38+
def apply(pattern: String): Exclude = Exclude(ModuleIdPattern(pattern))
39+
}
40+
41+
/**
42+
* Interface for patterns that can be matched against [[ModuleId]]s, either based on patterns that fully match one or
43+
* more of the (group, artifact, version) segments ([[PerSegmentPatterns]]), or that partially match any of them
44+
* ([[AnySegmentPattern]])
45+
*/
46+
sealed trait ModuleIdPattern {
47+
/**
48+
* Does this [[ModuleId]] match this pattern?
49+
*/
50+
def apply(moduleId: ModuleId): Boolean
51+
}
52+
53+
object ModuleIdPattern {
54+
55+
/**
56+
* Build a [[Regex]] from a [[String]] specifying a module-id pattern:
57+
*
58+
* - escape "dot" (`.`) characters
59+
* - replace "glob"/stars with `.*`
60+
*
61+
* This is more ergonomic than PCRE for Maven coordinates, since literal dots are used frequently, and other
62+
* regex-special-chars (e.g. `[`/`]`, `{`/`}`, `+`) are not.
63+
*/
64+
def regex(s: String): Regex =
65+
if (s.isEmpty)
66+
any
67+
else
68+
s
69+
.replace(".", "\\.")
70+
.replace("*", ".*")
71+
.r
72+
73+
private val any = ".*".r
74+
75+
/**
76+
* Generate a [[ModuleIdPattern]] from a loose Maven-coordinate-style specification:
77+
*
78+
* - "foo": coordinates whose group, artifact, or version contains "foo"
79+
* - "foo:": coordinates whose group is exactly "foo"
80+
* - "foo*": coordinates whose group begins with "foo"
81+
* - "*foo": coordinates whose group ends with "foo"
82+
* - "*foo*": coordinates whose group contains "foo"
83+
* - ":foo", ":foo:": coordinates whose name is "foo"
84+
* - ":foo*": coordinates whose name begins with "foo"
85+
* - "::*foo": coordinates whose version ends with "foo"
86+
* - "foo*::*bar*": coordinates whose group begins with "foo" and version contains "bar"
87+
*/
88+
def apply(s: String): ModuleIdPattern =
89+
s.split(":", -1) match {
90+
// encodes a partial-match against any segment
91+
case Array(_) AnySegmentPattern(regex(s))
92+
93+
// encode full-matches against all segments with non-empty patterns
94+
case Array(group, name) PerSegmentPatterns(regex(group), regex(name), any)
95+
case Array(group, name, version) PerSegmentPatterns(regex(group), regex(name), regex(version))
96+
}
97+
}
98+
99+
/**
100+
* Match [[ModuleId]]s whose group, name, and version each fully-match the corresponding provided [[Regex]]
101+
*/
102+
case class PerSegmentPatterns(group: Regex,
103+
name: Regex,
104+
version: Regex)
105+
extends ModuleIdPattern {
106+
def apply(moduleId: ModuleId): Boolean =
107+
moduleId match {
108+
case ModuleId(group(), name(), version())
109+
true
110+
case _
111+
false
112+
}
113+
}
114+
115+
/**
116+
* Match [[ModuleId]]s where at least one of their {group, name, version} partially-match [[regex]]
117+
*/
118+
case class AnySegmentPattern(regex: Regex)
119+
extends ModuleIdPattern {
120+
def apply(m: ModuleId): Boolean =
121+
Seq(
122+
m.organisation,
123+
m.name,
124+
m.version
125+
)
126+
.exists(
127+
regex
128+
.findFirstIn(_)
129+
.isDefined
130+
)
131+
}

src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
package net.virtualvoid.sbt.graph
1818
package rendering
1919

20-
import net.virtualvoid.sbt.graph.model.{ Module, ModuleGraph }
20+
import net.virtualvoid.sbt.graph.model.{ FilterRule, Module, ModuleGraph }
2121
import util.AsciiTreeLayout.toAscii
2222
import util.ConsoleUtils._
2323

2424
object AsciiTree {
25-
def asciiTree(graph: ModuleGraph): String = {
26-
val deps = graph.dependencyMap
25+
def apply(graph: ModuleGraph, filterRules: FilterRule*): String = {
26+
val deps = graph.filter(filterRules: _*)
2727

2828
// there should only be one root node (the project itself)
2929
graph

src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/build.sbt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ libraryDependencies ++= Seq(
55
"ch.qos.logback" % "logback-classic" % "1.0.7"
66
)
77

8-
TaskKey[Unit]("check") := {
8+
InputKey[Unit]("check") := {
99
val report = (ivyReport in Test).value
10-
val graph = (asciiTree in Test).value
10+
val graph = (asciiTree in Test).evaluated
1111
def sanitize(str: String): String = str.split('\n').drop(1).map(_.trim).mkString("\n")
1212
val expectedGraph =
1313
"""default:default-e95e05_2.9.2:0.1-SNAPSHOT [S]
1414
| +-ch.qos.logback:logback-classic:1.0.7
15-
| | +-ch.qos.logback:logback-core:1.0.7
1615
| | +-org.slf4j:slf4j-api:1.6.6 (evicted by: 1.7.2)
1716
| | +-org.slf4j:slf4j-api:1.7.2
1817
| |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
> check
1+
> check -qos

src/sbt-test/sbt-dependency-graph/intervalRangedVersions/build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ libraryDependencies ++= Seq(
66
"com.codahale" % "jerkson_2.9.1" % "0.5.0"
77
)
88

9-
TaskKey[Unit]("check") := {
9+
InputKey[Unit]("check") := {
1010
val report = (ivyReport in Test).value
11-
val graph = (asciiTree in Test).value
11+
val graph = (asciiTree in Test).evaluated
1212

1313
def sanitize(str: String): String = str.split('\n').drop(1).map(_.trim).mkString("\n")
1414
val expectedGraph =

src/sbt-test/sbt-dependency-graph/showMissingUpdates/build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ scalaVersion := "2.9.2"
33
libraryDependencies +=
44
"at.blub" % "blib" % "1.2.3" % "test"
55

6-
TaskKey[Unit]("check") := {
6+
InputKey[Unit]("check") := {
77
val report = (ivyReport in Test).value
8-
val graph = (asciiTree in Test).value
8+
val graph = (asciiTree in Test).evaluated
99

1010
def sanitize(str: String): String = str.split('\n').drop(1).mkString("\n")
1111
val expectedGraph =

0 commit comments

Comments
 (0)