Repository: patrickhoefler/dockerfilegraph
Pull request #265 (2023-01-28 00:58:28)

Add the `concentrate` flag
https://graphviz.org/docs/attrs/concentrate/


Patch:
diff --git a/.gitignore b/.gitignore
index 5b4943c..e2f19ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,5 @@ dockerfilegraph
 
 # dockerfilegraph
 Dockerfile.*
-!Dockerfile.alpine
+!/Dockerfile.alpine
+!example/Dockerfile.large
diff --git a/README.md b/README.md
index a1fea8e..2f8e7f5 100644
--- a/README.md
+++ b/README.md
@@ -109,13 +109,14 @@ Usage:
   dockerfilegraph [flags]
 
 Flags:
+  -c, --concentrate       concentrate the edges (default false)
   -d, --dpi int           dots per inch of the PNG export (default 96)
   -e, --edgestyle         style of the graph edges, one of: default, solid (default default)
   -f, --filename string   name of the Dockerfile (default "Dockerfile")
   -h, --help              help for dockerfilegraph
       --layers            display all layers (default false)
       --legend            add a legend (default false)
-  -o, --output            output file format, one of: canon, dot, pdf, png, svg (default pdf)
+  -o, --output            output file format, one of: canon, dot, pdf, png, raw, svg (default pdf)
       --version           display the version of dockerfilegraph
 ```
 
diff --git a/example/Dockerfile.large b/example/Dockerfile.large
new file mode 100644
index 0000000..10f89c1
--- /dev/null
+++ b/example/Dockerfile.large
@@ -0,0 +1,16 @@
+FROM scratch AS base
+COPY --from=external_0 file_0_0 /
+COPY --from=external_1 file_1_0 /
+COPY --from=external_2 file_2_0 /
+COPY --from=external_3 file_3_0 /
+COPY --from=external_4 file_4_0 /
+COPY --from=external_5 file_5_0 /
+COPY --from=external_6 file_6_0 /
+
+FROM base AS build
+RUN --mount=type=cache,from=buildcache,source=/build,target=/build make build
+
+FROM build AS final
+COPY --from=supplemental file_supplemental_0 /
+COPY --from=supplemental file_supplemental_1 /
+COPY --from=supplemental file_supplemental_2 /
diff --git a/internal/cmd/root.go b/internal/cmd/root.go
index cb9e5ea..f96eb85 100644
--- a/internal/cmd/root.go
+++ b/internal/cmd/root.go
@@ -14,13 +14,14 @@ import (
 )
 
 var (
-	dpiFlag       int
-	edgeStyleFlag enum
-	filenameFlag  string
-	layersFlag    bool
-	legendFlag    bool
-	outputFlag    enum
-	versionFlag   bool
+	concentrateFlag bool
+	dpiFlag         int
+	edgeStyleFlag   enum
+	filenameFlag    string
+	layersFlag      bool
+	legendFlag      bool
+	outputFlag      enum
+	versionFlag     bool
 )
 
 // dfgWriter is a writer that prints to stdout. When testing, we
@@ -57,7 +58,11 @@ It outputs a graph representation of the build process.`,
 			defer os.Remove(dotFile.Name())
 
 			dotFileContent := dockerfile2dot.BuildDotFile(
-				dockerfile, legendFlag, layersFlag, edgeStyleFlag.String(),
+				dockerfile,
+				concentrateFlag,
+				edgeStyleFlag.String(),
+				layersFlag,
+				legendFlag,
 			)
 
 			_, err = dotFile.Write([]byte(dotFileContent))
@@ -72,6 +77,17 @@ It outputs a graph representation of the build process.`,
 
 			filename := "Dockerfile." + outputFlag.String()
 
+			if outputFlag.String() == "raw" {
+				err = os.Rename(dotFile.Name(), filename)
+				if err != nil {
+					return
+				}
+
+				fmt.Fprintf(dfgWriter, "Successfully created %s\n", filename)
+
+				return
+			}
+
 			dotArgs := []string{
 				"-T" + outputFlag.String(),
 				"-o" + filename,
@@ -104,6 +120,14 @@ It outputs a graph representation of the build process.`,
 	}
 
 	// Flags
+	rootCmd.Flags().BoolVarP(
+		&concentrateFlag,
+		"concentrate",
+		"c",
+		false,
+		"concentrate the edges (default false)",
+	)
+
 	rootCmd.Flags().IntVarP(
 		&dpiFlag,
 		"dpi",
@@ -142,7 +166,7 @@ It outputs a graph representation of the build process.`,
 		"add a legend (default false)",
 	)
 
-	outputFlag = newEnum("pdf", "canon", "dot", "png", "svg")
+	outputFlag = newEnum("pdf", "canon", "dot", "png", "raw", "svg")
 	rootCmd.Flags().VarP(
 		&outputFlag,
 		"output",
diff --git a/internal/dockerfile2dot/build.go b/internal/dockerfile2dot/build.go
index 563eb14..f4ff511 100644
--- a/internal/dockerfile2dot/build.go
+++ b/internal/dockerfile2dot/build.go
@@ -10,8 +10,11 @@ import (
 
 // BuildDotFile builds a GraphViz .dot file from a simplified Dockerfile
 func BuildDotFile(
-	simplifiedDockerfile SimplifiedDockerfile, legend bool, layers bool,
+	simplifiedDockerfile SimplifiedDockerfile,
+	concentrate bool,
 	edgeStyle string,
+	layers bool,
+	legend bool,
 ) string {
 	// Create a new graph
 	graph := gographviz.NewEscape()
@@ -20,6 +23,9 @@ func BuildDotFile(
 	_ = graph.AddAttr("G", "compound", "true") // allow edges between clusters
 	_ = graph.AddAttr("G", "nodesep", "1")
 	_ = graph.AddAttr("G", "rankdir", "LR")
+	if concentrate {
+		_ = graph.AddAttr("G", "concentrate", "true")
+	}
 
 	// Add the legend if requested
 	if legend {


Test patch:
diff --git a/internal/cmd/root_test.go b/internal/cmd/root_test.go
index 3d97287..e18c742 100644
--- a/internal/cmd/root_test.go
+++ b/internal/cmd/root_test.go
@@ -26,13 +26,14 @@ var usage = `Usage:
   dockerfilegraph [flags]
 
 Flags:
+  -c, --concentrate       concentrate the edges (default false)
   -d, --dpi int           dots per inch of the PNG export (default 96)
   -e, --edgestyle         style of the graph edges, one of: default, solid (default default)
   -f, --filename string   name of the Dockerfile (default "Dockerfile")
   -h, --help              help for dockerfilegraph
       --layers            display all layers (default false)
       --legend            add a legend (default false)
-  -o, --output            output file format, one of: canon, dot, pdf, png, svg (default pdf)
+  -o, --output            output file format, one of: canon, dot, pdf, png, raw, svg (default pdf)
       --version           display the version of dockerfilegraph
 `
 
@@ -141,126 +142,64 @@ It outputs a graph representation of the build process.
 		},
 		{
 			name:        "layers flag",
-			cliArgs:     []string{"--layers", "-o", "canon"},
-			wantOut:     "Successfully created Dockerfile.canon\n",
-			wantOutFile: "Dockerfile.canon",
+			cliArgs:     []string{"--layers", "-o", "raw"},
+			wantOut:     "Successfully created Dockerfile.raw\n",
+			wantOutFile: "Dockerfile.raw",
+			//nolint:lll
 			wantOutFileContent: `digraph G {
-	graph [compound=true,
-		nodesep=1,
-		rankdir=LR
-	];
-	node [label="\N"];
+	compound=true;
+	nodesep=1;
+	rankdir=LR;
+	stage_0_layer_0->stage_0_layer_1;
+	external_image_0->stage_0_layer_0;
+	stage_1_layer_0->stage_1_layer_1;
+	external_image_1->stage_1_layer_0;
+	external_image_2->stage_1_layer_1[ arrowhead=ediamond, style=dotted ];
+	stage_2_layer_0->stage_2_layer_1;
+	stage_2_layer_1->stage_2_layer_2;
+	stage_2_layer_2->stage_2_layer_3;
+	external_image_3->stage_2_layer_0;
+	stage_0_layer_1->stage_2_layer_1[ arrowhead=empty, ltail=cluster_stage_0, style=dashed ];
+	stage_1_layer_1->stage_2_layer_2[ arrowhead=empty, ltail=cluster_stage_1, style=dashed ];
 	subgraph cluster_stage_0 {
-		graph [label=ubuntu,
-			margin=16
-		];
-		stage_0_layer_0	[fillcolor=white,
-			label="FROM ubuntu:lates...",
-			penwidth=0.5,
-			shape=box,
-			style="filled,rounded",
-			width=2];
-		stage_0_layer_1	[fillcolor=white,
-			label="RUN   apt-get upd...",
-			penwidth=0.5,
-			shape=box,
-			style="filled,rounded",
-			width=2];
-		stage_0_layer_0 -> stage_0_layer_1;
-	}
+	label=ubuntu;
+	margin=16;
+	stage_0_layer_0 [ fillcolor=white, label="FROM ubuntu:lates...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ];
+	stage_0_layer_1 [ fillcolor=white, label="RUN   apt-get upd...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ];
+
+}
+;
 	subgraph cluster_stage_1 {
-		graph [label=build,
-			margin=16
-		];
-		stage_1_layer_0	[fillcolor=white,
-			label="FROM golang:1.19 ...",
-			penwidth=0.5,
-			shape=box,
-			style="filled,rounded",
-			width=2];
-		stage_1_layer_1	[fillcolor=white,
-			label="RUN --mount=type=...",
-			penwidth=0.5,
-			shape=box,
-			style="filled,rounded",
-			width=2];
-		stage_1_layer_0 -> stage_1_layer_1;
-	}
+	label=build;
+	margin=16;
+	stage_1_layer_0 [ fillcolor=white, label="FROM golang:1.19 ...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ];
+	stage_1_layer_1 [ fillcolor=white, label="RUN --mount=type=...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ];
+
+}
+;
 	subgraph cluster_stage_2 {
-		graph [fillcolor=grey90,
-			label=release,
-			margin=16,
-			style=filled
-		];
-		stage_2_layer_0	[fillcolor=white,
-			label="FROM scratch AS r...",
-			penwidth=0.5,
-			shape=box,
-			style="filled,rounded",
-			width=2];
-		stage_2_layer_1	[fillcolor=white,
-			label="COPY --from=ubunt...",
-			penwidth=0.5,
-			shape=box,
-			style="filled,rounded",
-			width=2];
-		stage_2_layer_0 -> stage_2_layer_1;
-		stage_2_layer_2	[fillcolor=white,
-			label="COPY --from=build...",
-			penwidth=0.5,
-			shape=box,
-			style="filled,rounded",
-			width=2];
-		stage_2_layer_1 -> stage_2_layer_2;
-		stage_2_layer_3	[fillcolor=white,
-			label="ENTRYPOINT ['/exa...",
-			penwidth=0.5,
-			shape=box,
-			style="filled,rounded",
-			width=2];
-		stage_2_layer_2 -> stage_2_layer_3;
-	}
-	stage_0_layer_1 -> stage_2_layer_1	[arrowhead=empty,
-		ltail=cluster_stage_0,
-		style=dashed];
-	external_image_0	[color=grey20,
-		fontcolor=grey20,
-		label="ubuntu:latest",
-		shape=box,
-		style="dashed,rounded",
-		width=2];
-	external_image_0 -> stage_0_layer_0;
-	stage_1_layer_1 -> stage_2_layer_2	[arrowhead=empty,
-		ltail=cluster_stage_1,
-		style=dashed];
-	external_image_1	[color=grey20,
-		fontcolor=grey20,
-		label="golang:1.19",
-		shape=box,
-		style="dashed,rounded",
-		width=2];
-	external_image_1 -> stage_1_layer_0;
-	external_image_2	[color=grey20,
-		fontcolor=grey20,
-		label=buildcache,
-		shape=box,
-		style="dashed,rounded",
-		width=2];
-	external_image_2 -> stage_1_layer_1	[arrowhead=ediamond,
-		style=dotted];
-	external_image_3	[color=grey20,
-		fontcolor=grey20,
-		label=scratch,
-		shape=box,
-		style="dashed,rounded",
-		width=2];
-	external_image_3 -> stage_2_layer_0;
+	fillcolor=grey90;
+	label=release;
+	margin=16;
+	style=filled;
+	stage_2_layer_0 [ fillcolor=white, label="FROM scratch AS r...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ];
+	stage_2_layer_1 [ fillcolor=white, label="COPY --from=ubunt...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ];
+	stage_2_layer_2 [ fillcolor=white, label="COPY --from=build...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ];
+	stage_2_layer_3 [ fillcolor=white, label="ENTRYPOINT ['/exa...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ];
+
+}
+;
+	external_image_0 [ color=grey20, fontcolor=grey20, label="ubuntu:latest", shape=box, style="dashed,rounded", width=2 ];
+	external_image_1 [ color=grey20, fontcolor=grey20, label="golang:1.19", shape=box, style="dashed,rounded", width=2 ];
+	external_image_2 [ color=grey20, fontcolor=grey20, label="buildcache", shape=box, style="dashed,rounded", width=2 ];
+	external_image_3 [ color=grey20, fontcolor=grey20, label="scratch", shape=box, style="dashed,rounded", width=2 ];
+
 }
 `,
 		},
 		{
 			name:        "layers flag with solid edges",
-			cliArgs:     []string{"--layers", "-o", "canon", "-e", "solid"},
+			cliArgs:     []string{"--layers", "-e", "solid", "-o", "canon"},
 			wantOut:     "Successfully created Dockerfile.canon\n",
 			wantOutFile: "Dockerfile.canon",
 			wantOutFileContent: `digraph G {
@@ -375,12 +314,13 @@ It outputs a graph representation of the build process.
 `,
 		},
 		{
-			name:        "legend flag",
-			cliArgs:     []string{"--legend", "-o", "canon"},
+			name:        "legend flag with concentrated edges",
+			cliArgs:     []string{"--legend", "--concentrate", "-o", "canon"},
 			wantOut:     "Successfully created Dockerfile.canon\n",
 			wantOutFile: "Dockerfile.canon",
 			wantOutFileContent: `digraph G {
 	graph [compound=true,
+		concentrate=true,
 		nodesep=1,
 		rankdir=LR
 	];
@@ -459,7 +399,7 @@ It outputs a graph representation of the build process.
 		},
 		{
 			name:        "legend flag with solid edges",
-			cliArgs:     []string{"--legend", "-o", "canon", "-e", "solid"},
+			cliArgs:     []string{"--legend", "-e", "solid", "-o", "canon"},
 			wantOut:     "Successfully created Dockerfile.canon\n",
 			wantOutFile: "Dockerfile.canon",
 			wantOutFileContent: `digraph G {
diff --git a/internal/dockerfile2dot/build_test.go b/internal/dockerfile2dot/build_test.go
index a7cd861..0ab76c0 100644
--- a/internal/dockerfile2dot/build_test.go
+++ b/internal/dockerfile2dot/build_test.go
@@ -7,10 +7,11 @@ import (
 
 func TestBuildDotFile(t *testing.T) {
 	type args struct {
+		simplifiedDockerfile SimplifiedDockerfile
+		concentrate          bool
 		edgeStyle            string
 		layers               bool
 		legend               bool
-		simplifiedDockerfile SimplifiedDockerfile
 	}
 	tests := []struct {
 		name         string
@@ -85,8 +86,11 @@ func TestBuildDotFile(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			if got := BuildDotFile(
-				tt.args.simplifiedDockerfile, tt.args.legend, tt.args.layers,
+				tt.args.simplifiedDockerfile,
+				tt.args.concentrate,
 				tt.args.edgeStyle,
+				tt.args.layers,
+				tt.args.legend,
 			); !strings.Contains(got, tt.wantContains) {
 				t.Errorf(
 					"BuildDotFile() = %v, did not contain %v", got, tt.wantContains,