internal/gitaly/service/operations/submodules.go (217 lines of code) (raw):

package operations import ( "context" "errors" "fmt" "strings" "gitlab.com/gitlab-org/gitaly/v16/internal/git" "gitlab.com/gitlab-org/gitaly/v16/internal/git/localrepo" "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/structerr" "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb" ) // Error strings present in the legacy Ruby implementation. const ( legacyErrPrefixInvalidSubmodulePath = "Invalid submodule path" ) // UserUpdateSubmodule updates a submodule to point to a new commit. func (s *Server) UserUpdateSubmodule(ctx context.Context, req *gitalypb.UserUpdateSubmoduleRequest) (*gitalypb.UserUpdateSubmoduleResponse, error) { if err := s.locator.ValidateRepository(ctx, req.GetRepository()); err != nil { return nil, structerr.NewInvalidArgument("%w", err) } quarantineDir, quarantineRepo, err := s.quarantinedRepo(ctx, req.GetRepository()) if err != nil { return nil, err } objectHash, err := quarantineRepo.ObjectHash(ctx) if err != nil { return nil, fmt.Errorf("detecting object hash: %w", err) } if err := validateUserUpdateSubmoduleRequest(s.locator, objectHash, req); err != nil { return nil, structerr.NewInvalidArgument("%w", err) } branches, err := quarantineRepo.GetBranches(ctx) if err != nil { return nil, structerr.NewInternal("get branches: %w", err) } if len(branches) == 0 { return &gitalypb.UserUpdateSubmoduleResponse{ CommitError: "Repository is empty", }, nil } referenceName := git.NewReferenceNameFromBranchName(string(req.GetBranch())) var oldOID git.ObjectID if expectedOldOID := req.GetExpectedOldOid(); expectedOldOID != "" { objectHash, err := quarantineRepo.ObjectHash(ctx) if err != nil { return nil, structerr.NewInternal("detecting object hash: %w", err) } oldOID, err = objectHash.FromHex(expectedOldOID) if err != nil { return nil, structerr.NewInvalidArgument("invalid expected old object ID: %w", err).WithMetadata("old_object_id", expectedOldOID) } oldOID, err = quarantineRepo.ResolveRevision(ctx, git.Revision(fmt.Sprintf("%s^{object}", oldOID))) if err != nil { return nil, structerr.NewInvalidArgument("cannot resolve expected old object ID: %w", err). WithMetadata("old_object_id", expectedOldOID) } } else { oldOID, err = quarantineRepo.ResolveRevision(ctx, referenceName.Revision()) if err != nil { if errors.Is(err, git.ErrReferenceNotFound) { return nil, structerr.NewInvalidArgument("Cannot find branch") } return nil, structerr.NewInternal("resolving revision: %w", err) } } commitID, err := s.updateSubmodule(ctx, quarantineRepo, req) if err != nil { errStr := strings.TrimSpace(err.Error()) var resp *gitalypb.UserUpdateSubmoduleResponse if strings.Contains(errStr, legacyErrPrefixInvalidSubmodulePath) { resp = &gitalypb.UserUpdateSubmoduleResponse{ CommitError: legacyErrPrefixInvalidSubmodulePath, } s.logger. WithError(err). ErrorContext(ctx, "UserUpdateSubmodule: git2go subcommand failure") } if strings.Contains(errStr, "is already at") { resp = &gitalypb.UserUpdateSubmoduleResponse{ CommitError: errStr, } } if resp != nil { return resp, nil } return nil, structerr.NewInternal("submodule subcommand: %w", err) } commitOID, err := objectHash.FromHex(commitID) if err != nil { return nil, structerr.NewInvalidArgument("cannot parse commit ID: %w", err) } if err := s.updateReferenceWithHooks( ctx, req.GetRepository(), req.GetUser(), quarantineDir, referenceName, commitOID, oldOID, ); err != nil { var customHookErr updateref.CustomHookError if errors.As(err, &customHookErr) { return &gitalypb.UserUpdateSubmoduleResponse{ PreReceiveError: customHookErr.Error(), }, nil } var updateRefError updateref.Error if errors.As(err, &updateRefError) { return &gitalypb.UserUpdateSubmoduleResponse{ // TODO: this needs to be converted to a structured error, and once done we should stop // returning this Ruby-esque error message in favor of the actual error that was // returned by `updateReferenceWithHooks()`. CommitError: fmt.Sprintf("Could not update %s. Please refresh and try again.", updateRefError.Reference), }, nil } return nil, structerr.NewInternal("updating ref with hooks: %w", err) } return &gitalypb.UserUpdateSubmoduleResponse{ BranchUpdate: &gitalypb.OperationBranchUpdate{ CommitId: commitID, BranchCreated: false, RepoCreated: false, }, }, nil } func validateUserUpdateSubmoduleRequest(locator storage.Locator, objectHash git.ObjectHash, req *gitalypb.UserUpdateSubmoduleRequest) error { if req.GetUser() == nil { return errors.New("empty User") } if req.GetCommitSha() == "" { return errors.New("empty CommitSha") } if err := objectHash.ValidateHex(req.GetCommitSha()); err != nil { return errors.New("invalid CommitSha") } if len(req.GetBranch()) == 0 { return errors.New("empty Branch") } if len(req.GetSubmodule()) == 0 { return errors.New("empty Submodule") } if len(req.GetCommitMessage()) == 0 { return errors.New("empty CommitMessage") } return nil } // legacyGit2GoSubmoduleAlreadyAtShaErr is used to maintain backwards // compatibility with the git2go error. type legacyGit2GoSubmoduleAlreadyAtShaError struct { submodulePath string commitSha string } func (l *legacyGit2GoSubmoduleAlreadyAtShaError) Error() string { return fmt.Sprintf("The submodule %s is already at %s", l.submodulePath, l.commitSha) } func (s *Server) updateSubmodule(ctx context.Context, quarantineRepo *localrepo.Repo, req *gitalypb.UserUpdateSubmoduleRequest) (string, error) { var treeID git.ObjectID fullTree, err := quarantineRepo.ReadTree( ctx, git.NewReferenceNameFromBranchName(string(req.GetBranch())).Revision(), localrepo.WithRecursive(), ) if err != nil { if errors.Is(err, git.ErrReferenceNotFound) { return "", fmt.Errorf("submodule: %s", legacyErrPrefixInvalidSubmodulePath) } return "", fmt.Errorf("error reading tree: %w", err) } if err := fullTree.Modify( string(req.GetSubmodule()), func(t *localrepo.TreeEntry) error { replaceWith := git.ObjectID(req.GetCommitSha()) if t.Type != localrepo.Submodule { return fmt.Errorf("submodule: %s", legacyErrPrefixInvalidSubmodulePath) } if replaceWith == t.OID { return &legacyGit2GoSubmoduleAlreadyAtShaError{ submodulePath: string(req.GetSubmodule()), commitSha: string(replaceWith), } } t.OID = replaceWith return nil }, ); err != nil { if errors.Is(err, localrepo.ErrEntryNotFound) { return "", fmt.Errorf("submodule: %s", legacyErrPrefixInvalidSubmodulePath) } var git2GoErr *legacyGit2GoSubmoduleAlreadyAtShaError if errors.As(err, &git2GoErr) { return "", err } return "", fmt.Errorf("modifying tree: %w", err) } if err := fullTree.Write(ctx, quarantineRepo); err != nil { return "", fmt.Errorf("writing tree: %w", err) } treeID = fullTree.OID currentBranchCommit, err := quarantineRepo.ResolveRevision(ctx, git.Revision(req.GetBranch())) if err != nil { return "", fmt.Errorf("resolving submodule branch: %w", err) } authorSignature, err := git.SignatureFromRequest(req) if err != nil { return "", structerr.NewInvalidArgument("%w", err) } newCommitID, err := quarantineRepo.WriteCommit(ctx, localrepo.WriteCommitConfig{ Parents: []git.ObjectID{currentBranchCommit}, AuthorDate: authorSignature.When, AuthorName: authorSignature.Name, AuthorEmail: authorSignature.Email, CommitterName: authorSignature.Name, CommitterEmail: authorSignature.Email, CommitterDate: authorSignature.When, Message: string(req.GetCommitMessage()), TreeID: treeID, }) if err != nil { return "", fmt.Errorf("creating commit %w", err) } return string(newCommitID), nil }