Skip to content

Commit

Permalink
Merge pull request #385 from john-cai/add-follow-tags
Browse files Browse the repository at this point in the history
git: add --follow-tags option for pushes
  • Loading branch information
mcuadros committed Oct 26, 2021
2 parents 4ec1753 + 5340c58 commit 243e7c8
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 0 deletions.
8 changes: 8 additions & 0 deletions common_test.go
Expand Up @@ -198,3 +198,11 @@ func AssertReferences(c *C, r *Repository, expected map[string]string) {
c.Assert(obtained, DeepEquals, expected)
}
}

func AssertReferencesMissing(c *C, r *Repository, expected []string) {
for _, name := range expected {
_, err := r.Reference(plumbing.ReferenceName(name), false)
c.Assert(err, NotNil)
c.Assert(err, Equals, plumbing.ErrReferenceNotFound)
}
}
3 changes: 3 additions & 0 deletions options.go
Expand Up @@ -213,6 +213,9 @@ type PushOptions struct {
// RequireRemoteRefs only allows a remote ref to be updated if its current
// value is the one specified here.
RequireRemoteRefs []config.RefSpec
// FollowTags will send any annotated tags with a commit target reachable from
// the refs already being pushed
FollowTags bool
}

// Validate validates the fields and sets the default values.
Expand Down
78 changes: 78 additions & 0 deletions remote.go
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"strings"
"time"

"github.com/go-git/go-billy/v5/osfs"
Expand Down Expand Up @@ -225,6 +226,77 @@ func (r *Remote) useRefDeltas(ar *packp.AdvRefs) bool {
return !ar.Capabilities.Supports(capability.OFSDelta)
}

func (r *Remote) addReachableTags(localRefs []*plumbing.Reference, remoteRefs storer.ReferenceStorer, req *packp.ReferenceUpdateRequest) error {
tags := make(map[plumbing.Reference]struct{})
// get a list of all tags locally
for _, ref := range localRefs {
if strings.HasPrefix(string(ref.Name()), "refs/tags") {
tags[*ref] = struct{}{}
}
}

remoteRefIter, err := remoteRefs.IterReferences()
if err != nil {
return err
}

// remove any that are already on the remote
if err := remoteRefIter.ForEach(func(reference *plumbing.Reference) error {
if _, ok := tags[*reference]; ok {
delete(tags, *reference)
}

return nil
}); err != nil {
return err
}

for tag, _ := range tags {
tagObject, err := object.GetObject(r.s, tag.Hash())
var tagCommit *object.Commit
if err != nil {
return fmt.Errorf("get tag object: %w\n", err)
}

if tagObject.Type() != plumbing.TagObject {
continue
}

annotatedTag, ok := tagObject.(*object.Tag)
if !ok {
return errors.New("could not get annotated tag object")
}

tagCommit, err = object.GetCommit(r.s, annotatedTag.Target)
if err != nil {
return fmt.Errorf("get annotated tag commit: %w\n", err)
}

// only include tags that are reachable from one of the refs
// already being pushed
for _, cmd := range req.Commands {
if tag.Name() == cmd.Name {
continue
}

if strings.HasPrefix(cmd.Name.String(), "refs/tags") {
continue
}

c, err := object.GetCommit(r.s, cmd.New)
if err != nil {
return fmt.Errorf("get commit %v: %w", cmd.Name, err)
}

if isAncestor, err := tagCommit.IsAncestor(c); err == nil && isAncestor {
req.Commands = append(req.Commands, &packp.Command{Name: tag.Name(), New: tag.Hash()})
}
}
}

return nil
}

func (r *Remote) newReferenceUpdateRequest(
o *PushOptions,
localRefs []*plumbing.Reference,
Expand All @@ -246,6 +318,12 @@ func (r *Remote) newReferenceUpdateRequest(
return nil, err
}

if o.FollowTags {
if err := r.addReachableTags(localRefs, remoteRefs, req); err != nil {
return nil, err
}
}

return req, nil
}

Expand Down
60 changes: 60 additions & 0 deletions remote_test.go
Expand Up @@ -591,6 +591,66 @@ func (s *RemoteSuite) TestPushTags(c *C) {
})
}

func (s *RemoteSuite) TestPushFollowTags(c *C) {
url, clean := s.TemporalDir()
defer clean()

server, err := PlainInit(url, true)
c.Assert(err, IsNil)

fs := fixtures.ByURL("https://github.com/git-fixtures/basic.git").One().DotGit()
sto := filesystem.NewStorage(fs, cache.NewObjectLRUDefault())

r := NewRemote(sto, &config.RemoteConfig{
Name: DefaultRemoteName,
URLs: []string{url},
})

localRepo := newRepository(sto, fs)
tipTag, err := localRepo.CreateTag(
"tip",
plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"),
&CreateTagOptions{
Message: "an annotated tag",
},
)
c.Assert(err, IsNil)

initialTag, err := localRepo.CreateTag(
"initial-commit",
plumbing.NewHash("b029517f6300c2da0f4b651b8642506cd6aaf45d"),
&CreateTagOptions{
Message: "a tag for the initial commit",
},
)
c.Assert(err, IsNil)

_, err = localRepo.CreateTag(
"master-tag",
plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"),
&CreateTagOptions{
Message: "a tag with a commit not reachable from branch",
},
)
c.Assert(err, IsNil)

err = r.Push(&PushOptions{
RefSpecs: []config.RefSpec{"+refs/heads/branch:refs/heads/branch"},
FollowTags: true,
})
c.Assert(err, IsNil)

AssertReferences(c, server, map[string]string{
"refs/heads/branch": "e8d3ffab552895c19b9fcf7aa264d277cde33881",
"refs/tags/tip": tipTag.Hash().String(),
"refs/tags/initial-commit": initialTag.Hash().String(),
})

AssertReferencesMissing(c, server, []string{
"refs/tags/master-tag",
})
}

func (s *RemoteSuite) TestPushNoErrAlreadyUpToDate(c *C) {
fs := fixtures.Basic().One().DotGit()
sto := filesystem.NewStorage(fs, cache.NewObjectLRUDefault())
Expand Down

0 comments on commit 243e7c8

Please sign in to comment.