Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Steinhardt <psteinhardt@gitlab.com>2022-06-07 16:04:46 +0300
committerPatrick Steinhardt <psteinhardt@gitlab.com>2022-06-10 13:40:18 +0300
commit30d4fd427fc63daef560f1608b3833b827ab54ac (patch)
treee2c4eedee6495a2701e782fa0049720cba0963be
parentbdd33f61948e5fa22da595f9b87e7d5032c19c26 (diff)
operations: Use structured errors in UserDeleteBranchpks-user-delete-branch-structured-errors
UserDeleteBranch is returning successfully in some error cases. This is a pattern we want to get rid of, so let's convert the RPC to instead use the structured errors via the new `UserDeleteBranchError` message type. This fixes some cases where we know to create replication jobs because the `UserDeleteBranch` RPC returns successfully, but didn't cast a single vote. Changelog: fixed
-rw-r--r--internal/gitaly/service/operations/branches.go63
-rw-r--r--internal/gitaly/service/operations/branches_test.go244
-rw-r--r--internal/metadata/featureflag/ff_user_delete_branch_structured_errors.go5
-rw-r--r--proto/go/gitalypb/operations_grpc.pb.go22
-rw-r--r--proto/operations.proto11
-rw-r--r--ruby/proto/gitaly/operations_services_pb.rb11
6 files changed, 302 insertions, 54 deletions
diff --git a/internal/gitaly/service/operations/branches.go b/internal/gitaly/service/operations/branches.go
index db5757eb3..5adf2d990 100644
--- a/internal/gitaly/service/operations/branches.go
+++ b/internal/gitaly/service/operations/branches.go
@@ -6,7 +6,9 @@ import (
"gitlab.com/gitlab-org/gitaly/v15/internal/git"
"gitlab.com/gitlab-org/gitaly/v15/internal/git/updateref"
+ "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/hook"
"gitlab.com/gitlab-org/gitaly/v15/internal/helper"
+ "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag"
"gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -163,6 +165,67 @@ func (s *Server) UserDeleteBranch(ctx context.Context, req *gitalypb.UserDeleteB
}
if err := s.updateReferenceWithHooks(ctx, req.Repository, req.User, nil, referenceName, git.ZeroOID, referenceValue); err != nil {
+ if featureflag.UserDeleteBranchStructuredErrors.IsEnabled(ctx) {
+ var notAllowedError hook.NotAllowedError
+ var customHookErr updateref.CustomHookError
+ var updateRefError updateref.Error
+
+ if errors.As(err, &notAllowedError) {
+ detailedErr, err := helper.ErrWithDetails(
+ helper.ErrPermissionDeniedf("deletion denied by access checks: %w", err),
+ &gitalypb.UserDeleteBranchError{
+ Error: &gitalypb.UserDeleteBranchError_AccessCheck{
+ AccessCheck: &gitalypb.AccessCheckError{
+ ErrorMessage: notAllowedError.Message,
+ UserId: notAllowedError.UserID,
+ Protocol: notAllowedError.Protocol,
+ Changes: notAllowedError.Changes,
+ },
+ },
+ },
+ )
+ if err != nil {
+ return nil, helper.ErrInternalf("error details: %w", err)
+ }
+
+ return nil, detailedErr
+ } else if errors.As(err, &customHookErr) {
+ detailedErr, err := helper.ErrWithDetails(
+ helper.ErrPermissionDeniedf("deletion denied by custom hooks: %w", err),
+ &gitalypb.UserDeleteBranchError{
+ Error: &gitalypb.UserDeleteBranchError_CustomHook{
+ CustomHook: customHookErr.Proto(),
+ },
+ },
+ )
+ if err != nil {
+ return nil, helper.ErrInternalf("error details: %w", err)
+ }
+
+ return nil, detailedErr
+ } else if errors.As(err, &updateRefError) {
+ detailedErr, err := helper.ErrWithDetails(
+ helper.ErrFailedPreconditionf("reference update failed: %w", updateRefError),
+ &gitalypb.UserDeleteBranchError{
+ Error: &gitalypb.UserDeleteBranchError_ReferenceUpdate{
+ ReferenceUpdate: &gitalypb.ReferenceUpdateError{
+ ReferenceName: []byte(updateRefError.Reference.String()),
+ OldOid: updateRefError.OldOID.String(),
+ NewOid: updateRefError.NewOID.String(),
+ },
+ },
+ },
+ )
+ if err != nil {
+ return nil, helper.ErrInternalf("error details: %w", err)
+ }
+
+ return nil, detailedErr
+ }
+
+ return nil, helper.ErrInternalf("deleting reference: %w", err)
+ }
+
var customHookErr updateref.CustomHookError
if errors.As(err, &customHookErr) {
return &gitalypb.UserDeleteBranchResponse{
diff --git a/internal/gitaly/service/operations/branches_test.go b/internal/gitaly/service/operations/branches_test.go
index dcf595905..da0262bcb 100644
--- a/internal/gitaly/service/operations/branches_test.go
+++ b/internal/gitaly/service/operations/branches_test.go
@@ -16,6 +16,7 @@ import (
"gitlab.com/gitlab-org/gitaly/v15/internal/gitlab"
"gitlab.com/gitlab-org/gitaly/v15/internal/helper"
"gitlab.com/gitlab-org/gitaly/v15/internal/metadata"
+ "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testserver"
@@ -379,7 +380,11 @@ func TestUserCreateBranch_failure(t *testing.T) {
func TestUserDeleteBranch_success(t *testing.T) {
t.Parallel()
- ctx := testhelper.Context(t)
+ testhelper.NewFeatureSets(featureflag.UserDeleteBranchStructuredErrors).Run(t, testUserDeleteBranchSuccess)
+}
+
+func testUserDeleteBranchSuccess(t *testing.T, ctx context.Context) {
+ t.Parallel()
ctx, cfg, repo, repoPath, client := setupOperationsService(t, ctx)
@@ -434,7 +439,25 @@ func TestUserDeleteBranch_success(t *testing.T) {
func TestUserDeleteBranch_allowed(t *testing.T) {
t.Parallel()
- ctx := testhelper.Context(t)
+ testhelper.NewFeatureSets(featureflag.UserDeleteBranchStructuredErrors).Run(t, testUserDeleteBranchAllowed)
+}
+
+func testUserDeleteBranchAllowed(t *testing.T, ctx context.Context) {
+ t.Parallel()
+
+ responseIfUnstructured := func(response *gitalypb.UserDeleteBranchResponse) *gitalypb.UserDeleteBranchResponse {
+ if featureflag.UserDeleteBranchStructuredErrors.IsEnabled(ctx) {
+ return nil
+ }
+ return response
+ }
+
+ errIfStructured := func(err error) error {
+ if featureflag.UserDeleteBranchStructuredErrors.IsEnabled(ctx) {
+ return err
+ }
+ return nil
+ }
for _, tc := range []struct {
desc string
@@ -454,18 +477,48 @@ func TestUserDeleteBranch_allowed(t *testing.T) {
allowed: func(context.Context, gitlab.AllowedParams) (bool, string, error) {
return false, "something something", nil
},
- expectedResponse: &gitalypb.UserDeleteBranchResponse{
+ expectedErr: errIfStructured(errWithDetails(t,
+ helper.ErrPermissionDeniedf("deletion denied by access checks: GitLab: something something"),
+ &gitalypb.UserDeleteBranchError{
+ Error: &gitalypb.UserDeleteBranchError_AccessCheck{
+ AccessCheck: &gitalypb.AccessCheckError{
+ Protocol: "web",
+ UserId: "user-123",
+ ErrorMessage: "something something",
+ Changes: []byte(fmt.Sprintf(
+ "%s %s refs/heads/branch\n", "549090fbeacc6607bc70648d3ba554c355e670c5", git.ZeroOID,
+ )),
+ },
+ },
+ },
+ )),
+ expectedResponse: responseIfUnstructured(&gitalypb.UserDeleteBranchResponse{
PreReceiveError: "GitLab: something something",
- },
+ }),
},
{
desc: "error",
allowed: func(context.Context, gitlab.AllowedParams) (bool, string, error) {
return false, "something something", fmt.Errorf("something else")
},
- expectedResponse: &gitalypb.UserDeleteBranchResponse{
+ expectedErr: errIfStructured(errWithDetails(t,
+ helper.ErrPermissionDeniedf("deletion denied by access checks: GitLab: something else"),
+ &gitalypb.UserDeleteBranchError{
+ Error: &gitalypb.UserDeleteBranchError_AccessCheck{
+ AccessCheck: &gitalypb.AccessCheckError{
+ Protocol: "web",
+ UserId: "user-123",
+ ErrorMessage: "something else",
+ Changes: []byte(fmt.Sprintf(
+ "%s %s refs/heads/branch\n", "549090fbeacc6607bc70648d3ba554c355e670c5", git.ZeroOID,
+ )),
+ },
+ },
+ },
+ )),
+ expectedResponse: responseIfUnstructured(&gitalypb.UserDeleteBranchResponse{
PreReceiveError: "GitLab: something else",
- },
+ }),
},
} {
t.Run(tc.desc, func(t *testing.T) {
@@ -489,11 +542,15 @@ func TestUserDeleteBranch_allowed(t *testing.T) {
func TestUserDeleteBranch_concurrentUpdate(t *testing.T) {
t.Parallel()
+ testhelper.NewFeatureSets(featureflag.UserDeleteBranchStructuredErrors).Run(t, testUserDeleteBranchConcurrentUpdate)
+}
+
+func testUserDeleteBranchConcurrentUpdate(t *testing.T, ctx context.Context) {
+ t.Parallel()
- ctx := testhelper.Context(t)
ctx, cfg, repo, repoPath, client := setupOperationsService(t, ctx)
- gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("concurrent-update"))
+ commitID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("concurrent-update"))
// Create a git-update-ref(1) process that's locking the "concurrent-update" branch. We do
// not commit the update yet though to keep the reference locked to simulate concurrent
@@ -511,13 +568,32 @@ func TestUserDeleteBranch_concurrentUpdate(t *testing.T) {
BranchName: []byte("concurrent-update"),
User: gittest.TestUser,
})
- testhelper.RequireGrpcError(t, helper.ErrFailedPreconditionf("Could not update refs/heads/concurrent-update. Please refresh and try again."), err)
+ if featureflag.UserDeleteBranchStructuredErrors.IsEnabled(ctx) {
+ testhelper.RequireGrpcError(t, errWithDetails(t,
+ helper.ErrFailedPreconditionf("reference update failed: Could not update refs/heads/concurrent-update. Please refresh and try again."),
+ &gitalypb.UserDeleteBranchError{
+ Error: &gitalypb.UserDeleteBranchError_ReferenceUpdate{
+ ReferenceUpdate: &gitalypb.ReferenceUpdateError{
+ OldOid: commitID.String(),
+ NewOid: git.ZeroOID.String(),
+ ReferenceName: []byte("refs/heads/concurrent-update"),
+ },
+ },
+ },
+ ), err)
+ } else {
+ testhelper.RequireGrpcError(t, helper.ErrFailedPreconditionf("Could not update refs/heads/concurrent-update. Please refresh and try again."), err)
+ }
require.Nil(t, response)
}
func TestUserDeleteBranch_hooks(t *testing.T) {
t.Parallel()
- ctx := testhelper.Context(t)
+ testhelper.NewFeatureSets(featureflag.UserDeleteBranchStructuredErrors).Run(t, testUserDeleteBranchHooks)
+}
+
+func testUserDeleteBranchHooks(t *testing.T, ctx context.Context) {
+ t.Parallel()
ctx, cfg, repo, repoPath, client := setupOperationsService(t, ctx)
@@ -546,6 +622,11 @@ func TestUserDeleteBranch_hooks(t *testing.T) {
func TestUserDeleteBranch_transaction(t *testing.T) {
t.Parallel()
+ testhelper.NewFeatureSets(featureflag.UserDeleteBranchStructuredErrors).Run(t, testUserDeleteBranchTransaction)
+}
+
+func testUserDeleteBranchTransaction(t *testing.T, ctx context.Context) {
+ t.Parallel()
cfg, repo, repoPath := testcfg.BuildWithRepo(t)
// This creates a new branch "delete-me" which exists both in the packed-refs file and as a
@@ -571,7 +652,7 @@ func TestUserDeleteBranch_transaction(t *testing.T) {
deps.GetUpdaterWithHooks(),
))
})
- ctx := testhelper.Context(t)
+
ctx, err := txinfo.InjectTransaction(ctx, 1, "node", true)
require.NoError(t, err)
ctx = metadata.IncomingToOutgoing(ctx)
@@ -598,7 +679,11 @@ func TestUserDeleteBranch_transaction(t *testing.T) {
func TestUserDeleteBranch_invalidArgument(t *testing.T) {
t.Parallel()
- ctx := testhelper.Context(t)
+ testhelper.NewFeatureSets(featureflag.UserDeleteBranchStructuredErrors).Run(t, testUserDeleteBranchInvalidArgument)
+}
+
+func testUserDeleteBranchInvalidArgument(t *testing.T, ctx context.Context) {
+ t.Parallel()
ctx, _, repo, _, client := setupOperationsService(t, ctx)
@@ -649,7 +734,11 @@ func TestUserDeleteBranch_invalidArgument(t *testing.T) {
func TestUserDeleteBranch_hookFailure(t *testing.T) {
t.Parallel()
- ctx := testhelper.Context(t)
+ testhelper.NewFeatureSets(featureflag.UserDeleteBranchStructuredErrors).Run(t, testUserDeleteBranchHookFailure)
+}
+
+func testUserDeleteBranchHookFailure(t *testing.T, ctx context.Context) {
+ t.Parallel()
ctx, cfg, repo, repoPath, client := setupOperationsService(t, ctx)
@@ -664,13 +753,41 @@ func TestUserDeleteBranch_hookFailure(t *testing.T) {
hookContent := []byte("#!/bin/sh\necho GL_ID=$GL_ID\nexit 1")
- for _, hookName := range gitlabPreHooks {
- t.Run(hookName, func(t *testing.T) {
- gittest.WriteCustomHook(t, repoPath, hookName, hookContent)
+ for _, tc := range []struct {
+ hookName string
+ hookType gitalypb.CustomHookError_HookType
+ }{
+ {
+ hookName: "pre-receive",
+ hookType: gitalypb.CustomHookError_HOOK_TYPE_PRERECEIVE,
+ },
+ {
+ hookName: "update",
+ hookType: gitalypb.CustomHookError_HOOK_TYPE_UPDATE,
+ },
+ } {
+ t.Run(tc.hookName, func(t *testing.T) {
+ gittest.WriteCustomHook(t, repoPath, tc.hookName, hookContent)
response, err := client.UserDeleteBranch(ctx, request)
- require.NoError(t, err)
- require.Contains(t, response.PreReceiveError, "GL_ID="+gittest.TestUser.GlId)
+ if featureflag.UserDeleteBranchStructuredErrors.IsEnabled(ctx) {
+ testhelper.RequireGrpcError(t, errWithDetails(t,
+ helper.ErrPermissionDeniedf("deletion denied by custom hooks: %s\n", "GL_ID=user-123"),
+ &gitalypb.UserDeleteBranchError{
+ Error: &gitalypb.UserDeleteBranchError_CustomHook{
+ CustomHook: &gitalypb.CustomHookError{
+ HookType: tc.hookType,
+ Stdout: []byte("GL_ID=user-123\n"),
+ },
+ },
+ },
+ ), err)
+
+ require.Nil(t, response)
+ } else {
+ require.NoError(t, err)
+ require.Contains(t, response.PreReceiveError, "GL_ID="+gittest.TestUser.GlId)
+ }
branches := gittest.Exec(t, cfg, "-C", repoPath, "for-each-ref", "--", "refs/heads/"+branchNameInput)
require.Contains(t, string(branches), branchNameInput, "branch name does not exist in branches list")
@@ -680,14 +797,20 @@ func TestUserDeleteBranch_hookFailure(t *testing.T) {
func TestBranchHookOutput(t *testing.T) {
t.Parallel()
- ctx := testhelper.Context(t)
+ testhelper.NewFeatureSets(featureflag.UserDeleteBranchStructuredErrors).Run(t, testBranchHookOutput)
+}
+
+func testBranchHookOutput(t *testing.T, ctx context.Context) {
+ t.Parallel()
ctx, cfg, repo, repoPath, client := setupOperationsService(t, ctx)
testCases := []struct {
- desc string
- hookContent string
- output func(hookPath string) string
+ desc string
+ hookContent string
+ output func(hookPath string) string
+ expectedStderr string
+ expectedStdout string
}{
{
desc: "empty stdout and empty stderr",
@@ -697,35 +820,55 @@ func TestBranchHookOutput(t *testing.T) {
},
},
{
- desc: "empty stdout and some stderr",
- hookContent: "#!/bin/sh\necho stderr >&2\nexit 1",
- output: func(string) string { return "stderr\n" },
+ desc: "empty stdout and some stderr",
+ hookContent: "#!/bin/sh\necho stderr >&2\nexit 1",
+ output: func(string) string { return "stderr\n" },
+ expectedStderr: "stderr\n",
},
{
- desc: "some stdout and empty stderr",
- hookContent: "#!/bin/sh\necho stdout\nexit 1",
- output: func(string) string { return "stdout\n" },
+ desc: "some stdout and empty stderr",
+ hookContent: "#!/bin/sh\necho stdout\nexit 1",
+ output: func(string) string { return "stdout\n" },
+ expectedStdout: "stdout\n",
},
{
- desc: "some stdout and some stderr",
- hookContent: "#!/bin/sh\necho stdout\necho stderr >&2\nexit 1",
- output: func(string) string { return "stderr\n" },
+ desc: "some stdout and some stderr",
+ hookContent: "#!/bin/sh\necho stdout\necho stderr >&2\nexit 1",
+ output: func(string) string { return "stderr\n" },
+ expectedStderr: "stderr\n",
+ expectedStdout: "stdout\n",
},
{
- desc: "whitespace stdout and some stderr",
- hookContent: "#!/bin/sh\necho ' '\necho stderr >&2\nexit 1",
- output: func(string) string { return "stderr\n" },
+ desc: "whitespace stdout and some stderr",
+ hookContent: "#!/bin/sh\necho ' '\necho stderr >&2\nexit 1",
+ output: func(string) string { return "stderr\n" },
+ expectedStderr: "stderr\n",
+ expectedStdout: " \n",
},
{
- desc: "some stdout and whitespace stderr",
- hookContent: "#!/bin/sh\necho stdout\necho ' ' >&2\nexit 1",
- output: func(string) string { return "stdout\n" },
+ desc: "some stdout and whitespace stderr",
+ hookContent: "#!/bin/sh\necho stdout\necho ' ' >&2\nexit 1",
+ output: func(string) string { return "stdout\n" },
+ expectedStderr: " \n",
+ expectedStdout: "stdout\n",
},
}
- for _, hookName := range gitlabPreHooks {
+ for _, hookTestCase := range []struct {
+ hookName string
+ hookType gitalypb.CustomHookError_HookType
+ }{
+ {
+ hookName: "pre-receive",
+ hookType: gitalypb.CustomHookError_HOOK_TYPE_PRERECEIVE,
+ },
+ {
+ hookName: "update",
+ hookType: gitalypb.CustomHookError_HOOK_TYPE_UPDATE,
+ },
+ } {
for _, testCase := range testCases {
- t.Run(hookName+"/"+testCase.desc, func(t *testing.T) {
+ t.Run(hookTestCase.hookName+"/"+testCase.desc, func(t *testing.T) {
branchNameInput := "some-branch"
createRequest := &gitalypb.UserCreateBranchRequest{
Repository: repo,
@@ -739,7 +882,7 @@ func TestBranchHookOutput(t *testing.T) {
User: gittest.TestUser,
}
- hookFilename := gittest.WriteCustomHook(t, repoPath, hookName, []byte(testCase.hookContent))
+ hookFilename := gittest.WriteCustomHook(t, repoPath, hookTestCase.hookName, []byte(testCase.hookContent))
expectedError := testCase.output(hookFilename)
createResponse, err := client.UserCreateBranch(ctx, createRequest)
@@ -750,8 +893,25 @@ func TestBranchHookOutput(t *testing.T) {
defer gittest.Exec(t, cfg, "-C", repoPath, "branch", "-d", branchNameInput)
deleteResponse, err := client.UserDeleteBranch(ctx, deleteRequest)
- require.NoError(t, err)
- require.Equal(t, expectedError, deleteResponse.PreReceiveError)
+ if featureflag.UserDeleteBranchStructuredErrors.IsEnabled(ctx) {
+ testhelper.RequireGrpcError(t, errWithDetails(t,
+ helper.ErrPermissionDeniedf("deletion denied by custom hooks: %s", expectedError),
+ &gitalypb.UserDeleteBranchError{
+ Error: &gitalypb.UserDeleteBranchError_CustomHook{
+ CustomHook: &gitalypb.CustomHookError{
+ HookType: hookTestCase.hookType,
+ Stdout: []byte(testCase.expectedStdout),
+ Stderr: []byte(testCase.expectedStderr),
+ },
+ },
+ },
+ ), err)
+
+ require.Nil(t, deleteResponse)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, expectedError, deleteResponse.PreReceiveError)
+ }
})
}
}
diff --git a/internal/metadata/featureflag/ff_user_delete_branch_structured_errors.go b/internal/metadata/featureflag/ff_user_delete_branch_structured_errors.go
new file mode 100644
index 000000000..75d527112
--- /dev/null
+++ b/internal/metadata/featureflag/ff_user_delete_branch_structured_errors.go
@@ -0,0 +1,5 @@
+package featureflag
+
+// UserDeleteBranchStructuredErrors enables the use of structured errors for the UserDeleteBranch
+// RPC.
+var UserDeleteBranchStructuredErrors = NewFeatureFlag("user_delete_branch_structured_errors", false)
diff --git a/proto/go/gitalypb/operations_grpc.pb.go b/proto/go/gitalypb/operations_grpc.pb.go
index 3ee7dabeb..0171226c2 100644
--- a/proto/go/gitalypb/operations_grpc.pb.go
+++ b/proto/go/gitalypb/operations_grpc.pb.go
@@ -28,9 +28,14 @@ type OperationServiceClient interface {
//
// - Returns `InvalidArgument` in case either the branch name or user are not set.
// - Returns `FailedPrecondition` in case the branch does not exist.
- // - Returns `OK` with a `PreReceiveError` in case custom hooks refused the update.
- // - Returns `FailedPrecondition` in case updating the reference fails because of a concurrent
- // write to the same reference.
+ // - Returns `OK` with a `PreReceiveError` in case custom hooks refused the update. If the
+ // `gitaly_user_delete_branch_structured_errors` feature flag is enabled this error case will
+ // instead return `PermissionDenied` with either a `CustomHook` or AccessCheck` structured
+ // error.
+ // - Returns `FailedPrecondition` in case updating the reference fails because
+ // of a concurrent write to the same reference. If the
+ // `gitaly_user_delete_branch_structured_errors` feature flag is set this error case will
+ // instead return `FailedPrecondition` with a `ReferenceUpdate` structured error.
UserDeleteBranch(ctx context.Context, in *UserDeleteBranchRequest, opts ...grpc.CallOption) (*UserDeleteBranchResponse, error)
// UserCreateTag creates a new tag.
UserCreateTag(ctx context.Context, in *UserCreateTagRequest, opts ...grpc.CallOption) (*UserCreateTagResponse, error)
@@ -338,9 +343,14 @@ type OperationServiceServer interface {
//
// - Returns `InvalidArgument` in case either the branch name or user are not set.
// - Returns `FailedPrecondition` in case the branch does not exist.
- // - Returns `OK` with a `PreReceiveError` in case custom hooks refused the update.
- // - Returns `FailedPrecondition` in case updating the reference fails because of a concurrent
- // write to the same reference.
+ // - Returns `OK` with a `PreReceiveError` in case custom hooks refused the update. If the
+ // `gitaly_user_delete_branch_structured_errors` feature flag is enabled this error case will
+ // instead return `PermissionDenied` with either a `CustomHook` or AccessCheck` structured
+ // error.
+ // - Returns `FailedPrecondition` in case updating the reference fails because
+ // of a concurrent write to the same reference. If the
+ // `gitaly_user_delete_branch_structured_errors` feature flag is set this error case will
+ // instead return `FailedPrecondition` with a `ReferenceUpdate` structured error.
UserDeleteBranch(context.Context, *UserDeleteBranchRequest) (*UserDeleteBranchResponse, error)
// UserCreateTag creates a new tag.
UserCreateTag(context.Context, *UserCreateTagRequest) (*UserCreateTagResponse, error)
diff --git a/proto/operations.proto b/proto/operations.proto
index 5ecf78553..927dc67a8 100644
--- a/proto/operations.proto
+++ b/proto/operations.proto
@@ -35,9 +35,14 @@ service OperationService {
//
// - Returns `InvalidArgument` in case either the branch name or user are not set.
// - Returns `FailedPrecondition` in case the branch does not exist.
- // - Returns `OK` with a `PreReceiveError` in case custom hooks refused the update.
- // - Returns `FailedPrecondition` in case updating the reference fails because of a concurrent
- // write to the same reference.
+ // - Returns `OK` with a `PreReceiveError` in case custom hooks refused the update. If the
+ // `gitaly_user_delete_branch_structured_errors` feature flag is enabled this error case will
+ // instead return `PermissionDenied` with either a `CustomHook` or AccessCheck` structured
+ // error.
+ // - Returns `FailedPrecondition` in case updating the reference fails because
+ // of a concurrent write to the same reference. If the
+ // `gitaly_user_delete_branch_structured_errors` feature flag is set this error case will
+ // instead return `FailedPrecondition` with a `ReferenceUpdate` structured error.
rpc UserDeleteBranch(UserDeleteBranchRequest) returns (UserDeleteBranchResponse) {
option (op_type) = {
op: MUTATOR
diff --git a/ruby/proto/gitaly/operations_services_pb.rb b/ruby/proto/gitaly/operations_services_pb.rb
index b71684ee1..c7e22ac03 100644
--- a/ruby/proto/gitaly/operations_services_pb.rb
+++ b/ruby/proto/gitaly/operations_services_pb.rb
@@ -28,9 +28,14 @@ module Gitaly
#
# - Returns `InvalidArgument` in case either the branch name or user are not set.
# - Returns `FailedPrecondition` in case the branch does not exist.
- # - Returns `OK` with a `PreReceiveError` in case custom hooks refused the update.
- # - Returns `FailedPrecondition` in case updating the reference fails because of a concurrent
- # write to the same reference.
+ # - Returns `OK` with a `PreReceiveError` in case custom hooks refused the update. If the
+ # `gitaly_user_delete_branch_structured_errors` feature flag is enabled this error case will
+ # instead return `PermissionDenied` with either a `CustomHook` or AccessCheck` structured
+ # error.
+ # - Returns `FailedPrecondition` in case updating the reference fails because
+ # of a concurrent write to the same reference. If the
+ # `gitaly_user_delete_branch_structured_errors` feature flag is set this error case will
+ # instead return `FailedPrecondition` with a `ReferenceUpdate` structured error.
rpc :UserDeleteBranch, ::Gitaly::UserDeleteBranchRequest, ::Gitaly::UserDeleteBranchResponse
# UserCreateTag creates a new tag.
rpc :UserCreateTag, ::Gitaly::UserCreateTagRequest, ::Gitaly::UserCreateTagResponse