github.com/git-lfs/git-lfs@v2.5.2+incompatible/commands/pull.go (about)

     1  package commands
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"sync"
     9  
    10  	"github.com/git-lfs/git-lfs/config"
    11  	"github.com/git-lfs/git-lfs/errors"
    12  	"github.com/git-lfs/git-lfs/git"
    13  	"github.com/git-lfs/git-lfs/lfs"
    14  	"github.com/git-lfs/git-lfs/subprocess"
    15  	"github.com/git-lfs/git-lfs/tq"
    16  )
    17  
    18  // Handles the process of checking out a single file, and updating the git
    19  // index.
    20  func newSingleCheckout(gitEnv config.Environment, remote string) abstractCheckout {
    21  	manifest := getTransferManifestOperationRemote("download", remote)
    22  
    23  	clean, ok := gitEnv.Get("filter.lfs.clean")
    24  	if !ok || len(clean) == 0 {
    25  		return &noOpCheckout{manifest: manifest}
    26  	}
    27  
    28  	// Get a converter from repo-relative to cwd-relative
    29  	// Since writing data & calling git update-index must be relative to cwd
    30  	pathConverter, err := lfs.NewRepoToCurrentPathConverter(cfg)
    31  	if err != nil {
    32  		Panic(err, "Could not convert file paths")
    33  	}
    34  
    35  	return &singleCheckout{
    36  		gitIndexer:    &gitIndexer{},
    37  		pathConverter: pathConverter,
    38  		manifest:      manifest,
    39  	}
    40  }
    41  
    42  type abstractCheckout interface {
    43  	Manifest() *tq.Manifest
    44  	Skip() bool
    45  	Run(*lfs.WrappedPointer)
    46  	Close()
    47  }
    48  
    49  type singleCheckout struct {
    50  	gitIndexer    *gitIndexer
    51  	pathConverter lfs.PathConverter
    52  	manifest      *tq.Manifest
    53  }
    54  
    55  func (c *singleCheckout) Manifest() *tq.Manifest {
    56  	return c.manifest
    57  }
    58  
    59  func (c *singleCheckout) Skip() bool {
    60  	return false
    61  }
    62  
    63  func (c *singleCheckout) Run(p *lfs.WrappedPointer) {
    64  	cwdfilepath := c.pathConverter.Convert(p.Name)
    65  
    66  	// Check the content - either missing or still this pointer (not exist is ok)
    67  	filepointer, err := lfs.DecodePointerFromFile(cwdfilepath)
    68  	if err != nil && !os.IsNotExist(err) {
    69  		if errors.IsNotAPointerError(err) {
    70  			// File has non-pointer content, leave it alone
    71  			return
    72  		}
    73  
    74  		LoggedError(err, "Checkout error: %s", err)
    75  		return
    76  	}
    77  
    78  	if filepointer != nil && filepointer.Oid != p.Oid {
    79  		// User has probably manually reset a file to another commit
    80  		// while leaving it a pointer; don't mess with this
    81  		return
    82  	}
    83  
    84  	gitfilter := lfs.NewGitFilter(cfg)
    85  	err = gitfilter.SmudgeToFile(cwdfilepath, p.Pointer, false, c.manifest, nil)
    86  	if err != nil {
    87  		if errors.IsDownloadDeclinedError(err) {
    88  			// acceptable error, data not local (fetch not run or include/exclude)
    89  			LoggedError(err, "Skipped checkout for %q, content not local. Use fetch to download.", p.Name)
    90  		} else {
    91  			FullError(fmt.Errorf("Could not check out %q", p.Name))
    92  		}
    93  		return
    94  	}
    95  
    96  	// errors are only returned when the gitIndexer is starting a new cmd
    97  	if err := c.gitIndexer.Add(cwdfilepath); err != nil {
    98  		Panic(err, "Could not update the index")
    99  	}
   100  }
   101  
   102  func (c *singleCheckout) Close() {
   103  	if err := c.gitIndexer.Close(); err != nil {
   104  		LoggedError(err, "Error updating the git index:\n%s", c.gitIndexer.Output())
   105  	}
   106  }
   107  
   108  type noOpCheckout struct {
   109  	manifest *tq.Manifest
   110  }
   111  
   112  func (c *noOpCheckout) Manifest() *tq.Manifest {
   113  	return c.manifest
   114  }
   115  
   116  func (c *noOpCheckout) Skip() bool {
   117  	return true
   118  }
   119  
   120  func (c *noOpCheckout) Run(p *lfs.WrappedPointer) {}
   121  func (c *noOpCheckout) Close()                    {}
   122  
   123  // Don't fire up the update-index command until we have at least one file to
   124  // give it. Otherwise git interprets the lack of arguments to mean param-less update-index
   125  // which can trigger entire working copy to be re-examined, which triggers clean filters
   126  // and which has unexpected side effects (e.g. downloading filtered-out files)
   127  type gitIndexer struct {
   128  	cmd    *subprocess.Cmd
   129  	input  io.WriteCloser
   130  	output bytes.Buffer
   131  	mu     sync.Mutex
   132  }
   133  
   134  func (i *gitIndexer) Add(path string) error {
   135  	i.mu.Lock()
   136  	defer i.mu.Unlock()
   137  
   138  	if i.cmd == nil {
   139  		// Fire up the update-index command
   140  		cmd := git.UpdateIndexFromStdin()
   141  		cmd.Stdout = &i.output
   142  		cmd.Stderr = &i.output
   143  		stdin, err := cmd.StdinPipe()
   144  		if err != nil {
   145  			return err
   146  		}
   147  		err = cmd.Start()
   148  		if err != nil {
   149  			return err
   150  		}
   151  		i.cmd = cmd
   152  		i.input = stdin
   153  	}
   154  
   155  	i.input.Write([]byte(path + "\n"))
   156  	return nil
   157  }
   158  
   159  func (i *gitIndexer) Output() string {
   160  	return i.output.String()
   161  }
   162  
   163  func (i *gitIndexer) Close() error {
   164  	i.mu.Lock()
   165  	defer i.mu.Unlock()
   166  
   167  	if i.input != nil {
   168  		i.input.Close()
   169  	}
   170  
   171  	if i.cmd != nil {
   172  		return i.cmd.Wait()
   173  	}
   174  
   175  	return nil
   176  }