in internal/gitaly/storage/storagemgr/partition/migration/xxxx_ref_backend_migration.go [20:193]
func NewReferenceBackendMigration(
id uint64,
targetBackend git.ReferenceBackend,
localRepoFactory localrepo.Factory,
flag *featureflag.FeatureFlag,
) Migration {
name := fmt.Sprintf("migrate_ref_backend_%s", targetBackend.Name)
return Migration{
ID: id,
Name: name,
Fn: func(ctx context.Context, tx storage.Transaction, storageName string, relativePath string) error {
scopedFactory, err := localRepoFactory.ScopeByStorage(ctx, storageName)
if err != nil {
return fmt.Errorf("creating storage scoped factory: %w", err)
}
originalRepo := &gitalypb.Repository{RelativePath: relativePath}
repo := scopedFactory.Build(tx.RewriteRepository(originalRepo).GetRelativePath())
currentBackend, err := repo.ReferenceBackend(ctx)
if err != nil {
return fmt.Errorf("reference backend: %w", err)
}
// If the repository is already using the expected backend, there is
// nothing to do.
if currentBackend.Name == targetBackend.Name {
return nil
}
getHash := func(repo *localrepo.Repo) ([]byte, error) {
hasher := sha256.New()
stderr := &bytes.Buffer{}
err := repo.ExecAndWait(ctx, gitcmd.Command{
Name: "for-each-ref",
Flags: []gitcmd.Option{
gitcmd.Flag{Name: "--format=%(refname) %(objectname) %(symref)"},
gitcmd.Flag{Name: "--include-root-refs"},
},
}, gitcmd.WithStdout(hasher), gitcmd.WithStderr(stderr))
if err != nil {
return nil, fmt.Errorf("running for-each-ref: %w, stderr: %q", err, stderr)
}
return hasher.Sum(nil), nil
}
preMigrationHash, err := getHash(repo)
if err != nil {
return fmt.Errorf("hashing all refs pre-migration: %w", err)
}
repoPath, err := repo.Path(ctx)
if err != nil {
return fmt.Errorf("repo path: %w", err)
}
relPath, err := filepath.Rel(tx.FS().Root(), repoPath)
if err != nil {
return fmt.Errorf("relative path: %w", err)
}
if err := storage.RecordDirectoryRemoval(tx.FS(), tx.FS().Root(), filepath.Join(relPath, "refs")); err != nil {
return fmt.Errorf("removing refs directory: %w", err)
}
if targetBackend == git.ReferenceBackendReftables {
if err := tx.FS().RecordRemoval(filepath.Join(relPath, "packed-refs")); err != nil {
return fmt.Errorf("removing packed-refs: %w", err)
}
} else if targetBackend == git.ReferenceBackendFiles {
if err := storage.RecordDirectoryRemoval(tx.FS(), tx.FS().Root(), filepath.Join(relPath, "reftable")); err != nil {
return fmt.Errorf("removing reftable directory: %w", err)
}
} else {
return fmt.Errorf("unknown backend provided: %s", targetBackend.Name)
}
if err := tx.FS().RecordRemoval(filepath.Join(relPath, "HEAD")); err != nil {
return fmt.Errorf("removing HEAD: %w", err)
}
if err := tx.FS().RecordRemoval(filepath.Join(relPath, "config")); err != nil {
return fmt.Errorf("removing config: %w", err)
}
stderr := &bytes.Buffer{}
err = repo.ExecAndWait(ctx, gitcmd.Command{
Name: "refs",
Action: "migrate",
Flags: []gitcmd.Option{
gitcmd.Flag{Name: fmt.Sprintf("--ref-format=%s", targetBackend.Name)},
},
}, gitcmd.WithStderr(stderr))
if err != nil {
return fmt.Errorf("running refs migrate: %w, stderr: %q", err, stderr)
}
// Since Git 2.48, the files backend will directly write to packed-refs
// during ref-migrations. But for versions before that, we need to manually
// pack the refs. Let's do that. We can remove this block once Git 2.48 is
// made the minimum version.
if targetBackend == git.ReferenceBackendFiles {
gitVersion, err := repo.GitVersion(ctx)
if err != nil {
return fmt.Errorf("git version: %w", err)
}
if gitVersion.LessThan(git.NewVersion(2, 48, 0, 0)) {
err = repo.ExecAndWait(ctx, gitcmd.Command{
Name: "pack-refs",
Flags: []gitcmd.Option{
gitcmd.Flag{Name: "--all"},
},
}, gitcmd.WithStderr(stderr))
if err != nil {
return fmt.Errorf("running pack-refs: %w, stderr: %q", err, stderr)
}
}
}
postMigrationHash, err := getHash(repo)
if err != nil {
return fmt.Errorf("hashing all refs post-migration: %w", err)
}
if !bytes.Equal(preMigrationHash, postMigrationHash) {
return errors.New("reference hash does not match")
}
if err := tx.FS().RecordFile(filepath.Join(relPath, "config")); err != nil {
return fmt.Errorf("recording config: %w", err)
}
if err := tx.FS().RecordFile(filepath.Join(relPath, "HEAD")); err != nil {
return fmt.Errorf("recording HEAD: %w", err)
}
if targetBackend == git.ReferenceBackendReftables {
if err := tx.FS().RecordDirectory(filepath.Join(relPath, "refs")); err != nil {
return fmt.Errorf("recording refs: %w", err)
}
if err := tx.FS().RecordFile(filepath.Join(relPath, "refs/heads")); err != nil {
return fmt.Errorf("recording refs/heads: %w", err)
}
if err := storage.RecordDirectoryCreation(tx.FS(), filepath.Join(relPath, "reftable")); err != nil {
return fmt.Errorf("recording reftable dir: %w", err)
}
} else if targetBackend == git.ReferenceBackendFiles {
if err := tx.FS().RecordFile(filepath.Join(relPath, "packed-refs")); err != nil {
return fmt.Errorf("recording packed-refs: %w", err)
}
if err := storage.RecordDirectoryCreation(tx.FS(), filepath.Join(relPath, "refs")); err != nil {
return fmt.Errorf("recording refs dir: %w", err)
}
} else {
return fmt.Errorf("unknown backend provided: %s", targetBackend.Name)
}
return nil
},
IsDisabled: func(ctx context.Context) bool {
if flag != nil {
return flag.IsDisabled(ctx)
}
return false
},
}
}