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] == "@" 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.go b/cli/command/image/tree.go index ff0c54a4cc22..48b7d409fff1 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 @@ -111,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, @@ -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 { @@ -545,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) } 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)) + }) + } +}