image/list: Show collapsed tree by default

Use the new tree view by default and only fallback if format or old
view-related options are used.

The expanded view is shown when `--tree` is passed.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski
2025-10-31 18:10:19 +01:00
parent cdaae144e9
commit c41815f17a
2 changed files with 73 additions and 36 deletions
+38 -19
View File
@@ -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 {
+35 -17
View File
@@ -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 {