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, &notAllowedError) { 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 }