Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions models/repo/fork.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,14 @@ 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 {
if _, err := db.GetEngine(ctx).Table("repository").ID(srcForkID).Cols("fork_id", "is_fork").Update(&Repository{ForkID: forkedRepoID, IsFork: true}); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Database transaction is necessary. And what happens for the other fork repositories of the original parent repository?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect nothing needs to happen to the other forks. This is about just one particular fork.

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
}
2 changes: 2 additions & 0 deletions modules/structs/fork.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
27 changes: 27 additions & 0 deletions routers/api/v1/repo/fork.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
16 changes: 14 additions & 2 deletions services/repository/fork.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type ForkRepoOptions struct {
Name string
Description string
SingleBranch string
Reparent bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the previous implementation to have a new API to do the reparent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous implementation? Or do you mean this should be a new API call, like Repository/CreateNewParent or similar name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do a new API call here, can we still use the repo_service.ForkRepoOptions{Reparent:true} or split off that logic on top of a fork?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think introducing a new API endpoint would be more appropriate. Also, the Reparent field in ForkRepoOptions feels a bit awkward.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as we can use the internal Fork implementation already, I'm ok in introducing another API.

}

// ForkRepository forks a repository
Expand Down Expand Up @@ -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 {
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
Expand Down
5 changes: 5 additions & 0 deletions templates/swagger/v1_json.tmpl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions tests/integration/repo_fork_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -129,3 +132,66 @@ 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)
}

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