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

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"time"
     7  
     8  	"github.com/git-lfs/git-lfs/filepathfilter"
     9  	"github.com/git-lfs/git-lfs/git"
    10  	"github.com/git-lfs/git-lfs/lfs"
    11  	"github.com/git-lfs/git-lfs/tasklog"
    12  	"github.com/git-lfs/git-lfs/tq"
    13  	"github.com/rubyist/tracerx"
    14  	"github.com/spf13/cobra"
    15  )
    16  
    17  var (
    18  	fetchRecentArg bool
    19  	fetchAllArg    bool
    20  	fetchPruneArg  bool
    21  )
    22  
    23  func getIncludeExcludeArgs(cmd *cobra.Command) (include, exclude *string) {
    24  	includeFlag := cmd.Flag("include")
    25  	excludeFlag := cmd.Flag("exclude")
    26  	if includeFlag.Changed {
    27  		include = &includeArg
    28  	}
    29  	if excludeFlag.Changed {
    30  		exclude = &excludeArg
    31  	}
    32  
    33  	return
    34  }
    35  
    36  func fetchCommand(cmd *cobra.Command, args []string) {
    37  	requireInRepo()
    38  
    39  	var refs []*git.Ref
    40  
    41  	if len(args) > 0 {
    42  		// Remote is first arg
    43  		if err := cfg.SetValidRemote(args[0]); err != nil {
    44  			Exit("Invalid remote name %q: %s", args[0], err)
    45  		}
    46  	}
    47  
    48  	if len(args) > 1 {
    49  		resolvedrefs, err := git.ResolveRefs(args[1:])
    50  		if err != nil {
    51  			Panic(err, "Invalid ref argument: %v", args[1:])
    52  		}
    53  		refs = resolvedrefs
    54  	} else if !fetchAllArg {
    55  		ref, err := git.CurrentRef()
    56  		if err != nil {
    57  			Panic(err, "Could not fetch")
    58  		}
    59  		refs = []*git.Ref{ref}
    60  	}
    61  
    62  	success := true
    63  	gitscanner := lfs.NewGitScanner(nil)
    64  	defer gitscanner.Close()
    65  
    66  	include, exclude := getIncludeExcludeArgs(cmd)
    67  	fetchPruneCfg := lfs.NewFetchPruneConfig(cfg.Git)
    68  
    69  	if fetchAllArg {
    70  		if fetchRecentArg || len(args) > 1 {
    71  			Exit("Cannot combine --all with ref arguments or --recent")
    72  		}
    73  		if include != nil || exclude != nil {
    74  			Exit("Cannot combine --all with --include or --exclude")
    75  		}
    76  		if len(cfg.FetchIncludePaths()) > 0 || len(cfg.FetchExcludePaths()) > 0 {
    77  			Print("Ignoring global include / exclude paths to fulfil --all")
    78  		}
    79  		success = fetchAll()
    80  
    81  	} else { // !all
    82  		filter := buildFilepathFilter(cfg, include, exclude)
    83  
    84  		// Fetch refs sequentially per arg order; duplicates in later refs will be ignored
    85  		for _, ref := range refs {
    86  			Print("fetch: Fetching reference %s", ref.Refspec())
    87  			s := fetchRef(ref.Sha, filter)
    88  			success = success && s
    89  		}
    90  
    91  		if fetchRecentArg || fetchPruneCfg.FetchRecentAlways {
    92  			s := fetchRecent(fetchPruneCfg, refs, filter)
    93  			success = success && s
    94  		}
    95  	}
    96  
    97  	if fetchPruneArg {
    98  		verify := fetchPruneCfg.PruneVerifyRemoteAlways
    99  		// no dry-run or verbose options in fetch, assume false
   100  		prune(fetchPruneCfg, verify, false, false)
   101  	}
   102  
   103  	if !success {
   104  		c := getAPIClient()
   105  		e := c.Endpoints.Endpoint("download", cfg.Remote())
   106  		Exit("error: failed to fetch some objects from '%s'", e.Url)
   107  	}
   108  }
   109  
   110  func pointersToFetchForRef(ref string, filter *filepathfilter.Filter) ([]*lfs.WrappedPointer, error) {
   111  	var pointers []*lfs.WrappedPointer
   112  	var multiErr error
   113  	tempgitscanner := lfs.NewGitScanner(func(p *lfs.WrappedPointer, err error) {
   114  		if err != nil {
   115  			if multiErr != nil {
   116  				multiErr = fmt.Errorf("%v\n%v", multiErr, err)
   117  			} else {
   118  				multiErr = err
   119  			}
   120  			return
   121  		}
   122  
   123  		pointers = append(pointers, p)
   124  	})
   125  
   126  	tempgitscanner.Filter = filter
   127  
   128  	if err := tempgitscanner.ScanTree(ref); err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	tempgitscanner.Close()
   133  	return pointers, multiErr
   134  }
   135  
   136  // Fetch all binaries for a given ref (that we don't have already)
   137  func fetchRef(ref string, filter *filepathfilter.Filter) bool {
   138  	pointers, err := pointersToFetchForRef(ref, filter)
   139  	if err != nil {
   140  		Panic(err, "Could not scan for Git LFS files")
   141  	}
   142  	return fetchAndReportToChan(pointers, filter, nil)
   143  }
   144  
   145  // Fetch all previous versions of objects from since to ref (not including final state at ref)
   146  // So this will fetch all the '-' sides of the diff from since to ref
   147  func fetchPreviousVersions(ref string, since time.Time, filter *filepathfilter.Filter) bool {
   148  	var pointers []*lfs.WrappedPointer
   149  
   150  	tempgitscanner := lfs.NewGitScanner(func(p *lfs.WrappedPointer, err error) {
   151  		if err != nil {
   152  			Panic(err, "Could not scan for Git LFS previous versions")
   153  			return
   154  		}
   155  
   156  		pointers = append(pointers, p)
   157  	})
   158  
   159  	tempgitscanner.Filter = filter
   160  
   161  	if err := tempgitscanner.ScanPreviousVersions(ref, since, nil); err != nil {
   162  		ExitWithError(err)
   163  	}
   164  
   165  	tempgitscanner.Close()
   166  	return fetchAndReportToChan(pointers, filter, nil)
   167  }
   168  
   169  // Fetch recent objects based on config
   170  func fetchRecent(fetchconf lfs.FetchPruneConfig, alreadyFetchedRefs []*git.Ref, filter *filepathfilter.Filter) bool {
   171  	if fetchconf.FetchRecentRefsDays == 0 && fetchconf.FetchRecentCommitsDays == 0 {
   172  		return true
   173  	}
   174  
   175  	ok := true
   176  	// Make a list of what unique commits we've already fetched for to avoid duplicating work
   177  	uniqueRefShas := make(map[string]string, len(alreadyFetchedRefs))
   178  	for _, ref := range alreadyFetchedRefs {
   179  		uniqueRefShas[ref.Sha] = ref.Name
   180  	}
   181  	// First find any other recent refs
   182  	if fetchconf.FetchRecentRefsDays > 0 {
   183  		Print("fetch: Fetching recent branches within %v days", fetchconf.FetchRecentRefsDays)
   184  		refsSince := time.Now().AddDate(0, 0, -fetchconf.FetchRecentRefsDays)
   185  		refs, err := git.RecentBranches(refsSince, fetchconf.FetchRecentRefsIncludeRemotes, cfg.Remote())
   186  		if err != nil {
   187  			Panic(err, "Could not scan for recent refs")
   188  		}
   189  		for _, ref := range refs {
   190  			// Don't fetch for the same SHA twice
   191  			if prevRefName, ok := uniqueRefShas[ref.Sha]; ok {
   192  				if ref.Name != prevRefName {
   193  					tracerx.Printf("Skipping fetch for %v, already fetched via %v", ref.Name, prevRefName)
   194  				}
   195  			} else {
   196  				uniqueRefShas[ref.Sha] = ref.Name
   197  				Print("fetch: Fetching reference %s", ref.Name)
   198  				k := fetchRef(ref.Sha, filter)
   199  				ok = ok && k
   200  			}
   201  		}
   202  	}
   203  	// For every unique commit we've fetched, check recent commits too
   204  	if fetchconf.FetchRecentCommitsDays > 0 {
   205  		for commit, refName := range uniqueRefShas {
   206  			// We measure from the last commit at the ref
   207  			summ, err := git.GetCommitSummary(commit)
   208  			if err != nil {
   209  				Error("Couldn't scan commits at %v: %v", refName, err)
   210  				continue
   211  			}
   212  			Print("fetch: Fetching changes within %v days of %v", fetchconf.FetchRecentCommitsDays, refName)
   213  			commitsSince := summ.CommitDate.AddDate(0, 0, -fetchconf.FetchRecentCommitsDays)
   214  			k := fetchPreviousVersions(commit, commitsSince, filter)
   215  			ok = ok && k
   216  		}
   217  
   218  	}
   219  	return ok
   220  }
   221  
   222  func fetchAll() bool {
   223  	pointers := scanAll()
   224  	Print("fetch: Fetching all references...")
   225  	return fetchAndReportToChan(pointers, nil, nil)
   226  }
   227  
   228  func scanAll() []*lfs.WrappedPointer {
   229  	// This could be a long process so use the chan version & report progress
   230  	task := tasklog.NewSimpleTask()
   231  	defer task.Complete()
   232  
   233  	logger := tasklog.NewLogger(OutputWriter)
   234  	logger.Enqueue(task)
   235  	var numObjs int64
   236  
   237  	// use temp gitscanner to collect pointers
   238  	var pointers []*lfs.WrappedPointer
   239  	var multiErr error
   240  	tempgitscanner := lfs.NewGitScanner(func(p *lfs.WrappedPointer, err error) {
   241  		if err != nil {
   242  			if multiErr != nil {
   243  				multiErr = fmt.Errorf("%v\n%v", multiErr, err)
   244  			} else {
   245  				multiErr = err
   246  			}
   247  			return
   248  		}
   249  
   250  		numObjs++
   251  		task.Logf("fetch: %d object(s) found", numObjs)
   252  		pointers = append(pointers, p)
   253  	})
   254  
   255  	if err := tempgitscanner.ScanAll(nil); err != nil {
   256  		Panic(err, "Could not scan for Git LFS files")
   257  	}
   258  
   259  	tempgitscanner.Close()
   260  
   261  	if multiErr != nil {
   262  		Panic(multiErr, "Could not scan for Git LFS files")
   263  	}
   264  
   265  	return pointers
   266  }
   267  
   268  // Fetch and report completion of each OID to a channel (optional, pass nil to skip)
   269  // Returns true if all completed with no errors, false if errors were written to stderr/log
   270  func fetchAndReportToChan(allpointers []*lfs.WrappedPointer, filter *filepathfilter.Filter, out chan<- *lfs.WrappedPointer) bool {
   271  	ready, pointers, meter := readyAndMissingPointers(allpointers, filter)
   272  	q := newDownloadQueue(
   273  		getTransferManifestOperationRemote("download", cfg.Remote()),
   274  		cfg.Remote(), tq.WithProgress(meter),
   275  	)
   276  
   277  	if out != nil {
   278  		// If we already have it, or it won't be fetched
   279  		// report it to chan immediately to support pull/checkout
   280  		for _, p := range ready {
   281  			out <- p
   282  		}
   283  
   284  		dlwatch := q.Watch()
   285  
   286  		go func() {
   287  			// fetch only reports single OID, but OID *might* be referenced by multiple
   288  			// WrappedPointers if same content is at multiple paths, so map oid->slice
   289  			oidToPointers := make(map[string][]*lfs.WrappedPointer, len(pointers))
   290  			for _, pointer := range pointers {
   291  				plist := oidToPointers[pointer.Oid]
   292  				oidToPointers[pointer.Oid] = append(plist, pointer)
   293  			}
   294  
   295  			for t := range dlwatch {
   296  				plist, ok := oidToPointers[t.Oid]
   297  				if !ok {
   298  					continue
   299  				}
   300  				for _, p := range plist {
   301  					out <- p
   302  				}
   303  			}
   304  			close(out)
   305  		}()
   306  	}
   307  
   308  	for _, p := range pointers {
   309  		tracerx.Printf("fetch %v [%v]", p.Name, p.Oid)
   310  
   311  		q.Add(downloadTransfer(p))
   312  	}
   313  
   314  	processQueue := time.Now()
   315  	q.Wait()
   316  	tracerx.PerformanceSince("process queue", processQueue)
   317  
   318  	ok := true
   319  	for _, err := range q.Errors() {
   320  		ok = false
   321  		FullError(err)
   322  	}
   323  	return ok
   324  }
   325  
   326  func readyAndMissingPointers(allpointers []*lfs.WrappedPointer, filter *filepathfilter.Filter) ([]*lfs.WrappedPointer, []*lfs.WrappedPointer, *tq.Meter) {
   327  	logger := tasklog.NewLogger(os.Stdout)
   328  	meter := buildProgressMeter(false, tq.Download)
   329  	logger.Enqueue(meter)
   330  
   331  	seen := make(map[string]bool, len(allpointers))
   332  	missing := make([]*lfs.WrappedPointer, 0, len(allpointers))
   333  	ready := make([]*lfs.WrappedPointer, 0, len(allpointers))
   334  
   335  	for _, p := range allpointers {
   336  		// no need to download the same object multiple times
   337  		if seen[p.Oid] {
   338  			continue
   339  		}
   340  
   341  		seen[p.Oid] = true
   342  
   343  		// no need to download objects that exist locally already
   344  		lfs.LinkOrCopyFromReference(cfg, p.Oid, p.Size)
   345  		if cfg.LFSObjectExists(p.Oid, p.Size) {
   346  			ready = append(ready, p)
   347  			continue
   348  		}
   349  
   350  		missing = append(missing, p)
   351  		meter.Add(p.Size)
   352  	}
   353  
   354  	return ready, missing, meter
   355  }
   356  
   357  func init() {
   358  	RegisterCommand("fetch", fetchCommand, func(cmd *cobra.Command) {
   359  		cmd.Flags().StringVarP(&includeArg, "include", "I", "", "Include a list of paths")
   360  		cmd.Flags().StringVarP(&excludeArg, "exclude", "X", "", "Exclude a list of paths")
   361  		cmd.Flags().BoolVarP(&fetchRecentArg, "recent", "r", false, "Fetch recent refs & commits")
   362  		cmd.Flags().BoolVarP(&fetchAllArg, "all", "a", false, "Fetch all LFS files ever referenced")
   363  		cmd.Flags().BoolVarP(&fetchPruneArg, "prune", "p", false, "After fetching, prune old data")
   364  	})
   365  }