From 2802f96e9773d194f8a88dd4050b43ae4dd2fd78 Mon Sep 17 00:00:00 2001 From: koalajoe23 Date: Tue, 9 Sep 2025 22:13:41 +0200 Subject: [PATCH] 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. --- cmd/serv.go | 10 +++--- models/fixtures/user_redirect.yml | 4 +++ routers/private/serv.go | 25 +++++++++++++ tests/integration/git_ssh_redirect_test.go | 42 ++++++++++++++++++++++ 4 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 tests/integration/git_ssh_redirect_test.go diff --git a/cmd/serv.go b/cmd/serv.go index 089d0e3bb7..60f7fb92ff 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -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) diff --git a/models/fixtures/user_redirect.yml b/models/fixtures/user_redirect.yml index 8ff7993398..c668cb6c3b 100644 --- a/models/fixtures/user_redirect.yml +++ b/models/fixtures/user_redirect.yml @@ -2,3 +2,7 @@ id: 1 lower_name: olduser1 redirect_user_id: 1 +- + id: 2 + lower_name: olduser2 + redirect_user_id: 2 diff --git a/routers/private/serv.go b/routers/private/serv.go index b879be0dc2..3dfe4d21da 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -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) diff --git a/tests/integration/git_ssh_redirect_test.go b/tests/integration/git_ssh_redirect_test.go new file mode 100644 index 0000000000..5e35ed2a74 --- /dev/null +++ b/tests/integration/git_ssh_redirect_test.go @@ -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)) + }) + } + }) +}