1package git23import (4 "bytes"5 "context"6 "fmt"7 "strings"8)910// parseTag parses tag information from the (uncompressed) raw data of the tag11// object. It assumes "\n\n" separates the header from the rest of the message.12func parseTag(data []byte) (*Tag, error) {13 tag := new(Tag)14 // we now have the contents of the commit object. Let's investigate.15 nextline := 016l:17 for {18 eol := bytes.IndexByte(data[nextline:], '\n')19 switch {20 case eol > 0:21 line := data[nextline : nextline+eol]22 spacepos := bytes.IndexByte(line, ' ')23 reftype := line[:spacepos]24 switch string(reftype) {25 case "object":26 id, err := NewIDFromString(string(line[spacepos+1:]))27 if err != nil {28 return nil, err29 }30 tag.commitID = id31 case "type":32 case "tagger":33 sig, err := parseSignature(line[spacepos+1:])34 if err != nil {35 return nil, err36 }37 tag.tagger = sig38 }39 nextline += eol + 140 case eol == 0:41 tag.message = string(data[nextline+1:])42 break l43 default:44 break l45 }46 }47 return tag, nil48}4950// getTag returns a tag by given object id.51func (r *Repository) getTag(ctx context.Context, id Oid) (*Tag, error) {52 t, ok := r.cachedTags.Get(id.String())53 if ok {54 logf("Cached tag hit: %s", id)55 return t.(*Tag), nil56 }5758 // Check tag type59 typ, err := r.CatFileType(ctx, id.String())60 if err != nil {61 return nil, err62 }6364 var tag *Tag65 switch typ {66 case ObjectCommit: // Tag is a commit67 tag = &Tag{68 typ: ObjectCommit,69 id: id,70 commitID: id,71 repo: r,72 }7374 case ObjectTag: // Tag is an annotation75 data, err := exec(ctx, r.path, []string{"cat-file", "-p", id.String()}, nil)76 if err != nil {77 return nil, err78 }7980 tag, err = parseTag(data)81 if err != nil {82 return nil, err83 }84 tag.typ = ObjectTag85 tag.id = id86 tag.repo = r87 default:88 return nil, fmt.Errorf("unsupported tag type: %s", typ)89 }9091 r.cachedTags.Set(id.String(), tag)92 return tag, nil93}9495// TagOptions contains optional arguments for getting a tag.96//97// Docs: https://git-scm.com/docs/git-cat-file98type TagOptions struct {99 CommandOptions100}101102// Tag returns a Git tag by given name, e.g. "v1.0.0".103func (r *Repository) Tag(ctx context.Context, name string, opts ...TagOptions) (*Tag, error) {104 var opt TagOptions105 if len(opts) > 0 {106 opt = opts[0]107 }108109 refspec := RefsTags + name110 refs, err := r.ShowRef(ctx, ShowRefOptions{111 Tags: true,112 Patterns: []string{refspec},113 CommandOptions: opt.CommandOptions,114 })115 if err != nil {116 return nil, err117 } else if len(refs) == 0 {118 return nil, ErrReferenceNotExist119 }120121 id, err := NewIDFromString(refs[0].ID)122 if err != nil {123 return nil, err124 }125126 tag, err := r.getTag(ctx, id)127 if err != nil {128 return nil, err129 }130 tag.refspec = refspec131 return tag, nil132}133134// TagsOptions contains optional arguments for listing tags.135//136// Docs: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---list137type TagsOptions struct {138 // SortKet sorts tags with provided tag key, optionally prefixed with '-' to sort tags in descending order.139 SortKey string140 // Pattern filters tags matching the specified pattern.141 Pattern string142 CommandOptions143}144145// Tags returns a list of tags of the repository.146func (r *Repository) Tags(ctx context.Context, opts ...TagsOptions) ([]string, error) {147 var opt TagsOptions148 if len(opts) > 0 {149 opt = opts[0]150 }151152 args := []string{"tag", "--list"}153 if opt.SortKey != "" {154 args = append(args, "--sort="+opt.SortKey)155 } else {156 args = append(args, "--sort=-creatordate")157 }158 args = append(args, "--end-of-options")159 if opt.Pattern != "" {160 args = append(args, opt.Pattern)161 }162163 stdout, err := exec(ctx, r.path, args, opt.Envs)164 if err != nil {165 return nil, err166 }167168 tags := strings.Split(string(stdout), "\n")169 tags = tags[:len(tags)-1]170171 return tags, nil172}173174// CreateTagOptions contains optional arguments for creating a tag.175//176// Docs: https://git-scm.com/docs/git-tag177type CreateTagOptions struct {178 // Annotated marks a tag as annotated rather than lightweight.179 Annotated bool180 // Message specifies a tagging message for the annotated tag. It is ignored when tag is not annotated.181 Message string182 // Author is the author of the tag. It is ignored when tag is not annotated.183 Author *Signature184 CommandOptions185}186187// CreateTag creates a new tag on given revision.188func (r *Repository) CreateTag(ctx context.Context, name, rev string, opts ...CreateTagOptions) error {189 var opt CreateTagOptions190 if len(opts) > 0 {191 opt = opts[0]192 }193194 args := []string{"tag"}195 var envs []string196 if opt.Annotated {197 args = append(args, "-a", name)198 args = append(args, "--message", opt.Message)199 if opt.Author != nil {200 envs = committerEnvs(opt.Author)201 }202 args = append(args, "--end-of-options")203 } else {204 args = append(args, "--end-of-options", name)205 }206 args = append(args, rev)207 envs = append(envs, opt.Envs...)208 _, err := exec(ctx, r.path, args, envs)209 return err210}211212// DeleteTagOptions contains optional arguments for deleting a tag.213//214// Docs: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---delete215type DeleteTagOptions struct {216 CommandOptions217}218219// DeleteTag deletes a tag from the repository.220func (r *Repository) DeleteTag(ctx context.Context, name string, opts ...DeleteTagOptions) error {221 var opt DeleteTagOptions222 if len(opts) > 0 {223 opt = opts[0]224 }225226 args := []string{"tag", "--delete", "--end-of-options", name}227 _, err := exec(ctx, r.path, args, opt.Envs)228 return err229}