Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Create Printer object to manage output #1292

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ LDFLAGS += -X $(PROJECT_PKG)/internal/version.GitTreeState=${GIT_DIRTY}

.PHONY: test
test: tidy vendor check-encoding ## tidy and run tests
$(GO_EXE) build ./...
$(GO_EXE) test -race -v -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...

.PHONY: teste2e
Expand Down
8 changes: 4 additions & 4 deletions cmd/oras/internal/display/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ import (
)

// NewPushHandler returns status and metadata handlers for push command.
func NewPushHandler(format string, tty *os.File, verbose bool) (status.PushHandler, metadata.PushHandler) {
func NewPushHandler(format string, tty *os.File, printer *status.Printer) (status.PushHandler, metadata.PushHandler) {
var statusHandler status.PushHandler
if tty != nil {
statusHandler = status.NewTTYPushHandler(tty)
} else if format == "" {
statusHandler = status.NewTextPushHandler(verbose)
statusHandler = status.NewTextPushHandler(printer)
} else {
statusHandler = status.NewDiscardHandler()
}
Expand All @@ -50,12 +50,12 @@ func NewPushHandler(format string, tty *os.File, verbose bool) (status.PushHandl
}

// NewAttachHandler returns status and metadata handlers for attach command.
func NewAttachHandler(format string, tty *os.File, verbose bool) (status.AttachHandler, metadata.AttachHandler) {
func NewAttachHandler(format string, tty *os.File, printer *status.Printer) (status.AttachHandler, metadata.AttachHandler) {
var statusHandler status.AttachHandler
if tty != nil {
statusHandler = status.NewTTYAttachHandler(tty)
} else if format == "" {
statusHandler = status.NewTextAttachHandler(verbose)
statusHandler = status.NewTextAttachHandler(printer)
} else {
statusHandler = status.NewDiscardHandler()
}
Expand Down
43 changes: 43 additions & 0 deletions cmd/oras/internal/display/metadata/model/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ limitations under the License.
package model

import (
"context"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras/cmd/oras/internal/display/status"
"sync"
)

// DigestReference is a reference to an artifact with digest.
Expand Down Expand Up @@ -70,3 +74,42 @@ func FromDescriptor(name string, desc ocispec.Descriptor) Descriptor {
}
return ret
}

// ShortDigest converts the digest of the descriptor to a short form for displaying.
func ShortDigest(desc ocispec.Descriptor) (digestString string) {
digestString = desc.Digest.String()
if err := desc.Digest.Validate(); err == nil {
if algo := desc.Digest.Algorithm(); algo == digest.SHA256 {
digestString = desc.Digest.Encoded()[:12]
}
}
return digestString
}

// PrintDescriptor prints descriptor status and name.
func PrintDescriptor(printer *status.Printer, desc ocispec.Descriptor, status string) {
name, ok := desc.Annotations[ocispec.AnnotationTitle]
if !ok {
name = desc.MediaType
printer.PrintVerbose(status, ShortDigest(desc), name)
return
}
printer.Print(status, ShortDigest(desc), name)
return
}

// PrintSuccessorStatus prints transfer status of successors.
func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, committed *sync.Map, prompt string, printer *status.Printer) error {
successors, err := content.Successors(ctx, fetcher, desc)
if err != nil {
return err
}
for _, s := range successors {
name := s.Annotations[ocispec.AnnotationTitle]
if v, ok := committed.Load(s.Digest.String()); ok && v != name {
// Reprint status for deduplicated content
PrintDescriptor(printer, desc, prompt)
}
}
return nil
}
32 changes: 0 additions & 32 deletions cmd/oras/internal/display/status/convert.go

This file was deleted.

81 changes: 37 additions & 44 deletions cmd/oras/internal/display/status/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,63 +19,54 @@ import (
"context"
"fmt"
"io"
"os"
"sync"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/registry"
)

var printLock sync.Mutex
type Printer struct {
qweeah marked this conversation as resolved.
Show resolved Hide resolved
printLock sync.Mutex
verbose bool
qweeah marked this conversation as resolved.
Show resolved Hide resolved
errors bool
}

// PrintFunc is the function type returned by StatusPrinter.
type PrintFunc func(ocispec.Descriptor) error
var printer Printer

// Print objects to display concurrent-safely.
func Print(a ...any) error {
printLock.Lock()
defer printLock.Unlock()
_, err := fmt.Println(a...)
return err
// NewPrinter creates a printer object
func NewPrinter(verbose bool) *Printer {
printer = Printer{verbose: verbose}
return &printer
}

// StatusPrinter returns a tracking function for transfer status.
func StatusPrinter(status string, verbose bool) PrintFunc {
return func(desc ocispec.Descriptor) error {
return PrintStatus(desc, status, verbose)
// Print objects to display concurrent-safely.
func (p *Printer) Print(a ...any) {
p.printLock.Lock()
defer p.printLock.Unlock()
_, err := fmt.Println(a...)
if err != nil {
if !p.errors {
p.errors = true
_, _ = fmt.Fprintf(os.Stderr, "Display output error: %w\n", err)
return
}
}
return
}

// PrintStatus prints transfer status.
func PrintStatus(desc ocispec.Descriptor, status string, verbose bool) error {
name, ok := desc.Annotations[ocispec.AnnotationTitle]
if !ok {
// no status for unnamed content
if !verbose {
return nil
}
name = desc.MediaType
// PrintVerbose display in verbose mode.
func (p *Printer) PrintVerbose(a ...any) {
if p.verbose {
p.Print(a...)
}
return Print(status, ShortDigest(desc), name)
return
}

// PrintSuccessorStatus prints transfer status of successors.
func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, committed *sync.Map, print PrintFunc) error {
successors, err := content.Successors(ctx, fetcher, desc)
if err != nil {
return err
}
for _, s := range successors {
name := s.Annotations[ocispec.AnnotationTitle]
if v, ok := committed.Load(s.Digest.String()); ok && v != name {
// Reprint status for deduplicated content
if err := print(s); err != nil {
return err
}
}
}
return nil
// Print objects to display concurrent-safely.
func Print(a ...any) {
printer.Print(a...)
}

// NewTagStatusPrinter creates a wrapper type for printing tag status.
Expand Down Expand Up @@ -119,13 +110,14 @@ func (p *tagManifestStatusForRepo) PushReference(ctx context.Context, expected o
if p.printHint != nil {
p.printHint.Do(func() {
ref := p.refPrefix + "@" + expected.Digest.String()
_ = Print("Tagging", ref)
Print("Tagging", ref)
})
}
if err := p.Repository.PushReference(ctx, expected, content, reference); err != nil {
return err
}
return Print("Tagged", reference)
Print("Tagged", reference)
return nil
}

type tagManifestStatusForTarget struct {
Expand All @@ -139,12 +131,13 @@ func (p *tagManifestStatusForTarget) Tag(ctx context.Context, desc ocispec.Descr
if p.printHint != nil {
p.printHint.Do(func() {
ref := p.refPrefix + "@" + desc.Digest.String()
_ = Print("Tagging", ref)
Print("Tagging", ref)
})
}

if err := p.Target.Tag(ctx, desc, reference); err != nil {
return err
}
return Print("Tagged", reference)
Print("Tagged", reference)
return nil
}
31 changes: 15 additions & 16 deletions cmd/oras/internal/display/status/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package status
import (
"context"
"fmt"
"oras.land/oras/cmd/oras/internal/display/metadata/model"
"sync"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
Expand All @@ -27,23 +28,20 @@ import (

// TextPushHandler handles text status output for push events.
type TextPushHandler struct {
verbose bool
printer *Printer
}

// NewTextPushHandler returns a new handler for push command.
func NewTextPushHandler(verbose bool) PushHandler {
func NewTextPushHandler(printer *Printer) PushHandler {
return &TextPushHandler{
verbose: verbose,
printer: printer,
}
}

// OnFileLoading is called when a file is being prepared for upload.
func (ph *TextPushHandler) OnFileLoading(name string) error {
if !ph.verbose {
return nil
}
_, err := fmt.Println("Preparing", name)
return err
printer.PrintVerbose("Preparing", name)
return nil
}

// OnEmptyArtifact is called when an empty artifact is being uploaded.
Expand All @@ -68,21 +66,22 @@ func (ph *TextPushHandler) UpdateCopyOptions(opts *oras.CopyGraphOptions, fetche
committed := &sync.Map{}
opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
return PrintStatus(desc, promptExists, ph.verbose)
model.PrintDescriptor(ph.printer, desc, promptExists)
return nil
}
opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
return PrintStatus(desc, promptUploading, ph.verbose)
model.PrintDescriptor(ph.printer, desc, promptUploading)
return nil
}
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
if err := PrintSuccessorStatus(ctx, desc, fetcher, committed, StatusPrinter(promptSkipped, ph.verbose)); err != nil {
return err
}
return PrintStatus(desc, promptUploaded, ph.verbose)
PrintSuccessorStatus(ctx, desc, fetcher, committed, promptSkipped, ph.printer)
model.PrintDescriptor(ph.printer, desc, promptUploaded)
return nil
}
}

// NewTextAttachHandler returns a new handler for attach command.
func NewTextAttachHandler(verbose bool) AttachHandler {
return NewTextPushHandler(verbose)
func NewTextAttachHandler(printer *Printer) AttachHandler {
return NewTextPushHandler(printer)
}
2 changes: 1 addition & 1 deletion cmd/oras/internal/display/status/track/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
type GraphTarget interface {
oras.GraphTarget
io.Closer
Prompt(desc ocispec.Descriptor, prompt string) error
Prompt(desc ocispec.Descriptor) error
Inner() oras.GraphTarget
}

Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"context"
"errors"
"fmt"
"oras.land/oras/cmd/oras/internal/display/status"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -113,7 +114,8 @@ func runAttach(cmd *cobra.Command, opts *attachOptions) error {
Recommendation: `To attach to an existing artifact, please provide files via argument or annotations via flag "--annotation". Run "oras attach -h" for more options and examples`,
}
}
displayStatus, displayMetadata := display.NewAttachHandler(opts.Template, opts.TTY, opts.Verbose)
printer := status.NewPrinter(opts.Verbose)
displayStatus, displayMetadata := display.NewAttachHandler(opts.Template, opts.TTY, printer)

// prepare manifest
store, err := file.New("")
Expand Down
Loading