Files
gitea-MIRROR/modules/git/ref.go
wxiaoguang 772f365254 refactor: use RunWithStderr for git commands (#40295)
If the stderr output is not handled, then it needs to use RunWithStderr
to make sure the git error message is included in the returned error.
Otherwise, when the git command fails, the error log won't show useful
information.

Ref:

* #39795 : no useful information in log messages
*
https://github.com/go-gitea/gitea/pull/40160#pullrequestreview-5752720866
2026-11-09 17:59:52 +00:00

269 lines
7.1 KiB
Go

// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"strconv"
"strings"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
)
const (
// RemotePrefix is the base directory of the remotes information of git.
RemotePrefix = "refs/remotes/"
// PullPrefix is the base directory of the pull information of git.
PullPrefix = "refs/pull/"
pullSuffix = "/head"
hiddenRefPrefix = "refs/gitea"
)
// IsValidRefPattern ensures that the provided string could be a valid reference
func IsValidRefPattern(name string) bool {
return !globalVars().refNamePatternInvalid.MatchString(name)
}
func SanitizeRefPattern(name string) string {
return globalVars().refNamePatternInvalid.ReplaceAllString(name, "_")
}
// Reference represents a Git ref.
type Reference struct {
Name string
repo *Repository
Object ObjectID // The id of this commit object
Type string
}
// Commit return the commit of the reference
func (ref *Reference) Commit(ctx context.Context) (*Commit, error) {
return ref.repo.getCommit(ctx, ref.Object)
}
// ShortName returns the short name of the reference
func (ref *Reference) ShortName() string {
return RefName(ref.Name).ShortName()
}
// RefGroup returns the group type of the reference
func (ref *Reference) RefGroup() string {
return RefName(ref.Name).RefGroup()
}
// ForPrefix special ref to create a pull request: refs/for/<target-branch>/<topic-branch>
// or refs/for/<targe-branch> -o topic='<topic-branch>'
const ForPrefix = "refs/for/"
// TODO: /refs/for-review for suggest change interface
// RefName represents a full git reference name
type RefName string
const RefNameHead = "HEAD"
func RefNameFromBranch(shortName string) RefName {
return RefName(BranchPrefix + shortName)
}
func RefNameFromTag(shortName string) RefName {
return RefName(TagPrefix + shortName)
}
func RefNameFromCommit(shortName string) RefName {
if !isStringLowerHex(shortName) {
setting.PanicInDevOrTesting("BUG! invalid commit id %s", shortName)
return RefName("refs/invalid-commit/" + shortName)
}
return RefName(shortName)
}
func RefNameFromPullIndex(prIndex int64) RefName {
return RefName(PullPrefix + strconv.FormatInt(prIndex, 10) + pullSuffix)
}
// MergeRefNameFromPullIndex is the test merge of an open pull request
func MergeRefNameFromPullIndex(prIndex int64) RefName {
return RefName(PullPrefix + strconv.FormatInt(prIndex, 10) + "/merge")
}
// MergedRefNameFromPullIndex keeps the merged commit after the base branch is deleted or rewritten
func MergedRefNameFromPullIndex(prIndex int64) RefName {
return RefName(hiddenRefPrefix + "/pull/" + strconv.FormatInt(prIndex, 10) + "/merged")
}
// RunRefNameFromRunID keeps a run's test merge after refs/pull/<index>/merge moved on
func RunRefNameFromRunID(runID int64) RefName {
return RefName(hiddenRefPrefix + "/run/" + strconv.FormatInt(runID, 10))
}
func (ref RefName) String() string {
return string(ref)
}
func (ref RefName) IsBranch() bool {
return strings.HasPrefix(string(ref), BranchPrefix)
}
func (ref RefName) IsTag() bool {
return strings.HasPrefix(string(ref), TagPrefix)
}
func (ref RefName) IsRemote() bool {
return strings.HasPrefix(string(ref), RemotePrefix)
}
func (ref RefName) IsPull() bool {
return strings.HasPrefix(string(ref), PullPrefix) && strings.IndexByte(string(ref)[len(PullPrefix):], '/') > -1
}
func (ref RefName) IsFor() bool {
return strings.HasPrefix(string(ref), ForPrefix)
}
func (ref RefName) nameWithoutPrefix(prefix string) string {
if after, ok := strings.CutPrefix(string(ref), prefix); ok {
return after
}
return ""
}
// TagName returns simple tag name if it's an operation to a tag
func (ref RefName) TagName() string {
return ref.nameWithoutPrefix(TagPrefix)
}
// BranchName returns simple branch name if it's an operation to branch
func (ref RefName) BranchName() string {
return ref.nameWithoutPrefix(BranchPrefix)
}
func (ref RefName) PullIndex() (int64, bool) {
refName := string(ref)
s, ok := strings.CutPrefix(refName, PullPrefix)
if !ok {
return 0, false
}
pullStr, last, ok := strings.CutLast(s, "/")
if !ok || last != "head" {
return 0, false
}
pullIndex, err := strconv.ParseInt(pullStr, 10, 64)
if err != nil {
return 0, false
}
return pullIndex, true
}
// ForBranchName returns the branch name part of refs like refs/for/<branch_name>
func (ref RefName) ForBranchName() string {
return ref.nameWithoutPrefix(ForPrefix)
}
func (ref RefName) RemoteName() string {
return ref.nameWithoutPrefix(RemotePrefix)
}
// ShortName returns the short name of the reference name
func (ref RefName) ShortName() string {
if ref.IsBranch() {
return ref.BranchName()
}
if ref.IsTag() {
return ref.TagName()
}
if ref.IsRemote() {
return ref.RemoteName()
}
if ref.IsPull() {
return strings.TrimSuffix(ref.nameWithoutPrefix(PullPrefix), pullSuffix)
}
if ref.IsFor() {
return ref.ForBranchName()
}
return string(ref) // usually it is a commit ID, or "HEAD"
}
// RefGroup returns the group type of the reference
// Using the name of the directory under .git/refs
func (ref RefName) RefGroup() string {
if ref.IsBranch() {
return "heads"
}
if ref.IsTag() {
return "tags"
}
if ref.IsRemote() {
return "remotes"
}
if ref.IsPull() {
return "pull"
}
if ref.IsFor() {
return "for"
}
return ""
}
// RefType is a simple ref type of the reference, it is used for UI and webhooks
type RefType string
const (
RefTypeBranch RefType = "branch"
RefTypeTag RefType = "tag"
RefTypeCommit RefType = "commit"
)
// RefType returns the simple ref type of the reference, e.g. branch, tag
// It's different from RefGroup, which is using the name of the directory under .git/refs
func (ref RefName) RefType() RefType {
switch {
case ref.IsBranch():
return RefTypeBranch
case ref.IsTag():
return RefTypeTag
case IsStringValidObjectID(nil, string(ref), 6):
return RefTypeCommit
}
return ""
}
// RefWebLinkPath returns a path for the reference that can be used in a web link:
// * "branch/<branch_name>"
// * "tag/<tag_name>"
// * "commit/<commit_id>"
// It returns an empty string if the reference is not a branch, tag or commit.
func (ref RefName) RefWebLinkPath() string {
refType := ref.RefType()
if refType == "" {
return ""
}
return string(refType) + "/" + util.PathEscapeSegments(ref.ShortName())
}
func ParseRefSuffix(ref string) (refName, refSuffix string) {
// Partially support https://git-scm.com/docs/gitrevisions
suffixIdx := -1 // earliest suffix mark, so a combined suffix like "main~2^" stays intact
for _, mark := range []string{"@{", "^", "~"} {
if idx := strings.Index(ref, mark); idx != -1 && (suffixIdx == -1 || idx < suffixIdx) {
suffixIdx = idx
}
}
if suffixIdx == -1 {
return ref, ""
}
return ref[:suffixIdx], ref[suffixIdx:]
}
func UpdateRef(ctx context.Context, repo RepositoryFacade, refName, newCommitID string) error {
return gitcmd.NewCommand("update-ref").AddDynamicArguments(refName, newCommitID).WithRepo(repo).RunWithStderr(ctx)
}
func RemoveRef(ctx context.Context, repo RepositoryFacade, refName string) error {
return gitcmd.NewCommand("update-ref", "--no-deref", "-d").AddDynamicArguments(refName).WithRepo(repo).RunWithStderr(ctx)
}