2018-06-13 21:10:32 +00:00
|
|
|
package commands
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
|
|
|
|
"github.com/git-lfs/git-lfs/errors"
|
|
|
|
"github.com/git-lfs/git-lfs/filepathfilter"
|
|
|
|
"github.com/git-lfs/git-lfs/git"
|
|
|
|
"github.com/git-lfs/git-lfs/git/githistory"
|
|
|
|
"github.com/git-lfs/git-lfs/lfs"
|
|
|
|
"github.com/git-lfs/git-lfs/tasklog"
|
|
|
|
"github.com/git-lfs/git-lfs/tools"
|
2018-07-05 16:49:10 +00:00
|
|
|
"github.com/git-lfs/gitobj"
|
2018-06-13 21:10:32 +00:00
|
|
|
"github.com/spf13/cobra"
|
|
|
|
)
|
|
|
|
|
|
|
|
func migrateExportCommand(cmd *cobra.Command, args []string) {
|
|
|
|
l := tasklog.NewLogger(os.Stderr)
|
|
|
|
defer l.Close()
|
|
|
|
|
|
|
|
db, err := getObjectDatabase()
|
|
|
|
if err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
rewriter := getHistoryRewriter(cmd, db, l)
|
|
|
|
|
|
|
|
filter := rewriter.Filter()
|
|
|
|
if len(filter.Include()) <= 0 {
|
|
|
|
ExitWithError(errors.Errorf("fatal: one or more files must be specified with --include"))
|
|
|
|
}
|
|
|
|
|
|
|
|
tracked := trackedFromExportFilter(filter)
|
|
|
|
gitfilter := lfs.NewGitFilter(cfg)
|
|
|
|
|
2018-06-21 21:03:54 +00:00
|
|
|
opts := &githistory.RewriteOptions{
|
2018-06-13 21:10:32 +00:00
|
|
|
Verbose: migrateVerbose,
|
|
|
|
ObjectMapFilePath: objectMapFilePath,
|
2018-07-05 16:49:10 +00:00
|
|
|
BlobFn: func(path string, b *gitobj.Blob) (*gitobj.Blob, error) {
|
2018-06-13 21:10:32 +00:00
|
|
|
if filepath.Base(path) == ".gitattributes" {
|
|
|
|
return b, nil
|
|
|
|
}
|
|
|
|
|
2018-06-25 21:36:57 +00:00
|
|
|
ptr, err := lfs.DecodePointer(b.Contents)
|
|
|
|
if err != nil {
|
2018-06-28 21:58:41 +00:00
|
|
|
if errors.IsNotAPointerError(err) {
|
|
|
|
return b, nil
|
|
|
|
}
|
2018-06-25 21:36:57 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2018-06-13 21:10:32 +00:00
|
|
|
|
2018-06-25 21:36:57 +00:00
|
|
|
downloadPath, err := gitfilter.ObjectPath(ptr.Oid)
|
|
|
|
if err != nil {
|
2018-06-13 21:10:32 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-07-05 16:49:10 +00:00
|
|
|
return gitobj.NewBlobFromFile(downloadPath)
|
2018-06-13 21:10:32 +00:00
|
|
|
},
|
|
|
|
|
2018-07-05 16:49:10 +00:00
|
|
|
TreeCallbackFn: func(path string, t *gitobj.Tree) (*gitobj.Tree, error) {
|
2018-06-13 21:10:32 +00:00
|
|
|
if path != "/" {
|
|
|
|
// Ignore non-root trees.
|
|
|
|
return t, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ours := tracked
|
|
|
|
theirs, err := trackedFromAttrs(db, t)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a blob of the attributes that are optionally
|
|
|
|
// present in the "t" tree's .gitattributes blob, and
|
|
|
|
// union in the patterns that we've tracked.
|
|
|
|
//
|
|
|
|
// Perform this Union() operation each time we visit a
|
|
|
|
// root tree such that if the underlying .gitattributes
|
|
|
|
// is present and has a diff between commits in the
|
|
|
|
// range of commits to migrate, those changes are
|
|
|
|
// preserved.
|
|
|
|
blob, err := trackedToBlob(db, theirs.Clone().Union(ours))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finally, return a copy of the tree "t" that has the
|
|
|
|
// new .gitattributes file included/replaced.
|
2018-07-05 16:49:10 +00:00
|
|
|
return t.Merge(&gitobj.TreeEntry{
|
2018-06-13 21:10:32 +00:00
|
|
|
Name: ".gitattributes",
|
|
|
|
Filemode: 0100644,
|
|
|
|
Oid: blob,
|
|
|
|
}), nil
|
|
|
|
},
|
|
|
|
|
|
|
|
UpdateRefs: true,
|
2018-06-21 21:03:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
requireInRepo()
|
|
|
|
|
|
|
|
opts, err = rewriteOptions(args, opts, l)
|
|
|
|
if err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
|
|
|
|
2018-06-27 18:03:23 +00:00
|
|
|
remote := cfg.Remote()
|
|
|
|
if cmd.Flag("remote").Changed {
|
|
|
|
remote = exportRemote
|
|
|
|
}
|
|
|
|
remoteURL := getAPIClient().Endpoints.RemoteEndpoint("download", remote).Url
|
|
|
|
if remoteURL == "" && cmd.Flag("remote").Changed {
|
|
|
|
ExitWithError(errors.Errorf("fatal: invalid remote %s provided", remote))
|
|
|
|
}
|
|
|
|
|
2018-06-21 21:03:54 +00:00
|
|
|
// If we have a valid remote, pre-download all objects using the Transfer Queue
|
2018-06-27 18:03:23 +00:00
|
|
|
if remoteURL != "" {
|
|
|
|
q := newDownloadQueue(getTransferManifestOperationRemote("Download", remote), remote)
|
2018-06-25 23:21:55 +00:00
|
|
|
gs := lfs.NewGitScanner(func(p *lfs.WrappedPointer, err error) {
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-06-26 00:08:58 +00:00
|
|
|
if !filter.Allows(p.Name) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-06-25 23:21:55 +00:00
|
|
|
downloadPath, err := gitfilter.ObjectPath(p.Oid)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := os.Stat(downloadPath); os.IsNotExist(err) {
|
|
|
|
q.Add(p.Name, downloadPath, p.Oid, p.Size)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
gs.ScanRefs(opts.Include, opts.Exclude, nil)
|
2018-06-21 21:03:54 +00:00
|
|
|
|
|
|
|
q.Wait()
|
|
|
|
|
|
|
|
for _, err := range q.Errors() {
|
|
|
|
if err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Perform the rewrite
|
|
|
|
if _, err := rewriter.Rewrite(opts); err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
2018-06-13 21:10:32 +00:00
|
|
|
|
2018-06-25 21:10:31 +00:00
|
|
|
// Only perform `git-checkout(1) -f` if the repository is non-bare.
|
2018-06-13 21:10:32 +00:00
|
|
|
if bare, _ := git.IsBare(); !bare {
|
|
|
|
t := l.Waiter("migrate: checkout")
|
|
|
|
err := git.Checkout("", nil, true)
|
|
|
|
t.Complete()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
|
|
|
}
|
2018-07-02 23:13:29 +00:00
|
|
|
|
|
|
|
fetchPruneCfg := lfs.NewFetchPruneConfig(cfg.Git)
|
|
|
|
|
|
|
|
// Set our preservation time-window for objects existing on the remote to
|
|
|
|
// 0. Because the newly rewritten commits have not yet been pushed, some
|
|
|
|
// exported objects can still exist on the remote within the time window
|
|
|
|
// and thus will not be pruned from the cache.
|
|
|
|
fetchPruneCfg.FetchRecentRefsDays = 0
|
|
|
|
|
|
|
|
// Prune our cache
|
|
|
|
prune(fetchPruneCfg, false, false, true)
|
2018-06-13 21:10:32 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 21:10:31 +00:00
|
|
|
// trackedFromExportFilter returns an ordered set of strings where each entry
|
|
|
|
// is a line we intend to place in the .gitattributes file. It adds/removes the
|
|
|
|
// filter/diff/merge=lfs attributes based on patterns included/excluded in the
|
|
|
|
// given filter. Since `migrate export` removes files from Git LFS, it will
|
|
|
|
// remove attributes for included files, and add attributes for excluded files
|
2018-06-13 21:10:32 +00:00
|
|
|
func trackedFromExportFilter(filter *filepathfilter.Filter) *tools.OrderedSet {
|
|
|
|
tracked := tools.NewOrderedSet()
|
|
|
|
|
|
|
|
for _, include := range filter.Include() {
|
2018-06-25 21:46:24 +00:00
|
|
|
tracked.Add(fmt.Sprintf("%s text !filter !merge !diff", escapeAttrPattern(include)))
|
2018-06-13 21:10:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, exclude := range filter.Exclude() {
|
|
|
|
tracked.Add(fmt.Sprintf("%s filter=lfs diff=lfs merge=lfs -text", escapeAttrPattern(exclude)))
|
|
|
|
}
|
|
|
|
|
|
|
|
return tracked
|
|
|
|
}
|