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

Commit 2e4fa73

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 2e4fa73

File tree

3 files changed

+73
-48
lines changed

3 files changed

+73
-48
lines changed

storage/filesystem/internal/dotgit/dotgit.go

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -460,20 +460,26 @@ func (d *DotGit) addRefsFromPackedRefs(refs *[]*plumbing.Reference, seen map[plu
460460
return nil
461461
}
462462

463-
func (d *DotGit) openAndLockPackedRefs() (pr billy.File, err error) {
463+
func (d *DotGit) openAndLockPackedRefs(doCreate bool) (
464+
pr billy.File, err error) {
464465
var f billy.File
465466
defer func() {
466467
if err != nil && f != nil {
467468
ioutil.CheckClose(f, &err)
468469
}
469470
}()
470471

472+
openFlags := os.O_RDWR
473+
if doCreate {
474+
openFlags |= os.O_CREATE
475+
}
476+
471477
// Keep trying to open and lock the file until we're sure the file
472478
// didn't change between the open and the lock.
473479
for {
474-
f, err = d.fs.Open(packedRefsPath)
480+
f, err = d.fs.OpenFile(packedRefsPath, openFlags, 0600)
475481
if err != nil {
476-
if os.IsNotExist(err) {
482+
if os.IsNotExist(err) && !doCreate {
477483
return nil, nil
478484
}
479485

@@ -507,31 +513,25 @@ func (d *DotGit) openAndLockPackedRefs() (pr billy.File, err error) {
507513
}
508514

509515
func (d *DotGit) rewritePackedRefsWithoutRef(name plumbing.ReferenceName) (err error) {
510-
pr, err := d.openAndLockPackedRefs()
516+
pr, err := d.openAndLockPackedRefs(false)
511517
if err != nil {
512518
return err
513519
}
514520
if pr == nil {
515521
return nil
516522
}
517-
doClosePR := true
518-
defer func() {
519-
if doClosePR {
520-
ioutil.CheckClose(pr, &err)
521-
}
522-
}()
523+
defer ioutil.CheckClose(pr, &err)
523524

524525
// Creating the temp file in the same directory as the target file
525526
// improves our chances for rename operation to be atomic.
526527
tmp, err := d.fs.TempFile("", tmpPackedRefsPrefix)
527528
if err != nil {
528529
return err
529530
}
530-
doCloseTmp := true
531+
tmpName := tmp.Name()
531532
defer func() {
532-
if doCloseTmp {
533-
ioutil.CheckClose(tmp, &err)
534-
}
533+
ioutil.CheckClose(tmp, &err)
534+
_ = d.fs.Remove(tmpName) // don't check err, we might have renamed it
535535
}()
536536

537537
s := bufio.NewScanner(pr)
@@ -558,26 +558,10 @@ func (d *DotGit) rewritePackedRefsWithoutRef(name plumbing.ReferenceName) (err e
558558
}
559559

560560
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
561+
return nil
578562
}
579563

580-
return d.fs.Rename(tmp.Name(), packedRefsPath)
564+
return d.rewritePackedRefsWhileLocked(tmp, pr)
581565
}
582566

583567
// process lines from a packed-refs file
@@ -691,20 +675,15 @@ func (d *DotGit) CountLooseRefs() (int, error) {
691675
// packed, plus all tags.
692676
func (d *DotGit) PackRefs() (err error) {
693677
// 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)
678+
f, err := d.openAndLockPackedRefs(true)
695679
if err != nil {
696680
return err
697681
}
698682
defer ioutil.CheckClose(f, &err)
699683

700-
err = f.Lock()
701-
if err != nil {
702-
return err
703-
}
704-
705684
// Gather all refs using addRefsFromRefDir and addRefsFromPackedRefs.
706685
var refs []*plumbing.Reference
707-
var seen = make(map[plumbing.ReferenceName]bool)
686+
seen := make(map[plumbing.ReferenceName]bool)
708687
if err := d.addRefsFromRefDir(&refs, seen); err != nil {
709688
return err
710689
}
@@ -722,12 +701,12 @@ func (d *DotGit) PackRefs() (err error) {
722701
if err != nil {
723702
return err
724703
}
725-
doCloseTmp := true
704+
tmpName := tmp.Name()
726705
defer func() {
727-
if doCloseTmp {
728-
ioutil.CheckClose(tmp, &err)
729-
}
706+
ioutil.CheckClose(tmp, &err)
707+
_ = d.fs.Remove(tmpName) // don't check err, we might have renamed it
730708
}()
709+
731710
w := bufio.NewWriter(tmp)
732711
for _, ref := range refs {
733712
_, err := w.WriteString(ref.String() + "\n")
@@ -741,11 +720,7 @@ func (d *DotGit) PackRefs() (err error) {
741720
}
742721

743722
// 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)
723+
err = d.rewritePackedRefsWhileLocked(tmp, f)
749724
if err != nil {
750725
return err
751726
}
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)