Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API to manage repo tranfers #17963

Merged
merged 19 commits into from
Dec 24, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
71 changes: 71 additions & 0 deletions integrations/api_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,77 @@ func TestAPIRepoTransfer(t *testing.T) {
_ = models.DeleteRepository(user, repo.OwnerID, repo.ID)
}

func transfer(t *testing.T) *repo_model.Repository {
//create repo to move
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session)
repoName := "moveME"
apiRepo := new(api.Repository)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/repos?token=%s", token), &api.CreateRepoOption{
Name: repoName,
Description: "repo move around",
Private: false,
Readme: "Default",
AutoInit: true,
})

resp := session.MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, apiRepo)

repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}).(*repo_model.Repository)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer?token=%s", repo.OwnerName, repo.Name, token), &api.TransferRepoOption{
NewOwner: "user4",
})
session.MakeRequest(t, req, http.StatusCreated)

return repo
}

func TestAPIAcceptTransfer(t *testing.T) {
defer prepareTestEnv(t)()

repo := transfer(t)

// try to accept with not authorized user
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session)
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
session.MakeRequest(t, req, http.StatusForbidden)

// accept transfer
session = loginUser(t, "user4")
token = getTokenForLoggedInUser(t, session)

req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/accept?token=%s", repo.OwnerName, repo.Name, token))
resp := session.MakeRequest(t, req, http.StatusAccepted)
apiRepo := new(api.Repository)
DecodeJSON(t, resp, apiRepo)
assert.Equal(t, "user4", apiRepo.Owner.UserName)
}

func TestAPIRejectTransfer(t *testing.T) {
defer prepareTestEnv(t)()

repo := transfer(t)

// try to reject with not authorized user
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session)
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
session.MakeRequest(t, req, http.StatusForbidden)

// reject transfer
session = loginUser(t, "user4")
token = getTokenForLoggedInUser(t, session)

req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
resp := session.MakeRequest(t, req, http.StatusOK)
apiRepo := new(api.Repository)
DecodeJSON(t, resp, apiRepo)
assert.Equal(t, "user2", apiRepo.Owner.UserName)
}

func TestAPIGenerateRepo(t *testing.T) {
defer prepareTestEnv(t)()

Expand Down
30 changes: 30 additions & 0 deletions modules/convert/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
)

Expand Down Expand Up @@ -106,6 +107,20 @@ func innerToRepo(repo *repo_model.Repository, mode perm.AccessMode, isParent boo
}
}

var transfer *api.RepoTransfer
if repo.Status == repo_model.RepositoryPendingTransfer {
t, err := models.GetPendingRepositoryTransfer(repo)
if err != nil {
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
log.Debug("GetPendingRepositoryTransfer", err)
} else {
if err := t.LoadAttributes(); err != nil {
log.Debug("LoadRecipient", err)
} else {
transfer = ToRepoTransfer(t)
}
}
}

return &api.Repository{
ID: repo.ID,
Owner: ToUserWithAccessMode(repo.Owner, mode),
Expand Down Expand Up @@ -151,5 +166,20 @@ func innerToRepo(repo *repo_model.Repository, mode perm.AccessMode, isParent boo
AvatarURL: repo.AvatarLink(),
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
MirrorInterval: mirrorInterval,
RepoTransfer: transfer,
}
}

// ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer
func ToRepoTransfer(t *models.RepoTransfer) *api.RepoTransfer {
var teams []*api.Team
for _, v := range t.Teams {
teams = append(teams, ToTeam(v))
}

return &api.RepoTransfer{
Doer: ToUser(t.Doer, nil),
Recipient: ToUser(t.Recipient, nil),
Teams: teams,
}
}
3 changes: 2 additions & 1 deletion modules/indexer/code/indexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import (
"path/filepath"
"testing"

_ "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/unittest"

_ "code.gitea.io/gitea/models"

"github.com/stretchr/testify/assert"
)

Expand Down
3 changes: 2 additions & 1 deletion modules/indexer/issues/indexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
"testing"
"time"

_ "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"

_ "code.gitea.io/gitea/models"

"github.com/stretchr/testify/assert"
"gopkg.in/ini.v1"
)
Expand Down
3 changes: 2 additions & 1 deletion modules/indexer/stats/indexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
"testing"
"time"

_ "code.gitea.io/gitea/models"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"

_ "code.gitea.io/gitea/models"

"github.com/stretchr/testify/assert"
"gopkg.in/ini.v1"
)
Expand Down
8 changes: 8 additions & 0 deletions modules/structs/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type Repository struct {
AvatarURL string `json:"avatar_url"`
Internal bool `json:"internal"`
MirrorInterval string `json:"mirror_interval"`
RepoTransfer *RepoTransfer `json:"repo_transfer"`
lunny marked this conversation as resolved.
Show resolved Hide resolved
}

// CreateRepoOption options when creating repository
Expand Down Expand Up @@ -336,3 +337,10 @@ var (
CodebaseService,
}
)

// RepoTransfer represents a pending repo transfer
type RepoTransfer struct {
Doer *User `json:"doer"`
Recipient *User `json:"recipient"`
Teams []*Team `json:"teams"`
}
2 changes: 2 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,8 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
m.Post("/transfer/accept", reqToken(), repo.AcceptTransfer)
m.Post("/transfer/reject", reqToken(), repo.RejectTransfer)
m.Combo("/notifications").
Get(reqToken(), notify.ListRepoNotifications).
Put(reqToken(), notify.ReadRepoNotifications)
Expand Down
105 changes: 105 additions & 0 deletions routers/api/v1/repo/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,108 @@ func Transfer(ctx *context.APIContext) {
log.Trace("Repository transferred: %s -> %s", ctx.Repo.Repository.FullName(), newOwner.Name)
ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx.Repo.Repository, perm.AccessModeAdmin))
}

// AcceptTransfer accept a repo transfer
func AcceptTransfer(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/transfer/accept repository acceptRepoTransfer
// ---
// summary: Accept a repo transfer
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo to transfer
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to transfer
// type: string
// required: true
// responses:
// "202":
// "$ref": "#/responses/Repository"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"

err := acceptOrRejectRepoTransfer(ctx, true)
if ctx.Written() {
return
}
if err != nil {
ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err)
return
}

ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx.Repo.Repository, ctx.Repo.AccessMode))

}

// RejectTransfer reject a repo transfer
func RejectTransfer(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/transfer/reject repository rejectRepoTransfer
// ---
// summary: Reject a repo transfer
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo to transfer
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to transfer
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Repository"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"

err := acceptOrRejectRepoTransfer(ctx, false)
if ctx.Written() {
return
}
if err != nil {
ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err)
return
}

ctx.JSON(http.StatusOK, convert.ToRepo(ctx.Repo.Repository, ctx.Repo.AccessMode))
}

func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
if err != nil {
6543 marked this conversation as resolved.
Show resolved Hide resolved
return err
}

if err := repoTransfer.LoadAttributes(); err != nil {
return err
}

if !repoTransfer.CanUserAcceptTransfer(ctx.User) {
ctx.Error(http.StatusForbidden, "CanUserAcceptTransfer", nil)
return fmt.Errorf("user does not have permissions to do this")
}

if accept {
if err := repo_service.TransferOwnership(repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
return err
}
} else {
if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
return err
}
}

return nil
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 0 deletions services/asymkey/ssh_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/login"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"

"github.com/stretchr/testify/assert"
)

Expand Down
1 change: 1 addition & 0 deletions services/mailer/mailer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"code.gitea.io/gitea/modules/setting"

"github.com/stretchr/testify/assert"
)

Expand Down
3 changes: 2 additions & 1 deletion services/webhook/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import (
"path/filepath"
"testing"

_ "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/unittest"

_ "code.gitea.io/gitea/models"
)

func TestMain(m *testing.M) {
Expand Down
Loading