Skip to content

Commit 1412d6e

Browse files
committed
Don't fail if we lack objects the server has
A Git LFS client may not have the entire history of the objects for the repository. However, in some situations, we traverse the entire history of a branch when pushing it, meaning that we need to process every LFS object in the history of that branch. If the objects for the entire history are not present, we currently fail to push. Instead, let's mark objects we don't have on disk as missing and only fail when we would need to upload those objects. We'll know the server has the objects if the batch response provides no actions to take for them when we request an upload. Pass the missing flag down through the code, and always set it to false for non-uploads. If for some reason we fail to properly flag a missing object, we will still fail later on when we cannot open the file, just in a messier and more poorly controlled way. The technique used here will attempt to abort the batch as soon as we notice a problem, which means that in the common case (less than 100 objects) we won't have transferred any objects, so the user can notice the failure as soon as possible. Update the tests to look for a string which will occur in the error message, since we no longer produce the system error message for ENOENT.
1 parent e244628 commit 1412d6e

File tree

12 files changed

+123
-38
lines changed

12 files changed

+123
-38
lines changed

commands/command_migrate_export.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func migrateExportCommand(cmd *cobra.Command, args []string) {
136136
}
137137

138138
if _, err := os.Stat(downloadPath); os.IsNotExist(err) {
139-
q.Add(p.Name, downloadPath, p.Oid, p.Size)
139+
q.Add(p.Name, downloadPath, p.Oid, p.Size, false)
140140
}
141141
})
142142
gs.ScanRefs(opts.Include, opts.Exclude, nil)

commands/command_smudge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func delayedSmudge(gf *lfs.GitFilter, s *git.FilterProcessScanner, to io.Writer,
5959

6060
if !skip && filter.Allows(filename) {
6161
if _, statErr := os.Stat(path); statErr != nil {
62-
q.Add(filename, path, ptr.Oid, ptr.Size)
62+
q.Add(filename, path, ptr.Oid, ptr.Size, false)
6363
return 0, true, ptr, nil
6464
}
6565

commands/commands.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ func buildFilepathFilter(config *config.Configuration, includeArg, excludeArg *s
131131
return filepathfilter.New(inc, exc)
132132
}
133133

134-
func downloadTransfer(p *lfs.WrappedPointer) (name, path, oid string, size int64) {
134+
func downloadTransfer(p *lfs.WrappedPointer) (name, path, oid string, size int64, missing bool) {
135135
path, _ = cfg.Filesystem().ObjectPath(p.Oid)
136-
return p.Name, path, p.Oid, p.Size
136+
return p.Name, path, p.Oid, p.Size, false
137137
}
138138

139139
// Get user-readable manual install steps for hooks

commands/uploader.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ func (c *uploadContext) UploadPointers(q *tq.TransferQueue, unfiltered ...*lfs.W
246246
ExitWithError(err)
247247
}
248248

249-
q.Add(t.Name, t.Path, t.Oid, t.Size)
249+
q.Add(t.Name, t.Path, t.Oid, t.Size, t.Missing)
250250
c.SetUploaded(p.Oid)
251251
}
252252
}
@@ -338,6 +338,8 @@ var (
338338
)
339339

340340
func (c *uploadContext) uploadTransfer(p *lfs.WrappedPointer) (*tq.Transfer, error) {
341+
var missing bool
342+
341343
filename := p.Name
342344
oid := p.Oid
343345

@@ -347,40 +349,38 @@ func (c *uploadContext) uploadTransfer(p *lfs.WrappedPointer) (*tq.Transfer, err
347349
}
348350

349351
if len(filename) > 0 {
350-
if err = c.ensureFile(filename, localMediaPath, oid); err != nil && !errors.IsCleanPointerError(err) {
352+
if missing, err = c.ensureFile(filename, localMediaPath, oid); err != nil && !errors.IsCleanPointerError(err) {
351353
return nil, err
352354
}
353355
}
354356

355357
return &tq.Transfer{
356-
Name: filename,
357-
Path: localMediaPath,
358-
Oid: oid,
359-
Size: p.Size,
358+
Name: filename,
359+
Path: localMediaPath,
360+
Oid: oid,
361+
Size: p.Size,
362+
Missing: missing,
360363
}, nil
361364
}
362365

363366
// ensureFile makes sure that the cleanPath exists before pushing it. If it
364367
// does not exist, it attempts to clean it by reading the file at smudgePath.
365-
func (c *uploadContext) ensureFile(smudgePath, cleanPath, oid string) error {
368+
func (c *uploadContext) ensureFile(smudgePath, cleanPath, oid string) (bool, error) {
366369
if _, err := os.Stat(cleanPath); err == nil {
367-
return nil
370+
return false, nil
368371
}
369372

370373
localPath := filepath.Join(cfg.LocalWorkingDir(), smudgePath)
371374
file, err := os.Open(localPath)
372375
if err != nil {
373-
if c.allowMissing {
374-
return nil
375-
}
376-
return errors.Wrapf(err, "Unable to find source for object %v (try running git lfs fetch --all)", oid)
376+
return !c.allowMissing, nil
377377
}
378378

379379
defer file.Close()
380380

381381
stat, err := file.Stat()
382382
if err != nil {
383-
return err
383+
return false, err
384384
}
385385

386386
cleaned, err := c.gitfilter.Clean(file, file.Name(), stat.Size(), nil)
@@ -389,9 +389,9 @@ func (c *uploadContext) ensureFile(smudgePath, cleanPath, oid string) error {
389389
}
390390

391391
if err != nil {
392-
return err
392+
return false, err
393393
}
394-
return nil
394+
return false, nil
395395
}
396396

397397
// supportsLockingAPI returns whether or not a given url is known to support

lfs/gitfilter_smudge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (f *GitFilter) downloadFile(writer io.Writer, ptr *Pointer, workingfile, me
9696
tq.WithProgressCallback(cb),
9797
tq.RemoteRef(f.RemoteRef()),
9898
)
99-
q.Add(filepath.Base(workingfile), mediafile, ptr.Oid, ptr.Size)
99+
q.Add(filepath.Base(workingfile), mediafile, ptr.Oid, ptr.Size, false)
100100
q.Wait()
101101

102102
if errs := q.Errors(); len(errs) > 0 {

t/git-lfs-test-server-api/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ func buildTestData(repo *t.Repo, manifest *tq.Manifest) (oidsExist, oidsMissing
206206
if err != nil {
207207
return nil, nil, err
208208
}
209-
uploadQueue.Add(t.Name, t.Path, t.Oid, t.Size)
209+
uploadQueue.Add(t.Name, t.Path, t.Oid, t.Size, false)
210210
}
211211
uploadQueue.Wait()
212212

t/t-pre-push.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,7 @@ begin_test "pre-push reject missing pointers (lfs.allowincompletepush default)"
412412
exit 1
413413
fi
414414

415-
grep "no such file or directory" push.log || # unix
416-
grep "cannot find the file" push.log # windows
415+
grep 'Unable to find source' push.log
417416

418417
refute_server_object "$reponame" "$present_oid"
419418
refute_server_object "$reponame" "$missing_oid"

t/t-push-failures-local.sh

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,7 @@ begin_test "push reject missing objects (lfs.allowincompletepush false)"
9090
exit 1
9191
fi
9292

93-
grep "no such file or directory" push.log || # unix
94-
grep "cannot find the file" push.log # windows
95-
grep "failed to push some refs" push.log
93+
grep 'Unable to find source' push.log
9694

9795
refute_server_object "$reponame" "$present_oid"
9896
refute_server_object "$reponame" "$missing_oid"

t/t-push.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,3 +694,49 @@ begin_test "push with invalid pushInsteadof"
694694
git lfs push origin master
695695
)
696696
end_test
697+
698+
begin_test 'push with data the server already has'
699+
(
700+
set -e
701+
702+
reponame="push-server-data"
703+
setup_remote_repo "$reponame"
704+
clone_repo "$reponame" "$reponame"
705+
706+
git lfs track "*.dat"
707+
git add .gitattributes
708+
git commit -m "initial commit"
709+
710+
contents="abc123"
711+
contents_oid="$(calc_oid "$contents")"
712+
printf "%s" "$contents" > a.dat
713+
git add a.dat
714+
git commit -m "add a.dat"
715+
716+
git push origin master
717+
718+
assert_server_object "$reponame" "$contents_oid"
719+
720+
git checkout -b side
721+
722+
# Use a different file name for the second file; otherwise, this test
723+
# unexpectedly passes with the old code, since we fail to notice that the
724+
# object we run through the clean filter is not the object we wanted.
725+
contents2="def456"
726+
contents2_oid="$(calc_oid "$contents2")"
727+
printf "%s" "$contents2" > b.dat
728+
git add b.dat
729+
git commit -m "add b.dat"
730+
731+
# We remove the original object. The server already has this.
732+
delete_local_object "$contents_oid"
733+
734+
# We use the URL so that we cannot take advantage of the existing "origin/*"
735+
# refs that we know the server must have. We will traverse the entire history
736+
# for this push, and we should not fail because the server already has the
737+
# object we deleted above.
738+
git push "$(git config remote.origin.url)" side
739+
740+
assert_server_object "$reponame" "$contents2_oid"
741+
)
742+
end_test

tq/api.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ func (c *tqClient) Batch(remote string, bReq *batchRequest) (*BatchResponse, err
5555
bReq.TransferAdapterNames = nil
5656
}
5757

58+
missing := make(map[string]bool)
59+
for _, obj := range bReq.Objects {
60+
missing[obj.Oid] = obj.Missing
61+
}
62+
5863
bRes.endpoint = c.Endpoints.Endpoint(bReq.Operation, remote)
5964
requestedAt := time.Now()
6065

@@ -81,6 +86,7 @@ func (c *tqClient) Batch(remote string, bReq *batchRequest) (*BatchResponse, err
8186
}
8287

8388
for _, obj := range bRes.Objects {
89+
obj.Missing = missing[obj.Oid]
8490
for _, a := range obj.Actions {
8591
a.createdAt = requestedAt
8692
}

0 commit comments

Comments
 (0)