Skip to content

Commit

Permalink
Add ForceWithLease Push Option
Browse files Browse the repository at this point in the history
--force-with-lease allows a push to force push with some safety
measures. If the ref on the remote is what we expect, then the force
push is allowed to happen.
See https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegt
for more information
  • Loading branch information
john-cai authored and John Cai committed Nov 2, 2021
1 parent 3211a7a commit 7db545b
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 6 deletions.
15 changes: 15 additions & 0 deletions options.go
Expand Up @@ -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 == "" {
Expand Down
43 changes: 37 additions & 6 deletions remote.go
Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand All @@ -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.
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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

Expand Down
127 changes: 127 additions & 0 deletions remote_test.go
Expand Up @@ -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()

Expand Down

0 comments on commit 7db545b

Please sign in to comment.