0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-12-09 04:41:48 +01:00

Merge c18b98cf3f869ae8b3f16886d828cca9db0e3cc8 into c287a8cdb589172bbba8969357a671dabc6596bd

This commit is contained in:
Adam Majer 2025-12-06 13:40:19 -05:00 committed by GitHub
commit 744dcc268c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 128 additions and 2 deletions

View File

@ -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
})
}

View File

@ -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"`
}

View File

@ -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) {

View File

@ -53,6 +53,7 @@ type ForkRepoOptions struct {
Name string
Description string
SingleBranch string
Reparent bool
}
// ForkRepository forks a repository
@ -109,8 +110,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

View File

@ -22950,6 +22950,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"

View File

@ -7,10 +7,13 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"path"
"strconv"
"testing"
"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"
@ -129,3 +132,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)
}