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

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  
     8  	"github.com/git-lfs/git-lfs/errors"
     9  	"github.com/git-lfs/git-lfs/filepathfilter"
    10  	"github.com/git-lfs/git-lfs/git"
    11  	"github.com/git-lfs/git-lfs/git/githistory"
    12  	"github.com/git-lfs/git-lfs/lfs"
    13  	"github.com/git-lfs/git-lfs/tasklog"
    14  	"github.com/git-lfs/git-lfs/tools"
    15  	"github.com/git-lfs/gitobj"
    16  	"github.com/spf13/cobra"
    17  )
    18  
    19  func migrateExportCommand(cmd *cobra.Command, args []string) {
    20  	ensureWorkingCopyClean(os.Stdin, os.Stderr)
    21  
    22  	l := tasklog.NewLogger(os.Stderr)
    23  	defer l.Close()
    24  
    25  	db, err := getObjectDatabase()
    26  	if err != nil {
    27  		ExitWithError(err)
    28  	}
    29  	defer db.Close()
    30  
    31  	rewriter := getHistoryRewriter(cmd, db, l)
    32  
    33  	filter := rewriter.Filter()
    34  	if len(filter.Include()) <= 0 {
    35  		ExitWithError(errors.Errorf("fatal: one or more files must be specified with --include"))
    36  	}
    37  
    38  	tracked := trackedFromExportFilter(filter)
    39  	gitfilter := lfs.NewGitFilter(cfg)
    40  
    41  	opts := &githistory.RewriteOptions{
    42  		Verbose:           migrateVerbose,
    43  		ObjectMapFilePath: objectMapFilePath,
    44  		BlobFn: func(path string, b *gitobj.Blob) (*gitobj.Blob, error) {
    45  			if filepath.Base(path) == ".gitattributes" {
    46  				return b, nil
    47  			}
    48  
    49  			ptr, err := lfs.DecodePointer(b.Contents)
    50  			if err != nil {
    51  				if errors.IsNotAPointerError(err) {
    52  					return b, nil
    53  				}
    54  				return nil, err
    55  			}
    56  
    57  			downloadPath, err := gitfilter.ObjectPath(ptr.Oid)
    58  			if err != nil {
    59  				return nil, err
    60  			}
    61  
    62  			return gitobj.NewBlobFromFile(downloadPath)
    63  		},
    64  
    65  		TreeCallbackFn: func(path string, t *gitobj.Tree) (*gitobj.Tree, error) {
    66  			if path != "/" {
    67  				// Ignore non-root trees.
    68  				return t, nil
    69  			}
    70  
    71  			ours := tracked
    72  			theirs, err := trackedFromAttrs(db, t)
    73  			if err != nil {
    74  				return nil, err
    75  			}
    76  
    77  			// Create a blob of the attributes that are optionally
    78  			// present in the "t" tree's .gitattributes blob, and
    79  			// union in the patterns that we've tracked.
    80  			//
    81  			// Perform this Union() operation each time we visit a
    82  			// root tree such that if the underlying .gitattributes
    83  			// is present and has a diff between commits in the
    84  			// range of commits to migrate, those changes are
    85  			// preserved.
    86  			blob, err := trackedToBlob(db, theirs.Clone().Union(ours))
    87  			if err != nil {
    88  				return nil, err
    89  			}
    90  
    91  			// Finally, return a copy of the tree "t" that has the
    92  			// new .gitattributes file included/replaced.
    93  			return t.Merge(&gitobj.TreeEntry{
    94  				Name:     ".gitattributes",
    95  				Filemode: 0100644,
    96  				Oid:      blob,
    97  			}), nil
    98  		},
    99  
   100  		UpdateRefs: true,
   101  	}
   102  
   103  	requireInRepo()
   104  
   105  	opts, err = rewriteOptions(args, opts, l)
   106  	if err != nil {
   107  		ExitWithError(err)
   108  	}
   109  
   110  	remote := cfg.Remote()
   111  	if cmd.Flag("remote").Changed {
   112  		remote = exportRemote
   113  	}
   114  	remoteURL := getAPIClient().Endpoints.RemoteEndpoint("download", remote).Url
   115  	if remoteURL == "" && cmd.Flag("remote").Changed {
   116  		ExitWithError(errors.Errorf("fatal: invalid remote %s provided", remote))
   117  	}
   118  
   119  	// If we have a valid remote, pre-download all objects using the Transfer Queue
   120  	if remoteURL != "" {
   121  		q := newDownloadQueue(getTransferManifestOperationRemote("Download", remote), remote)
   122  		gs := lfs.NewGitScanner(func(p *lfs.WrappedPointer, err error) {
   123  			if err != nil {
   124  				return
   125  			}
   126  
   127  			if !filter.Allows(p.Name) {
   128  				return
   129  			}
   130  
   131  			downloadPath, err := gitfilter.ObjectPath(p.Oid)
   132  			if err != nil {
   133  				return
   134  			}
   135  
   136  			if _, err := os.Stat(downloadPath); os.IsNotExist(err) {
   137  				q.Add(p.Name, downloadPath, p.Oid, p.Size)
   138  			}
   139  		})
   140  		gs.ScanRefs(opts.Include, opts.Exclude, nil)
   141  
   142  		q.Wait()
   143  
   144  		for _, err := range q.Errors() {
   145  			if err != nil {
   146  				ExitWithError(err)
   147  			}
   148  		}
   149  	}
   150  
   151  	// Perform the rewrite
   152  	if _, err := rewriter.Rewrite(opts); err != nil {
   153  		ExitWithError(err)
   154  	}
   155  
   156  	// Only perform `git-checkout(1) -f` if the repository is non-bare.
   157  	if bare, _ := git.IsBare(); !bare {
   158  		t := l.Waiter("migrate: checkout")
   159  		err := git.Checkout("", nil, true)
   160  		t.Complete()
   161  
   162  		if err != nil {
   163  			ExitWithError(err)
   164  		}
   165  	}
   166  
   167  	fetchPruneCfg := lfs.NewFetchPruneConfig(cfg.Git)
   168  
   169  	// Set our preservation time-window for objects existing on the remote to
   170  	// 0. Because the newly rewritten commits have not yet been pushed, some
   171  	// exported objects can still exist on the remote within the time window
   172  	// and thus will not be pruned from the cache.
   173  	fetchPruneCfg.FetchRecentRefsDays = 0
   174  
   175  	// Prune our cache
   176  	prune(fetchPruneCfg, false, false, true)
   177  }
   178  
   179  // trackedFromExportFilter returns an ordered set of strings where each entry
   180  // is a line we intend to place in the .gitattributes file. It adds/removes the
   181  // filter/diff/merge=lfs attributes based on patterns included/excluded in the
   182  // given filter. Since `migrate export` removes files from Git LFS, it will
   183  // remove attributes for included files, and add attributes for excluded files
   184  func trackedFromExportFilter(filter *filepathfilter.Filter) *tools.OrderedSet {
   185  	tracked := tools.NewOrderedSet()
   186  
   187  	for _, include := range filter.Include() {
   188  		tracked.Add(fmt.Sprintf("%s text !filter !merge !diff", escapeAttrPattern(include)))
   189  	}
   190  
   191  	for _, exclude := range filter.Exclude() {
   192  		tracked.Add(fmt.Sprintf("%s filter=lfs diff=lfs merge=lfs -text", escapeAttrPattern(exclude)))
   193  	}
   194  
   195  	return tracked
   196  }