1
1
mirror of https://github.com/go-gitea/gitea synced 2025-09-10 10:48:28 +00:00

check user and repo for redirects when using git via SSH transport (#35416)

fixes #30565 

When using git with a gitea hosted repository, the HTTP-Transport did
honor the user and repository redirects, which are created when renaming
a user or repo and also when transferring ownership of a repo to a
different organization. This is extremely helpful, as repo URLs remain
stable and do not have to be migrated on each client's worktree and
other places, e.g. CI at once.

The SSH transport - which I favor - did not know of these redirections
and I implemented a lookup during the `serv` command.
This commit is contained in:
koalajoe23
2025-09-09 22:13:41 +02:00
committed by GitHub
parent b9efbe9fe6
commit 2802f96e97
4 changed files with 76 additions and 5 deletions

View File

@@ -229,11 +229,6 @@ func runServ(ctx context.Context, c *cli.Command) error {
username := repoPathFields[0]
reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
// LowerCase and trim the repoPath as that's how they are stored.
// This should be done after splitting the repoPath into username and reponame
// so that username and reponame are not affected.
repoPath = strings.ToLower(strings.TrimSpace(repoPath))
if !repo.IsValidSSHAccessRepoName(reponame) {
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
}
@@ -280,6 +275,11 @@ func runServ(ctx context.Context, c *cli.Command) error {
return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error)
}
// LowerCase and trim the repoPath as that's how they are stored.
// This should be done after splitting the repoPath into username and reponame
// so that username and reponame are not affected.
repoPath = strings.ToLower(results.OwnerName + "/" + results.RepoName + ".git")
// LFS SSH protocol
if verb == git.CmdVerbLfsTransfer {
token, err := getLFSAuthToken(ctx, lfsVerb, results)

View File

@@ -2,3 +2,7 @@
id: 1
lower_name: olduser1
redirect_user_id: 1
-
id: 2
lower_name: olduser2
redirect_user_id: 2

View File

@@ -108,6 +108,18 @@ func ServCommand(ctx *context.PrivateContext) {
results.RepoName = repoName[:len(repoName)-5]
}
// Check if there is a user redirect for the requested owner
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
if err == nil {
owner, err := user_model.GetUserByID(ctx, redirectedUserID)
if err == nil {
log.Info("User %s has been redirected to %s", results.OwnerName, owner.Name)
results.OwnerName = owner.Name
} else {
log.Warn("User %s has a redirect to user with ID %d, but no user with this ID could be found. Trying without redirect...", results.OwnerName, redirectedUserID)
}
}
owner, err := user_model.GetUserByName(ctx, results.OwnerName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
@@ -131,6 +143,19 @@ func ServCommand(ctx *context.PrivateContext) {
return
}
redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName)
if err == nil {
redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID)
if err == nil {
log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name)
results.RepoName = redirectedRepo.Name
results.OwnerName = redirectedRepo.OwnerName
owner.ID = redirectedRepo.OwnerID
} else {
log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID)
}
}
// Now get the Repository and set the results section
repoExist := true
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName)

View File

@@ -0,0 +1,42 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
)
func TestGitSSHRedirect(t *testing.T) {
onGiteaRun(t, testGitSSHRedirect)
}
func testGitSSHRedirect(t *testing.T, u *url.URL) {
apiTestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
withKeyFile(t, "my-testing-key", func(keyFile string) {
t.Run("CreateUserKey", doAPICreateUserKey(apiTestContext, "test-key", keyFile))
testCases := []struct {
testName string
userName string
repoName string
}{
{"Test untouched", "user2", "repo1"},
{"Test renamed user", "olduser2", "repo1"},
{"Test renamed repo", "user2", "oldrepo1"},
{"Test renamed user and repo", "olduser2", "oldrepo1"},
}
for _, tc := range testCases {
t.Run(tc.testName, func(t *testing.T) {
cloneURL := createSSHUrl(fmt.Sprintf("%s/%s.git", tc.userName, tc.repoName), u)
t.Run("Clone", doGitClone(t.TempDir(), cloneURL))
})
}
})
}