aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris Marchesi <chrism@vancluevertech.com>2018-08-18 11:03:33 -0700
committerChris Marchesi <chrism@vancluevertech.com>2018-08-21 17:43:29 -0700
commitb9f5efe3dbee0cd15a553bcc03f7a37f3dcaa676 (patch)
tree3aca261e608995102d63b8898f1e19c11177562e
parent7b6c1266556f59ac436fada3fa6106d4a84f9b56 (diff)
downloadgo-git-b9f5efe3dbee0cd15a553bcc03f7a37f3dcaa676.tar.gz
git: Add tagging support
This adds a few methods: * CreateTag, which can be used to create both lightweight and annotated tags with a supplied TagObjectOptions struct. PGP signing is possible as well. * Tag, to fetch a single tag ref. As opposed to Tags or TagObjects, this will also fetch the tag object if it exists and return it along with the output. Lightweight tags just return the object as nil. * DeleteTag, to delete a tag. This simply deletes the ref. The object is left orphaned to be GCed later. I'm not 100% sure if DeleteTag is the correct behavior - looking for details on exactly *what* happens to a tag object if you delete the ref and not the tag were sparse, and groking the Git source did not really produce much insight to the untrained eye. This may be something that comes up in review. If deletion of the object is necessary, the in-memory storer may require some updates to allow DeleteLooseObject to be supported. Signed-off-by: Chris Marchesi <chrism@vancluevertech.com>
-rw-r--r--options.go47
-rw-r--r--repository.go134
-rw-r--r--repository_test.go265
3 files changed, 443 insertions, 3 deletions
diff --git a/options.go b/options.go
index 7b1570f..6b00b0d 100644
--- a/options.go
+++ b/options.go
@@ -348,8 +348,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 +378,48 @@ 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")
+ ErrBadObjectType = errors.New("bad object type for tagging")
+)
+
+// TagObjectOptions describes how a tag object should be created.
+type TagObjectOptions struct {
+ // Tagger defines the signature of the tag creator.
+ Tagger *object.Signature
+ // Message defines the annotation of the tag.
+ Message string
+ // TargetType is the object type of the target. The object specified by
+ // Target must be of this type.
+ TargetType plumbing.ObjectType
+ // 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 *TagObjectOptions) Validate(r *Repository, hash plumbing.Hash) error {
+ if o.Tagger == nil {
+ return ErrMissingTagger
+ }
+
+ if o.Message == "" {
+ return ErrMissingMessage
+ }
+
+ if o.TargetType == plumbing.InvalidObject || o.TargetType == plumbing.AnyObject {
+ return ErrBadObjectType
+ }
+
+ if _, err := r.Object(o.TargetType, hash); err != nil {
+ return err
+ }
+
+ 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/repository.go b/repository.go
index 818cfb3..ab14eba 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"
@@ -31,7 +34,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")
@@ -484,6 +492,130 @@ 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 *TagObjectOptions) (*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 *TagObjectOptions) (plumbing.Hash, error) {
+ if err := opts.Validate(r, hash); err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ tag := &object.Tag{
+ Name: name,
+ Tagger: *opts.Tagger,
+ Message: opts.Message,
+ TargetType: opts.TargetType,
+ 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 fetches a tag from the repository. The tag is returned as a raw
+// reference. If the tag is annotated, a non-nil tag object is returned.
+func (r *Repository) Tag(name string) (*plumbing.Reference, *object.Tag, 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, nil, ErrTagNotFound
+ }
+
+ return nil, nil, err
+ }
+
+ obj, err := r.TagObject(ref.Hash())
+ if err != nil && err != plumbing.ErrObjectNotFound {
+ return nil, nil, err
+ }
+
+ return ref, obj, nil
+}
+
+// DeleteTag deletes a tag from the repository.
+func (r *Repository) DeleteTag(name string) error {
+ _, obj, err := r.Tag(name)
+ if err != nil {
+ return err
+ }
+
+ if err = r.Storer.RemoveReference(plumbing.ReferenceName(path.Join("refs", "tags", name))); err != nil {
+ return err
+ }
+
+ // Delete the tag object if this was an annotated tag.
+ if obj != nil {
+ return r.DeleteObject(obj.Hash)
+ }
+
+ return nil
+}
+
func (r *Repository) resolveToCommitHash(h plumbing.Hash) (plumbing.Hash, error) {
obj, err := r.Storer.EncodedObject(plumbing.AnyObject, h)
if err != nil {
diff --git a/repository_test.go b/repository_test.go
index 261af7a..4fd26a1 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/object"
@@ -1275,6 +1278,268 @@ 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, obj, err := r.Tag("foobar")
+ c.Assert(err, IsNil)
+ c.Assert(obj, 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, &TagObjectOptions{
+ Tagger: defaultSignature(),
+ Message: "foo bar baz qux",
+ TargetType: plumbing.CommitObject,
+ })
+ c.Assert(err, IsNil)
+
+ tag, obj, err := r.Tag("foobar")
+ c.Assert(err, IsNil)
+ c.Assert(obj, NotNil)
+
+ 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, &TagObjectOptions{
+ Message: "foo bar baz qux",
+ TargetType: plumbing.CommitObject,
+ })
+ c.Assert(ref, IsNil)
+ c.Assert(err, Equals, ErrMissingTagger)
+
+ ref, err = r.CreateTag("foobar", expectedHash, &TagObjectOptions{
+ Tagger: defaultSignature(),
+ TargetType: plumbing.CommitObject,
+ })
+ c.Assert(ref, IsNil)
+ c.Assert(err, Equals, ErrMissingMessage)
+
+ ref, err = r.CreateTag("foobar", expectedHash, &TagObjectOptions{
+ Tagger: defaultSignature(),
+ Message: "foo bar baz qux",
+ })
+ c.Assert(ref, IsNil)
+ c.Assert(err, Equals, ErrBadObjectType)
+
+ ref, err = r.CreateTag("foobar", expectedHash, &TagObjectOptions{
+ Tagger: defaultSignature(),
+ Message: "foo bar baz qux",
+ TargetType: plumbing.TagObject,
+ })
+ 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(), &TagObjectOptions{
+ Tagger: defaultSignature(),
+ Message: "foo bar baz qux",
+ TargetType: plumbing.CommitObject,
+ SignKey: key,
+ })
+ c.Assert(err, IsNil)
+
+ _, obj, err := r.Tag("foobar")
+ c.Assert(err, IsNil)
+ c.Assert(obj, NotNil)
+
+ // 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(), &TagObjectOptions{
+ Tagger: defaultSignature(),
+ Message: "foo bar baz qux",
+ TargetType: plumbing.CommitObject,
+ SignKey: key,
+ })
+ c.Assert(err, Equals, openpgperr.InvalidArgumentError("signing key is encrypted"))
+}
+
+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, obj, err := r.Tag("lightweight-tag")
+ c.Assert(err, IsNil)
+ c.Assert(obj, 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, obj, err := r.Tag("lightweight-tag-tag")
+ c.Assert(tag, IsNil)
+ c.Assert(obj, IsNil)
+ c.Assert(err, Equals, ErrTagNotFound)
+}
+
+func (s *RepositorySuite) TestTagAnnotated(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, obj, err := r.Tag("annotated-tag")
+ c.Assert(err, IsNil)
+ c.Assert(obj, NotNil)
+
+ expectedHash := plumbing.NewHash("b742a2a9fa0afcfa9a6fad080980fbc26b007c69")
+ expectedTarget := plumbing.NewHash("f7b877701fbf855b44c0a9e86f3fdce2c298b07f")
+ actualHash := tag.Hash()
+ c.Assert(expectedHash, Equals, actualHash)
+ c.Assert(obj.Hash, Equals, expectedHash)
+ c.Assert(obj.Type(), Equals, plumbing.TagObject)
+ c.Assert(obj.Target, Equals, expectedTarget)
+}
+
+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) TestBranches(c *C) {
f := fixtures.ByURL("https://github.com/git-fixtures/root-references.git").One()
sto, err := filesystem.NewStorage(f.DotGit())