-
Notifications
You must be signed in to change notification settings - Fork 534
git: Add tagging support #928
Changes from 7 commits
b9f5efe
9b73a3e
8c3c8b3
2c2b532
8d8d4c5
119459a
01631f0
6b3f46b
1000bc0
b19b3b8
9cc654c
f8adfff
bf0593d
9ce4eea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
|
@@ -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,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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 { | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
Uh oh!
There was an error while loading. Please reload this page.