Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

git: add --follow-tags option for pushes #385

Merged
merged 1 commit into from Oct 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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