Skip to content
10 changes: 5 additions & 5 deletions cmd/serv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions models/fixtures/user_redirect.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
id: 1
lower_name: olduser1
redirect_user_id: 1
-
id: 2
lower_name: olduser2
redirect_user_id: 2
25 changes: 25 additions & 0 deletions routers/private/serv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions tests/integration/git_ssh_redirect_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
})
}