fix(context): find the Gitea remote among all remotes of a clone

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-18 17:16:57 +02:00
1 parent 51709abdc0
commit bae885a4d2
6 files changed
+205 -27

No files matched your search

+1 -1
View File
@@ -77,7 +77,7 @@ func TestDoctorReportsEveryCheckInsteadOfFailingFirst(t *testing.T) {
{
name: "no account for the instance of the remote", env: map[string]string{"TEA_HOST": server.URL},
statuses: "Config ok, Instance ok, Account failed, Repository skipped, Git warning",
account: "no account for " + server.URL + " (instance from " + strings.Trim(origin, "()") + "); run tea auth login --host " + server.URL + ", or use -R or --host to target another instance",
account: "no account for " + server.URL + " (instance from " + strings.Trim(origin, "()") + "); run tea auth login --host " + server.URL + ", or use -R to target another repository",
},
{
name: "token of another instance", env: map[string]string{"TEA_HOST": "https://other.example.org", "GITEA_TOKEN": "token"},
+4 -3
View File
@@ -290,9 +290,10 @@ func helpTopics(globals *pflag.FlagSet) []*cobra.Command {
` + indent(flagUsages(flags)) + `
tea selects the repository from --repo, TEA_REPO, git config tea.repository or a
Git remote (--remote, else the current branch's remote, upstream or origin), and the
instance from the repository URL, --host, TEA_HOST or GITEA_INSTANCE_URL, default_host,
or the only configured instance.
Git remote (--remote, else the current branch's remote, upstream or origin, skipping
remotes on instances without an account, or not on --host or the GITEA_TOKEN instance),
and the instance from the repository URL, --host, TEA_HOST or GITEA_INSTANCE_URL,
default_host, or the only configured instance.
Environment variables:
+63 -13
View File
@@ -228,21 +228,73 @@ func (a *application) repositorySource(ctx context.Context, t *target, s setting
return saved, nil
}
remote := cmp.Or(a.options.remote, s.Preferences.Remote)
remote, err := git.DiscoverRemote(ctx, a.gitProbe(), a.options.cwd, remote)
hosts, err := a.repositoryHosts(s.Hosts.Hosts)
if err != nil {
return "", err
}
urls := map[string]string{}
// Only remotes on an instance tea can act on name a repository, so that a clone whose origin is
// on another forge still finds its Gitea remote.
repository := func(name string) string {
raw, err := probe("remote", "get-url", name)
if err != nil {
return ""
}
urls[name] = raw
instance, slug, err := settings.RepositoryURL(raw, hosts)
if err != nil || !a.canActOn(instance, hosts) {
return ""
}
return instance + "/" + strings.ToLower(slug)
}
remote, err = git.DiscoverRemote(ctx, a.gitProbe(), a.options.cwd, remote, repository)
if errors.Is(err, git.ErrAmbiguousRemote) {
return "", clierr.Wrap(clierr.Usage, err)
}
if err != nil {
return "", clierr.Wrap(clierr.Usage, errRepositoryRequired)
}
raw, err := probe("remote", "get-url", remote)
if err != nil {
return "", fmt.Errorf("cannot read the URL of Git remote %q: %w", remote, err)
raw, known := urls[remote]
if !known {
if raw, err = probe("remote", "get-url", remote); err != nil {
return "", fmt.Errorf("cannot read the URL of Git remote %q: %w", remote, err)
}
}
t.RepoSource = "remote " + remote
return raw, nil
}
// canActOn reports whether tea can use a discovered remote on instance: it is the --host instance,
// else the GITEA_TOKEN instance while that token is set (connect then ignores stored accounts), else
// an instance with an account.
func (a *application) canActOn(instance string, hosts map[string]settings.Host) bool {
selected := a.options.host
if selected == "" && a.deps.Getenv("GITEA_TOKEN") != "" {
selected = a.tokenInstance()
}
if selected != "" {
resolved, err := settings.ResolveHost(selected, hosts)
return err == nil && resolved == instance
}
return len(hosts[instance].Users) > 0
}
// repositoryHosts returns the instances a repository URL may name: with --host, only that one when
// it is configured.
func (a *application) repositoryHosts(configured map[string]settings.Host) (map[string]settings.Host, error) {
if a.options.host == "" {
return configured, nil
}
explicit, err := settings.ResolveHost(a.options.host, configured)
if err != nil {
return nil, clierr.Wrap(clierr.Usage, err)
}
if host, exists := configured[explicit]; exists {
return map[string]settings.Host{explicit: host}, nil
}
return configured, nil
}
// agreesWithRepoFlag rejects an argument naming another repository than --repo.
func (a *application) agreesWithRepoFlag(req targetRequest, hosts map[string]settings.Host) error {
if a.options.repo == "" {
@@ -264,15 +316,9 @@ func (a *application) agreesWithRepoFlag(req targetRequest, hosts map[string]set
// selectRepository parses raw into the target's repository and, for URLs and HOST/OWNER/REPO,
// its instance. With --host, URLs match only that instance.
func (a *application) selectRepository(ctx context.Context, t *target, s settings.Snapshot, raw string) error {
configured := s.Hosts.Hosts
if a.options.host != "" {
explicit, err := settings.ResolveHost(a.options.host, configured)
if err != nil {
return clierr.Wrap(clierr.Usage, err)
}
if host, exists := configured[explicit]; exists {
configured = map[string]settings.Host{explicit: host}
}
configured, err := a.repositoryHosts(s.Hosts.Hosts)
if err != nil {
return err
}
ref, err := settings.ParseRepository(raw, configured)
if err != nil {
@@ -386,6 +432,10 @@ func (t target) accountError(user string) error {
if t.HostSource != "--host" {
text += " (instance from " + describeSource(t.HostSource) + ")"
}
// --host cannot override the instance that the repository names.
if t.RepoSource != "" && t.HostSource == t.RepoSource {
return errors.New(text + "; run tea auth login --host " + t.Host + ", or use -R to target another repository")
}
return errors.New(text + "; run tea auth login --host " + t.Host + ", or use -R or --host to target another instance")
}
+39 -1
View File
@@ -6,11 +6,13 @@ package app
import (
"context"
"encoding/json"
"maps"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing"
@@ -233,6 +235,42 @@ func TestRepositoryResolutionPrecedence(t *testing.T) {
require.False(t, runner.prompt)
}
func TestRepositoryDiscoveryFindsGiteaRemoteBesideAnotherForge(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"full_name":"` + strings.TrimPrefix(r.URL.Path, "/api/v1/repos/") + `"}`))
}))
defer server.Close()
for _, tc := range []struct {
name string
remotes map[string]string
args []string
env map[string]string
want string
}{
{"origin on GitHub", map[string]string{"origin": "https://github.com/example/project.git", "gitea": server.URL + "/alice/project.git"}, nil, nil, "alice/project"},
{"no origin", map[string]string{"github": "https://github.com/example/project.git", "gitea": server.URL + "/alice/project.git"}, nil, nil, "alice/project"},
{"--host selects its remote", map[string]string{"origin": "https://gitea.com/alice/project.git", "gitea": server.URL + "/bob/tool.git"}, []string{"--host", server.URL}, nil, "bob/tool"},
{"GITEA_TOKEN selects its instance", map[string]string{"origin": "https://gitea.com/carol/project.git", "gitea": server.URL + "/alice/project.git"}, nil, map[string]string{"GITEA_TOKEN": "token", "TEA_HOST": server.URL}, "alice/project"},
{"remotes naming one repository", map[string]string{"origin": "https://github.com/example/project.git", "gitea": server.URL + "/alice/project.git", "gitea-ssh": "git@127.0.0.1:alice/project.git"}, nil, nil, "alice/project"},
} {
t.Run(tc.name, func(t *testing.T) {
deps, out, _ := testDeps(t)
if tc.env != nil {
deps.Getenv = envFunc(tc.env)
}
configureAccounts(t, deps, server.URL, "alice", "alice")
configureAccounts(t, deps, "https://gitea.com", "carol", "carol")
results := map[string]string{"remote": strings.Join(slices.Sorted(maps.Keys(tc.remotes)), "\n")}
for name, url := range tc.remotes {
results["remote get-url "+name] = url
}
deps.Git = &fakeGit{results: results}
require.NoError(t, Execute(context.Background(), deps, append([]string{"repo", "view", "--no-input", "--json=full_name"}, tc.args...)))
require.JSONEq(t, `{"full_name":"`+tc.want+`"}`, out.String())
})
}
}
func TestRepositoryArgumentsAcceptShorthands(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"full_name":"` + strings.TrimPrefix(r.URL.Path, "/api/v1/repos/") + `"}`))
@@ -285,7 +323,7 @@ func TestAccountErrorsNameTheInstanceAndAccounts(t *testing.T) {
{"unknown --user", []string{"alice", "bob"}, "alice", []string{"-R", "alice/project", "--user", "nobody"}, nil, `account "nobody" is not logged in on https://example.org (accounts: alice, bob)`},
{"unknown --user without accounts", nil, "", []string{"-R", "alice/project", "--user", "nobody", "--host", instance}, nil, `account "nobody" is not logged in on https://example.org; run tea auth login --host https://example.org`},
{"no active account", []string{"alice", "bob"}, "", []string{"-R", "alice/project"}, nil, "no active account on https://example.org (accounts: alice, bob); pass --user or run tea auth switch"},
{"instance from a remote", []string{"alice"}, "alice", nil, remote, `no account for https://gitea.com (instance from git remote "origin"); run tea auth login --host https://gitea.com, or use -R or --host to target another instance`},
{"instance from a remote", []string{"alice"}, "alice", nil, remote, `no account for https://gitea.com (instance from git remote "origin"); run tea auth login --host https://gitea.com, or use -R to target another repository`},
} {
t.Run(tc.name, func(t *testing.T) {
deps, _, _ := testDeps(t)