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:
10
cmd/serv.go
10
cmd/serv.go
@@ -229,11 +229,6 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
|||||||
username := repoPathFields[0]
|
username := repoPathFields[0]
|
||||||
reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
|
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) {
|
if !repo.IsValidSSHAccessRepoName(reponame) {
|
||||||
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", 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)
|
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
|
// LFS SSH protocol
|
||||||
if verb == git.CmdVerbLfsTransfer {
|
if verb == git.CmdVerbLfsTransfer {
|
||||||
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
||||||
|
@@ -2,3 +2,7 @@
|
|||||||
id: 1
|
id: 1
|
||||||
lower_name: olduser1
|
lower_name: olduser1
|
||||||
redirect_user_id: 1
|
redirect_user_id: 1
|
||||||
|
-
|
||||||
|
id: 2
|
||||||
|
lower_name: olduser2
|
||||||
|
redirect_user_id: 2
|
||||||
|
@@ -108,6 +108,18 @@ func ServCommand(ctx *context.PrivateContext) {
|
|||||||
results.RepoName = repoName[:len(repoName)-5]
|
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)
|
owner, err := user_model.GetUserByName(ctx, results.OwnerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if user_model.IsErrUserNotExist(err) {
|
if user_model.IsErrUserNotExist(err) {
|
||||||
@@ -131,6 +143,19 @@ func ServCommand(ctx *context.PrivateContext) {
|
|||||||
return
|
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
|
// Now get the Repository and set the results section
|
||||||
repoExist := true
|
repoExist := true
|
||||||
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName)
|
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName)
|
||||||
|
42
tests/integration/git_ssh_redirect_test.go
Normal file
42
tests/integration/git_ssh_redirect_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Reference in New Issue
Block a user