internal/git/localrepo/repo.go (177 lines of code) (raw):

package localrepo import ( "context" "errors" "fmt" "io" "os" "runtime/trace" "sync" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v16/internal/command" "gitlab.com/gitlab-org/gitaly/v16/internal/git" "gitlab.com/gitlab-org/gitaly/v16/internal/git/catfile" "gitlab.com/gitlab-org/gitaly/v16/internal/git/gitcmd" "gitlab.com/gitlab-org/gitaly/v16/internal/git/gittest" "gitlab.com/gitlab-org/gitaly/v16/internal/git/quarantine" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage/mode" "gitlab.com/gitlab-org/gitaly/v16/internal/log" "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" "google.golang.org/protobuf/proto" ) // Repo represents a local Git repository. type Repo struct { storage.Repository logger log.Logger locator storage.Locator gitCmdFactory gitcmd.CommandFactory catfileCache catfile.Cache detectObjectHash func(context.Context) (git.ObjectHash, error) detectRefBackend func(context.Context) (git.ReferenceBackend, error) } // New creates a new Repo from its protobuf representation. func New(logger log.Logger, locator storage.Locator, gitCmdFactory gitcmd.CommandFactory, catfileCache catfile.Cache, repo storage.Repository) *Repo { var ( detectObjectHashOnce sync.Once objectHash git.ObjectHash objectHashErr error detectRefBackendOnce sync.Once refBackend git.ReferenceBackend refBackendErr error ) return &Repo{ Repository: repo, logger: logger, locator: locator, gitCmdFactory: gitCmdFactory, catfileCache: catfileCache, // These are implemented as closures in order to make it safe to share these functions between // other localrepo instances derived from this one. The closures hide the details and avoid // copying the sync.Once used to facilitate the caching. detectObjectHash: func(ctx context.Context) (git.ObjectHash, error) { detectObjectHashOnce.Do(func() { path, err := locator.GetRepoPath(ctx, repo) if err != nil { objectHashErr = fmt.Errorf("get repo path: %w", err) return } objectHash, objectHashErr = gitcmd.DetectObjectHash(ctx, path) }) return objectHash, objectHashErr }, detectRefBackend: func(ctx context.Context) (git.ReferenceBackend, error) { detectRefBackendOnce.Do(func() { path, err := locator.GetRepoPath(ctx, repo) if err != nil { refBackendErr = fmt.Errorf("get repo path: %w", err) return } refBackend, refBackendErr = gitcmd.DetectReferenceBackend(ctx, path) }) return refBackend, refBackendErr }, } } // NewFrom creates a new Repo from its protobuf representation using dependencies of another Repo. func NewFrom(other *Repo, repo storage.Repository) *Repo { return New(other.logger, other.locator, other.gitCmdFactory, other.catfileCache, repo) } // Quarantine return the repository quarantined. The quarantine directory becomes the repository's // main object directory and the original object directory is configured as an alternate. func (repo *Repo) Quarantine(ctx context.Context, quarantineDirectory string) (*Repo, error) { pbRepo, ok := repo.Repository.(*gitalypb.Repository) if !ok { return nil, fmt.Errorf("unexpected repository type %t", repo.Repository) } repoPath, err := repo.locator.GetRepoPath(ctx, repo, storage.WithRepositoryVerificationSkipped()) if err != nil { return nil, fmt.Errorf("repo path: %w", err) } quarantinedRepo, err := quarantine.Apply(repoPath, pbRepo, quarantineDirectory) if err != nil { return nil, fmt.Errorf("apply quarantine: %w", err) } quarantined := NewFrom(repo, quarantinedRepo) // Share the object hash and reference backend detection with the parent to avoid // re-resolving them. quarantined.detectObjectHash = repo.detectObjectHash quarantined.detectRefBackend = repo.detectRefBackend return quarantined, nil } // QuarantineOnly returns the repository with only the quarantine directory configured as an object // directory by dropping the alternate object directories. Returns an error if the repository doesn't // have a quarantine directory configured. // // Only the alternates configured in the *gitalypb.Repository object are dropped, not the alternates // that could be in `objects/info/alternates`. Dropping the configured alternates does however also // implicitly remove the `objects/info/alternates` in the alternate object directory since the file // would exist there. The quarantine directory itself would not typically contain an // `objects/info/alternates` file. func (repo *Repo) QuarantineOnly() (*Repo, error) { pbRepo, ok := repo.Repository.(*gitalypb.Repository) if !ok { return nil, fmt.Errorf("unexpected repository type %t", repo.Repository) } cloneRepo := proto.Clone(pbRepo).(*gitalypb.Repository) cloneRepo.GitAlternateObjectDirectories = nil if cloneRepo.GetGitObjectDirectory() == "" { return nil, errors.New("repository wasn't quarantined") } return New( repo.logger, repo.locator, repo.gitCmdFactory, repo.catfileCache, cloneRepo, ), nil } // NewTestRepo constructs a Repo. It is intended as a helper function for tests which assembles // dependencies ad-hoc from the given config. func NewTestRepo(tb testing.TB, cfg config.Cfg, repo storage.Repository, factoryOpts ...gitcmd.ExecCommandFactoryOption) *Repo { tb.Helper() if cfg.SocketPath != testcfg.UnconfiguredSocketPath { repo = gittest.RewrittenRepository(tb, testhelper.Context(tb), cfg, &gitalypb.Repository{ StorageName: repo.GetStorageName(), RelativePath: repo.GetRelativePath(), GitObjectDirectory: repo.GetGitObjectDirectory(), GitAlternateObjectDirectories: repo.GetGitAlternateObjectDirectories(), }) } //nolint:forbidigo // We can't use the testhelper package here given that this is production code, so we can't //use `teshelper.NewDiscardingLogEntry()`. logrusLogger := logrus.New() logrusLogger.Out = io.Discard logger := log.FromLogrusEntry(logrus.NewEntry(logrusLogger)) gitCmdFactory, cleanup, err := gitcmd.NewExecCommandFactory(cfg, logger, factoryOpts...) tb.Cleanup(cleanup) require.NoError(tb, err) catfileCache := catfile.NewCache(cfg) tb.Cleanup(catfileCache.Stop) locator := config.NewLocator(cfg) return New(logger, locator, gitCmdFactory, catfileCache, repo) } // Exec creates a git command with the given args and Repo, executed in the // Repo. It validates the arguments in the command before executing. func (repo *Repo) Exec(ctx context.Context, cmd gitcmd.Command, opts ...gitcmd.CmdOpt) (*command.Command, error) { refBackend, err := repo.ReferenceBackend(ctx) if err != nil { return nil, err } opts = append(opts, gitcmd.WithReferenceBackend(refBackend)) return repo.gitCmdFactory.New(ctx, repo, cmd, opts...) } // ExecAndWait is similar to Exec, but waits for the command to exit before // returning. func (repo *Repo) ExecAndWait(ctx context.Context, cmd gitcmd.Command, opts ...gitcmd.CmdOpt) error { command, err := repo.Exec(ctx, cmd, opts...) if err != nil { return err } return command.Wait() } // GitVersion returns the Git version in use. func (repo *Repo) GitVersion(ctx context.Context) (git.Version, error) { return repo.gitCmdFactory.GitVersion(ctx) } func errorWithStderr(err error, stderr []byte) error { if len(stderr) == 0 { return err } return fmt.Errorf("%w, stderr: %q", err, stderr) } // StorageTempDir returns the temporary dir for the storage where the repo is on. // When this directory does not exist yet, it's being created. func (repo *Repo) StorageTempDir() (string, error) { tempPath, err := repo.locator.TempDir(repo.GetStorageName()) if err != nil { return "", err } if err := os.MkdirAll(tempPath, mode.Directory); err != nil { return "", err } return tempPath, nil } // ObjectHash detects the object hash used by this particular repository. func (repo *Repo) ObjectHash(ctx context.Context) (git.ObjectHash, error) { defer trace.StartRegion(ctx, "ObjectHash").End() return repo.detectObjectHash(ctx) } // ReferenceBackend detects the reference backend used by this repository. func (repo *Repo) ReferenceBackend(ctx context.Context) (git.ReferenceBackend, error) { defer trace.StartRegion(ctx, "ReferenceBackend").End() return repo.detectRefBackend(ctx) }