feat(app): parse issue, pull request and run references

resolveRef accepts N, #N, [HOST/]OWNER/REPO#N and issue, pull request or run
URLs; a URL or qualified number selects its repository for the invocation and
must agree with --repo. resolveRefs does the same for several references.

pullSelector also accepts a head branch or OWNER:BRANCH and, without an
argument, finds the pull request of the checked-out branch: the one pr
checkout recorded in branch.<name>.tea-pr, else the open pull request whose
head is the branch's upstream in the upstream remote's repository, else one
whose head has the local branch's name, else N of a pr/N branch. This lets pr
commands find the pull request of a branch pr checkout created.

target.expandMe resolves @me in user-valued flags to the login tea acts as,
and git.CurrentBranchConfig reads the branch configuration involved.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 22:48:50 +02:00
1 parent 2ef6090734
commit 1505df2bb1
4 files changed
+621

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"errors"
"strconv"
"strings"
)
// ErrNotOnBranch reports a detached HEAD or a directory outside a checkout.
var ErrNotOnBranch = errors.New("not on a branch")
// BranchConfig is the configuration of a local branch that locates its pull request.
type BranchConfig struct {
// Name is the local branch.
Name string
// Remote and Merge are the remote and branch the branch tracks, Merge without refs/heads/;
// both are empty for a branch that tracks nothing or another local branch.
Remote, Merge string
// PullNumber and BaseRemote are what pr checkout recorded: the pull request the branch was
// checked out from and the remote of that pull request's base repository.
PullNumber int64
BaseRemote string
}
// CurrentBranchConfig reads the configuration of the checked-out branch, failing with
// ErrNotOnBranch when HEAD is detached or dir is not a checkout.
func CurrentBranchConfig(ctx context.Context, runner Runner, dir string) (BranchConfig, error) {
name, err := runner.Output(ctx, dir, false, "symbolic-ref", "--short", "HEAD")
if err != nil || name == "" {
return BranchConfig{}, ErrNotOnBranch
}
return ReadBranchConfig(ctx, runner, dir, name), nil
}
// ReadBranchConfig reads branch.<name>.remote, .merge and the keys pr checkout records.
// Missing or malformed keys stay empty.
func ReadBranchConfig(ctx context.Context, runner Runner, dir, name string) BranchConfig {
get := func(key string) string {
value, err := runner.Output(ctx, dir, false, "config", "--get", "branch."+name+"."+key)
if err != nil {
return ""
}
return value
}
config := BranchConfig{Name: name, Remote: get("remote"), BaseRemote: get(pullBaseRemoteKey)}
if config.Remote == "." {
config.Remote = ""
}
if config.Remote != "" {
config.Merge = strings.TrimPrefix(get("merge"), "refs/heads/")
}
if n, err := strconv.ParseInt(get(pullNumberKey), 10, 64); err == nil && n > 0 {
config.PullNumber = n
}
return config
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestCurrentBranchConfig(t *testing.T) {
const head = "symbolic-ref --short HEAD"
for _, test := range []struct {
name string
runner discoverRunner
want BranchConfig
err error
}{
{name: "detached", runner: discoverRunner{}, err: ErrNotOnBranch},
{name: "untracked", runner: discoverRunner{head: "topic"}, want: BranchConfig{Name: "topic"}},
{name: "tracking", runner: discoverRunner{
head: "feat/one", "config --get branch.feat/one.remote": "origin", "config --get branch.feat/one.merge": "refs/heads/feat/one",
}, want: BranchConfig{Name: "feat/one", Remote: "origin", Merge: "feat/one"}},
{name: "local upstream", runner: discoverRunner{
head: "topic", "config --get branch.topic.remote": ".", "config --get branch.topic.merge": "refs/heads/main",
}, want: BranchConfig{Name: "topic"}},
{name: "pr checkout", runner: discoverRunner{
head: "pr/17", "config --get branch.pr/17.remote": "pulls/bob/project", "config --get branch.pr/17.merge": "refs/heads/bob/fix",
"config --get branch.pr/17.tea-pr": "17", "config --get branch.pr/17.tea-base-remote": "origin",
}, want: BranchConfig{Name: "pr/17", Remote: "pulls/bob/project", Merge: "bob/fix", PullNumber: 17, BaseRemote: "origin"}},
{name: "malformed pull number", runner: discoverRunner{head: "x", "config --get branch.x.tea-pr": "seventeen"}, want: BranchConfig{Name: "x"}},
} {
t.Run(test.name, func(t *testing.T) {
config, err := CurrentBranchConfig(context.Background(), test.runner, "/work")
require.ErrorIs(t, err, test.err)
require.Equal(t, test.want, config)
})
}
}