in internal/gitaly/service/operations/commit_files.go [518:746]
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),
}})
}