aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--common_test.go8
-rw-r--r--options.go11
-rw-r--r--remote.go95
-rw-r--r--remote_test.go72
-rw-r--r--worktree.go1
5 files changed, 182 insertions, 5 deletions
diff --git a/common_test.go b/common_test.go
index 5f5bc4c..b47f5bb 100644
--- a/common_test.go
+++ b/common_test.go
@@ -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)
+ }
+}
diff --git a/options.go b/options.go
index 7068796..3bd6876 100644
--- a/options.go
+++ b/options.go
@@ -91,6 +91,8 @@ func (o *CloneOptions) Validate() error {
type PullOptions struct {
// Name of the remote to be pulled. If empty, uses the default.
RemoteName string
+ // RemoteURL overrides the remote repo address with a custom URL
+ RemoteURL string
// Remote branch to clone. If empty, uses HEAD.
ReferenceName plumbing.ReferenceName
// Fetch only ReferenceName if true.
@@ -147,7 +149,9 @@ const (
type FetchOptions struct {
// Name of the remote to fetch from. Defaults to origin.
RemoteName string
- RefSpecs []config.RefSpec
+ // RemoteURL overrides the remote repo address with a custom URL
+ RemoteURL string
+ RefSpecs []config.RefSpec
// Depth limit fetching to the specified number of commits from the tip of
// each remote branch history.
Depth int
@@ -192,6 +196,8 @@ func (o *FetchOptions) Validate() error {
type PushOptions struct {
// RemoteName is the name of the remote to be pushed to.
RemoteName string
+ // RemoteURL overrides the remote repo address with a custom URL
+ RemoteURL string
// RefSpecs specify what destination ref to update with what source
// object. A refspec with empty src can be used to delete a reference.
RefSpecs []config.RefSpec
@@ -213,6 +219,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.
diff --git a/remote.go b/remote.go
index 4a06106..9f2995d 100644
--- a/remote.go
+++ b/remote.go
@@ -5,10 +5,12 @@ import (
"errors"
"fmt"
"io"
+ "strings"
"time"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5/config"
+ "github.com/go-git/go-git/v5/internal/url"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/format/packfile"
@@ -103,7 +105,11 @@ func (r *Remote) PushContext(ctx context.Context, o *PushOptions) (err error) {
return fmt.Errorf("remote names don't match: %s != %s", o.RemoteName, r.c.Name)
}
- s, err := newSendPackSession(r.c.URLs[0], o.Auth, o.InsecureSkipTLS, o.CABundle)
+ if o.RemoteURL == "" {
+ o.RemoteURL = r.c.URLs[0]
+ }
+
+ s, err := newSendPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle)
if err != nil {
return err
}
@@ -183,12 +189,12 @@ func (r *Remote) PushContext(ctx context.Context, o *PushOptions) (err error) {
var hashesToPush []plumbing.Hash
// Avoid the expensive revlist operation if we're only doing deletes.
if !allDelete {
- if r.c.IsFirstURLLocal() {
+ if url.IsLocalEndpoint(o.RemoteURL) {
// If we're are pushing to a local repo, it might be much
// faster to use a local storage layer to get the commits
// to ignore, when calculating the object revlist.
localStorer := filesystem.NewStorage(
- osfs.New(r.c.URLs[0]), cache.NewObjectLRUDefault())
+ osfs.New(o.RemoteURL), cache.NewObjectLRUDefault())
hashesToPush, err = revlist.ObjectsWithStorageForIgnores(
r.s, localStorer, objects, haves)
} else {
@@ -225,6 +231,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,
@@ -246,6 +323,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
}
@@ -314,7 +397,11 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen
o.RefSpecs = r.c.Fetch
}
- s, err := newUploadPackSession(r.c.URLs[0], o.Auth, o.InsecureSkipTLS, o.CABundle)
+ if o.RemoteURL == "" {
+ o.RemoteURL = r.c.URLs[0]
+ }
+
+ s, err := newUploadPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle)
if err != nil {
return nil, err
}
diff --git a/remote_test.go b/remote_test.go
index 1efc9da..0283e64 100644
--- a/remote_test.go
+++ b/remote_test.go
@@ -46,6 +46,12 @@ func (s *RemoteSuite) TestFetchInvalidSchemaEndpoint(c *C) {
c.Assert(err, ErrorMatches, ".*unsupported scheme.*")
}
+func (s *RemoteSuite) TestFetchOverriddenEndpoint(c *C) {
+ r := NewRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"http://perfectly-valid-url.example.com"}})
+ err := r.Fetch(&FetchOptions{RemoteURL: "http://\\"})
+ c.Assert(err, ErrorMatches, ".*invalid character.*")
+}
+
func (s *RemoteSuite) TestFetchInvalidFetchOptions(c *C) {
r := NewRemote(nil, &config.RemoteConfig{Name: "foo", URLs: []string{"qux://foo"}})
invalid := config.RefSpec("^*$ñ")
@@ -591,6 +597,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())
@@ -903,6 +969,12 @@ func (s *RemoteSuite) TestPushNonExistentEndpoint(c *C) {
c.Assert(err, NotNil)
}
+func (s *RemoteSuite) TestPushOverriddenEndpoint(c *C) {
+ r := NewRemote(nil, &config.RemoteConfig{Name: "origin", URLs: []string{"http://perfectly-valid-url.example.com"}})
+ err := r.Push(&PushOptions{RemoteURL: "http://\\"})
+ c.Assert(err, ErrorMatches, ".*invalid character.*")
+}
+
func (s *RemoteSuite) TestPushInvalidSchemaEndpoint(c *C) {
r := NewRemote(nil, &config.RemoteConfig{Name: "origin", URLs: []string{"qux://foo"}})
err := r.Push(&PushOptions{})
diff --git a/worktree.go b/worktree.go
index f23d9f1..362d10e 100644
--- a/worktree.go
+++ b/worktree.go
@@ -73,6 +73,7 @@ func (w *Worktree) PullContext(ctx context.Context, o *PullOptions) error {
fetchHead, err := remote.fetch(ctx, &FetchOptions{
RemoteName: o.RemoteName,
+ RemoteURL: o.RemoteURL,
Depth: o.Depth,
Auth: o.Auth,
Progress: o.Progress,