From 67f5e3413bbdca614f20042bf12b326f897a4b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Fri, 12 Dec 2025 11:16:01 +0100 Subject: [PATCH 1/5] image: Fix dangling image detection with graphdrivers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The isDangling function was incorrectly identifying images as dangling when they had no RepoTags but had valid RepoDigests. This can occur when the graphdrivers are used instead of the containerd image store. An image should only be considered dangling if it has no RepoTags, regardless of whether it has RepoDigests. Signed-off-by: Paweł Gronowski --- cli/command/image/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/command/image/list.go b/cli/command/image/list.go index 40915b892470..7efaf8426d20 100644 --- a/cli/command/image/list.go +++ b/cli/command/image/list.go @@ -177,7 +177,7 @@ func shouldUseTree(options imagesOptions) (bool, error) { // isDangling is a copy of [formatter.isDangling]. func isDangling(img image.Summary) bool { - if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 { + if len(img.RepoTags) == 0 { return true } return len(img.RepoTags) == 1 && img.RepoTags[0] == ":" && len(img.RepoDigests) == 1 && img.RepoDigests[0] == "@" From 150a25b9ff45798ab6f23a365d6d8979faa670c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Fri, 12 Dec 2025 11:17:42 +0100 Subject: [PATCH 2/5] image/tree: Extract untagged image name to const MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski --- cli/command/image/tree.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/command/image/tree.go b/cli/command/image/tree.go index ff0c54a4cc22..2a497e6e77d8 100644 --- a/cli/command/image/tree.go +++ b/cli/command/image/tree.go @@ -22,6 +22,8 @@ import ( "github.com/opencontainers/go-digest" ) +const untaggedName = "" + type treeOptions struct { images []imagetypes.Summary all bool @@ -433,7 +435,7 @@ func printChildren(out tui.Output, headers []imgColumn, img topImage, normalColo func printNames(out tui.Output, headers []imgColumn, img topImage, color, untaggedColor aec.ANSI) { if len(img.Names) == 0 { - _, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, "")) + _, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, untaggedName)) } for nameIdx, name := range img.Names { From b315983898ddd5bd487e8e85828ed0e84c2726ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Fri, 12 Dec 2025 11:18:27 +0100 Subject: [PATCH 3/5] image/tree: Fix width calculation for untagged images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When calculating column widths for the tree view, untagged images weren't being properly accounted for in the width calculation. This caused layout issues when there were tagged images were shorter than the `` string. Signed-off-by: Paweł Gronowski --- cli/command/image/tree.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/command/image/tree.go b/cli/command/image/tree.go index 2a497e6e77d8..ea8dc1a6cdf5 100644 --- a/cli/command/image/tree.go +++ b/cli/command/image/tree.go @@ -547,7 +547,11 @@ func (h imgColumn) PrintR(clr aec.ANSI, s string) string { func widestFirstColumnValue(headers []imgColumn, images []topImage) int { width := len(headers[0].Title) for _, img := range images { - for _, name := range img.Names { + names := img.Names + if len(names) == 0 { + names = []string{untaggedName} + } + for _, name := range names { if len(name) > width { width = len(name) } From 0d88411f1bf28e9233ceec18770214ee056da887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Fri, 12 Dec 2025 11:22:47 +0100 Subject: [PATCH 4/5] image/tree: Remove --all flag check for untagged images in non-expanded view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts part of the logic introduced in 207bf52c27 which incorrectly gated untagged images behind the --all flag in non-expanded view. The original fix was addressing the wrong layer of the problem. The actual issue was that dangling images were being incorrectly passed to the tree code in the first place. This was properly fixed in 67f5e3413 which corrected the dangling image detection logic to properly filter them out before reaching the tree display code. Now that dangling images are correctly filtered upstream, untagged images that reach the tree view should be displayed regardless of the --all flag setting. Signed-off-by: Paweł Gronowski --- cli/command/image/tree.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/command/image/tree.go b/cli/command/image/tree.go index ea8dc1a6cdf5..48b7d409fff1 100644 --- a/cli/command/image/tree.go +++ b/cli/command/image/tree.go @@ -113,7 +113,7 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) (int, continue } - if opts.all && len(sortedTags) == 0 { + if len(sortedTags) == 0 { view.images = append(view.images, topImage{ Details: topDetails, Children: children, From 09a46645a0e34ff6ef631b52963f0ab814c04b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Fri, 12 Dec 2025 12:39:26 +0100 Subject: [PATCH 5/5] image/tree: Add golden test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski --- ...uccess.expanded-view-with-platforms.golden | 5 + ...mixed-tagged-untagged-with-children.golden | 10 + ...and-success.untagged-with-platforms.golden | 5 + ...-success.width-calculation-untagged.golden | 4 + cli/command/image/tree_test.go | 199 ++++++++++++++++++ 5 files changed, 223 insertions(+) create mode 100644 cli/command/image/testdata/tree-command-success.expanded-view-with-platforms.golden create mode 100644 cli/command/image/testdata/tree-command-success.mixed-tagged-untagged-with-children.golden create mode 100644 cli/command/image/testdata/tree-command-success.untagged-with-platforms.golden create mode 100644 cli/command/image/testdata/tree-command-success.width-calculation-untagged.golden diff --git a/cli/command/image/testdata/tree-command-success.expanded-view-with-platforms.golden b/cli/command/image/testdata/tree-command-success.expanded-view-with-platforms.golden new file mode 100644 index 000000000000..2fd950562bd9 --- /dev/null +++ b/cli/command/image/testdata/tree-command-success.expanded-view-with-platforms.golden @@ -0,0 +1,5 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +multiplatform:latest aaaaaaaaaaaa 25.5 MB 20.2 MB U +├─ linux/amd64 bbbbbbbbbbbb 12.1 MB 10.0 MB +└─ linux/arm64 cccccccccccc 13.4 MB 10.2 MB U + diff --git a/cli/command/image/testdata/tree-command-success.mixed-tagged-untagged-with-children.golden b/cli/command/image/testdata/tree-command-success.mixed-tagged-untagged-with-children.golden new file mode 100644 index 000000000000..ce0b92828cda --- /dev/null +++ b/cli/command/image/testdata/tree-command-success.mixed-tagged-untagged-with-children.golden @@ -0,0 +1,10 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +app:v1 +app:latest 101010101010 30.5 MB 25.2 MB U +└─ linux/amd64 202020202020 15.2 MB 12.6 MB U + + 303030303030 12.3 MB 10.1 MB +└─ linux/arm/v7 404040404040 6.1 MB 5.0 MB + +base:alpine 505050505050 5.5 MB 5.5 MB + diff --git a/cli/command/image/testdata/tree-command-success.untagged-with-platforms.golden b/cli/command/image/testdata/tree-command-success.untagged-with-platforms.golden new file mode 100644 index 000000000000..465b67857312 --- /dev/null +++ b/cli/command/image/testdata/tree-command-success.untagged-with-platforms.golden @@ -0,0 +1,5 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA + dddddddddddd 18.5 MB 15.2 MB +├─ linux/amd64 eeeeeeeeeeee 9.2 MB 7.6 MB +└─ linux/arm64 ffffffffffff 9.3 MB 7.6 MB + diff --git a/cli/command/image/testdata/tree-command-success.width-calculation-untagged.golden b/cli/command/image/testdata/tree-command-success.width-calculation-untagged.golden new file mode 100644 index 000000000000..6c58f0aa4b7c --- /dev/null +++ b/cli/command/image/testdata/tree-command-success.width-calculation-untagged.golden @@ -0,0 +1,4 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +a:1 111111111111 5.5 MB 2.5 MB + 222222222222 3.2 MB 1.6 MB +short:v1 333333333333 7.1 MB 3.5 MB U diff --git a/cli/command/image/tree_test.go b/cli/command/image/tree_test.go index 8e340cc844e4..4516c08a1bae 100644 --- a/cli/command/image/tree_test.go +++ b/cli/command/image/tree_test.go @@ -1,11 +1,13 @@ package image import ( + "fmt" "strings" "testing" "github.com/docker/cli/internal/test" "gotest.tools/v3/assert" + "gotest.tools/v3/golden" ) func TestPrintImageTreeAnsiTty(t *testing.T) { @@ -154,3 +156,200 @@ func TestPrintImageTreeAnsiTty(t *testing.T) { }) } } + +func TestPrintImageTreeGolden(t *testing.T) { + testCases := []struct { + name string + view treeView + expanded bool + }{ + { + name: "width-calculation-untagged", + expanded: false, + view: treeView{ + images: []topImage{ + { + Names: []string{"a:1"}, + Details: imageDetails{ + ID: "sha256:1111111111111111111111111111111111111111111111111111111111111111", + DiskUsage: "5.5 MB", + InUse: false, + ContentSize: "2.5 MB", + }, + }, + { + // Untagged image name is longer than "a:1" + Names: []string{}, + Details: imageDetails{ + ID: "sha256:2222222222222222222222222222222222222222222222222222222222222222", + DiskUsage: "3.2 MB", + InUse: false, + ContentSize: "1.6 MB", + }, + }, + { + Names: []string{"short:v1"}, + Details: imageDetails{ + ID: "sha256:3333333333333333333333333333333333333333333333333333333333333333", + DiskUsage: "7.1 MB", + InUse: true, + ContentSize: "3.5 MB", + }, + }, + }, + imageSpacing: false, + }, + }, + { + name: "expanded-view-with-platforms", + expanded: false, + view: treeView{ + images: []topImage{ + { + Names: []string{"multiplatform:latest"}, + Details: imageDetails{ + ID: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + DiskUsage: "25.5 MB", + InUse: true, + ContentSize: "20.2 MB", + }, + Children: []subImage{ + { + Platform: "linux/amd64", + Available: true, + Details: imageDetails{ + ID: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + DiskUsage: "12.1 MB", + InUse: false, + ContentSize: "10.0 MB", + }, + }, + { + Platform: "linux/arm64", + Available: true, + Details: imageDetails{ + ID: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + DiskUsage: "13.4 MB", + InUse: true, + ContentSize: "10.2 MB", + }, + }, + }, + }, + }, + imageSpacing: true, + }, + }, + { + name: "untagged-with-platforms", + expanded: false, + view: treeView{ + images: []topImage{ + { + Names: []string{}, + Details: imageDetails{ + ID: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + DiskUsage: "18.5 MB", + InUse: false, + ContentSize: "15.2 MB", + }, + Children: []subImage{ + { + Platform: "linux/amd64", + Available: true, + Details: imageDetails{ + ID: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + DiskUsage: "9.2 MB", + InUse: false, + ContentSize: "7.6 MB", + }, + }, + { + Platform: "linux/arm64", + Available: false, + Details: imageDetails{ + ID: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + DiskUsage: "9.3 MB", + InUse: false, + ContentSize: "7.6 MB", + }, + }, + }, + }, + }, + imageSpacing: true, + }, + }, + { + name: "mixed-tagged-untagged-with-children", + expanded: false, + view: treeView{ + images: []topImage{ + { + Names: []string{"app:v1", "app:latest"}, + Details: imageDetails{ + ID: "sha256:1010101010101010101010101010101010101010101010101010101010101010", + DiskUsage: "30.5 MB", + InUse: true, + ContentSize: "25.2 MB", + }, + Children: []subImage{ + { + Platform: "linux/amd64", + Available: true, + Details: imageDetails{ + ID: "sha256:2020202020202020202020202020202020202020202020202020202020202020", + DiskUsage: "15.2 MB", + InUse: true, + ContentSize: "12.6 MB", + }, + }, + }, + }, + { + Names: []string{}, + Details: imageDetails{ + ID: "sha256:3030303030303030303030303030303030303030303030303030303030303030", + DiskUsage: "12.3 MB", + InUse: false, + ContentSize: "10.1 MB", + }, + Children: []subImage{ + { + Platform: "linux/arm/v7", + Available: true, + Details: imageDetails{ + ID: "sha256:4040404040404040404040404040404040404040404040404040404040404040", + DiskUsage: "6.1 MB", + InUse: false, + ContentSize: "5.0 MB", + }, + }, + }, + }, + { + Names: []string{"base:alpine"}, + Details: imageDetails{ + ID: "sha256:5050505050505050505050505050505050505050505050505050505050505050", + DiskUsage: "5.5 MB", + InUse: false, + ContentSize: "5.5 MB", + }, + }, + }, + imageSpacing: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(nil) + cli.Out().SetIsTerminal(false) + + printImageTree(cli, tc.view) + + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("tree-command-success.%s.golden", tc.name)) + }) + } +}