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  }