Files
gitea-MIRROR/modules/git/repo_ref.go
bircni e0b2f3425e fix(actions): run pull_request workflows on the merge commit (#40076)
`pull_request` workflow runs checked out the PR head, so a PR could pass
CI and still break the base branch once merged.

Gitea now maintains `refs/pull/<index>/merge`, a test merge of the head
into the base branch, for open pull requests. It is updated on creation
and mergeability checks and removed on close or merge.

- `pull_request` runs read their workflows from the test merge and
expose it as `github.ref`, `github.sha` and
`github.event.pull_request.merge_commit_sha`. Commit statuses, the API's
`head_sha`, `workflow_run` payloads and `paths` filters stay on the
head.
- Runs fall back to the head when the merge conflicts, the pull request
is closed or git is older than 2.40.
- Hidden refs keep each run's test merge (`refs/gitea/run/<id>`) and
merged commits (`refs/gitea/pull/<index>/merged`) from `git gc`, for
reruns and after the base branch is deleted or force-pushed.

Fixes: https://github.com/go-gitea/gitea/issues/27039
Fixes: https://github.com/go-gitea/gitea/issues/13134
Fixes: https://github.com/go-gitea/gitea/issues/30972
Docs: https://gitea.com/gitea/docs/pulls/617

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-09 12:42:40 +00:00

162 lines
4.8 KiB
Go

// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"bufio"
"context"
"io"
"strings"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
)
// GetRefs returns all references of the repository.
func (repo *Repository) GetRefs(ctx context.Context) ([]*Reference, error) {
return repo.GetRefsFiltered(ctx, "")
}
// ListOccurrences lists all refs of the given refType the given commit appears in sorted by creation date DESC
// refType should only be a literal "branch" or "tag" and nothing else
func (repo *Repository) ListOccurrences(ctx context.Context, refType, commitSHA string) ([]string, error) {
cmd := gitcmd.NewCommand()
switch refType {
case "branch":
cmd.AddArguments("branch")
case "tag":
cmd.AddArguments("tag")
default:
return nil, util.NewInvalidArgumentErrorf(`can only use "branch" or "tag" for refType, but got %q`, refType)
}
stdout, _, err := cmd.AddArguments("--no-color", "--sort=-creatordate", "--contains").
AddDynamicArguments(commitSHA).WithRepo(repo).RunStdString(ctx)
if err != nil {
return nil, err
}
refs := strings.Split(strings.TrimSpace(stdout), "\n")
if refType == "branch" {
return parseBranches(refs), nil
}
return parseTags(refs), nil
}
func parseBranches(refs []string) []string {
results := make([]string, 0, len(refs))
for _, ref := range refs {
if strings.HasPrefix(ref, "* ") { // current branch (main branch)
results = append(results, ref[len("* "):])
} else if strings.HasPrefix(ref, " ") { // all other branches
results = append(results, ref[len(" "):])
} else if ref != "" {
results = append(results, ref)
}
}
return results
}
func parseTags(refs []string) []string {
results := make([]string, 0, len(refs))
for _, ref := range refs {
if ref != "" {
results = append(results, ref)
}
}
return results
}
// UnstableGuessRefByShortName does the best guess to see whether a "short name" provided by user is a branch, tag or commit.
// It could guess wrongly if the input is already ambiguous. For example:
// * "refs/heads/the-name" vs "refs/heads/refs/heads/the-name"
// * "refs/tags/1234567890" vs commit "1234567890"
// In most cases, it SHOULD AVOID using this function, unless there is an irresistible reason (eg: make API friendly to end users)
// If the function is used, the caller SHOULD CHECK the ref type carefully.
func (repo *Repository) UnstableGuessRefByShortName(ctx context.Context, shortName string) RefName {
if repo.IsBranchExist(ctx, shortName) {
return RefNameFromBranch(shortName)
}
if repo.IsTagExist(ctx, shortName) {
return RefNameFromTag(shortName)
}
if strings.HasPrefix(shortName, "refs/") {
if repo.IsReferenceExist(ctx, shortName) {
return RefName(shortName)
}
}
commit, err := repo.GetCommit(ctx, shortName)
if err == nil {
commitIDString := commit.ID.String()
// make sure the "shortName" is either partial commit ID, or it is HEAD
if strings.HasPrefix(commitIDString, shortName) || shortName == RefNameHead {
return RefName(commitIDString)
} else {
setting.PanicInDevOrTesting("abuse of UnstableGuessRefByShortName, queried %s, got %s", shortName, commitIDString)
}
}
return ""
}
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) {
refs := make([]*Reference, 0)
cmd := gitcmd.NewCommand("for-each-ref")
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
err := cmd.WithRepo(repo).
WithPipelineFunc(func(context gitcmd.Context) error {
bufReader := bufio.NewReader(stdoutReader)
for {
// The output of for-each-ref is simply a list:
// <sha> SP <type> TAB <ref> LF
sha, err := bufReader.ReadString(' ')
if err == io.EOF {
break
}
if err != nil {
return err
}
sha = sha[:len(sha)-1]
typ, err := bufReader.ReadString('\t')
if err == io.EOF {
// This should not happen, but we'll tolerate it
break
}
if err != nil {
return err
}
typ = typ[:len(typ)-1]
refName, err := bufReader.ReadString('\n')
if err == io.EOF {
// This should not happen, but we'll tolerate it
break
}
if err != nil {
return err
}
refName = refName[:len(refName)-1]
// refName cannot be HEAD but can be remotes or stash
if strings.HasPrefix(refName, RemotePrefix) || strings.HasPrefix(refName, hiddenRefPrefix+"/") || refName == "/refs/stash" {
continue
}
if pattern == "" || strings.HasPrefix(refName, pattern) {
r := &Reference{
Name: refName,
Object: MustIDFromString(sha),
Type: typ,
repo: repo,
}
refs = append(refs, r)
}
}
return nil
}).RunWithStderr(ctx)
return refs, err
}