package commands
import (
"bytes"
"io"
"os"
"strings"
"sync"
"github.com/git-lfs/git-lfs/v3/config"
"github.com/git-lfs/git-lfs/v3/errors"
"github.com/git-lfs/git-lfs/v3/git"
"github.com/git-lfs/git-lfs/v3/lfs"
"github.com/git-lfs/git-lfs/v3/subprocess"
"github.com/git-lfs/git-lfs/v3/tq"
"github.com/git-lfs/git-lfs/v3/tr"
)
func newSingleCheckout(gitEnv config.Environment, remote string) abstractCheckout {
clean, ok := gitEnv.Get("filter.lfs.clean")
if !ok || len(clean) == 0 {
return &noOpCheckout{remote: remote}
}
pathConverter, err := lfs.NewRepoToCurrentPathConverter(cfg)
if err != nil {
Panic(err, tr.Tr.Get("Could not convert file paths"))
}
return &singleCheckout{
gitIndexer: &gitIndexer{},
pathConverter: pathConverter,
manifest: nil,
remote: remote,
}
}
type abstractCheckout interface {
Manifest() tq.Manifest
Skip() bool
Run(*lfs.WrappedPointer)
RunToPath(*lfs.WrappedPointer, string) error
Close()
}
type singleCheckout struct {
gitIndexer *gitIndexer
pathConverter lfs.PathConverter
manifest tq.Manifest
remote string
}
func (c *singleCheckout) Manifest() tq.Manifest {
if c.manifest == nil {
c.manifest = getTransferManifestOperationRemote("download", c.remote)
}
return c.manifest
}
func (c *singleCheckout) Skip() bool {
return false
}
func (c *singleCheckout) Run(p *lfs.WrappedPointer) {
cwdfilepath := c.pathConverter.Convert(p.Name)
filepointer, err := lfs.DecodePointerFromFile(cwdfilepath)
if err != nil {
if os.IsNotExist(err) {
output, err := git.DiffIndexWithPaths("HEAD", true, []string{p.Name})
if err != nil {
LoggedError(err, tr.Tr.Get("Checkout error trying to run diff-index: %s", err))
return
}
if strings.HasPrefix(output, ":100644 000000 ") || strings.HasPrefix(output, ":100755 000000 ") {
return
}
} else {
if errors.IsNotAPointerError(err) || errors.IsBadPointerKeyError(err) {
return
}
LoggedError(err, tr.Tr.Get("Checkout error: %s", err))
return
}
}
if filepointer != nil && filepointer.Oid != p.Oid {
return
}
if err := c.RunToPath(p, cwdfilepath); err != nil {
if errors.IsDownloadDeclinedError(err) {
Error(tr.Tr.Get("Skipped checkout for %q, content not local. Use fetch to download.", p.Name))
} else {
FullError(errors.New(tr.Tr.Get("could not check out %q", p.Name)))
}
return
}
if err := c.gitIndexer.Add(cwdfilepath); err != nil {
Panic(err, tr.Tr.Get("Could not update the index"))
}
}
func (c *singleCheckout) RunToPath(p *lfs.WrappedPointer, path string) error {
gitfilter := lfs.NewGitFilter(cfg)
return gitfilter.SmudgeToFile(path, p.Pointer, false, c.manifest, nil)
}
func (c *singleCheckout) Close() {
if err := c.gitIndexer.Close(); err != nil {
LoggedError(err, "%s\n%s", tr.Tr.Get("Error updating the Git index:"), c.gitIndexer.Output())
}
}
type noOpCheckout struct {
manifest tq.Manifest
remote string
}
func (c *noOpCheckout) Manifest() tq.Manifest {
if c.manifest == nil {
c.manifest = getTransferManifestOperationRemote("download", c.remote)
}
return c.manifest
}
func (c *noOpCheckout) Skip() bool {
return true
}
func (c *noOpCheckout) RunToPath(p *lfs.WrappedPointer, path string) error {
return nil
}
func (c *noOpCheckout) Run(p *lfs.WrappedPointer) {}
func (c *noOpCheckout) Close() {}
type gitIndexer struct {
cmd *subprocess.Cmd
input io.WriteCloser
output bytes.Buffer
mu sync.Mutex
}
func (i *gitIndexer) Add(path string) error {
i.mu.Lock()
defer i.mu.Unlock()
if i.cmd == nil {
cmd, err := git.UpdateIndexFromStdin()
if err != nil {
return err
}
cmd.Stdout = &i.output
cmd.Stderr = &i.output
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
err = cmd.Start()
if err != nil {
return err
}
i.cmd = cmd
i.input = stdin
}
i.input.Write([]byte(path + "\n"))
return nil
}
func (i *gitIndexer) Output() string {
return i.output.String()
}
func (i *gitIndexer) Close() error {
i.mu.Lock()
defer i.mu.Unlock()
if i.input != nil {
i.input.Close()
}
if i.cmd != nil {
return i.cmd.Wait()
}
return nil
}