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

Commit b9f5efe

Browse files
committed
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>
1 parent 7b6c126 commit b9f5efe

File tree

3 files changed

+443
-3
lines changed

3 files changed

+443
-3
lines changed

options.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,9 @@ type CommitOptions struct {
348348
// Parents are the parents commits for the new commit, by default when
349349
// len(Parents) is zero, the hash of HEAD reference is used.
350350
Parents []plumbing.Hash
351-
// A key to sign the commit with. A nil value here means the commit will not
352-
// be signed. The private key must be present and already decrypted.
351+
// SignKey denotes a key to sign the commit with. A nil value here means the
352+
// commit will not be signed. The private key must be present and already
353+
// decrypted.
353354
SignKey *openpgp.Entity
354355
}
355356

@@ -377,6 +378,48 @@ func (o *CommitOptions) Validate(r *Repository) error {
377378
return nil
378379
}
379380

381+
var (
382+
ErrMissingName = errors.New("name field is required")
383+
ErrMissingTagger = errors.New("tagger field is required")
384+
ErrMissingMessage = errors.New("message field is required")
385+
ErrBadObjectType = errors.New("bad object type for tagging")
386+
)
387+
388+
// TagObjectOptions describes how a tag object should be created.
389+
type TagObjectOptions struct {
390+
// Tagger defines the signature of the tag creator.
391+
Tagger *object.Signature
392+
// Message defines the annotation of the tag.
393+
Message string
394+
// TargetType is the object type of the target. The object specified by
395+
// Target must be of this type.
396+
TargetType plumbing.ObjectType
397+
// SignKey denotes a key to sign the tag with. A nil value here means the tag
398+
// will not be signed. The private key must be present and already decrypted.
399+
SignKey *openpgp.Entity
400+
}
401+
402+
// Validate validates the fields and sets the default values.
403+
func (o *TagObjectOptions) Validate(r *Repository, hash plumbing.Hash) error {
404+
if o.Tagger == nil {
405+
return ErrMissingTagger
406+
}
407+
408+
if o.Message == "" {
409+
return ErrMissingMessage
410+
}
411+
412+
if o.TargetType == plumbing.InvalidObject || o.TargetType == plumbing.AnyObject {
413+
return ErrBadObjectType
414+
}
415+
416+
if _, err := r.Object(o.TargetType, hash); err != nil {
417+
return err
418+
}
419+
420+
return nil
421+
}
422+
380423
// ListOptions describes how a remote list should be performed.
381424
type ListOptions struct {
382425
// Auth credentials, if required, to use with the remote repository.

repository.go

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package git
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
78
stdioutil "io/ioutil"
89
"os"
10+
"path"
911
"path/filepath"
1012
"strings"
1113
"time"
1214

15+
"golang.org/x/crypto/openpgp"
1316
"gopkg.in/src-d/go-git.v4/config"
1417
"gopkg.in/src-d/go-git.v4/internal/revision"
1518
"gopkg.in/src-d/go-git.v4/plumbing"
@@ -31,7 +34,12 @@ var (
3134
// ErrBranchExists an error stating the specified branch already exists
3235
ErrBranchExists = errors.New("branch already exists")
3336
// ErrBranchNotFound an error stating the specified branch does not exist
34-
ErrBranchNotFound = errors.New("branch not found")
37+
ErrBranchNotFound = errors.New("branch not found")
38+
// ErrTagExists an error stating the specified tag already exists
39+
ErrTagExists = errors.New("tag already exists")
40+
// ErrTagNotFound an error stating the specified tag does not exist
41+
ErrTagNotFound = errors.New("tag not found")
42+
3543
ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
3644
ErrRepositoryNotExists = errors.New("repository does not exist")
3745
ErrRepositoryAlreadyExists = errors.New("repository already exists")
@@ -484,6 +492,130 @@ func (r *Repository) DeleteBranch(name string) error {
484492
return r.Storer.SetConfig(cfg)
485493
}
486494

495+
// CreateTag creates a tag. If opts is included, the tag is an annotated tag,
496+
// otherwise a lightweight tag is created.
497+
func (r *Repository) CreateTag(name string, hash plumbing.Hash, opts *TagObjectOptions) (*plumbing.Reference, error) {
498+
rname := plumbing.ReferenceName(path.Join("refs", "tags", name))
499+
500+
_, err := r.Storer.Reference(rname)
501+
switch err {
502+
case nil:
503+
// Tag exists, this is an error
504+
return nil, ErrTagExists
505+
case plumbing.ErrReferenceNotFound:
506+
// Tag missing, available for creation, pass this
507+
default:
508+
// Some other error
509+
return nil, err
510+
}
511+
512+
var target plumbing.Hash
513+
if opts != nil {
514+
target, err = r.createTagObject(name, hash, opts)
515+
if err != nil {
516+
return nil, err
517+
}
518+
} else {
519+
target = hash
520+
}
521+
522+
ref := plumbing.NewHashReference(rname, target)
523+
if err = r.Storer.SetReference(ref); err != nil {
524+
return nil, err
525+
}
526+
527+
return ref, nil
528+
}
529+
530+
func (r *Repository) createTagObject(name string, hash plumbing.Hash, opts *TagObjectOptions) (plumbing.Hash, error) {
531+
if err := opts.Validate(r, hash); err != nil {
532+
return plumbing.ZeroHash, err
533+
}
534+
535+
tag := &object.Tag{
536+
Name: name,
537+
Tagger: *opts.Tagger,
538+
Message: opts.Message,
539+
TargetType: opts.TargetType,
540+
Target: hash,
541+
}
542+
543+
if opts.SignKey != nil {
544+
sig, err := r.buildTagSignature(tag, opts.SignKey)
545+
if err != nil {
546+
return plumbing.ZeroHash, err
547+
}
548+
549+
tag.PGPSignature = sig
550+
}
551+
552+
obj := r.Storer.NewEncodedObject()
553+
if err := tag.Encode(obj); err != nil {
554+
return plumbing.ZeroHash, err
555+
}
556+
557+
return r.Storer.SetEncodedObject(obj)
558+
}
559+
560+
func (r *Repository) buildTagSignature(tag *object.Tag, signKey *openpgp.Entity) (string, error) {
561+
encoded := &plumbing.MemoryObject{}
562+
if err := tag.Encode(encoded); err != nil {
563+
return "", err
564+
}
565+
566+
rdr, err := encoded.Reader()
567+
if err != nil {
568+
return "", err
569+
}
570+
571+
var b bytes.Buffer
572+
if err := openpgp.ArmoredDetachSign(&b, signKey, rdr, nil); err != nil {
573+
return "", err
574+
}
575+
576+
return b.String(), nil
577+
}
578+
579+
// Tag fetches a tag from the repository. The tag is returned as a raw
580+
// reference. If the tag is annotated, a non-nil tag object is returned.
581+
func (r *Repository) Tag(name string) (*plumbing.Reference, *object.Tag, error) {
582+
ref, err := r.Reference(plumbing.ReferenceName(path.Join("refs", "tags", name)), false)
583+
if err != nil {
584+
if err == plumbing.ErrReferenceNotFound {
585+
// Return a friendly error for this one, versus just ReferenceNotFound.
586+
return nil, nil, ErrTagNotFound
587+
}
588+
589+
return nil, nil, err
590+
}
591+
592+
obj, err := r.TagObject(ref.Hash())
593+
if err != nil && err != plumbing.ErrObjectNotFound {
594+
return nil, nil, err
595+
}
596+
597+
return ref, obj, nil
598+
}
599+
600+
// DeleteTag deletes a tag from the repository.
601+
func (r *Repository) DeleteTag(name string) error {
602+
_, obj, err := r.Tag(name)
603+
if err != nil {
604+
return err
605+
}
606+
607+
if err = r.Storer.RemoveReference(plumbing.ReferenceName(path.Join("refs", "tags", name))); err != nil {
608+
return err
609+
}
610+
611+
// Delete the tag object if this was an annotated tag.
612+
if obj != nil {
613+
return r.DeleteObject(obj.Hash)
614+
}
615+
616+
return nil
617+
}
618+
487619
func (r *Repository) resolveToCommitHash(h plumbing.Hash) (plumbing.Hash, error) {
488620
obj, err := r.Storer.EncodedObject(plumbing.AnyObject, h)
489621
if err != nil {

0 commit comments

Comments
 (0)