Skip to content

Commit 2802f96

Browse files
authored
check user and repo for redirects when using git via SSH transport (go-gitea#35416)
fixes go-gitea#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.
1 parent b9efbe9 commit 2802f96

File tree

4 files changed

+76
-5
lines changed

4 files changed

+76
-5
lines changed

cmd/serv.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,6 @@ func runServ(ctx context.Context, c *cli.Command) error {
229229
username := repoPathFields[0]
230230
reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
231231

232-
// LowerCase and trim the repoPath as that's how they are stored.
233-
// This should be done after splitting the repoPath into username and reponame
234-
// so that username and reponame are not affected.
235-
repoPath = strings.ToLower(strings.TrimSpace(repoPath))
236-
237232
if !repo.IsValidSSHAccessRepoName(reponame) {
238233
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
239234
}
@@ -280,6 +275,11 @@ func runServ(ctx context.Context, c *cli.Command) error {
280275
return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error)
281276
}
282277

278+
// LowerCase and trim the repoPath as that's how they are stored.
279+
// This should be done after splitting the repoPath into username and reponame
280+
// so that username and reponame are not affected.
281+
repoPath = strings.ToLower(results.OwnerName + "/" + results.RepoName + ".git")
282+
283283
// LFS SSH protocol
284284
if verb == git.CmdVerbLfsTransfer {
285285
token, err := getLFSAuthToken(ctx, lfsVerb, results)

models/fixtures/user_redirect.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
id: 1
33
lower_name: olduser1
44
redirect_user_id: 1
5+
-
6+
id: 2
7+
lower_name: olduser2
8+
redirect_user_id: 2

routers/private/serv.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,18 @@ func ServCommand(ctx *context.PrivateContext) {
108108
results.RepoName = repoName[:len(repoName)-5]
109109
}
110110

111+
// Check if there is a user redirect for the requested owner
112+
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
113+
if err == nil {
114+
owner, err := user_model.GetUserByID(ctx, redirectedUserID)
115+
if err == nil {
116+
log.Info("User %s has been redirected to %s", results.OwnerName, owner.Name)
117+
results.OwnerName = owner.Name
118+
} else {
119+
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)
120+
}
121+
}
122+
111123
owner, err := user_model.GetUserByName(ctx, results.OwnerName)
112124
if err != nil {
113125
if user_model.IsErrUserNotExist(err) {
@@ -131,6 +143,19 @@ func ServCommand(ctx *context.PrivateContext) {
131143
return
132144
}
133145

146+
redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName)
147+
if err == nil {
148+
redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID)
149+
if err == nil {
150+
log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name)
151+
results.RepoName = redirectedRepo.Name
152+
results.OwnerName = redirectedRepo.OwnerName
153+
owner.ID = redirectedRepo.OwnerID
154+
} else {
155+
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)
156+
}
157+
}
158+
134159
// Now get the Repository and set the results section
135160
repoExist := true
136161
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package integration
5+
6+
import (
7+
"fmt"
8+
"net/url"
9+
"testing"
10+
11+
auth_model "code.gitea.io/gitea/models/auth"
12+
)
13+
14+
func TestGitSSHRedirect(t *testing.T) {
15+
onGiteaRun(t, testGitSSHRedirect)
16+
}
17+
18+
func testGitSSHRedirect(t *testing.T, u *url.URL) {
19+
apiTestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
20+
21+
withKeyFile(t, "my-testing-key", func(keyFile string) {
22+
t.Run("CreateUserKey", doAPICreateUserKey(apiTestContext, "test-key", keyFile))
23+
24+
testCases := []struct {
25+
testName string
26+
userName string
27+
repoName string
28+
}{
29+
{"Test untouched", "user2", "repo1"},
30+
{"Test renamed user", "olduser2", "repo1"},
31+
{"Test renamed repo", "user2", "oldrepo1"},
32+
{"Test renamed user and repo", "olduser2", "oldrepo1"},
33+
}
34+
35+
for _, tc := range testCases {
36+
t.Run(tc.testName, func(t *testing.T) {
37+
cloneURL := createSSHUrl(fmt.Sprintf("%s/%s.git", tc.userName, tc.repoName), u)
38+
t.Run("Clone", doGitClone(t.TempDir(), cloneURL))
39+
})
40+
}
41+
})
42+
}

0 commit comments

Comments
 (0)