diff options
-rw-r--r-- | _examples/remotes/main.go | 2 | ||||
-rw-r--r-- | options.go | 15 | ||||
-rw-r--r-- | plumbing/format/index/encoder.go | 34 | ||||
-rw-r--r-- | plumbing/format/index/encoder_test.go | 26 | ||||
-rw-r--r-- | plumbing/protocol/packp/updreq.go | 6 | ||||
-rw-r--r-- | remote.go | 43 | ||||
-rw-r--r-- | remote_test.go | 127 | ||||
-rw-r--r-- | storage/filesystem/dotgit/dotgit_test.go | 7 | ||||
-rw-r--r-- | storage/filesystem/object_test.go | 4 |
9 files changed, 235 insertions, 29 deletions
diff --git a/_examples/remotes/main.go b/_examples/remotes/main.go index b1a91a9..d09957e 100644 --- a/_examples/remotes/main.go +++ b/_examples/remotes/main.go @@ -33,7 +33,7 @@ func main() { CheckIfError(err) // List remotes from a repository - Info("git remotes -v") + Info("git remote -v") list, err := r.Remotes() CheckIfError(err) @@ -228,10 +228,25 @@ type PushOptions struct { // FollowTags will send any annotated tags with a commit target reachable from // the refs already being pushed FollowTags bool + // ForceWithLease allows a force push as long as the remote ref adheres to a "lease" + ForceWithLease *ForceWithLease // PushOptions sets options to be transferred to the server during push. Options map[string]string } +// ForceWithLease sets fields on the lease +// If neither RefName nor Hash are set, ForceWithLease protects +// all refs in the refspec by ensuring the ref of the remote in the local repsitory +// matches the one in the ref advertisement. +type ForceWithLease struct { + // RefName, when set will protect the ref by ensuring it matches the + // hash in the ref advertisement. + RefName plumbing.ReferenceName + // Hash is the expected object id of RefName. The push will be rejected unless this + // matches the corresponding object id of RefName in the refs advertisement. + Hash plumbing.Hash +} + // Validate validates the fields and sets the default values. func (o *PushOptions) Validate() error { if o.RemoteName == "" { diff --git a/plumbing/format/index/encoder.go b/plumbing/format/index/encoder.go index 00d4e7a..2c94d93 100644 --- a/plumbing/format/index/encoder.go +++ b/plumbing/format/index/encoder.go @@ -14,7 +14,7 @@ import ( var ( // EncodeVersionSupported is the range of supported index versions - EncodeVersionSupported uint32 = 2 + EncodeVersionSupported uint32 = 3 // ErrInvalidTimestamp is returned by Encode if a Index with a Entry with // negative timestamp values @@ -36,9 +36,9 @@ func NewEncoder(w io.Writer) *Encoder { // Encode writes the Index to the stream of the encoder. func (e *Encoder) Encode(idx *Index) error { - // TODO: support versions v3 and v4 + // TODO: support v4 // TODO: support extensions - if idx.Version != EncodeVersionSupported { + if idx.Version > EncodeVersionSupported { return ErrUnsupportedVersion } @@ -68,8 +68,12 @@ func (e *Encoder) encodeEntries(idx *Index) error { if err := e.encodeEntry(entry); err != nil { return err } + entryLength := entryHeaderLength + if entry.IntentToAdd || entry.SkipWorktree { + entryLength += 2 + } - wrote := entryHeaderLength + len(entry.Name) + wrote := entryLength + len(entry.Name) if err := e.padEntry(wrote); err != nil { return err } @@ -79,10 +83,6 @@ func (e *Encoder) encodeEntries(idx *Index) error { } func (e *Encoder) encodeEntry(entry *Entry) error { - if entry.IntentToAdd || entry.SkipWorktree { - return ErrUnsupportedVersion - } - sec, nsec, err := e.timeToUint32(&entry.CreatedAt) if err != nil { return err @@ -110,9 +110,25 @@ func (e *Encoder) encodeEntry(entry *Entry) error { entry.GID, entry.Size, entry.Hash[:], - flags, } + flagsFlow := []interface{}{flags} + + if entry.IntentToAdd || entry.SkipWorktree { + var extendedFlags uint16 + + if entry.IntentToAdd { + extendedFlags |= intentToAddMask + } + if entry.SkipWorktree { + extendedFlags |= skipWorkTreeMask + } + + flagsFlow = []interface{}{flags | entryExtended, extendedFlags} + } + + flow = append(flow, flagsFlow...) + if err := binary.Write(e.w, flow...); err != nil { return err } diff --git a/plumbing/format/index/encoder_test.go b/plumbing/format/index/encoder_test.go index b7a73cb..25c24f1 100644 --- a/plumbing/format/index/encoder_test.go +++ b/plumbing/format/index/encoder_test.go @@ -57,7 +57,7 @@ func (s *IndexSuite) TestEncode(c *C) { } func (s *IndexSuite) TestEncodeUnsupportedVersion(c *C) { - idx := &Index{Version: 3} + idx := &Index{Version: 4} buf := bytes.NewBuffer(nil) e := NewEncoder(buf) @@ -67,24 +67,40 @@ func (s *IndexSuite) TestEncodeUnsupportedVersion(c *C) { func (s *IndexSuite) TestEncodeWithIntentToAddUnsupportedVersion(c *C) { idx := &Index{ - Version: 2, + Version: 3, Entries: []*Entry{{IntentToAdd: true}}, } buf := bytes.NewBuffer(nil) e := NewEncoder(buf) err := e.Encode(idx) - c.Assert(err, Equals, ErrUnsupportedVersion) + c.Assert(err, IsNil) + + output := &Index{} + d := NewDecoder(buf) + err = d.Decode(output) + c.Assert(err, IsNil) + + c.Assert(cmp.Equal(idx, output), Equals, true) + c.Assert(output.Entries[0].IntentToAdd, Equals, true) } func (s *IndexSuite) TestEncodeWithSkipWorktreeUnsupportedVersion(c *C) { idx := &Index{ - Version: 2, + Version: 3, Entries: []*Entry{{SkipWorktree: true}}, } buf := bytes.NewBuffer(nil) e := NewEncoder(buf) err := e.Encode(idx) - c.Assert(err, Equals, ErrUnsupportedVersion) + c.Assert(err, IsNil) + + output := &Index{} + d := NewDecoder(buf) + err = d.Decode(output) + c.Assert(err, IsNil) + + c.Assert(cmp.Equal(idx, output), Equals, true) + c.Assert(output.Entries[0].SkipWorktree, Equals, true) } diff --git a/plumbing/protocol/packp/updreq.go b/plumbing/protocol/packp/updreq.go index 46ad6fd..5dbd8ac 100644 --- a/plumbing/protocol/packp/updreq.go +++ b/plumbing/protocol/packp/updreq.go @@ -87,9 +87,9 @@ type Action string const ( Create Action = "create" - Update = "update" - Delete = "delete" - Invalid = "invalid" + Update Action = "update" + Delete Action = "delete" + Invalid Action = "invalid" ) type Command struct { @@ -326,7 +326,7 @@ func (r *Remote) newReferenceUpdateRequest( } } - if err := r.addReferencesToUpdate(o.RefSpecs, localRefs, remoteRefs, req, o.Prune); err != nil { + if err := r.addReferencesToUpdate(o.RefSpecs, localRefs, remoteRefs, req, o.Prune, o.ForceWithLease); err != nil { return nil, err } @@ -568,6 +568,7 @@ func (r *Remote) addReferencesToUpdate( remoteRefs storer.ReferenceStorer, req *packp.ReferenceUpdateRequest, prune bool, + forceWithLease *ForceWithLease, ) error { // This references dictionary will be used to search references by name. refsDict := make(map[string]*plumbing.Reference) @@ -581,7 +582,7 @@ func (r *Remote) addReferencesToUpdate( return err } } else { - err := r.addOrUpdateReferences(rs, localRefs, refsDict, remoteRefs, req) + err := r.addOrUpdateReferences(rs, localRefs, refsDict, remoteRefs, req, forceWithLease) if err != nil { return err } @@ -603,6 +604,7 @@ func (r *Remote) addOrUpdateReferences( refsDict map[string]*plumbing.Reference, remoteRefs storer.ReferenceStorer, req *packp.ReferenceUpdateRequest, + forceWithLease *ForceWithLease, ) error { // If it is not a wilcard refspec we can directly search for the reference // in the references dictionary. @@ -616,11 +618,11 @@ func (r *Remote) addOrUpdateReferences( return nil } - return r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req) + return r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req, forceWithLease) } for _, ref := range localRefs { - err := r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req) + err := r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req, forceWithLease) if err != nil { return err } @@ -706,7 +708,7 @@ func (r *Remote) addCommit(rs config.RefSpec, func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec, remoteRefs storer.ReferenceStorer, localRef *plumbing.Reference, - req *packp.ReferenceUpdateRequest) error { + req *packp.ReferenceUpdateRequest, forceWithLease *ForceWithLease) error { if localRef.Type() != plumbing.HashReference { return nil @@ -738,7 +740,11 @@ func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec, return nil } - if !rs.IsForceUpdate() { + if forceWithLease != nil { + if err = r.checkForceWithLease(localRef, cmd, forceWithLease); err != nil { + return err + } + } else if !rs.IsForceUpdate() { if err := checkFastForwardUpdate(r.s, remoteRefs, cmd); err != nil { return err } @@ -748,6 +754,31 @@ func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec, return nil } +func (r *Remote) checkForceWithLease(localRef *plumbing.Reference, cmd *packp.Command, forceWithLease *ForceWithLease) error { + remotePrefix := fmt.Sprintf("refs/remotes/%s/", r.Config().Name) + + ref, err := storer.ResolveReference( + r.s, + plumbing.ReferenceName(remotePrefix+strings.Replace(localRef.Name().String(), "refs/heads/", "", -1))) + if err != nil { + return err + } + + if forceWithLease.RefName.String() == "" || (forceWithLease.RefName == cmd.Name) { + expectedOID := ref.Hash() + + if !forceWithLease.Hash.IsZero() { + expectedOID = forceWithLease.Hash + } + + if cmd.Old != expectedOID { + return fmt.Errorf("non-fast-forward update: %s", cmd.Name.String()) + } + } + + return nil +} + func (r *Remote) references() ([]*plumbing.Reference, error) { var localRefs []*plumbing.Reference diff --git a/remote_test.go b/remote_test.go index df07c08..ebe9a55 100644 --- a/remote_test.go +++ b/remote_test.go @@ -816,6 +816,133 @@ func (s *RemoteSuite) TestPushForceWithOption(c *C) { c.Assert(newRef, Not(DeepEquals), oldRef) } +func (s *RemoteSuite) TestPushForceWithLease_success(c *C) { + testCases := []struct { + desc string + forceWithLease ForceWithLease + }{ + { + desc: "no arguments", + forceWithLease: ForceWithLease{}, + }, + { + desc: "ref name", + forceWithLease: ForceWithLease{ + RefName: plumbing.ReferenceName("refs/heads/branch"), + }, + }, + { + desc: "ref name and sha", + forceWithLease: ForceWithLease{ + RefName: plumbing.ReferenceName("refs/heads/branch"), + Hash: plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"), + }, + }, + } + + for _, tc := range testCases { + c.Log("Executing test cases:", tc.desc) + + f := fixtures.Basic().One() + sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault()) + dstFs := f.DotGit() + dstSto := filesystem.NewStorage(dstFs, cache.NewObjectLRUDefault()) + + newCommit := plumbing.NewHashReference( + "refs/heads/branch", plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"), + ) + c.Assert(sto.SetReference(newCommit), IsNil) + + ref, err := sto.Reference("refs/heads/branch") + c.Log(ref.String()) + + url := dstFs.Root() + r := NewRemote(sto, &config.RemoteConfig{ + Name: DefaultRemoteName, + URLs: []string{url}, + }) + + oldRef, err := dstSto.Reference("refs/heads/branch") + c.Assert(err, IsNil) + c.Assert(oldRef, NotNil) + + c.Assert(r.Push(&PushOptions{ + RefSpecs: []config.RefSpec{"refs/heads/branch:refs/heads/branch"}, + ForceWithLease: &ForceWithLease{}, + }), IsNil) + + newRef, err := dstSto.Reference("refs/heads/branch") + c.Assert(err, IsNil) + c.Assert(newRef, DeepEquals, newCommit) + } +} + +func (s *RemoteSuite) TestPushForceWithLease_failure(c *C) { + testCases := []struct { + desc string + forceWithLease ForceWithLease + }{ + { + desc: "no arguments", + forceWithLease: ForceWithLease{}, + }, + { + desc: "ref name", + forceWithLease: ForceWithLease{ + RefName: plumbing.ReferenceName("refs/heads/branch"), + }, + }, + { + desc: "ref name and sha", + forceWithLease: ForceWithLease{ + RefName: plumbing.ReferenceName("refs/heads/branch"), + Hash: plumbing.NewHash("152175bf7e5580299fa1f0ba41ef6474cc043b70"), + }, + }, + } + + for _, tc := range testCases { + c.Log("Executing test cases:", tc.desc) + + f := fixtures.Basic().One() + sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault()) + c.Assert(sto.SetReference( + plumbing.NewHashReference( + "refs/heads/branch", plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"), + ), + ), IsNil) + + dstFs := f.DotGit() + dstSto := filesystem.NewStorage(dstFs, cache.NewObjectLRUDefault()) + c.Assert(dstSto.SetReference( + plumbing.NewHashReference( + "refs/heads/branch", plumbing.NewHash("ad7897c0fb8e7d9a9ba41fa66072cf06095a6cfc"), + ), + ), IsNil) + + url := dstFs.Root() + r := NewRemote(sto, &config.RemoteConfig{ + Name: DefaultRemoteName, + URLs: []string{url}, + }) + + oldRef, err := dstSto.Reference("refs/heads/branch") + c.Assert(err, IsNil) + c.Assert(oldRef, NotNil) + + err = r.Push(&PushOptions{ + RefSpecs: []config.RefSpec{"refs/heads/branch:refs/heads/branch"}, + ForceWithLease: &ForceWithLease{}, + }) + + c.Assert(err, DeepEquals, errors.New("non-fast-forward update: refs/heads/branch")) + + newRef, err := dstSto.Reference("refs/heads/branch") + c.Assert(err, IsNil) + c.Assert(newRef, Not(DeepEquals), plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")) + } +} + func (s *RemoteSuite) TestPushPrune(c *C) { fs := fixtures.Basic().One().DotGit() diff --git a/storage/filesystem/dotgit/dotgit_test.go b/storage/filesystem/dotgit/dotgit_test.go index 4c2ae94..1a09fde 100644 --- a/storage/filesystem/dotgit/dotgit_test.go +++ b/storage/filesystem/dotgit/dotgit_test.go @@ -3,6 +3,7 @@ package dotgit import ( "bufio" "encoding/hex" + "io" "io/ioutil" "os" "path/filepath" @@ -510,13 +511,13 @@ func (s *SuiteDotGit) TestObjectPackWithKeepDescriptors(c *C) { c.Assert(filepath.Ext(pack.Name()), Equals, ".pack") // Move to an specific offset - pack.Seek(42, os.SEEK_SET) + pack.Seek(42, io.SeekStart) pack2, err := dir.ObjectPack(plumbing.NewHash(f.PackfileHash)) c.Assert(err, IsNil) // If the file is the same the offset should be the same - offset, err := pack2.Seek(0, os.SEEK_CUR) + offset, err := pack2.Seek(0, io.SeekCurrent) c.Assert(err, IsNil) c.Assert(offset, Equals, int64(42)) @@ -527,7 +528,7 @@ func (s *SuiteDotGit) TestObjectPackWithKeepDescriptors(c *C) { c.Assert(err, IsNil) // If the file is opened again its offset should be 0 - offset, err = pack2.Seek(0, os.SEEK_CUR) + offset, err = pack2.Seek(0, io.SeekCurrent) c.Assert(err, IsNil) c.Assert(offset, Equals, int64(0)) diff --git a/storage/filesystem/object_test.go b/storage/filesystem/object_test.go index 59b40d3..1c3267b 100644 --- a/storage/filesystem/object_test.go +++ b/storage/filesystem/object_test.go @@ -71,7 +71,7 @@ func (s *FsSuite) TestGetFromPackfileKeepDescriptors(c *C) { pack1, err := dg.ObjectPack(packfiles[0]) c.Assert(err, IsNil) - pack1.Seek(42, os.SEEK_SET) + pack1.Seek(42, io.SeekStart) err = o.Close() c.Assert(err, IsNil) @@ -79,7 +79,7 @@ func (s *FsSuite) TestGetFromPackfileKeepDescriptors(c *C) { pack2, err := dg.ObjectPack(packfiles[0]) c.Assert(err, IsNil) - offset, err := pack2.Seek(0, os.SEEK_CUR) + offset, err := pack2.Seek(0, io.SeekCurrent) c.Assert(err, IsNil) c.Assert(offset, Equals, int64(0)) |