diff options
author | John Cai <jcai@gitlab.com> | 2022-11-13 05:10:03 +0300 |
---|---|---|
committer | John Cai <jcai@gitlab.com> | 2023-03-04 01:19:28 +0300 |
commit | 2e7a4fef2525e66e473ff555b027be1496910110 (patch) | |
tree | 9840beea937df4ef32b1e01e4e01c8522d197660 | |
parent | d47eef8f6b234b30d4034e1c5d2febad83e373af (diff) |
operations: Reimplement UserUpdateSubmodule using git plumbing commands
Updating a submodule is simply updating the oid of the commit at the
path. We can do this by manually rewriting trees and writing a new
commit. If we impelement it this way, we don't need to rely on Git2Go.
-rw-r--r-- | internal/gitaly/service/operations/submodules.go | 126 | ||||
-rw-r--r-- | internal/gitaly/service/operations/submodules_test.go | 40 |
2 files changed, 164 insertions, 2 deletions
diff --git a/internal/gitaly/service/operations/submodules.go b/internal/gitaly/service/operations/submodules.go index 52cd4a724..74da62969 100644 --- a/internal/gitaly/service/operations/submodules.go +++ b/internal/gitaly/service/operations/submodules.go @@ -4,15 +4,18 @@ import ( "context" "errors" "fmt" + "path/filepath" "regexp" "strings" "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" "gitlab.com/gitlab-org/gitaly/v15/internal/git" "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/lstree" "gitlab.com/gitlab-org/gitaly/v15/internal/git/updateref" "gitlab.com/gitlab-org/gitaly/v15/internal/git2go" "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/service" + "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag" "gitlab.com/gitlab-org/gitaly/v15/internal/structerr" "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb" ) @@ -58,6 +61,120 @@ func validateUserUpdateSubmoduleRequest(req *gitalypb.UserUpdateSubmoduleRequest return nil } +func (s *Server) updateSubmodule(ctx context.Context, quarantineRepo *localrepo.Repo, req *gitalypb.UserUpdateSubmoduleRequest) (string, error) { + path := filepath.Dir(string(req.GetSubmodule())) + base := filepath.Base(string(req.GetSubmodule())) + replaceWith := git.ObjectID(req.GetCommitSha()) + + var submoduleFound bool + + // First find the tree containing the submodule to be updated. + // Write a new tree object abc with the updated submodule. Then, write a new + // tree with the new tree abcabc. Continue iterating up the tree, + // writing a new tree object each time. + for { + entries, err := lstree.ListEntries( + ctx, + quarantineRepo, + git.Revision("refs/heads/"+string(req.GetBranch())), + &lstree.ListEntriesConfig{ + RelativePath: path, + }, + ) + if err != nil { + if strings.Contains(err.Error(), "invalid object name") { + return "", fmt.Errorf("submodule: %s", git2go.LegacyErrPrefixInvalidSubmodulePath) + } + + return "", fmt.Errorf("error reading tree: %w", err) + } + + var newEntries []localrepo.TreeEntry + var newTreeID git.ObjectID + + for _, entry := range entries { + // If the entry's path does not match, then we simply + // want to retain this tree entry. + if entry.Path != base { + newEntries = append(newEntries, *entry) + continue + } + + // If we are at the submodule we want to replace, check + // if it's already at the value we want to replace, or + // if it's not a submodule. + if filepath.Join(path, entry.Path) == string(req.GetSubmodule()) { + if string(entry.OID) == req.GetCommitSha() { + return "", + //nolint:stylecheck + fmt.Errorf( + "The submodule %s is already at %s", + req.GetSubmodule(), + replaceWith, + ) + } + + if entry.Type != localrepo.Submodule { + return "", structerr.NewInvalidArgument("submodule %s is not a commit", req.GetSubmodule()) + } + } + + // Otherwise, create a new tree entry + submoduleFound = true + + newEntries = append(newEntries, localrepo.TreeEntry{ + Mode: entry.Mode, + Path: entry.Path, + OID: replaceWith, + }) + } + + newTreeID, err = quarantineRepo.WriteTree(ctx, newEntries) + if err != nil { + return "", fmt.Errorf("write tree: %w", err) + } + replaceWith = newTreeID + + if path == "." { + break + } + + base = filepath.Base(path) + path = filepath.Dir(path) + } + + if !submoduleFound { + return "", fmt.Errorf("submodule: %s", git2go.LegacyErrPrefixInvalidSubmodulePath) + } + + currentBranchCommit, err := quarantineRepo.ResolveRevision(ctx, git.Revision(req.GetBranch())) + if err != nil { + return "", fmt.Errorf("resolving submodule branch: %w", err) + } + + authorDate, err := dateFromProto(req) + if err != nil { + return "", structerr.NewInvalidArgument("%w", err) + } + + newCommitID, err := quarantineRepo.WriteCommit(ctx, localrepo.WriteCommitConfig{ + Parents: []git.ObjectID{currentBranchCommit}, + AuthorDate: authorDate, + AuthorName: string(req.GetUser().GetName()), + AuthorEmail: string(req.GetUser().GetEmail()), + CommitterName: string(req.GetUser().GetName()), + CommitterEmail: string(req.GetUser().GetEmail()), + CommitterDate: authorDate, + Message: string(req.GetCommitMessage()), + TreeID: replaceWith, + }) + if err != nil { + return "", fmt.Errorf("creating commit %w", err) + } + + return string(newCommitID), nil +} + func (s *Server) updateSubmoduleWithGit2Go(ctx context.Context, quarantineRepo *localrepo.Repo, req *gitalypb.UserUpdateSubmoduleRequest) (string, error) { repoPath, err := quarantineRepo.Path() if err != nil { @@ -131,7 +248,14 @@ func (s *Server) userUpdateSubmodule(ctx context.Context, req *gitalypb.UserUpda } } - commitID, err := s.updateSubmoduleWithGit2Go(ctx, quarantineRepo, req) + var commitID string + + if featureflag.SubmoduleInGit.IsEnabled(ctx) { + commitID, err = s.updateSubmodule(ctx, quarantineRepo, req) + } else { + commitID, err = s.updateSubmoduleWithGit2Go(ctx, quarantineRepo, req) + } + if err != nil { errStr := strings.TrimPrefix(err.Error(), "submodule: ") errStr = strings.TrimSpace(errStr) diff --git a/internal/gitaly/service/operations/submodules_test.go b/internal/gitaly/service/operations/submodules_test.go index 8047932fc..4f60ee7db 100644 --- a/internal/gitaly/service/operations/submodules_test.go +++ b/internal/gitaly/service/operations/submodules_test.go @@ -4,6 +4,7 @@ package operations import ( "bytes" + "context" "fmt" "path/filepath" "testing" @@ -14,6 +15,7 @@ import ( "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" "gitlab.com/gitlab-org/gitaly/v15/internal/git/lstree" "gitlab.com/gitlab-org/gitaly/v15/internal/helper/text" + "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag" "gitlab.com/gitlab-org/gitaly/v15/internal/structerr" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb" @@ -22,7 +24,11 @@ import ( func TestUserUpdateSubmodule(t *testing.T) { t.Parallel() - ctx := testhelper.Context(t) + testhelper.NewFeatureSets(featureflag.SubmoduleInGit). + Run(t, testUserUpdateSubmodule) +} + +func testUserUpdateSubmodule(t *testing.T, ctx context.Context) { ctx, cfg, client := setupOperationsServiceWithoutRepo(t, ctx) type setupData struct { @@ -454,6 +460,38 @@ func TestUserUpdateSubmodule(t *testing.T) { }, }, { + desc: "failure due to invalid submodule path", + subPath: "sub", + branch: "master", + setup: func(repoPath, subRepoPath string, repoProto, subRepoProto *gitalypb.Repository) setupData { + subCommitID := gittest.WriteCommit(t, cfg, subRepoPath) + gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("master"), gittest.WithTreeEntries( + gittest.TreeEntry{ + Mode: "100644", + Path: ".gitmodules", + Content: fmt.Sprintf(`[submodule "%s"]\n\tpath = %s\n\turl = file://%s`, "sub", "sub", subRepoPath), + }, + gittest.TreeEntry{OID: subCommitID, Mode: "160000", Path: "sub"}, + )) + commitID := gittest.WriteCommit(t, cfg, subRepoPath, gittest.WithParents(subCommitID)) + + return setupData{ + request: &gitalypb.UserUpdateSubmoduleRequest{ + User: gittest.TestUser, + CommitSha: string(commitID), + Branch: []byte("master"), + Repository: repoProto, + Submodule: []byte("foobar/does/not/exist"), + CommitMessage: []byte("Updating Submodule: sub"), + }, + expectedResponse: &gitalypb.UserUpdateSubmoduleResponse{ + CommitError: "Invalid submodule path", + }, + verify: func(t *testing.T) {}, + } + }, + }, + { desc: "failure due to same submodule reference", subPath: "sub", branch: "master", |