diff options
author | Patrick Steinhardt <psteinhardt@gitlab.com> | 2023-03-23 11:57:25 +0300 |
---|---|---|
committer | Patrick Steinhardt <psteinhardt@gitlab.com> | 2023-03-28 08:24:18 +0300 |
commit | 257ee33ca268d48c8f99dcbfeaaf7d8b19e07f06 (patch) | |
tree | bd532093e3f4bf1265a06c056cbc1e30b9f50e5c | |
parent | a84210839170bd8b8f7f0e604f36b02d5d06885c (diff) |
repository: Implement the new RepositoryInfo RPC
We have added Protobuf definitions for the new RepositoryInfo RPC in the
preceding commit. This commit provides the implementation.
-rw-r--r-- | internal/gitaly/service/repository/repository_info.go | 62 | ||||
-rw-r--r-- | internal/gitaly/service/repository/repository_info_test.go | 446 |
2 files changed, 508 insertions, 0 deletions
diff --git a/internal/gitaly/service/repository/repository_info.go b/internal/gitaly/service/repository/repository_info.go new file mode 100644 index 000000000..5eeab2e1e --- /dev/null +++ b/internal/gitaly/service/repository/repository_info.go @@ -0,0 +1,62 @@ +package repository + +import ( + "context" + "fmt" + + "gitlab.com/gitlab-org/gitaly/v15/internal/git/stats" + "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/service" + "gitlab.com/gitlab-org/gitaly/v15/internal/structerr" + "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb" +) + +func (s *server) RepositoryInfo( + ctx context.Context, + request *gitalypb.RepositoryInfoRequest, +) (*gitalypb.RepositoryInfoResponse, error) { + if err := service.ValidateRepository(request.Repository); err != nil { + return nil, structerr.NewInvalidArgument("%w", err) + } + + repo := s.localrepo(request.Repository) + + repoPath, err := repo.Path() + if err != nil { + return nil, err + } + + repoSize, err := dirSizeInBytes(repoPath) + if err != nil { + return nil, fmt.Errorf("calculating repository size: %w", err) + } + + repoInfo, err := stats.RepositoryInfoForRepository(repo) + if err != nil { + return nil, fmt.Errorf("deriving repository info: %w", err) + } + + return convertRepositoryInfo(uint64(repoSize), repoInfo), nil +} + +func convertRepositoryInfo(repoSize uint64, repoInfo stats.RepositoryInfo) *gitalypb.RepositoryInfoResponse { + // The loose objects size includes objects which are older than the grace period and thus + // stale, so we need to subtract the size of stale objects from the overall size. + recentLooseObjectsSize := repoInfo.LooseObjects.Size - repoInfo.LooseObjects.StaleSize + // The packfiles size includes the size of cruft packs that contain unreachable objects, so + // we need to subtract the size of cruft packs from the overall size. + recentPackfilesSize := repoInfo.Packfiles.Size - repoInfo.Packfiles.CruftSize + + return &gitalypb.RepositoryInfoResponse{ + Size: repoSize, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{ + LooseCount: repoInfo.References.LooseReferencesCount, + PackedSize: repoInfo.References.PackedReferencesSize, + }, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: repoInfo.LooseObjects.Size + repoInfo.Packfiles.Size, + RecentSize: recentLooseObjectsSize + recentPackfilesSize, + StaleSize: repoInfo.LooseObjects.StaleSize + repoInfo.Packfiles.CruftSize, + KeepSize: repoInfo.Packfiles.KeepSize, + }, + } +} diff --git a/internal/gitaly/service/repository/repository_info_test.go b/internal/gitaly/service/repository/repository_info_test.go new file mode 100644 index 000000000..5ff133cf3 --- /dev/null +++ b/internal/gitaly/service/repository/repository_info_test.go @@ -0,0 +1,446 @@ +package repository + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + gitalyerrors "gitlab.com/gitlab-org/gitaly/v15/internal/errors" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/stats" + "gitlab.com/gitlab-org/gitaly/v15/internal/helper/perm" + "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" +) + +func TestRepositoryInfo(t *testing.T) { + t.Parallel() + + ctx := testhelper.Context(t) + cfg, client := setupRepositoryServiceWithoutRepo(t) + + writeFile := func(t *testing.T, byteCount int, pathComponents ...string) string { + t.Helper() + path := filepath.Join(pathComponents...) + require.NoError(t, os.MkdirAll(filepath.Dir(path), perm.PrivateDir)) + require.NoError(t, os.WriteFile(path, bytes.Repeat([]byte{0}, byteCount), perm.PrivateFile)) + return path + } + + emptyRepoSize := func() uint64 { + _, repoPath := gittest.CreateRepository(t, ctx, cfg) + size, err := dirSizeInBytes(repoPath) + require.NoError(t, err) + return uint64(size) + }() + + type setupData struct { + request *gitalypb.RepositoryInfoRequest + expectedErr error + expectedResponse *gitalypb.RepositoryInfoResponse + } + + for _, tc := range []struct { + desc string + setup func(t *testing.T) setupData + }{ + { + desc: "unset repository", + setup: func(t *testing.T) setupData { + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: nil, + }, + expectedErr: testhelper.GitalyOrPraefect( + structerr.NewInvalidArgument("%w", gitalyerrors.ErrEmptyRepository), + structerr.NewInvalidArgument("repo scoped: %w", gitalyerrors.ErrEmptyRepository), + ), + } + }, + }, + { + desc: "invalid repository", + setup: func(t *testing.T) setupData { + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: &gitalypb.Repository{ + StorageName: cfg.Storages[0].Name, + RelativePath: "does/not/exist", + }, + }, + expectedErr: testhelper.GitalyOrPraefect( + structerr.NewNotFound("GetRepoPath: not a git repository: %q", filepath.Join(cfg.Storages[0].Path, "does", "not", "exist")), + structerr.NewNotFound( + "accessor call: route repository accessor: consistent storages: repository %q/%q not found", + cfg.Storages[0].Name, + "does/not/exist", + ), + ), + } + }, + }, + { + desc: "empty repository", + setup: func(t *testing.T) setupData { + repo, _ := gittest.CreateRepository(t, ctx, cfg) + + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: repo, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: emptyRepoSize, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{}, + }, + } + }, + }, + { + desc: "repository with loose reference", + setup: func(t *testing.T) setupData { + repo, repoPath := gittest.CreateRepository(t, ctx, cfg) + writeFile(t, 123, repoPath, "refs", "heads", "main") + + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: repo, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: emptyRepoSize + 123, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{ + LooseCount: 1, + }, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{}, + }, + } + }, + }, + { + desc: "repository with packed references", + setup: func(t *testing.T) setupData { + repo, repoPath := gittest.CreateRepository(t, ctx, cfg) + writeFile(t, 123, repoPath, "packed-refs") + + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: repo, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: emptyRepoSize + 123, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{ + PackedSize: 123, + }, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{}, + }, + } + }, + }, + { + desc: "repository with loose blob", + setup: func(t *testing.T) setupData { + repo, repoPath := gittest.CreateRepository(t, ctx, cfg) + + oid := gittest.DefaultObjectHash.ZeroOID.String() + writeFile(t, 123, repoPath, "objects", oid[0:2], oid[2:]) + + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: repo, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: emptyRepoSize + 123, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: 123, + RecentSize: 123, + }, + }, + } + }, + }, + { + desc: "repository with stale loose object", + setup: func(t *testing.T) setupData { + repo, repoPath := gittest.CreateRepository(t, ctx, cfg) + + oid := gittest.DefaultObjectHash.ZeroOID.String() + objectPath := writeFile(t, 123, repoPath, "objects", oid[0:2], oid[2:]) + stale := time.Now().Add(stats.StaleObjectsGracePeriod) + require.NoError(t, os.Chtimes(objectPath, stale, stale)) + + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: repo, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: emptyRepoSize + 123, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: 123, + StaleSize: 123, + }, + }, + } + }, + }, + { + desc: "repository with mixed stale and recent loose object", + setup: func(t *testing.T) setupData { + repo, repoPath := gittest.CreateRepository(t, ctx, cfg) + + stale := time.Now().Add(stats.StaleObjectsGracePeriod) + + recentOID := gittest.DefaultObjectHash.ZeroOID.String() + writeFile(t, 70, repoPath, "objects", recentOID[0:2], recentOID[2:]) + + staleOID := strings.Repeat("1", len(recentOID)) + staleObjectPath := writeFile(t, 700, repoPath, "objects", staleOID[0:2], staleOID[2:]) + require.NoError(t, os.Chtimes(staleObjectPath, stale, stale)) + + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: repo, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: emptyRepoSize + 70 + 700, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: 770, + RecentSize: 70, + StaleSize: 700, + }, + }, + } + }, + }, + { + desc: "repository with packfile", + setup: func(t *testing.T) setupData { + repo, repoPath := gittest.CreateRepository(t, ctx, cfg) + + writeFile(t, 123, repoPath, "objects", "pack", "pack-1234.pack") + + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: repo, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: emptyRepoSize + 123, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: 123, + RecentSize: 123, + }, + }, + } + }, + }, + { + desc: "repository with cruft pack", + setup: func(t *testing.T) setupData { + repo, repoPath := gittest.CreateRepository(t, ctx, cfg) + + writeFile(t, 123, repoPath, "objects", "pack", "pack-1234.pack") + writeFile(t, 7, repoPath, "objects", "pack", "pack-1234.mtimes") + + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: repo, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: emptyRepoSize + 123 + 7, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: 123, + StaleSize: 123, + }, + }, + } + }, + }, + { + desc: "repository with keep pack", + setup: func(t *testing.T) setupData { + repo, repoPath := gittest.CreateRepository(t, ctx, cfg) + + writeFile(t, 123, repoPath, "objects", "pack", "pack-1234.pack") + writeFile(t, 7, repoPath, "objects", "pack", "pack-1234.keep") + + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: repo, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: emptyRepoSize + 123 + 7, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: 123, + RecentSize: 123, + KeepSize: 123, + }, + }, + } + }, + }, + { + desc: "repository with different pack types", + setup: func(t *testing.T) setupData { + repo, repoPath := gittest.CreateRepository(t, ctx, cfg) + + writeFile(t, 70, repoPath, "objects", "pack", "pack-1.pack") + + writeFile(t, 700, repoPath, "objects", "pack", "pack-2.pack") + writeFile(t, 100, repoPath, "objects", "pack", "pack-2.keep") + + writeFile(t, 7000, repoPath, "objects", "pack", "pack-3.pack") + writeFile(t, 1000, repoPath, "objects", "pack", "pack-3.mtimes") + + return setupData{ + request: &gitalypb.RepositoryInfoRequest{ + Repository: repo, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: emptyRepoSize + 8000 + 800 + 70, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: 7770, + RecentSize: 770, + KeepSize: 700, + StaleSize: 7000, + }, + }, + } + }, + }, + } { + tc := tc + + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + setup := tc.setup(t) + + response, err := client.RepositoryInfo(ctx, setup.request) + testhelper.RequireGrpcError(t, setup.expectedErr, err) + testhelper.ProtoEqual(t, setup.expectedResponse, response) + }) + } +} + +func TestConvertRepositoryInfo(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + desc string + repoSize uint64 + repoInfo stats.RepositoryInfo + expectedResponse *gitalypb.RepositoryInfoResponse + }{ + { + desc: "all-zero", + expectedResponse: &gitalypb.RepositoryInfoResponse{ + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{}, + }, + }, + { + desc: "size", + repoSize: 123, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + Size: 123, + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{}, + }, + }, + { + desc: "references", + repoInfo: stats.RepositoryInfo{ + References: stats.ReferencesInfo{ + LooseReferencesCount: 123, + PackedReferencesSize: 456, + }, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{ + LooseCount: 123, + PackedSize: 456, + }, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{}, + }, + }, + { + desc: "loose objects", + repoInfo: stats.RepositoryInfo{ + LooseObjects: stats.LooseObjectsInfo{ + Size: 123, + StaleSize: 3, + }, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: 123, + RecentSize: 120, + StaleSize: 3, + }, + }, + }, + { + desc: "packfiles", + repoInfo: stats.RepositoryInfo{ + Packfiles: stats.PackfilesInfo{ + Size: 123, + CruftSize: 3, + KeepSize: 7, + }, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: 123, + RecentSize: 120, + StaleSize: 3, + KeepSize: 7, + }, + }, + }, + { + desc: "loose objects and packfiles", + repoInfo: stats.RepositoryInfo{ + LooseObjects: stats.LooseObjectsInfo{ + Size: 7, + StaleSize: 3, + }, + Packfiles: stats.PackfilesInfo{ + Size: 700, + CruftSize: 300, + KeepSize: 500, + }, + }, + expectedResponse: &gitalypb.RepositoryInfoResponse{ + References: &gitalypb.RepositoryInfoResponse_ReferencesInfo{}, + Objects: &gitalypb.RepositoryInfoResponse_ObjectsInfo{ + Size: 707, + RecentSize: 404, + StaleSize: 303, + KeepSize: 500, + }, + }, + }, + } { + tc := tc + + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + response := convertRepositoryInfo(tc.repoSize, tc.repoInfo) + require.Equal(t, tc.expectedResponse, response) + }) + } +} |