mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-09-01 17:35:08 +02:00
This PR follows #21535 (and replace #22592) ## Review without space diff https://github.com/go-gitea/gitea/pull/22678/files?diff=split&w=1 ## Purpose of this PR 1. Make git module command completely safe (risky user inputs won't be passed as argument option anymore) 2. Avoid low-level mistakes like https://github.com/go-gitea/gitea/pull/22098#discussion_r1045234918 3. Remove deprecated and dirty `CmdArgCheck` function, hide the `CmdArg` type 4. Simplify code when using git command ## The main idea of this PR * Move the `git.CmdArg` to the `internal` package, then no other package except `git` could use it. Then developers could never do `AddArguments(git.CmdArg(userInput))` any more. * Introduce `git.ToTrustedCmdArgs`, it's for user-provided and already trusted arguments. It's only used in a few cases, for example: use git arguments from config file, help unit test with some arguments. * Introduce `AddOptionValues` and `AddOptionFormat`, they make code more clear and simple: * Before: `AddArguments("-m").AddDynamicArguments(message)` * After: `AddOptionValues("-m", message)` * - * Before: `AddArguments(git.CmdArg(fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email)))` * After: `AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)` ## FAQ ### Why these changes were not done in #21535 ? #21535 is mainly a search&replace, it did its best to not change too much logic. Making the framework better needs a lot of changes, so this separate PR is needed as the second step. ### The naming of `AddOptionXxx` According to git's manual, the `--xxx` part is called `option`. ### How can it guarantee that `internal.CmdArg` won't be not misused? Go's specification guarantees that. Trying to access other package's internal package causes compilation error. And, `golangci-lint` also denies the git/internal package. Only the `git/command.go` can use it carefully. ### There is still a `ToTrustedCmdArgs`, will it still allow developers to make mistakes and pass untrusted arguments? Generally speaking, no. Because when using `ToTrustedCmdArgs`, the code will be very complex (see the changes for examples). Then developers and reviewers can know that something might be unreasonable. ### Why there was a `CmdArgCheck` and why it's removed? At the moment of #21535, to reduce unnecessary changes, `CmdArgCheck` was introduced as a hacky patch. Now, almost all code could be written as `cmd := NewCommand(); cmd.AddXxx(...)`, then there is no need for `CmdArgCheck` anymore. ### Why many codes for `signArg == ""` is deleted? Because in the old code, `signArg` could never be empty string, it's either `-S[key-id]` or `--no-gpg-sign`. So the `signArg == ""` is just dead code. --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
235 lines
6.4 KiB
Go
235 lines
6.4 KiB
Go
// Copyright 2015 The Gogs Authors. All rights reserved.
|
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package git
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/modules/git/foreachref"
|
|
"code.gitea.io/gitea/modules/util"
|
|
)
|
|
|
|
// TagPrefix tags prefix path on the repository
|
|
const TagPrefix = "refs/tags/"
|
|
|
|
// IsTagExist returns true if given tag exists in the repository.
|
|
func IsTagExist(ctx context.Context, repoPath, name string) bool {
|
|
return IsReferenceExist(ctx, repoPath, TagPrefix+name)
|
|
}
|
|
|
|
// CreateTag create one tag in the repository
|
|
func (repo *Repository) CreateTag(name, revision string) error {
|
|
_, _, err := NewCommand(repo.Ctx, "tag").AddDashesAndList(name, revision).RunStdString(&RunOpts{Dir: repo.Path})
|
|
return err
|
|
}
|
|
|
|
// CreateAnnotatedTag create one annotated tag in the repository
|
|
func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error {
|
|
_, _, err := NewCommand(repo.Ctx, "tag", "-a", "-m").AddDynamicArguments(message).AddDashesAndList(name, revision).RunStdString(&RunOpts{Dir: repo.Path})
|
|
return err
|
|
}
|
|
|
|
// GetTagNameBySHA returns the name of a tag from its tag object SHA or commit SHA
|
|
func (repo *Repository) GetTagNameBySHA(sha string) (string, error) {
|
|
if len(sha) < 5 {
|
|
return "", fmt.Errorf("SHA is too short: %s", sha)
|
|
}
|
|
|
|
stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--tags", "-d").RunStdString(&RunOpts{Dir: repo.Path})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
tagRefs := strings.Split(stdout, "\n")
|
|
for _, tagRef := range tagRefs {
|
|
if len(strings.TrimSpace(tagRef)) > 0 {
|
|
fields := strings.Fields(tagRef)
|
|
if strings.HasPrefix(fields[0], sha) && strings.HasPrefix(fields[1], TagPrefix) {
|
|
name := fields[1][len(TagPrefix):]
|
|
// annotated tags show up twice, we should only return if is not the ^{} ref
|
|
if !strings.HasSuffix(name, "^{}") {
|
|
return name, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return "", ErrNotExist{ID: sha}
|
|
}
|
|
|
|
// GetTagID returns the object ID for a tag (annotated tags have both an object SHA AND a commit SHA)
|
|
func (repo *Repository) GetTagID(name string) (string, error) {
|
|
stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--tags").AddDashesAndList(name).RunStdString(&RunOpts{Dir: repo.Path})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Make sure exact match is used: "v1" != "release/v1"
|
|
for _, line := range strings.Split(stdout, "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) == 2 && fields[1] == "refs/tags/"+name {
|
|
return fields[0], nil
|
|
}
|
|
}
|
|
return "", ErrNotExist{ID: name}
|
|
}
|
|
|
|
// GetTag returns a Git tag by given name.
|
|
func (repo *Repository) GetTag(name string) (*Tag, error) {
|
|
idStr, err := repo.GetTagID(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id, err := NewIDFromString(idStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tag, err := repo.getTag(id, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tag, nil
|
|
}
|
|
|
|
// GetTagWithID returns a Git tag by given name and ID
|
|
func (repo *Repository) GetTagWithID(idStr, name string) (*Tag, error) {
|
|
id, err := NewIDFromString(idStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tag, err := repo.getTag(id, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tag, nil
|
|
}
|
|
|
|
// GetTagInfos returns all tag infos of the repository.
|
|
func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
|
|
forEachRefFmt := foreachref.NewFormat("objecttype", "refname:short", "object", "objectname", "creator", "contents", "contents:signature")
|
|
|
|
stdoutReader, stdoutWriter := io.Pipe()
|
|
defer stdoutReader.Close()
|
|
defer stdoutWriter.Close()
|
|
stderr := strings.Builder{}
|
|
rc := &RunOpts{Dir: repo.Path, Stdout: stdoutWriter, Stderr: &stderr}
|
|
|
|
go func() {
|
|
err := NewCommand(repo.Ctx, "for-each-ref").
|
|
AddOptionFormat("--format=%s", forEachRefFmt.Flag()).
|
|
AddArguments("--sort", "-*creatordate", "refs/tags").Run(rc)
|
|
if err != nil {
|
|
_ = stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String()))
|
|
} else {
|
|
_ = stdoutWriter.Close()
|
|
}
|
|
}()
|
|
|
|
var tags []*Tag
|
|
parser := forEachRefFmt.Parser(stdoutReader)
|
|
for {
|
|
ref := parser.Next()
|
|
if ref == nil {
|
|
break
|
|
}
|
|
|
|
tag, err := parseTagRef(ref)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err)
|
|
}
|
|
tags = append(tags, tag)
|
|
}
|
|
if err := parser.Err(); err != nil {
|
|
return nil, 0, fmt.Errorf("GetTagInfos: parse output: %w", err)
|
|
}
|
|
|
|
sortTagsByTime(tags)
|
|
tagsTotal := len(tags)
|
|
if page != 0 {
|
|
tags = util.PaginateSlice(tags, page, pageSize).([]*Tag)
|
|
}
|
|
|
|
return tags, tagsTotal, nil
|
|
}
|
|
|
|
// parseTagRef parses a tag from a 'git for-each-ref'-produced reference.
|
|
func parseTagRef(ref map[string]string) (tag *Tag, err error) {
|
|
tag = &Tag{
|
|
Type: ref["objecttype"],
|
|
Name: ref["refname:short"],
|
|
}
|
|
|
|
tag.ID, err = NewIDFromString(ref["objectname"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse objectname '%s': %w", ref["objectname"], err)
|
|
}
|
|
|
|
if tag.Type == "commit" {
|
|
// lightweight tag
|
|
tag.Object = tag.ID
|
|
} else {
|
|
// annotated tag
|
|
tag.Object, err = NewIDFromString(ref["object"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse object '%s': %w", ref["object"], err)
|
|
}
|
|
}
|
|
|
|
tag.Tagger, err = newSignatureFromCommitline([]byte(ref["creator"]))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse tagger: %w", err)
|
|
}
|
|
|
|
tag.Message = ref["contents"]
|
|
// strip PGP signature if present in contents field
|
|
pgpStart := strings.Index(tag.Message, beginpgp)
|
|
if pgpStart >= 0 {
|
|
tag.Message = tag.Message[0:pgpStart]
|
|
}
|
|
|
|
// annotated tag with GPG signature
|
|
if tag.Type == "tag" && ref["contents:signature"] != "" {
|
|
payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n",
|
|
tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message))
|
|
tag.Signature = &CommitGPGSignature{
|
|
Signature: ref["contents:signature"],
|
|
Payload: payload,
|
|
}
|
|
}
|
|
|
|
return tag, nil
|
|
}
|
|
|
|
// GetAnnotatedTag returns a Git tag by its SHA, must be an annotated tag
|
|
func (repo *Repository) GetAnnotatedTag(sha string) (*Tag, error) {
|
|
id, err := NewIDFromString(sha)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Tag type must be "tag" (annotated) and not a "commit" (lightweight) tag
|
|
if tagType, err := repo.GetTagType(id); err != nil {
|
|
return nil, err
|
|
} else if ObjectType(tagType) != ObjectTag {
|
|
// not an annotated tag
|
|
return nil, ErrNotExist{ID: id.String()}
|
|
}
|
|
|
|
// Get tag name
|
|
name, err := repo.GetTagNameBySHA(id.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tag, err := repo.getTag(id, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tag, nil
|
|
}
|