Skip to content
This repository was 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 7 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
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")
)

// 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. 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 *TagObjectOptions) 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
185 changes: 181 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,153 @@ 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
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 *TagObjectOptions) (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 fetches a tag from the repository.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you avoid the word fetch here? Maybe just "returns"? I would rather avoid the possible confusion of any relation with actual git fetch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

//
// 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 {
ref, err := r.Tag(name)
if err != nil {
return err
}

obj, err := r.TagObject(ref.Hash())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Git does not delete the object even if it's a loose object. It just defers to prune as you pointed out. I think we should do as git, not just for compatibility, but also to avoid divergences with different storage implementations.

It would be nice to have a test for the Prune function that checks that if you create an annotated tag and delete the lightweight one, the annotated tag is effectively deleted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually thought I removed this, because as mentioned in the original PR message it's actually not even possible to test this with an annotated tag right now due to limitations of the in-memory storage interface.

I'll get it removed and add a test to delete an annotated tag with a Prune on the end.

if err != nil && err != plumbing.ErrObjectNotFound {
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 {
Expand Down Expand Up @@ -845,9 +1000,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