aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--object_walker.go2
-rw-r--r--options.go41
-rw-r--r--plumbing/object/tag.go15
-rw-r--r--repository.go171
-rw-r--r--repository_test.go392
-rw-r--r--worktree_commit_test.go106
6 files changed, 661 insertions, 66 deletions
diff --git a/object_walker.go b/object_walker.go
index 4cbbcca..f8b19cd 100644
--- a/object_walker.go
+++ b/object_walker.go
@@ -94,6 +94,8 @@ func (p *objectWalker) walkObjectTree(hash plumbing.Hash) error {
return err
}
}
+ case *object.Tag:
+ return p.walkObjectTree(obj.Target)
default:
// Error out on unhandled object types.
return fmt.Errorf("Unknown object %X %s %T\n", obj.ID(), obj.Type(), obj)
diff --git a/options.go b/options.go
index 7b1570f..b572770 100644
--- a/options.go
+++ b/options.go
@@ -3,6 +3,7 @@ package git
import (
"errors"
"regexp"
+ "strings"
"golang.org/x/crypto/openpgp"
"gopkg.in/src-d/go-git.v4/config"
@@ -348,8 +349,9 @@ type CommitOptions struct {
// Parents are the parents commits for the new commit, by default when
// len(Parents) is zero, the hash of HEAD reference is used.
Parents []plumbing.Hash
- // A key to sign the commit with. A nil value here means the commit will not
- // be signed. The private key must be present and already decrypted.
+ // SignKey denotes a key to sign the commit with. A nil value here means the
+ // commit will not be signed. The private key must be present and already
+ // decrypted.
SignKey *openpgp.Entity
}
@@ -377,6 +379,41 @@ func (o *CommitOptions) Validate(r *Repository) error {
return nil
}
+var (
+ ErrMissingName = errors.New("name field is required")
+ ErrMissingTagger = errors.New("tagger field is required")
+ ErrMissingMessage = errors.New("message field is required")
+)
+
+// CreateTagOptions describes how a tag object should be created.
+type CreateTagOptions struct {
+ // Tagger defines the signature of the tag creator.
+ Tagger *object.Signature
+ // Message defines the annotation of the tag. It is canonicalized during
+ // validation into the format expected by git - no leading whitespace and
+ // ending in a newline.
+ Message string
+ // SignKey denotes a key to sign the tag with. A nil value here means the tag
+ // will not be signed. The private key must be present and already decrypted.
+ SignKey *openpgp.Entity
+}
+
+// Validate validates the fields and sets the default values.
+func (o *CreateTagOptions) Validate(r *Repository, hash plumbing.Hash) error {
+ if o.Tagger == nil {
+ return ErrMissingTagger
+ }
+
+ if o.Message == "" {
+ return ErrMissingMessage
+ }
+
+ // Canonicalize the message into the expected message format.
+ o.Message = strings.TrimSpace(o.Message) + "\n"
+
+ return nil
+}
+
// ListOptions describes how a remote list should be performed.
type ListOptions struct {
// Auth credentials, if required, to use with the remote repository.
diff --git a/plumbing/object/tag.go b/plumbing/object/tag.go
index 905206b..03749f9 100644
--- a/plumbing/object/tag.go
+++ b/plumbing/object/tag.go
@@ -195,13 +195,14 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
return err
}
- if t.PGPSignature != "" && includeSig {
- // Split all the signature lines and write with a newline at the end.
- lines := strings.Split(t.PGPSignature, "\n")
- for _, line := range lines {
- if _, err = fmt.Fprintf(w, "%s\n", line); err != nil {
- return err
- }
+ // Note that this is highly sensitive to what it sent along in the message.
+ // Message *always* needs to end with a newline, or else the message and the
+ // signature will be concatenated into a corrupt object. Since this is a
+ // lower-level method, we assume you know what you are doing and have already
+ // done the needful on the message in the caller.
+ if includeSig {
+ if _, err = fmt.Fprint(w, t.PGPSignature); err != nil {
+ return err
}
}
diff --git a/repository.go b/repository.go
index bfe06a3..be1f057 100644
--- a/repository.go
+++ b/repository.go
@@ -1,15 +1,18 @@
package git
import (
+ "bytes"
"context"
"errors"
"fmt"
stdioutil "io/ioutil"
"os"
+ "path"
"path/filepath"
"strings"
"time"
+ "golang.org/x/crypto/openpgp"
"gopkg.in/src-d/go-git.v4/config"
"gopkg.in/src-d/go-git.v4/internal/revision"
"gopkg.in/src-d/go-git.v4/plumbing"
@@ -32,7 +35,12 @@ var (
// ErrBranchExists an error stating the specified branch already exists
ErrBranchExists = errors.New("branch already exists")
// ErrBranchNotFound an error stating the specified branch does not exist
- ErrBranchNotFound = errors.New("branch not found")
+ ErrBranchNotFound = errors.New("branch not found")
+ // ErrTagExists an error stating the specified tag already exists
+ ErrTagExists = errors.New("tag already exists")
+ // ErrTagNotFound an error stating the specified tag does not exist
+ ErrTagNotFound = errors.New("tag not found")
+
ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
ErrRepositoryNotExists = errors.New("repository does not exist")
ErrRepositoryAlreadyExists = errors.New("repository already exists")
@@ -478,6 +486,139 @@ func (r *Repository) DeleteBranch(name string) error {
return r.Storer.SetConfig(cfg)
}
+// CreateTag creates a tag. If opts is included, the tag is an annotated tag,
+// otherwise a lightweight tag is created.
+func (r *Repository) CreateTag(name string, hash plumbing.Hash, opts *CreateTagOptions) (*plumbing.Reference, error) {
+ rname := plumbing.ReferenceName(path.Join("refs", "tags", name))
+
+ _, err := r.Storer.Reference(rname)
+ switch err {
+ case nil:
+ // Tag exists, this is an error
+ return nil, ErrTagExists
+ case plumbing.ErrReferenceNotFound:
+ // Tag missing, available for creation, pass this
+ default:
+ // Some other error
+ return nil, err
+ }
+
+ var target plumbing.Hash
+ if opts != nil {
+ target, err = r.createTagObject(name, hash, opts)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ target = hash
+ }
+
+ ref := plumbing.NewHashReference(rname, target)
+ if err = r.Storer.SetReference(ref); err != nil {
+ return nil, err
+ }
+
+ return ref, nil
+}
+
+func (r *Repository) createTagObject(name string, hash plumbing.Hash, opts *CreateTagOptions) (plumbing.Hash, error) {
+ if err := opts.Validate(r, hash); err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ rawobj, err := object.GetObject(r.Storer, hash)
+ if err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ tag := &object.Tag{
+ Name: name,
+ Tagger: *opts.Tagger,
+ Message: opts.Message,
+ TargetType: rawobj.Type(),
+ Target: hash,
+ }
+
+ if opts.SignKey != nil {
+ sig, err := r.buildTagSignature(tag, opts.SignKey)
+ if err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ tag.PGPSignature = sig
+ }
+
+ obj := r.Storer.NewEncodedObject()
+ if err := tag.Encode(obj); err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ return r.Storer.SetEncodedObject(obj)
+}
+
+func (r *Repository) buildTagSignature(tag *object.Tag, signKey *openpgp.Entity) (string, error) {
+ encoded := &plumbing.MemoryObject{}
+ if err := tag.Encode(encoded); err != nil {
+ return "", err
+ }
+
+ rdr, err := encoded.Reader()
+ if err != nil {
+ return "", err
+ }
+
+ var b bytes.Buffer
+ if err := openpgp.ArmoredDetachSign(&b, signKey, rdr, nil); err != nil {
+ return "", err
+ }
+
+ return b.String(), nil
+}
+
+// Tag returns a tag from the repository.
+//
+// If you want to check to see if the tag is an annotated tag, you can call
+// TagObject on the hash of the reference in ForEach:
+//
+// ref, err := r.Tag("v0.1.0")
+// if err != nil {
+// // Handle error
+// }
+//
+// obj, err := r.TagObject(ref.Hash())
+// switch err {
+// case nil:
+// // Tag object present
+// case plumbing.ErrObjectNotFound:
+// // Not a tag object
+// default:
+// // Some other error
+// }
+//
+func (r *Repository) Tag(name string) (*plumbing.Reference, error) {
+ ref, err := r.Reference(plumbing.ReferenceName(path.Join("refs", "tags", name)), false)
+ if err != nil {
+ if err == plumbing.ErrReferenceNotFound {
+ // Return a friendly error for this one, versus just ReferenceNotFound.
+ return nil, ErrTagNotFound
+ }
+
+ return nil, err
+ }
+
+ return ref, nil
+}
+
+// DeleteTag deletes a tag from the repository.
+func (r *Repository) DeleteTag(name string) error {
+ _, err := r.Tag(name)
+ if err != nil {
+ return err
+ }
+
+ return r.Storer.RemoveReference(plumbing.ReferenceName(path.Join("refs", "tags", name)))
+}
+
func (r *Repository) resolveToCommitHash(h plumbing.Hash) (plumbing.Hash, error) {
obj, err := r.Storer.EncodedObject(plumbing.AnyObject, h)
if err != nil {
@@ -839,9 +980,31 @@ func (r *Repository) Log(o *LogOptions) (object.CommitIter, error) {
return nil, fmt.Errorf("invalid Order=%v", o.Order)
}
-// Tags returns all the References from Tags. This method returns only lightweight
-// tags. Note that not all the tags are lightweight ones. To return annotated tags
-// too, you need to call TagObjects() method.
+// Tags returns all the tag References in a repository.
+//
+// If you want to check to see if the tag is an annotated tag, you can call
+// TagObject on the hash Reference passed in through ForEach:
+//
+// iter, err := r.Tags()
+// if err != nil {
+// // Handle error
+// }
+//
+// if err := iter.ForEach(func (ref *plumbing.Reference) error {
+// obj, err := r.TagObject(ref.Hash())
+// switch err {
+// case nil:
+// // Tag object present
+// case plumbing.ErrObjectNotFound:
+// // Not a tag object
+// default:
+// // Some other error
+// return err
+// }
+// }); err != nil {
+// // Handle outer iterator error
+// }
+//
func (r *Repository) Tags() (storer.ReferenceIter, error) {
refIter, err := r.Storer.IterReferences()
if err != nil {
diff --git a/repository_test.go b/repository_test.go
index 88071cf..e48f42d 100644
--- a/repository_test.go
+++ b/repository_test.go
@@ -13,6 +13,9 @@ import (
"testing"
"time"
+ "golang.org/x/crypto/openpgp"
+ "golang.org/x/crypto/openpgp/armor"
+ openpgperr "golang.org/x/crypto/openpgp/errors"
"gopkg.in/src-d/go-git.v4/config"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/cache"
@@ -1273,6 +1276,395 @@ func (s *RepositorySuite) TestTags(c *C) {
c.Assert(count, Equals, 5)
}
+func (s *RepositorySuite) TestCreateTagLightweight(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ expected, err := r.Head()
+ c.Assert(err, IsNil)
+
+ ref, err := r.CreateTag("foobar", expected.Hash(), nil)
+ c.Assert(err, IsNil)
+ c.Assert(ref, NotNil)
+
+ actual, err := r.Tag("foobar")
+ c.Assert(err, IsNil)
+
+ c.Assert(expected.Hash(), Equals, actual.Hash())
+}
+
+func (s *RepositorySuite) TestCreateTagLightweightExists(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ expected, err := r.Head()
+ c.Assert(err, IsNil)
+
+ ref, err := r.CreateTag("lightweight-tag", expected.Hash(), nil)
+ c.Assert(ref, IsNil)
+ c.Assert(err, Equals, ErrTagExists)
+}
+
+func (s *RepositorySuite) TestCreateTagAnnotated(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ h, err := r.Head()
+ c.Assert(err, IsNil)
+
+ expectedHash := h.Hash()
+
+ ref, err := r.CreateTag("foobar", expectedHash, &CreateTagOptions{
+ Tagger: defaultSignature(),
+ Message: "foo bar baz qux",
+ })
+ c.Assert(err, IsNil)
+
+ tag, err := r.Tag("foobar")
+ c.Assert(err, IsNil)
+
+ obj, err := r.TagObject(tag.Hash())
+ c.Assert(err, IsNil)
+
+ c.Assert(ref, DeepEquals, tag)
+ c.Assert(obj.Hash, Equals, ref.Hash())
+ c.Assert(obj.Type(), Equals, plumbing.TagObject)
+ c.Assert(obj.Target, Equals, expectedHash)
+}
+
+func (s *RepositorySuite) TestCreateTagAnnotatedBadOpts(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ h, err := r.Head()
+ c.Assert(err, IsNil)
+
+ expectedHash := h.Hash()
+
+ ref, err := r.CreateTag("foobar", expectedHash, &CreateTagOptions{
+ Message: "foo bar baz qux",
+ })
+ c.Assert(ref, IsNil)
+ c.Assert(err, Equals, ErrMissingTagger)
+
+ ref, err = r.CreateTag("foobar", expectedHash, &CreateTagOptions{
+ Tagger: defaultSignature(),
+ })
+ c.Assert(ref, IsNil)
+ c.Assert(err, Equals, ErrMissingMessage)
+}
+
+func (s *RepositorySuite) TestCreateTagAnnotatedBadHash(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ ref, err := r.CreateTag("foobar", plumbing.ZeroHash, &CreateTagOptions{
+ Tagger: defaultSignature(),
+ Message: "foo bar baz qux",
+ })
+ c.Assert(ref, IsNil)
+ c.Assert(err, Equals, plumbing.ErrObjectNotFound)
+}
+
+func (s *RepositorySuite) TestCreateTagSigned(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ h, err := r.Head()
+ c.Assert(err, IsNil)
+
+ key := commitSignKey(c, true)
+ _, err = r.CreateTag("foobar", h.Hash(), &CreateTagOptions{
+ Tagger: defaultSignature(),
+ Message: "foo bar baz qux",
+ SignKey: key,
+ })
+ c.Assert(err, IsNil)
+
+ tag, err := r.Tag("foobar")
+ c.Assert(err, IsNil)
+
+ obj, err := r.TagObject(tag.Hash())
+ c.Assert(err, IsNil)
+
+ // Verify the tag.
+ pks := new(bytes.Buffer)
+ pkw, err := armor.Encode(pks, openpgp.PublicKeyType, nil)
+ c.Assert(err, IsNil)
+
+ err = key.Serialize(pkw)
+ c.Assert(err, IsNil)
+ err = pkw.Close()
+ c.Assert(err, IsNil)
+
+ actual, err := obj.Verify(pks.String())
+ c.Assert(err, IsNil)
+ c.Assert(actual.PrimaryKey, DeepEquals, key.PrimaryKey)
+}
+
+func (s *RepositorySuite) TestCreateTagSignedBadKey(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ h, err := r.Head()
+ c.Assert(err, IsNil)
+
+ key := commitSignKey(c, false)
+ _, err = r.CreateTag("foobar", h.Hash(), &CreateTagOptions{
+ Tagger: defaultSignature(),
+ Message: "foo bar baz qux",
+ SignKey: key,
+ })
+ c.Assert(err, Equals, openpgperr.InvalidArgumentError("signing key is encrypted"))
+}
+
+func (s *RepositorySuite) TestCreateTagCanonicalize(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ h, err := r.Head()
+ c.Assert(err, IsNil)
+
+ key := commitSignKey(c, true)
+ _, err = r.CreateTag("foobar", h.Hash(), &CreateTagOptions{
+ Tagger: defaultSignature(),
+ Message: "\n\nfoo bar baz qux\n\nsome message here",
+ SignKey: key,
+ })
+ c.Assert(err, IsNil)
+
+ tag, err := r.Tag("foobar")
+ c.Assert(err, IsNil)
+
+ obj, err := r.TagObject(tag.Hash())
+ c.Assert(err, IsNil)
+
+ // Assert the new canonicalized message.
+ c.Assert(obj.Message, Equals, "foo bar baz qux\n\nsome message here\n")
+
+ // Verify the tag.
+ pks := new(bytes.Buffer)
+ pkw, err := armor.Encode(pks, openpgp.PublicKeyType, nil)
+ c.Assert(err, IsNil)
+
+ err = key.Serialize(pkw)
+ c.Assert(err, IsNil)
+ err = pkw.Close()
+ c.Assert(err, IsNil)
+
+ actual, err := obj.Verify(pks.String())
+ c.Assert(err, IsNil)
+ c.Assert(actual.PrimaryKey, DeepEquals, key.PrimaryKey)
+}
+
+func (s *RepositorySuite) TestTagLightweight(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ expected := plumbing.NewHash("f7b877701fbf855b44c0a9e86f3fdce2c298b07f")
+
+ tag, err := r.Tag("lightweight-tag")
+ c.Assert(err, IsNil)
+
+ actual := tag.Hash()
+ c.Assert(expected, Equals, actual)
+}
+
+func (s *RepositorySuite) TestTagLightweightMissingTag(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ tag, err := r.Tag("lightweight-tag-tag")
+ c.Assert(tag, IsNil)
+ c.Assert(err, Equals, ErrTagNotFound)
+}
+
+func (s *RepositorySuite) TestDeleteTag(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ err = r.DeleteTag("lightweight-tag")
+ c.Assert(err, IsNil)
+
+ _, err = r.Tag("lightweight-tag")
+ c.Assert(err, Equals, ErrTagNotFound)
+}
+
+func (s *RepositorySuite) TestDeleteTagMissingTag(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ r, _ := Init(memory.NewStorage(), nil)
+ err := r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ err = r.DeleteTag("lightweight-tag-tag")
+ c.Assert(err, Equals, ErrTagNotFound)
+}
+
+func (s *RepositorySuite) TestDeleteTagAnnotated(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ dir, err := ioutil.TempDir("", "go-git-test-deletetag-annotated")
+ c.Assert(err, IsNil)
+
+ defer os.RemoveAll(dir) // clean up
+
+ fss, err := filesystem.NewStorage(osfs.New(dir))
+ c.Assert(err, IsNil)
+
+ r, _ := Init(fss, nil)
+ err = r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ ref, err := r.Tag("annotated-tag")
+ c.Assert(ref, NotNil)
+ c.Assert(err, IsNil)
+
+ obj, err := r.TagObject(ref.Hash())
+ c.Assert(obj, NotNil)
+ c.Assert(err, IsNil)
+
+ err = r.DeleteTag("annotated-tag")
+ c.Assert(err, IsNil)
+
+ _, err = r.Tag("annotated-tag")
+ c.Assert(err, Equals, ErrTagNotFound)
+
+ // Run a prune (and repack, to ensure that we are GCing everything regardless
+ // of the fixture in use) and try to get the tag object again.
+ //
+ // The repo needs to be re-opened after the repack.
+ err = r.Prune(PruneOptions{Handler: r.DeleteObject})
+ c.Assert(err, IsNil)
+
+ err = r.RepackObjects(&RepackConfig{})
+ c.Assert(err, IsNil)
+
+ r, err = PlainOpen(dir)
+ c.Assert(r, NotNil)
+ c.Assert(err, IsNil)
+
+ // Now check to see if the GC was effective in removing the tag object.
+ obj, err = r.TagObject(ref.Hash())
+ c.Assert(obj, IsNil)
+ c.Assert(err, Equals, plumbing.ErrObjectNotFound)
+}
+
+func (s *RepositorySuite) TestDeleteTagAnnotatedUnpacked(c *C) {
+ url := s.GetLocalRepositoryURL(
+ fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(),
+ )
+
+ dir, err := ioutil.TempDir("", "go-git-test-deletetag-annotated-unpacked")
+ c.Assert(err, IsNil)
+
+ defer os.RemoveAll(dir) // clean up
+
+ fss, err := filesystem.NewStorage(osfs.New(dir))
+ c.Assert(err, IsNil)
+
+ r, _ := Init(fss, nil)
+ err = r.clone(context.Background(), &CloneOptions{URL: url})
+ c.Assert(err, IsNil)
+
+ // Create a tag for the deletion test. This ensures that the ultimate loose
+ // object will be unpacked (as we aren't doing anything that should pack it),
+ // so that we can effectively test that a prune deletes it, without having to
+ // resort to a repack.
+ h, err := r.Head()
+ c.Assert(err, IsNil)
+
+ expectedHash := h.Hash()
+
+ ref, err := r.CreateTag("foobar", expectedHash, &CreateTagOptions{
+ Tagger: defaultSignature(),
+ Message: "foo bar baz qux",
+ })
+ c.Assert(err, IsNil)
+
+ tag, err := r.Tag("foobar")
+ c.Assert(err, IsNil)
+
+ obj, err := r.TagObject(tag.Hash())
+ c.Assert(obj, NotNil)
+ c.Assert(err, IsNil)
+
+ err = r.DeleteTag("foobar")
+ c.Assert(err, IsNil)
+
+ _, err = r.Tag("foobar")
+ c.Assert(err, Equals, ErrTagNotFound)
+
+ // As mentioned, only run a prune. We are not testing for packed objects
+ // here.
+ err = r.Prune(PruneOptions{Handler: r.DeleteObject})
+ c.Assert(err, IsNil)
+
+ // Now check to see if the GC was effective in removing the tag object.
+ obj, err = r.TagObject(ref.Hash())
+ c.Assert(obj, IsNil)
+ c.Assert(err, Equals, plumbing.ErrObjectNotFound)
+}
+
func (s *RepositorySuite) TestBranches(c *C) {
f := fixtures.ByURL("https://github.com/git-fixtures/root-references.git").One()
sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault())
diff --git a/worktree_commit_test.go b/worktree_commit_test.go
index 62aae8a..da377c6 100644
--- a/worktree_commit_test.go
+++ b/worktree_commit_test.go
@@ -310,59 +310,59 @@ func commitSignKey(c *C, decrypt bool) *openpgp.Entity {
const armoredKeyRing = `
-----BEGIN PGP PRIVATE KEY BLOCK-----
-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
-=8g3t
+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
+=gIM9
-----END PGP PRIVATE KEY BLOCK-----
`