code.gitea.io/gitea@v1.22.3/modules/git/repo_tag.go (about)

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