cli/command/formatter: add TrunateID utility

We were depending on pkg/stringid to truncate IDs for presentation. While
traditionally, we used a fixed length for "truncated" IDs, this is not
a strict requirement (any ID-prefix should work, but conflicts may
happen on shorter IDs).

This patch adds a local `TruncateID()` utility in the formatter package;
it's currently using the same implementation and length as the
`stringid.TruncateID` function, but may diverge in future.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-07-14 19:51:30 +02:00
parent c69d8bde4a
commit e0f4bc699c
20 changed files with 86 additions and 34 deletions

View File

@ -5,7 +5,6 @@ import (
"sync"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
)
@ -176,7 +175,7 @@ func (c *statsContext) Name() string {
func (c *statsContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.s.ID)
return formatter.TruncateID(c.s.ID)
}
return c.s.ID
}

View File

@ -7,7 +7,6 @@ import (
"time"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)
@ -115,7 +114,7 @@ func (c *buildCacheContext) MarshalJSON() ([]byte, error) {
func (c *buildCacheContext) ID() string {
id := c.v.ID
if c.trunc {
id = stringid.TruncateID(c.v.ID)
id = TruncateID(c.v.ID)
}
if c.v.InUse {
return id + "*"
@ -131,7 +130,7 @@ func (c *buildCacheContext) Parent() string {
parent = c.v.Parent //nolint:staticcheck // Ignore SA1019: Field was deprecated in API v1.42, but kept for backward compatibility
}
if c.trunc {
return stringid.TruncateID(parent)
return TruncateID(parent)
}
return parent
}

View File

@ -14,7 +14,6 @@ import (
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
@ -135,7 +134,7 @@ func (c *ContainerContext) MarshalJSON() ([]byte, error) {
// option being set, the full or truncated ID is returned.
func (c *ContainerContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.c.ID)
return TruncateID(c.c.ID)
}
return c.c.ID
}
@ -172,7 +171,7 @@ func (c *ContainerContext) Image() string {
return "<no image>"
}
if c.trunc {
if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
if trunc := TruncateID(c.c.ImageID); trunc == TruncateID(c.c.Image) {
return trunc
}
// truncate digest if no-trunc option was not selected

View File

@ -34,7 +34,7 @@ func TestContainerPsContext(t *testing.T) {
{
container: container.Summary{ID: containerID},
trunc: true,
expValue: stringid.TruncateID(containerID),
expValue: TruncateID(containerID),
call: ctx.ID,
},
{

View File

@ -27,6 +27,25 @@ func charWidth(r rune) int {
}
}
const shortLen = 12
// TruncateID returns a shorthand version of a string identifier for presentation,
// after trimming digest algorithm prefix (if any).
//
// This function is a copy of [stringid.TruncateID] for presentation / formatting
// purposes.
//
// [stringid.TruncateID]: https://github.com/moby/moby/blob/v28.3.2/pkg/stringid/stringid.go#L19
func TruncateID(id string) string {
if i := strings.IndexRune(id, ':'); i >= 0 {
id = id[i+1:]
}
if len(id) > shortLen {
id = id[:shortLen]
}
return id
}
// Ellipsis truncates a string to fit within maxDisplayWidth, and appends ellipsis (…).
// For maxDisplayWidth of 1 and lower, no ellipsis is appended.
// For maxDisplayWidth of 1, first char of string will return even if its width > 1.

View File

@ -7,6 +7,49 @@ import (
is "gotest.tools/v3/assert/cmp"
)
func TestTruncateID(t *testing.T) {
tests := []struct {
doc, id, expected string
}{
{
doc: "empty ID",
id: "",
expected: "",
},
{
// IDs are expected to be 12 (short) or 64 characters, and not be numeric only,
// but TruncateID should handle these gracefully.
doc: "invalid ID",
id: "1234",
expected: "1234",
},
{
doc: "full ID",
id: "90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2",
expected: "90435eec5c4e",
},
{
doc: "digest",
id: "sha256:90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2",
expected: "90435eec5c4e",
},
{
doc: "very long ID",
id: "90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a290435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2",
expected: "90435eec5c4e",
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
actual := TruncateID(tc.id)
if actual != tc.expected {
t.Errorf("expected: %q, got: %q", tc.expected, actual)
}
})
}
}
func TestEllipsis(t *testing.T) {
testcases := []struct {
source string

View File

@ -6,7 +6,6 @@ import (
"github.com/distribution/reference"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
)
@ -216,7 +215,7 @@ func (c *imageContext) MarshalJSON() ([]byte, error) {
func (c *imageContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.i.ID)
return TruncateID(c.i.ID)
}
return c.i.ID
}

View File

@ -27,7 +27,7 @@ func TestImageContext(t *testing.T) {
}{
{
imageCtx: imageContext{i: image.Summary{ID: imageID}, trunc: true},
expValue: stringid.TruncateID(imageID),
expValue: TruncateID(imageID),
call: ctx.ID,
},
{

View File

@ -7,7 +7,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
)
@ -72,7 +71,7 @@ func (c *historyContext) MarshalJSON() ([]byte, error) {
func (c *historyContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.h.ID)
return formatter.TruncateID(c.h.ID)
}
return c.h.ID
}

View File

@ -35,7 +35,7 @@ func TestHistoryContext_ID(t *testing.T) {
historyContext{
h: image.HistoryResponseItem{ID: id},
trunc: true,
}, stringid.TruncateID(id), ctx.ID,
}, formatter.TruncateID(id), ctx.ID,
},
}

View File

@ -12,10 +12,10 @@ import (
"github.com/containerd/platforms"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/tui"
"github.com/docker/docker/api/types/filters"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/morikuni/aec"
"github.com/opencontainers/go-digest"
@ -222,7 +222,7 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
Align: alignLeft,
Width: 12,
DetailsValue: func(d *imageDetails) string {
return stringid.TruncateID(d.ID)
return formatter.TruncateID(d.ID)
},
},
{

View File

@ -6,7 +6,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/stringid"
)
const (
@ -73,7 +72,7 @@ func (c *networkContext) MarshalJSON() ([]byte, error) {
func (c *networkContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.n.ID)
return formatter.TruncateID(c.n.ID)
}
return c.n.ID
}

View File

@ -35,7 +35,7 @@ func TestNetworkContext(t *testing.T) {
{networkContext{
n: network.Summary{ID: networkID},
trunc: true,
}, stringid.TruncateID(networkID), ctx.ID},
}, formatter.TruncateID(networkID), ctx.ID},
{networkContext{
n: network.Summary{Name: "network_name"},
}, "network_name", ctx.Name},

View File

@ -5,7 +5,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
)
const (
@ -66,7 +65,7 @@ func (c *pluginContext) MarshalJSON() ([]byte, error) {
func (c *pluginContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.p.ID)
return formatter.TruncateID(c.p.ID)
}
return c.p.ID
}

View File

@ -33,7 +33,7 @@ func TestPluginContext(t *testing.T) {
{pluginContext{
p: types.Plugin{ID: pluginID},
trunc: true,
}, stringid.TruncateID(pluginID), ctx.ID},
}, formatter.TruncateID(pluginID), ctx.ID},
{pluginContext{
p: types.Plugin{Name: "plugin_name"},
}, "plugin_name", ctx.Name},

View File

@ -14,7 +14,6 @@ import (
mounttypes "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
"github.com/fvbommel/sortorder"
"github.com/pkg/errors"
@ -645,7 +644,7 @@ func (c *serviceContext) MarshalJSON() ([]byte, error) {
}
func (c *serviceContext) ID() string {
return stringid.TruncateID(c.service.ID)
return formatter.TruncateID(c.service.ID)
}
func (c *serviceContext) Name() string {

View File

@ -13,13 +13,13 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/idresolver"
"github.com/docker/cli/internal/logdetails"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/stringid"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@ -220,7 +220,7 @@ func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string,
if f.opts.noTrunc {
taskName += "." + task.ID
} else {
taskName += "." + stringid.TruncateID(task.ID)
taskName += "." + formatter.TruncateID(task.ID)
}
}

View File

@ -11,12 +11,12 @@ import (
"strings"
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/stringid"
)
var (
@ -505,7 +505,7 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int
if task.Status.Err != "" {
u.progressOut.WriteProgress(progress.Progress{
ID: stringid.TruncateID(task.NodeID),
ID: formatter.TruncateID(task.NodeID),
Action: truncError(task.Status.Err),
})
return
@ -513,7 +513,7 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int
if !terminalState(task.DesiredState) && !terminalState(task.Status.State) {
u.progressOut.WriteProgress(progress.Progress{
ID: stringid.TruncateID(task.NodeID),
ID: formatter.TruncateID(task.NodeID),
Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State),
Current: numberedStates[task.Status.State],
Total: maxProgress,

View File

@ -8,7 +8,6 @@ import (
"github.com/distribution/reference"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)
@ -79,7 +78,7 @@ func (c *taskContext) MarshalJSON() ([]byte, error) {
func (c *taskContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.task.ID)
return formatter.TruncateID(c.task.ID)
}
return c.task.ID
}

View File

@ -5,7 +5,6 @@ import (
"strings"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/stringid"
)
const (
@ -119,7 +118,7 @@ func (c *signerInfoContext) Keys() string {
truncatedKeys := []string{}
if c.trunc {
for _, keyID := range c.s.Keys {
truncatedKeys = append(truncatedKeys, stringid.TruncateID(keyID))
truncatedKeys = append(truncatedKeys, formatter.TruncateID(keyID))
}
return strings.Join(truncatedKeys, ", ")
}