aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--options.go5
-rw-r--r--remote.go31
-rw-r--r--remote_test.go114
3 files changed, 148 insertions, 2 deletions
diff --git a/options.go b/options.go
index 4f913dc..635a883 100644
--- a/options.go
+++ b/options.go
@@ -166,7 +166,7 @@ const (
// AllTags fetch all tags from the remote (i.e., fetch remote tags
// refs/tags/* into local tags with the same name)
AllTags
- //NoTags fetch no tags from the remote at all
+ // NoTags fetch no tags from the remote at all
NoTags
)
@@ -198,6 +198,9 @@ type FetchOptions struct {
CABundle []byte
// ProxyOptions provides info required for connecting to a proxy.
ProxyOptions transport.ProxyOptions
+ // Prune specify that local refs that match given RefSpecs and that do
+ // not exist remotely will be removed.
+ Prune bool
}
// Validate validates the fields and sets the default values.
diff --git a/remote.go b/remote.go
index 0cb70bc..f07292b 100644
--- a/remote.go
+++ b/remote.go
@@ -470,6 +470,14 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen
}
}
+ var updatedPrune bool
+ if o.Prune {
+ updatedPrune, err = r.pruneRemotes(o.RefSpecs, localRefs, remoteRefs)
+ if err != nil {
+ return nil, err
+ }
+ }
+
updated, err := r.updateLocalReferenceStorage(o.RefSpecs, refs, remoteRefs, specToRefs, o.Tags, o.Force)
if err != nil {
return nil, err
@@ -482,7 +490,7 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen
}
}
- if !updated {
+ if !updated && !updatedPrune {
return remoteRefs, NoErrAlreadyUpToDate
}
@@ -574,6 +582,27 @@ func (r *Remote) fetchPack(ctx context.Context, o *FetchOptions, s transport.Upl
return err
}
+func (r *Remote) pruneRemotes(specs []config.RefSpec, localRefs []*plumbing.Reference, remoteRefs memory.ReferenceStorage) (bool, error) {
+ var updatedPrune bool
+ for _, spec := range specs {
+ rev := spec.Reverse()
+ for _, ref := range localRefs {
+ if !rev.Match(ref.Name()) {
+ continue
+ }
+ _, err := remoteRefs.Reference(rev.Dst(ref.Name()))
+ if errors.Is(err, plumbing.ErrReferenceNotFound) {
+ updatedPrune = true
+ err := r.s.RemoveReference(ref.Name())
+ if err != nil {
+ return false, err
+ }
+ }
+ }
+ }
+ return updatedPrune, nil
+}
+
func (r *Remote) addReferencesToUpdate(
refspecs []config.RefSpec,
localRefs []*plumbing.Reference,
diff --git a/remote_test.go b/remote_test.go
index 81c60bc..1ffedd4 100644
--- a/remote_test.go
+++ b/remote_test.go
@@ -1444,6 +1444,120 @@ func (s *RemoteSuite) TestPushRequireRemoteRefs(c *C) {
c.Assert(newRef, Not(DeepEquals), oldRef)
}
+func (s *RemoteSuite) TestFetchPrune(c *C) {
+ fs := fixtures.Basic().One().DotGit()
+
+ url, clean := s.TemporalDir()
+ defer clean()
+
+ _, err := PlainClone(url, true, &CloneOptions{
+ URL: fs.Root(),
+ })
+ c.Assert(err, IsNil)
+
+ dir, clean := s.TemporalDir()
+ defer clean()
+
+ r, err := PlainClone(dir, true, &CloneOptions{
+ URL: url,
+ })
+ c.Assert(err, IsNil)
+
+ remote, err := r.Remote(DefaultRemoteName)
+ c.Assert(err, IsNil)
+
+ ref, err := r.Reference(plumbing.ReferenceName("refs/heads/master"), true)
+ c.Assert(err, IsNil)
+
+ err = remote.Push(&PushOptions{RefSpecs: []config.RefSpec{
+ "refs/heads/master:refs/heads/branch",
+ }})
+ c.Assert(err, IsNil)
+
+ dirSave, clean := s.TemporalDir()
+ defer clean()
+
+ rSave, err := PlainClone(dirSave, true, &CloneOptions{
+ URL: url,
+ })
+ c.Assert(err, IsNil)
+
+ AssertReferences(c, rSave, map[string]string{
+ "refs/remotes/origin/branch": ref.Hash().String(),
+ })
+
+ err = remote.Push(&PushOptions{RefSpecs: []config.RefSpec{
+ ":refs/heads/branch",
+ }})
+
+ AssertReferences(c, rSave, map[string]string{
+ "refs/remotes/origin/branch": ref.Hash().String(),
+ })
+
+ err = rSave.Fetch(&FetchOptions{Prune: true})
+ c.Assert(err, IsNil)
+
+ _, err = rSave.Reference("refs/remotes/origin/branch", true)
+ c.Assert(err, ErrorMatches, "reference not found")
+}
+
+func (s *RemoteSuite) TestFetchPruneTags(c *C) {
+ fs := fixtures.Basic().One().DotGit()
+
+ url, clean := s.TemporalDir()
+ defer clean()
+
+ _, err := PlainClone(url, true, &CloneOptions{
+ URL: fs.Root(),
+ })
+ c.Assert(err, IsNil)
+
+ dir, clean := s.TemporalDir()
+ defer clean()
+
+ r, err := PlainClone(dir, true, &CloneOptions{
+ URL: url,
+ })
+ c.Assert(err, IsNil)
+
+ remote, err := r.Remote(DefaultRemoteName)
+ c.Assert(err, IsNil)
+
+ ref, err := r.Reference(plumbing.ReferenceName("refs/heads/master"), true)
+ c.Assert(err, IsNil)
+
+ err = remote.Push(&PushOptions{RefSpecs: []config.RefSpec{
+ "refs/heads/master:refs/tags/v1",
+ }})
+ c.Assert(err, IsNil)
+
+ dirSave, clean := s.TemporalDir()
+ defer clean()
+
+ rSave, err := PlainClone(dirSave, true, &CloneOptions{
+ URL: url,
+ })
+ c.Assert(err, IsNil)
+
+ AssertReferences(c, rSave, map[string]string{
+ "refs/tags/v1": ref.Hash().String(),
+ })
+
+ err = remote.Push(&PushOptions{RefSpecs: []config.RefSpec{
+ ":refs/tags/v1",
+ }})
+
+ AssertReferences(c, rSave, map[string]string{
+ "refs/tags/v1": ref.Hash().String(),
+ })
+
+ err = rSave.Fetch(&FetchOptions{Prune: true, RefSpecs: []config.RefSpec{"refs/tags/*:refs/tags/*"}})
+ c.Assert(err, IsNil)
+
+ _, err = rSave.Reference("refs/tags/v1", true)
+ c.Assert(err, ErrorMatches, "reference not found")
+}
+
func (s *RemoteSuite) TestCanPushShasToReference(c *C) {
d, err := os.MkdirTemp("", "TestCanPushShasToReference")
c.Assert(err, IsNil)