1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
|
package quarantine
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage"
"gitlab.com/gitlab-org/gitaly/v16/internal/helper/perm"
"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"
"google.golang.org/protobuf/proto"
)
// Dir is a quarantine directory for Git objects. Instead of writing new commits into the main
// repository, they're instead written into a temporary quarantine directory. This staging area can
// either be migrated into the main repository or, alternatively, will automatically be discarded
// when the context gets cancelled. If the quarantine environment is discarded without being staged,
// then none of the objects which have been created in the quarantine directory will end up in the
// main repository.
type Dir struct {
repo *gitalypb.Repository
quarantinedRepo *gitalypb.Repository
dir tempdir.Dir
locator storage.Locator
}
// New creates a new quarantine directory and returns the directory. The repository is cleaned
// up when the user invokes the Migrate() functionality on the Dir.
func New(ctx context.Context, repo *gitalypb.Repository, locator storage.Locator) (*Dir, error) {
repoPath, err := locator.GetRepoPath(repo, storage.WithRepositoryVerificationSkipped())
if err != nil {
return nil, structerr.NewInternal("getting repo path: %w", err)
}
quarantineDir, err := tempdir.NewWithPrefix(ctx, repo.GetStorageName(),
storage.QuarantineDirectoryPrefix(repo), locator)
if err != nil {
return nil, fmt.Errorf("creating quarantine: %w", err)
}
quarantinedRepo, err := Apply(repoPath, repo, quarantineDir.Path())
if err != nil {
return nil, err
}
return &Dir{
repo: repo,
quarantinedRepo: quarantinedRepo,
locator: locator,
dir: quarantineDir,
}, nil
}
// Apply applies the quarantine on the repository. This is done by setting the quarantineDirectory
// as the repository's object directory, and configuring the original object directory as an alternate.
func Apply(repoPath string, repo *gitalypb.Repository, quarantineDir string) (*gitalypb.Repository, error) {
relativePath, err := filepath.Rel(repoPath, quarantineDir)
if err != nil {
return nil, fmt.Errorf("creating quarantine: %w", err)
}
// All paths are relative to the repository root.
alternateObjectDirs := []string{"objects"}
if repo.GetGitObjectDirectory() != "" {
alternateObjectDirs = append(alternateObjectDirs, repo.GetGitObjectDirectory())
}
alternateObjectDirs = append(alternateObjectDirs, repo.GetGitAlternateObjectDirectories()...)
quarantinedRepo := proto.Clone(repo).(*gitalypb.Repository)
quarantinedRepo.GitObjectDirectory = relativePath
quarantinedRepo.GitAlternateObjectDirectories = alternateObjectDirs
return quarantinedRepo, nil
}
// QuarantinedRepo returns a Repository protobuf message with adjusted main and alternate object
// directories. If passed e.g. to the `git.ExecCommandFactory`, then all new objects will end up in
// the quarantine directory.
func (d *Dir) QuarantinedRepo() *gitalypb.Repository {
return d.quarantinedRepo
}
// Migrate migrates all objects part of the quarantine directory into the main repository and thus
// makes them generally available. This implementation follows the git.git's `tmp_objdir_migrate()`.
func (d *Dir) Migrate() error {
repoPath, err := d.locator.GetRepoPath(d.repo, storage.WithRepositoryVerificationSkipped())
if err != nil {
return fmt.Errorf("migrating quarantine: %w", err)
}
return migrate(d.dir.Path(), filepath.Join(repoPath, "objects"))
}
func migrate(sourcePath, targetPath string) error {
entries, err := os.ReadDir(sourcePath)
if err != nil {
return fmt.Errorf("reading directory: %w", err)
}
sortEntries(entries)
for _, entry := range entries {
if entry.Name() == "." {
continue
}
nestedTargetPath := filepath.Join(targetPath, entry.Name())
nestedSourcePath := filepath.Join(sourcePath, entry.Name())
if entry.IsDir() {
if err := os.Mkdir(nestedTargetPath, perm.PublicDir); err != nil {
if !errors.Is(err, os.ErrExist) {
return fmt.Errorf("creating target directory %q: %w", nestedTargetPath, err)
}
}
if err := migrate(nestedSourcePath, nestedTargetPath); err != nil {
return fmt.Errorf("migrating directory %q: %w", nestedSourcePath, err)
}
continue
}
if err := finalizeObjectFile(nestedSourcePath, nestedTargetPath); err != nil {
return fmt.Errorf("migrating object file %q: %w", nestedSourcePath, err)
}
}
if err := os.Remove(sourcePath); err != nil {
return fmt.Errorf("removing source directory: %w", err)
}
return nil
}
// finalizeObjectFile will move the object file (either a packfile, its metadata or or a loose
// object) to the target path. The move is either done with a hard link if supported or with a
// rename. No error is raised in case the target path exists already.
func finalizeObjectFile(sourcePath, targetPath string) error {
// We first try to link the file via a hardlink. The benefit compared to doing a rename is
// that in case of a collision, we do not replace the target.
err := os.Link(sourcePath, targetPath)
// In case the hardlink failed, we fall back to a rename.
renamed := false
if err != nil && !errors.Is(err, os.ErrExist) {
err = os.Rename(sourcePath, targetPath)
renamed = err == nil
}
if err != nil && !errors.Is(err, os.ErrExist) {
return fmt.Errorf("finalizing object file: %w", err)
}
if !renamed {
// It's fair to ignore the error here: we'll purge the quarantine directory anyway
// in case the context gets cancelled.
_ = os.Remove(sourcePath)
}
return nil
}
// sortEntries sorts packfiles and their associated metafiles such that we copy them over in the
// correct order.
func sortEntries(entries []os.DirEntry) {
sort.SliceStable(entries, func(i, j int) bool {
return packCopyPriority(entries[i].Name()) < packCopyPriority(entries[j].Name())
})
}
func packCopyPriority(name string) int {
switch {
case !strings.HasPrefix(name, "pack"):
return 0
case strings.HasSuffix(name, ".keep"):
return 1
case strings.HasSuffix(name, ".pack"):
return 2
case strings.HasSuffix(name, ".rev"):
return 3
case strings.HasSuffix(name, ".idx"):
return 4
default:
return 5
}
}
|