diff --git a/cli/command/image/list.go b/cli/command/image/list.go index fde226b609..6ad7798d88 100644 --- a/cli/command/image/list.go +++ b/cli/command/image/list.go @@ -82,38 +82,28 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command { return &cmd } -//nolint:gocyclo func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions) error { filters := options.filter.Value() if options.matchName != "" { filters.Add("reference", options.matchName) } - if options.tree { - if options.quiet { - return errors.New("--quiet is not yet supported with --tree") - } - if options.noTrunc { - return errors.New("--no-trunc is not yet supported with --tree") - } - if options.showDigests { - return errors.New("--show-digest is not yet supported with --tree") - } - if options.format != "" { - return errors.New("--format is not yet supported with --tree") - } + useTree, err := shouldUseTree(options) + if err != nil { + return err } listOpts := client.ImageListOptions{ All: options.all, Filters: filters, - Manifests: options.tree, + Manifests: useTree, } res, err := dockerCLI.Client().ImageList(ctx, listOpts) if err != nil { return err } + images := res.Items if !options.all { if _, ok := filters["dangling"]; !ok { @@ -121,11 +111,12 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions } } - if options.tree { + if useTree { return runTree(ctx, dockerCLI, treeOptions{ - images: images, - all: options.all, - filters: filters, + images: images, + all: options.all, + filters: filters, + expanded: options.tree, }) } @@ -155,6 +146,34 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions return nil } +func shouldUseTree(options imagesOptions) (bool, error) { + if options.quiet { + if options.tree { + return false, errors.New("--quiet is not yet supported with --tree") + } + return false, nil + } + if options.noTrunc { + if options.tree { + return false, errors.New("--no-trunc is not yet supported with --tree") + } + return false, nil + } + if options.showDigests { + if options.tree { + return false, errors.New("--show-digest is not yet supported with --tree") + } + return false, nil + } + if options.format != "" { + if options.tree { + return false, errors.New("--format is not yet supported with --tree") + } + return false, nil + } + return true, nil +} + // isDangling is a copy of [formatter.isDangling]. func isDangling(img image.Summary) bool { if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 { diff --git a/cli/command/image/tree.go b/cli/command/image/tree.go index c1586af58d..d356aef808 100644 --- a/cli/command/image/tree.go +++ b/cli/command/image/tree.go @@ -24,9 +24,10 @@ import ( ) type treeOptions struct { - images []imagetypes.Summary - all bool - filters client.Filters + images []imagetypes.Summary + all bool + filters client.Filters + expanded bool } type treeView struct { @@ -48,7 +49,7 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error if ctx.Err() != nil { return ctx.Err() } - details := imageDetails{ + topDetails := imageDetails{ ID: img.ID, DiskUsage: units.HumanSizeWithPrecision(float64(img.Size), 3), InUse: img.Containers > 0, @@ -67,41 +68,58 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error continue } + inUse := len(im.ImageData.Containers) > 0 + if inUse { + // Mark top-level parent image as used if any of its subimages are used. + topDetails.InUse = true + } + + if !opts.expanded { + continue + } + sub := subImage{ Platform: platforms.Format(im.ImageData.Platform), Available: im.Available, Details: imageDetails{ ID: im.ID, DiskUsage: units.HumanSizeWithPrecision(float64(im.Size.Total), 3), - InUse: len(im.ImageData.Containers) > 0, + InUse: inUse, ContentSize: units.HumanSizeWithPrecision(float64(im.Size.Content), 3), }, } - if sub.Details.InUse { - // Mark top-level parent image as used if any of its subimages are used. - details.InUse = true - } - children = append(children, sub) // Add extra spacing between images if there's at least one entry with children. view.imageSpacing = true } - details.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3) + topDetails.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3) // Sort tags for this image sortedTags := make([]string, len(img.RepoTags)) copy(sortedTags, img.RepoTags) slices.Sort(sortedTags) - view.images = append(view.images, topImage{ - Names: sortedTags, - Details: details, - Children: children, - created: img.Created, - }) + if opts.expanded { + view.images = append(view.images, topImage{ + Names: sortedTags, + Details: topDetails, + Children: children, + created: img.Created, + }) + continue + } + + for _, tag := range sortedTags { + view.images = append(view.images, topImage{ + Names: []string{tag}, + Details: topDetails, + Children: children, + created: img.Created, + }) + } } slices.SortFunc(view.images, func(a, b topImage) int {