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:
authorXing Xin <xingxin.xx@bytedance.com>2023-08-11 15:40:58 +0300
committerXing Xin <xingxin.xx@bytedance.com>2023-08-15 14:20:00 +0300
commitb5298f3eea5338f5324edccfec5f959196844aa9 (patch)
tree2e14fdefd27dd4cb80fc3633c52e3daeb4d53081
parent231230e104f5c8162a2f9212086dd81d41b707d3 (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.go116
-rw-r--r--internal/gitaly/service/operations/rebase_to_ref_test.go237
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: &timestamppb.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: &timestamppb.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")
+}