diff options
-rw-r--r-- | common_test.go | 8 | ||||
-rw-r--r-- | options.go | 11 | ||||
-rw-r--r-- | remote.go | 95 | ||||
-rw-r--r-- | remote_test.go | 72 | ||||
-rw-r--r-- | worktree.go | 1 |
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) + } +} @@ -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. @@ -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, |