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:
authorJames Fargher <proglottis@gmail.com>2021-02-18 05:20:23 +0300
committerJames Fargher <proglottis@gmail.com>2021-02-18 05:20:23 +0300
commita467322dcb6865f83d49ae15b0597329bc95cef5 (patch)
treea6772306fc9faebc21476ba3135ac4ae689b9d3e
parenta53df10474d4a29b0562f973448e129b205c62b1 (diff)
parent0ff1d165de464430f09cd8b00cf54c9f71273980 (diff)
Merge branch 'tc-user-cherry-pick-go' into 'master'
Port UserCherryPick to Go Closes #3071 See merge request gitlab-org/gitaly!2709
-rw-r--r--cmd/gitaly-git2go/cherry_pick.go113
-rw-r--r--cmd/gitaly-git2go/cherry_pick_test.go193
-rw-r--r--cmd/gitaly-git2go/main.go15
-rw-r--r--cmd/gitaly-git2go/revert.go2
-rw-r--r--cmd/gitaly-git2go/revert_test.go4
-rw-r--r--internal/git2go/cherry_pick.go33
-rw-r--r--internal/git2go/command.go6
-rw-r--r--internal/git2go/gob.go40
-rw-r--r--internal/git2go/merge.go5
-rw-r--r--internal/git2go/revert.go30
-rw-r--r--internal/gitaly/service/operations/cherry_pick.go103
-rw-r--r--internal/gitaly/service/operations/cherry_pick_test.go174
-rw-r--r--internal/gitaly/service/operations/revert.go25
-rw-r--r--internal/metadata/featureflag/feature_flags.go3
14 files changed, 665 insertions, 81 deletions
diff --git a/cmd/gitaly-git2go/cherry_pick.go b/cmd/gitaly-git2go/cherry_pick.go
new file mode 100644
index 000000000..b0aad7f3a
--- /dev/null
+++ b/cmd/gitaly-git2go/cherry_pick.go
@@ -0,0 +1,113 @@
+// +build static,system_libgit2
+
+package main
+
+import (
+ "context"
+ "encoding/gob"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "time"
+
+ git "github.com/libgit2/git2go/v30"
+ "gitlab.com/gitlab-org/gitaly/internal/git2go"
+)
+
+type cherryPickSubcommand struct{}
+
+func (cmd *cherryPickSubcommand) Flags() *flag.FlagSet {
+ return flag.NewFlagSet("cherry-pick", flag.ExitOnError)
+}
+
+func (cmd *cherryPickSubcommand) Run(ctx context.Context, r io.Reader, w io.Writer) error {
+ var request git2go.CherryPickCommand
+ if err := gob.NewDecoder(r).Decode(&request); err != nil {
+ return err
+ }
+
+ commitID, err := cmd.cherryPick(ctx, &request)
+ return gob.NewEncoder(w).Encode(git2go.Result{
+ CommitID: commitID,
+ Error: git2go.SerializableError(err),
+ })
+}
+
+func (cmd *cherryPickSubcommand) verify(ctx context.Context, r *git2go.CherryPickCommand) error {
+ if r.Repository == "" {
+ return errors.New("missing repository")
+ }
+ if r.AuthorName == "" {
+ return errors.New("missing author name")
+ }
+ if r.AuthorMail == "" {
+ return errors.New("missing author mail")
+ }
+ if r.Message == "" {
+ return errors.New("missing message")
+ }
+ if r.Ours == "" {
+ return errors.New("missing ours")
+ }
+ if r.Commit == "" {
+ return errors.New("missing commit")
+ }
+
+ return nil
+}
+
+func (cmd *cherryPickSubcommand) cherryPick(ctx context.Context, r *git2go.CherryPickCommand) (string, error) {
+ if err := cmd.verify(ctx, r); err != nil {
+ return "", err
+ }
+
+ if r.AuthorDate.IsZero() {
+ r.AuthorDate = time.Now()
+ }
+
+ repo, err := git.OpenRepository(r.Repository)
+ if err != nil {
+ return "", fmt.Errorf("could not open repository: %w", err)
+ }
+ defer repo.Free()
+
+ ours, err := lookupCommit(repo, r.Ours)
+ if err != nil {
+ return "", fmt.Errorf("ours commit lookup: %w", err)
+ }
+
+ pick, err := lookupCommit(repo, r.Commit)
+ if err != nil {
+ return "", fmt.Errorf("commit lookup: %w", err)
+ }
+
+ opts, err := git.DefaultCherrypickOptions()
+ if err != nil {
+ return "", fmt.Errorf("could not get default cherry-pick options: %w", err)
+ }
+ opts.Mainline = r.Mainline
+
+ index, err := repo.CherrypickCommit(pick, ours, opts)
+ if err != nil {
+ return "", fmt.Errorf("could not cherry-pick commit: %w", err)
+ }
+
+ if index.HasConflicts() {
+ return "", git2go.HasConflictsError{}
+ }
+
+ tree, err := index.WriteTreeTo(repo)
+ if err != nil {
+ return "", fmt.Errorf("could not write tree: %w", err)
+ }
+
+ committer := git.Signature(git2go.NewSignature(r.AuthorName, r.AuthorMail, r.AuthorDate))
+
+ commit, err := repo.CreateCommitFromIds("", &committer, &committer, r.Message, tree, ours.Id())
+ if err != nil {
+ return "", fmt.Errorf("could not create cherry-pick commit: %w", err)
+ }
+
+ return commit.String(), nil
+}
diff --git a/cmd/gitaly-git2go/cherry_pick_test.go b/cmd/gitaly-git2go/cherry_pick_test.go
new file mode 100644
index 000000000..bda4c16ed
--- /dev/null
+++ b/cmd/gitaly-git2go/cherry_pick_test.go
@@ -0,0 +1,193 @@
+// +build static,system_libgit2
+
+package main
+
+import (
+ "errors"
+ "testing"
+ "time"
+
+ git "github.com/libgit2/git2go/v30"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ cmdtesthelper "gitlab.com/gitlab-org/gitaly/cmd/gitaly-git2go/testhelper"
+ "gitlab.com/gitlab-org/gitaly/internal/git2go"
+ "gitlab.com/gitlab-org/gitaly/internal/gitaly/config"
+ "gitlab.com/gitlab-org/gitaly/internal/testhelper"
+)
+
+func TestCherryPick_validation(t *testing.T) {
+ _, repoPath, cleanup := testhelper.NewTestRepo(t)
+ defer cleanup()
+
+ testcases := []struct {
+ desc string
+ request git2go.CherryPickCommand
+ expectedErr string
+ }{
+ {
+ desc: "no arguments",
+ expectedErr: "cherry-pick: missing repository",
+ },
+ {
+ desc: "missing repository",
+ request: git2go.CherryPickCommand{AuthorName: "Foo", AuthorMail: "foo@example.com", Message: "Foo", Ours: "HEAD", Commit: "HEAD"},
+ expectedErr: "cherry-pick: missing repository",
+ },
+ {
+ desc: "missing author name",
+ request: git2go.CherryPickCommand{Repository: repoPath, AuthorMail: "foo@example.com", Message: "Foo", Ours: "HEAD", Commit: "HEAD"},
+ expectedErr: "cherry-pick: missing author name",
+ },
+ {
+ desc: "missing author mail",
+ request: git2go.CherryPickCommand{Repository: repoPath, AuthorName: "Foo", Message: "Foo", Ours: "HEAD", Commit: "HEAD"},
+ expectedErr: "cherry-pick: missing author mail",
+ },
+ {
+ desc: "missing message",
+ request: git2go.CherryPickCommand{Repository: repoPath, AuthorName: "Foo", AuthorMail: "foo@example.com", Ours: "HEAD", Commit: "HEAD"},
+ expectedErr: "cherry-pick: missing message",
+ },
+ {
+ desc: "missing ours",
+ request: git2go.CherryPickCommand{Repository: repoPath, AuthorName: "Foo", AuthorMail: "foo@example.com", Message: "Foo", Commit: "HEAD"},
+ expectedErr: "cherry-pick: missing ours",
+ },
+ {
+ desc: "missing commit",
+ request: git2go.CherryPickCommand{Repository: repoPath, AuthorName: "Foo", AuthorMail: "foo@example.com", Message: "Foo", Ours: "HEAD"},
+ expectedErr: "cherry-pick: missing commit",
+ },
+ }
+ for _, tc := range testcases {
+ t.Run(tc.desc, func(t *testing.T) {
+ ctx, cancel := testhelper.Context()
+ defer cancel()
+
+ _, err := tc.request.Run(ctx, config.Config)
+ require.EqualError(t, err, tc.expectedErr)
+ })
+ }
+}
+
+func TestCherryPick(t *testing.T) {
+ testcases := []struct {
+ desc string
+ base map[string]string
+ ours map[string]string
+ commit map[string]string
+ expected map[string]string
+ expectedCommitID string
+ expectedErr error
+ expectedErrMsg string
+ }{
+ {
+ desc: "trivial cherry-pick succeeds",
+ base: map[string]string{
+ "file": "foo",
+ },
+ ours: map[string]string{
+ "file": "foo",
+ },
+ commit: map[string]string{
+ "file": "foobar",
+ },
+ expected: map[string]string{
+ "file": "foobar",
+ },
+ expectedCommitID: "a6b964c97f96f6e479f602633a43bc83c84e6688",
+ },
+ {
+ desc: "conflicting cherry-pick fails",
+ base: map[string]string{
+ "file": "foo",
+ },
+ ours: map[string]string{
+ "file": "fooqux",
+ },
+ commit: map[string]string{
+ "file": "foobar",
+ },
+ expectedErr: git2go.HasConflictsError{},
+ expectedErrMsg: "cherry-pick: could not apply due to conflicts",
+ },
+ {
+ desc: "fails on nonexistent ours commit",
+ expectedErrMsg: "cherry-pick: ours commit lookup: could not lookup reference \"nonexistent\": revspec 'nonexistent' not found",
+ },
+ {
+ desc: "fails on nonexistent cherry-pick commit",
+ ours: map[string]string{
+ "file": "fooqux",
+ },
+ expectedErrMsg: "cherry-pick: commit lookup: could not lookup reference \"nonexistent\": revspec 'nonexistent' not found",
+ },
+ }
+ for _, tc := range testcases {
+ _, repoPath, cleanup := testhelper.NewTestRepo(t)
+ defer cleanup()
+
+ base := cmdtesthelper.BuildCommit(t, repoPath, []*git.Oid{nil}, tc.base)
+
+ var ours, commit = "nonexistent", "nonexistent"
+ if len(tc.ours) > 0 {
+ ours = cmdtesthelper.BuildCommit(t, repoPath, []*git.Oid{base}, tc.ours).String()
+ }
+ if len(tc.commit) > 0 {
+ commit = cmdtesthelper.BuildCommit(t, repoPath, []*git.Oid{base}, tc.commit).String()
+ }
+
+ authorDate := time.Date(2021, 1, 17, 14, 45, 51, 0, time.FixedZone("UTC+2", +2*60*60))
+
+ t.Run(tc.desc, func(t *testing.T) {
+ ctx, cancel := testhelper.Context()
+ defer cancel()
+
+ response, err := git2go.CherryPickCommand{
+ Repository: repoPath,
+ AuthorName: "Foo",
+ AuthorMail: "foo@example.com",
+ AuthorDate: authorDate,
+ Message: "Foo",
+ Ours: ours,
+ Commit: commit,
+ }.Run(ctx, config.Config)
+
+ if tc.expectedErrMsg != "" {
+ require.EqualError(t, err, tc.expectedErrMsg)
+
+ if tc.expectedErr != nil {
+ require.True(t, errors.Is(err, tc.expectedErr))
+ }
+ return
+ }
+
+ require.NoError(t, err)
+ assert.Equal(t, tc.expectedCommitID, response)
+
+ repo, err := git.OpenRepository(repoPath)
+ require.NoError(t, err)
+ defer repo.Free()
+
+ commitOid, err := git.NewOid(response)
+ require.NoError(t, err)
+
+ commit, err := repo.LookupCommit(commitOid)
+ require.NoError(t, err)
+
+ tree, err := commit.Tree()
+ require.NoError(t, err)
+ require.Len(t, tc.expected, int(tree.EntryCount()))
+
+ for name, contents := range tc.expected {
+ entry := tree.EntryByName(name)
+ require.NotNil(t, entry)
+
+ blob, err := repo.LookupBlob(entry.Id)
+ require.NoError(t, err)
+ require.Equal(t, []byte(contents), blob.Contents())
+ }
+ })
+ }
+}
diff --git a/cmd/gitaly-git2go/main.go b/cmd/gitaly-git2go/main.go
index 82dad85a9..ab7e1dfe2 100644
--- a/cmd/gitaly-git2go/main.go
+++ b/cmd/gitaly-git2go/main.go
@@ -18,13 +18,14 @@ type subcmd interface {
}
var subcommands = map[string]subcmd{
- "apply": &applySubcommand{},
- "commit": commitSubcommand{},
- "conflicts": &conflicts.Subcommand{},
- "merge": &mergeSubcommand{},
- "revert": &revertSubcommand{},
- "resolve": &resolveSubcommand{},
- "submodule": &submoduleSubcommand{},
+ "apply": &applySubcommand{},
+ "cherry-pick": &cherryPickSubcommand{},
+ "commit": commitSubcommand{},
+ "conflicts": &conflicts.Subcommand{},
+ "merge": &mergeSubcommand{},
+ "revert": &revertSubcommand{},
+ "resolve": &resolveSubcommand{},
+ "submodule": &submoduleSubcommand{},
}
const programName = "gitaly-git2go"
diff --git a/cmd/gitaly-git2go/revert.go b/cmd/gitaly-git2go/revert.go
index 8ef9dcffc..d3ba942f7 100644
--- a/cmd/gitaly-git2go/revert.go
+++ b/cmd/gitaly-git2go/revert.go
@@ -82,7 +82,7 @@ func (cmd *revertSubcommand) revert(ctx context.Context, request *git2go.RevertC
defer index.Free()
if index.HasConflicts() {
- return "", git2go.RevertConflictError{}
+ return "", git2go.HasConflictsError{}
}
tree, err := index.WriteTreeTo(repo)
diff --git a/cmd/gitaly-git2go/revert_test.go b/cmd/gitaly-git2go/revert_test.go
index 0010aab0b..06e35ce55 100644
--- a/cmd/gitaly-git2go/revert_test.go
+++ b/cmd/gitaly-git2go/revert_test.go
@@ -122,8 +122,8 @@ func TestRevert_trees(t *testing.T) {
return oursOid.String(), revertOid.String()
},
- expectedErr: "revert: could not revert due to conflicts",
- expectedErrAs: &git2go.RevertConflictError{},
+ expectedErr: "revert: could not apply due to conflicts",
+ expectedErrAs: &git2go.HasConflictsError{},
},
{
desc: "nonexistent ours fails",
diff --git a/internal/git2go/cherry_pick.go b/internal/git2go/cherry_pick.go
new file mode 100644
index 000000000..71c7c6f06
--- /dev/null
+++ b/internal/git2go/cherry_pick.go
@@ -0,0 +1,33 @@
+package git2go
+
+import (
+ "context"
+ "time"
+
+ "gitlab.com/gitlab-org/gitaly/internal/gitaly/config"
+)
+
+// CherryPickCommand contains parameters to perform a cherry pick.
+type CherryPickCommand struct {
+ // Repository is the path where to execute the cherry pick.
+ Repository string
+ // AuthorName is the author name for the resulting commit.
+ AuthorName string
+ // AuthorMail is the author mail for the resulting commit.
+ AuthorMail string
+ // AuthorDate is the author date of revert commit.
+ AuthorDate time.Time
+ // Message is the message to be used for the resulting commit.
+ Message string
+ // Ours is the commit that the revert is applied to.
+ Ours string
+ // Commit is the commit that is to be picked.
+ Commit string
+ // Mainline is the parent to be considered the mainline
+ Mainline uint
+}
+
+// Run performs a cherry pick via gitaly-git2go.
+func (m CherryPickCommand) Run(ctx context.Context, cfg config.Cfg) (string, error) {
+ return runWithGob(ctx, cfg, "cherry-pick", m)
+}
diff --git a/internal/git2go/command.go b/internal/git2go/command.go
index c65665de1..c64b1c899 100644
--- a/internal/git2go/command.go
+++ b/internal/git2go/command.go
@@ -5,6 +5,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
+ "errors"
"fmt"
"io"
"os/exec"
@@ -15,6 +16,11 @@ import (
"gitlab.com/gitlab-org/gitaly/internal/gitaly/config"
)
+var (
+ // ErrInvalidArgument is returned in case the merge arguments are invalid.
+ ErrInvalidArgument = errors.New("invalid parameters")
+)
+
func binaryPathFromCfg(cfg config.Cfg) string {
return filepath.Join(cfg.BinDir, "gitaly-git2go")
}
diff --git a/internal/git2go/gob.go b/internal/git2go/gob.go
index b400f815b..aa98b7964 100644
--- a/internal/git2go/gob.go
+++ b/internal/git2go/gob.go
@@ -1,9 +1,14 @@
package git2go
import (
+ "bytes"
+ "context"
"encoding/gob"
"errors"
+ "fmt"
"reflect"
+
+ "gitlab.com/gitlab-org/gitaly/internal/gitaly/config"
)
func init() {
@@ -24,7 +29,7 @@ var registeredTypes = map[interface{}]struct{}{
FileExistsError(""): {},
FileNotFoundError(""): {},
InvalidArgumentError(""): {},
- RevertConflictError{}: {},
+ HasConflictsError{}: {},
}
// Result is the serialized result.
@@ -47,6 +52,14 @@ func (err wrapError) Error() string { return err.Message }
func (err wrapError) Unwrap() error { return err.Err }
+// HasConflictsError is used when a change, for example a revert, could not be
+// applied due to a conflict.
+type HasConflictsError struct{}
+
+func (err HasConflictsError) Error() string {
+ return "could not apply due to conflicts"
+}
+
// SerializableError returns an error that is Gob serializable.
// Registered types are serialized directly. Unregistered types
// are transformed in to an opaque error using their error message.
@@ -69,3 +82,28 @@ func SerializableError(err error) error {
return err
}
+
+// runWithGob runs the specified gitaly-git2go cmd with the request gob-encoded
+// as input and returns the commit ID as string or an error.
+func runWithGob(ctx context.Context, cfg config.Cfg, cmd string, request interface{}) (string, error) {
+ input := &bytes.Buffer{}
+ if err := gob.NewEncoder(input).Encode(request); err != nil {
+ return "", fmt.Errorf("%s: %w", cmd, err)
+ }
+
+ output, err := run(ctx, binaryPathFromCfg(cfg), input, cmd)
+ if err != nil {
+ return "", fmt.Errorf("%s: %w", cmd, err)
+ }
+
+ var result Result
+ if err := gob.NewDecoder(output).Decode(&result); err != nil {
+ return "", fmt.Errorf("%s: %w", cmd, err)
+ }
+
+ if result.Error != nil {
+ return "", fmt.Errorf("%s: %w", cmd, result.Error)
+ }
+
+ return result.CommitID, nil
+}
diff --git a/internal/git2go/merge.go b/internal/git2go/merge.go
index e92856810..aca2035f7 100644
--- a/internal/git2go/merge.go
+++ b/internal/git2go/merge.go
@@ -16,11 +16,6 @@ const (
MergeRecursionLimit = 20
)
-var (
- // ErrInvalidArgument is returned in case the merge arguments are invalid.
- ErrInvalidArgument = errors.New("invalid parameters")
-)
-
// MergeCommand contains parameters to perform a merge.
type MergeCommand struct {
// Repository is the path to execute merge in.
diff --git a/internal/git2go/revert.go b/internal/git2go/revert.go
index c752abb2d..8e7af30ba 100644
--- a/internal/git2go/revert.go
+++ b/internal/git2go/revert.go
@@ -1,21 +1,12 @@
package git2go
import (
- "bytes"
"context"
- "encoding/gob"
- "fmt"
"time"
"gitlab.com/gitlab-org/gitaly/internal/gitaly/config"
)
-type RevertConflictError struct{}
-
-func (err RevertConflictError) Error() string {
- return "could not revert due to conflicts"
-}
-
type RevertCommand struct {
// Repository is the path to execute the revert in.
Repository string `json:"repository"`
@@ -36,24 +27,5 @@ type RevertCommand struct {
}
func (r RevertCommand) Run(ctx context.Context, cfg config.Cfg) (string, error) {
- input := &bytes.Buffer{}
- if err := gob.NewEncoder(input).Encode(r); err != nil {
- return "", fmt.Errorf("revert: %w", err)
- }
-
- output, err := run(ctx, binaryPathFromCfg(cfg), input, "revert")
- if err != nil {
- return "", fmt.Errorf("revert: %w", err)
- }
-
- var result Result
- if err := gob.NewDecoder(output).Decode(&result); err != nil {
- return "", fmt.Errorf("revert: %w", err)
- }
-
- if result.Error != nil {
- return "", fmt.Errorf("revert: %w", result.Error)
- }
-
- return result.CommitID, nil
+ return runWithGob(ctx, cfg, "revert", r)
}
diff --git a/internal/gitaly/service/operations/cherry_pick.go b/internal/gitaly/service/operations/cherry_pick.go
index 3c406abfd..ac0f9c74c 100644
--- a/internal/gitaly/service/operations/cherry_pick.go
+++ b/internal/gitaly/service/operations/cherry_pick.go
@@ -2,8 +2,16 @@ package operations
import (
"context"
+ "errors"
+ "fmt"
+ "gitlab.com/gitlab-org/gitaly/internal/git"
+ "gitlab.com/gitlab-org/gitaly/internal/git/localrepo"
+ "gitlab.com/gitlab-org/gitaly/internal/git2go"
"gitlab.com/gitlab-org/gitaly/internal/gitaly/rubyserver"
+ "gitlab.com/gitlab-org/gitaly/internal/helper"
+ "gitlab.com/gitlab-org/gitaly/internal/helper/text"
+ "gitlab.com/gitlab-org/gitaly/internal/metadata/featureflag"
"gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -14,6 +22,10 @@ func (s *Server) UserCherryPick(ctx context.Context, req *gitalypb.UserCherryPic
return nil, status.Errorf(codes.InvalidArgument, "UserCherryPick: %v", err)
}
+ if featureflag.IsEnabled(ctx, featureflag.GoUserCherryPick) {
+ return s.userCherryPick(ctx, req)
+ }
+
client, err := s.ruby.OperationServiceClient(ctx)
if err != nil {
return nil, err
@@ -26,3 +38,94 @@ func (s *Server) UserCherryPick(ctx context.Context, req *gitalypb.UserCherryPic
return client.UserCherryPick(clientCtx, req)
}
+
+func (s *Server) userCherryPick(ctx context.Context, req *gitalypb.UserCherryPickRequest) (*gitalypb.UserCherryPickResponse, error) {
+ startRevision, err := s.fetchStartRevision(ctx, req)
+ if err != nil {
+ return nil, err
+ }
+
+ localRepo := localrepo.New(s.gitCmdFactory, req.Repository, s.cfg)
+ repoHadBranches, err := localRepo.HasBranches(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ repoPath, err := s.locator.GetPath(req.Repository)
+ if err != nil {
+ return nil, err
+ }
+
+ var mainline uint
+ if len(req.Commit.ParentIds) > 1 {
+ mainline = 1
+ }
+
+ newrev, err := git2go.CherryPickCommand{
+ Repository: repoPath,
+ AuthorName: string(req.User.Name),
+ AuthorMail: string(req.User.Email),
+ Message: string(req.Message),
+ Commit: req.Commit.Id,
+ Ours: startRevision,
+ Mainline: mainline,
+ }.Run(ctx, s.cfg)
+ if err != nil {
+ switch {
+ case errors.As(err, &git2go.HasConflictsError{}):
+ return &gitalypb.UserCherryPickResponse{
+ CreateTreeError: err.Error(),
+ CreateTreeErrorCode: gitalypb.UserCherryPickResponse_CONFLICT,
+ }, nil
+ case errors.Is(err, git2go.ErrInvalidArgument):
+ return nil, helper.ErrInvalidArgument(err)
+ default:
+ return nil, helper.ErrInternalf("cherry-pick command: %w", err)
+ }
+ }
+
+ branch := "refs/heads/" + text.ChompBytes(req.BranchName)
+
+ branchCreated := false
+ oldrev, err := localRepo.ResolveRevision(ctx, git.Revision(fmt.Sprintf("%s^{commit}", branch)))
+ if errors.Is(err, git.ErrReferenceNotFound) {
+ branchCreated = true
+ oldrev = git.ZeroOID
+ } else if err != nil {
+ return nil, helper.ErrInvalidArgumentf("resolve ref: %w", err)
+ }
+
+ if req.DryRun {
+ newrev = startRevision
+ }
+
+ if !branchCreated {
+ ancestor, err := s.isAncestor(ctx, req.Repository, oldrev.String(), newrev)
+ if err != nil {
+ return nil, err
+ }
+ if !ancestor {
+ return &gitalypb.UserCherryPickResponse{
+ CommitError: "Branch diverged",
+ }, nil
+ }
+ }
+
+ if err := s.updateReferenceWithHooks(ctx, req.Repository, req.User, branch, newrev, oldrev.String()); err != nil {
+ if errors.As(err, &preReceiveError{}) {
+ return &gitalypb.UserCherryPickResponse{
+ PreReceiveError: err.Error(),
+ }, err
+ }
+
+ return nil, fmt.Errorf("update reference with hooks: %w", err)
+ }
+
+ return &gitalypb.UserCherryPickResponse{
+ BranchUpdate: &gitalypb.OperationBranchUpdate{
+ CommitId: newrev,
+ BranchCreated: branchCreated,
+ RepoCreated: !repoHadBranches,
+ },
+ }, nil
+}
diff --git a/internal/gitaly/service/operations/cherry_pick_test.go b/internal/gitaly/service/operations/cherry_pick_test.go
index 1d714c489..59b7085c3 100644
--- a/internal/gitaly/service/operations/cherry_pick_test.go
+++ b/internal/gitaly/service/operations/cherry_pick_test.go
@@ -2,6 +2,7 @@ package operations
import (
"context"
+ "fmt"
"testing"
"github.com/golang/protobuf/ptypes/timestamp"
@@ -9,16 +10,18 @@ import (
"gitlab.com/gitlab-org/gitaly/internal/git"
"gitlab.com/gitlab-org/gitaly/internal/git/localrepo"
"gitlab.com/gitlab-org/gitaly/internal/gitaly/config"
+ "gitlab.com/gitlab-org/gitaly/internal/metadata/featureflag"
"gitlab.com/gitlab-org/gitaly/internal/testhelper"
"gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
)
-func TestSuccessfulUserCherryPickRequest(t *testing.T) {
- ctxOuter, cancel := testhelper.Context()
- defer cancel()
+func TestServer_UserCherryPick_successful(t *testing.T) {
+ testWithFeature(t, featureflag.GoUserCherryPick, testServerUserCherryPickSuccessful)
+}
+func testServerUserCherryPickSuccessful(t *testing.T, ctxOuter context.Context) {
serverSocketPath, stop := runOperationServiceServer(t)
defer stop()
@@ -153,6 +156,12 @@ func TestSuccessfulUserCherryPickRequest(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.desc, func(t *testing.T) {
md := testhelper.GitalyServersMetadata(t, serverSocketPath)
+
+ mdFromCtx, ok := metadata.FromOutgoingContext(ctxOuter)
+ if ok {
+ md = metadata.Join(md, mdFromCtx)
+ }
+
ctx := metadata.NewOutgoingContext(ctxOuter, md)
response, err := client.UserCherryPick(ctx, testCase.request)
@@ -170,8 +179,7 @@ func TestSuccessfulUserCherryPickRequest(t *testing.T) {
require.Empty(t, response.CreateTreeErrorCode)
if testCase.request.DryRun {
- require.Equal(t, masterHeadCommit.Subject, headCommit.Subject)
- require.Equal(t, masterHeadCommit.Id, headCommit.Id)
+ testhelper.ProtoEqual(t, masterHeadCommit, headCommit)
} else {
require.Equal(t, testCase.request.Message, headCommit.Subject)
require.Equal(t, masterHeadCommit.Id, headCommit.ParentIds[0])
@@ -180,14 +188,11 @@ func TestSuccessfulUserCherryPickRequest(t *testing.T) {
}
}
-func TestSuccessfulGitHooksForUserCherryPickRequest(t *testing.T) {
- ctx, cancel := testhelper.Context()
- defer cancel()
-
- testSuccessfulGitHooksForUserCherryPickRequest(t, ctx)
+func TestServer_UserCherryPick_successful_git_hooks(t *testing.T) {
+ testWithFeature(t, featureflag.GoUserCherryPick, testServerUserCherryPickSuccessfulGitHooks)
}
-func testSuccessfulGitHooksForUserCherryPickRequest(t *testing.T, ctxOuter context.Context) {
+func testServerUserCherryPickSuccessfulGitHooks(t *testing.T, ctxOuter context.Context) {
serverSocketPath, stop := runOperationServiceServer(t)
defer stop()
@@ -232,7 +237,11 @@ func testSuccessfulGitHooksForUserCherryPickRequest(t *testing.T, ctxOuter conte
}
}
-func TestUserCherryPick_stableID(t *testing.T) {
+func TestServer_UserCherryPick_stableID(t *testing.T) {
+ testWithFeature(t, featureflag.GoUserCherryPick, testServerUserCherryPickStableID)
+}
+
+func testServerUserCherryPickStableID(t *testing.T, ctx context.Context) {
serverSocketPath, stop := runOperationServiceServer(t)
defer stop()
@@ -243,9 +252,6 @@ func TestUserCherryPick_stableID(t *testing.T) {
defer cleanup()
repo := localrepo.New(git.NewExecCommandFactory(config.Config), repoProto, config.Config)
- ctx, cancel := testhelper.Context()
- defer cancel()
-
destinationBranch := "cherry-picking-dst"
testhelper.MustRunCommand(t, nil, "git", "-C", repoPath, "branch", destinationBranch, "master")
@@ -297,10 +303,11 @@ func TestUserCherryPick_stableID(t *testing.T) {
}, pickedCommit)
}
-func TestFailedUserCherryPickRequestDueToValidations(t *testing.T) {
- ctxOuter, cancel := testhelper.Context()
- defer cancel()
+func TestServer_UserCherryPick_failed_validations(t *testing.T) {
+ testWithFeature(t, featureflag.GoUserCherryPick, testServerUserCherryPickFailedValidations)
+}
+func testServerUserCherryPickFailedValidations(t *testing.T, ctxOuter context.Context) {
serverSocketPath, stop := runOperationServiceServer(t)
defer stop()
@@ -378,10 +385,11 @@ func TestFailedUserCherryPickRequestDueToValidations(t *testing.T) {
}
}
-func TestFailedUserCherryPickRequestDueToPreReceiveError(t *testing.T) {
- ctxOuter, cancel := testhelper.Context()
- defer cancel()
+func TestServer_UserCherryPick_failed_with_PreReceiveError(t *testing.T) {
+ testWithFeature(t, featureflag.GoUserCherryPick, testServerUserCherryPickFailedWithPreReceiveError)
+}
+func testServerUserCherryPickFailedWithPreReceiveError(t *testing.T, ctxOuter context.Context) {
serverSocketPath, stop := runOperationServiceServer(t)
defer stop()
@@ -423,10 +431,11 @@ func TestFailedUserCherryPickRequestDueToPreReceiveError(t *testing.T) {
}
}
-func TestFailedUserCherryPickRequestDueToCreateTreeError(t *testing.T) {
- ctxOuter, cancel := testhelper.Context()
- defer cancel()
+func TestServer_UserCherryPick_failed_with_CreateTreeError(t *testing.T) {
+ testWithFeature(t, featureflag.GoUserCherryPick, testServerUserCherryPickFailedWithCreateTreeError)
+}
+func testServerUserCherryPickFailedWithCreateTreeError(t *testing.T, ctxOuter context.Context) {
serverSocketPath, stop := runOperationServiceServer(t)
defer stop()
@@ -461,10 +470,11 @@ func TestFailedUserCherryPickRequestDueToCreateTreeError(t *testing.T) {
require.Equal(t, gitalypb.UserCherryPickResponse_EMPTY, response.CreateTreeErrorCode)
}
-func TestFailedUserCherryPickRequestDueToCommitError(t *testing.T) {
- ctxOuter, cancel := testhelper.Context()
- defer cancel()
+func TestServer_UserCherryPick_failed_with_CommitError(t *testing.T) {
+ testWithFeature(t, featureflag.GoUserCherryPick, testServerUserCherryPickFailedWithCommitError)
+}
+func testServerUserCherryPickFailedWithCommitError(t *testing.T, ctxOuter context.Context) {
serverSocketPath, stop := runOperationServiceServer(t)
defer stop()
@@ -499,3 +509,113 @@ func TestFailedUserCherryPickRequestDueToCommitError(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "Branch diverged", response.CommitError)
}
+
+func TestServer_UserCherryPick_failed_with_conflict(t *testing.T) {
+ testWithFeature(t, featureflag.GoUserCherryPick, testServerUserCherryPickFailedWithConflict)
+}
+
+func testServerUserCherryPickFailedWithConflict(t *testing.T, ctxOuter context.Context) {
+ serverSocketPath, stop := runOperationServiceServer(t)
+ defer stop()
+
+ client, conn := newOperationClient(t, serverSocketPath)
+ defer conn.Close()
+
+ repoProto, repoPath, cleanup := testhelper.NewTestRepo(t)
+ defer cleanup()
+ repo := localrepo.New(git.NewExecCommandFactory(config.Config), repoProto, config.Config)
+
+ destinationBranch := "cherry-picking-dst"
+ testhelper.MustRunCommand(t, nil, "git", "-C", repoPath, "branch", destinationBranch, "conflict_branch_a")
+
+ // This commit cannot be applied to the destinationBranch above
+ cherryPickedCommit, err := repo.ReadCommit(ctxOuter, git.Revision("f0f390655872bb2772c85a0128b2fbc2d88670cb"))
+ require.NoError(t, err)
+
+ request := &gitalypb.UserCherryPickRequest{
+ Repository: repoProto,
+ User: testhelper.TestUser,
+ Commit: cherryPickedCommit,
+ BranchName: []byte(destinationBranch),
+ Message: []byte("Cherry-picking " + cherryPickedCommit.Id),
+ }
+
+ md := testhelper.GitalyServersMetadata(t, serverSocketPath)
+ ctx := testhelper.MergeOutgoingMetadata(ctxOuter, md)
+
+ response, err := client.UserCherryPick(ctx, request)
+ require.NoError(t, err)
+ require.NotEmpty(t, response.CreateTreeError)
+ require.Equal(t, gitalypb.UserCherryPickResponse_CONFLICT, response.CreateTreeErrorCode)
+}
+
+func TestServer_UserCherryPick_successful_with_given_commits(t *testing.T) {
+ testWithFeature(t, featureflag.GoUserCherryPick, testServerUserCherryPickSuccessfulWithGivenCommits)
+}
+
+func testServerUserCherryPickSuccessfulWithGivenCommits(t *testing.T, ctxOuter context.Context) {
+ serverSocketPath, stop := runOperationServiceServer(t)
+ defer stop()
+
+ client, conn := newOperationClient(t, serverSocketPath)
+ defer conn.Close()
+
+ repoProto, repoPath, cleanup := testhelper.NewTestRepo(t)
+ defer cleanup()
+ repo := localrepo.New(git.NewExecCommandFactory(config.Config), repoProto, config.Config)
+
+ testCases := []struct {
+ desc string
+ startRevision git.Revision
+ cherryRevision git.Revision
+ }{
+ {
+ desc: "merge commit",
+ startRevision: "281d3a76f31c812dbf48abce82ccf6860adedd81",
+ cherryRevision: "6907208d755b60ebeacb2e9dfea74c92c3449a1f",
+ },
+ }
+
+ for i, testCase := range testCases {
+ t.Run(testCase.desc, func(t *testing.T) {
+ md := testhelper.GitalyServersMetadata(t, serverSocketPath)
+
+ mdFromCtx, ok := metadata.FromOutgoingContext(ctxOuter)
+ if ok {
+ md = metadata.Join(md, mdFromCtx)
+ }
+
+ ctx := metadata.NewOutgoingContext(ctxOuter, md)
+
+ destinationBranch := fmt.Sprintf("cherry-picking-%d", i)
+
+ testhelper.MustRunCommand(t, nil, "git", "-C", repoPath, "branch", destinationBranch, testCase.startRevision.String())
+
+ commit, err := repo.ReadCommit(ctxOuter, testCase.cherryRevision)
+ require.NoError(t, err)
+
+ request := &gitalypb.UserCherryPickRequest{
+ Repository: repoProto,
+ User: testhelper.TestUser,
+ Commit: commit,
+ BranchName: []byte(destinationBranch),
+ Message: []byte("Cherry-picking " + testCase.cherryRevision.String()),
+ }
+
+ response, err := client.UserCherryPick(ctx, request)
+ require.NoError(t, err)
+
+ newHead, err := repo.ReadCommit(ctx, git.Revision(destinationBranch))
+ require.NoError(t, err)
+
+ expectedResponse := &gitalypb.UserCherryPickResponse{
+ BranchUpdate: &gitalypb.OperationBranchUpdate{CommitId: newHead.Id},
+ }
+
+ testhelper.ProtoEqual(t, expectedResponse, response)
+
+ require.Equal(t, request.Message, newHead.Subject)
+ require.Equal(t, testCase.startRevision.String(), newHead.ParentIds[0])
+ })
+ }
+}
diff --git a/internal/gitaly/service/operations/revert.go b/internal/gitaly/service/operations/revert.go
index 8f8780a7d..bde78398e 100644
--- a/internal/gitaly/service/operations/revert.go
+++ b/internal/gitaly/service/operations/revert.go
@@ -65,7 +65,7 @@ func (s *Server) UserRevert(ctx context.Context, req *gitalypb.UserRevertRequest
Mainline: mainline,
}.Run(ctx, s.cfg)
if err != nil {
- if errors.As(err, &git2go.RevertConflictError{}) {
+ if errors.As(err, &git2go.HasConflictsError{}) {
return &gitalypb.UserRevertResponse{
CreateTreeError: err.Error(),
CreateTreeErrorCode: gitalypb.UserRevertResponse_CONFLICT,
@@ -138,15 +138,22 @@ func (s *Server) rubyUserRevert(ctx context.Context, req *gitalypb.UserRevertReq
return client.UserRevert(clientCtx, req)
}
-func (s *Server) fetchStartRevision(ctx context.Context, req *gitalypb.UserRevertRequest) (string, error) {
- startBranchName := req.StartBranchName
+type requestFetchingStartRevision interface {
+ GetRepository() *gitalypb.Repository
+ GetBranchName() []byte
+ GetStartRepository() *gitalypb.Repository
+ GetStartBranchName() []byte
+}
+
+func (s *Server) fetchStartRevision(ctx context.Context, req requestFetchingStartRevision) (string, error) {
+ startBranchName := req.GetStartBranchName()
if len(startBranchName) == 0 {
- startBranchName = req.BranchName
+ startBranchName = req.GetBranchName()
}
- startRepository := req.StartRepository
+ startRepository := req.GetStartRepository()
if startRepository == nil {
- startRepository = req.Repository
+ startRepository = req.GetRepository()
}
remote, err := remoterepo.New(ctx, startRepository, s.conns)
@@ -158,13 +165,13 @@ func (s *Server) fetchStartRevision(ctx context.Context, req *gitalypb.UserRever
return "", helper.ErrInvalidArgumentf("resolve start ref: %w", err)
}
- if req.StartRepository == nil {
+ if req.GetStartRepository() == nil {
return startRevision.String(), nil
}
- _, err = localrepo.New(s.gitCmdFactory, req.Repository, s.cfg).ResolveRevision(ctx, startRevision.Revision()+"^{commit}")
+ _, err = localrepo.New(s.gitCmdFactory, req.GetRepository(), s.cfg).ResolveRevision(ctx, startRevision.Revision()+"^{commit}")
if errors.Is(err, git.ErrReferenceNotFound) {
- if err := s.fetchRemoteObject(ctx, req.Repository, req.StartRepository, startRevision.String()); err != nil {
+ if err := s.fetchRemoteObject(ctx, req.GetRepository(), req.GetStartRepository(), startRevision.String()); err != nil {
return "", helper.ErrInternalf("fetch start: %w", err)
}
} else if err != nil {
diff --git a/internal/metadata/featureflag/feature_flags.go b/internal/metadata/featureflag/feature_flags.go
index ecfc7d07a..096d0ddf3 100644
--- a/internal/metadata/featureflag/feature_flags.go
+++ b/internal/metadata/featureflag/feature_flags.go
@@ -17,6 +17,8 @@ var (
LogCommandStats = FeatureFlag{Name: "log_command_stats", OnByDefault: false}
// GoUserFFBranch enables the Go implementation of UserFFBranch
GoUserFFBranch = FeatureFlag{Name: "go_user_ff_branch", OnByDefault: true}
+ // GoUserCherryPick enables the Go implementation of UserCherryPick
+ GoUserCherryPick = FeatureFlag{Name: "go_user_cherry_pick", OnByDefault: false}
// GoUserUpdateBranch enables the Go implementation of UserUpdateBranch
GoUserUpdateBranch = FeatureFlag{Name: "go_user_update_branch", OnByDefault: false}
// GoUserCommitFiles enables the Go implementation of UserCommitFiles
@@ -103,6 +105,7 @@ var All = []FeatureFlag{
LogCommandStats,
ReferenceTransactions,
GoUserFFBranch,
+ GoUserCherryPick,
GoUserUpdateBranch,
GoUserCommitFiles,
GoResolveConflicts,