mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-11 19:34:47 +01:00
Add reparent option to Repository CreateFork API
Normally, a fork becomes the `child` of the source repository. In this case, the fork becomes the new `parent` of the source repository. Closes: #34848
This commit is contained in:
parent
38ad58575a
commit
7e7ac0dbf2
@ -101,3 +101,14 @@ func GetForksByUserAndOrgs(ctx context.Context, user *user_model.User, repo *Rep
|
|||||||
repoList = append(repoList, orgForks...)
|
repoList = append(repoList, orgForks...)
|
||||||
return repoList, nil
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -9,4 +9,6 @@ type CreateForkOption struct {
|
|||||||
Organization *string `json:"organization"`
|
Organization *string `json:"organization"`
|
||||||
// name of the forked repository
|
// name of the forked repository
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
|
// set the target fork as the parent of the source repository
|
||||||
|
Reparent bool `json:"reparent"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,6 +133,32 @@ func CreateFork(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !ctx.Doer.IsAdmin {
|
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)
|
isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@ -156,6 +182,7 @@ func CreateFork(ctx *context.APIContext) {
|
|||||||
BaseRepo: repo,
|
BaseRepo: repo,
|
||||||
Name: name,
|
Name: name,
|
||||||
Description: repo.Description,
|
Description: repo.Description,
|
||||||
|
Reparent: form.Reparent,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
|
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
|
||||||
|
|||||||
@ -52,6 +52,7 @@ type ForkRepoOptions struct {
|
|||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
SingleBranch string
|
SingleBranch string
|
||||||
|
Reparent bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForkRepository forks a repository
|
// ForkRepository forks a repository
|
||||||
@ -108,8 +109,19 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
|||||||
if err = createRepositoryInDB(ctx, doer, owner, repo, true); err != nil {
|
if err = createRepositoryInDB(ctx, doer, owner, repo, true); err != nil {
|
||||||
return err
|
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
|
||||||
|
}
|
||||||
|
if err = repo_model.IncrementRepoForkNum(ctx, repo.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
|
// copy lfs files failure should not be ignored
|
||||||
|
|||||||
5
templates/swagger/v1_json.tmpl
generated
5
templates/swagger/v1_json.tmpl
generated
@ -22880,6 +22880,11 @@
|
|||||||
"description": "organization name, if forking into an organization",
|
"description": "organization name, if forking into an organization",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Organization"
|
"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"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
|||||||
@ -7,10 +7,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/auth"
|
||||||
org_model "code.gitea.io/gitea/models/organization"
|
org_model "code.gitea.io/gitea/models/organization"
|
||||||
|
"code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
@ -129,3 +132,66 @@ func TestForkListLimitedAndPrivateRepos(t *testing.T) {
|
|||||||
assert.Equal(t, 2, htmlDoc.Find(forkItemSelector).Length())
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user