diff options
author | Patrick Steinhardt <psteinhardt@gitlab.com> | 2023-01-18 15:59:24 +0300 |
---|---|---|
committer | Patrick Steinhardt <psteinhardt@gitlab.com> | 2023-01-18 15:59:24 +0300 |
commit | 39bb9d29c7fbc9248a351b011874b747ced03826 (patch) | |
tree | df9b7694bf195d7be9e40899a874f321fe9010d8 | |
parent | d140b0fe949ae58efcdc1cf492d4046650fbb9b4 (diff) | |
parent | ab8b4a46a2e3bcaeae9bde283af43f325eb5f832 (diff) |
Merge branch 'pks-housekeeping-multi-pack-index-infrastructure' into 'master'
housekeeping: Add infrastructure for multi-pack indices
See merge request https://gitlab.com/gitlab-org/gitaly/-/merge_requests/5266
Merged-by: Patrick Steinhardt <psteinhardt@gitlab.com>
Approved-by: karthik nayak <knayak@gitlab.com>
Reviewed-by: Patrick Steinhardt <psteinhardt@gitlab.com>
Reviewed-by: Quang-Minh Nguyen <qmnguyen@gitlab.com>
Reviewed-by: karthik nayak <knayak@gitlab.com>
-rw-r--r-- | internal/git/housekeeping/objects.go | 11 | ||||
-rw-r--r-- | internal/git/housekeeping/objects_test.go | 294 | ||||
-rw-r--r-- | internal/git/housekeeping/optimize_repository_test.go | 23 | ||||
-rw-r--r-- | internal/git/housekeeping/testhelper_test.go | 26 | ||||
-rw-r--r-- | internal/git/stats/repository_info.go | 41 | ||||
-rw-r--r-- | internal/git/stats/repository_info_test.go | 38 | ||||
-rw-r--r-- | internal/git/version.go | 10 | ||||
-rw-r--r-- | internal/git/version_test.go | 18 |
8 files changed, 397 insertions, 64 deletions
diff --git a/internal/git/housekeeping/objects.go b/internal/git/housekeeping/objects.go index 398401315..54b474d6f 100644 --- a/internal/git/housekeeping/objects.go +++ b/internal/git/housekeeping/objects.go @@ -6,6 +6,7 @@ import ( "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/repository" + "gitlab.com/gitlab-org/gitaly/v15/internal/structerr" ) const ( @@ -24,11 +25,17 @@ type RepackObjectsConfig struct { // reason to set this to `false`, except for legacy compatibility reasons with existing RPC // behaviour WriteBitmap bool + // WriteMultiPackIndex determines whether a multi-pack index should be written or not. + WriteMultiPackIndex bool } // RepackObjects repacks objects in the given repository and updates the commit-graph. The way // objects are repacked is determined via the RepackObjectsConfig. func RepackObjects(ctx context.Context, repo *localrepo.Repo, cfg RepackObjectsConfig) error { + if !cfg.FullRepack && !cfg.WriteMultiPackIndex && cfg.WriteBitmap { + return structerr.NewInvalidArgument("cannot write packfile bitmap for an incremental repack") + } + var options []git.Option if cfg.FullRepack { options = append(options, @@ -38,6 +45,10 @@ func RepackObjects(ctx context.Context, repo *localrepo.Repo, cfg RepackObjectsC ) } + if cfg.WriteMultiPackIndex { + options = append(options, git.Flag{Name: "--write-midx"}) + } + if err := repo.ExecAndWait(ctx, git.Command{ Name: "repack", Flags: append([]git.Option{ diff --git a/internal/git/housekeeping/objects_test.go b/internal/git/housekeeping/objects_test.go index 9076f3a14..5d0ba1622 100644 --- a/internal/git/housekeeping/objects_test.go +++ b/internal/git/housekeeping/objects_test.go @@ -7,54 +7,284 @@ import ( "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest" "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" - "gitlab.com/gitlab-org/gitaly/v15/internal/git/stats" + "gitlab.com/gitlab-org/gitaly/v15/internal/structerr" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg" ) -func requireObjectCount(t *testing.T, repo *localrepo.Repo, expectedObjects uint64) { - t.Helper() - - objects, err := stats.LooseObjects(repo) - require.NoError(t, err) - require.Equal(t, expectedObjects, objects) -} - -func requirePackfileCount(t *testing.T, repo *localrepo.Repo, expectedPackfiles uint64) { - t.Helper() - - packfiles, err := stats.PackfilesCount(repo) - require.NoError(t, err) - require.Equal(t, expectedPackfiles, packfiles) -} - func TestRepackObjects(t *testing.T) { t.Parallel() ctx := testhelper.Context(t) cfg := testcfg.Build(t) - t.Run("no server info is written", func(t *testing.T) { - t.Parallel() + gitVersion, err := gittest.NewCommandFactory(t, cfg).GitVersion(ctx) + require.NoError(t, err) - repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{ - SkipCreationViaService: true, - }) - repo := localrepo.NewTestRepo(t, cfg, repoProto) + // repack is a custom helper function that repacks while explicitly disabling the update of + // server info. This is done so that we assert that the actual repacking logic doesn't write + // the server info. + repack := func(t *testing.T, repoPath string, args ...string) { + gittest.Exec(t, cfg, append([]string{ + "-c", "repack.updateServerInfo=false", "-C", repoPath, "repack", + }, args...)...) + } - gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("main")) + for _, tc := range []struct { + desc string + setup func(t *testing.T, repoPath string) + repackCfg RepackObjectsConfig + stateBeforeRepack objectsState + stateAfterRepack objectsState + expectedErr error + }{ + { + desc: "incremental repack packs objects", + setup: func(t *testing.T, repoPath string) { + gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("main")) + }, + stateBeforeRepack: objectsState{ + looseObjects: 2, + }, + stateAfterRepack: objectsState{ + packfiles: 1, + }, + }, + { + desc: "incremental repack does not repack packfiles", + setup: func(t *testing.T, repoPath string) { + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first"), gittest.WithBranch("first")) + repack(t, repoPath, "-d") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("second"), gittest.WithBranch("second")) + repack(t, repoPath, "-d") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("third"), gittest.WithBranch("third")) + }, + stateBeforeRepack: objectsState{ + packfiles: 2, + looseObjects: 1, + }, + stateAfterRepack: objectsState{ + packfiles: 3, + }, + }, + { + desc: "incremental repack with bitmap returns an error", + setup: func(t *testing.T, repoPath string) { + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first"), gittest.WithBranch("first")) + }, + repackCfg: RepackObjectsConfig{ + WriteBitmap: true, + }, + stateBeforeRepack: objectsState{ + looseObjects: 2, + }, + stateAfterRepack: objectsState{ + looseObjects: 2, + }, + expectedErr: structerr.NewInvalidArgument("cannot write packfile bitmap for an incremental repack"), + }, + { + desc: "full repack packs packfiles and loose objects", + setup: func(t *testing.T, repoPath string) { + // We seed the repository so that it contains two packfiles and one loose object. + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first"), gittest.WithBranch("first")) + repack(t, repoPath, "-d") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("second"), gittest.WithBranch("second")) + repack(t, repoPath, "-d") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("third"), gittest.WithBranch("third")) + }, + repackCfg: RepackObjectsConfig{ + FullRepack: true, + }, + stateBeforeRepack: objectsState{ + looseObjects: 1, + packfiles: 2, + }, + stateAfterRepack: objectsState{ + packfiles: 1, + }, + }, + { + desc: "full repack packs packfiles and loose objects and writes a bitmap", + setup: func(t *testing.T, repoPath string) { + // We seed the repository so that it contains two packfiles and one loose object. + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first"), gittest.WithBranch("first")) + repack(t, repoPath, "-d") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("second"), gittest.WithBranch("second")) + repack(t, repoPath, "-d") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("third"), gittest.WithBranch("third")) + }, + repackCfg: RepackObjectsConfig{ + FullRepack: true, + WriteBitmap: true, + }, + stateBeforeRepack: objectsState{ + looseObjects: 1, + packfiles: 2, + }, + stateAfterRepack: objectsState{ + packfiles: 1, + hasBitmap: true, + }, + }, + { + desc: "multi-pack-index with incremental repack", + setup: func(t *testing.T, repoPath string) { + gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("master")) + }, + repackCfg: RepackObjectsConfig{ + WriteMultiPackIndex: true, + }, + stateBeforeRepack: objectsState{ + looseObjects: 2, + }, + stateAfterRepack: objectsState{ + packfiles: 1, + hasMultiPackIndex: true, + }, + }, + { + desc: "multi-pack-index allows incremental repacks with bitmaps", + setup: func(t *testing.T, repoPath string) { + gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("master")) + }, + repackCfg: RepackObjectsConfig{ + WriteMultiPackIndex: true, + WriteBitmap: true, + }, + stateBeforeRepack: objectsState{ + looseObjects: 2, + }, + stateAfterRepack: objectsState{ + packfiles: 1, + hasMultiPackIndex: true, + hasMultiPackIndexBitmap: true, + }, + }, + { + desc: "multi-pack-index with full repack packs packfiles and loose objects", + setup: func(t *testing.T, repoPath string) { + // We seed the repository so that it contains two packfiles and one loose object. + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first"), gittest.WithBranch("first")) + repack(t, repoPath, "-d") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("second"), gittest.WithBranch("second")) + repack(t, repoPath, "-d") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("third"), gittest.WithBranch("third")) + }, + repackCfg: RepackObjectsConfig{ + FullRepack: true, + WriteMultiPackIndex: true, + }, + stateBeforeRepack: objectsState{ + looseObjects: 1, + packfiles: 2, + }, + stateAfterRepack: objectsState{ + packfiles: 1, + hasMultiPackIndex: true, + }, + }, + { + desc: "multi-pack-index with full repack and bitmap writes bitmap", + setup: func(t *testing.T, repoPath string) { + // We seed the repository so that it contains two packfiles and one loose object. + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first"), gittest.WithBranch("first")) + repack(t, repoPath, "-d") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("second"), gittest.WithBranch("second")) + repack(t, repoPath, "-d") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("third"), gittest.WithBranch("third")) + }, + repackCfg: RepackObjectsConfig{ + FullRepack: true, + WriteBitmap: true, + WriteMultiPackIndex: true, + }, + stateBeforeRepack: objectsState{ + looseObjects: 1, + packfiles: 2, + }, + stateAfterRepack: objectsState{ + packfiles: 1, + hasMultiPackIndex: true, + hasMultiPackIndexBitmap: true, + }, + }, + { + desc: "multi-pack-index with incremental repack removes preexisting bitmaps with newish Git", + setup: func(t *testing.T, repoPath string) { + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first"), gittest.WithBranch("first")) + repack(t, repoPath, "-A", "-d", "-b") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("second"), gittest.WithBranch("second")) + }, + repackCfg: RepackObjectsConfig{ + WriteBitmap: true, + WriteMultiPackIndex: true, + }, + stateBeforeRepack: objectsState{ + looseObjects: 1, + packfiles: 1, + hasBitmap: true, + }, + stateAfterRepack: objectsState{ + packfiles: 2, + // Git v2.38.0 does not yet remove redundant pack-based bitmaps. + // This is getting fixed via 55d902cd61 (builtin/repack.c: remove + // redundant pack-based bitmaps, 2022-10-17), which is part of Git + // v2.39.0 and newer. + // + // Local tests don't show that this is a problem. Most importantly, + // Git does not seem to warn about these bitmaps. So let's just + // ignore them for now. + hasBitmap: !gitVersion.MidxDeletesRedundantBitmaps(), + hasMultiPackIndex: true, + hasMultiPackIndexBitmap: true, + }, + }, + { + desc: "multi-pack-index with full repack removes preexisting bitmaps", + setup: func(t *testing.T, repoPath string) { + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("first"), gittest.WithBranch("first")) + repack(t, repoPath, "-A", "-d", "-b") + gittest.WriteCommit(t, cfg, repoPath, gittest.WithMessage("second"), gittest.WithBranch("second")) + repack(t, repoPath, "-d") + }, + repackCfg: RepackObjectsConfig{ + FullRepack: true, + WriteBitmap: true, + WriteMultiPackIndex: true, + }, + stateBeforeRepack: objectsState{ + packfiles: 2, + hasBitmap: true, + }, + stateAfterRepack: objectsState{ + packfiles: 1, + hasMultiPackIndex: true, + hasMultiPackIndexBitmap: true, + }, + }, + } { + tc := tc - requireObjectCount(t, repo, 2) - requirePackfileCount(t, repo, 0) + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() - require.NoError(t, RepackObjects(ctx, repo, RepackObjectsConfig{})) + repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{ + SkipCreationViaService: true, + }) + repo := localrepo.NewTestRepo(t, cfg, repoProto) - requireObjectCount(t, repo, 0) - requirePackfileCount(t, repo, 1) + tc.setup(t, repoPath) - require.NoFileExists(t, filepath.Join(repoPath, "info", "refs")) - require.NoFileExists(t, filepath.Join(repoPath, "objects", "info", "packs")) - }) + requireObjectsState(t, repo, tc.stateBeforeRepack) + require.Equal(t, tc.expectedErr, RepackObjects(ctx, repo, tc.repackCfg)) + requireObjectsState(t, repo, tc.stateAfterRepack) + + // There should not be any server info data in the repository. + require.NoFileExists(t, filepath.Join(repoPath, "info", "refs")) + require.NoFileExists(t, filepath.Join(repoPath, "objects", "info", "packs")) + }) + } testRepoAndPool(t, "delta islands", func(t *testing.T, relativePath string) { t.Parallel() diff --git a/internal/git/housekeeping/optimize_repository_test.go b/internal/git/housekeeping/optimize_repository_test.go index b2caef466..13ddc07a3 100644 --- a/internal/git/housekeeping/optimize_repository_test.go +++ b/internal/git/housekeeping/optimize_repository_test.go @@ -30,16 +30,6 @@ func TestRepackIfNeeded(t *testing.T) { ctx := testhelper.Context(t) cfg := testcfg.Build(t) - requirePackfilesAndLooseObjects := func(t *testing.T, repo *localrepo.Repo, expectedPackfiles, expectedLooseObjects uint64) { - t.Helper() - - info, err := stats.RepositoryInfoForRepository(repo) - require.NoError(t, err) - - require.Equal(t, expectedPackfiles, info.Packfiles.Count) - require.Equal(t, expectedLooseObjects, info.LooseObjects.Count) - } - t.Run("no repacking", func(t *testing.T) { repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{ SkipCreationViaService: true, @@ -56,7 +46,9 @@ func TestRepackIfNeeded(t *testing.T) { require.False(t, didRepack) require.Equal(t, RepackObjectsConfig{}, repackObjectsCfg) - requirePackfilesAndLooseObjects(t, repo, 0, 2) + requireObjectsState(t, repo, objectsState{ + looseObjects: 2, + }) }) t.Run("incremental repack", func(t *testing.T) { @@ -79,7 +71,10 @@ func TestRepackIfNeeded(t *testing.T) { require.True(t, didRepack) require.Equal(t, RepackObjectsConfig{}, repackObjectsCfg) - requirePackfilesAndLooseObjects(t, repo, 2, 0) + requireObjectsState(t, repo, objectsState{ + packfiles: 2, + hasBitmap: true, + }) }) t.Run("full repack", func(t *testing.T) { @@ -107,7 +102,9 @@ func TestRepackIfNeeded(t *testing.T) { FullRepack: true, }, repackObjectsCfg) - requirePackfilesAndLooseObjects(t, repo, 1, 0) + requireObjectsState(t, repo, objectsState{ + packfiles: 1, + }) }) } diff --git a/internal/git/housekeeping/testhelper_test.go b/internal/git/housekeeping/testhelper_test.go index 49499923f..b6faaa265 100644 --- a/internal/git/housekeeping/testhelper_test.go +++ b/internal/git/housekeeping/testhelper_test.go @@ -3,7 +3,10 @@ package housekeeping import ( "testing" + "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/stats" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" ) @@ -23,3 +26,26 @@ func testRepoAndPool(t *testing.T, desc string, testFunc func(t *testing.T, rela }) }) } + +type objectsState struct { + looseObjects uint64 + packfiles uint64 + hasBitmap bool + hasMultiPackIndex bool + hasMultiPackIndexBitmap bool +} + +func requireObjectsState(tb testing.TB, repo *localrepo.Repo, expectedState objectsState) { + tb.Helper() + + repoInfo, err := stats.RepositoryInfoForRepository(repo) + require.NoError(tb, err) + + require.Equal(tb, expectedState, objectsState{ + looseObjects: repoInfo.LooseObjects.Count, + packfiles: repoInfo.Packfiles.Count, + hasBitmap: repoInfo.Packfiles.HasBitmap, + hasMultiPackIndex: repoInfo.Packfiles.HasMultiPackIndex, + hasMultiPackIndexBitmap: repoInfo.Packfiles.HasMultiPackIndexBitmap, + }) +} diff --git a/internal/git/stats/repository_info.go b/internal/git/stats/repository_info.go index 9885ec895..363b267bd 100644 --- a/internal/git/stats/repository_info.go +++ b/internal/git/stats/repository_info.go @@ -240,6 +240,10 @@ type PackfilesInfo struct { GarbageSize uint64 `json:"garbage_size"` // HasBitmap indicates whether the packfiles have a bitmap. HasBitmap bool `json:"has_bitmap"` + // HasMultiPackIndex indicates whether there is a multi-pack-index. + HasMultiPackIndex bool `json:"has_multi_pack_index"` + // HasMultiPackIndexBitmap indicates whether the multi-pack-index has a bitmap. + HasMultiPackIndexBitmap bool `json:"has_multi_pack_index_bitmap"` } // PackfilesInfoForRepository derives various information about packfiles for the given repository. @@ -269,11 +273,28 @@ func PackfilesInfoForRepository(repo *localrepo.Repo) (PackfilesInfo, error) { return PackfilesInfo{}, fmt.Errorf("getting packfile info: %w", err) } - // We're overly lenient here and only verify for known prefixes. This would already - // catch things like temporary packfiles, but it wouldn't catch other bogus files. - // This is on purpose though because Git has grown more and more metadata-style file - // formats, and we don't want to copy the list here. - if !strings.HasPrefix(entry.Name(), "pack-") { + entryName := entry.Name() + + switch { + case strings.HasPrefix(entryName, "pack-"): + // We're overly lenient here and only verify packfiles for known suffixes. + // As a consequence, we don't catch garbage files here. This is on purpose + // though because Git has grown more and more metadata-style file formats, + // and we don't want to copy the list here. + switch { + case strings.HasSuffix(entryName, ".pack"): + info.Count++ + if entryInfo.Size() > 0 { + info.Size += uint64(entryInfo.Size()) + } + case strings.HasSuffix(entryName, ".bitmap"): + info.HasBitmap = true + } + case entryName == "multi-pack-index": + info.HasMultiPackIndex = true + case strings.HasPrefix(entryName, "multi-pack-index-") && strings.HasSuffix(entryName, ".bitmap"): + info.HasMultiPackIndexBitmap = true + default: info.GarbageCount++ if entryInfo.Size() > 0 { info.GarbageSize += uint64(entryInfo.Size()) @@ -281,16 +302,6 @@ func PackfilesInfoForRepository(repo *localrepo.Repo) (PackfilesInfo, error) { continue } - - switch { - case strings.HasSuffix(entry.Name(), ".pack"): - info.Count++ - if entryInfo.Size() > 0 { - info.Size += uint64(entryInfo.Size()) - } - case strings.HasSuffix(entry.Name(), ".bitmap"): - info.HasBitmap = true - } } return info, nil diff --git a/internal/git/stats/repository_info_test.go b/internal/git/stats/repository_info_test.go index 8fa25c6fe..1c2be2ead 100644 --- a/internal/git/stats/repository_info_test.go +++ b/internal/git/stats/repository_info_test.go @@ -737,6 +737,32 @@ func TestPackfileInfoForRepository(t *testing.T) { }) }) + t.Run("multi-pack-index", func(t *testing.T) { + repo, repoPath := createRepo(t) + + packfileDir := filepath.Join(repoPath, "objects", "pack") + require.NoError(t, os.MkdirAll(packfileDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(packfileDir, "multi-pack-index"), nil, 0o644)) + + requirePackfilesInfo(t, repo, PackfilesInfo{ + HasMultiPackIndex: true, + }) + }) + + t.Run("multi-pack-index with bitmap", func(t *testing.T) { + repo, repoPath := createRepo(t) + + packfileDir := filepath.Join(repoPath, "objects", "pack") + require.NoError(t, os.MkdirAll(packfileDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(packfileDir, "multi-pack-index"), nil, 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(packfileDir, "multi-pack-index-c0363841cc7e5783a996c72f0a4a7ae4440aaa40.bitmap"), nil, 0o644)) + + requirePackfilesInfo(t, repo, PackfilesInfo{ + HasMultiPackIndex: true, + HasMultiPackIndexBitmap: true, + }) + }) + t.Run("multiple packfiles with other data structures", func(t *testing.T) { repo, repoPath := createRepo(t) @@ -750,15 +776,19 @@ func TestPackfileInfoForRepository(t *testing.T) { "pack-foo.pack", "pack-foo.idx", "garbage", + "multi-pack-index", + "multi-pack-index-c0363841cc7e5783a996c72f0a4a7ae4440aaa40.bitmap", } { require.NoError(t, os.WriteFile(filepath.Join(packfileDir, file), []byte("1"), 0o644)) } requirePackfilesInfo(t, repo, PackfilesInfo{ - Count: 2, - Size: 2, - GarbageCount: 1, - GarbageSize: 1, + Count: 2, + Size: 2, + GarbageCount: 1, + GarbageSize: 1, + HasMultiPackIndex: true, + HasMultiPackIndexBitmap: true, }) }) } diff --git a/internal/git/version.go b/internal/git/version.go index ee44ddd32..2d13c1187 100644 --- a/internal/git/version.go +++ b/internal/git/version.go @@ -82,6 +82,16 @@ func (v Version) PatchIDRespectsBinaries() bool { }) } +// MidxDeletesRedundantBitmaps detects whether the given Git version deletes redundant pack-based +// bitmaps when writing multi-pack-indices. This feature has been added via 55d902cd61 +// (builtin/repack.c: remove redundant pack-based bitmaps, 2022-10-17), which is part of Git +// v2.39.0 and newer. +func (v Version) MidxDeletesRedundantBitmaps() bool { + return !v.LessThan(Version{ + major: 2, minor: 39, patch: 0, + }) +} + // LessThan determines whether the version is older than another version. func (v Version) LessThan(other Version) bool { switch { diff --git a/internal/git/version_test.go b/internal/git/version_test.go index ea0777696..93f2e4eef 100644 --- a/internal/git/version_test.go +++ b/internal/git/version_test.go @@ -142,3 +142,21 @@ func TestVersion_PatchIDRespectsBinaries(t *testing.T) { }) } } + +func TestVersion_MidxDeletesRedundantBitmaps(t *testing.T) { + for _, tc := range []struct { + version string + expect bool + }{ + {"1.0.0", false}, + {"2.38.2", false}, + {"2.39.0", true}, + {"3.0.0", true}, + } { + t.Run(tc.version, func(t *testing.T) { + version, err := parseVersion(tc.version) + require.NoError(t, err) + require.Equal(t, tc.expect, version.MidxDeletesRedundantBitmaps()) + }) + } +} |