feat: unrealted histories error handling (#2642)

* comments added
* return proper status and message for unrelated history
* code cleanup
* requested changes
* feat: unrealted histories error handling
CODE-2402
Enver Biševac 2024-09-06 11:41:21 +00:00 committed by Harness
parent 88485ade75
commit 61c794da0b
7 changed files with 97 additions and 20 deletions

View File

@ -21,6 +21,7 @@ import (
"github.com/harness/gitness/app/api/controller"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/git"
"github.com/harness/gitness/git/api"
"github.com/harness/gitness/types/enum"
)
@ -57,6 +58,14 @@ func (c *Controller) MergeCheck(
HeadBranch: info.HeadRef,
})
if err != nil {
// git.Merge works with commits and error is not user-friendly
// and here we are modify base-ref and head-ref with user input
// values.
if uErr := api.AsUnrelatedHistoriesError(err); uErr != nil {
uErr.BaseRef = info.BaseRef
uErr.HeadRef = info.HeadRef
return MergeCheck{}, uErr
}
return MergeCheck{}, fmt.Errorf("merge check execution failed: %w", err)
}
if len(mergeOutput.ConflictFiles) > 0 {

View File

@ -23,6 +23,7 @@ import (
"github.com/harness/gitness/app/api/controller/repo"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/errors"
gittypes "github.com/harness/gitness/git/api"
)
@ -110,6 +111,13 @@ func HandleDiffStats(repoCtrl *repo.Controller) http.HandlerFunc {
path := request.GetOptionalRemainderFromPath(r)
output, err := repoCtrl.DiffStats(ctx, session, repoRef, path)
if uErr := gittypes.AsUnrelatedHistoriesError(err); uErr != nil {
render.JSON(w, http.StatusOK, &usererror.Error{
Message: uErr.Error(),
Values: uErr.Map(),
})
return
}
if err != nil {
render.TranslatedUserError(ctx, w, err)
return

View File

@ -22,7 +22,9 @@ import (
"os"
"strconv"
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/api"
"github.com/harness/gitness/types"
"github.com/rs/zerolog/log"
@ -75,6 +77,15 @@ func JSONArrayDynamic[T comparable](ctx context.Context, w http.ResponseWriter,
}
if err != nil {
// based on discussion unrelated histories error should return
// StatusOK on diff.
if uErr := api.AsUnrelatedHistoriesError(err); uErr != nil {
JSON(w, http.StatusOK, &usererror.Error{
Message: uErr.Error(),
Values: uErr.Map(),
})
return
}
// User canceled the request - no need to do anything
if errors.Is(err, context.Canceled) {
return

View File

@ -25,6 +25,7 @@ import (
"github.com/harness/gitness/app/services/webhook"
"github.com/harness/gitness/blob"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/api"
"github.com/harness/gitness/lock"
"github.com/harness/gitness/store"
"github.com/harness/gitness/types/check"
@ -37,6 +38,7 @@ func Translate(ctx context.Context, err error) *Error {
rError *Error
checkError *check.ValidationError
appError *errors.Error
unrelatedHistoriesErr *api.UnrelatedHistoriesError
maxBytesErr *http.MaxBytesError
codeOwnersTooLargeError *codeowners.TooLargeError
codeOwnersFileParseError *codeowners.FileParseError
@ -96,6 +98,12 @@ func Translate(ctx context.Context, err error) *Error {
appError.Message,
appError.Details,
)
case errors.As(err, &unrelatedHistoriesErr):
return NewWithPayload(
http.StatusBadRequest,
err.Error(),
unrelatedHistoriesErr.Map(),
)
// webhook errors
case errors.Is(err, webhook.ErrWebhookNotRetriggerable):

View File

@ -19,7 +19,6 @@ import (
"strings"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/enum"
"github.com/rs/zerolog/log"
)
@ -133,33 +132,64 @@ func processGitErrorf(err error, format string, args ...interface{}) error {
return errors.NotFound("repository not found")
case strings.Contains(err.Error(), "reference already exists"):
return errors.Conflict("reference already exists")
case strings.Contains(err.Error(), "no merge base"):
if len(args) >= 2 {
return &UnrelatedHistoriesError{
BaseRef: strings.TrimSpace(args[0].(string)),
HeadRef: strings.TrimSpace(args[1].(string)),
}
}
return &UnrelatedHistoriesError{}
default:
return fallbackErr
}
}
// MergeUnrelatedHistoriesError represents an error if merging fails due to unrelated histories.
type MergeUnrelatedHistoriesError struct {
Method enum.MergeMethod
StdOut string
StdErr string
Err error
type UnrelatedHistoriesError struct {
BaseRef string
HeadRef string
}
func IsMergeUnrelatedHistoriesError(err error) bool {
return errors.Is(err, &MergeUnrelatedHistoriesError{})
func (e *UnrelatedHistoriesError) Map() map[string]any {
return map[string]any{
"base_ref": e.BaseRef,
"head_ref": e.HeadRef,
}
}
func (e *MergeUnrelatedHistoriesError) Error() string {
return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", e.Err, e.StdErr, e.StdOut)
func (e *UnrelatedHistoriesError) Is(err error) bool {
var target *UnrelatedHistoriesError
ok := errors.As(err, &target)
if !ok {
return false
}
return target.BaseRef == e.BaseRef && target.HeadRef == e.HeadRef
}
func (e *MergeUnrelatedHistoriesError) Unwrap() error {
return e.Err
func (e *UnrelatedHistoriesError) Error() string {
if e.BaseRef == "" || e.HeadRef == "" {
return "unrelated commit histories error"
}
// remove branch and tag prefixes, original remains in struct fields
// we just need to remove first occurrence.
baseRef := strings.TrimPrefix(e.BaseRef, BranchPrefix)
baseRef = strings.TrimPrefix(baseRef, TagPrefix)
headRef := strings.TrimPrefix(e.HeadRef, BranchPrefix)
headRef = strings.TrimPrefix(headRef, TagPrefix)
return fmt.Sprintf("%s and %s have entirely different commit histories.", baseRef, headRef)
}
//nolint:errorlint // the purpose of this method is to check whether the target itself if of this type.
func (e *MergeUnrelatedHistoriesError) Is(target error) bool {
_, ok := target.(*MergeUnrelatedHistoriesError)
return ok
// IsUnrelatedHistoriesError checks if an error is a UnrelatedHistoriesError.
func IsUnrelatedHistoriesError(err error) bool {
return AsUnrelatedHistoriesError(err) != nil
}
func AsUnrelatedHistoriesError(err error) *UnrelatedHistoriesError {
var target *UnrelatedHistoriesError
ok := errors.As(err, &target)
if !ok {
return nil
}
return target
}

View File

@ -56,15 +56,26 @@ func (g *Git) GetMergeBase(
}
}
stdout := &bytes.Buffer{}
cmd := command.New("merge-base",
command.WithArg(base, head),
)
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
err := cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdout(stdout),
command.WithStderr(stderr),
)
if err != nil {
// git merge-base rev1 rev2
// if there is unrelated history then stderr is empty with
// exit code 1. This cannot be handled in processGitErrorf because stderr is empty.
if command.AsError(err).IsExitCode(1) && stderr.Len() == 0 {
return sha.None, "", &UnrelatedHistoriesError{
BaseRef: base,
HeadRef: head,
}
}
return sha.None, "", processGitErrorf(err, "failed to get merge-base [%s, %s]", base, head)
}

View File

@ -184,12 +184,12 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
baseCommitSHA, err := s.git.GetFullCommitID(ctx, repoPath, params.BaseBranch)
if err != nil {
return MergeOutput{}, fmt.Errorf("failed to get merge base branch commit SHA: %w", err)
return MergeOutput{}, fmt.Errorf("failed to get base branch commit SHA: %w", err)
}
headCommitSHA, err := s.git.GetFullCommitID(ctx, repoPath, params.HeadBranch)
if err != nil {
return MergeOutput{}, fmt.Errorf("failed to get merge base branch commit SHA: %w", err)
return MergeOutput{}, fmt.Errorf("failed to get head branch commit SHA: %w", err)
}
if !params.HeadExpectedSHA.IsEmpty() && !params.HeadExpectedSHA.Equal(headCommitSHA) {