Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'internal/gitaly/service/cleanup')
-rw-r--r--internal/gitaly/service/cleanup/rewrite_history.go184
-rw-r--r--internal/gitaly/service/cleanup/rewrite_history_test.go518
-rw-r--r--internal/gitaly/service/cleanup/server.go3
-rw-r--r--internal/gitaly/service/cleanup/testhelper_test.go2
4 files changed, 707 insertions, 0 deletions
diff --git a/internal/gitaly/service/cleanup/rewrite_history.go b/internal/gitaly/service/cleanup/rewrite_history.go
new file mode 100644
index 000000000..7bbdd258e
--- /dev/null
+++ b/internal/gitaly/service/cleanup/rewrite_history.go
@@ -0,0 +1,184 @@
+package cleanup
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "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/structerr"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/tempdir"
+ "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
+)
+
+// RewriteHistory uses git-filter-repo(1) to remove specified blobs from commit history and
+// replace blobs to redact specified text patterns. This does not delete the removed blobs from
+// the object database, they must be garbage collected separately.
+func (s *server) RewriteHistory(server gitalypb.CleanupService_RewriteHistoryServer) error {
+ ctx := server.Context()
+
+ request, err := server.Recv()
+ if err != nil {
+ return fmt.Errorf("receiving initial request: %w", err)
+ }
+
+ repoProto := request.GetRepository()
+ if err := s.locator.ValidateRepository(repoProto); err != nil {
+ return structerr.NewInvalidArgument("%w", err)
+ }
+
+ repo := s.localrepo(repoProto)
+
+ objectHash, err := repo.ObjectHash(ctx)
+ if err != nil {
+ return fmt.Errorf("detecting object hash: %w", err)
+ }
+
+ if objectHash.Format == "sha256" {
+ return structerr.NewInvalidArgument("git-filter-repo does not support repositories using the SHA256 object format")
+ }
+
+ // Unset repository so that we can validate that repository is not sent on subsequent requests.
+ request.Repository = nil
+
+ blobsToRemove := make([]string, 0, len(request.GetBlobs()))
+ redactions := make([][]byte, 0, len(request.GetRedactions()))
+
+ for {
+ if request.GetRepository() != nil {
+ return structerr.NewInvalidArgument("subsequent requests must not contain repository")
+ }
+
+ if len(request.GetBlobs()) == 0 && len(request.GetRedactions()) == 0 {
+ return structerr.NewInvalidArgument("no object IDs or text replacements specified")
+ }
+
+ for _, oid := range request.GetBlobs() {
+ if err := objectHash.ValidateHex(oid); err != nil {
+ return structerr.NewInvalidArgument("validating object ID: %w", err).WithMetadata("oid", oid)
+ }
+ blobsToRemove = append(blobsToRemove, oid)
+ }
+
+ for _, pattern := range request.GetRedactions() {
+ if strings.Contains(string(pattern), "\n") {
+ // We deliberately do not log the invalid pattern as this is
+ // likely to contain sensitive information.
+ return structerr.NewInvalidArgument("redaction pattern contains newline")
+ }
+ redactions = append(redactions, pattern)
+ }
+
+ request, err = server.Recv()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+
+ return fmt.Errorf("receiving next request: %w", err)
+ }
+ }
+
+ if err := s.runFilterRepo(ctx, repo, repoProto, blobsToRemove, redactions); err != nil {
+ return fmt.Errorf("rewriting repository history: %w", err)
+ }
+
+ if err := server.SendAndClose(&gitalypb.RewriteHistoryResponse{}); err != nil {
+ return fmt.Errorf("sending RewriteHistoryResponse: %w", err)
+ }
+
+ return nil
+}
+
+func (s *server) runFilterRepo(
+ ctx context.Context,
+ repo *localrepo.Repo,
+ repoProto *gitalypb.Repository,
+ blobsToRemove []string,
+ redactions [][]byte,
+) error {
+ // Place argument files in a tempdir so that cleanup is handled automatically.
+ tmpDir, err := tempdir.New(ctx, repo.GetStorageName(), s.logger, s.locator)
+ if err != nil {
+ return fmt.Errorf("create tempdir: %w", err)
+ }
+
+ flags := make([]git.Option, 0, 2)
+
+ if len(blobsToRemove) > 0 {
+ blobPath, err := writeArgFile("strip-blobs", tmpDir.Path(), []byte(strings.Join(blobsToRemove, "\n")))
+ if err != nil {
+ return err
+ }
+
+ flags = append(flags, git.Flag{Name: "--strip-blobs-with-ids=" + blobPath})
+ }
+
+ if len(redactions) > 0 {
+ replacePath, err := writeArgFile("replace-text", tmpDir.Path(), bytes.Join(redactions, []byte("\n")))
+ if err != nil {
+ return err
+ }
+
+ flags = append(flags, git.Flag{Name: "--replace-text=" + replacePath})
+ }
+
+ var stdout, stderr strings.Builder
+ if err := repo.ExecAndWait(ctx,
+ git.Command{
+ Name: "filter-repo",
+ Flags: append([]git.Option{
+ // Prevent automatic cleanup tasks like deleting 'origin' and running git-gc(1).
+ git.Flag{Name: "--partial"},
+ // Bypass check that repository is not a fresh clone.
+ git.Flag{Name: "--force"},
+ // filter-repo will by default create 'replace' refs for refs it rewrites, but Gitaly
+ // disables this feature. This option will update any existing user-created replace refs,
+ // while preventing the creation of new ones.
+ git.Flag{Name: "--replace-refs=update-no-add"},
+ // Pass '--quiet' to child git processes.
+ git.Flag{Name: "--quiet"},
+ }, flags...),
+ },
+ git.WithRefTxHook(repo),
+ git.WithStdout(&stdout),
+ git.WithStderr(&stderr),
+ ); err != nil {
+ var exitErr *exec.ExitError
+ if errors.As(err, &exitErr) {
+ return structerr.New("git-filter-repo failed with exit code %d", exitErr.ExitCode()).WithMetadataItems(
+ structerr.MetadataItem{Key: "stdout", Value: stdout.String()},
+ structerr.MetadataItem{Key: "stderr", Value: stderr.String()},
+ )
+ }
+ return fmt.Errorf("running git-filter-repo: %w", err)
+ }
+
+ return nil
+}
+
+func writeArgFile(name string, dir string, input []byte) (string, error) {
+ f, err := os.CreateTemp(dir, name)
+ if err != nil {
+ return "", fmt.Errorf("creating %q file: %w", name, err)
+ }
+
+ path := f.Name()
+
+ _, err = f.Write(input)
+ if err != nil {
+ return "", fmt.Errorf("writing %q file: %w", name, err)
+ }
+
+ if err := f.Close(); err != nil {
+ return "", fmt.Errorf("closing %q file: %w", name, err)
+ }
+
+ return path, nil
+}
diff --git a/internal/gitaly/service/cleanup/rewrite_history_test.go b/internal/gitaly/service/cleanup/rewrite_history_test.go
new file mode 100644
index 000000000..0d2c4363c
--- /dev/null
+++ b/internal/gitaly/service/cleanup/rewrite_history_test.go
@@ -0,0 +1,518 @@
+package cleanup
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/git"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/git/gittest"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/structerr"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper/testcfg"
+ "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
+)
+
+func TestRewriteHistory(t *testing.T) {
+ t.Parallel()
+ gittest.SkipWithSHA256(t)
+
+ ctx := testhelper.Context(t)
+ cfg := testcfg.Build(t)
+ testcfg.BuildGitalyHooks(t, cfg)
+ cfg.SocketPath = runCleanupServiceServer(t, cfg)
+
+ client, conn := newCleanupServiceClient(t, cfg.SocketPath)
+ t.Cleanup(func() { conn.Close() })
+
+ addUnmodifiedRefs := func(repoPath string) []git.Reference {
+ unmodifiedCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("unmodified"), gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "unmodified-file", Mode: "100644", Content: "can't touch this"},
+ ))
+ unmodifiedTag := gittest.WriteTag(t, cfg, repoPath, "unmodified-tag", unmodifiedCommit.Revision(), gittest.WriteTagConfig{
+ Message: "annotated",
+ })
+ keepRef := git.ReferenceName("refs/keeparound/" + unmodifiedCommit.String())
+ gittest.WriteRef(t, cfg, repoPath, keepRef, unmodifiedCommit)
+
+ return []git.Reference{
+ {
+ Name: "refs/heads/unmodified",
+ Target: unmodifiedCommit.String(),
+ },
+ {
+ Name: "refs/tags/unmodified-tag",
+ Target: unmodifiedTag.String(),
+ },
+ {
+ Name: keepRef,
+ Target: unmodifiedCommit.String(),
+ },
+ }
+ }
+
+ type setupData struct {
+ requests []*gitalypb.RewriteHistoryRequest
+ repoPath string
+ expectedRefs []git.Reference
+ expectedErr error
+ expectedResponse *gitalypb.RewriteHistoryResponse
+ }
+
+ for _, tc := range []struct {
+ desc string
+ setup func(t *testing.T) setupData
+ }{
+ {
+ desc: "no requests",
+ setup: func(t *testing.T) setupData {
+ return setupData{
+ requests: nil,
+ expectedErr: testhelper.GitalyOrPraefect(
+ structerr.NewInternal("receiving initial request: EOF"),
+ structerr.NewInternal("EOF"),
+ ),
+ }
+ },
+ },
+ {
+ desc: "missing repository",
+ setup: func(t *testing.T) setupData {
+ repoPath := gittest.NewRepositoryName(t)
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: &gitalypb.Repository{
+ StorageName: cfg.Storages[0].Name,
+ RelativePath: repoPath,
+ },
+ Redactions: [][]byte{[]byte("hunter2")},
+ },
+ },
+ expectedErr: testhelper.ToInterceptedMetadata(
+ structerr.New("%w", storage.NewRepositoryNotFoundError(cfg.Storages[0].Name, repoPath)),
+ ),
+ }
+ },
+ },
+ {
+ desc: "repository in subsequent request",
+ setup: func(t *testing.T) setupData {
+ repo, _ := gittest.CreateRepository(t, ctx, cfg)
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Redactions: [][]byte{[]byte("hunter2")},
+ },
+ {
+ Repository: repo,
+ Redactions: [][]byte{[]byte("secretpassword")},
+ },
+ },
+ expectedErr: structerr.NewInvalidArgument("subsequent requests must not contain repository"),
+ }
+ },
+ },
+ {
+ desc: "empty request",
+ setup: func(t *testing.T) setupData {
+ repo, _ := gittest.CreateRepository(t, ctx, cfg)
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ },
+ },
+ expectedErr: structerr.NewInvalidArgument("no object IDs or text replacements specified"),
+ }
+ },
+ },
+ {
+ desc: "remove invalid oid",
+ setup: func(t *testing.T) setupData {
+ repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Blobs: []string{"invalid oid"},
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: unmodifiedRefs,
+ expectedErr: testhelper.WithInterceptedMetadata(
+ structerr.NewInvalidArgument("validating object ID: invalid object ID: \"invalid oid\", expected length %v, got 11", gittest.DefaultObjectHash.EncodedLen()),
+ "oid", "invalid oid",
+ ),
+ }
+ },
+ },
+ {
+ desc: "redaction pattern with newline",
+ setup: func(t *testing.T) setupData {
+ repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Redactions: [][]byte{[]byte("hunter\n2")},
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: unmodifiedRefs,
+ expectedErr: structerr.NewInvalidArgument("redaction pattern contains newline"),
+ }
+ },
+ },
+ {
+ desc: "redaction pattern with escaped newline",
+ setup: func(t *testing.T) setupData {
+ repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Redactions: [][]byte{[]byte("hunter\\n2")},
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: unmodifiedRefs,
+ expectedResponse: &gitalypb.RewriteHistoryResponse{},
+ }
+ },
+ },
+ {
+ desc: "remove non-existent oid",
+ setup: func(t *testing.T) setupData {
+ repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Blobs: []string{strings.Repeat("a", gittest.DefaultObjectHash.EncodedLen())},
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: unmodifiedRefs,
+ expectedResponse: &gitalypb.RewriteHistoryResponse{},
+ }
+ },
+ },
+ {
+ desc: "remove blobs",
+ setup: func(t *testing.T) setupData {
+ repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ blobToRemove := gittest.WriteBlob(t, cfg, repoPath, []byte("big blob"))
+ _ = gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("main"), gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "remove-me", Mode: "100644", OID: blobToRemove},
+ gittest.TreeEntry{Path: "a-file", Mode: "100644", Content: "foobar"},
+ ))
+ updatedCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "a-file", Mode: "100644", Content: "foobar"},
+ ))
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Blobs: []string{blobToRemove.String()},
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: append(unmodifiedRefs, []git.Reference{
+ {
+ Name: "refs/heads/main",
+ Target: updatedCommit.String(),
+ },
+ }...),
+ expectedResponse: &gitalypb.RewriteHistoryResponse{},
+ }
+ },
+ },
+ {
+ desc: "redact blobs",
+ setup: func(t *testing.T) setupData {
+ repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ _ = gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("main"), gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "redact-me", Mode: "100644", Content: "my password is hunter2"},
+ ))
+ updatedCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "redact-me", Mode: "100644", Content: "my password is ***REMOVED***"},
+ ))
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Redactions: [][]byte{
+ []byte("hunter2"),
+ },
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: append(unmodifiedRefs, []git.Reference{
+ {
+ Name: "refs/heads/main",
+ Target: updatedCommit.String(),
+ },
+ }...),
+ expectedResponse: &gitalypb.RewriteHistoryResponse{},
+ }
+ },
+ },
+ {
+ desc: "multiple requests",
+ setup: func(t *testing.T) setupData {
+ repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ blobToRemove := gittest.WriteBlob(t, cfg, repoPath, []byte("big blob"))
+ _ = gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("main"), gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "remove-me", Mode: "100644", OID: blobToRemove},
+ gittest.TreeEntry{Path: "redact-me", Mode: "100644", Content: "my password is hunter2"},
+ ))
+ updatedCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "redact-me", Mode: "100644", Content: "my password is ***REMOVED***"},
+ ))
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repoProto,
+ Blobs: []string{blobToRemove.String()},
+ },
+ {
+ Redactions: [][]byte{[]byte("hunter2")},
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: append(unmodifiedRefs, []git.Reference{
+ {
+ Name: "refs/heads/main",
+ Target: updatedCommit.String(),
+ },
+ }...),
+ expectedResponse: &gitalypb.RewriteHistoryResponse{},
+ }
+ },
+ },
+ {
+ desc: "empty branch deleted",
+ setup: func(t *testing.T) setupData {
+ repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ blobToRemove := gittest.WriteBlob(t, cfg, repoPath, []byte("big blob"))
+ _ = gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("main"), gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "remove-me", Mode: "100644", OID: blobToRemove},
+ ))
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Blobs: []string{blobToRemove.String()},
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: unmodifiedRefs,
+ expectedResponse: &gitalypb.RewriteHistoryResponse{},
+ }
+ },
+ },
+ {
+ desc: "tag updated",
+ setup: func(t *testing.T) setupData {
+ repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ blobToRemove := gittest.WriteBlob(t, cfg, repoPath, []byte("big blob"))
+ commit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "remove-me", Mode: "100644", OID: blobToRemove},
+ gittest.TreeEntry{Path: "a-file", Mode: "100644", Content: "foobar"},
+ ))
+ _ = gittest.WriteTag(t, cfg, repoPath, "updated-tag", commit.Revision())
+
+ updatedCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "a-file", Mode: "100644", Content: "foobar"},
+ ))
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Blobs: []string{blobToRemove.String()},
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: append(unmodifiedRefs, []git.Reference{
+ {
+ Name: "refs/tags/updated-tag",
+ Target: updatedCommit.String(),
+ },
+ }...),
+ expectedResponse: &gitalypb.RewriteHistoryResponse{},
+ }
+ },
+ },
+ {
+ desc: "empty tag deleted",
+ setup: func(t *testing.T) setupData {
+ repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ blobToRemove := gittest.WriteBlob(t, cfg, repoPath, []byte("big blob"))
+ commit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "a-file", Mode: "100644", OID: blobToRemove},
+ ))
+ _ = gittest.WriteTag(t, cfg, repoPath, "deleted-tag", commit.Revision(), gittest.WriteTagConfig{
+ Message: "annotated",
+ })
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Blobs: []string{blobToRemove.String()},
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: unmodifiedRefs,
+ expectedResponse: &gitalypb.RewriteHistoryResponse{},
+ }
+ },
+ },
+ {
+ desc: "remove blob in pool repo",
+ setup: func(t *testing.T) setupData {
+ testhelper.SkipWithWAL(t, `
+Object pools are not yet supported with transaction management.`)
+
+ repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
+ unmodifiedRefs := addUnmodifiedRefs(repoPath)
+
+ poolBlob := gittest.WriteBlob(t, cfg, repoPath, []byte("pool blob"))
+ parentCommit := gittest.WriteCommit(t, cfg, repoPath,
+ gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "remove-me", Mode: "100644", OID: poolBlob},
+ gittest.TreeEntry{Path: "other-pool-file", Mode: "100644", Content: "pool blob to retain"},
+ ),
+ gittest.WithBranch("main"),
+ )
+
+ gittest.CreateObjectPool(t, ctx, cfg, repo, gittest.CreateObjectPoolConfig{
+ LinkRepositoryToObjectPool: true,
+ })
+
+ _ = gittest.WriteCommit(t, cfg, repoPath,
+ gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "remove-me", Mode: "100644", OID: poolBlob},
+ gittest.TreeEntry{Path: "a-file", Mode: "100644", Content: "local blob"},
+ ),
+ gittest.WithParents(parentCommit),
+ gittest.WithBranch("main"),
+ )
+
+ updatedParent := gittest.WriteCommit(t, cfg, repoPath,
+ gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "other-pool-file", Mode: "100644", Content: "pool blob to retain"},
+ ),
+ )
+ updatedCommit := gittest.WriteCommit(t, cfg, repoPath,
+ gittest.WithTreeEntries(
+ gittest.TreeEntry{Path: "a-file", Mode: "100644", Content: "local blob"},
+ ),
+ gittest.WithParents(updatedParent),
+ )
+
+ return setupData{
+ requests: []*gitalypb.RewriteHistoryRequest{
+ {
+ Repository: repo,
+ Blobs: []string{poolBlob.String()},
+ },
+ },
+ repoPath: repoPath,
+ expectedRefs: append(unmodifiedRefs, []git.Reference{
+ {
+ Name: "refs/heads/main",
+ Target: updatedCommit.String(),
+ },
+ }...),
+ expectedResponse: &gitalypb.RewriteHistoryResponse{},
+ }
+ },
+ },
+ } {
+ tc := tc
+
+ t.Run(tc.desc, func(t *testing.T) {
+ t.Parallel()
+
+ setup := tc.setup(t)
+
+ stream, err := client.RewriteHistory(ctx)
+ require.NoError(t, err)
+
+ for _, request := range setup.requests {
+ require.NoError(t, stream.Send(request))
+ }
+
+ response, err := stream.CloseAndRecv()
+ testhelper.RequireGrpcError(t, setup.expectedErr, err)
+ testhelper.ProtoEqual(t, setup.expectedResponse, response)
+
+ if setup.repoPath != "" {
+ refs := gittest.GetReferences(t, cfg, setup.repoPath)
+ require.ElementsMatch(t, refs, setup.expectedRefs)
+ }
+ })
+ }
+}
+
+func TestRewriteHistory_SHA256(t *testing.T) {
+ t.Parallel()
+
+ if !gittest.ObjectHashIsSHA256() {
+ t.Skip("test is not compatible with SHA1")
+ }
+
+ ctx := testhelper.Context(t)
+ cfg := testcfg.Build(t)
+ testcfg.BuildGitalyHooks(t, cfg)
+ cfg.SocketPath = runCleanupServiceServer(t, cfg)
+
+ client, conn := newCleanupServiceClient(t, cfg.SocketPath)
+ t.Cleanup(func() { conn.Close() })
+
+ repo, _ := gittest.CreateRepository(t, ctx, cfg)
+
+ stream, err := client.RewriteHistory(ctx)
+ require.NoError(t, err)
+
+ require.NoError(t, stream.Send(&gitalypb.RewriteHistoryRequest{
+ Repository: repo,
+ Redactions: [][]byte{[]byte("hunter2")},
+ }))
+
+ response, err := stream.CloseAndRecv()
+ require.Nil(t, response)
+ testhelper.RequireGrpcError(t, structerr.NewInvalidArgument("git-filter-repo does not support repositories using the SHA256 object format"), err)
+}
diff --git a/internal/gitaly/service/cleanup/server.go b/internal/gitaly/service/cleanup/server.go
index 3fd2d06f6..33058e5a6 100644
--- a/internal/gitaly/service/cleanup/server.go
+++ b/internal/gitaly/service/cleanup/server.go
@@ -6,6 +6,7 @@ import (
"gitlab.com/gitlab-org/gitaly/v16/internal/git/localrepo"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/service"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/transaction"
"gitlab.com/gitlab-org/gitaly/v16/internal/log"
"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
)
@@ -16,6 +17,7 @@ type server struct {
locator storage.Locator
gitCmdFactory git.CommandFactory
catfileCache catfile.Cache
+ txManager transaction.Manager
}
// NewServer creates a new instance of a grpc CleanupServer
@@ -25,6 +27,7 @@ func NewServer(deps *service.Dependencies) gitalypb.CleanupServiceServer {
locator: deps.GetLocator(),
gitCmdFactory: deps.GetGitCmdFactory(),
catfileCache: deps.GetCatfileCache(),
+ txManager: deps.GetTxManager(),
}
}
diff --git a/internal/gitaly/service/cleanup/testhelper_test.go b/internal/gitaly/service/cleanup/testhelper_test.go
index 3f488eba8..8dd085f22 100644
--- a/internal/gitaly/service/cleanup/testhelper_test.go
+++ b/internal/gitaly/service/cleanup/testhelper_test.go
@@ -8,6 +8,7 @@ import (
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/config"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/service"
hookservice "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/service/hook"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/service/objectpool"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/service/repository"
"gitlab.com/gitlab-org/gitaly/v16/internal/testhelper"
"gitlab.com/gitlab-org/gitaly/v16/internal/testhelper/testcfg"
@@ -38,6 +39,7 @@ func runCleanupServiceServer(t *testing.T, cfg config.Cfg) string {
gitalypb.RegisterCleanupServiceServer(srv, NewServer(deps))
gitalypb.RegisterHookServiceServer(srv, hookservice.NewServer(deps))
gitalypb.RegisterRepositoryServiceServer(srv, repository.NewServer(deps))
+ gitalypb.RegisterObjectPoolServiceServer(srv, objectpool.NewServer(deps))
})
}