From 140fac63e2dd4cf5bc3eaa0c3c1c9c0a983c0635 Mon Sep 17 00:00:00 2001 From: somaz <112675579+somaz94@users.noreply.github.com> Date: Sat, 31 Oct 2026 02:28:47 +0900 Subject: [PATCH] fix(repo): honor ALLOW_FORK_INTO_SAME_OWNER for organizations (#40025) With `[repository] ALLOW_FORK_INTO_SAME_OWNER = true` a user can fork their own repository, but an organization still cannot fork a repository it owns. The fork button check and the owner list on the fork page compared the organization with the repository owner directly instead of asking `CanUserForkBetweenOwners`, and the walk over parent repositories dropped the owning organization whenever the setting was on. Organizations now follow the setting the same way users already do, and nothing changes when it is off. Fixes https://github.com/go-gitea/gitea/issues/40021 Assisted-by: Claude Code:claude-opus-5-5 --- modules/repository/fork.go | 2 +- modules/repository/fork_test.go | 29 ++++++++++++++++++++++++++++- routers/web/repo/fork.go | 13 +++++-------- tests/integration/repo_fork_test.go | 8 ++++++++ 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/modules/repository/fork.go b/modules/repository/fork.go index 4acdfea6774..7236bd3ef70 100644 --- a/modules/repository/fork.go +++ b/modules/repository/fork.go @@ -35,7 +35,7 @@ func CanUserForkRepo(ctx context.Context, user *user_model.User, repo *repo_mode return false, err } for _, org := range ownedOrgs { - if repo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, repo.ID) { + if CanUserForkBetweenOwners(repo.OwnerID, org.ID) && !repo_model.HasForkedRepo(ctx, org.ID, repo.ID) { return true, nil } } diff --git a/modules/repository/fork_test.go b/modules/repository/fork_test.go index f9d5cf6f9a6..59b68d69d27 100644 --- a/modules/repository/fork_test.go +++ b/modules/repository/fork_test.go @@ -6,6 +6,10 @@ package repository import ( "testing" + "gitea.dev/models/db" + repo_model "gitea.dev/models/repo" + "gitea.dev/models/unittest" + user_model "gitea.dev/models/user" "gitea.dev/modules/setting" "gitea.dev/modules/test" @@ -13,7 +17,7 @@ import ( ) func TestCanUserForkBetweenOwners(t *testing.T) { - defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner) + defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner)() setting.Repository.AllowForkIntoSameOwner = true assert.True(t, CanUserForkBetweenOwners(1, 1)) @@ -23,3 +27,26 @@ func TestCanUserForkBetweenOwners(t *testing.T) { assert.False(t, CanUserForkBetweenOwners(1, 1)) assert.True(t, CanUserForkBetweenOwners(1, 2)) } + +func TestCanUserForkRepoIntoOwningOrg(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner)() + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + // user2's own fork leaves org3, the owner, as the only target + assert.NoError(t, db.Insert(t.Context(), &repo_model.Repository{ + OwnerID: 2, Name: "repo3-fork", LowerName: "repo3-fork", + IsFork: true, ForkID: 3, + })) + + setting.Repository.AllowForkIntoSameOwner = true + ok, err := CanUserForkRepo(t.Context(), user2, repo3) + assert.NoError(t, err) + assert.True(t, ok) + + setting.Repository.AllowForkIntoSameOwner = false + ok, err = CanUserForkRepo(t.Context(), user2, repo3) + assert.NoError(t, err) + assert.False(t, ok) +} diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 6e60a9a3d2e..d2b8559722a 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -7,6 +7,7 @@ import ( "errors" "net/http" "net/url" + "slices" "gitea.dev/models/db" git_model "gitea.dev/models/git" @@ -60,7 +61,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { } var orgs []*organization.Organization for _, org := range ownedOrgs { - if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) { + if repository.CanUserForkBetweenOwners(forkRepo.OwnerID, org.ID) && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) { orgs = append(orgs, org) } } @@ -69,14 +70,10 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { for { if !repository.CanUserForkBetweenOwners(ctx.Doer.ID, traverseParentRepo.OwnerID) { canForkToUser = false - } else { - for i, org := range orgs { - if org.ID == traverseParentRepo.OwnerID { - orgs = append(orgs[:i], orgs[i+1:]...) - break - } - } } + orgs = slices.DeleteFunc(orgs, func(org *organization.Organization) bool { + return !repository.CanUserForkBetweenOwners(org.ID, traverseParentRepo.OwnerID) + }) if !traverseParentRepo.IsFork { break diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index 5c34b83aa1a..134155ba8dd 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -13,6 +13,7 @@ import ( org_model "gitea.dev/models/organization" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/setting" "gitea.dev/modules/structs" "gitea.dev/modules/test" org_service "gitea.dev/services/org" @@ -80,6 +81,13 @@ func TestRepoForkToOrg(t *testing.T) { assert.False(t, exists, "Forking should not be allowed anymore") } +func TestRepoForkIntoSameOrg(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner, true)() + session := loginUser(t, "user2") + testRepoFork(t, session, "org3", "repo3", "org3", "repo3-fork", "") +} + func TestForkListLimitedAndPrivateRepos(t *testing.T) { defer tests.PrepareTestEnv(t)() forkItemSelector := ".fork-list .item"