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

Commit c07c778

Browse files
authored
Merge pull request #665 from keybase/strib/gh-fast-forward-fetch
remote: support for non-force, fast-forward-only fetches
2 parents 6dda959 + cbab840 commit c07c778

File tree

10 files changed

+205
-22
lines changed

10 files changed

+205
-22
lines changed

options.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ type PullOptions struct {
9595
// stored, if nil nothing is stored and the capability (if supported)
9696
// no-progress, is sent to the server to avoid send this information.
9797
Progress sideband.Progress
98+
// Force allows the pull to update a local branch even when the remote
99+
// branch does not descend from it.
100+
Force bool
98101
}
99102

100103
// Validate validates the fields and sets the default values.
@@ -142,6 +145,9 @@ type FetchOptions struct {
142145
// Tags describe how the tags will be fetched from the remote repository,
143146
// by default is TagFollowing.
144147
Tags TagMode
148+
// Force allows the fetch to update a local branch even when the remote
149+
// branch does not descend from it.
150+
Force bool
145151
}
146152

147153
// Validate validates the fields and sets the default values.

plumbing/storer/reference.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ var ErrMaxResolveRecursion = errors.New("max. recursion level reached")
1616
// ReferenceStorer is a generic storage of references.
1717
type ReferenceStorer interface {
1818
SetReference(*plumbing.Reference) error
19+
// CheckAndSetReference sets the reference `new`, but if `old` is
20+
// not `nil`, it first checks that the current stored value for
21+
// `old.Name()` matches the given reference value in `old`. If
22+
// not, it returns an error and doesn't update `new`.
23+
CheckAndSetReference(new, old *plumbing.Reference) error
1924
Reference(plumbing.ReferenceName) (*plumbing.Reference, error)
2025
IterReferences() (ReferenceIter, error)
2126
RemoveReference(plumbing.ReferenceName) error

remote.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
var (
2626
NoErrAlreadyUpToDate = errors.New("already up-to-date")
2727
ErrDeleteRefNotSupported = errors.New("server does not support delete-refs")
28+
ErrForceNeeded = errors.New("some refs were not updated")
2829
)
2930

3031
const (
@@ -302,7 +303,7 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (storer.ReferenceSt
302303
}
303304
}
304305

305-
updated, err := r.updateLocalReferenceStorage(o.RefSpecs, refs, remoteRefs, o.Tags)
306+
updated, err := r.updateLocalReferenceStorage(o.RefSpecs, refs, remoteRefs, o.Tags, o.Force)
306307
if err != nil {
307308
return nil, err
308309
}
@@ -773,8 +774,11 @@ func (r *Remote) updateLocalReferenceStorage(
773774
specs []config.RefSpec,
774775
fetchedRefs, remoteRefs memory.ReferenceStorage,
775776
tagMode TagMode,
777+
force bool,
776778
) (updated bool, err error) {
777779
isWildcard := true
780+
forceNeeded := false
781+
778782
for _, spec := range specs {
779783
if !spec.IsWildcard() {
780784
isWildcard = false
@@ -789,9 +793,25 @@ func (r *Remote) updateLocalReferenceStorage(
789793
continue
790794
}
791795

792-
new := plumbing.NewHashReference(spec.Dst(ref.Name()), ref.Hash())
796+
localName := spec.Dst(ref.Name())
797+
old, _ := storer.ResolveReference(r.s, localName)
798+
new := plumbing.NewHashReference(localName, ref.Hash())
799+
800+
// If the ref exists locally as a branch and force is not specified,
801+
// only update if the new ref is an ancestor of the old
802+
if old != nil && old.Name().IsBranch() && !force && !spec.IsForceUpdate() {
803+
ff, err := isFastForward(r.s, old.Hash(), new.Hash())
804+
if err != nil {
805+
return updated, err
806+
}
807+
808+
if !ff {
809+
forceNeeded = true
810+
continue
811+
}
812+
}
793813

794-
refUpdated, err := updateReferenceStorerIfNeeded(r.s, new)
814+
refUpdated, err := checkAndUpdateReferenceStorerIfNeeded(r.s, new, old)
795815
if err != nil {
796816
return updated, err
797817
}
@@ -819,6 +839,10 @@ func (r *Remote) updateLocalReferenceStorage(
819839
updated = true
820840
}
821841

842+
if err == nil && forceNeeded {
843+
err = ErrForceNeeded
844+
}
845+
822846
return
823847
}
824848

remote_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,65 @@ func (s *RemoteSuite) doTestFetchNoErrAlreadyUpToDate(c *C, url string) {
308308
c.Assert(err, Equals, NoErrAlreadyUpToDate)
309309
}
310310

311+
func (s *RemoteSuite) testFetchFastForward(c *C, sto storage.Storer) {
312+
r := newRemote(sto, &config.RemoteConfig{
313+
URLs: []string{s.GetBasicLocalRepositoryURL()},
314+
})
315+
316+
s.testFetch(c, r, &FetchOptions{
317+
RefSpecs: []config.RefSpec{
318+
config.RefSpec("+refs/heads/master:refs/heads/master"),
319+
},
320+
}, []*plumbing.Reference{
321+
plumbing.NewReferenceFromStrings("refs/heads/master", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5"),
322+
})
323+
324+
// First make sure that we error correctly when a force is required.
325+
err := r.Fetch(&FetchOptions{
326+
RefSpecs: []config.RefSpec{
327+
config.RefSpec("refs/heads/branch:refs/heads/master"),
328+
},
329+
})
330+
c.Assert(err, Equals, ErrForceNeeded)
331+
332+
// And that forcing it fixes the problem.
333+
err = r.Fetch(&FetchOptions{
334+
RefSpecs: []config.RefSpec{
335+
config.RefSpec("+refs/heads/branch:refs/heads/master"),
336+
},
337+
})
338+
c.Assert(err, IsNil)
339+
340+
// Now test that a fast-forward, non-force fetch works.
341+
r.s.SetReference(plumbing.NewReferenceFromStrings(
342+
"refs/heads/master", "918c48b83bd081e863dbe1b80f8998f058cd8294",
343+
))
344+
s.testFetch(c, r, &FetchOptions{
345+
RefSpecs: []config.RefSpec{
346+
config.RefSpec("refs/heads/master:refs/heads/master"),
347+
},
348+
}, []*plumbing.Reference{
349+
plumbing.NewReferenceFromStrings("refs/heads/master", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5"),
350+
})
351+
}
352+
353+
func (s *RemoteSuite) TestFetchFastForwardMem(c *C) {
354+
s.testFetchFastForward(c, memory.NewStorage())
355+
}
356+
357+
func (s *RemoteSuite) TestFetchFastForwardFS(c *C) {
358+
dir, err := ioutil.TempDir("", "fetch")
359+
c.Assert(err, IsNil)
360+
361+
defer os.RemoveAll(dir) // clean up
362+
363+
fss, err := filesystem.NewStorage(osfs.New(dir))
364+
c.Assert(err, IsNil)
365+
366+
// This exercises `storage.filesystem.Storage.CheckAndSetReference()`.
367+
s.testFetchFastForward(c, fss)
368+
}
369+
311370
func (s *RemoteSuite) TestString(c *C) {
312371
r := newRemote(nil, &config.RemoteConfig{
313372
Name: "foo",

repository.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -625,17 +625,17 @@ func (r *Repository) calculateRemoteHeadReference(spec []config.RefSpec,
625625
return refs
626626
}
627627

628-
func updateReferenceStorerIfNeeded(
629-
s storer.ReferenceStorer, r *plumbing.Reference) (updated bool, err error) {
630-
628+
func checkAndUpdateReferenceStorerIfNeeded(
629+
s storer.ReferenceStorer, r, old *plumbing.Reference) (
630+
updated bool, err error) {
631631
p, err := s.Reference(r.Name())
632632
if err != nil && err != plumbing.ErrReferenceNotFound {
633633
return false, err
634634
}
635635

636636
// we use the string method to compare references, is the easiest way
637637
if err == plumbing.ErrReferenceNotFound || r.String() != p.String() {
638-
if err := s.SetReference(r); err != nil {
638+
if err := s.CheckAndSetReference(r, old); err != nil {
639639
return false, err
640640
}
641641

@@ -645,6 +645,11 @@ func updateReferenceStorerIfNeeded(
645645
return false, nil
646646
}
647647

648+
func updateReferenceStorerIfNeeded(
649+
s storer.ReferenceStorer, r *plumbing.Reference) (updated bool, err error) {
650+
return checkAndUpdateReferenceStorerIfNeeded(s, r, nil)
651+
}
652+
648653
// Fetch fetches references along with the objects necessary to complete
649654
// their histories, from the remote named as FetchOptions.RemoteName.
650655
//

storage/filesystem/internal/dotgit/dotgit.go

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"bufio"
66
"errors"
77
"fmt"
8+
"io"
89
stdioutil "io/ioutil"
910
"os"
1011
"strings"
@@ -239,7 +240,39 @@ func (d *DotGit) Object(h plumbing.Hash) (billy.File, error) {
239240
return d.fs.Open(file)
240241
}
241242

242-
func (d *DotGit) SetRef(r *plumbing.Reference) error {
243+
func (d *DotGit) readReferenceFrom(rd io.Reader, name string) (ref *plumbing.Reference, err error) {
244+
b, err := stdioutil.ReadAll(rd)
245+
if err != nil {
246+
return nil, err
247+
}
248+
249+
line := strings.TrimSpace(string(b))
250+
return plumbing.NewReferenceFromStrings(name, line), nil
251+
}
252+
253+
func (d *DotGit) checkReferenceAndTruncate(f billy.File, old *plumbing.Reference) error {
254+
if old == nil {
255+
return nil
256+
}
257+
ref, err := d.readReferenceFrom(f, old.Name().String())
258+
if err != nil {
259+
return err
260+
}
261+
if ref.Hash() != old.Hash() {
262+
return fmt.Errorf("reference has changed concurrently")
263+
}
264+
_, err = f.Seek(0, io.SeekStart)
265+
if err != nil {
266+
return err
267+
}
268+
err = f.Truncate(0)
269+
if err != nil {
270+
return err
271+
}
272+
return nil
273+
}
274+
275+
func (d *DotGit) SetRef(r, old *plumbing.Reference) error {
243276
var content string
244277
switch r.Type() {
245278
case plumbing.SymbolicReference:
@@ -248,13 +281,34 @@ func (d *DotGit) SetRef(r *plumbing.Reference) error {
248281
content = fmt.Sprintln(r.Hash().String())
249282
}
250283

251-
f, err := d.fs.Create(r.Name().String())
284+
// If we are not checking an old ref, just truncate the file.
285+
mode := os.O_RDWR | os.O_CREATE
286+
if old == nil {
287+
mode |= os.O_TRUNC
288+
}
289+
290+
f, err := d.fs.OpenFile(r.Name().String(), mode, 0666)
252291
if err != nil {
253292
return err
254293
}
255294

256295
defer ioutil.CheckClose(f, &err)
257296

297+
// Lock is unlocked by the deferred Close above. This is because Unlock
298+
// does not imply a fsync and thus there would be a race between
299+
// Unlock+Close and other concurrent writers. Adding Sync to go-billy
300+
// could work, but this is better (and avoids superfluous syncs).
301+
err = f.Lock()
302+
if err != nil {
303+
return err
304+
}
305+
306+
// this is a no-op to call even when old is nil.
307+
err = d.checkReferenceAndTruncate(f, old)
308+
if err != nil {
309+
return err
310+
}
311+
258312
_, err = f.Write([]byte(content))
259313
return err
260314
}
@@ -523,13 +577,7 @@ func (d *DotGit) readReferenceFile(path, name string) (ref *plumbing.Reference,
523577
}
524578
defer ioutil.CheckClose(f, &err)
525579

526-
b, err := stdioutil.ReadAll(f)
527-
if err != nil {
528-
return nil, err
529-
}
530-
531-
line := strings.TrimSpace(string(b))
532-
return plumbing.NewReferenceFromStrings(name, line), nil
580+
return d.readReferenceFrom(f, name)
533581
}
534582

535583
// Module return a billy.Filesystem poiting to the module folder

storage/filesystem/internal/dotgit/dotgit_test.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,24 +55,25 @@ func (s *SuiteDotGit) TestSetRefs(c *C) {
5555
fs := osfs.New(tmp)
5656
dir := New(fs)
5757

58-
err = dir.SetRef(plumbing.NewReferenceFromStrings(
58+
firstFoo := plumbing.NewReferenceFromStrings(
5959
"refs/heads/foo",
6060
"e8d3ffab552895c19b9fcf7aa264d277cde33881",
61-
))
61+
)
62+
err = dir.SetRef(firstFoo, nil)
6263

6364
c.Assert(err, IsNil)
6465

6566
err = dir.SetRef(plumbing.NewReferenceFromStrings(
6667
"refs/heads/symbolic",
6768
"ref: refs/heads/foo",
68-
))
69+
), nil)
6970

7071
c.Assert(err, IsNil)
7172

7273
err = dir.SetRef(plumbing.NewReferenceFromStrings(
7374
"bar",
7475
"e8d3ffab552895c19b9fcf7aa264d277cde33881",
75-
))
76+
), nil)
7677
c.Assert(err, IsNil)
7778

7879
refs, err := dir.Refs()
@@ -105,6 +106,20 @@ func (s *SuiteDotGit) TestSetRefs(c *C) {
105106
c.Assert(ref, NotNil)
106107
c.Assert(ref.Hash().String(), Equals, "e8d3ffab552895c19b9fcf7aa264d277cde33881")
107108

109+
// Check that SetRef with a non-nil `old` works.
110+
err = dir.SetRef(plumbing.NewReferenceFromStrings(
111+
"refs/heads/foo",
112+
"6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
113+
), firstFoo)
114+
c.Assert(err, IsNil)
115+
116+
// `firstFoo` is no longer the right `old` reference, so this
117+
// should fail.
118+
err = dir.SetRef(plumbing.NewReferenceFromStrings(
119+
"refs/heads/foo",
120+
"6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
121+
), firstFoo)
122+
c.Assert(err, NotNil)
108123
}
109124

110125
func (s *SuiteDotGit) TestRefsFromPackedRefs(c *C) {
@@ -192,7 +207,7 @@ func (s *SuiteDotGit) TestRemoveRefFromReferenceFileAndPackedRefs(c *C) {
192207
err := dir.SetRef(plumbing.NewReferenceFromStrings(
193208
"refs/remotes/origin/branch",
194209
"e8d3ffab552895c19b9fcf7aa264d277cde33881",
195-
))
210+
), nil)
196211

197212
// Make sure it only appears once in the refs list.
198213
refs, err := dir.Refs()

storage/filesystem/reference.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ type ReferenceStorage struct {
1111
}
1212

1313
func (r *ReferenceStorage) SetReference(ref *plumbing.Reference) error {
14-
return r.dir.SetRef(ref)
14+
return r.dir.SetRef(ref, nil)
15+
}
16+
17+
func (r *ReferenceStorage) CheckAndSetReference(ref, old *plumbing.Reference) error {
18+
return r.dir.SetRef(ref, old)
1519
}
1620

1721
func (r *ReferenceStorage) Reference(n plumbing.ReferenceName) (*plumbing.Reference, error) {

0 commit comments

Comments
 (0)