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:
-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--internal/git2go/cherry_pick.go33
-rw-r--r--internal/gitaly/service/operations/cherry_pick.go96
5 files changed, 442 insertions, 8 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/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/gitaly/service/operations/cherry_pick.go b/internal/gitaly/service/operations/cherry_pick.go
index f8e59f675..ac0f9c74c 100644
--- a/internal/gitaly/service/operations/cherry_pick.go
+++ b/internal/gitaly/service/operations/cherry_pick.go
@@ -2,8 +2,15 @@ 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"
@@ -33,5 +40,92 @@ func (s *Server) UserCherryPick(ctx context.Context, req *gitalypb.UserCherryPic
}
func (s *Server) userCherryPick(ctx context.Context, req *gitalypb.UserCherryPickRequest) (*gitalypb.UserCherryPickResponse, error) {
- return nil, nil
+ 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
}