Skip to content
This repository has been archived by the owner on Sep 11, 2020. It is now read-only.

git: Add tagging support #928

Merged
merged 14 commits into from
Sep 10, 2018
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions object_walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 39 additions & 2 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package git
import (
"errors"
"regexp"
"strings"

"golang.org/x/crypto/openpgp"
"gopkg.in/src-d/go-git.v4/config"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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.
Expand Down
15 changes: 8 additions & 7 deletions plumbing/object/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
171 changes: 167 additions & 4 deletions repository.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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")
Expand Down Expand Up @@ -484,6 +492,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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified with plain git that if the tag object creation succeeds and the lightweight creation fails, it does not try to clean up and just leaves a dangling tag object behind. So we're good here (I had doubts initially).

}
} 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 {
Expand Down Expand Up @@ -845,9 +986,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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the correction, this was, indeed, confusing at least!

//
// 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 {
Expand Down
Loading