diff options
-rw-r--r-- | options.go | 3 | ||||
-rw-r--r-- | remote.go | 34 | ||||
-rw-r--r-- | remote_test.go | 53 |
3 files changed, 90 insertions, 0 deletions
@@ -210,6 +210,9 @@ type PushOptions struct { InsecureSkipTLS bool // CABundle specify additional ca bundle with system cert pool CABundle []byte + // RequireRemoteRefs only allows a remote ref to be updated if its current + // value is the one specified here. + RequireRemoteRefs []config.RefSpec } // Validate validates the fields and sets the default values. @@ -119,6 +119,10 @@ func (r *Remote) PushContext(ctx context.Context, o *PushOptions) (err error) { return err } + if err := r.checkRequireRemoteRefs(o.RequireRemoteRefs, remoteRefs); err != nil { + return err + } + isDelete := false allDelete := true for _, rs := range o.RefSpecs { @@ -1166,3 +1170,33 @@ outer: return r.s.SetShallow(shallows) } + +func (r *Remote) checkRequireRemoteRefs(requires []config.RefSpec, remoteRefs storer.ReferenceStorer) error { + for _, require := range requires { + if require.IsWildcard() { + return fmt.Errorf("wildcards not supported in RequireRemoteRefs, got %s", require.String()) + } + + name := require.Dst("") + remote, err := remoteRefs.Reference(name) + if err != nil { + return fmt.Errorf("remote ref %s required to be %s but is absent", name.String(), require.Src()) + } + + var requireHash string + if require.IsExactSHA1() { + requireHash = require.Src() + } else { + target, err := storer.ResolveReference(remoteRefs, plumbing.ReferenceName(require.Src())) + if err != nil { + return fmt.Errorf("could not resolve ref %s in RequireRemoteRefs", require.Src()) + } + requireHash = target.Hash().String() + } + + if remote.Hash().String() != requireHash { + return fmt.Errorf("remote ref %s required to be %s but is %s", name.String(), requireHash, remote.Hash().String()) + } + } + return nil +} diff --git a/remote_test.go b/remote_test.go index 3446f1a..bc05b7e 100644 --- a/remote_test.go +++ b/remote_test.go @@ -971,3 +971,56 @@ func (s *RemoteSuite) TestUseRefDeltas(c *C) { ar.Capabilities.Delete(capability.OFSDelta) c.Assert(r.useRefDeltas(ar), Equals, true) } + +func (s *RemoteSuite) TestPushRequireRemoteRefs(c *C) { + f := fixtures.Basic().One() + sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault()) + + dstFs := f.DotGit() + dstSto := filesystem.NewStorage(dstFs, cache.NewObjectLRUDefault()) + + url := dstFs.Root() + r := NewRemote(sto, &config.RemoteConfig{ + Name: DefaultRemoteName, + URLs: []string{url}, + }) + + oldRef, err := dstSto.Reference(plumbing.ReferenceName("refs/heads/branch")) + c.Assert(err, IsNil) + c.Assert(oldRef, NotNil) + + otherRef, err := dstSto.Reference(plumbing.ReferenceName("refs/heads/master")) + c.Assert(err, IsNil) + c.Assert(otherRef, NotNil) + + err = r.Push(&PushOptions{ + RefSpecs: []config.RefSpec{"refs/heads/master:refs/heads/branch"}, + RequireRemoteRefs: []config.RefSpec{config.RefSpec(otherRef.Hash().String() + ":refs/heads/branch")}, + }) + c.Assert(err, ErrorMatches, "remote ref refs/heads/branch required to be .* but is .*") + + newRef, err := dstSto.Reference(plumbing.ReferenceName("refs/heads/branch")) + c.Assert(err, IsNil) + c.Assert(newRef, DeepEquals, oldRef) + + err = r.Push(&PushOptions{ + RefSpecs: []config.RefSpec{"refs/heads/master:refs/heads/branch"}, + RequireRemoteRefs: []config.RefSpec{config.RefSpec(oldRef.Hash().String() + ":refs/heads/branch")}, + }) + c.Assert(err, ErrorMatches, "non-fast-forward update: .*") + + newRef, err = dstSto.Reference(plumbing.ReferenceName("refs/heads/branch")) + c.Assert(err, IsNil) + c.Assert(newRef, DeepEquals, oldRef) + + err = r.Push(&PushOptions{ + RefSpecs: []config.RefSpec{"refs/heads/master:refs/heads/branch"}, + RequireRemoteRefs: []config.RefSpec{config.RefSpec(oldRef.Hash().String() + ":refs/heads/branch")}, + Force: true, + }) + c.Assert(err, IsNil) + + newRef, err = dstSto.Reference(plumbing.ReferenceName("refs/heads/branch")) + c.Assert(err, IsNil) + c.Assert(newRef, Not(DeepEquals), oldRef) +} |