Implement `migrate export` subcommand by PastelMobileSuit · Pull Request #3084 · git-lfs/git-lfs · GitHub
Skip to content

Implement migrate export subcommand - #3084

Merged
PastelMobileSuit merged 19 commits into
masterfrom
pastelmobilesuit-migrate-export
Jul 3, 2018
Merged

PastelMobileSuit merged 19 commits into
masterfrom
pastelmobilesuit-migrate-export

Conversation

@PastelMobileSuit

Copy link
Copy Markdown
Contributor

This PR implements a migrate export subcommand as described in #3057. The command requires a list of patterns specified with the --include argument and will convert any pointers matching those patterns from a Git LFS pointer to a regular Git object. Any objects that don't already exist in the cache will be downloaded before history is rewritten using the transfer queue.

I wanted to get this PR up for review now before it gets too large. I'm planning on a few more changes in subsequent PRs after this gets merged:

  • Documentation
  • Pruning the cache of unreachable objects
  • Smarter modification of .gitattributes files

@PastelMobileSuit
PastelMobileSuit force-pushed the pastelmobilesuit-migrate-export branch from ebd9ac3 to d84383b Compare June 23, 2018 16:20

@ttaylorr ttaylorr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, @PastelMobileSuit! I left a handful of comments as a first-pass over these changes.

With regards to the comment in the body of your PR, I think that both (1) documentation and (2) cache pruning can go in this PR, too. For (2), I think we should remove everything that we can (i.e., everything that we migrated out of Git LFS, and keep everything that we didn't.)

Could you explain a little bit more about what you mean w.r.t "smarter .gitattributes modification"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it might make sense to bring in a few of the flags from import, namely: --verbose and --object-map. I think that we can leave --no-rewrite off for now, but it might make sense to bring that in later.

Comment thread commands/command_migrate.go Outdated
cmd.PersistentFlags().BoolVar(&migrateSkipFetch, "skip-fetch", false, "Assume up-to-date remote references.")

cmd.AddCommand(importCmd, info)
cmd.AddCommand(importCmd, info, exportCmd)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit; let's put exportCmd before the other two arguments, to keep this list alphabetized.

Comment thread commands/command_migrate_export.go Outdated
}

// trackedFromExportFilter returns an ordered set of strings where each entry is a
// line in the .gitattributes file. It adds/removes the fiter/diff/merge=lfs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify this line? I think the last sentence in this block makes the intended purpose of the function clear, but the term .gitattributes I am unsure of in this context alone.

Maybe something like:

"... where each entry is a line we intend to place in the repository's .gitattributes file."

gitfilter := lfs.NewGitFilter(cfg)

opts := &githistory.RewriteOptions{
Verbose: migrateVerbose,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this and the ObjectMapFilePath are good to have, but presently don't have any effect, since the flags are not added to the export command.

Comment thread commands/command_migrate_export.go Outdated
return b, nil
}

var buf bytes.Buffer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may run into a problem where the size of the contents that we're smudging are larger than what we would like to hold in memory. Since buf is local to the stack frame, we'll have to worry about that here.

Since we already do a pass over history below with ScanForPointers, I think we can avoid smudging here, since the large object that we're looking for is present in the .git/lfs/objects cache.

So, we can use the MediaPath function on gitfilter to find the location of this file on disk. We'll have to open that file and then pass the *os.File (which implements io.Reader) to the *odb.Blob constructor's Contents field.

We'll need to close the file later, but something like this could work:

diff --git a/git/odb/blob.go b/git/odb/blob.go
index 7f9d66ae..e9cc52f8 100644
--- a/git/odb/blob.go
+++ b/git/odb/blob.go
@@ -3,6 +3,9 @@ package odb
 import (
 	"bytes"
 	"io"
+	"os"
+
+	"github.com/git-lfs/git-lfs/errors"
 )
 
 // Blob represents a Git object of type "blob".
@@ -19,6 +22,42 @@ type Blob struct {
 	closeFn func() error
 }
 
+// NewBlobFromFile returns a new *Blob that contains the contents of the file
+// at location "path" on disk. NewBlobFromFile does not read the file ahead of
+// time, and instead defers this task until encoding the blob to the object
+// database.
+//
+// If the file cannot be opened or stat(1)-ed, an error will be returned.
+//
+// When the blob receives a function call Close(), the file will also be closed,
+// and any error encountered in doing so will be returned from Close().
+func NewBlobFromFile(path string) (*Blob, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, errors.Wrapf(err, "git/odb: could not open: %s",
+			path)
+	}
+
+	stat, err := f.Stat()
+	if err != nil {
+		return nil, errors.Wrapf(err, "git/odb: could not stat %s",
+			path)
+	}
+
+	return &Blob{
+		Contents: f,
+		Size:     stat.Size(),
+
+		closeFn: func() error {
+			if err := f.Close(); err != nil {
+				return errors.Wrapf(err,
+					"git/odb: could not close %s", path)
+			}
+			return nil
+		},
+	}, nil
+}
+
 // NewBlobFromBytes returns a new *Blob that yields the data given.
 func NewBlobFromBytes(contents []byte) *Blob {
 	return &Blob{

Comment thread commands/command_migrate_export.go Outdated
}

// If we have a valid remote, pre-download all objects using the Transfer Queue
if remoteURL := getAPIClient().Endpoints.RemoteEndpoint("download", cfg.Remote()).Url; remoteURL != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. What do you think about allowing [--remote=<remote>] as an optional argument in this command, to specify where clients would like to get their objects from?

We could even take it a step further and allow them to specify multiple remotes. Package tq today doesn't know how to interact with multiple remotes at once (and I'm inclined not to teach it how to do so), but we could work around this by looping through each given remote. Maybe something like:

var wg sync.WaitGroup

for _, remote := range remotes {
        wg.Add(1)
        go downloadObjects(remotes, wg)
}

wg.Wait()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good idea! We can just default to cfg.Remote() like we do here if the argument isn't provided.

Do we want to support multiple remotes for this? What would the use cases for that look like? I'm concerned about potential conflicts in object downloads, for example if one provided remote points to a fork of another repo, which is also provided as a remote.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that supporting just the one remote is fine for now; I can see this becoming somewhat complicated because the transfer queue really isn't designed for multi-remote operations.

I think that there are a few approaches we can take in the longer term, but for now, I think that supporting one remote via --remote is totally good. If a user is (1) the bleeding-edge version of Git LFS, and (2) wants to do a multi-remote export, they can always use git lfs fetch && git lfs migrate export instead.

Longer-term, I think we could do one of the following:

  1. Teach package tq how to handle multiple remotes. This would provide a consistent interface for using the transfer queue, but will be only used at this singular call-site, and seems too complicated for too little benefit.

  2. Keep many transfer queues alive, and round-robin the objects through them. This has the benefit of simplicity and no requirements for change inside of package tq, and the external call-sites should be fairly straightforward, too. If an earlier transfer queue has the same object as a later one, it doesn't really matter which one answers first. This has the drawback of having to wait for one transfer queue to cycle before the next one in line can determine which objects they need to get.

I think that we could theoretically get more efficient than that, but I bet that that's an OK starting point for now.

Comment thread commands/command_migrate_export.go Outdated
ExitWithError(err)
}

// Only perform `git-checkout(1) -f` if the repository is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit; I think that we can re-wrap this line here and in git lfs migrate import (though perhaps the later should go in a preparatory commit).

Comment thread commands/command_migrate_export.go Outdated
tracked := tools.NewOrderedSet()

for _, include := range filter.Include() {
tracked.Add(fmt.Sprintf("%s text -filter -merge -diff", escapeAttrPattern(include)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. I think that we are supposed to spell the -filter as !filter, though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the gitattributes documentation, it looks like - sets the value of an attribute to false, whereas ! sets the value of an attribute to unspecified. Since we're doing an export here, it seems like it might be better to use -

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what it looks like to me, too:

https://github.com/git/git/blob/53f9a3e157dbbc901a02ac2c73346d375e24978c/attr.c#L334-L335

But, I think that ATTR__UNSET is more appropriate; according to the merge documentation from that same page:

Unset

Take the version from the current branch as the tentative merge result, and
declare that the merge has conflicts. This is suitable for binary files that
do not have a well-defined merge semantics.

Unspecified

By default, this uses the same built-in 3-way merge driver as is the case when
the merge attribute is set. However, the merge.default configuration
variable can name different merge driver to be used with paths for which the
merge attribute is unspecified.

I think that given the choice between the two, we'd prefer unspecified, which defaults back to the configuration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense! In that case I'll go ahead and switch this over

Comment thread git/githistory/rewriter.go Outdated
return rewriter
}

// ScanForPointers scans through the range of commits given by

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. Historically, we've used package lfs's GitScanner type to do such a thing. It's backed by the now-outdated git/git.go, which shells out to git(1), but it does roughly what you're doing here. I'd like someday to replace it with a git/odb-backed implementation, but for now that doesn't exist. Instead of growing two separate implementations, how about we lean on what's already provided and replace it over time in a later PR?

diff --git a/commands/command_migrate_export.go b/commands/command_migrate_export.go
index b234eac9..53e64f49 100644
--- a/commands/command_migrate_export.go
+++ b/commands/command_migrate_export.go
@@ -104,10 +104,15 @@ func migrateExportCommand(cmd *cobra.Command, args []string) {
 	// If we have a valid remote, pre-download all objects using the Transfer Queue
 	if remoteURL := getAPIClient().Endpoints.RemoteEndpoint("download", cfg.Remote()).Url; remoteURL != "" {
 		q := newDownloadQueue(getTransferManifestOperationRemote("Download", cfg.Remote()), cfg.Remote())
-		if err := rewriter.ScanForPointers(q, opts, gitfilter); err != nil {
+		scan := lfs.NewGitScanner(func(p *lfs.WrappedPointer, err error) {
+			if err != nil {
+				return
+			}
+			q.Add(p.Name, p.Name, p.Oid, p.Size)
+		})
+		if err := scan.ScanRefComplex(opts.Include, opts.Exclude, nil); err != nil {
 			ExitWithError(err)
 		}
-
 		q.Wait()
 
 		for _, err := range q.Errors() {
diff --git a/lfs/gitscanner.go b/lfs/gitscanner.go
index 15a6d58c..20d6e49d 100644
--- a/lfs/gitscanner.go
+++ b/lfs/gitscanner.go
@@ -92,6 +92,17 @@ func (s *GitScanner) ScanLeftToRemote(left string, cb GitScannerFoundPointer) er
 	return scanRefsToChan(s, callback, left, "", s.opts(ScanLeftToRemoteMode))
 }
 
+func (s *GitScanner) ScanRefComplex(include, exclude []string, cb GitScannerFoundPointer) error {
+	callback, err := firstGitScannerCallback(cb, s.FoundPointer)
+	if err != nil {
+		return err
+	}
+
+	opts := s.opts(ScanRefsMode)
+	opts.SkipDeletedBlobs = false
+	return scanRefsToChan(s, callback, include, exclude, opts)
+}
+
 // ScanRefRange scans through all commits from the given left and right refs,
 // including git objects that have been modified or deleted.
 func (s *GitScanner) ScanRefRange(left, right string, cb GitScannerFoundPointer) error {
@@ -102,7 +113,7 @@ func (s *GitScanner) ScanRefRange(left, right string, cb GitScannerFoundPointer)
 
 	opts := s.opts(ScanRefsMode)
 	opts.SkipDeletedBlobs = false
-	return scanRefsToChan(s, callback, left, right, opts)
+	return scanLeftRightToChan(s, callback, left, right, opts)
 }
 
 // ScanRefWithDeleted scans through all objects in the given ref, including
diff --git a/lfs/gitscanner_refs.go b/lfs/gitscanner_refs.go
index e66daee2..024ff117 100644
--- a/lfs/gitscanner_refs.go
+++ b/lfs/gitscanner_refs.go
@@ -33,10 +33,7 @@ func (s *lockableNameSet) Check(blobSha string) (string, bool) {
 
 func noopFoundLockable(name string) {}
 
-// scanRefsToChan takes a ref and returns a channel of WrappedPointer objects
-// for all Git LFS pointers it finds for that ref.
-// Reports unique oids once only, not multiple times if >1 file uses the same content
-func scanRefsToChan(scanner *GitScanner, pointerCb GitScannerFoundPointer, refLeft, refRight string, opt *ScanRefsOptions) error {
+func scanRefsToChan(scanner *GitScanner, pointerCb GitScannerFoundPointer, include, exclude []string, opt *ScanRefsOptions) error {
 	if opt == nil {
 		panic("no scan ref options")
 	}
@@ -134,3 +131,10 @@ func revListShas(include, exclude []string, opt *ScanRefsOptions) (*StringChanne
 
 	return NewStringChannelWrapper(revs, errs), nil
 }
+
+// scanLeftRightToChan takes a ref and returns a channel of WrappedPointer objects
+// for all Git LFS pointers it finds for that ref.
+// Reports unique oids once only, not multiple times if >1 file uses the same content
+func scanLeftRightToChan(scanner *GitScanner, pointerCb GitScannerFoundPointer, refLeft, refRight string, opt *ScanRefsOptions) error {
+	return scanRefsToChan(scanner, pointerCb, []string{refLeft}, []string{refRight}, opt)
+}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I agree with you, we should be using the GitScanner for this code - I'll go ahead and replace what I've done with the ScanForPointers function

@PastelMobileSuit

Copy link
Copy Markdown
Contributor Author

@ttaylorr Thanks for the review! In regards to "smarter .gitattributes modification", I mean some of the things discussed in #3057, such as removing redundant patterns from the .gitattributes file.

@PastelMobileSuit

PastelMobileSuit commented Jun 25, 2018 •

Copy link
Copy Markdown
Contributor Author

@ttaylorr I've pushed up an additional set of commits to address most of your feedback! Still to come:

  • Cache Pruning
  • A remote flag
  • Docs

@ttaylorr ttaylorr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, @PastelMobileSuit.

Comment thread commands/command_migrate_export.go Outdated

var buf bytes.Buffer
ptr, err := lfs.DecodePointer(b.Contents)
if errors.IsNotAPointerError(err) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit; let's push this down into the below conditional, since an error will never be nil and pass errors.IsNotAPointerError(...).

Comment thread commands/command_migrate_export.go Outdated
}

// If we have a valid remote, pre-download all objects using the Transfer Queue
if remoteURL := getAPIClient().Endpoints.RemoteEndpoint("download", cfg.Remote()).Url; remoteURL != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that supporting just the one remote is fine for now; I can see this becoming somewhat complicated because the transfer queue really isn't designed for multi-remote operations.

I think that there are a few approaches we can take in the longer term, but for now, I think that supporting one remote via --remote is totally good. If a user is (1) the bleeding-edge version of Git LFS, and (2) wants to do a multi-remote export, they can always use git lfs fetch && git lfs migrate export instead.

Longer-term, I think we could do one of the following:

  1. Teach package tq how to handle multiple remotes. This would provide a consistent interface for using the transfer queue, but will be only used at this singular call-site, and seems too complicated for too little benefit.

  2. Keep many transfer queues alive, and round-robin the objects through them. This has the benefit of simplicity and no requirements for change inside of package tq, and the external call-sites should be fairly straightforward, too. If an earlier transfer queue has the same object as a later one, it doesn't really matter which one answers first. This has the drawback of having to wait for one transfer queue to cycle before the next one in line can determine which objects they need to get.

I think that we could theoretically get more efficient than that, but I bet that that's an OK starting point for now.

return
}

if _, err := os.Stat(downloadPath); os.IsNotExist(err) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. I wonder if we could add this to the Add() function on the transfer queue itself. I can imagine that other callers would find this useful.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried this, but unfortunately it led to a bunch of inexplicable failures about objects not being found by the transfer queue when running the export tests. I verified that the conditional I added to the function is indeed correct so I'm not sure what's going wrong - it's definitely trying to download the files since they don't exist on disk. This problem bled over into other tests that leverage the transfer queue as well, such as fetch.

I can try to do some additional debugging but I'm inclined to leave this as-is for now. This is here to prevent download of an object that exists on disk but isn't present on the remote (say, if a recently-migrated pointer hasn't been pushed up to the remote repository yet). The transfer queue is leveraged by lots of code in Git LFS, and it seems like re-downloading an object file won't necessarily be wrong in all cases

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can try to do some additional debugging but I'm inclined to leave this as-is for now.

That's fine, let's mark this as #leftoverbits, and touch this in a future pull request.

Comment thread commands/command_migrate_export.go Outdated
// Only perform `git-checkout(1) -f` if the repository is
// non-bare.
// Prune the cache
for _, object := range exported {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will exported ever contain an object evicted by the migration, but used elsewhere on disk? An example demonstrating this is a repository with two files, both tracked in LFS, sharing the same contents at different paths. If only one of those paths is migrated out, the other will still wish to retain the object.

Perhaps we could invoke the git lfs prune command here, instead?

Comment thread lfs/gitscanner.go

// ScanRefs through all commits reachable by refs contained in "include" and
// not reachable by any refs included in "excluded"
func (s *GitScanner) ScanRefs(include, exclude []string, cb GitScannerFoundPointer) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ Nice.

Comment thread lfs/gitscanner_refs.go
// for all Git LFS pointers it finds for that ref.
// Reports unique oids once only, not multiple times if >1 file uses the same content
func scanLeftRightToChan(scanner *GitScanner, pointerCb GitScannerFoundPointer, refLeft, refRight string, opt *ScanRefsOptions) error {
return scanRefsToChan(scanner, pointerCb, []string{refLeft, refRight}, nil, opt)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, should this be:

return scanRefsToChan(..., []string{refLeft}, []string{refRight}, nil, opt)

instead? I think that they should be split, because the right is a boundary of everything we don't want in the range.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is correct as-is. My gut reaction was the same, but

[]string{refLeft, refRight}, nil

was being used internally by scanRefsToChan() before I changed its functionality here. Additionally, changing it to

[]string{refLeft}, []string{refRight},

causes tests that leverage this function to fail. I'm still doing some investigation as to why and will come back with an update when I better understand the rev scanner.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're totally right. I just checked the code and the way that it is in your diff is 👍, my comment is incorrect.

@PastelMobileSuit

PastelMobileSuit commented Jun 28, 2018 •

Copy link
Copy Markdown
Contributor Author

@ttaylorr I've gone ahead and addressed your feedback! I've also added the --remote flag and docs

Comment thread commands/command_migrate_export.go Outdated
// 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.PruneOffsetDays = 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a comment above but this was required to ensure certain objects were properly pruned. prune preserves any cached objects that exist on the remote from commits within a time window of PruneOffsetDays + FetchRecentRefsDays. Since migrate export rewrites commits but does not push them, exported objects can still exist on the remote, and can be retained in the cache if this time window is not set to 0.

A negative side effect of this is that all objects that exist on the remote will be pruned, regardless of whether or not the associated object was exported. It seems like this should be okay though, as if necessary Git LFS can just re-download the pruned files.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With respect to my comment below, what happens if we remove these assignments iff we perform the prune after we do the checkout?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These values only affect whether to preserve pointers referenced in the remote git history, so they're still required if the prune is moved after the checkout

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like in general though we don't actually need to set PruneOffsetDays, just FetchRecentRefsDays, so I'll remove it

Comment thread commands/command_migrate_export.go Outdated
tracked := trackedFromExportFilter(filter)
gitfilter := lfs.NewGitFilter(cfg)

var exported []string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be removed? I see that we append() into it, but I don't think the contents are used anywhere.

// 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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this create an empty .gitattributes blob if no patterns are given?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might if there was also no pre-existing .gitattributes file, but right now we require a pattern being given to migrate export through the --include argument, so it shouldn't be an issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification, that sounds good to me.

Comment thread commands/command_migrate_export.go Outdated
fetchPruneCfg.FetchRecentRefsDays = 0

// Prune our cache
prune(fetchPruneCfg, false, false, true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm; do we need to do the checkout first to make sure that we don't have reference-able LFS pointers in the current checkout?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem to make a difference in regards to what gets pruned (all tests still pass if it's moved below the checkout), but I agree that after the checkout is the correct place for it! I'll go ahead and move it

Comment thread docs/man/git-lfs-migrate.1.ronn Outdated
not provided, defaults to 'origin'.

The 'export' mode requires at minimum a pattern provided with the `--include`
argument to specify which files to export. The export command will modify the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a clarifying sentence in the middle? Perhaps something like:

In other words, --include-ed files will be removed from Git LFS, while --exclude-ed files will retain their Git LFS status.

Comment thread test/test-migrate-export.sh Outdated

git lfs migrate export --include="*.md, *.txt"

[ ! $(assert_pointer "refs/heads/master" "a.md" "$md_oid" "140") ]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these subshells needed? If they are, perhaps we could wrap this in a new function in test/testhelpers.sh called refute_pointer?

filter := rewriter.Filter()
if len(filter.Include()) <= 0 {
ExitWithError(errors.Errorf("fatal: one or more files must be specified with --include"))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this error message, but I'm wondering what it is users would expect iff they didn't provide --include or --exclude. My intuition tells me that a user invoking git lfs migrate export --everything would want everything to be removed from Git LFS, so perhaps a default of --include=* (under the semantics that are implemented below) would make sense.

What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't think we should use --everything for this, as it's already being used to specify that all local references should be migrated.

Maybe we should add a separate flag, --export-all?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, though I wasn't suggesting that --everything be used for that purpose. Rather, that git lfs migrate export (with --everything) would be a common invocation, and that because it is a common invocation, we should also assume --include="*" if no --include or --exclude flag was given

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay! I see what you mean.

I mentioned this below but I'm a bit wary of this. I think it's easy to mistype git lfs migrate export when you mean to do something more specific, thus migrating everything in your repository. If your repository has lots of large files, this could also lead to a very large number of downloads.

Saying that, this is the way that git lfs migrate import works so it may be fine. If we were to implement it I'd like to do it the way import does it, i.e., maintaining a list of file extensions that it's done an operation on and adding attributes for each one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Good point, I agree that it could be upsetting for users to download many GiB of LFS object data.

That said, I think that we should leave it as-is, since I don't want to mirror the behavior of migrate import here, which I think should have generated an error as you have implemented it here.

Comment thread commands/command_migrate_export.go Outdated
// 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.PruneOffsetDays = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With respect to my comment below, what happens if we remove these assignments iff we perform the prune after we do the checkout?

assert_pointer "refs/heads/master" "a.txt" "$txt_oid" "120"
assert_pointer "refs/heads/master" "a.md" "$md_oid" "140"

git lfs migrate export --include="*" --exclude="a.md"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this would be the place to put it, but I think that the --include="*" flag should only be the default if no --include or --exclude argument is given, since I think writing patterns against an implicit --include="*" would be confusing for users.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not sure I understand what you mean here. Do you mean that if we run git lfs migrate export, it should export all pointers? I'm a little worried about how easy that could make it for a user to accidentally do that operation, especially given the potentially large download that could occur.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding my comment above, I was suggesting that if we decide on a default value of "*" for --include, it should only be the default when neither --include= nor --exclude= is given.

git push origin master
make_bare

git lfs migrate export --everything --include="*"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be any assertions following this? I think that the corresponding migrate import test is blank because it was simply fixing a crash, but we should check that the export works correctly here.

Comment thread test/test-migrate-export.sh Outdated

grep "fatal: invalid remote zz provided" migrate.log
)
end_test No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit; I think that this is missing a newline here.

@PastelMobileSuit

Copy link
Copy Markdown
Contributor Author

@ttaylorr ttaylorr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PastelMobileSuit Awesome work once again! Thanks for responding to all of my feedback. Please merge this once you are ready to do so.

Implement a basic version of the `migrate export` subcommand,
which simply traverses git history and smudges any LFS pointers
matching given patterns.
If a valid remote is present, pre-download any LFS objects prior
to a history rewrite using the transfer queue.
When using the transfer queue to download files on export, ensure
they are downloaded to the cache directory
When scanning a tree for pointers, don't download files that
already exist in the cache
Add additional integration tests to test various functionality
of the `migrate export` command
Add support and tests for the `--verbose` and `--object-map`
flags to the `migrate export` command
When performing an export, directly convert an LFS pointer to an
object using the downloaded object file, skipping over the smudge.

Thanks to @ttaylorr for the implementation of the
NewBlobFromFile() function
Use `!` to set an attribute to Unspecified instead of `-` to set
an attribute to False
Use the GitScanner to scan through revisions and download pointers
with the transfer queue rather than the rewriter
After performing an export, prune object files for exported files
from the cache.
Flush the object cache in the export remote test before the
export is performed to ensure object downloading is tested
Add a --remote flag to the `migrate export` command allowing
specification of a remote from which to download objects
After performing an export, clean the cache with `prune()` instead
of deleting all exported objects
* Remove unused `exported` variable
* Perform prune after checkout
* Clarify use of --include and --exclude in docs
* Add assertions to bare repository test
@PastelMobileSuit
PastelMobileSuit force-pushed the pastelmobilesuit-migrate-export branch from 4f8deb1 to 17e793a Compare July 3, 2018 18:16
@PastelMobileSuit
PastelMobileSuit merged commit 22b9113 into master Jul 3, 2018
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request Jul 15, 2024
In commit 96775c6 of PR git-lfs#2929 the
"migrate import (--object-map)" test was added to the test suite for
the "git lfs migrate import" command to validate the --object-map option
for that command, which was introduced in the same PR.  This test
makes use of the "mktemp" shell command to generate a unique temporary
directory, into which the test then writes several files including
the Git LFS migration's object map file.

Likewise, in commit 7632a20 of PR git-lfs#3084,
a similar "migrate export (--object-map)" test was added to the test
suite for the "git lfs migrate export" command, which was introduced
in that PR, and this test also uses the "mktemp" shell command to
create a unique temporary directory.

However, neither test removes these temporary directories after
completion, so they remain in place indefinitely in whatever system
location is used by the "mktemp" command.  This is in addition to
the the temporary directory created by the "mktemp" command which
is used throughout all our test suites, whose path we store in the
GIT_LFS_TEST_DIR environment variable so it can be reused by all
test suites after the first one to run.

In a previous commit in this PR we introduced a new lfstest-genrandom
utility command for our test suite, and we can now make use of it to
replace both instances where our migration tests make an extra call
to the "mktemp" shell command (beyond the one used to populate the
GIT_LFS_TEST_DIR environment variable).

In both tests, we generate a unique file path under the path in the
GIT_LFS_TEST_DIR environment variable, using the --base64url flag with
our lfstest-genrandom command to ensure we have a safe directory name,
and then we create the directory.
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request Aug 24, 2024
In commit 96775c6 of PR git-lfs#2929 the
"migrate import (--object-map)" test was added to the test suite for
the "git lfs migrate import" command to validate the --object-map option
for that command, which was introduced in the same PR.  This test
makes use of the "mktemp" shell command to generate a unique temporary
directory, into which the test then writes several files including
the Git LFS migration's object map file.

Likewise, in commit 7632a20 of PR git-lfs#3084,
a similar "migrate export (--object-map)" test was added to the test
suite for the "git lfs migrate export" command, which was introduced
in that PR, and this test also uses the "mktemp" shell command to
create a unique temporary directory.

However, neither test removes these temporary directories after
completion, so they remain in place indefinitely in whatever system
location is used by the "mktemp" command.  This is in addition to
the the temporary directory created by the "mktemp" command which
is used throughout all our test suites, whose path we store in the
GIT_LFS_TEST_DIR environment variable so it can be reused by all
test suites after the first one to run.

In a previous commit in this PR we introduced a new lfstest-genrandom
utility program for our test suite, and we can now make use of it to
replace both instances where our migration tests make an extra call
to the "mktemp" shell command (beyond the one used to populate the
GIT_LFS_TEST_DIR environment variable).

In both tests, we generate a unique file path under the path in the
GIT_LFS_TEST_DIR environment variable, using the --base64url flag with
our lfstest-genrandom program to ensure we have a safe directory name,
and then we create the directory.
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request Aug 30, 2024
In commit 96775c6 of PR git-lfs#2929 the
"migrate import (--object-map)" test was added to the test suite for
the "git lfs migrate import" command to validate the --object-map option
for that command, which was introduced in the same PR.  This test
makes use of the "mktemp" shell command to generate a unique temporary
directory, into which the test then writes several files including
the Git LFS migration's object map file.

Likewise, in commit 7632a20 of PR git-lfs#3084,
a similar "migrate export (--object-map)" test was added to the test
suite for the "git lfs migrate export" command, which was introduced
in that PR, and this test also uses the "mktemp" shell command to
create a unique temporary directory.

However, neither test removes these temporary directories after
completion, so they remain in place indefinitely in whatever system
location is used by the "mktemp" command.  This is in addition to
the the temporary directory created by the "mktemp" command which
is used throughout all our test suites, whose path we store in the
GIT_LFS_TEST_DIR environment variable so it can be reused by all
test suites after the first one to run.

In a previous commit in this PR we introduced a new lfstest-genrandom
utility program for our test suite, and we can now make use of it to
replace both instances where our migration tests make an extra call
to the "mktemp" shell command (beyond the one used to populate the
GIT_LFS_TEST_DIR environment variable).

In both tests, we generate a unique file path under the path in the
GIT_LFS_TEST_DIR environment variable, using the --base64url flag with
our lfstest-genrandom program to ensure we have a safe directory name,
and then we create the directory.
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request Sep 18, 2024
In commit 96775c6 of PR git-lfs#2929 the
"migrate import (--object-map)" test was added to the test suite for
the "git lfs migrate import" command to validate the --object-map option
for that command, which was introduced in the same PR.  This test
makes use of the "mktemp" shell command to generate a unique temporary
directory, into which the test then writes several files including
the Git LFS migration's object map file.

Likewise, in commit 7632a20 of PR git-lfs#3084,
a similar "migrate export (--object-map)" test was added to the test
suite for the "git lfs migrate export" command, which was introduced
in that PR, and this test also uses the "mktemp" shell command to
create a unique temporary directory.

However, neither test removes these temporary directories after
completion, so they remain in place indefinitely in whatever system
location is used by the "mktemp" command.  This is in addition to
the the temporary directory created by the "mktemp" command which
is used throughout all our test suites, whose path we store in the
GIT_LFS_TEST_DIR environment variable so it can be reused by all
test suites after the first one to run.

In a previous commit in this PR we introduced a new lfstest-genrandom
utility program for our test suite, and we can now make use of it to
replace both instances where our migration tests make an extra call
to the "mktemp" shell command (beyond the one used to populate the
GIT_LFS_TEST_DIR environment variable).

In both tests, we generate a unique file path under the path in the
GIT_LFS_TEST_DIR environment variable, using the --base64url flag with
our lfstest-genrandom program to ensure we have a safe directory name,
and then we create the directory.
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request Sep 18, 2024
In commit 96775c6 of PR git-lfs#2929 the
"migrate import (--object-map)" test was added to the test suite for
the "git lfs migrate import" command to validate the --object-map option
for that command, which was introduced in the same PR.  This test
makes use of the mktemp(1) shell command to generate a unique temporary
directory, into which the test then writes several files including
the Git LFS migration's object map file.

Likewise, in commit 7632a20 of PR git-lfs#3084,
a similar "migrate export (--object-map)" test was added to the test
suite for the "git lfs migrate export" command, which was introduced
in that PR, and this test also uses the mktemp(1) shell command to
create a unique temporary directory.

However, neither test removes these temporary directories after
completion, so they remain in place indefinitely in whatever system
location is used by the mktemp(1) command.  This is in addition to the
temporary directory created by the mktemp(1) command which is used
throughout all our test suites, whose path we store in the
GIT_LFS_TEST_DIR variable so it can be reused by all test suites after
the first one to run.  (At least, that is the intent of the variable,
but in practice, because we do not export it, and because each test
script runs in its own shell session, each script creates another
temporary directory.  We will address this issue in a subsequent PR.)

In a previous commit in this PR we introduced a new lfstest-genrandom
utility program for our test suite, so we can now make use of it to
replace both instances where our migration tests make an extra call
to the mktemp(1) shell command (beyond the one used to populate the
GIT_LFS_TEST_DIR variable).

In both tests, we now create a unique directory whose path begins
with the path in the GIT_LFS_TEST_DIR variable, followed by a directory
name partially generated by our lfstest-genrandom program.  We pass
the --base64url flag to the program ensure it returns only characters
safe to use in a file or directory name on any OS platform.
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request Sep 19, 2024
In commit 96775c6 of PR git-lfs#2929 the
"migrate import (--object-map)" test was added to the test suite for
the "git lfs migrate import" command to validate the --object-map option
for that command, which was introduced in the same PR.  This test
makes use of the mktemp(1) shell command to generate a unique temporary
directory, into which the test then writes several files including
the Git LFS migration's object map file.

Likewise, in commit 7632a20 of PR git-lfs#3084,
a similar "migrate export (--object-map)" test was added to the test
suite for the "git lfs migrate export" command, which was introduced
in that PR, and this test also uses the mktemp(1) shell command to
create a unique temporary directory.

However, neither test removes these temporary directories after
completion, so they remain in place indefinitely in whatever system
location is used by the mktemp(1) command.  This is in addition to the
temporary directory created by the mktemp(1) command which is used
throughout all our test suites, whose path we store in the
GIT_LFS_TEST_DIR variable so it can be reused by all test suites after
the first one to run.  (At least, that is the intent of the variable,
but in practice, because we do not export it, and because each test
script runs in its own shell session, each script creates another
temporary directory.  We will address this issue in a subsequent PR.)

In a previous commit in this PR we introduced a new lfstest-genrandom
utility program for our test suite, so we can now make use of it to
replace both instances where our migration tests make an extra call
to the mktemp(1) shell command (beyond the one used to populate the
GIT_LFS_TEST_DIR variable).

In both tests, we now create a unique directory whose path begins
with the path in the GIT_LFS_TEST_DIR variable, followed by a directory
name partially generated by our lfstest-genrandom program.  We pass
the --base64url flag to the program ensure it returns only characters
safe to use in a file or directory name on any OS platform.
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request Sep 19, 2024
In commit 96775c6 of PR git-lfs#2929 the
"migrate import (--object-map)" test was added to the test suite for
the "git lfs migrate import" command to validate the --object-map option
for that command, which was introduced in the same PR.  This test
makes use of the mktemp(1) shell command to generate a unique temporary
directory, into which the test then writes several files including
the Git LFS migration's object map file.

Likewise, in commit 7632a20 of PR git-lfs#3084,
a similar "migrate export (--object-map)" test was added to the test
suite for the "git lfs migrate export" command, which was introduced
in that PR, and this test also uses the mktemp(1) shell command to
create a unique temporary directory.

However, neither test removes these temporary directories after
completion, so they remain in place indefinitely in whatever system
location is used by the mktemp(1) command.  This is in addition to the
temporary directory created by the mktemp(1) command which is used
throughout all our test suites, whose path we store in the
GIT_LFS_TEST_DIR variable so it can be reused by all test suites after
the first one to run.  (At least, that is the intent of the variable,
but in practice, because we do not export it, and because each test
script runs in its own shell session, each script creates another
temporary directory.  We will address this issue in a subsequent PR.)

In a previous commit in this PR we introduced a new lfstest-genrandom
utility program for our test suite, so we can now make use of it to
replace both instances where our migration tests make an extra call
to the mktemp(1) shell command (beyond the one used to populate the
GIT_LFS_TEST_DIR variable).

In both tests, we now create a unique directory whose path begins
with the path in the GIT_LFS_TEST_DIR variable, followed by a directory
name partially generated by our lfstest-genrandom program.  We pass
the --base64url flag to the program ensure it returns only characters
safe to use in a file or directory name on any OS platform.
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request May 27, 2026
We first introduced the "git lfs migrate export" command in PR git-lfs#3084,
and in commit 433d185 we implemented
a feature whereby if a valid Git remote is found, the command
pre-emptively downloads Git LFS objects from the remote before
performing the export migration.

To download the objects, the command establishes a transfer queue
by calling the getTransferManifestOperationRemote() function, which
expects an "operation" parameter whose value should be either "upload"
or "download".  However, as noted by larsxschneider during the review
of this PR, we accidentally pass the value "Download" rather than
"download".

For consistency with our other commands we now revise the "operation"
parameter passed to the getTransferManifestOperationRemote() function
by the "git lfs migrate export" command to the all-lowercase value
"download".

We happened to avoid any consequences from this oversight for a few
reasons.  The command only creates a single transfer queue, so while
we stored the queue in the global "tqManifest" map with a key that
contains the "Download" string, we never tried to look it up again
with a key that contained a "download" string instead.

Meanwhile, in the getEndpoint() function of the "endpointGitFinder"
structure of our "lfsapi" package, we only compare the operation name
against the string "upload" and treat any other value as implying a
download operation.

Finally, although we pass the manifest returned by the
getTransferManifestOperationRemote() function to the newDownloadQueue()
function, that function then explicitly specifies the Download constant
from our "tq" package to that package's NewTransferQueue() function.
The Download constant is an enumerated value of the Direction type,
as opposed to an arbitrary string.

In a future PR we may refactor a number of our packages so they use
a common, enumerated set of constants to identify the two possible
directions our transfer queues and manifests support.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants