File opensuse-reparent.patch of Package gitea

diff --git a/models/repo/fork.go b/models/repo/fork.go
index 1c75e86458..da00ccef40 100644
--- a/models/repo/fork.go
+++ b/models/repo/fork.go
@@ -101,3 +101,16 @@ func GetForksByUserAndOrgs(ctx context.Context, user *user_model.User, repo *Rep
 	repoList = append(repoList, orgForks...)
 	return repoList, nil
 }
+
+// ReparentFork sets the fork to be an unforked repository and the forked repo becomes its fork
+func ReparentFork(ctx context.Context, forkedRepoID, srcForkID int64) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		if _, err := db.GetEngine(ctx).Table("repository").ID(srcForkID).Cols("fork_id", "is_fork").Update(&Repository{ForkID: forkedRepoID, IsFork: true}); err != nil {
+			return err
+		}
+		if _, err := db.GetEngine(ctx).Table("repository").ID(forkedRepoID).Cols("fork_id", "is_fork", "num_forks").Update(&Repository{ForkID: 0, NumForks: 1, IsFork: false}); err != nil {
+			return err
+		}
+		return nil
+	})
+}
diff --git a/modules/structs/fork.go b/modules/structs/fork.go
index eb7774afbc..3a404f23b9 100644
--- a/modules/structs/fork.go
+++ b/modules/structs/fork.go
@@ -9,4 +9,6 @@ type CreateForkOption struct {
 	Organization *string `json:"organization"`
 	// name of the forked repository
 	Name *string `json:"name"`
+	// set the target fork as the parent of the source repository
+	Reparent bool `json:"reparent"`
 }
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index 58f66954e1..48cb6a8956 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -133,6 +133,32 @@ func CreateFork(ctx *context.APIContext) {
 			return
 		}
 		if !ctx.Doer.IsAdmin {
+			if form.Reparent {
+				// we need to have owner rights in source and target to use reparent option
+				err := repo.LoadOwner(ctx)
+				if err != nil {
+					ctx.APIErrorInternal(err)
+					return
+				}
+				if repo.Owner.IsOrganization() {
+					srcOrg, err := organization.GetOrgByID(ctx, repo.OwnerID)
+					if err != nil {
+						ctx.APIErrorInternal(err)
+						return
+					}
+					isAdminForSrc, err := srcOrg.IsOrgAdmin(ctx, ctx.Doer.ID)
+					if err != nil {
+						ctx.APIErrorInternal(err)
+						return
+					}
+					if !isAdminForSrc {
+						ctx.APIError(http.StatusForbidden, fmt.Sprintf("User '%s' is not an Admin of the Organization '%s'", ctx.Doer.Name, srcOrg.Name))
+						return
+					}
+				} else if repo.OwnerID != ctx.Doer.ID {
+					ctx.APIError(http.StatusForbidden, fmt.Sprintf("User '%s' is not the owner of the source repository and repository is in user space", ctx.Doer.Name))
+				}
+			}
 			isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID)
 			if err != nil {
 				ctx.APIErrorInternal(err)
@@ -156,6 +182,7 @@ func CreateFork(ctx *context.APIContext) {
 		BaseRepo:    repo,
 		Name:        name,
 		Description: repo.Description,
+		Reparent:    form.Reparent,
 	})
 	if err != nil {
 		if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
diff --git a/services/repository/fork.go b/services/repository/fork.go
index bd1554f163..37363972e4 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -52,6 +52,7 @@ type ForkRepoOptions struct {
 	Name         string
 	Description  string
 	SingleBranch string
+	Reparent     bool
 }
 
 // ForkRepository forks a repository
@@ -108,8 +109,16 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
 		if err = createRepositoryInDB(ctx, doer, owner, repo, true); err != nil {
 			return err
 		}
-		if err = repo_model.IncrementRepoForkNum(ctx, opts.BaseRepo.ID); err != nil {
-			return err
+
+		// swap fork_id, if we reparent
+		if opts.Reparent {
+			if err = repo_model.ReparentFork(ctx, repo.ID, opts.BaseRepo.ID); err != nil {
+				return err
+			}
+		} else {
+			if err = repo_model.IncrementRepoForkNum(ctx, opts.BaseRepo.ID); err != nil {
+				return err
+			}
 		}
 
 		// copy lfs files failure should not be ignored
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 3615edf771..1a041d2112 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -21679,6 +21679,11 @@
           "description": "organization name, if forking into an organization",
           "type": "string",
           "x-go-name": "Organization"
+        },
+        "reparent": {
+          "description": "set the target fork as the parent of the source repository",
+          "type": "boolean",
+          "x-go-name": "Reparent"
         }
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index db2caaf6ca..18676c772f 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -7,11 +7,14 @@ import (
 	"fmt"
 	"net/http"
 	"net/http/httptest"
+	"path"
 	"strconv"
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/auth"
 	org_model "code.gitea.io/gitea/models/organization"
+	"code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/structs"
@@ -130,3 +133,70 @@ func TestForkListLimitedAndPrivateRepos(t *testing.T) {
 		assert.Equal(t, 2, htmlDoc.Find(forkItemSelector).Length())
 	})
 }
+
+func TestAPICreateForkWithReparent(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	u := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	source := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
+
+	session := loginUser(t, u.Name)
+	token := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeWriteRepository)
+
+	urlPath := path.Join("/api/v1/repos", source.OwnerName, source.Name, "forks")
+	name := "reparented"
+	req := NewRequestWithJSON(t, "POST", urlPath, &structs.CreateForkOption{
+		Reparent: true,
+		Name:     &name,
+	})
+	req.Header.Add("Authorization", "token "+token)
+	resp := session.MakeRequest(t, req, http.StatusAccepted)
+
+	var result structs.Repository
+	DecodeJSON(t, resp, &result)
+
+	assert.Equal(t, "reparented", result.Name)
+
+	orig := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: source.ID})
+	forked := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: result.ID})
+
+	assert.Equal(t, int64(0), forked.ForkID)
+	assert.False(t, forked.IsFork)
+	assert.Equal(t, forked.ID, orig.ForkID)
+	assert.True(t, orig.IsFork)
+	assert.Equal(t, 1, forked.NumForks)
+	assert.Equal(t, 0, orig.NumForks)
+}
+
+func TestAPICreateForkWithoutReparent(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	u := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	source := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
+
+	session := loginUser(t, u.Name)
+	token := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeWriteRepository)
+
+	urlPath := path.Join("/api/v1/repos", source.OwnerName, source.Name, "forks")
+	name := "standard"
+	req := NewRequestWithJSON(t, "POST", urlPath, &structs.CreateForkOption{
+		Name: &name,
+	})
+	req.Header.Add("Authorization", "token "+token)
+	resp := session.MakeRequest(t, req, http.StatusAccepted)
+
+	var result structs.Repository
+	DecodeJSON(t, resp, &result)
+
+	assert.Equal(t, "standard", result.Name)
+
+	orig := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: source.ID})
+	forked := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: result.ID})
+
+	assert.Equal(t, source.ID, forked.ForkID)
+	assert.True(t, forked.IsFork)
+	assert.Equal(t, int64(0), orig.ForkID)
+	assert.False(t, orig.IsFork)
+	assert.Equal(t, 0, forked.NumForks)
+	assert.Equal(t, 1, orig.NumForks)
+}
openSUSE Build Service is sponsored by