diff options
-rw-r--r-- | changelogs/unreleased/smh-port-user-commit-files.yml | 5 | ||||
-rw-r--r-- | internal/gitaly/service/operations/commit_files.go | 356 | ||||
-rw-r--r-- | internal/gitaly/service/operations/commit_files_test.go | 84 | ||||
-rw-r--r-- | internal/gitaly/service/operations/server.go | 8 | ||||
-rw-r--r-- | internal/metadata/featureflag/feature_flags.go | 3 | ||||
-rw-r--r-- | proto/go/gitalypb/operations.pb.go | 124 | ||||
-rw-r--r-- | proto/operations.proto | 72 | ||||
-rw-r--r-- | ruby/proto/gitaly/operations_services_pb.rb | 4 |
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 |