internal/gitaly/service/operations/user_delete_branch.go (95 lines of code) (raw):
package operations
import (
"context"
"errors"
"fmt"
"gitlab.com/gitlab-org/gitaly/v16/internal/git"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/hook"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/hook/updateref"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage"
"gitlab.com/gitlab-org/gitaly/v16/internal/structerr"
"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
)
func validateUserDeleteBranchRequest(ctx context.Context, locator storage.Locator, in *gitalypb.UserDeleteBranchRequest) error {
if err := locator.ValidateRepository(ctx, in.GetRepository()); err != nil {
return err
}
if len(in.GetBranchName()) == 0 {
return errors.New("bad request: empty branch name")
}
if in.GetUser() == nil {
return errors.New("bad request: empty user")
}
return nil
}
// UserDeleteBranch force-deletes a single branch in the context of a specific user. It executes
// hooks and contacts Rails to verify that the user is indeed allowed to delete that branch.
func (s *Server) UserDeleteBranch(ctx context.Context, req *gitalypb.UserDeleteBranchRequest) (*gitalypb.UserDeleteBranchResponse, error) {
if err := validateUserDeleteBranchRequest(ctx, s.locator, req); err != nil {
return nil, structerr.NewInvalidArgument("%w", err)
}
referenceName := git.NewReferenceNameFromBranchName(string(req.GetBranchName()))
repo := s.localRepoFactory.Build(req.GetRepository())
objectHash, err := repo.ObjectHash(ctx)
if err != nil {
return nil, fmt.Errorf("detecting object format: %w", err)
}
var referenceValue git.ObjectID
if expectedOldOID := req.GetExpectedOldOid(); expectedOldOID != "" {
referenceValue, err = objectHash.FromHex(expectedOldOID)
if err != nil {
return nil, structerr.NewInvalidArgument("invalid expected old object ID: %w", err).WithMetadata("old_object_id", expectedOldOID)
}
referenceValue, err = repo.ResolveRevision(
ctx, git.Revision(fmt.Sprintf("%s^{object}", referenceValue)),
)
if err != nil {
return nil, structerr.NewInvalidArgument("cannot resolve expected old object ID: %w", err).
WithMetadata("old_object_id", expectedOldOID)
}
} else {
referenceValue, err = repo.ResolveRevision(ctx, referenceName.Revision())
if err != nil {
return nil, structerr.NewFailedPrecondition("branch not found: %q", req.GetBranchName())
}
}
if err := s.updateReferenceWithHooks(ctx, req.GetRepository(), req.GetUser(), nil, referenceName, objectHash.ZeroOID, referenceValue); err != nil {
var notAllowedError hook.NotAllowedError
var customHookErr updateref.CustomHookError
var updateRefError updateref.Error
if errors.As(err, ¬AllowedError) {
return nil, structerr.NewPermissionDenied("deletion denied by access checks: %w", err).WithDetail(
&gitalypb.UserDeleteBranchError{
Error: &gitalypb.UserDeleteBranchError_AccessCheck{
AccessCheck: &gitalypb.AccessCheckError{
ErrorMessage: notAllowedError.Message,
UserId: notAllowedError.UserID,
Protocol: notAllowedError.Protocol,
Changes: notAllowedError.Changes,
},
},
},
)
} else if errors.As(err, &customHookErr) {
return nil, structerr.NewPermissionDenied("deletion denied by custom hooks: %w", err).WithDetail(
&gitalypb.UserDeleteBranchError{
Error: &gitalypb.UserDeleteBranchError_CustomHook{
CustomHook: customHookErr.Proto(),
},
},
)
} else if errors.As(err, &updateRefError) {
return nil, structerr.NewFailedPrecondition("reference update failed: %w", updateRefError).WithDetail(
&gitalypb.UserDeleteBranchError{
Error: &gitalypb.UserDeleteBranchError_ReferenceUpdate{
ReferenceUpdate: &gitalypb.ReferenceUpdateError{
ReferenceName: []byte(updateRefError.Reference.String()),
OldOid: updateRefError.OldOID.String(),
NewOid: updateRefError.NewOID.String(),
},
},
},
)
}
return nil, structerr.NewInternal("deleting reference: %w", err)
}
return &gitalypb.UserDeleteBranchResponse{}, nil
}