internal/gitaly/service/repository/archive.go (227 lines of code) (raw):
package repository
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"strings"
"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/smudge"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage"
"gitlab.com/gitlab-org/gitaly/v16/internal/structerr"
"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
"gitlab.com/gitlab-org/gitaly/v16/streamio"
"google.golang.org/protobuf/proto"
)
type archiveParams struct {
writer io.Writer
in *gitalypb.GetArchiveRequest
compressArgs []string
format string
archivePath string
exclude []string
}
func (s *server) GetArchive(in *gitalypb.GetArchiveRequest, stream gitalypb.RepositoryService_GetArchiveServer) error {
ctx := stream.Context()
repository := in.GetRepository()
if err := s.locator.ValidateRepository(ctx, repository); err != nil {
return structerr.NewInvalidArgument("%w", err)
}
compressArgs, format := parseArchiveFormat(in.GetFormat())
repo := s.localRepoFactory.Build(repository)
repoRoot, err := repo.Path(ctx)
if err != nil {
return err
}
path, err := storage.ValidateRelativePath(repoRoot, string(in.GetPath()))
if err != nil {
return structerr.NewInvalidArgument("%w", err)
}
exclude := make([]string, len(in.GetExclude()))
for i, ex := range in.GetExclude() {
exclude[i], err = storage.ValidateRelativePath(repoRoot, string(ex))
if err != nil {
return structerr.NewInvalidArgument("%w", err)
}
}
if err := validateGetArchiveRequest(in, format); err != nil {
return err
}
if err := s.validateGetArchivePrecondition(ctx, repo, in.GetCommitId(), path, exclude); err != nil {
return err
}
if in.GetElidePath() {
// `git archive <commit ID>:<path>` expects exclusions to be relative to path
pathSlash := path + string(os.PathSeparator)
for i := range exclude {
if !strings.HasPrefix(exclude[i], pathSlash) {
return structerr.NewInvalidArgument("invalid exclude: %q is not a subdirectory of %q", exclude[i], path)
}
exclude[i] = exclude[i][len(pathSlash):]
}
}
writer := streamio.NewWriter(func(p []byte) error {
return stream.Send(&gitalypb.GetArchiveResponse{Data: p})
})
s.logger.WithField("request_hash", requestHash(in)).InfoContext(ctx, "request details")
return s.handleArchive(ctx, archiveParams{
writer: writer,
in: in,
compressArgs: compressArgs,
format: format,
archivePath: path,
exclude: exclude,
})
}
func parseArchiveFormat(format gitalypb.GetArchiveRequest_Format) ([]string, string) {
switch format {
case gitalypb.GetArchiveRequest_TAR:
return nil, "tar"
case gitalypb.GetArchiveRequest_TAR_GZ:
return []string{"gzip", "-c", "-n"}, "tar"
case gitalypb.GetArchiveRequest_TAR_BZ2:
return []string{"bzip2", "-c"}, "tar"
case gitalypb.GetArchiveRequest_ZIP:
return nil, "zip"
}
return nil, ""
}
func validateGetArchiveRequest(in *gitalypb.GetArchiveRequest, format string) error {
if err := git.ValidateRevision([]byte(in.GetCommitId())); err != nil {
return structerr.NewInvalidArgument("invalid commitId: %w", err)
}
if len(format) == 0 {
return structerr.NewInvalidArgument("invalid format")
}
return nil
}
func (s *server) validateGetArchivePrecondition(
ctx context.Context,
repo gitcmd.RepositoryExecutor,
commitID string,
path string,
exclude []string,
) error {
objectReader, cancel, err := s.catfileCache.ObjectReader(ctx, repo)
if err != nil {
return err
}
defer cancel()
f := catfile.NewTreeEntryFinder(objectReader)
if path != "." {
if ok, err := findGetArchivePath(ctx, f, commitID, path); err != nil {
return err
} else if !ok {
return structerr.NewFailedPrecondition("path doesn't exist")
}
} else {
objectInfoReader, cancel, err := s.catfileCache.ObjectInfoReader(ctx, repo)
if err != nil {
return err
}
defer cancel()
repoHash, err := repo.ObjectHash(ctx)
if err != nil {
return err
}
rootTree, err := objectInfoReader.Info(ctx, git.ObjectID(commitID).Revision()+"^{tree}")
if err != nil {
return err
}
// Root tree is empty, nothing to return.
if rootTree.ObjectID() == repoHash.EmptyTreeOID {
return structerr.NewFailedPrecondition("path doesn't exist")
}
}
for i, exclude := range exclude {
if ok, err := findGetArchivePath(ctx, f, commitID, exclude); err != nil {
return err
} else if !ok {
return structerr.NewFailedPrecondition("exclude[%d] doesn't exist", i)
}
}
return nil
}
func findGetArchivePath(ctx context.Context, f *catfile.TreeEntryFinder, commitID, path string) (ok bool, err error) {
treeEntry, err := f.FindByRevisionAndPath(ctx, commitID, path)
if err != nil {
return false, err
}
if treeEntry == nil || len(treeEntry.GetOid()) == 0 {
return false, nil
}
return true, nil
}
func (s *server) handleArchive(ctx context.Context, p archiveParams) error {
var args []string
pathspecs := make([]string, 0, len(p.exclude)+1)
if !p.in.GetElidePath() {
// git archive [options] <commit ID> -- <path> [exclude*]
args = []string{p.in.GetCommitId()}
pathspecs = append(pathspecs, p.archivePath)
} else if p.archivePath != "." {
// git archive [options] <commit ID>:<path> -- [exclude*]
args = []string{p.in.GetCommitId() + ":" + p.archivePath}
} else {
// git archive [options] <commit ID> -- [exclude*]
args = []string{p.in.GetCommitId()}
}
for _, exclude := range p.exclude {
pathspecs = append(pathspecs, ":(exclude)"+exclude)
}
var env []string
var config []gitcmd.ConfigPair
if p.in.GetIncludeLfsBlobs() {
smudgeCfg := smudge.Config{
GlRepository: p.in.GetRepository().GetGlRepository(),
Gitlab: s.cfg.Gitlab,
TLS: s.cfg.TLS,
DriverType: smudge.DriverTypeProcess,
}
smudgeEnv, err := smudgeCfg.Environment()
if err != nil {
return fmt.Errorf("setting up smudge environment: %w", err)
}
smudgeGitConfig, err := smudgeCfg.GitConfiguration(s.cfg)
if err != nil {
return fmt.Errorf("setting up smudge gitconfig: %w", err)
}
env = append(
env,
smudgeEnv,
)
config = append(config, smudgeGitConfig)
}
repo := s.localRepoFactory.Build(p.in.GetRepository())
archiveCommand, err := repo.Exec(ctx, gitcmd.Command{
Name: "archive",
Flags: []gitcmd.Option{gitcmd.ValueFlag{Name: "--format", Value: p.format}, gitcmd.ValueFlag{Name: "--prefix", Value: p.in.GetPrefix() + "/"}},
Args: args,
PostSepArgs: pathspecs,
}, gitcmd.WithEnv(env...), gitcmd.WithConfig(config...), gitcmd.WithSetupStdout())
if err != nil {
return err
}
if len(p.compressArgs) > 0 {
command, err := command.New(ctx, s.logger, p.compressArgs,
command.WithStdin(archiveCommand), command.WithStdout(p.writer),
)
if err != nil {
return err
}
if err := command.Wait(); err != nil {
return err
}
} else if _, err = io.Copy(p.writer, archiveCommand); err != nil {
return err
}
return archiveCommand.Wait()
}
func requestHash(req proto.Message) string {
reqBytes, err := proto.Marshal(req)
if err != nil {
return "failed to hash request"
}
hash := sha256.Sum256(reqBytes)
return hex.EncodeToString(hash[:])
}