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--changelogs/unreleased/smh-port-user-commit-files.yml5
-rw-r--r--internal/gitaly/service/operations/commit_files.go356
-rw-r--r--internal/gitaly/service/operations/commit_files_test.go84
-rw-r--r--internal/gitaly/service/operations/server.go8
-rw-r--r--internal/metadata/featureflag/feature_flags.go3
-rw-r--r--proto/go/gitalypb/operations.pb.go124
-rw-r--r--proto/operations.proto72
-rw-r--r--ruby/proto/gitaly/operations_services_pb.rb4
8 files changed, 585 insertions, 71 deletions
diff --git a/changelogs/unreleased/smh-port-user-commit-files.yml b/changelogs/unreleased/smh-port-user-commit-files.yml
new file mode 100644
index 000000000..752252f35
--- /dev/null
+++ b/changelogs/unreleased/smh-port-user-commit-files.yml
@@ -0,0 +1,5 @@
+---
+title: Port UserCommitFiles to Go
+merge_request: 2655
+author:
+type: performance
diff --git a/internal/gitaly/service/operations/commit_files.go b/internal/gitaly/service/operations/commit_files.go
index b67bf30aa..e341b78bd 100644
--- a/internal/gitaly/service/operations/commit_files.go
+++ b/internal/gitaly/service/operations/commit_files.go
@@ -1,15 +1,38 @@
package operations
import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "errors"
"fmt"
+ "io"
+ "time"
+ "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus"
+ "gitlab.com/gitlab-org/gitaly/internal/command"
"gitlab.com/gitlab-org/gitaly/internal/git"
+ "gitlab.com/gitlab-org/gitaly/internal/git2go"
"gitlab.com/gitlab-org/gitaly/internal/gitaly/rubyserver"
+ "gitlab.com/gitlab-org/gitaly/internal/gitalyssh"
+ "gitlab.com/gitlab-org/gitaly/internal/helper"
+ "gitlab.com/gitlab-org/gitaly/internal/metadata/featureflag"
+ "gitlab.com/gitlab-org/gitaly/internal/storage"
"gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
+type indexError string
+
+func (err indexError) Error() string { return string(err) }
+
+func errorWithStderr(err error, stderr *bytes.Buffer) error {
+ return fmt.Errorf("%w, stderr: %q", err, stderr)
+}
+
+// UserCommitFiles allows for committing from a set of actions. See the protobuf documentation
+// for details.
func (s *server) UserCommitFiles(stream gitalypb.OperationService_UserCommitFilesServer) error {
firstRequest, err := stream.Recv()
if err != nil {
@@ -26,6 +49,39 @@ func (s *server) UserCommitFiles(stream gitalypb.OperationService_UserCommitFile
}
ctx := stream.Context()
+
+ if featureflag.IsEnabled(ctx, featureflag.GoUserCommitFiles) {
+ if err := s.userCommitFiles(ctx, header, stream); err != nil {
+ var (
+ response gitalypb.UserCommitFilesResponse
+ indexError indexError
+ preReceiveError preReceiveError
+ )
+
+ switch {
+ case errors.As(err, &indexError):
+ response = gitalypb.UserCommitFilesResponse{IndexError: indexError.Error()}
+ case errors.As(err, new(git2go.DirectoryExistsError)):
+ response = gitalypb.UserCommitFilesResponse{IndexError: "A directory with this name already exists"}
+ case errors.As(err, new(git2go.FileExistsError)):
+ response = gitalypb.UserCommitFilesResponse{IndexError: "A file with this name already exists"}
+ case errors.As(err, new(git2go.FileNotFoundError)):
+ response = gitalypb.UserCommitFilesResponse{IndexError: "A file with this name doesn't exist"}
+ case errors.As(err, &preReceiveError):
+ response = gitalypb.UserCommitFilesResponse{PreReceiveError: preReceiveError.Error()}
+ case errors.As(err, new(git2go.InvalidArgumentError)):
+ return helper.ErrInvalidArgument(err)
+ default:
+ return err
+ }
+
+ ctxlogrus.Extract(ctx).WithError(err).Error("user commit files failed")
+ return stream.SendAndClose(&response)
+ }
+
+ return nil
+ }
+
client, err := s.ruby.OperationServiceClient(ctx)
if err != nil {
return err
@@ -65,6 +121,306 @@ func (s *server) UserCommitFiles(stream gitalypb.OperationService_UserCommitFile
return stream.SendAndClose(response)
}
+func validatePath(rootPath, relPath string) (string, error) {
+ if relPath == "" {
+ return "", indexError("You must provide a file path")
+ }
+
+ path, err := storage.ValidateRelativePath(rootPath, relPath)
+ if err != nil {
+ if errors.Is(err, storage.ErrRelativePathEscapesRoot) {
+ return "", indexError("Path cannot include directory traversal")
+ }
+
+ return "", err
+ }
+
+ if relPath != path {
+ return "", indexError("Path cannot include directory traversal")
+ }
+
+ return path, nil
+}
+
+func (s *server) userCommitFiles(ctx context.Context, header *gitalypb.UserCommitFilesRequestHeader, stream gitalypb.OperationService_UserCommitFilesServer) error {
+ repoPath, err := s.locator.GetRepoPath(header.Repository)
+ if err != nil {
+ return fmt.Errorf("get repo path: %w", err)
+ }
+
+ localRepo := git.NewRepository(header.Repository)
+
+ targetBranchName := "refs/heads/" + string(header.BranchName)
+ targetBranchCommit, err := localRepo.ResolveRefish(ctx, targetBranchName+"^{commit}")
+ if err != nil {
+ if !errors.Is(err, git.ErrReferenceNotFound) {
+ return fmt.Errorf("resolve target branch commit: %w", err)
+ }
+
+ // the branch is being created
+ }
+
+ parentCommitOID := header.StartSha
+ if parentCommitOID == "" {
+ parentCommitOID, err = s.resolveParentCommit(
+ ctx,
+ localRepo,
+ header.StartRepository,
+ targetBranchName,
+ targetBranchCommit,
+ string(header.StartBranchName),
+ )
+ if err != nil {
+ return fmt.Errorf("resolve parent commit: %w", err)
+ }
+ }
+
+ if parentCommitOID != targetBranchCommit {
+ if err := s.fetchMissingCommit(ctx, header.Repository, header.StartRepository, parentCommitOID); err != nil {
+ return fmt.Errorf("fetch missing commit: %w", err)
+ }
+ }
+
+ type action struct {
+ header *gitalypb.UserCommitFilesActionHeader
+ content []byte
+ }
+
+ var pbActions []action
+
+ for {
+ req, err := stream.Recv()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+
+ return fmt.Errorf("receive request: %w", err)
+ }
+
+ switch payload := req.GetAction().GetUserCommitFilesActionPayload().(type) {
+ case *gitalypb.UserCommitFilesAction_Header:
+ pbActions = append(pbActions, action{header: payload.Header})
+ case *gitalypb.UserCommitFilesAction_Content:
+ if len(pbActions) == 0 {
+ return errors.New("content sent before action")
+ }
+
+ // append the content to the previous action
+ content := &pbActions[len(pbActions)-1].content
+ *content = append(*content, payload.Content...)
+ default:
+ return fmt.Errorf("unhandled action payload type: %T", payload)
+ }
+ }
+
+ actions := make([]git2go.Action, 0, len(pbActions))
+ for _, pbAction := range pbActions {
+ if _, ok := gitalypb.UserCommitFilesActionHeader_ActionType_name[int32(pbAction.header.Action)]; !ok {
+ return fmt.Errorf("NoMethodError: undefined method `downcase' for %d:Integer", pbAction.header.Action)
+ }
+
+ path, err := validatePath(repoPath, string(pbAction.header.FilePath))
+ if err != nil {
+ return fmt.Errorf("validate path: %w", err)
+ }
+
+ content := io.Reader(bytes.NewReader(pbAction.content))
+ if pbAction.header.Base64Content {
+ content = base64.NewDecoder(base64.StdEncoding, content)
+ }
+
+ switch pbAction.header.Action {
+ case gitalypb.UserCommitFilesActionHeader_CREATE:
+ blobID, err := localRepo.WriteBlob(ctx, path, content)
+ if err != nil {
+ return fmt.Errorf("write created blob: %w", err)
+ }
+
+ actions = append(actions, git2go.CreateFile{
+ OID: blobID,
+ Path: path,
+ ExecutableMode: pbAction.header.ExecuteFilemode,
+ })
+ case gitalypb.UserCommitFilesActionHeader_CHMOD:
+ actions = append(actions, git2go.ChangeFileMode{
+ Path: path,
+ ExecutableMode: pbAction.header.ExecuteFilemode,
+ })
+ case gitalypb.UserCommitFilesActionHeader_MOVE:
+ prevPath, err := validatePath(repoPath, string(pbAction.header.PreviousPath))
+ if err != nil {
+ return fmt.Errorf("validate previous path: %w", err)
+ }
+
+ var oid string
+ if !pbAction.header.InferContent {
+ var err error
+ oid, err = localRepo.WriteBlob(ctx, path, content)
+ if err != nil {
+ return err
+ }
+ }
+
+ actions = append(actions, git2go.MoveFile{
+ Path: prevPath,
+ NewPath: path,
+ OID: oid,
+ })
+ case gitalypb.UserCommitFilesActionHeader_UPDATE:
+ oid, err := localRepo.WriteBlob(ctx, path, content)
+ if err != nil {
+ return fmt.Errorf("write updated blob: %w", err)
+ }
+
+ actions = append(actions, git2go.UpdateFile{
+ Path: path,
+ OID: oid,
+ })
+ case gitalypb.UserCommitFilesActionHeader_DELETE:
+ actions = append(actions, git2go.DeleteFile{
+ Path: path,
+ })
+ case gitalypb.UserCommitFilesActionHeader_CREATE_DIR:
+ actions = append(actions, git2go.CreateDirectory{
+ Path: path,
+ })
+ }
+ }
+
+ authorName := header.User.Name
+ if len(header.CommitAuthorName) > 0 {
+ authorName = header.CommitAuthorName
+ }
+
+ authorEmail := header.User.Email
+ if len(header.CommitAuthorEmail) > 0 {
+ authorEmail = header.CommitAuthorEmail
+ }
+
+ commitID, err := s.git2go.Commit(ctx, git2go.CommitParams{
+ Repository: repoPath,
+ Author: git2go.NewSignature(string(authorName), string(authorEmail), time.Now()),
+ Message: string(header.CommitMessage),
+ Parent: parentCommitOID,
+ Actions: actions,
+ })
+ if err != nil {
+ return fmt.Errorf("commit: %w", err)
+ }
+
+ hasBranches, err := hasBranches(ctx, header.Repository)
+ if err != nil {
+ return fmt.Errorf("was repo created: %w", err)
+ }
+
+ oldRevision := parentCommitOID
+ if targetBranchCommit == "" {
+ oldRevision = git.NullSHA
+ } else if header.Force {
+ oldRevision = targetBranchCommit
+ }
+
+ if err := s.updateReferenceWithHooks(ctx, header.Repository, header.User, targetBranchName, commitID, oldRevision); err != nil {
+ return fmt.Errorf("update reference: %w", err)
+ }
+
+ return stream.SendAndClose(&gitalypb.UserCommitFilesResponse{BranchUpdate: &gitalypb.OperationBranchUpdate{
+ CommitId: commitID,
+ RepoCreated: !hasBranches,
+ BranchCreated: parentCommitOID == "",
+ }})
+}
+
+func (s *server) resolveParentCommit(ctx context.Context, local git.Repository, remote *gitalypb.Repository, targetBranch, targetBranchCommit, startBranch string) (string, error) {
+ if remote == nil && startBranch == "" {
+ return targetBranchCommit, nil
+ }
+
+ repo := local
+ if remote != nil {
+ var err error
+ repo, err = git.NewRemoteRepository(ctx, remote, s.conns)
+ if err != nil {
+ return "", fmt.Errorf("remote repository: %w", err)
+ }
+ }
+
+ branch := targetBranch
+ if startBranch != "" {
+ branch = "refs/heads/" + startBranch
+ }
+
+ return repo.ResolveRefish(ctx, branch+"^{commit}")
+}
+
+func (s *server) fetchMissingCommit(ctx context.Context, local, remote *gitalypb.Repository, commitID string) error {
+ if _, err := git.NewRepository(local).ResolveRefish(ctx, commitID+"^{commit}"); err != nil {
+ if !errors.Is(err, git.ErrReferenceNotFound) || remote == nil {
+ return fmt.Errorf("lookup parent commit: %w", err)
+ }
+
+ if err := s.fetchRemoteObject(ctx, local, remote, commitID); err != nil {
+ return fmt.Errorf("fetch parent commit: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func (s *server) fetchRemoteObject(ctx context.Context, local, remote *gitalypb.Repository, sha string) error {
+ env, err := gitalyssh.UploadPackEnv(ctx, &gitalypb.SSHUploadPackRequest{
+ Repository: remote,
+ GitConfigOptions: []string{"uploadpack.allowAnySHA1InWant=true"},
+ })
+ if err != nil {
+ return fmt.Errorf("upload pack env: %w", err)
+ }
+
+ stderr := &bytes.Buffer{}
+ cmd, err := git.SafeCmdWithEnv(ctx, env, local, nil,
+ git.SubCmd{
+ Name: "fetch",
+ Flags: []git.Option{git.Flag{Name: "--no-tags"}},
+ Args: []string{"ssh://gitaly/internal.git", sha},
+ },
+ git.WithStderr(stderr),
+ )
+ if err != nil {
+ return err
+ }
+
+ if err := cmd.Wait(); err != nil {
+ return errorWithStderr(err, stderr)
+ }
+
+ return nil
+}
+
+func hasBranches(ctx context.Context, repo *gitalypb.Repository) (bool, error) {
+ stderr := &bytes.Buffer{}
+ cmd, err := git.SafeCmd(ctx, repo, nil,
+ git.SubCmd{
+ Name: "show-ref",
+ Flags: []git.Option{git.Flag{Name: "--heads"}, git.Flag{"--dereference"}},
+ },
+ git.WithStderr(stderr),
+ )
+ if err != nil {
+ return false, err
+ }
+
+ if err := cmd.Wait(); err != nil {
+ if status, ok := command.ExitStatus(err); ok && status == 1 {
+ return false, nil
+ }
+
+ return false, errorWithStderr(err, stderr)
+ }
+
+ return true, nil
+}
+
func validateUserCommitFilesHeader(header *gitalypb.UserCommitFilesRequestHeader) error {
if header.GetRepository() == nil {
return fmt.Errorf("empty Repository")
diff --git a/internal/gitaly/service/operations/commit_files_test.go b/internal/gitaly/service/operations/commit_files_test.go
index 740fa5519..029e4e65b 100644
--- a/internal/gitaly/service/operations/commit_files_test.go
+++ b/internal/gitaly/service/operations/commit_files_test.go
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitaly/internal/git/log"
"gitlab.com/gitlab-org/gitaly/internal/helper/text"
+ "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"
@@ -22,14 +23,17 @@ var (
)
func testImplementations(t *testing.T, test func(t *testing.T, ctx context.Context)) {
- ctx, cancel := testhelper.Context()
+ goCtx, cancel := testhelper.Context()
defer cancel()
+ rubyCtx := featureflag.OutgoingCtxWithDisabledFeatureFlags(goCtx, featureflag.GoUserCommitFiles)
+
for _, tc := range []struct {
desc string
context context.Context
}{
- {desc: "ruby", context: ctx},
+ {desc: "go", context: goCtx},
+ {desc: "ruby", context: rubyCtx},
} {
t.Run(tc.desc, func(t *testing.T) { test(t, tc.context) })
}
@@ -1018,52 +1022,64 @@ func testSuccessfulUserCommitFilesRequestStartSha(t *testing.T, ctx context.Cont
}
func TestSuccessfulUserCommitFilesRequestStartShaRemoteRepository(t *testing.T) {
- testImplementations(t, testSuccessfulUserCommitFilesRequestStartShaRemoteRepository)
+ testImplementations(t, testSuccessfulUserCommitFilesRemoteRepositoryRequest(func(header *gitalypb.UserCommitFilesRequest) {
+ setStartSha(header, "1e292f8fedd741b75372e19097c76d327140c312")
+ }))
}
-func testSuccessfulUserCommitFilesRequestStartShaRemoteRepository(t *testing.T, ctx context.Context) {
- serverSocketPath, stop := runOperationServiceServer(t)
- defer stop()
+func TestSuccessfulUserCommitFilesRequestStartBranchRemoteRepository(t *testing.T) {
+ testImplementations(t, testSuccessfulUserCommitFilesRemoteRepositoryRequest(func(header *gitalypb.UserCommitFilesRequest) {
+ setStartBranchName(header, []byte("master"))
+ }))
+}
- client, conn := newOperationClient(t, serverSocketPath)
- defer conn.Close()
+func testSuccessfulUserCommitFilesRemoteRepositoryRequest(setHeader func(header *gitalypb.UserCommitFilesRequest)) func(*testing.T, context.Context) {
+ // Regular table driven test did not work here as there is some state shared in the helpers between the subtests.
+ // Running them in different top level tests works, so we use a parameterized function instead to share the code.
+ return func(t *testing.T, ctx context.Context) {
+ serverSocketPath, stop := runOperationServiceServer(t)
+ defer stop()
- testRepo, _, cleanupFn := testhelper.NewTestRepo(t)
- defer cleanupFn()
+ client, conn := newOperationClient(t, serverSocketPath)
+ defer conn.Close()
- newRepo, _, newRepoCleanupFn := testhelper.InitBareRepo(t)
- defer newRepoCleanupFn()
+ testRepo, _, cleanupFn := testhelper.NewTestRepo(t)
+ defer cleanupFn()
- for key, values := range testhelper.GitalyServersMetadata(t, serverSocketPath) {
- for _, value := range values {
- ctx = metadata.AppendToOutgoingContext(ctx, key, value)
+ newRepo, _, newRepoCleanupFn := testhelper.InitBareRepo(t)
+ defer newRepoCleanupFn()
+
+ for key, values := range testhelper.GitalyServersMetadata(t, serverSocketPath) {
+ for _, value := range values {
+ ctx = metadata.AppendToOutgoingContext(ctx, key, value)
+ }
}
- }
- targetBranchName := "new"
+ targetBranchName := "new"
- startCommit, err := log.GetCommit(ctx, testRepo, "master")
- require.NoError(t, err)
+ startCommit, err := log.GetCommit(ctx, testRepo, "master")
+ require.NoError(t, err)
- headerRequest := headerRequest(newRepo, testhelper.TestUser, targetBranchName, commitFilesMessage)
- setStartSha(headerRequest, startCommit.Id)
- setStartRepository(headerRequest, testRepo)
+ headerRequest := headerRequest(newRepo, testhelper.TestUser, targetBranchName, commitFilesMessage)
+ setHeader(headerRequest)
+ setStartRepository(headerRequest, testRepo)
- stream, err := client.UserCommitFiles(ctx)
- require.NoError(t, err)
- require.NoError(t, stream.Send(headerRequest))
- require.NoError(t, stream.Send(createFileHeaderRequest("TEST.md")))
- require.NoError(t, stream.Send(actionContentRequest("Test")))
+ stream, err := client.UserCommitFiles(ctx)
+ require.NoError(t, err)
+ require.NoError(t, stream.Send(headerRequest))
+ require.NoError(t, stream.Send(createFileHeaderRequest("TEST.md")))
+ require.NoError(t, stream.Send(actionContentRequest("Test")))
- resp, err := stream.CloseAndRecv()
- require.NoError(t, err)
+ resp, err := stream.CloseAndRecv()
+ require.NoError(t, err)
- update := resp.GetBranchUpdate()
- newTargetBranchCommit, err := log.GetCommit(ctx, newRepo, targetBranchName)
- require.NoError(t, err)
+ update := resp.GetBranchUpdate()
+ newTargetBranchCommit, err := log.GetCommit(ctx, newRepo, targetBranchName)
+ require.NoError(t, err)
- require.Equal(t, newTargetBranchCommit.Id, update.CommitId)
- require.Equal(t, newTargetBranchCommit.ParentIds, []string{startCommit.Id})
+ require.Equal(t, newTargetBranchCommit.Id, update.CommitId)
+ require.Equal(t, newTargetBranchCommit.ParentIds, []string{startCommit.Id})
+ }
}
func TestSuccessfulUserCommitFilesRequestWithSpecialCharactersInSignature(t *testing.T) {
diff --git a/internal/gitaly/service/operations/server.go b/internal/gitaly/service/operations/server.go
index 6c2892f19..a568579f6 100644
--- a/internal/gitaly/service/operations/server.go
+++ b/internal/gitaly/service/operations/server.go
@@ -1,6 +1,10 @@
package operations
import (
+ "path/filepath"
+
+ "gitlab.com/gitlab-org/gitaly/client"
+ "gitlab.com/gitlab-org/gitaly/internal/git2go"
"gitlab.com/gitlab-org/gitaly/internal/gitaly/config"
"gitlab.com/gitlab-org/gitaly/internal/gitaly/hook"
"gitlab.com/gitlab-org/gitaly/internal/gitaly/rubyserver"
@@ -13,6 +17,8 @@ type server struct {
ruby *rubyserver.Server
hookManager hook.Manager
locator storage.Locator
+ conns *client.Pool
+ git2go git2go.Executor
}
// NewServer creates a new instance of a grpc OperationServiceServer
@@ -22,5 +28,7 @@ func NewServer(cfg config.Cfg, rs *rubyserver.Server, hookManager hook.Manager,
cfg: cfg,
hookManager: hookManager,
locator: locator,
+ conns: client.NewPool(),
+ git2go: git2go.New(filepath.Join(cfg.BinDir, "gitaly-git2go")),
}
}
diff --git a/internal/metadata/featureflag/feature_flags.go b/internal/metadata/featureflag/feature_flags.go
index 497219873..7b29e6255 100644
--- a/internal/metadata/featureflag/feature_flags.go
+++ b/internal/metadata/featureflag/feature_flags.go
@@ -34,6 +34,8 @@ var (
GoUserSquash = FeatureFlag{Name: "go_user_squash", OnByDefault: false}
// GoListConflictFiles enables the Go implementation of ListConflictFiles
GoListConflictFiles = FeatureFlag{Name: "go_list_conflict_files", OnByDefault: false}
+ // GoUserCommitFiles enables the Go implementation of UserCommitFiles
+ GoUserCommitFiles = FeatureFlag{Name: "go_user_commit_files", OnByDefault: false}
)
// All includes all feature flags.
@@ -49,6 +51,7 @@ var All = []FeatureFlag{
GoUserDeleteBranch,
GoUserSquash,
GoListConflictFiles,
+ GoUserCommitFiles,
}
const (
diff --git a/proto/go/gitalypb/operations.pb.go b/proto/go/gitalypb/operations.pb.go
index 0a0f73fd9..8ce862f77 100644
--- a/proto/go/gitalypb/operations.pb.go
+++ b/proto/go/gitalypb/operations.pb.go
@@ -83,12 +83,18 @@ func (UserRevertResponse_CreateTreeError) EnumDescriptor() ([]byte, []int) {
type UserCommitFilesActionHeader_ActionType int32
const (
- UserCommitFilesActionHeader_CREATE UserCommitFilesActionHeader_ActionType = 0
+ // CREATE creates a new file.
+ UserCommitFilesActionHeader_CREATE UserCommitFilesActionHeader_ActionType = 0
+ // CREATE_DIR creates a new directory.
UserCommitFilesActionHeader_CREATE_DIR UserCommitFilesActionHeader_ActionType = 1
- UserCommitFilesActionHeader_UPDATE UserCommitFilesActionHeader_ActionType = 2
- UserCommitFilesActionHeader_MOVE UserCommitFilesActionHeader_ActionType = 3
- UserCommitFilesActionHeader_DELETE UserCommitFilesActionHeader_ActionType = 4
- UserCommitFilesActionHeader_CHMOD UserCommitFilesActionHeader_ActionType = 5
+ // UPDATE updates an existing file.
+ UserCommitFilesActionHeader_UPDATE UserCommitFilesActionHeader_ActionType = 2
+ // MOVE moves an existing file to a new path.
+ UserCommitFilesActionHeader_MOVE UserCommitFilesActionHeader_ActionType = 3
+ // DELETE deletes an existing file.
+ UserCommitFilesActionHeader_DELETE UserCommitFilesActionHeader_ActionType = 4
+ // CHMOD changes the permissions of an existing file.
+ UserCommitFilesActionHeader_CHMOD UserCommitFilesActionHeader_ActionType = 5
)
var UserCommitFilesActionHeader_ActionType_name = map[int32]string{
@@ -943,12 +949,15 @@ func (m *UserMergeToRefResponse) GetPreReceiveError() string {
return ""
}
+// OperationBranchUpdate contains the details of a branch update.
type OperationBranchUpdate struct {
- // If this string is non-empty the branch has been updated.
+ // commit_id is set to the OID of the created commit if a branch was created or updated.
CommitId string `protobuf:"bytes,1,opt,name=commit_id,json=commitId,proto3" json:"commit_id,omitempty"`
- // Used for cache invalidation in GitLab
+ // repo_created indicates whether the branch created was the first one in the repository.
+ // Used for cache invalidation in GitLab.
RepoCreated bool `protobuf:"varint,2,opt,name=repo_created,json=repoCreated,proto3" json:"repo_created,omitempty"`
- // Used for cache invalidation in GitLab
+ // branch_created indicates whether the branch already existed in the repository
+ // and was updated or whether it was created. Used for cache invalidation in GitLab.
BranchCreated bool `protobuf:"varint,3,opt,name=branch_created,json=branchCreated,proto3" json:"branch_created,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
@@ -1443,12 +1452,29 @@ func (m *UserRevertResponse) GetCreateTreeErrorCode() UserRevertResponse_CreateT
return UserRevertResponse_NONE
}
+// UserCommitFilesActionHeader contains the details of the action to be performed.
type UserCommitFilesActionHeader struct {
- Action UserCommitFilesActionHeader_ActionType `protobuf:"varint,1,opt,name=action,proto3,enum=gitaly.UserCommitFilesActionHeader_ActionType" json:"action,omitempty"`
- FilePath []byte `protobuf:"bytes,2,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"`
- PreviousPath []byte `protobuf:"bytes,3,opt,name=previous_path,json=previousPath,proto3" json:"previous_path,omitempty"`
- Base64Content bool `protobuf:"varint,4,opt,name=base64_content,json=base64Content,proto3" json:"base64_content,omitempty"`
- ExecuteFilemode bool `protobuf:"varint,5,opt,name=execute_filemode,json=executeFilemode,proto3" json:"execute_filemode,omitempty"`
+ // action is the type of the action taken to build a commit. Not all fields are
+ // used for all of the actions.
+ Action UserCommitFilesActionHeader_ActionType `protobuf:"varint,1,opt,name=action,proto3,enum=gitaly.UserCommitFilesActionHeader_ActionType" json:"action,omitempty"`
+ // file_path refers to the file or directory being modified. The meaning differs for each
+ // action:
+ // 1. CREATE: path of the file to create
+ // 2. CREATE_DIR: path of the directory to create
+ // 3. UPDATE: path of the file to update
+ // 4. MOVE: the new path of the moved file
+ // 5. DELETE: path of the file to delete
+ // 6. CHMOD: path of the file to modify permissions for
+ FilePath []byte `protobuf:"bytes,2,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"`
+ // previous_path is used in MOVE action to specify the path of the file to move.
+ PreviousPath []byte `protobuf:"bytes,3,opt,name=previous_path,json=previousPath,proto3" json:"previous_path,omitempty"`
+ // base64_content indicates the content of the file is base64 encoded. The encoding
+ // must be the standard base64 encoding defined in RFC 4648. Only used for CREATE and
+ // UPDATE actions.
+ Base64Content bool `protobuf:"varint,4,opt,name=base64_content,json=base64Content,proto3" json:"base64_content,omitempty"`
+ // execute_filemode determines whether the file is created with execute permissions.
+ // The field is only used in CREATE and CHMOD actions.
+ ExecuteFilemode bool `protobuf:"varint,5,opt,name=execute_filemode,json=executeFilemode,proto3" json:"execute_filemode,omitempty"`
// Move actions that change the file path, but not its content, should set
// infer_content to true instead of populating the content field. Ignored for
// other action types.
@@ -1525,6 +1551,7 @@ func (m *UserCommitFilesActionHeader) GetInferContent() bool {
return false
}
+// UserCommitFilesAction is the request message used to stream in the actions to build a commit.
type UserCommitFilesAction struct {
// Types that are valid to be assigned to UserCommitFilesActionPayload:
// *UserCommitFilesAction_Header
@@ -1605,20 +1632,39 @@ func (*UserCommitFilesAction) XXX_OneofWrappers() []interface{} {
}
}
+// UserCommitFilesRequestHeader is the header of the UserCommitFiles that defines the commit details,
+// parent and other information related to the call.
type UserCommitFilesRequestHeader struct {
- Repository *Repository `protobuf:"bytes,1,opt,name=repository,proto3" json:"repository,omitempty"`
- User *User `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"`
- BranchName []byte `protobuf:"bytes,3,opt,name=branch_name,json=branchName,proto3" json:"branch_name,omitempty"`
- CommitMessage []byte `protobuf:"bytes,4,opt,name=commit_message,json=commitMessage,proto3" json:"commit_message,omitempty"`
- CommitAuthorName []byte `protobuf:"bytes,5,opt,name=commit_author_name,json=commitAuthorName,proto3" json:"commit_author_name,omitempty"`
- CommitAuthorEmail []byte `protobuf:"bytes,6,opt,name=commit_author_email,json=commitAuthorEmail,proto3" json:"commit_author_email,omitempty"`
- StartBranchName []byte `protobuf:"bytes,7,opt,name=start_branch_name,json=startBranchName,proto3" json:"start_branch_name,omitempty"`
- StartRepository *Repository `protobuf:"bytes,8,opt,name=start_repository,json=startRepository,proto3" json:"start_repository,omitempty"`
- Force bool `protobuf:"varint,9,opt,name=force,proto3" json:"force,omitempty"`
- StartSha string `protobuf:"bytes,10,opt,name=start_sha,json=startSha,proto3" json:"start_sha,omitempty"`
- XXX_NoUnkeyedLiteral struct{} `json:"-"`
- XXX_unrecognized []byte `json:"-"`
- XXX_sizecache int32 `json:"-"`
+ // repository is the target repository where to apply the commit.
+ Repository *Repository `protobuf:"bytes,1,opt,name=repository,proto3" json:"repository,omitempty"`
+ // user is the user peforming the call.
+ User *User `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"`
+ // branch_name is the name of the branch to point to the new commit. If start_sha and start_branch_name
+ // are not defined, the commit of branch_name is used as the parent commit.
+ BranchName []byte `protobuf:"bytes,3,opt,name=branch_name,json=branchName,proto3" json:"branch_name,omitempty"`
+ // commit_message is the message to use in the commit.
+ CommitMessage []byte `protobuf:"bytes,4,opt,name=commit_message,json=commitMessage,proto3" json:"commit_message,omitempty"`
+ // commit_author_name is the commit author's name. If not provided, the user's name is
+ // used instead.
+ CommitAuthorName []byte `protobuf:"bytes,5,opt,name=commit_author_name,json=commitAuthorName,proto3" json:"commit_author_name,omitempty"`
+ // commit_author_email is the commit author's email. If not provided, the user's email is
+ // used instead.
+ CommitAuthorEmail []byte `protobuf:"bytes,6,opt,name=commit_author_email,json=commitAuthorEmail,proto3" json:"commit_author_email,omitempty"`
+ // start_branch_name specifies the branch whose commit to use as the parent commit. Takes priority
+ // over branch_name. Optional.
+ StartBranchName []byte `protobuf:"bytes,7,opt,name=start_branch_name,json=startBranchName,proto3" json:"start_branch_name,omitempty"`
+ // start_repository specifies which contains the parent commit. If not specified, repository itself
+ // is used to look up the parent commit. Optional.
+ StartRepository *Repository `protobuf:"bytes,8,opt,name=start_repository,json=startRepository,proto3" json:"start_repository,omitempty"`
+ // force determines whether to force update the target branch specified by branch_name to
+ // point to the new commit.
+ Force bool `protobuf:"varint,9,opt,name=force,proto3" json:"force,omitempty"`
+ // start_sha specifies the SHA of the commit to use as the parent of new commit. Takes priority
+ // over start_branch_name and branc_name. Optional.
+ StartSha string `protobuf:"bytes,10,opt,name=start_sha,json=startSha,proto3" json:"start_sha,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
}
func (m *UserCommitFilesRequestHeader) Reset() { *m = UserCommitFilesRequestHeader{} }
@@ -1716,6 +1762,7 @@ func (m *UserCommitFilesRequestHeader) GetStartSha() string {
return ""
}
+// UserCommitFiles is the request of UserCommitFiles.
type UserCommitFilesRequest struct {
// Types that are valid to be assigned to UserCommitFilesRequestPayload:
// *UserCommitFilesRequest_Header
@@ -1796,13 +1843,18 @@ func (*UserCommitFilesRequest) XXX_OneofWrappers() []interface{} {
}
}
+// UserCommitFilesResponse is the response object of UserCommitFiles.
type UserCommitFilesResponse struct {
- BranchUpdate *OperationBranchUpdate `protobuf:"bytes,1,opt,name=branch_update,json=branchUpdate,proto3" json:"branch_update,omitempty"`
- IndexError string `protobuf:"bytes,2,opt,name=index_error,json=indexError,proto3" json:"index_error,omitempty"`
- PreReceiveError string `protobuf:"bytes,3,opt,name=pre_receive_error,json=preReceiveError,proto3" json:"pre_receive_error,omitempty"`
- XXX_NoUnkeyedLiteral struct{} `json:"-"`
- XXX_unrecognized []byte `json:"-"`
- XXX_sizecache int32 `json:"-"`
+ // branch_update contains the details of the commit and the branch update.
+ BranchUpdate *OperationBranchUpdate `protobuf:"bytes,1,opt,name=branch_update,json=branchUpdate,proto3" json:"branch_update,omitempty"`
+ // index_error is set to the error message when an invalid action was attempted, such as
+ // trying to create a file that already existed.
+ IndexError string `protobuf:"bytes,2,opt,name=index_error,json=indexError,proto3" json:"index_error,omitempty"`
+ // pre_receive_error is set when the pre-receive hook errored.
+ PreReceiveError string `protobuf:"bytes,3,opt,name=pre_receive_error,json=preReceiveError,proto3" json:"pre_receive_error,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
}
func (m *UserCommitFilesResponse) Reset() { *m = UserCommitFilesResponse{} }
@@ -2772,6 +2824,10 @@ type OperationServiceClient interface {
UserMergeBranch(ctx context.Context, opts ...grpc.CallOption) (OperationService_UserMergeBranchClient, error)
UserFFBranch(ctx context.Context, in *UserFFBranchRequest, opts ...grpc.CallOption) (*UserFFBranchResponse, error)
UserCherryPick(ctx context.Context, in *UserCherryPickRequest, opts ...grpc.CallOption) (*UserCherryPickResponse, error)
+ // UserCommitFiles builds a commit from a stream of actions and updates the target branch to point to it.
+ // UserCommitFilesRequest with a UserCommitFilesRequestHeader must be sent as the first message of the stream.
+ // Following that, a variable number of actions can be sent to build a new commit. Each action consists of
+ // a header followed by content if used by the action.
UserCommitFiles(ctx context.Context, opts ...grpc.CallOption) (OperationService_UserCommitFilesClient, error)
UserRebaseConfirmable(ctx context.Context, opts ...grpc.CallOption) (OperationService_UserRebaseConfirmableClient, error)
UserRevert(ctx context.Context, in *UserRevertRequest, opts ...grpc.CallOption) (*UserRevertResponse, error)
@@ -3028,6 +3084,10 @@ type OperationServiceServer interface {
UserMergeBranch(OperationService_UserMergeBranchServer) error
UserFFBranch(context.Context, *UserFFBranchRequest) (*UserFFBranchResponse, error)
UserCherryPick(context.Context, *UserCherryPickRequest) (*UserCherryPickResponse, error)
+ // UserCommitFiles builds a commit from a stream of actions and updates the target branch to point to it.
+ // UserCommitFilesRequest with a UserCommitFilesRequestHeader must be sent as the first message of the stream.
+ // Following that, a variable number of actions can be sent to build a new commit. Each action consists of
+ // a header followed by content if used by the action.
UserCommitFiles(OperationService_UserCommitFilesServer) error
UserRebaseConfirmable(OperationService_UserRebaseConfirmableServer) error
UserRevert(context.Context, *UserRevertRequest) (*UserRevertResponse, error)
diff --git a/proto/operations.proto b/proto/operations.proto
index d2a2680d6..741e43ad5 100644
--- a/proto/operations.proto
+++ b/proto/operations.proto
@@ -53,6 +53,11 @@ service OperationService {
op: MUTATOR
};
}
+
+ // UserCommitFiles builds a commit from a stream of actions and updates the target branch to point to it.
+ // UserCommitFilesRequest with a UserCommitFilesRequestHeader must be sent as the first message of the stream.
+ // Following that, a variable number of actions can be sent to build a new commit. Each action consists of
+ // a header followed by content if used by the action.
rpc UserCommitFiles(stream UserCommitFilesRequest) returns (UserCommitFilesResponse) {
option (op_type) = {
op: MUTATOR
@@ -193,12 +198,15 @@ message UserMergeToRefResponse {
string pre_receive_error = 2;
}
+// OperationBranchUpdate contains the details of a branch update.
message OperationBranchUpdate {
- // If this string is non-empty the branch has been updated.
+ // commit_id is set to the OID of the created commit if a branch was created or updated.
string commit_id = 1;
- // Used for cache invalidation in GitLab
+ // repo_created indicates whether the branch created was the first one in the repository.
+ // Used for cache invalidation in GitLab.
bool repo_created = 2;
- // Used for cache invalidation in GitLab
+ // branch_created indicates whether the branch already existed in the repository
+ // and was updated or whether it was created. Used for cache invalidation in GitLab.
bool branch_created = 3;
}
@@ -264,19 +272,42 @@ message UserRevertResponse {
CreateTreeError create_tree_error_code = 5;
}
+// UserCommitFilesActionHeader contains the details of the action to be performed.
message UserCommitFilesActionHeader {
enum ActionType {
+ // CREATE creates a new file.
CREATE = 0;
+ // CREATE_DIR creates a new directory.
CREATE_DIR = 1;
+ // UPDATE updates an existing file.
UPDATE = 2;
+ // MOVE moves an existing file to a new path.
MOVE = 3;
+ // DELETE deletes an existing file.
DELETE = 4;
+ // CHMOD changes the permissions of an existing file.
CHMOD = 5;
}
+ // action is the type of the action taken to build a commit. Not all fields are
+ // used for all of the actions.
ActionType action = 1;
+ // file_path refers to the file or directory being modified. The meaning differs for each
+ // action:
+ // 1. CREATE: path of the file to create
+ // 2. CREATE_DIR: path of the directory to create
+ // 3. UPDATE: path of the file to update
+ // 4. MOVE: the new path of the moved file
+ // 5. DELETE: path of the file to delete
+ // 6. CHMOD: path of the file to modify permissions for
bytes file_path = 2;
+ // previous_path is used in MOVE action to specify the path of the file to move.
bytes previous_path = 3;
+ // base64_content indicates the content of the file is base64 encoded. The encoding
+ // must be the standard base64 encoding defined in RFC 4648. Only used for CREATE and
+ // UPDATE actions.
bool base64_content = 4;
+ // execute_filemode determines whether the file is created with execute permissions.
+ // The field is only used in CREATE and CHMOD actions.
bool execute_filemode = 5;
// Move actions that change the file path, but not its content, should set
// infer_content to true instead of populating the content field. Ignored for
@@ -284,38 +315,69 @@ message UserCommitFilesActionHeader {
bool infer_content = 6;
}
+// UserCommitFilesAction is the request message used to stream in the actions to build a commit.
message UserCommitFilesAction {
oneof user_commit_files_action_payload {
+ // header contains the details of action being performed. Header must be sent before the
+ // content if content is used by the action.
UserCommitFilesActionHeader header = 1;
+ // content is the content of the file streamed in one or more messages. Only used with CREATE
+ // and UPDATE actions.
bytes content = 2;
}
}
+// UserCommitFilesRequestHeader is the header of the UserCommitFiles that defines the commit details,
+// parent and other information related to the call.
message UserCommitFilesRequestHeader {
+ // repository is the target repository where to apply the commit.
Repository repository = 1 [(target_repository)=true];
+ // user is the user peforming the call.
User user = 2;
+ // branch_name is the name of the branch to point to the new commit. If start_sha and start_branch_name
+ // are not defined, the commit of branch_name is used as the parent commit.
bytes branch_name = 3;
+ // commit_message is the message to use in the commit.
bytes commit_message = 4;
+ // commit_author_name is the commit author's name. If not provided, the user's name is
+ // used instead.
bytes commit_author_name = 5;
+ // commit_author_email is the commit author's email. If not provided, the user's email is
+ // used instead.
bytes commit_author_email = 6;
+ // start_branch_name specifies the branch whose commit to use as the parent commit. Takes priority
+ // over branch_name. Optional.
bytes start_branch_name = 7;
+ // start_repository specifies which contains the parent commit. If not specified, repository itself
+ // is used to look up the parent commit. Optional.
Repository start_repository = 8;
+ // force determines whether to force update the target branch specified by branch_name to
+ // point to the new commit.
bool force = 9;
+ // start_sha specifies the SHA of the commit to use as the parent of new commit. Takes priority
+ // over start_branch_name and branc_name. Optional.
string start_sha = 10;
}
+// UserCommitFiles is the request of UserCommitFiles.
message UserCommitFilesRequest {
oneof user_commit_files_request_payload {
- // For each request stream there should be first a request with a header and
- // then n requests with actions
+ // header defines the details of where to comnit, the details and which commit to use as the parent.
+ // header must always be sent as the first request of the stream.
UserCommitFilesRequestHeader header = 1;
+ // action contains an action to build a commit. There can be multiple actions per stream.
UserCommitFilesAction action = 2;
}
}
+// UserCommitFilesResponse is the response object of UserCommitFiles.
message UserCommitFilesResponse {
+ // branch_update contains the details of the commit and the branch update.
OperationBranchUpdate branch_update = 1;
+ // index_error is set to the error message when an invalid action was attempted, such as
+ // trying to create a file that already existed.
string index_error = 2;
+ // pre_receive_error is set when the pre-receive hook errored.
string pre_receive_error = 3;
}
diff --git a/ruby/proto/gitaly/operations_services_pb.rb b/ruby/proto/gitaly/operations_services_pb.rb
index c16b3a13c..3c98fd830 100644
--- a/ruby/proto/gitaly/operations_services_pb.rb
+++ b/ruby/proto/gitaly/operations_services_pb.rb
@@ -23,6 +23,10 @@ module Gitaly
rpc :UserMergeBranch, stream(Gitaly::UserMergeBranchRequest), stream(Gitaly::UserMergeBranchResponse)
rpc :UserFFBranch, Gitaly::UserFFBranchRequest, Gitaly::UserFFBranchResponse
rpc :UserCherryPick, Gitaly::UserCherryPickRequest, Gitaly::UserCherryPickResponse
+ # UserCommitFiles builds a commit from a stream of actions and updates the target branch to point to it.
+ # UserCommitFilesRequest with a UserCommitFilesRequestHeader must be sent as the first message of the stream.
+ # Following that, a variable number of actions can be sent to build a new commit. Each action consists of
+ # a header followed by content if used by the action.
rpc :UserCommitFiles, stream(Gitaly::UserCommitFilesRequest), Gitaly::UserCommitFilesResponse
rpc :UserRebaseConfirmable, stream(Gitaly::UserRebaseConfirmableRequest), stream(Gitaly::UserRebaseConfirmableResponse)
rpc :UserRevert, Gitaly::UserRevertRequest, Gitaly::UserRevertResponse