diff options
author | Xing Xin <xingxin.xx@bytedance.com> | 2023-08-11 15:40:58 +0300 |
---|---|---|
committer | Xing Xin <xingxin.xx@bytedance.com> | 2023-08-15 14:20:00 +0300 |
commit | b5298f3eea5338f5324edccfec5f959196844aa9 (patch) | |
tree | 2e14fdefd27dd4cb80fc3633c52e3daeb4d53081 | |
parent | 231230e104f5c8162a2f9212086dd81d41b707d3 (diff) |
operations: Split UserRebaseX to separate files
This commit splits the implementation and tests for UserRebaseX RPCs
into separate files. This will help improve the code organization and
readability.
-rw-r--r-- | internal/gitaly/service/operations/rebase_confirmable.go (renamed from internal/gitaly/service/operations/rebase.go) | 104 | ||||
-rw-r--r-- | internal/gitaly/service/operations/rebase_confirmable_test.go (renamed from internal/gitaly/service/operations/rebase_test.go) | 219 | ||||
-rw-r--r-- | internal/gitaly/service/operations/rebase_to_ref.go | 116 | ||||
-rw-r--r-- | internal/gitaly/service/operations/rebase_to_ref_test.go | 237 |
4 files changed, 353 insertions, 323 deletions
diff --git a/internal/gitaly/service/operations/rebase.go b/internal/gitaly/service/operations/rebase_confirmable.go index 0a6619e71..6e531d052 100644 --- a/internal/gitaly/service/operations/rebase.go +++ b/internal/gitaly/service/operations/rebase_confirmable.go @@ -1,7 +1,6 @@ package operations import ( - "context" "errors" "fmt" "strings" @@ -232,106 +231,3 @@ func validateUserRebaseToRefRequest(locator storage.Locator, in *gitalypb.UserRe return nil } - -// UserRebaseToRef overwrites the given TargetRef with the result of rebasing -// SourceSHA on top of FirstParentRef, and returns the SHA of the HEAD of -// TargetRef. -func (s *Server) UserRebaseToRef(ctx context.Context, request *gitalypb.UserRebaseToRefRequest) (*gitalypb.UserRebaseToRefResponse, error) { - if err := validateUserRebaseToRefRequest(s.locator, request); err != nil { - return nil, structerr.NewInvalidArgument("%w", err) - } - - quarantineDir, quarantineRepo, err := s.quarantinedRepo(ctx, request.Repository) - if err != nil { - return nil, structerr.NewInternal("creating repo quarantine: %w", err) - } - - repoPath, err := quarantineRepo.Path() - if err != nil { - return nil, err - } - - oid, err := quarantineRepo.ResolveRevision(ctx, git.Revision(request.FirstParentRef)) - if err != nil { - return nil, structerr.NewInvalidArgument("invalid FirstParentRef") - } - - sourceOID, err := quarantineRepo.ResolveRevision(ctx, git.Revision(request.SourceSha)) - if err != nil { - return nil, structerr.NewInvalidArgument("invalid SourceSha") - } - - // Resolve the current state of the target reference. We do not care whether it - // exists or not, but what we do want to assert is that the target reference doesn't - // change while we compute the merge commit as a small protection against races. - objectHash, err := quarantineRepo.ObjectHash(ctx) - if err != nil { - return nil, structerr.NewInternal("detecting object hash: %w", err) - } - - var oldTargetOID git.ObjectID - if expectedOldOID := request.GetExpectedOldOid(); expectedOldOID != "" { - oldTargetOID, err = objectHash.FromHex(expectedOldOID) - if err != nil { - return nil, structerr.NewInvalidArgument("invalid expected old object ID: %w", err).WithMetadata("old_object_id", expectedOldOID) - } - - oldTargetOID, err = quarantineRepo.ResolveRevision( - ctx, git.Revision(fmt.Sprintf("%s^{object}", oldTargetOID)), - ) - if err != nil { - return nil, structerr.NewInvalidArgument("cannot resolve expected old object ID: %w", err). - WithMetadata("old_object_id", expectedOldOID) - } - } else if targetRef, err := quarantineRepo.GetReference(ctx, git.ReferenceName(request.TargetRef)); err == nil { - if targetRef.IsSymbolic { - return nil, structerr.NewFailedPrecondition("target reference is symbolic: %q", request.TargetRef) - } - - oid, err := objectHash.FromHex(targetRef.Target) - if err != nil { - return nil, structerr.NewInternal("invalid target revision: %w", err) - } - - oldTargetOID = oid - } else if errors.Is(err, git.ErrReferenceNotFound) { - oldTargetOID = objectHash.ZeroOID - } else { - return nil, structerr.NewInternal("could not read target reference: %w", err) - } - - committer := git2go.NewSignature(string(request.User.Name), string(request.User.Email), time.Now()) - if request.Timestamp != nil { - committer.When = request.Timestamp.AsTime() - } - - rebasedOID, err := s.git2goExecutor.Rebase(ctx, quarantineRepo, git2go.RebaseCommand{ - Repository: repoPath, - Committer: committer, - CommitID: sourceOID, - UpstreamCommitID: oid, - SkipEmptyCommits: true, - }) - if err != nil { - var conflictErr git2go.ConflictingFilesError - if errors.As(err, &conflictErr) { - return nil, structerr.NewFailedPrecondition("failed to rebase %s on %s while preparing %s due to conflict", - sourceOID, oid, string(request.TargetRef)) - } - - return nil, structerr.NewInternal("rebasing commits: %w", err) - } - - if err := quarantineDir.Migrate(); err != nil { - return nil, structerr.NewInternal("migrating quarantined objects: %w", err) - } - - repo := s.localrepo(request.GetRepository()) - if err := repo.UpdateRef(ctx, git.ReferenceName(request.TargetRef), rebasedOID, oldTargetOID); err != nil { - return nil, structerr.NewFailedPrecondition("could not update %s. Please refresh and try again", string(request.TargetRef)) - } - - return &gitalypb.UserRebaseToRefResponse{ - CommitId: rebasedOID.String(), - }, nil -} diff --git a/internal/gitaly/service/operations/rebase_test.go b/internal/gitaly/service/operations/rebase_confirmable_test.go index 16770a933..dbc118792 100644 --- a/internal/gitaly/service/operations/rebase_test.go +++ b/internal/gitaly/service/operations/rebase_confirmable_test.go @@ -3,7 +3,6 @@ package operations import ( - "errors" "fmt" "io" "testing" @@ -844,221 +843,3 @@ func buildUserRebaseConfirmableApplyRequest(apply bool) *gitalypb.UserRebaseConf }, } } - -func TestUserRebaseToRef_successful(t *testing.T) { - t.Parallel() - - ctx, cfg, client := setupOperationsServiceWithoutRepo(t, testhelper.Context(t)) - repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg) - repo := localrepo.NewTestRepo(t, cfg, repoProto) - - mergeBaseOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first commit")) - - sourceOID := gittest.WriteCommit(t, cfg, repoPath, - gittest.WithMessage("commit source SHA"), - gittest.WithParents(mergeBaseOID), - gittest.WithTree(gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{ - {Path: "file", Mode: "100644", OID: gittest.WriteBlob(t, cfg, repoPath, []byte("source blob"))}, - })), - ) - - firstParentRef := "refs/heads/main" - firstParentRefBlobID := gittest.WriteBlob(t, cfg, repoPath, []byte("first parent ref blob")) - firstParentRefTreeID := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{ - {Path: "other-file", Mode: "100644", OID: firstParentRefBlobID}, - }) - firstParentRefOID := gittest.WriteCommit(t, cfg, repoPath, - gittest.WithReference(firstParentRef), - gittest.WithMessage("first parent ref commit"), - gittest.WithParents(mergeBaseOID), - gittest.WithTree(firstParentRefTreeID), - ) - - targetRef := "refs/merge-requests/1234/train" - - request := &gitalypb.UserRebaseToRefRequest{ - Repository: repoProto, - User: gittest.TestUser, - SourceSha: sourceOID.String(), - FirstParentRef: []byte(firstParentRef), - TargetRef: []byte(targetRef), - Timestamp: ×tamppb.Timestamp{Seconds: 100000000}, - } - - response, err := client.UserRebaseToRef(ctx, request) - require.NoError(t, err, "rebase error") - - rebasedCommitID := gittest.ResolveRevision(t, cfg, repoPath, response.CommitId) - require.NotEqual(t, rebasedCommitID, firstParentRefOID.String(), "no rebase occurred") - - currentTargetRefOID := gittest.ResolveRevision(t, cfg, repoPath, targetRef) - require.Equal(t, currentTargetRefOID.String(), response.CommitId, "target ref does not point to rebased commit") - - _, err = repo.ReadCommit(ctx, git.Revision(response.CommitId)) - require.NoError(t, err, "rebased commit is unreadable") - - currentParentRefOID := gittest.ResolveRevision(t, cfg, repoPath, firstParentRef) - require.Equal(t, currentParentRefOID, firstParentRefOID, "first parent ref got mutated") -} - -func TestUserRebaseToRef_failure(t *testing.T) { - t.Parallel() - - ctx, cfg, client := setupOperationsServiceWithoutRepo(t, testhelper.Context(t)) - repo, repoPath := gittest.CreateRepository(t, ctx, cfg) - - mergeBaseOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first commit")) - - validTargetRef := []byte("refs/merge-requests/x/merge") - validTargetRefOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("commit to target ref")) - validSourceOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("commit source SHA"), gittest.WithParents(mergeBaseOID)) - validSourceSha := validSourceOID.String() - validFirstParentRef := []byte("refs/heads/main") - gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first parent ref commit"), gittest.WithReference(string(validFirstParentRef)), gittest.WithParents(mergeBaseOID)) - - testCases := []struct { - desc string - repo *gitalypb.Repository - user *gitalypb.User - targetRef []byte - sourceSha string - firstParentRef []byte - expectedOldOID string - expectedError error - }{ - { - desc: "empty repository", - user: gittest.TestUser, - targetRef: validTargetRef, - sourceSha: validSourceSha, - firstParentRef: validFirstParentRef, - expectedError: structerr.NewInvalidArgument("%w", storage.ErrRepositoryNotSet), - }, - { - desc: "empty user", - repo: repo, - sourceSha: validSourceSha, - targetRef: validTargetRef, - firstParentRef: validFirstParentRef, - expectedError: structerr.NewInvalidArgument("%w", errors.New("empty User")), - }, - { - desc: "empty source SHA", - repo: repo, - user: gittest.TestUser, - firstParentRef: validFirstParentRef, - targetRef: validTargetRef, - expectedError: structerr.NewInvalidArgument("%w", errors.New("empty SourceSha")), - }, - { - desc: "non-existing source SHA commit", - repo: repo, - user: gittest.TestUser, - targetRef: validTargetRef, - sourceSha: "f001", - firstParentRef: validFirstParentRef, - expectedError: structerr.NewInvalidArgument("%w", errors.New("invalid SourceSha")), - }, - { - desc: "empty first parent ref", - repo: repo, - user: gittest.TestUser, - targetRef: validTargetRef, - sourceSha: validSourceSha, - expectedError: structerr.NewInvalidArgument("%w", errors.New("empty FirstParentRef")), - }, - { - desc: "invalid target ref", - repo: repo, - user: gittest.TestUser, - targetRef: []byte("refs/heads/branch"), - sourceSha: validSourceSha, - firstParentRef: validFirstParentRef, - expectedError: structerr.NewInvalidArgument("%w", errors.New("invalid TargetRef")), - }, - { - desc: "non-existing first parent ref", - repo: repo, - user: gittest.TestUser, - targetRef: validTargetRef, - sourceSha: validSourceSha, - firstParentRef: []byte("refs/heads/branch"), - expectedError: structerr.NewInvalidArgument("invalid FirstParentRef"), - }, - { - desc: "target_ref not at expected_old_oid", - repo: repo, - user: gittest.TestUser, - targetRef: validTargetRef, - sourceSha: validSourceSha, - firstParentRef: validFirstParentRef, - expectedOldOID: validSourceSha, // arbitrary valid SHA - expectedError: structerr.NewFailedPrecondition("could not update %s. Please refresh and try again", validTargetRef), - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.desc, func(t *testing.T) { - t.Parallel() - // reset target ref between tests - gittest.WriteRef(t, cfg, repoPath, git.ReferenceName(validTargetRef), validTargetRefOID) - - request := &gitalypb.UserRebaseToRefRequest{ - Repository: tc.repo, - User: tc.user, - TargetRef: tc.targetRef, - SourceSha: tc.sourceSha, - FirstParentRef: tc.firstParentRef, - ExpectedOldOid: tc.expectedOldOID, - } - _, err := client.UserRebaseToRef(ctx, request) - testhelper.RequireGrpcError(t, err, tc.expectedError) - }) - } -} - -func TestUserRebaseToRef_conflict(t *testing.T) { - t.Parallel() - - ctx, cfg, client := setupOperationsServiceWithoutRepo(t, testhelper.Context(t)) - repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg) - - mergeBaseOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first commit")) - - firstParentRef := "refs/heads/main" - firstParentRefBlobID := gittest.WriteBlob(t, cfg, repoPath, []byte("first parent ref blob")) - firstParentRefTreeID := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{ - {Path: "file", Mode: "100644", OID: firstParentRefBlobID}, - }) - firstParentRefOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first parent ref commit"), gittest.WithTree(firstParentRefTreeID), gittest.WithReference(firstParentRef), gittest.WithParents(mergeBaseOID)) - - sourceBlobID := gittest.WriteBlob(t, cfg, repoPath, []byte("source blob")) - - sourceOID := gittest.WriteCommit(t, cfg, repoPath, - gittest.WithMessage("source commit"), - gittest.WithParents(mergeBaseOID), - gittest.WithTree(gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{ - {Path: "file", Mode: "100644", OID: sourceBlobID}, - })), - ) - - targetRef := "refs/merge-requests/9999/merge" - targetRefOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithReference(targetRef)) - - request := &gitalypb.UserRebaseToRefRequest{ - Repository: repoProto, - User: gittest.TestUser, - TargetRef: []byte(targetRef), - SourceSha: sourceOID.String(), - FirstParentRef: []byte(firstParentRef), - } - response, err := client.UserRebaseToRef(ctx, request) - - require.Nil(t, response) - testhelper.RequireGrpcError(t, structerr.NewFailedPrecondition(fmt.Sprintf("failed to rebase %s on %s while preparing %s due to conflict", sourceOID, firstParentRefOID, targetRef)), err) - - currentTargetRefOID := gittest.ResolveRevision(t, cfg, repoPath, targetRef) - require.Equal(t, targetRefOID, currentTargetRefOID, "target ref should not change when the rebase fails due to GitError") -} diff --git a/internal/gitaly/service/operations/rebase_to_ref.go b/internal/gitaly/service/operations/rebase_to_ref.go new file mode 100644 index 000000000..277d99fda --- /dev/null +++ b/internal/gitaly/service/operations/rebase_to_ref.go @@ -0,0 +1,116 @@ +package operations + +import ( + "context" + "errors" + "fmt" + "time" + + "gitlab.com/gitlab-org/gitaly/v16/internal/git" + "gitlab.com/gitlab-org/gitaly/v16/internal/git2go" + "gitlab.com/gitlab-org/gitaly/v16/internal/structerr" + "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb" +) + +// UserRebaseToRef overwrites the given TargetRef with the result of rebasing +// SourceSHA on top of FirstParentRef, and returns the SHA of the HEAD of +// TargetRef. +func (s *Server) UserRebaseToRef(ctx context.Context, request *gitalypb.UserRebaseToRefRequest) (*gitalypb.UserRebaseToRefResponse, error) { + if err := validateUserRebaseToRefRequest(s.locator, request); err != nil { + return nil, structerr.NewInvalidArgument("%w", err) + } + + quarantineDir, quarantineRepo, err := s.quarantinedRepo(ctx, request.Repository) + if err != nil { + return nil, structerr.NewInternal("creating repo quarantine: %w", err) + } + + repoPath, err := quarantineRepo.Path() + if err != nil { + return nil, err + } + + oid, err := quarantineRepo.ResolveRevision(ctx, git.Revision(request.FirstParentRef)) + if err != nil { + return nil, structerr.NewInvalidArgument("invalid FirstParentRef") + } + + sourceOID, err := quarantineRepo.ResolveRevision(ctx, git.Revision(request.SourceSha)) + if err != nil { + return nil, structerr.NewInvalidArgument("invalid SourceSha") + } + + // Resolve the current state of the target reference. We do not care whether it + // exists or not, but what we do want to assert is that the target reference doesn't + // change while we compute the merge commit as a small protection against races. + objectHash, err := quarantineRepo.ObjectHash(ctx) + if err != nil { + return nil, structerr.NewInternal("detecting object hash: %w", err) + } + + var oldTargetOID git.ObjectID + if expectedOldOID := request.GetExpectedOldOid(); expectedOldOID != "" { + oldTargetOID, err = objectHash.FromHex(expectedOldOID) + if err != nil { + return nil, structerr.NewInvalidArgument("invalid expected old object ID: %w", err).WithMetadata("old_object_id", expectedOldOID) + } + + oldTargetOID, err = quarantineRepo.ResolveRevision( + ctx, git.Revision(fmt.Sprintf("%s^{object}", oldTargetOID)), + ) + if err != nil { + return nil, structerr.NewInvalidArgument("cannot resolve expected old object ID: %w", err). + WithMetadata("old_object_id", expectedOldOID) + } + } else if targetRef, err := quarantineRepo.GetReference(ctx, git.ReferenceName(request.TargetRef)); err == nil { + if targetRef.IsSymbolic { + return nil, structerr.NewFailedPrecondition("target reference is symbolic: %q", request.TargetRef) + } + + oid, err := objectHash.FromHex(targetRef.Target) + if err != nil { + return nil, structerr.NewInternal("invalid target revision: %w", err) + } + + oldTargetOID = oid + } else if errors.Is(err, git.ErrReferenceNotFound) { + oldTargetOID = objectHash.ZeroOID + } else { + return nil, structerr.NewInternal("could not read target reference: %w", err) + } + + committer := git.NewSignature(string(request.User.Name), string(request.User.Email), time.Now()) + if request.Timestamp != nil { + committer.When = request.Timestamp.AsTime() + } + + rebasedOID, err := s.git2goExecutor.Rebase(ctx, quarantineRepo, git2go.RebaseCommand{ + Repository: repoPath, + Committer: committer, + CommitID: sourceOID, + UpstreamCommitID: oid, + SkipEmptyCommits: true, + }) + if err != nil { + var conflictErr git2go.ConflictingFilesError + if errors.As(err, &conflictErr) { + return nil, structerr.NewFailedPrecondition("failed to rebase %s on %s while preparing %s due to conflict", + sourceOID, oid, string(request.TargetRef)) + } + + return nil, structerr.NewInternal("rebasing commits: %w", err) + } + + if err := quarantineDir.Migrate(); err != nil { + return nil, structerr.NewInternal("migrating quarantined objects: %w", err) + } + + repo := s.localrepo(request.GetRepository()) + if err := repo.UpdateRef(ctx, git.ReferenceName(request.TargetRef), rebasedOID, oldTargetOID); err != nil { + return nil, structerr.NewFailedPrecondition("could not update %s. Please refresh and try again", string(request.TargetRef)) + } + + return &gitalypb.UserRebaseToRefResponse{ + CommitId: rebasedOID.String(), + }, nil +} diff --git a/internal/gitaly/service/operations/rebase_to_ref_test.go b/internal/gitaly/service/operations/rebase_to_ref_test.go new file mode 100644 index 000000000..49afe5f46 --- /dev/null +++ b/internal/gitaly/service/operations/rebase_to_ref_test.go @@ -0,0 +1,237 @@ +//go:build !gitaly_test_sha256 + +package operations + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v16/internal/git" + "gitlab.com/gitlab-org/gitaly/v16/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v16/internal/git/localrepo" + "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage" + "gitlab.com/gitlab-org/gitaly/v16/internal/structerr" + "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" + "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestUserRebaseToRef_successful(t *testing.T) { + t.Parallel() + + ctx, cfg, client := setupOperationsServiceWithoutRepo(t, testhelper.Context(t)) + repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg) + repo := localrepo.NewTestRepo(t, cfg, repoProto) + + mergeBaseOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first commit")) + + sourceOID := gittest.WriteCommit(t, cfg, repoPath, + gittest.WithMessage("commit source SHA"), + gittest.WithParents(mergeBaseOID), + gittest.WithTree(gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{ + {Path: "file", Mode: "100644", OID: gittest.WriteBlob(t, cfg, repoPath, []byte("source blob"))}, + })), + ) + + firstParentRef := "refs/heads/main" + firstParentRefBlobID := gittest.WriteBlob(t, cfg, repoPath, []byte("first parent ref blob")) + firstParentRefTreeID := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{ + {Path: "other-file", Mode: "100644", OID: firstParentRefBlobID}, + }) + firstParentRefOID := gittest.WriteCommit(t, cfg, repoPath, + gittest.WithReference(firstParentRef), + gittest.WithMessage("first parent ref commit"), + gittest.WithParents(mergeBaseOID), + gittest.WithTree(firstParentRefTreeID), + ) + + targetRef := "refs/merge-requests/1234/train" + + request := &gitalypb.UserRebaseToRefRequest{ + Repository: repoProto, + User: gittest.TestUser, + SourceSha: sourceOID.String(), + FirstParentRef: []byte(firstParentRef), + TargetRef: []byte(targetRef), + Timestamp: ×tamppb.Timestamp{Seconds: 100000000}, + } + + response, err := client.UserRebaseToRef(ctx, request) + require.NoError(t, err, "rebase error") + + rebasedCommitID := gittest.ResolveRevision(t, cfg, repoPath, response.CommitId) + require.NotEqual(t, rebasedCommitID, firstParentRefOID.String(), "no rebase occurred") + + currentTargetRefOID := gittest.ResolveRevision(t, cfg, repoPath, targetRef) + require.Equal(t, currentTargetRefOID.String(), response.CommitId, "target ref does not point to rebased commit") + + _, err = repo.ReadCommit(ctx, git.Revision(response.CommitId)) + require.NoError(t, err, "rebased commit is unreadable") + + currentParentRefOID := gittest.ResolveRevision(t, cfg, repoPath, firstParentRef) + require.Equal(t, currentParentRefOID, firstParentRefOID, "first parent ref got mutated") +} + +func TestUserRebaseToRef_failure(t *testing.T) { + t.Parallel() + + ctx, cfg, client := setupOperationsServiceWithoutRepo(t, testhelper.Context(t)) + repo, repoPath := gittest.CreateRepository(t, ctx, cfg) + + mergeBaseOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first commit")) + + validTargetRef := []byte("refs/merge-requests/x/merge") + validTargetRefOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("commit to target ref")) + validSourceOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("commit source SHA"), gittest.WithParents(mergeBaseOID)) + validSourceSha := validSourceOID.String() + validFirstParentRef := []byte("refs/heads/main") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first parent ref commit"), gittest.WithReference(string(validFirstParentRef)), gittest.WithParents(mergeBaseOID)) + + testCases := []struct { + desc string + repo *gitalypb.Repository + user *gitalypb.User + targetRef []byte + sourceSha string + firstParentRef []byte + expectedOldOID string + expectedError error + }{ + { + desc: "empty repository", + user: gittest.TestUser, + targetRef: validTargetRef, + sourceSha: validSourceSha, + firstParentRef: validFirstParentRef, + expectedError: structerr.NewInvalidArgument("%w", storage.ErrRepositoryNotSet), + }, + { + desc: "empty user", + repo: repo, + sourceSha: validSourceSha, + targetRef: validTargetRef, + firstParentRef: validFirstParentRef, + expectedError: structerr.NewInvalidArgument("%w", errors.New("empty User")), + }, + { + desc: "empty source SHA", + repo: repo, + user: gittest.TestUser, + firstParentRef: validFirstParentRef, + targetRef: validTargetRef, + expectedError: structerr.NewInvalidArgument("%w", errors.New("empty SourceSha")), + }, + { + desc: "non-existing source SHA commit", + repo: repo, + user: gittest.TestUser, + targetRef: validTargetRef, + sourceSha: "f001", + firstParentRef: validFirstParentRef, + expectedError: structerr.NewInvalidArgument("%w", errors.New("invalid SourceSha")), + }, + { + desc: "empty first parent ref", + repo: repo, + user: gittest.TestUser, + targetRef: validTargetRef, + sourceSha: validSourceSha, + expectedError: structerr.NewInvalidArgument("%w", errors.New("empty FirstParentRef")), + }, + { + desc: "invalid target ref", + repo: repo, + user: gittest.TestUser, + targetRef: []byte("refs/heads/branch"), + sourceSha: validSourceSha, + firstParentRef: validFirstParentRef, + expectedError: structerr.NewInvalidArgument("%w", errors.New("invalid TargetRef")), + }, + { + desc: "non-existing first parent ref", + repo: repo, + user: gittest.TestUser, + targetRef: validTargetRef, + sourceSha: validSourceSha, + firstParentRef: []byte("refs/heads/branch"), + expectedError: structerr.NewInvalidArgument("invalid FirstParentRef"), + }, + { + desc: "target_ref not at expected_old_oid", + repo: repo, + user: gittest.TestUser, + targetRef: validTargetRef, + sourceSha: validSourceSha, + firstParentRef: validFirstParentRef, + expectedOldOID: validSourceSha, // arbitrary valid SHA + expectedError: structerr.NewFailedPrecondition("could not update %s. Please refresh and try again", validTargetRef), + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + // reset target ref between tests + gittest.WriteRef(t, cfg, repoPath, git.ReferenceName(validTargetRef), validTargetRefOID) + + request := &gitalypb.UserRebaseToRefRequest{ + Repository: tc.repo, + User: tc.user, + TargetRef: tc.targetRef, + SourceSha: tc.sourceSha, + FirstParentRef: tc.firstParentRef, + ExpectedOldOid: tc.expectedOldOID, + } + _, err := client.UserRebaseToRef(ctx, request) + testhelper.RequireGrpcError(t, err, tc.expectedError) + }) + } +} + +func TestUserRebaseToRef_conflict(t *testing.T) { + t.Parallel() + + ctx, cfg, client := setupOperationsServiceWithoutRepo(t, testhelper.Context(t)) + repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg) + + mergeBaseOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first commit")) + + firstParentRef := "refs/heads/main" + firstParentRefBlobID := gittest.WriteBlob(t, cfg, repoPath, []byte("first parent ref blob")) + firstParentRefTreeID := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{ + {Path: "file", Mode: "100644", OID: firstParentRefBlobID}, + }) + firstParentRefOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first parent ref commit"), gittest.WithTree(firstParentRefTreeID), gittest.WithReference(firstParentRef), gittest.WithParents(mergeBaseOID)) + + sourceBlobID := gittest.WriteBlob(t, cfg, repoPath, []byte("source blob")) + + sourceOID := gittest.WriteCommit(t, cfg, repoPath, + gittest.WithMessage("source commit"), + gittest.WithParents(mergeBaseOID), + gittest.WithTree(gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{ + {Path: "file", Mode: "100644", OID: sourceBlobID}, + })), + ) + + targetRef := "refs/merge-requests/9999/merge" + targetRefOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithReference(targetRef)) + + request := &gitalypb.UserRebaseToRefRequest{ + Repository: repoProto, + User: gittest.TestUser, + TargetRef: []byte(targetRef), + SourceSha: sourceOID.String(), + FirstParentRef: []byte(firstParentRef), + } + response, err := client.UserRebaseToRef(ctx, request) + + require.Nil(t, response) + testhelper.RequireGrpcError(t, structerr.NewFailedPrecondition(fmt.Sprintf("failed to rebase %s on %s while preparing %s due to conflict", sourceOID, firstParentRefOID, targetRef)), err) + + currentTargetRefOID := gittest.ResolveRevision(t, cfg, repoPath, targetRef) + require.Equal(t, targetRefOID, currentTargetRefOID, "target ref should not change when the rebase fails due to GitError") +} |