Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

Commit d532648

Browse files
committed
dotgit: rewrite packed-refs while holding lock
Windows file system doesn't let us rename over a file while holding that file's lock, so use rewrite as a last resort. It could result in a partially-written file, if there's a failure at the wrong time.
1 parent 5a6cc4e commit d532648

File tree

3 files changed

+103
-60
lines changed

3 files changed

+103
-60
lines changed

storage/filesystem/internal/dotgit/dotgit.go

Lines changed: 53 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -387,17 +387,7 @@ func (d *DotGit) Ref(name plumbing.ReferenceName) (*plumbing.Reference, error) {
387387
return d.packedRef(name)
388388
}
389389

390-
func (d *DotGit) findPackedRefs() ([]*plumbing.Reference, error) {
391-
f, err := d.fs.Open(packedRefsPath)
392-
if err != nil {
393-
if os.IsNotExist(err) {
394-
return nil, nil
395-
}
396-
return nil, err
397-
}
398-
399-
defer ioutil.CheckClose(f, &err)
400-
390+
func (d *DotGit) findPackedRefsInFile(f billy.File) ([]*plumbing.Reference, error) {
401391
s := bufio.NewScanner(f)
402392
var refs []*plumbing.Reference
403393
for s.Scan() {
@@ -414,6 +404,19 @@ func (d *DotGit) findPackedRefs() ([]*plumbing.Reference, error) {
414404
return refs, s.Err()
415405
}
416406

407+
func (d *DotGit) findPackedRefs() ([]*plumbing.Reference, error) {
408+
f, err := d.fs.Open(packedRefsPath)
409+
if err != nil {
410+
if os.IsNotExist(err) {
411+
return nil, nil
412+
}
413+
return nil, err
414+
}
415+
416+
defer ioutil.CheckClose(f, &err)
417+
return d.findPackedRefsInFile(f)
418+
}
419+
417420
func (d *DotGit) packedRef(name plumbing.ReferenceName) (*plumbing.Reference, error) {
418421
refs, err := d.findPackedRefs()
419422
if err != nil {
@@ -460,20 +463,41 @@ func (d *DotGit) addRefsFromPackedRefs(refs *[]*plumbing.Reference, seen map[plu
460463
return nil
461464
}
462465

463-
func (d *DotGit) openAndLockPackedRefs() (pr billy.File, err error) {
466+
func (d *DotGit) addRefsFromPackedRefsFile(refs *[]*plumbing.Reference, f billy.File, seen map[plumbing.ReferenceName]bool) (err error) {
467+
packedRefs, err := d.findPackedRefsInFile(f)
468+
if err != nil {
469+
return err
470+
}
471+
472+
for _, ref := range packedRefs {
473+
if !seen[ref.Name()] {
474+
*refs = append(*refs, ref)
475+
seen[ref.Name()] = true
476+
}
477+
}
478+
return nil
479+
}
480+
481+
func (d *DotGit) openAndLockPackedRefs(doCreate bool) (
482+
pr billy.File, err error) {
464483
var f billy.File
465484
defer func() {
466485
if err != nil && f != nil {
467486
ioutil.CheckClose(f, &err)
468487
}
469488
}()
470489

490+
openFlags := os.O_RDWR
491+
if doCreate {
492+
openFlags |= os.O_CREATE
493+
}
494+
471495
// Keep trying to open and lock the file until we're sure the file
472496
// didn't change between the open and the lock.
473497
for {
474-
f, err = d.fs.Open(packedRefsPath)
498+
f, err = d.fs.OpenFile(packedRefsPath, openFlags, 0600)
475499
if err != nil {
476-
if os.IsNotExist(err) {
500+
if os.IsNotExist(err) && !doCreate {
477501
return nil, nil
478502
}
479503

@@ -507,31 +531,25 @@ func (d *DotGit) openAndLockPackedRefs() (pr billy.File, err error) {
507531
}
508532

509533
func (d *DotGit) rewritePackedRefsWithoutRef(name plumbing.ReferenceName) (err error) {
510-
pr, err := d.openAndLockPackedRefs()
534+
pr, err := d.openAndLockPackedRefs(false)
511535
if err != nil {
512536
return err
513537
}
514538
if pr == nil {
515539
return nil
516540
}
517-
doClosePR := true
518-
defer func() {
519-
if doClosePR {
520-
ioutil.CheckClose(pr, &err)
521-
}
522-
}()
541+
defer ioutil.CheckClose(pr, &err)
523542

524543
// Creating the temp file in the same directory as the target file
525544
// improves our chances for rename operation to be atomic.
526545
tmp, err := d.fs.TempFile("", tmpPackedRefsPrefix)
527546
if err != nil {
528547
return err
529548
}
530-
doCloseTmp := true
549+
tmpName := tmp.Name()
531550
defer func() {
532-
if doCloseTmp {
533-
ioutil.CheckClose(tmp, &err)
534-
}
551+
ioutil.CheckClose(tmp, &err)
552+
_ = d.fs.Remove(tmpName) // don't check err, we might have renamed it
535553
}()
536554

537555
s := bufio.NewScanner(pr)
@@ -558,26 +576,10 @@ func (d *DotGit) rewritePackedRefsWithoutRef(name plumbing.ReferenceName) (err e
558576
}
559577

560578
if !found {
561-
doCloseTmp = false
562-
ioutil.CheckClose(tmp, &err)
563-
if err != nil {
564-
return err
565-
}
566-
// Delete the temp file if nothing needed to be removed.
567-
return d.fs.Remove(tmp.Name())
568-
}
569-
570-
doClosePR = false
571-
if err := pr.Close(); err != nil {
572-
return err
573-
}
574-
575-
doCloseTmp = false
576-
if err := tmp.Close(); err != nil {
577-
return err
579+
return nil
578580
}
579581

580-
return d.fs.Rename(tmp.Name(), packedRefsPath)
582+
return d.rewritePackedRefsWhileLocked(tmp, pr)
581583
}
582584

583585
// process lines from a packed-refs file
@@ -691,20 +693,15 @@ func (d *DotGit) CountLooseRefs() (int, error) {
691693
// packed, plus all tags.
692694
func (d *DotGit) PackRefs() (err error) {
693695
// Lock packed-refs, and create it if it doesn't exist yet.
694-
f, err := d.fs.OpenFile(packedRefsPath, os.O_RDWR|os.O_CREATE, 0600)
696+
f, err := d.openAndLockPackedRefs(true)
695697
if err != nil {
696698
return err
697699
}
698700
defer ioutil.CheckClose(f, &err)
699701

700-
err = f.Lock()
701-
if err != nil {
702-
return err
703-
}
704-
705702
// Gather all refs using addRefsFromRefDir and addRefsFromPackedRefs.
706703
var refs []*plumbing.Reference
707-
var seen = make(map[plumbing.ReferenceName]bool)
704+
seen := make(map[plumbing.ReferenceName]bool)
708705
if err := d.addRefsFromRefDir(&refs, seen); err != nil {
709706
return err
710707
}
@@ -713,7 +710,7 @@ func (d *DotGit) PackRefs() (err error) {
713710
return nil
714711
}
715712
numLooseRefs := len(refs)
716-
if err := d.addRefsFromPackedRefs(&refs, seen); err != nil {
713+
if err := d.addRefsFromPackedRefsFile(&refs, f, seen); err != nil {
717714
return err
718715
}
719716

@@ -722,12 +719,12 @@ func (d *DotGit) PackRefs() (err error) {
722719
if err != nil {
723720
return err
724721
}
725-
doCloseTmp := true
722+
tmpName := tmp.Name()
726723
defer func() {
727-
if doCloseTmp {
728-
ioutil.CheckClose(tmp, &err)
729-
}
724+
ioutil.CheckClose(tmp, &err)
725+
_ = d.fs.Remove(tmpName) // don't check err, we might have renamed it
730726
}()
727+
731728
w := bufio.NewWriter(tmp)
732729
for _, ref := range refs {
733730
_, err := w.WriteString(ref.String() + "\n")
@@ -741,11 +738,7 @@ func (d *DotGit) PackRefs() (err error) {
741738
}
742739

743740
// Rename the temp packed-refs file.
744-
doCloseTmp = false
745-
if err := tmp.Close(); err != nil {
746-
return err
747-
}
748-
err = d.fs.Rename(tmp.Name(), packedRefsPath)
741+
err = d.rewritePackedRefsWhileLocked(tmp, f)
749742
if err != nil {
750743
return err
751744
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// +build !windows
2+
3+
package dotgit
4+
5+
import "gopkg.in/src-d/go-billy.v4"
6+
7+
func (d *DotGit) rewritePackedRefsWhileLocked(
8+
tmp billy.File, pr billy.File) error {
9+
// On non-Windows platforms, we can have atomic rename.
10+
return d.fs.Rename(tmp.Name(), pr.Name())
11+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// +build windows
2+
3+
package dotgit
4+
5+
import (
6+
"io"
7+
8+
"gopkg.in/src-d/go-billy.v4"
9+
)
10+
11+
func (d *DotGit) rewritePackedRefsWhileLocked(
12+
tmp billy.File, pr billy.File) error {
13+
// If we aren't using the bare Windows filesystem as the storage
14+
// layer, we might be able to get away with a rename over a locked
15+
// file.
16+
err := d.fs.Rename(tmp.Name(), pr.Name())
17+
if err == nil {
18+
return nil
19+
}
20+
21+
// Otherwise, Windows doesn't let us rename over a locked file, so
22+
// we have to do a straight copy. Unfortunately this could result
23+
// in a partially-written file if the process fails before the
24+
// copy completes.
25+
_, err = pr.Seek(0, io.SeekStart)
26+
if err != nil {
27+
return err
28+
}
29+
err = pr.Truncate(0)
30+
if err != nil {
31+
return err
32+
}
33+
_, err = tmp.Seek(0, io.SeekStart)
34+
if err != nil {
35+
return err
36+
}
37+
_, err = io.Copy(pr, tmp)
38+
return err
39+
}

0 commit comments

Comments
 (0)