From 47111f4abb882621e7a79481574ae1f2f0fea265 Mon Sep 17 00:00:00 2001 From: vg006 Date: Fri, 23 Jan 2026 18:29:04 +0530 Subject: [PATCH 01/10] feat(pkg): Initialize package errors Signed-off-by: vg006 --- pkg/errors/errors.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pkg/errors/errors.go diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 000000000..81b597a5d --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,14 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package errors From c907fda5cb6ee4c8fb308baf13639368713dc1ad Mon Sep 17 00:00:00 2001 From: vg006 Date: Tue, 27 Jan 2026 22:06:40 +0530 Subject: [PATCH 02/10] feat(pkg): Add error type and methods Signed-off-by: vg006 --- pkg/errors/errors.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 81b597a5d..0a5732588 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -12,3 +12,40 @@ // See the License for the specific language governing permissions and // limitations under the License. package errors + +type Error struct { + cause error + message string + hint string +} + +func New(message string) *Error { + return &Error{ + message: message, + } +} + +func (e *Error) WithCause(cause error) *Error { + e.cause = cause + return e +} + +func (e *Error) WithHint(hint string) *Error { + e.hint = hint + return e +} + +func (e *Error) Error() string { + if e.cause != nil { + return e.message + ": " + e.cause.Error() + } + return e.message +} + +func (e *Error) Hint() string { + return e.hint +} + +func (e *Error) Unwrap() error { + return e.cause +} From 9db5509680085bc86717232abd5c3c3f60780ce6 Mon Sep 17 00:00:00 2001 From: vg006 Date: Fri, 30 Jan 2026 08:28:08 +0530 Subject: [PATCH 03/10] refac(pkg): Update error parse with utils Signed-off-by: vg006 --- pkg/errors/errors.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 0a5732588..ca69c8cdf 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -13,6 +13,10 @@ // limitations under the License. package errors +import ( + "github.com/goharbor/harbor-cli/pkg/utils" +) + type Error struct { cause error message string @@ -37,11 +41,16 @@ func (e *Error) WithHint(hint string) *Error { func (e *Error) Error() string { if e.cause != nil { - return e.message + ": " + e.cause.Error() + causeMsg := utils.ParseHarborErrorMsg(e.cause) + return e.message + ": " + causeMsg } return e.message } +func (e *Error) Status() string { + return utils.ParseHarborErrorCode(e.cause) +} + func (e *Error) Hint() string { return e.hint } From f5f5a892b191bca93c60adb7228b85dbbe44934a Mon Sep 17 00:00:00 2001 From: vg006 Date: Fri, 30 Jan 2026 21:15:55 +0530 Subject: [PATCH 04/10] fix(pkg): Add utilities to fix cycle import Signed-off-by: vg006 --- pkg/errors/utils.go | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 pkg/errors/utils.go diff --git a/pkg/errors/utils.go b/pkg/errors/utils.go new file mode 100644 index 000000000..134811aff --- /dev/null +++ b/pkg/errors/utils.go @@ -0,0 +1,60 @@ +package errors + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" +) + +type harborErrorPayload struct { + Errors []struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"errors"` +} + +func isHarborError(err error) *Error { + var e *Error + if as(err, &e) { + return e + } + return nil +} + +func parseHarborErrorMsg(err error) string { + if err == nil { + return "" + } + + val := reflect.ValueOf(err) + if val.Kind() == reflect.Pointer { + val = val.Elem() + } + field := val.FieldByName("Payload") + if field.IsValid() { + payload := field.Interface() + jsonBytes, jsonErr := json.Marshal(payload) + if jsonErr == nil { + var harborErr harborErrorPayload + if unmarshalErr := json.Unmarshal(jsonBytes, &harborErr); unmarshalErr == nil { + if len(harborErr.Errors) > 0 { + return harborErr.Errors[0].Message + } + } + } + } + return fmt.Sprintf("%v", err.Error()) +} + +func parseHarborErrorCode(err error) string { + parts := strings.Split(err.Error(), "]") + if len(parts) >= 2 { + codePart := strings.TrimSpace(parts[1]) + if strings.HasPrefix(codePart, "[") && len(codePart) == 4 { + code := codePart[1:4] + return code + } + } + return "" +} From 74c989233591767a5ae410f64428abb6efa59ec3 Mon Sep 17 00:00:00 2001 From: vg006 Date: Fri, 30 Jan 2026 21:16:26 +0530 Subject: [PATCH 05/10] feat(pkg): Add functions and methods to errors Signed-off-by: vg006 --- pkg/errors/errors.go | 75 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index ca69c8cdf..95d2f137c 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -14,13 +14,18 @@ package errors import ( - "github.com/goharbor/harbor-cli/pkg/utils" + "errors" + "fmt" +) + +var ( + as = errors.As ) type Error struct { cause error message string - hint string + hints []string } func New(message string) *Error { @@ -29,32 +34,74 @@ func New(message string) *Error { } } +func Newf(format string, args ...any) *Error { + return &Error{ + message: fmt.Sprintf(format, args...), + } +} + +func AsError(err error) *Error { + var e *Error + if errors.As(err, &e) { + return e + } + return &Error{ + message: err.Error(), + } +} + +func IsError(err error) bool { + var e *Error + return as(err, &e) +} + +func Cause(err error) error { + if e := isHarborError(err); e != nil { + return e.Cause() + } + return nil +} + +func Hints(err error) []string { + if e := isHarborError(err); e != nil { + return e.Hints() + } + return nil +} + +func Status(err error) string { + if e := isHarborError(err); e != nil { + return e.Status() + } + return "" +} + func (e *Error) WithCause(cause error) *Error { - e.cause = cause + if e.cause == nil { + e.cause = cause + } return e } func (e *Error) WithHint(hint string) *Error { - e.hint = hint + e.hints = append(e.hints, hint) return e } func (e *Error) Error() string { if e.cause != nil { - causeMsg := utils.ParseHarborErrorMsg(e.cause) + causeMsg := parseHarborErrorMsg(e.cause) return e.message + ": " + causeMsg } return e.message } -func (e *Error) Status() string { - return utils.ParseHarborErrorCode(e.cause) -} +func (e *Error) Cause() error { return e.cause } -func (e *Error) Hint() string { - return e.hint -} +func (e *Error) Status() string { return parseHarborErrorCode(e.cause) } -func (e *Error) Unwrap() error { - return e.cause -} +func (e *Error) Hints() []string { return e.hints } + +func (e *Error) Message() string { return e.message } + +func (e *Error) Unwrap() error { return e.cause } From 114940d86ee83157493f36ff6c96dede4ca42234 Mon Sep 17 00:00:00 2001 From: vg006 Date: Wed, 18 Feb 2026 11:43:50 +0530 Subject: [PATCH 06/10] fix(pkg): Update error package and Lint Signed-off-by: vg006 --- pkg/errors/errors.go | 1 + pkg/errors/utils.go | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 95d2f137c..fac02d8f5 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -47,6 +47,7 @@ func AsError(err error) *Error { } return &Error{ message: err.Error(), + cause: err, } } diff --git a/pkg/errors/utils.go b/pkg/errors/utils.go index 134811aff..8b68f667a 100644 --- a/pkg/errors/utils.go +++ b/pkg/errors/utils.go @@ -1,3 +1,16 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package errors import ( From 14841627a4148b451e6ee093b90c8442eaca1b75 Mon Sep 17 00:00:00 2001 From: vg006 Date: Thu, 19 Feb 2026 14:26:44 +0530 Subject: [PATCH 07/10] refac(pkg): Update the Error structure Signed-off-by: vg006 --- pkg/errors/errors.go | 117 +++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 37 deletions(-) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index fac02d8f5..c4b9b4a5c 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -22,32 +22,105 @@ var ( as = errors.As ) +type Frame struct { + Message string + Hints []string +} + type Error struct { - cause error - message string - hints []string + frames []Frame + cause error } func New(message string) *Error { return &Error{ - message: message, + frames: []Frame{{Message: message}}, } } func Newf(format string, args ...any) *Error { return &Error{ - message: fmt.Sprintf(format, args...), + frames: []Frame{{Message: fmt.Sprintf(format, args...)}}, + } +} + +func Wrap(err error, message string) *Error { + e := AsError(err) + e.frames = append([]Frame{{Message: message}}, e.frames...) + return e +} + +func Wrapf(err error, format string, args ...any) *Error { + return Wrap(err, fmt.Sprintf(format, args...)) +} + +func (e *Error) WithHint(hint string) *Error { + if len(e.frames) > 0 { + e.frames[0].Hints = append(e.frames[0].Hints, hint) + } + return e +} + +func (e *Error) WithCause(cause error) *Error { + if e.cause == nil { + e.cause = cause + } + return e +} + +func (e *Error) Error() string { + if len(e.frames) == 0 { + return "" + } + return e.frames[len(e.frames)-1].Message +} + +func (e *Error) Message() string { + if len(e.frames) == 0 { + return "" + } + return e.frames[0].Message +} + +func (e *Error) Errors() []string { + msgs := make([]string, len(e.frames)) + for i, f := range e.frames { + msgs[i] = f.Message + } + return msgs +} + +func (e *Error) Hints() []string { + var all []string + for _, f := range e.frames { + all = append(all, f.Hints...) } + return all } +func (e *Error) Frames() []Frame { + return e.frames +} + +func (e *Error) Cause() error { return e.cause } + +func (e *Error) Status() string { + if e.cause == nil { + return "" + } + return parseHarborErrorCode(e.cause) +} + +func (e *Error) Unwrap() error { return e.cause } + func AsError(err error) *Error { var e *Error if errors.As(err, &e) { return e } return &Error{ - message: err.Error(), - cause: err, + frames: []Frame{{Message: parseHarborErrorMsg(err)}}, + cause: err, } } @@ -76,33 +149,3 @@ func Status(err error) string { } return "" } - -func (e *Error) WithCause(cause error) *Error { - if e.cause == nil { - e.cause = cause - } - return e -} - -func (e *Error) WithHint(hint string) *Error { - e.hints = append(e.hints, hint) - return e -} - -func (e *Error) Error() string { - if e.cause != nil { - causeMsg := parseHarborErrorMsg(e.cause) - return e.message + ": " + causeMsg - } - return e.message -} - -func (e *Error) Cause() error { return e.cause } - -func (e *Error) Status() string { return parseHarborErrorCode(e.cause) } - -func (e *Error) Hints() []string { return e.hints } - -func (e *Error) Message() string { return e.message } - -func (e *Error) Unwrap() error { return e.cause } From e1e5e71e6e49184b4dd8581674b37ffd1e55191e Mon Sep 17 00:00:00 2001 From: vg006 Date: Thu, 19 Feb 2026 14:34:06 +0530 Subject: [PATCH 08/10] feat(pkg): Add tests to the errors package Signed-off-by: vg006 --- pkg/errors/errors_test.go | 268 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 pkg/errors/errors_test.go diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go new file mode 100644 index 000000000..1844207ee --- /dev/null +++ b/pkg/errors/errors_test.go @@ -0,0 +1,268 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package errors_test + +import ( + "encoding/json" + "errors" + "fmt" + "testing" + + harborerr "github.com/goharbor/harbor-cli/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNew_SingleFrame(t *testing.T) { + err := harborerr.New("something went wrong") + require.NotNil(t, err) + assert.Equal(t, "something went wrong", err.Error()) + assert.Equal(t, "something went wrong", err.Message()) + assert.Equal(t, []string{"something went wrong"}, err.Errors()) + assert.Empty(t, err.Hints()) + assert.Nil(t, err.Cause()) +} + +func TestNewf_FormatsMessage(t *testing.T) { + err := harborerr.Newf("resource %q not found", "project-x") + require.NotNil(t, err) + assert.Equal(t, `resource "project-x" not found`, err.Error()) + assert.Equal(t, `resource "project-x" not found`, err.Message()) +} + +func TestWrap_PushesOutermostFrame(t *testing.T) { + root := harborerr.New("root problem") + wrapped := harborerr.Wrap(root, "operation failed") + + assert.Equal(t, "root problem", wrapped.Error()) + assert.Equal(t, "operation failed", wrapped.Message()) + assert.Equal(t, []string{"operation failed", "root problem"}, wrapped.Errors()) +} + +func TestWrapf_FormatsOutermostFrame(t *testing.T) { + root := harborerr.New("auth failed") + wrapped := harborerr.Wrapf(root, "login for user %q", "alice") + + assert.Equal(t, "auth failed", wrapped.Error()) + assert.Equal(t, `login for user "alice"`, wrapped.Message()) + assert.Equal(t, []string{`login for user "alice"`, "auth failed"}, wrapped.Errors()) +} + +func TestWrap_ThreeLevels(t *testing.T) { + root := harborerr.New("db timeout") + mid := harborerr.Wrap(root, "repository unavailable") + top := harborerr.Wrap(mid, "delete artifact failed") + + assert.Equal(t, "db timeout", top.Error()) + assert.Equal(t, "delete artifact failed", top.Message()) + assert.Equal(t, []string{"delete artifact failed", "repository unavailable", "db timeout"}, top.Errors()) +} + +func TestWrap_PlainStdlibError(t *testing.T) { + plain := errors.New("network error") + wrapped := harborerr.Wrap(plain, "could not reach registry") + + assert.Equal(t, "network error", wrapped.Error()) + assert.Equal(t, "could not reach registry", wrapped.Message()) + assert.Equal(t, []string{"could not reach registry", "network error"}, wrapped.Errors()) +} + +func TestErrors_SingleFrame(t *testing.T) { + err := harborerr.New("standalone") + assert.Equal(t, []string{"standalone"}, err.Errors()) +} + +func TestErrors_OutermostFirst(t *testing.T) { + err := harborerr.Wrap(harborerr.Wrap(harborerr.New("level-0"), "level-1"), "level-2") + assert.Equal(t, []string{"level-2", "level-1", "level-0"}, err.Errors()) +} + +func TestWithHint_AttachesToOutermostFrame(t *testing.T) { + err := harborerr.New("error").WithHint("try again later") + assert.Equal(t, []string{"try again later"}, err.Hints()) +} + +func TestWithHint_MultipleHintsOnSameFrame(t *testing.T) { + err := harborerr.New("error"). + WithHint("hint one"). + WithHint("hint two"). + WithHint("hint three") + assert.Equal(t, []string{"hint one", "hint two", "hint three"}, err.Hints()) +} + +func TestHints_AcrossFrames_OutermostFirst(t *testing.T) { + root := harborerr.New("root").WithHint("root-hint") + top := harborerr.Wrap(root, "top").WithHint("top-hint") + + assert.Equal(t, []string{"top-hint", "root-hint"}, top.Hints()) +} + +func TestHints_PackageLevel_HarborError(t *testing.T) { + err := harborerr.New("error").WithHint("check config") + assert.Equal(t, []string{"check config"}, harborerr.Hints(err)) +} + +func TestHints_PackageLevel_PlainError(t *testing.T) { + assert.Nil(t, harborerr.Hints(errors.New("plain"))) +} + +func TestIsError_True_DirectHarborError(t *testing.T) { + assert.True(t, harborerr.IsError(harborerr.New("err"))) +} + +func TestIsError_True_WrappedWithFmtErrorf(t *testing.T) { + wrapped := fmt.Errorf("outer: %w", harborerr.New("inner")) + assert.True(t, harborerr.IsError(wrapped)) +} + +func TestIsError_False_PlainError(t *testing.T) { + assert.False(t, harborerr.IsError(errors.New("plain"))) +} + +func TestAsError_FromHarborError_ReturnsSame(t *testing.T) { + original := harborerr.New("original") + wrapped := fmt.Errorf("wrapped: %w", original) + + result := harborerr.AsError(wrapped) + require.NotNil(t, result) + assert.Equal(t, "original", result.Error()) +} + +func TestAsError_FromPlainError_WrapsIntoSingleFrame(t *testing.T) { + plain := errors.New("plain error") + result := harborerr.AsError(plain) + require.NotNil(t, result) + assert.Equal(t, "plain error", result.Error()) + assert.Equal(t, []string{"plain error"}, result.Errors()) + assert.Equal(t, plain, result.Cause()) +} + +func TestWithCause_AttachesCauseForUnwrap(t *testing.T) { + sentinel := errors.New("sentinel") + err := harborerr.New("wrapper").WithCause(sentinel) + + assert.Equal(t, sentinel, err.Cause()) + assert.True(t, errors.Is(err, sentinel)) +} + +func TestWithCause_OnlyFirstCauseIsStored(t *testing.T) { + first := errors.New("first") + second := errors.New("second") + err := harborerr.New("e").WithCause(first).WithCause(second) + assert.Equal(t, first, err.Cause()) +} + +func TestCause_PackageLevel_HarborError(t *testing.T) { + sentinel := errors.New("root") + err := harborerr.New("top").WithCause(sentinel) + assert.Equal(t, sentinel, harborerr.Cause(err)) +} + +func TestCause_PackageLevel_PlainError(t *testing.T) { + assert.Nil(t, harborerr.Cause(errors.New("plain"))) +} + +func TestUnwrap_ErrorsAs_FindsOuterFrame(t *testing.T) { + inner := harborerr.New("inner") + outer := harborerr.New("outer").WithCause(inner) + + var target *harborerr.Error + assert.True(t, errors.As(outer, &target)) + assert.Equal(t, "outer", target.Error()) +} + +func TestStatus_PlainError_ReturnsEmpty(t *testing.T) { + assert.Equal(t, "", harborerr.Status(errors.New("plain"))) +} + +func TestStatus_NoCause_ReturnsEmpty(t *testing.T) { + assert.Equal(t, "", harborerr.Status(harborerr.New("no cause"))) +} + +type harborPayloadError struct { + Payload *harborPayloadBody +} + +type harborPayloadBody struct { + Errors []harborPayloadEntry `json:"errors"` +} + +type harborPayloadEntry struct { + Code string `json:"code"` + Message string `json:"message"` +} + +func (h *harborPayloadError) Error() string { + if h.Payload != nil && len(h.Payload.Errors) > 0 { + b, _ := json.Marshal(h.Payload) + return fmt.Sprintf("[%s] %s", h.Payload.Errors[0].Code, string(b)) + } + return "harbor error" +} + +func TestWrap_HarborPayloadCause_ExtractsMessage(t *testing.T) { + apiErr := &harborPayloadError{ + Payload: &harborPayloadBody{ + Errors: []harborPayloadEntry{ + {Code: "NOT_FOUND", Message: "repository does not exist"}, + }, + }, + } + err := harborerr.Wrap(apiErr, "delete artifact failed") + + assert.Equal(t, "repository does not exist", err.Error()) + assert.Equal(t, "delete artifact failed", err.Message()) + assert.Equal(t, []string{"delete artifact failed", "repository does not exist"}, err.Errors()) +} + +func TestFrames_SingleFrame(t *testing.T) { + err := harborerr.New("root").WithHint("hint-a") + frames := err.Frames() + require.Len(t, frames, 1) + assert.Equal(t, "root", frames[0].Message) + assert.Equal(t, []string{"hint-a"}, frames[0].Hints) +} + +func TestFrames_MultipleFrames_OutermostFirst(t *testing.T) { + root := harborerr.New("root").WithHint("root-hint") + mid := harborerr.Wrap(root, "mid").WithHint("mid-hint") + top := harborerr.Wrap(mid, "top").WithHint("top-hint") + + frames := top.Frames() + require.Len(t, frames, 3) + assert.Equal(t, "top", frames[0].Message) + assert.Equal(t, []string{"top-hint"}, frames[0].Hints) + assert.Equal(t, "mid", frames[1].Message) + assert.Equal(t, []string{"mid-hint"}, frames[1].Hints) + assert.Equal(t, "root", frames[2].Message) + assert.Equal(t, []string{"root-hint"}, frames[2].Hints) +} + +func TestFrames_NoHints(t *testing.T) { + err := harborerr.Wrap(harborerr.New("root"), "top") + frames := err.Frames() + require.Len(t, frames, 2) + assert.Empty(t, frames[0].Hints) + assert.Empty(t, frames[1].Hints) +} + +func TestChaining_WrapWithHints(t *testing.T) { + root := harborerr.New("connection refused").WithHint("check firewall rules") + top := harborerr.Wrap(root, "could not reach registry").WithHint("verify server URL") + + assert.Equal(t, "connection refused", top.Error()) + assert.Equal(t, "could not reach registry", top.Message()) + assert.Equal(t, []string{"could not reach registry", "connection refused"}, top.Errors()) + assert.Equal(t, []string{"verify server URL", "check firewall rules"}, top.Hints()) +} From f462a9b349577290c51a799641c20659a1d57a1e Mon Sep 17 00:00:00 2001 From: vg006 Date: Tue, 17 Mar 2026 19:37:46 +0530 Subject: [PATCH 09/10] refac(pkg): update the methods, tests and styles Signed-off-by: vg006 --- pkg/errors/errors.go | 106 +++++++++--- pkg/errors/errors_test.go | 343 ++++++++++++++++++++------------------ pkg/views/styles.go | 12 ++ 3 files changed, 278 insertions(+), 183 deletions(-) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index c4b9b4a5c..a35b671b9 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -16,6 +16,10 @@ package errors import ( "errors" "fmt" + "strings" + + "github.com/charmbracelet/lipgloss/tree" + "github.com/goharbor/harbor-cli/pkg/views" ) var ( @@ -32,9 +36,9 @@ type Error struct { cause error } -func New(message string) *Error { +func New(message string, hints ...string) *Error { return &Error{ - frames: []Frame{{Message: message}}, + frames: []Frame{{Message: message, Hints: hints}}, } } @@ -44,27 +48,15 @@ func Newf(format string, args ...any) *Error { } } -func Wrap(err error, message string) *Error { - e := AsError(err) - e.frames = append([]Frame{{Message: message}}, e.frames...) - return e -} - -func Wrapf(err error, format string, args ...any) *Error { - return Wrap(err, fmt.Sprintf(format, args...)) -} - -func (e *Error) WithHint(hint string) *Error { - if len(e.frames) > 0 { - e.frames[0].Hints = append(e.frames[0].Hints, hint) +func NewWithCause(cause error, message string, hints ...string) *Error { + return &Error{ + cause: cause, + frames: []Frame{{Message: message, Hints: hints}}, } - return e } -func (e *Error) WithCause(cause error) *Error { - if e.cause == nil { - e.cause = cause - } +func (e *Error) WithMessage(message string, hints ...string) *Error { + e.frames = append(e.frames, Frame{Message: message, Hints: hints}) return e } @@ -72,7 +64,78 @@ func (e *Error) Error() string { if len(e.frames) == 0 { return "" } - return e.frames[len(e.frames)-1].Message + + var parts []string + + rootFrame := e.frames[0] + parts = append(parts, views.ErrCauseStyle.Render(rootFrame.Message)) + + if e.cause != nil { + if code := parseHarborErrorCode(e.cause); code != "" { + parts = append(parts, + views.ErrTitleStyle.Render("Code: ")+views.ErrCauseStyle.Render(code), + ) + } + } + + if e.cause != nil { + causeText := e.cause.Error() + if he := isHarborError(e.cause); he != nil { + causeText = he.Message() + } + + cause := views.ErrTitleStyle.Render("Cause: ") + causeText = views.ErrCauseStyle.Render(causeText) + causeTree := tree.Root(cause + causeText). + Enumerator(tree.RoundedEnumerator). + EnumeratorStyle(views.ErrEnumeratorStyle). + ItemStyle(views.ErrHintStyle) + + if he := isHarborError(e.cause); he != nil { + for _, h := range he.Hints() { + causeTree.Child(h) + } + } + parts = append(parts, causeTree.String()) + } + + if len(rootFrame.Hints) > 0 { + hintsTree := tree.New(). + Root("Hints:"). + RootStyle(views.ErrTitleStyle). + Enumerator(tree.RoundedEnumerator). + EnumeratorStyle(views.ErrEnumeratorStyle). + ItemStyle(views.ErrHintStyle) + + for _, h := range rootFrame.Hints { + hintsTree.Child(h) + } + parts = append(parts, hintsTree.String()) + } + + if len(e.frames) > 1 { + msgTree := tree.New(). + Root("Messages:"). + RootStyle(views.ErrTitleStyle). + Enumerator(tree.RoundedEnumerator). + EnumeratorStyle(views.ErrEnumeratorStyle). + ItemStyle(views.ErrTitleStyle) + + for _, f := range e.frames[1:] { + msgWithHints := tree.Root(f.Message). + RootStyle(views.ErrTitleStyle). + Enumerator(tree.RoundedEnumerator). + EnumeratorStyle(views.ErrEnumeratorStyle). + ItemStyle(views.ErrHintStyle) + for _, h := range f.Hints { + msgWithHints.Child(h) + } + msgTree.Child(msgWithHints) + } + parts = append(parts, msgTree.String()) + } + + return strings.Join(parts, "\n") } func (e *Error) Message() string { @@ -112,7 +175,6 @@ func (e *Error) Status() string { } func (e *Error) Unwrap() error { return e.cause } - func AsError(err error) *Error { var e *Error if errors.As(err, &e) { diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go index 1844207ee..403558d4d 100644 --- a/pkg/errors/errors_test.go +++ b/pkg/errors/errors_test.go @@ -14,7 +14,6 @@ package errors_test import ( - "encoding/json" "errors" "fmt" "testing" @@ -24,245 +23,267 @@ import ( "github.com/stretchr/testify/require" ) -func TestNew_SingleFrame(t *testing.T) { +func TestNew_MessageOnly(t *testing.T) { err := harborerr.New("something went wrong") require.NotNil(t, err) - assert.Equal(t, "something went wrong", err.Error()) + + output := err.Error() + assert.Contains(t, output, "something went wrong") assert.Equal(t, "something went wrong", err.Message()) assert.Equal(t, []string{"something went wrong"}, err.Errors()) assert.Empty(t, err.Hints()) assert.Nil(t, err.Cause()) } +func TestNew_MessageWithHints(t *testing.T) { + err := harborerr.New("auth failed", "check your credentials", "ensure token is not expired") + require.NotNil(t, err) + + output := err.Error() + assert.Contains(t, output, "auth failed") + assert.Contains(t, output, "check your credentials") + assert.Contains(t, output, "ensure token is not expired") + + assert.Equal(t, "auth failed", err.Message()) + assert.Equal(t, []string{"check your credentials", "ensure token is not expired"}, err.Hints()) + assert.Nil(t, err.Cause()) +} + func TestNewf_FormatsMessage(t *testing.T) { err := harborerr.Newf("resource %q not found", "project-x") require.NotNil(t, err) - assert.Equal(t, `resource "project-x" not found`, err.Error()) + + output := err.Error() + assert.Contains(t, output, `resource "project-x" not found`) assert.Equal(t, `resource "project-x" not found`, err.Message()) + assert.Empty(t, err.Hints()) } -func TestWrap_PushesOutermostFrame(t *testing.T) { - root := harborerr.New("root problem") - wrapped := harborerr.Wrap(root, "operation failed") - - assert.Equal(t, "root problem", wrapped.Error()) - assert.Equal(t, "operation failed", wrapped.Message()) - assert.Equal(t, []string{"operation failed", "root problem"}, wrapped.Errors()) -} +func TestNewWithCause_Basic(t *testing.T) { + cause := errors.New("connection refused") + err := harborerr.NewWithCause(cause, "could not reach registry") -func TestWrapf_FormatsOutermostFrame(t *testing.T) { - root := harborerr.New("auth failed") - wrapped := harborerr.Wrapf(root, "login for user %q", "alice") + output := err.Error() + assert.Contains(t, output, "could not reach registry") + assert.Contains(t, output, "Cause:") + assert.Contains(t, output, "connection refused") + assert.NotContains(t, output, "Messages:") - assert.Equal(t, "auth failed", wrapped.Error()) - assert.Equal(t, `login for user "alice"`, wrapped.Message()) - assert.Equal(t, []string{`login for user "alice"`, "auth failed"}, wrapped.Errors()) + assert.Equal(t, cause, err.Cause()) + assert.Equal(t, "could not reach registry", err.Message()) } -func TestWrap_ThreeLevels(t *testing.T) { - root := harborerr.New("db timeout") - mid := harborerr.Wrap(root, "repository unavailable") - top := harborerr.Wrap(mid, "delete artifact failed") +func TestNewWithCause_WithHints(t *testing.T) { + cause := errors.New("401 Unauthorized") + err := harborerr.NewWithCause(cause, "authentication failed", + "ensure your credentials are correct", + "run `harbor login` to re-authenticate", + ) + + output := err.Error() + assert.Contains(t, output, "authentication failed") + assert.Contains(t, output, "ensure your credentials are correct") + assert.Contains(t, output, "run `harbor login` to re-authenticate") + assert.Contains(t, output, "Cause:") + assert.Contains(t, output, "401 Unauthorized") + assert.NotContains(t, output, "Messages:") - assert.Equal(t, "db timeout", top.Error()) - assert.Equal(t, "delete artifact failed", top.Message()) - assert.Equal(t, []string{"delete artifact failed", "repository unavailable", "db timeout"}, top.Errors()) + assert.Equal(t, cause, err.Cause()) + assert.Equal(t, []string{ + "ensure your credentials are correct", + "run `harbor login` to re-authenticate", + }, err.Hints()) } -func TestWrap_PlainStdlibError(t *testing.T) { - plain := errors.New("network error") - wrapped := harborerr.Wrap(plain, "could not reach registry") +func TestNewWithCause_CauseIsHarborError_ShowsCauseHintsInHeader(t *testing.T) { + inner := harborerr.New("db connection lost", "check that the database is running") + outer := harborerr.NewWithCause(inner, "repository unavailable") - assert.Equal(t, "network error", wrapped.Error()) - assert.Equal(t, "could not reach registry", wrapped.Message()) - assert.Equal(t, []string{"could not reach registry", "network error"}, wrapped.Errors()) + output := outer.Error() + assert.Contains(t, output, "repository unavailable") + assert.Contains(t, output, "check that the database is running") + assert.NotContains(t, output, "Messages:") } -func TestErrors_SingleFrame(t *testing.T) { - err := harborerr.New("standalone") - assert.Equal(t, []string{"standalone"}, err.Errors()) -} +func TestWithMessage_AppendsFrame(t *testing.T) { + err := harborerr.NewWithCause( + errors.New("dial tcp 127.0.0.1:5432: connect: connection refused"), + "repository unavailable", "check that the database is running", + ).WithMessage("failed to delete artifact", + "retry after resolving the underlying issue", + "use --force to skip confirmation prompts", + ) -func TestErrors_OutermostFirst(t *testing.T) { - err := harborerr.Wrap(harborerr.Wrap(harborerr.New("level-0"), "level-1"), "level-2") - assert.Equal(t, []string{"level-2", "level-1", "level-0"}, err.Errors()) -} + output := err.Error() + assert.Contains(t, output, "repository unavailable") + assert.Contains(t, output, "check that the database is running") + assert.Contains(t, output, "Cause:") + assert.Contains(t, output, "connection refused") + assert.Contains(t, output, "Messages:") + assert.Contains(t, output, "failed to delete artifact") + assert.Contains(t, output, "retry after resolving the underlying issue") + assert.Contains(t, output, "use --force to skip confirmation prompts") -func TestWithHint_AttachesToOutermostFrame(t *testing.T) { - err := harborerr.New("error").WithHint("try again later") - assert.Equal(t, []string{"try again later"}, err.Hints()) + assert.Equal(t, "repository unavailable", err.Message()) + assert.Len(t, err.Frames(), 2) } -func TestWithHint_MultipleHintsOnSameFrame(t *testing.T) { - err := harborerr.New("error"). - WithHint("hint one"). - WithHint("hint two"). - WithHint("hint three") - assert.Equal(t, []string{"hint one", "hint two", "hint three"}, err.Hints()) +func TestWithMessage_MultipleFrames(t *testing.T) { + err := harborerr.New("step 1"). + WithMessage("step 2", "hint A"). + WithMessage("step 3", "hint B", "hint C") + + assert.Len(t, err.Frames(), 3) + assert.Equal(t, "step 1", err.Frames()[0].Message) + assert.Equal(t, "step 2", err.Frames()[1].Message) + assert.Equal(t, []string{"hint A"}, err.Frames()[1].Hints) + assert.Equal(t, "step 3", err.Frames()[2].Message) + assert.Equal(t, []string{"hint B", "hint C"}, err.Frames()[2].Hints) } -func TestHints_AcrossFrames_OutermostFirst(t *testing.T) { - root := harborerr.New("root").WithHint("root-hint") - top := harborerr.Wrap(root, "top").WithHint("top-hint") +func TestWithMessage_NoCause_NoRootHeader(t *testing.T) { + err := harborerr.New("first").WithMessage("second", "a hint") - assert.Equal(t, []string{"top-hint", "root-hint"}, top.Hints()) + output := err.Error() + assert.NotContains(t, output, "Root:") + assert.NotContains(t, output, "Code:") + assert.Contains(t, output, "first") + assert.Contains(t, output, "second") + assert.Contains(t, output, "a hint") } -func TestHints_PackageLevel_HarborError(t *testing.T) { - err := harborerr.New("error").WithHint("check config") - assert.Equal(t, []string{"check config"}, harborerr.Hints(err)) +func TestError_EmptyFrames(t *testing.T) { + err := &harborerr.Error{} + assert.Equal(t, "", err.Error()) } -func TestHints_PackageLevel_PlainError(t *testing.T) { - assert.Nil(t, harborerr.Hints(errors.New("plain"))) -} +func TestError_NoCause_SingleFrameNoHints(t *testing.T) { + err := harborerr.New("operation not supported") + output := err.Error() -func TestIsError_True_DirectHarborError(t *testing.T) { - assert.True(t, harborerr.IsError(harborerr.New("err"))) + assert.NotContains(t, output, "Root:") + assert.NotContains(t, output, "Code:") + assert.Contains(t, output, "operation not supported") } -func TestIsError_True_WrappedWithFmtErrorf(t *testing.T) { - wrapped := fmt.Errorf("outer: %w", harborerr.New("inner")) - assert.True(t, harborerr.IsError(wrapped)) +func TestError_NoCause_SingleFrameWithHints(t *testing.T) { + err := harborerr.New("plugin system not implemented", + "this command is a placeholder for future plugin management", + ) + + output := err.Error() + assert.NotContains(t, output, "Root:") + assert.Contains(t, output, "plugin system not implemented") + assert.Contains(t, output, "this command is a placeholder for future plugin management") } -func TestIsError_False_PlainError(t *testing.T) { - assert.False(t, harborerr.IsError(errors.New("plain"))) +func TestMessage_ReturnsFirstFrame(t *testing.T) { + err := harborerr.New("first").WithMessage("second") + assert.Equal(t, "first", err.Message()) } -func TestAsError_FromHarborError_ReturnsSame(t *testing.T) { - original := harborerr.New("original") - wrapped := fmt.Errorf("wrapped: %w", original) +func TestErrors_AllMessages(t *testing.T) { + err := harborerr.New("a").WithMessage("b").WithMessage("c") + assert.Equal(t, []string{"a", "b", "c"}, err.Errors()) +} - result := harborerr.AsError(wrapped) - require.NotNil(t, result) - assert.Equal(t, "original", result.Error()) +func TestHints_AcrossAllFrames(t *testing.T) { + err := harborerr.New("m1", "h1", "h2").WithMessage("m2", "h3") + assert.Equal(t, []string{"h1", "h2", "h3"}, err.Hints()) } -func TestAsError_FromPlainError_WrapsIntoSingleFrame(t *testing.T) { - plain := errors.New("plain error") - result := harborerr.AsError(plain) - require.NotNil(t, result) - assert.Equal(t, "plain error", result.Error()) - assert.Equal(t, []string{"plain error"}, result.Errors()) - assert.Equal(t, plain, result.Cause()) +func TestFrames_ReturnsAll(t *testing.T) { + err := harborerr.New("root", "hint-a") + frames := err.Frames() + require.Len(t, frames, 1) + assert.Equal(t, "root", frames[0].Message) + assert.Equal(t, []string{"hint-a"}, frames[0].Hints) } -func TestWithCause_AttachesCauseForUnwrap(t *testing.T) { +func TestCause_ReturnsUnderlyingError(t *testing.T) { sentinel := errors.New("sentinel") - err := harborerr.New("wrapper").WithCause(sentinel) - + err := harborerr.NewWithCause(sentinel, "wrapper") assert.Equal(t, sentinel, err.Cause()) - assert.True(t, errors.Is(err, sentinel)) } -func TestWithCause_OnlyFirstCauseIsStored(t *testing.T) { - first := errors.New("first") - second := errors.New("second") - err := harborerr.New("e").WithCause(first).WithCause(second) - assert.Equal(t, first, err.Cause()) +func TestCause_NilWhenNoCause(t *testing.T) { + err := harborerr.New("standalone") + assert.Nil(t, err.Cause()) } -func TestCause_PackageLevel_HarborError(t *testing.T) { - sentinel := errors.New("root") - err := harborerr.New("top").WithCause(sentinel) - assert.Equal(t, sentinel, harborerr.Cause(err)) +func TestStatus_NoCause_ReturnsEmpty(t *testing.T) { + assert.Equal(t, "", harborerr.Status(harborerr.New("no cause"))) } -func TestCause_PackageLevel_PlainError(t *testing.T) { - assert.Nil(t, harborerr.Cause(errors.New("plain"))) +func TestStatus_PlainError_ReturnsEmpty(t *testing.T) { + assert.Equal(t, "", harborerr.Status(errors.New("plain"))) } -func TestUnwrap_ErrorsAs_FindsOuterFrame(t *testing.T) { +func TestUnwrap_ReturnsTheCause(t *testing.T) { + sentinel := errors.New("sentinel") + err := harborerr.NewWithCause(sentinel, "wrapper") + + assert.Equal(t, sentinel, err.Unwrap()) + assert.True(t, errors.Is(err, sentinel)) +} + +func TestErrorsAs_FindsHarborError(t *testing.T) { inner := harborerr.New("inner") - outer := harborerr.New("outer").WithCause(inner) + outer := harborerr.NewWithCause(inner, "outer") var target *harborerr.Error assert.True(t, errors.As(outer, &target)) - assert.Equal(t, "outer", target.Error()) + assert.Contains(t, target.Error(), "inner") + assert.Contains(t, target.Error(), "outer") } -func TestStatus_PlainError_ReturnsEmpty(t *testing.T) { - assert.Equal(t, "", harborerr.Status(errors.New("plain"))) +func TestIsError_True_DirectHarborError(t *testing.T) { + assert.True(t, harborerr.IsError(harborerr.New("err"))) } -func TestStatus_NoCause_ReturnsEmpty(t *testing.T) { - assert.Equal(t, "", harborerr.Status(harborerr.New("no cause"))) +func TestIsError_True_WrappedWithFmtErrorf(t *testing.T) { + wrapped := fmt.Errorf("outer: %w", harborerr.New("inner")) + assert.True(t, harborerr.IsError(wrapped)) } -type harborPayloadError struct { - Payload *harborPayloadBody +func TestIsError_False_PlainError(t *testing.T) { + assert.False(t, harborerr.IsError(errors.New("plain"))) } -type harborPayloadBody struct { - Errors []harborPayloadEntry `json:"errors"` -} +func TestAsError_FromHarborError_ReturnsSame(t *testing.T) { + original := harborerr.New("original") + wrapped := fmt.Errorf("wrapped: %w", original) -type harborPayloadEntry struct { - Code string `json:"code"` - Message string `json:"message"` + result := harborerr.AsError(wrapped) + require.NotNil(t, result) + assert.Contains(t, result.Error(), "original") } -func (h *harborPayloadError) Error() string { - if h.Payload != nil && len(h.Payload.Errors) > 0 { - b, _ := json.Marshal(h.Payload) - return fmt.Sprintf("[%s] %s", h.Payload.Errors[0].Code, string(b)) - } - return "harbor error" -} +func TestAsError_FromPlainError_WrapsIntoSingleFrame(t *testing.T) { + plain := errors.New("plain error") + result := harborerr.AsError(plain) + require.NotNil(t, result) -func TestWrap_HarborPayloadCause_ExtractsMessage(t *testing.T) { - apiErr := &harborPayloadError{ - Payload: &harborPayloadBody{ - Errors: []harborPayloadEntry{ - {Code: "NOT_FOUND", Message: "repository does not exist"}, - }, - }, - } - err := harborerr.Wrap(apiErr, "delete artifact failed") - - assert.Equal(t, "repository does not exist", err.Error()) - assert.Equal(t, "delete artifact failed", err.Message()) - assert.Equal(t, []string{"delete artifact failed", "repository does not exist"}, err.Errors()) + assert.Contains(t, result.Error(), "plain error") + assert.Equal(t, []string{"plain error"}, result.Errors()) + assert.Equal(t, plain, result.Cause()) } -func TestFrames_SingleFrame(t *testing.T) { - err := harborerr.New("root").WithHint("hint-a") - frames := err.Frames() - require.Len(t, frames, 1) - assert.Equal(t, "root", frames[0].Message) - assert.Equal(t, []string{"hint-a"}, frames[0].Hints) +func TestCause_PackageLevel_HarborError(t *testing.T) { + sentinel := errors.New("root") + err := harborerr.NewWithCause(sentinel, "top") + assert.Equal(t, sentinel, harborerr.Cause(err)) } -func TestFrames_MultipleFrames_OutermostFirst(t *testing.T) { - root := harborerr.New("root").WithHint("root-hint") - mid := harborerr.Wrap(root, "mid").WithHint("mid-hint") - top := harborerr.Wrap(mid, "top").WithHint("top-hint") - - frames := top.Frames() - require.Len(t, frames, 3) - assert.Equal(t, "top", frames[0].Message) - assert.Equal(t, []string{"top-hint"}, frames[0].Hints) - assert.Equal(t, "mid", frames[1].Message) - assert.Equal(t, []string{"mid-hint"}, frames[1].Hints) - assert.Equal(t, "root", frames[2].Message) - assert.Equal(t, []string{"root-hint"}, frames[2].Hints) +func TestCause_PackageLevel_PlainError(t *testing.T) { + assert.Nil(t, harborerr.Cause(errors.New("plain"))) } -func TestFrames_NoHints(t *testing.T) { - err := harborerr.Wrap(harborerr.New("root"), "top") - frames := err.Frames() - require.Len(t, frames, 2) - assert.Empty(t, frames[0].Hints) - assert.Empty(t, frames[1].Hints) +func TestHints_PackageLevel_HarborError(t *testing.T) { + err := harborerr.New("error", "check config") + assert.Equal(t, []string{"check config"}, harborerr.Hints(err)) } -func TestChaining_WrapWithHints(t *testing.T) { - root := harborerr.New("connection refused").WithHint("check firewall rules") - top := harborerr.Wrap(root, "could not reach registry").WithHint("verify server URL") - - assert.Equal(t, "connection refused", top.Error()) - assert.Equal(t, "could not reach registry", top.Message()) - assert.Equal(t, []string{"could not reach registry", "connection refused"}, top.Errors()) - assert.Equal(t, []string{"verify server URL", "check firewall rules"}, top.Hints()) +func TestHints_PackageLevel_PlainError(t *testing.T) { + assert.Nil(t, harborerr.Hints(errors.New("plain"))) } diff --git a/pkg/views/styles.go b/pkg/views/styles.go index 240ef23bc..1bc64ed42 100644 --- a/pkg/views/styles.go +++ b/pkg/views/styles.go @@ -37,6 +37,18 @@ var ( var BaseStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()).Padding(0, 1) +var ( + ErrCauseStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("203")). + Bold(true) + ErrTitleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("214")) + ErrHintStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("44")) + ErrEnumeratorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("242")).MarginRight(1) +) + func RedText(strs ...string) string { var msg strings.Builder for _, str := range strs { From e7afe9beaac080c1d3ec1706440f585e35ede62b Mon Sep 17 00:00:00 2001 From: vg006 Date: Mon, 27 Apr 2026 20:51:20 +0530 Subject: [PATCH 10/10] fix(pkg): resolve review comments in errors Signed-off-by: vg006 --- pkg/errors/errors.go | 14 +++++++++++++- pkg/errors/utils.go | 17 ++++++++++++++++- pkg/views/styles.go | 3 ++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index a35b671b9..aea38a441 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -162,7 +162,19 @@ func (e *Error) Hints() []string { } func (e *Error) Frames() []Frame { - return e.frames + if len(e.frames) == 0 { + return nil + } + + frames := make([]Frame, len(e.frames)) + for i, f := range e.frames { + frames[i].Message = f.Message + if len(f.Hints) > 0 { + frames[i].Hints = append([]string(nil), f.Hints...) + } + } + + return frames } func (e *Error) Cause() error { return e.cause } diff --git a/pkg/errors/utils.go b/pkg/errors/utils.go index 8b68f667a..b24b7a6ea 100644 --- a/pkg/errors/utils.go +++ b/pkg/errors/utils.go @@ -17,6 +17,7 @@ import ( "encoding/json" "fmt" "reflect" + "regexp" "strings" ) @@ -42,8 +43,14 @@ func parseHarborErrorMsg(err error) string { val := reflect.ValueOf(err) if val.Kind() == reflect.Pointer { + if val.IsNil() { + return err.Error() + } val = val.Elem() } + if val.Kind() != reflect.Struct { + return err.Error() + } field := val.FieldByName("Payload") if field.IsValid() { payload := field.Interface() @@ -61,7 +68,9 @@ func parseHarborErrorMsg(err error) string { } func parseHarborErrorCode(err error) string { - parts := strings.Split(err.Error(), "]") + errStr := err.Error() + + parts := strings.Split(errStr, "]") if len(parts) >= 2 { codePart := strings.TrimSpace(parts[1]) if strings.HasPrefix(codePart, "[") && len(codePart) == 4 { @@ -69,5 +78,11 @@ func parseHarborErrorCode(err error) string { return code } } + + re := regexp.MustCompile(`\(status\s+(\d{3})\)`) + if matches := re.FindStringSubmatch(errStr); len(matches) > 1 { + return matches[1] + } + return "" } diff --git a/pkg/views/styles.go b/pkg/views/styles.go index 1bc64ed42..4ce188594 100644 --- a/pkg/views/styles.go +++ b/pkg/views/styles.go @@ -46,7 +46,8 @@ var ( ErrHintStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("44")) ErrEnumeratorStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("242")).MarginRight(1) + Foreground(lipgloss.Color("242")). + MarginRight(1) ) func RedText(strs ...string) string {