github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/git/repo_tag.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 // Copyright 2015 The Gogs Authors. All rights reserved. 7 8 package git 9 10 import ( 11 "context" 12 "fmt" 13 "io" 14 "strings" 15 16 "github.com/gitbundle/modules/git/foreachref" 17 "github.com/gitbundle/modules/util" 18 ) 19 20 // TagPrefix tags prefix path on the repository 21 const TagPrefix = "refs/tags/" 22 23 // IsTagExist returns true if given tag exists in the repository. 24 func IsTagExist(ctx context.Context, repoPath, name string) bool { 25 return IsReferenceExist(ctx, repoPath, TagPrefix+name) 26 } 27 28 // CreateTag create one tag in the repository 29 func (repo *Repository) CreateTag(name, revision string) error { 30 _, _, err := NewCommand(repo.Ctx, "tag", "--", name, revision).RunStdString(&RunOpts{Dir: repo.Path}) 31 return err 32 } 33 34 // CreateAnnotatedTag create one annotated tag in the repository 35 func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error { 36 _, _, err := NewCommand(repo.Ctx, "tag", "-a", "-m", message, "--", name, revision).RunStdString(&RunOpts{Dir: repo.Path}) 37 return err 38 } 39 40 // GetTagNameBySHA returns the name of a tag from its tag object SHA or commit SHA 41 func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { 42 if len(sha) < 5 { 43 return "", fmt.Errorf("SHA is too short: %s", sha) 44 } 45 46 stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--tags", "-d").RunStdString(&RunOpts{Dir: repo.Path}) 47 if err != nil { 48 return "", err 49 } 50 51 tagRefs := strings.Split(stdout, "\n") 52 for _, tagRef := range tagRefs { 53 if len(strings.TrimSpace(tagRef)) > 0 { 54 fields := strings.Fields(tagRef) 55 if strings.HasPrefix(fields[0], sha) && strings.HasPrefix(fields[1], TagPrefix) { 56 name := fields[1][len(TagPrefix):] 57 // annotated tags show up twice, we should only return if is not the ^{} ref 58 if !strings.HasSuffix(name, "^{}") { 59 return name, nil 60 } 61 } 62 } 63 } 64 return "", ErrNotExist{ID: sha} 65 } 66 67 // GetTagID returns the object ID for a tag (annotated tags have both an object SHA AND a commit SHA) 68 func (repo *Repository) GetTagID(name string) (string, error) { 69 stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--tags", "--", name).RunStdString(&RunOpts{Dir: repo.Path}) 70 if err != nil { 71 return "", err 72 } 73 // Make sure exact match is used: "v1" != "release/v1" 74 for _, line := range strings.Split(stdout, "\n") { 75 fields := strings.Fields(line) 76 if len(fields) == 2 && fields[1] == "refs/tags/"+name { 77 return fields[0], nil 78 } 79 } 80 return "", ErrNotExist{ID: name} 81 } 82 83 // GetTag returns a Git tag by given name. 84 func (repo *Repository) GetTag(name string) (*Tag, error) { 85 idStr, err := repo.GetTagID(name) 86 if err != nil { 87 return nil, err 88 } 89 90 id, err := NewIDFromString(idStr) 91 if err != nil { 92 return nil, err 93 } 94 95 tag, err := repo.getTag(id, name) 96 if err != nil { 97 return nil, err 98 } 99 return tag, nil 100 } 101 102 // GetTagWithID returns a Git tag by given name and ID 103 func (repo *Repository) GetTagWithID(idStr, name string) (*Tag, error) { 104 id, err := NewIDFromString(idStr) 105 if err != nil { 106 return nil, err 107 } 108 109 tag, err := repo.getTag(id, name) 110 if err != nil { 111 return nil, err 112 } 113 return tag, nil 114 } 115 116 // GetTagInfos returns all tag infos of the repository. 117 func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { 118 forEachRefFmt := foreachref.NewFormat("objecttype", "refname:short", "object", "objectname", "creator", "contents", "contents:signature") 119 120 stdoutReader, stdoutWriter := io.Pipe() 121 defer stdoutReader.Close() 122 defer stdoutWriter.Close() 123 stderr := strings.Builder{} 124 rc := &RunOpts{Dir: repo.Path, Stdout: stdoutWriter, Stderr: &stderr} 125 126 go func() { 127 err := NewCommand(repo.Ctx, "for-each-ref", "--format", forEachRefFmt.Flag(), "--sort", "-*creatordate", "refs/tags").Run(rc) 128 if err != nil { 129 _ = stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String())) 130 } else { 131 _ = stdoutWriter.Close() 132 } 133 }() 134 135 var tags []*Tag 136 parser := forEachRefFmt.Parser(stdoutReader) 137 for { 138 ref := parser.Next() 139 if ref == nil { 140 break 141 } 142 143 tag, err := parseTagRef(ref) 144 if err != nil { 145 return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err) 146 } 147 tags = append(tags, tag) 148 } 149 if err := parser.Err(); err != nil { 150 return nil, 0, fmt.Errorf("GetTagInfos: parse output: %w", err) 151 } 152 153 sortTagsByTime(tags) 154 tagsTotal := len(tags) 155 if page != 0 { 156 tags = util.PaginateSlice(tags, page, pageSize).([]*Tag) 157 } 158 159 return tags, tagsTotal, nil 160 } 161 162 // parseTagRef parses a tag from a 'git for-each-ref'-produced reference. 163 func parseTagRef(ref map[string]string) (tag *Tag, err error) { 164 tag = &Tag{ 165 Type: ref["objecttype"], 166 Name: ref["refname:short"], 167 } 168 169 tag.ID, err = NewIDFromString(ref["objectname"]) 170 if err != nil { 171 return nil, fmt.Errorf("parse objectname '%s': %v", ref["objectname"], err) 172 } 173 174 if tag.Type == "commit" { 175 // lightweight tag 176 tag.Object = tag.ID 177 } else { 178 // annotated tag 179 tag.Object, err = NewIDFromString(ref["object"]) 180 if err != nil { 181 return nil, fmt.Errorf("parse object '%s': %v", ref["object"], err) 182 } 183 } 184 185 tag.Tagger, err = newSignatureFromCommitline([]byte(ref["creator"])) 186 if err != nil { 187 return nil, fmt.Errorf("parse tagger: %w", err) 188 } 189 190 tag.Message = ref["contents"] 191 // strip PGP signature if present in contents field 192 pgpStart := strings.Index(tag.Message, beginpgp) 193 if pgpStart >= 0 { 194 tag.Message = tag.Message[0:pgpStart] 195 } 196 197 // annotated tag with GPG signature 198 if tag.Type == "tag" && ref["contents:signature"] != "" { 199 payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n", 200 tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message)) 201 tag.Signature = &CommitGPGSignature{ 202 Signature: ref["contents:signature"], 203 Payload: payload, 204 } 205 } 206 207 return tag, nil 208 } 209 210 // GetAnnotatedTag returns a Git tag by its SHA, must be an annotated tag 211 func (repo *Repository) GetAnnotatedTag(sha string) (*Tag, error) { 212 id, err := NewIDFromString(sha) 213 if err != nil { 214 return nil, err 215 } 216 217 // Tag type must be "tag" (annotated) and not a "commit" (lightweight) tag 218 if tagType, err := repo.GetTagType(id); err != nil { 219 return nil, err 220 } else if ObjectType(tagType) != ObjectTag { 221 // not an annotated tag 222 return nil, ErrNotExist{ID: id.String()} 223 } 224 225 // Get tag name 226 name, err := repo.GetTagNameBySHA(id.String()) 227 if err != nil { 228 return nil, err 229 } 230 231 tag, err := repo.getTag(id, name) 232 if err != nil { 233 return nil, err 234 } 235 return tag, nil 236 }