From 01a501f4f73fa4c1d827e90de29e6345f735ee02 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Mon, 17 Oct 2022 19:25:02 +0200 Subject: [PATCH] Dependency graph with colors (based on the organization) (#7052) --- main/src/main/resources/graph.html | 85 +------- .../sbt/internal/graph/rendering/DOT.scala | 31 ++- .../sbt/plugins/DependencyTreeKeys.scala | 3 + .../sbt/plugins/DependencyTreeSettings.scala | 9 +- .../testDotFileGeneration/build.sbt | 50 ++--- .../testHtmlFileGeneration/build.sbt | 197 ++++++------------ 6 files changed, 127 insertions(+), 248 deletions(-) diff --git a/main/src/main/resources/graph.html b/main/src/main/resources/graph.html index b8c4a45b2..039b77d6a 100644 --- a/main/src/main/resources/graph.html +++ b/main/src/main/resources/graph.html @@ -30,86 +30,15 @@ THE SOFTWARE. Dependency Graph - - - - + + + - - - - - - - - - + +
+ - diff --git a/main/src/main/scala/sbt/internal/graph/rendering/DOT.scala b/main/src/main/scala/sbt/internal/graph/rendering/DOT.scala index d53fb1023..5c7dae6c9 100644 --- a/main/src/main/scala/sbt/internal/graph/rendering/DOT.scala +++ b/main/src/main/scala/sbt/internal/graph/rendering/DOT.scala @@ -11,23 +11,38 @@ package graph package rendering object DOT { - val EvictedStyle = "stroke-dasharray: 5,5" + val EvictedStyle = "dashed" def dotGraph( graph: ModuleGraph, dotHead: String, nodeFormation: (String, String, String) => String, - labelRendering: HTMLLabelRendering + labelRendering: HTMLLabelRendering, + colors: Boolean ): String = { val nodes = { for (n <- graph.nodes) yield { - val style = if (n.isEvicted) EvictedStyle else "" val label = nodeFormation(n.id.organization, n.id.name, n.id.version) - """ "%s"[%s style="%s"]""".format( - n.id.idString, - labelRendering.renderLabel(label), - style - ) + val style = if (n.isEvicted) EvictedStyle else "" + val penwidth = if (n.isEvicted) "3" else "5" + val color = if (colors) { + val orgHash = n.id.organization.hashCode + val r = (orgHash >> 16) & 0xFF + val g = (orgHash >> 8) & 0xFF + val b = (orgHash >> 0) & 0xFF + val r1 = (r * 0.90).toInt + val g1 = (g * 0.90).toInt + val b1 = (b * 0.90).toInt + (r1 << 16) | (g1 << 8) | (b1 << 0) + } else 0 + s""" "%s"[shape=box %s style="%s" penwidth="%s" color="%s"]""" + .format( + n.id.idString, + labelRendering.renderLabel(label), + style, + penwidth, + f"#$color%06X", + ) } }.sorted.mkString("\n") diff --git a/main/src/main/scala/sbt/plugins/DependencyTreeKeys.scala b/main/src/main/scala/sbt/plugins/DependencyTreeKeys.scala index 24d1d08b2..5268941ef 100644 --- a/main/src/main/scala/sbt/plugins/DependencyTreeKeys.scala +++ b/main/src/main/scala/sbt/plugins/DependencyTreeKeys.scala @@ -41,6 +41,9 @@ abstract class DependencyTreeKeys { taskKey[File]("Creates a graphml file containing the dependency-graph for a project") val dependencyDotFile = settingKey[File]("The location the dot file should be generated at") + val dependencyDotNodeColors = settingKey[Boolean]( + "The boxes of nodes are painted with colors. Otherwise they're black." + ) val dependencyDotNodeLabel = settingKey[(String, String, String) => String]( "Returns a formated string of a dependency. Takes organization, name and version as parameters" ) diff --git a/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala b/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala index 2ffe91128..88ed9d374 100644 --- a/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala +++ b/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala @@ -109,15 +109,17 @@ object DependencyTreeSettings { dependencyTreeModuleGraph0.value, dependencyDotHeader.value, dependencyDotNodeLabel.value, - rendering.DOT.AngleBrackets + rendering.DOT.AngleBrackets, + dependencyDotNodeColors.value ), dependencyDot := writeToFile(dependencyDot / asString, dependencyDotFile).value, dependencyDotHeader := """|digraph "dependency-graph" { - | graph[rankdir="LR"] + | graph[rankdir="LR"; splines=polyline] | edge [ | arrowtail="none" | ]""".stripMargin, + dependencyDotNodeColors := true, dependencyDotNodeLabel := { (organization: String, name: String, version: String) => """%s
%s
%s""".format(organization, name, version) }, @@ -192,7 +194,8 @@ object DependencyTreeSettings { graph, dependencyDotHeader.value, dependencyDotNodeLabel.value, - rendering.DOT.LabelTypeHtml + rendering.DOT.AngleBrackets, + dependencyDotNodeColors.value ) val link = DagreHTML.createLink(dotGraph, target.value) streams.value.log.info(s"HTML graph written to $link") diff --git a/sbt-app/src/sbt-test/dependency-graph/testDotFileGeneration/build.sbt b/sbt-app/src/sbt-test/dependency-graph/testDotFileGeneration/build.sbt index fc61f4b22..7e79f27d0 100644 --- a/sbt-app/src/sbt-test/dependency-graph/testDotFileGeneration/build.sbt +++ b/sbt-app/src/sbt-test/dependency-graph/testDotFileGeneration/build.sbt @@ -11,43 +11,43 @@ lazy val justATransitiveDependencyProject = project lazy val justADependencyProject = project lazy val test_project = project - .dependsOn(justADependencyProject, justATransitiveDependencyProject) - .settings( - TaskKey[Unit]("check") := { - val dotFile = (dependencyDot in Compile).value - val expectedGraph = - """digraph "dependency-graph" { - | graph[rankdir="LR"] + .dependsOn(justADependencyProject, justATransitiveDependencyProject) + .settings( + TaskKey[Unit]("check") := { + val dotFile = (dependencyDot in Compile).value + val expectedGraph = + """digraph "dependency-graph" { + | graph[rankdir="LR"; splines=polyline] | edge [ | arrowtail="none" | ] - | "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT"[label=justadependencyproject_2.9.2
0.1-SNAPSHOT> style=""] - | "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT"[label=justatransitivedependencyproject_2.9.2
0.1-SNAPSHOT> style=""] - | "justatransivitedependencyendpointproject:justatransivitedependencyendpointproject_2.9.2:0.1-SNAPSHOT"[label=justatransivitedependencyendpointproject_2.9.2
0.1-SNAPSHOT> style=""] - | "test_project:test_project_2.9.2:0.1-SNAPSHOT"[label=test_project_2.9.2
0.1-SNAPSHOT> style=""] + | "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT"[shape=box label=justadependencyproject_2.9.2
0.1-SNAPSHOT> style="" penwidth="5" color="#B6E316"] + | "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT"[shape=box label=justatransitivedependencyproject_2.9.2
0.1-SNAPSHOT> style="" penwidth="5" color="#0E92BE"] + | "justatransivitedependencyendpointproject:justatransivitedependencyendpointproject_2.9.2:0.1-SNAPSHOT"[shape=box label=justatransivitedependencyendpointproject_2.9.2
0.1-SNAPSHOT> style="" penwidth="5" color="#9EAD1B"] + | "test_project:test_project_2.9.2:0.1-SNAPSHOT"[shape=box label=test_project_2.9.2
0.1-SNAPSHOT> style="" penwidth="5" color="#C37661"] | "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT" -> "justatransivitedependencyendpointproject:justatransivitedependencyendpointproject_2.9.2:0.1-SNAPSHOT" | "test_project:test_project_2.9.2:0.1-SNAPSHOT" -> "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT" | "test_project:test_project_2.9.2:0.1-SNAPSHOT" -> "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT" |} """.stripMargin - val graph : String = scala.io.Source.fromFile(dotFile.getAbsolutePath).mkString - val errors = compareByLine(graph, expectedGraph) - require(errors.isEmpty , errors.mkString("\n")) - () - } - ) + val graph: String = scala.io.Source.fromFile(dotFile.getAbsolutePath).mkString + val errors = compareByLine(graph, expectedGraph) + require(errors.isEmpty, errors.mkString("\n")) + () + } + ) -def compareByLine(got : String, expected : String) : Seq[String] = { +def compareByLine(got: String, expected: String): Seq[String] = { val errors = ListBuffer[String]() - got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach { case((got_line : String, expected_line : String), i : Int) => - if(got_line != expected_line) { - errors.append( - """not matching lines at line %s + got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach { + case ((got_line: String, expected_line: String), i: Int) => + if (got_line != expected_line) { + errors.append("""not matching lines at line %s |expected: %s |got: %s - |""".stripMargin.format(i,expected_line, got_line)) - } + |""".stripMargin.format(i, expected_line, got_line)) + } } errors -} \ No newline at end of file +} diff --git a/sbt-app/src/sbt-test/dependency-graph/testHtmlFileGeneration/build.sbt b/sbt-app/src/sbt-test/dependency-graph/testHtmlFileGeneration/build.sbt index bd4cf6257..7a2eb062d 100644 --- a/sbt-app/src/sbt-test/dependency-graph/testHtmlFileGeneration/build.sbt +++ b/sbt-app/src/sbt-test/dependency-graph/testHtmlFileGeneration/build.sbt @@ -11,146 +11,75 @@ lazy val justATransitiveDependencyProject = project lazy val justADependencyProject = project lazy val test_project = project - .dependsOn(justADependencyProject, justATransitiveDependencyProject) - .settings( - TaskKey[Unit]("check") := { - val htmlFile = (dependencyBrowseGraphHTML in Compile).value - val expectedHtml = - """ - | - | - | - | - |Dependency Graph - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | + .dependsOn(justADependencyProject, justATransitiveDependencyProject) + .settings( + TaskKey[Unit]("check") := { + val htmlFile = (dependencyBrowseGraphHTML in Compile).value + val expectedHtml = + """ + | + | + | + | + |Dependency Graph + | + | + | + | + | + | + | + |
+ | + | + | + | """.stripMargin - val html : String = scala.io.Source.fromFile(htmlFile).mkString - val errors = compareByLine(html, expectedHtml) - require(errors.isEmpty , errors.mkString("\n")) - () - } - ) + val html: String = scala.io.Source.fromFile(htmlFile).mkString + val errors = compareByLine(html, expectedHtml) + require(errors.isEmpty, errors.mkString("\n")) + () + } + ) -def compareByLine(got : String, expected : String) : Seq[String] = { +def compareByLine(got: String, expected: String): Seq[String] = { val errors = ListBuffer[String]() - got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach { case((got_line : String, expected_line : String), i : Int) => - if(got_line != expected_line) { - errors.append( - """not matching lines at line %s + got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach { + case ((got_line: String, expected_line: String), i: Int) => + if (got_line != expected_line) { + errors.append("""not matching lines at line %s |expected: %s |got: %s - |""".stripMargin.format(i,expected_line, got_line)) - } + |""".stripMargin.format(i, expected_line, got_line)) + } } errors }