internal/gitaly/service/operations/commit_files.go (730 lines of code) (raw):
package operations
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"gitlab.com/gitlab-org/gitaly/v16/internal/git"
"gitlab.com/gitlab-org/gitaly/v16/internal/git/gitcmd"
"gitlab.com/gitlab-org/gitaly/v16/internal/git/localrepo"
"gitlab.com/gitlab-org/gitaly/v16/internal/git/remoterepo"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/hook/updateref"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage"
"gitlab.com/gitlab-org/gitaly/v16/internal/log"
"gitlab.com/gitlab-org/gitaly/v16/internal/structerr"
"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
)
// unknownIndexError is an unspecified error that was produced by performing an invalid operation on the index.
type unknownIndexError string
// Error returns the error message of the unknown index error.
func (err unknownIndexError) Error() string { return string(err) }
// indexErrorType specifies which of the known index error types has occurred.
type indexErrorType uint
const (
// ErrDirectoryExists represent a directory exists error.
errDirectoryExists indexErrorType = iota
// ErrDirectoryTraversal represent a directory traversal error.
errDirectoryTraversal
// ErrEmptyPath represent an empty path error.
errEmptyPath
// ErrFileExists represent a file exists error.
errFileExists
// ErrFileNotFound represent a file not found error.
errFileNotFound
// ErrInvalidPath represent an invalid path error.
errInvalidPath
)
// IndexError is a well-defined error that was produced by performing an invalid operation on the index.
type indexError struct {
path string
errorType indexErrorType
}
// Error returns the error message associated with the error type.
func (err indexError) Error() string {
switch err.errorType {
case errDirectoryExists:
return "A directory with this name already exists"
case errDirectoryTraversal:
return "Path cannot include directory traversal"
case errEmptyPath:
return "You must provide a file path"
case errFileExists:
return "A file with this name already exists"
case errFileNotFound:
return "A file with this name doesn't exist"
case errInvalidPath:
return fmt.Sprintf("invalid path: %q", err.path)
default:
panic(fmt.Sprintf("unhandled IndexErrorType: %v", err.errorType))
}
}
// Proto returns the Protobuf representation of this error.
func (err indexError) Proto() *gitalypb.IndexError {
errType := gitalypb.IndexError_ERROR_TYPE_UNSPECIFIED
switch err.errorType {
case errDirectoryExists:
errType = gitalypb.IndexError_ERROR_TYPE_DIRECTORY_EXISTS
case errDirectoryTraversal:
errType = gitalypb.IndexError_ERROR_TYPE_DIRECTORY_TRAVERSAL
case errEmptyPath:
errType = gitalypb.IndexError_ERROR_TYPE_EMPTY_PATH
case errFileExists:
errType = gitalypb.IndexError_ERROR_TYPE_FILE_EXISTS
case errFileNotFound:
errType = gitalypb.IndexError_ERROR_TYPE_FILE_NOT_FOUND
case errInvalidPath:
errType = gitalypb.IndexError_ERROR_TYPE_INVALID_PATH
}
return &gitalypb.IndexError{
Path: []byte(err.path),
ErrorType: errType,
}
}
// StructuredError returns the structured error.
func (err indexError) StructuredError() structerr.Error {
e := errors.New(err.Error())
switch err.errorType {
case errDirectoryExists, errFileExists:
return structerr.NewAlreadyExists("%w", e)
case errDirectoryTraversal, errEmptyPath, errInvalidPath:
return structerr.NewInvalidArgument("%w", e)
case errFileNotFound:
return structerr.NewNotFound("%w", e)
default:
return structerr.NewInternal("%w", e)
}
}
// invalidArgumentError is returned when an invalid argument is provided.
type invalidArgumentError string
func (err invalidArgumentError) Error() string { return string(err) }
// UserCommitFiles allows for committing from a set of actions. See the protobuf documentation
// for details.
func (s *Server) UserCommitFiles(stream gitalypb.OperationService_UserCommitFilesServer) error {
ctx := stream.Context()
firstRequest, err := stream.Recv()
if err != nil {
return err
}
header := firstRequest.GetHeader()
if header == nil {
return structerr.NewInvalidArgument("empty UserCommitFilesRequestHeader")
}
if err := s.locator.ValidateRepository(ctx, header.GetRepository()); err != nil {
return structerr.NewInvalidArgument("%w", err)
}
repo := s.localRepoFactory.Build(header.GetRepository())
objectHash, err := repo.ObjectHash(ctx)
if err != nil {
return fmt.Errorf("detecting object hash: %w", err)
}
if err := validateUserCommitFilesHeader(header, objectHash); err != nil {
return structerr.NewInvalidArgument("%w", err)
}
if err := s.userCommitFiles(ctx, header, stream, objectHash); err != nil {
if fields := log.CustomFieldsFromContext(ctx); fields != nil {
fields.RecordMetadata("repository_storage", header.GetRepository().GetStorageName())
fields.RecordMetadata("repository_relative_path", header.GetRepository().GetRelativePath())
fields.RecordMetadata("branch_name", header.GetBranchName())
fields.RecordMetadata("start_branch_name", header.GetStartBranchName())
fields.RecordMetadata("start_sha", header.GetStartSha())
fields.RecordMetadata("force", header.GetForce())
}
if startRepo := header.GetStartRepository(); startRepo != nil {
if fields := log.CustomFieldsFromContext(ctx); fields != nil {
fields.RecordMetadata("start_repository_storage", startRepo.GetStorageName())
fields.RecordMetadata("start_repository_relative_path", startRepo.GetRelativePath())
}
}
var (
unknownErr unknownIndexError
indexErr indexError
customHookErr updateref.CustomHookError
)
switch {
case errors.As(err, &unknownErr):
// Problems that occur within git2go itself will still be returned
// as UnknownIndexErrors. The most common case of this would be
// creating an invalid path, e.g. '.git' but there are many other
// potential, if unusual, issues that could occur.
return unknownErr
case errors.As(err, &indexErr):
return indexErr.StructuredError().WithDetail(
&gitalypb.UserCommitFilesError{
Error: &gitalypb.UserCommitFilesError_IndexUpdate{
IndexUpdate: indexErr.Proto(),
},
},
)
case errors.As(err, &customHookErr):
return structerr.NewPermissionDenied("denied by custom hooks: %w", err).WithDetail(
&gitalypb.UserCommitFilesError{
Error: &gitalypb.UserCommitFilesError_CustomHook{
CustomHook: customHookErr.Proto(),
},
},
)
case errors.As(err, new(invalidArgumentError)):
return structerr.NewInvalidArgument("%w", err)
default:
return err
}
}
return nil
}
func validatePath(rootPath, relPath string) (string, error) {
if relPath == "" {
return "", indexError{errorType: errEmptyPath}
} else if strings.Contains(relPath, "//") {
// This is a workaround to address a quirk in porting the RPC from Ruby to Go.
// GitLab's QA pipeline runs tests with filepath 'invalid://file/name/here'.
// Go's filepath.Clean returns 'invalid:/file/name/here'. The Ruby implementation's
// filepath normalization accepted the path as is. Adding a file with this path to the
// index via Rugged failed with an invalid path error. As Go's cleaning resulted a valid
// filepath, adding the file succeeded, which made the QA pipeline's specs fail.
//
// The Rails code expects to receive an error prefixed with 'invalid path', which is done
// here to retain compatibility.
return "", indexError{errorType: errInvalidPath, path: relPath}
}
path, err := storage.ValidateRelativePath(rootPath, relPath)
if err != nil {
if errors.Is(err, storage.ErrRelativePathEscapesRoot) {
return "", indexError{errorType: errDirectoryTraversal, path: relPath}
}
return "", err
}
return path, nil
}
// applyAction applies an action to an TreeEntry.
func applyAction(
ctx context.Context,
action commitAction,
root *localrepo.TreeEntry,
repo *localrepo.Repo,
) error {
switch action := action.(type) {
case changeFileMode:
if err := root.Modify(
action.Path,
func(entry *localrepo.TreeEntry) error {
if action.ExecutableMode {
if entry.Mode != "100755" {
entry.Mode = "100755"
}
} else {
if entry.Mode == "100755" {
entry.Mode = "100644"
}
}
return nil
}); err != nil {
return translateError(err, action.Path)
}
case updateFile:
if err := root.Modify(
action.Path,
func(entry *localrepo.TreeEntry) error {
entry.OID = git.ObjectID(action.OID)
return nil
}); err != nil {
return translateError(err, action.Path)
}
case moveFile:
entry, err := root.Get(action.Path)
if err != nil {
return translateError(err, action.Path)
}
if entry.Type != localrepo.Blob {
return indexError{
path: action.Path,
errorType: errFileNotFound,
}
}
mode := entry.Mode
if action.OID == "" {
action.OID = string(entry.OID)
}
if err := root.Delete(action.Path); err != nil {
return translateError(err, action.Path)
}
if err := root.Add(
action.NewPath,
localrepo.TreeEntry{
OID: git.ObjectID(action.OID),
Mode: mode,
Path: filepath.Base(action.NewPath),
},
localrepo.WithOverwriteDirectory(),
); err != nil {
return translateError(err, action.NewPath)
}
case createDirectory:
if entry, err := root.Get(action.Path); err != nil && !errors.Is(err, localrepo.ErrEntryNotFound) {
return translateError(err, action.Path)
} else if entry != nil {
switch entry.Type {
case localrepo.Tree, localrepo.Submodule:
return indexError{
path: action.Path,
errorType: errDirectoryExists,
}
default:
return indexError{
path: action.Path,
errorType: errFileExists,
}
}
}
blobID, err := repo.WriteBlob(ctx, strings.NewReader(""), localrepo.WriteBlobConfig{
Path: filepath.Join(action.Path, ".gitkeep"),
})
if err != nil {
return err
}
if err := root.Add(
filepath.Join(action.Path, ".gitkeep"),
localrepo.TreeEntry{
Mode: "100644",
Path: ".gitkeep",
Type: localrepo.Blob,
OID: blobID,
},
); err != nil {
if errors.Is(err, localrepo.ErrEntryExists) {
return indexError{
path: action.Path,
errorType: errDirectoryExists,
}
}
return translateError(err, action.Path)
}
case createFile:
mode := "100644"
if action.ExecutableMode {
mode = "100755"
}
if err := root.Add(
action.Path,
localrepo.TreeEntry{
OID: git.ObjectID(action.OID),
Path: filepath.Base(action.Path),
Type: localrepo.Blob,
Mode: mode,
},
localrepo.WithOverwriteDirectory(),
); err != nil {
return translateError(err, action.Path)
}
case deleteFile:
if err := root.Delete(
action.Path,
); err != nil {
return translateError(err, action.Path)
}
default:
return errors.New("unsupported action")
}
return nil
}
// translateLocalrepoError converts errors returned by the `localrepo` package into nice errors that we can return to the caller.
// Most importantly, these errors will carry metadata that helps to figure out what exactly has gone wrong.
func translateError(err error, path string) error {
switch {
case errors.Is(err, localrepo.ErrEntryNotFound) ||
errors.Is(err, localrepo.ErrObjectNotFound):
return indexError{
path: path,
errorType: errFileNotFound,
}
case errors.Is(err, localrepo.ErrEmptyPath) ||
errors.Is(err, localrepo.ErrPathTraversal) ||
errors.Is(err, localrepo.ErrAbsolutePath) ||
errors.Is(err, localrepo.ErrDisallowedPath):
//The error coming back from git2go has the path in single
//quotes. This is to match the git2go error for now.
//nolint:gitaly-linters
return unknownIndexError(
fmt.Sprintf("invalid path: '%s'", path),
)
case errors.Is(err, localrepo.ErrPathTraversal):
return indexError{
path: path,
errorType: errDirectoryTraversal,
}
case errors.Is(err, localrepo.ErrEntryExists):
return indexError{
path: path,
errorType: errFileExists,
}
}
return err
}
var errSignatureMissingNameOrEmail = errors.New(
"commit: failed to parse signature - Signature cannot have an empty name or email",
)
func (s *Server) userCommitFilesGit(
ctx context.Context,
header *gitalypb.UserCommitFilesRequestHeader,
parentCommitOID git.ObjectID,
quarantineRepo *localrepo.Repo,
repoPath string,
actions []commitAction,
) (git.ObjectID, error) {
committerSignature, err := git.SignatureFromRequest(header)
if err != nil {
return "", structerr.NewInvalidArgument("%w", err)
}
var treeish git.ObjectID
if parentCommitOID != "" {
treeish, err = quarantineRepo.ResolveRevision(
ctx,
git.Revision(fmt.Sprintf("%s^{tree}", parentCommitOID)),
)
if err != nil {
return "", fmt.Errorf("getting tree id: %w", err)
}
}
treeEntry := &localrepo.TreeEntry{
Mode: "040000",
Type: localrepo.Tree,
}
if treeish != "" {
treeEntry, err = quarantineRepo.ReadTree(
ctx,
git.Revision(treeish),
localrepo.WithRecursive(),
)
if err != nil {
return "", fmt.Errorf("reading tree: %w", err)
}
}
for _, action := range actions {
if err = applyAction(
ctx,
action,
treeEntry,
quarantineRepo,
); err != nil {
return "", fmt.Errorf("performing action %T: %w", action, err)
}
}
if err := treeEntry.Write(
ctx,
quarantineRepo,
); err != nil {
return "", fmt.Errorf("writing tree %w", err)
}
treeish = treeEntry.OID
if treeish == "" {
objectHash, err := quarantineRepo.ObjectHash(ctx)
if err != nil {
return "", fmt.Errorf("getting object hash: %w", err)
}
treeish = objectHash.EmptyTreeOID
}
cfg := localrepo.WriteCommitConfig{
AuthorDate: committerSignature.When,
AuthorName: strings.TrimSpace(string(header.GetCommitAuthorName())),
AuthorEmail: strings.TrimSpace(string(header.GetCommitAuthorEmail())),
CommitterDate: committerSignature.When,
CommitterName: committerSignature.Name,
CommitterEmail: committerSignature.Email,
Message: string(header.GetCommitMessage()),
TreeID: treeish,
GitConfig: s.gitConfig,
Sign: header.GetSign(),
}
if cfg.AuthorName == "" {
cfg.AuthorName = cfg.CommitterName
}
if cfg.AuthorEmail == "" {
cfg.AuthorEmail = cfg.CommitterEmail
}
if cfg.AuthorName == "" || cfg.AuthorEmail == "" {
return "", structerr.NewInvalidArgument("%w", errSignatureMissingNameOrEmail)
}
if parentCommitOID != "" {
cfg.Parents = []git.ObjectID{parentCommitOID}
}
return quarantineRepo.WriteCommit(
ctx,
cfg,
)
}
func (s *Server) userCommitFiles(
ctx context.Context,
header *gitalypb.UserCommitFilesRequestHeader,
stream gitalypb.OperationService_UserCommitFilesServer,
objectHash git.ObjectHash,
) error {
quarantineDir, quarantineRepo, err := s.quarantinedRepo(ctx, header.GetRepository())
if err != nil {
return err
}
repoPath, err := quarantineRepo.Path(ctx)
if err != nil {
return err
}
remoteRepo := header.GetStartRepository()
if sameRepository(header.GetRepository(), remoteRepo) {
// Some requests set a StartRepository that refers to the same repository as the target repository.
// This check never works behind Praefect. See: https://gitlab.com/gitlab-org/gitaly/-/issues/3294
// Plain Gitalies still benefit from identifying the case and avoiding unnecessary RPC to resolve the
// branch.
remoteRepo = nil
}
targetBranchName := git.NewReferenceNameFromBranchName(string(header.GetBranchName()))
targetBranchCommit, err := quarantineRepo.ResolveRevision(ctx, targetBranchName.Revision()+"^{commit}")
if err != nil {
if !errors.Is(err, git.ErrReferenceNotFound) {
return fmt.Errorf("resolve target branch commit: %w", err)
}
// the branch is being created
}
var parentCommitOID git.ObjectID
if header.GetStartSha() == "" {
parentCommitOID, err = s.resolveParentCommit(
ctx,
quarantineRepo,
remoteRepo,
targetBranchName,
targetBranchCommit,
string(header.GetStartBranchName()),
)
if err != nil {
return fmt.Errorf("resolve parent commit: %w", err)
}
} else {
parentCommitOID, err = objectHash.FromHex(header.GetStartSha())
if err != nil {
return structerr.NewInvalidArgument("cannot resolve parent commit: %w", err)
}
}
if parentCommitOID != targetBranchCommit {
if err := s.fetchMissingCommit(ctx, quarantineRepo, remoteRepo, 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([]commitAction, 0, len(pbActions))
for _, pbAction := range pbActions {
if _, ok := gitalypb.UserCommitFilesActionHeader_ActionType_name[int32(pbAction.header.GetAction())]; !ok {
return structerr.NewInvalidArgument("NoMethodError: undefined method `downcase' for %d:Integer", pbAction.header.GetAction())
}
path, err := validatePath(repoPath, string(pbAction.header.GetFilePath()))
if err != nil {
return structerr.NewInvalidArgument("validate path: %w", err)
}
content := io.Reader(bytes.NewReader(pbAction.content))
if pbAction.header.GetBase64Content() {
content = base64.NewDecoder(base64.StdEncoding, content)
}
switch pbAction.header.GetAction() {
case gitalypb.UserCommitFilesActionHeader_CREATE:
blobID, err := quarantineRepo.WriteBlob(ctx, content, localrepo.WriteBlobConfig{
Path: path,
})
if err != nil {
return fmt.Errorf("write created blob: %w", err)
}
actions = append(actions, createFile{
OID: blobID.String(),
Path: path,
ExecutableMode: pbAction.header.GetExecuteFilemode(),
})
case gitalypb.UserCommitFilesActionHeader_CHMOD:
actions = append(actions, changeFileMode{
Path: path,
ExecutableMode: pbAction.header.GetExecuteFilemode(),
})
case gitalypb.UserCommitFilesActionHeader_MOVE:
prevPath, err := validatePath(repoPath, string(pbAction.header.GetPreviousPath()))
if err != nil {
return structerr.NewInvalidArgument("validate previous path: %w", err)
}
var oid git.ObjectID
if !pbAction.header.GetInferContent() {
var err error
oid, err = quarantineRepo.WriteBlob(ctx, content, localrepo.WriteBlobConfig{
Path: path,
})
if err != nil {
return err
}
}
actions = append(actions, moveFile{
Path: prevPath,
NewPath: path,
OID: oid.String(),
})
case gitalypb.UserCommitFilesActionHeader_UPDATE:
oid, err := quarantineRepo.WriteBlob(ctx, content, localrepo.WriteBlobConfig{
Path: path,
})
if err != nil {
return fmt.Errorf("write updated blob: %w", err)
}
actions = append(actions, updateFile{
Path: path,
OID: oid.String(),
})
case gitalypb.UserCommitFilesActionHeader_DELETE:
actions = append(actions, deleteFile{
Path: path,
})
case gitalypb.UserCommitFilesActionHeader_CREATE_DIR:
actions = append(actions, createDirectory{
Path: path,
})
}
}
commitID, err := s.userCommitFilesGit(
ctx,
header,
parentCommitOID,
quarantineRepo,
repoPath,
actions,
)
if err != nil {
if errors.Is(err, localrepo.ErrDisallowedCharacters) {
return structerr.NewInvalidArgument("%w", errSignatureMissingNameOrEmail)
}
return err
}
hasBranches, err := quarantineRepo.HasBranches(ctx)
if err != nil {
return fmt.Errorf("was repo created: %w", err)
}
var oldRevision git.ObjectID
if expectedOldOID := header.GetExpectedOldOid(); expectedOldOID != "" {
oldRevision, err = objectHash.FromHex(expectedOldOID)
if err != nil {
return structerr.NewInvalidArgument("invalid expected old object ID: %w", err).WithMetadata("old_object_id", expectedOldOID)
}
oldRevision, err = resolveRevision(ctx, s.localRepoFactory.Build(header.GetRepository()), oldRevision)
if err != nil {
return structerr.NewInvalidArgument("cannot resolve expected old object ID: %w", err).
WithMetadata("old_object_id", expectedOldOID)
}
} else {
oldRevision = parentCommitOID
if targetBranchCommit == "" {
oldRevision = objectHash.ZeroOID
} else if header.GetForce() {
oldRevision = targetBranchCommit
}
}
if err := s.updateReferenceWithHooks(ctx, header.GetRepository(), header.GetUser(), quarantineDir, targetBranchName, commitID, oldRevision); err != nil {
if errors.As(err, &updateref.Error{}) {
return structerr.NewFailedPrecondition("%w", err)
}
return fmt.Errorf("update reference: %w", err)
}
return stream.SendAndClose(&gitalypb.UserCommitFilesResponse{BranchUpdate: &gitalypb.OperationBranchUpdate{
CommitId: commitID.String(),
RepoCreated: !hasBranches,
BranchCreated: objectHash.IsZeroOID(oldRevision),
}})
}
func sameRepository(repoA, repoB *gitalypb.Repository) bool {
return repoA.GetStorageName() == repoB.GetStorageName() &&
repoA.GetRelativePath() == repoB.GetRelativePath()
}
func (s *Server) resolveParentCommit(
ctx context.Context,
local gitcmd.Repository,
remote *gitalypb.Repository,
targetBranch git.ReferenceName,
targetBranchCommit git.ObjectID,
startBranch string,
) (git.ObjectID, error) {
if remote == nil && startBranch == "" {
return targetBranchCommit, nil
}
repo := local
if remote != nil {
var err error
repo, err = remoterepo.New(ctx, remote, s.conns)
if err != nil {
return "", fmt.Errorf("remote repository: %w", err)
}
}
if hasBranches, err := repo.HasBranches(ctx); err != nil {
return "", fmt.Errorf("has branches: %w", err)
} else if !hasBranches {
// GitLab sends requests to UserCommitFiles where target repository
// and start repository are the same. If the request hits Gitaly directly,
// Gitaly could check if the repos are the same by comparing their storages
// and relative paths and simply resolve the branch locally. When request is proxied
// through Praefect, the start repository's storage is not rewritten, thus Gitaly can't
// identify the repos as being the same.
//
// If the start repository is set, we have to resolve the branch there as it
// might be on a different commit than the local repository. As Gitaly can't identify
// the repositories are the same behind Praefect, it has to perform an RPC to resolve
// the branch. The resolving would fail as the branch does not yet exist in the start
// repository, which is actually the local repository.
//
// Due to this, we check if the remote has any branches. If not, we likely hit this case
// and we're creating the first branch. If so, we'll just return the commit that was
// already resolved locally.
//
// See: https://gitlab.com/gitlab-org/gitaly/-/issues/3294
return targetBranchCommit, nil
}
branch := targetBranch
if startBranch != "" {
branch = git.NewReferenceNameFromBranchName(startBranch)
}
refish := branch + "^{commit}"
commit, err := repo.ResolveRevision(ctx, git.Revision(refish))
if err != nil {
return "", fmt.Errorf("resolving refish %q in %T: %w", refish, repo, err)
}
return commit, nil
}
func (s *Server) fetchMissingCommit(
ctx context.Context,
localRepo *localrepo.Repo,
remoteRepo *gitalypb.Repository,
commit git.ObjectID,
) error {
if _, err := localRepo.ResolveRevision(ctx, commit.Revision()+"^{commit}"); err != nil {
if !errors.Is(err, git.ErrReferenceNotFound) || remoteRepo == nil {
return fmt.Errorf("lookup parent commit: %w", err)
}
if err := localRepo.FetchInternal(
ctx,
remoteRepo,
[]string{commit.String()},
localrepo.FetchOpts{Tags: localrepo.FetchOptsTagsNone},
); err != nil {
return fmt.Errorf("fetch parent commit: %w", err)
}
}
return nil
}
func validateUserCommitFilesHeader(header *gitalypb.UserCommitFilesRequestHeader, objectHash git.ObjectHash) error {
if header.GetUser() == nil {
return errors.New("empty User")
}
if len(header.GetCommitMessage()) == 0 {
return errors.New("empty CommitMessage")
}
if len(header.GetBranchName()) == 0 {
return errors.New("empty BranchName")
}
startSha := header.GetStartSha()
if len(startSha) > 0 {
err := objectHash.ValidateHex(startSha)
if err != nil {
return err
}
}
return nil
}
// commitAction represents an action taken to build a commit.
type commitAction interface{ action() }
// isAction is used ensuring type safety for actions.
type isAction struct{}
func (isAction) action() {}
// changeFileMode sets a file's mode to either regular or executable file.
// FileNotFoundError is returned when attempting to change a non-existent
// file's mode.
type changeFileMode struct {
isAction
// Path is the path of the whose mode to change.
Path string
// ExecutableMode indicates whether the file mode should be changed to executable or not.
ExecutableMode bool
}
// createDirectory creates a directory in the given path with a '.gitkeep' file inside.
// FileExistsError is returned if a file already exists at the provided path.
// DirectoryExistsError is returned if a directory already exists at the provided
// path.
type createDirectory struct {
isAction
// Path is the path of the directory to create.
Path string
}
// createFile creates a file using the provided path, mode and oid as the blob.
// FileExistsError is returned if a file exists at the given path.
type createFile struct {
isAction
// Path is the path of the file to create.
Path string
// ExecutableMode indicates whether the file mode should be executable or not.
ExecutableMode bool
// OID is the id of the object that contains the content of the file.
OID string
}
// deleteFile deletes a file or a directory from the provided path.
// FileNotFoundError is returned if the file does not exist.
type deleteFile struct {
isAction
// Path is the path of the file to delete.
Path string
}
// moveFile moves a file or a directory to the new path.
// FileNotFoundError is returned if the file does not exist.
type moveFile struct {
isAction
// Path is the path of the file to move.
Path string
// NewPath is the new path of the file.
NewPath string
// OID is the id of the object that contains the content of the file. If set,
// the file contents are updated to match the object, otherwise the file keeps
// the existing content.
OID string
}
// updateFile updates a file at the given path to point to the provided
// OID. FileNotFoundError is returned if the file does not exist.
type updateFile struct {
isAction
// Path is the path of the file to update.
Path string
// OID is the id of the object that contains the new content of the file.
OID string
}