Repository navigation
Implement migrate export subcommand - #3084
Conversation
ebd9ac3 to
d84383b
Compare
ttaylorr
left a comment
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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.
| cmd.PersistentFlags().BoolVar(&migrateSkipFetch, "skip-fetch", false, "Assume up-to-date remote references.") | ||
|
|
||
| cmd.AddCommand(importCmd, info) | ||
| cmd.AddCommand(importCmd, info, exportCmd) |
There was a problem hiding this comment.
Nit; let's put exportCmd before the other two arguments, to keep this list alphabetized.
| } | ||
|
|
||
| // 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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
| return b, nil | ||
| } | ||
|
|
||
| var buf bytes.Buffer |
There was a problem hiding this comment.
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{| } | ||
|
|
||
| // If we have a valid remote, pre-download all objects using the Transfer Queue | ||
| if remoteURL := getAPIClient().Endpoints.RemoteEndpoint("download", cfg.Remote()).Url; remoteURL != "" { |
There was a problem hiding this comment.
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()There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
-
Teach package
tqhow 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. -
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.
| ExitWithError(err) | ||
| } | ||
|
|
||
| // Only perform `git-checkout(1) -f` if the repository is |
There was a problem hiding this comment.
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).
| tracked := tools.NewOrderedSet() | ||
|
|
||
| for _, include := range filter.Include() { | ||
| tracked.Add(fmt.Sprintf("%s text -filter -merge -diff", escapeAttrPattern(include))) |
There was a problem hiding this comment.
Nice. I think that we are supposed to spell the -filter as !filter, though.
There was a problem hiding this comment.
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 -
There was a problem hiding this comment.
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
themergeattribute is set. However, themerge.defaultconfiguration
variable can name different merge driver to be used with paths for which the
mergeattribute is unspecified.
I think that given the choice between the two, we'd prefer unspecified, which defaults back to the configuration.
There was a problem hiding this comment.
Ah, that makes sense! In that case I'll go ahead and switch this over
| return rewriter | ||
| } | ||
|
|
||
| // ScanForPointers scans through the range of commits given by |
There was a problem hiding this comment.
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)
+}There was a problem hiding this comment.
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
|
@ttaylorr I've pushed up an additional set of commits to address most of your feedback! Still to come:
|
ttaylorr
left a comment
There was a problem hiding this comment.
Looking good, @PastelMobileSuit.
|
|
||
| var buf bytes.Buffer | ||
| ptr, err := lfs.DecodePointer(b.Contents) | ||
| if errors.IsNotAPointerError(err) { |
There was a problem hiding this comment.
Nit; let's push this down into the below conditional, since an error will never be nil and pass errors.IsNotAPointerError(...).
| } | ||
|
|
||
| // If we have a valid remote, pre-download all objects using the Transfer Queue | ||
| if remoteURL := getAPIClient().Endpoints.RemoteEndpoint("download", cfg.Remote()).Url; remoteURL != "" { |
There was a problem hiding this comment.
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:
-
Teach package
tqhow 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. -
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| // Only perform `git-checkout(1) -f` if the repository is | ||
| // non-bare. | ||
| // Prune the cache | ||
| for _, object := range exported { |
There was a problem hiding this comment.
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?
|
|
||
| // 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 { |
| // 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Ah, you're totally right. I just checked the code and the way that it is in your diff is 👍, my comment is incorrect.
|
@ttaylorr I've gone ahead and addressed your feedback! I've also added the |
| // 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
With respect to my comment below, what happens if we remove these assignments iff we perform the prune after we do the checkout?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
It looks like in general though we don't actually need to set PruneOffsetDays, just FetchRecentRefsDays, so I'll remove it
| tracked := trackedFromExportFilter(filter) | ||
| gitfilter := lfs.NewGitFilter(cfg) | ||
|
|
||
| var exported []string |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
Will this create an empty .gitattributes blob if no patterns are given?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Thanks for the clarification, that sounds good to me.
| fetchPruneCfg.FetchRecentRefsDays = 0 | ||
|
|
||
| // Prune our cache | ||
| prune(fetchPruneCfg, false, false, true) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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.
|
|
||
| git lfs migrate export --include="*.md, *.txt" | ||
|
|
||
| [ ! $(assert_pointer "refs/heads/master" "a.md" "$md_oid" "140") ] |
There was a problem hiding this comment.
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")) | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| // 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 |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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="*" |
There was a problem hiding this comment.
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.
|
|
||
| grep "fatal: invalid remote zz provided" migrate.log | ||
| ) | ||
| end_test No newline at end of file |
There was a problem hiding this comment.
Nit; I think that this is missing a newline here.
ttaylorr
left a comment
There was a problem hiding this comment.
@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
4f8deb1 to
17e793a
Compare
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.
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.
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.
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.
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.
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.
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.
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.

This PR implements a
migrate exportsubcommand as described in #3057. The command requires a list of patterns specified with the--includeargument 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:
.gitattributesfiles