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 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
|
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/tools"
"github.com/git-lfs/git-lfs/v3/tq"
"github.com/git-lfs/git-lfs/v3/tr"
)
// Handles the process of checking out a single file, and updating the git
// index.
// Note that the current working directory will be changed to the root
// of the working tree, unless no work tree exists (e.g., the repository
// is bare and GIT_WORK_TREE is not defined), in which case Run() is a no-op.
func newSingleCheckout(gitEnv config.Environment, remote string) abstractCheckout {
clean, ok := gitEnv.Get("filter.lfs.clean")
if !ok || len(clean) == 0 {
return &noOpCheckout{remote: remote}
}
workingDir := cfg.LocalWorkingDir()
hasWorkTree := workingDir != ""
if hasWorkTree {
if err := os.Chdir(workingDir); err != nil {
FullError(errors.Wrap(err, tr.Tr.Get("Checkout error trying to change directory: %s", workingDir)))
hasWorkTree = false
}
}
return &singleCheckout{
gitIndexer: &gitIndexer{},
hasWorkTree: hasWorkTree,
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
hasWorkTree bool
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) {
if !c.hasWorkTree {
return
}
dirWalker := tools.NewDirWalkerForFile("", p.Name, cfg)
err := dirWalker.Walk()
var filepointer *lfs.Pointer
if err != nil {
if !os.IsNotExist(err) {
LoggedError(err, tr.Tr.Get("Checkout error trying to check path for %q: %s", p.Name, err))
return
}
} else {
// Check the content - either missing or still this pointer (not exist is ok)
filepointer, err = lfs.DecodePointerFromFile(p.Name)
}
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 ") {
// This file is deleted in the index. Don't try
// to check it out.
return
}
} else {
if errors.IsNotAPointerError(err) || errors.IsBadPointerKeyError(err) {
// File has non-pointer content, leave it alone
return
}
LoggedError(err, tr.Tr.Get("Checkout error for %q: %s", p.Name, err))
return
}
}
if filepointer != nil && filepointer.Oid != p.Oid {
// User has probably manually reset a file to another commit
// while leaving it a pointer; don't mess with this
return
}
if err != nil && os.IsNotExist(err) {
if err := dirWalker.WalkAndCreate(); err != nil {
LoggedError(err, tr.Tr.Get("Checkout error trying to create path for %q: %s", p.Name, err))
return
}
}
if err := c.RunToPath(p, p.Name); err != nil {
if errors.IsDownloadDeclinedError(err) {
// acceptable error, data not local (fetch not run or include/exclude)
Error(tr.Tr.Get("Skipped checkout for %q, content not local. Use fetch to download.", p.Name))
} else {
FullError(errors.Wrap(err, tr.Tr.Get("could not check out %q", p.Name)))
}
return
}
// errors are only returned when the gitIndexer is starting a new cmd
if err := c.gitIndexer.Add(p.Name); err != nil {
Panic(err, tr.Tr.Get("Could not update the index"))
}
}
// RunToPath checks out the pointer specified by p to the given path. It does
// not perform any sort of sanity checking or add the path to 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() {}
// Don't fire up the update-index command until we have at least one file to
// give it. Otherwise git interprets the lack of arguments to mean param-less update-index
// which can trigger entire working copy to be re-examined, which triggers clean filters
// and which has unexpected side effects (e.g. downloading filtered-out files)
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 {
// Fire up the update-index command
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
}
|