Skip to content

Repository transfer should be confirmed by the new owner before the transfer actually occurs #5744

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

Closed
wants to merge 64 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
86bd92c
add migrations
adelowo Jan 12, 2019
a904524
save to repo_transfer and don't actually perform a transfer just yet
adelowo Jan 12, 2019
e5fb931
make sure there can only be one transfer process at any given time
adelowo Jan 12, 2019
f08acee
add status to check to make sure we check for only 'pending' transfers
adelowo Jan 12, 2019
be9b779
start working on cancellation of a repo transfer request
adelowo Jan 13, 2019
771f814
add ability to cancel an ongoing transfer
adelowo Jan 13, 2019
ace5062
little cleanup
adelowo Jan 15, 2019
ac77491
fix build
adelowo Jan 16, 2019
1c64269
added comments
adelowo Jan 16, 2019
25386e1
fix review
adelowo Jan 24, 2019
02906e2
complete repo transfer
adelowo Feb 9, 2019
2e049da
Merge branch 'master' into repo_transfer
adelowo Mar 24, 2019
a7f7662
start work on email notification
adelowo Mar 24, 2019
ec154fd
cannot transfer an archived repo
adelowo Mar 26, 2019
ec3ece4
fix redirection
adelowo Mar 26, 2019
b515fd6
send email to user
adelowo Mar 26, 2019
79f9952
transfer repository acknowledgement
adelowo Mar 26, 2019
861d9ab
Merge branch 'master' into repo_transfer
adelowo Mar 26, 2019
13984c2
use member of owner team email address for repo transfers to an organ…
adelowo Mar 26, 2019
549aa0c
implement rejection of repo transfer
adelowo Mar 26, 2019
d749dda
fix link
adelowo Mar 26, 2019
5b1b4c4
try to fix tests
adelowo Mar 27, 2019
a29a22e
tests for starting and cancelling a repo transfer
adelowo Mar 27, 2019
7d701c1
fix mispellings
adelowo Mar 27, 2019
a155de7
add repo redirect after a successful repo transfer
adelowo Mar 27, 2019
008f942
use 404 instead of unauthorized
adelowo Mar 27, 2019
1943b82
Merge branch 'master' of github.com:go-gitea/gitea into repo_transfer
adelowo Mar 27, 2019
95d6b0b
fix merge conflicts
adelowo Apr 17, 2019
997d530
fix merge conflicts
adelowo Apr 17, 2019
69a62f5
fix build issues
adelowo Apr 17, 2019
b192f03
write notifications
adelowo Apr 19, 2019
8954618
fix godoc
adelowo Apr 19, 2019
4cf79f7
Merge remote-tracking branch 'origin/master' into repo_transfer
adelowo Apr 19, 2019
79f7ae4
Merge branch 'master' into repo_transfer
adelowo Apr 21, 2019
a5eb37c
Merge branch 'master' into repo_transfer
adelowo Apr 26, 2019
e093b20
Merge branch 'master' into repo_transfer
adelowo Apr 28, 2019
b7a5dc4
Merge branch 'master' into repo_transfer
adelowo May 4, 2019
b04ace2
Merge remote-tracking branch 'origin' into repo_transfer
adelowo Jan 26, 2020
efbbf45
fix merge conflicts
adelowo Jan 30, 2020
3a8dcf1
Merge remote-tracking branch 'origin' into repo_transfer
adelowo Feb 2, 2020
7e961ce
fix merge conflicts
adelowo Feb 3, 2020
736dcb5
Merge branch 'master' of github.com:go-gitea/gitea into repo_transfer
adelowo Feb 3, 2020
f339291
fix transfer logic and hook email up
adelowo Feb 4, 2020
bba603e
switch transfer logic to services/repo
adelowo Feb 4, 2020
5c578fc
make repository transfer as accepted when user accepts
adelowo Feb 4, 2020
e7501ed
fix test
adelowo Feb 4, 2020
557b353
Merge branch 'master' of github.com:go-gitea/gitea into repo_transfer
adelowo Feb 4, 2020
4dff694
fix ci
adelowo Feb 4, 2020
e870ec0
Merge branch 'master' of github.com:go-gitea/gitea into repo_transfer
adelowo Feb 7, 2020
5fe80cb
Merge remote-tracking branch 'origin' into repo_transfer
adelowo Feb 16, 2020
dd29d2f
add support for teams ID
adelowo Feb 16, 2020
f43d9a4
Add omit tag
adelowo Feb 16, 2020
e179100
Merge branch 'master' of github.com:go-gitea/gitea into repo_transfer
adelowo Feb 16, 2020
b481a5a
update swagger
adelowo Feb 16, 2020
2cd9093
Merge branch 'master' of github.com:go-gitea/gitea into repo_transfer
adelowo Feb 16, 2020
8d8bb35
fix test
adelowo Feb 16, 2020
114bc96
Merge branch 'master' of github.com:go-gitea/gitea into repo_transfer
adelowo Feb 16, 2020
57336cc
Merge branch 'master' of github.com:go-gitea/gitea into repo_transfer
adelowo Feb 16, 2020
4084c40
Fix swagger definition
adelowo Feb 16, 2020
bfd1bbd
Merge branch 'master' into repo_transfer
lafriks Feb 17, 2020
90578de
update migration
adelowo Feb 17, 2020
727dd45
Merge branch 'master' into repo_transfer
adelowo Feb 17, 2020
0bab4a3
Merge branch 'master' of github.com:go-gitea/gitea into repo_transfer
adelowo Feb 17, 2020
85c397f
Merge remote-tracking branch 'origin/master' into repo_transfer
adelowo Apr 2, 2020
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
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ github.com/RoaringBitmap/roaring v0.4.21 h1:WJ/zIlNX4wQZ9x8Ey33O1UaD9TCTakYsdLFS
github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/Unknwon/com v0.0.0-20190321035513-0fed4efef755 h1:1B7wb36fHLSwZfHg6ngZhhtIEHQjiC5H4p7qQGBEffg=
github.com/Unknwon/com v0.0.0-20190321035513-0fed4efef755/go.mod h1:voKvFVpXBJxdIPeqjoJuLK+UVcRlo/JLjeToGxPYu68=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
Expand Down
13 changes: 9 additions & 4 deletions integrations/api_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,12 +400,17 @@ func TestAPIRepoTransfer(t *testing.T) {
teams *[]int64
expectedStatus int
}{
// Transfer to a user with teams in another org should fail
{ctxUserID: 1, newOwner: "user3", teams: &[]int64{5}, expectedStatus: http.StatusForbidden},
// Transfer to a user with non-existent team IDs should fail
{ctxUserID: 1, newOwner: "user2", teams: &[]int64{2}, expectedStatus: http.StatusUnprocessableEntity},
// Transfer should go through
{ctxUserID: 1, newOwner: "user2", teams: nil, expectedStatus: http.StatusAccepted},
{ctxUserID: 2, newOwner: "user1", teams: nil, expectedStatus: http.StatusAccepted},
// Transfer already started.. Cannot start transfer to another
// user again
{ctxUserID: 1, newOwner: "user3", teams: &[]int64{2}, expectedStatus: http.StatusConflict},
// User does not have access to repo
{ctxUserID: 2, newOwner: "user6", teams: nil, expectedStatus: http.StatusForbidden},
{ctxUserID: 1, newOwner: "user2", teams: &[]int64{2}, expectedStatus: http.StatusUnprocessableEntity},
{ctxUserID: 1, newOwner: "user3", teams: &[]int64{5}, expectedStatus: http.StatusForbidden},
{ctxUserID: 1, newOwner: "user3", teams: &[]int64{2}, expectedStatus: http.StatusAccepted},
}

defer prepareTestEnv(t)()
Expand Down
34 changes: 34 additions & 0 deletions models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,40 @@ func (err ErrRepoNotExist) Error() string {
err.ID, err.UID, err.OwnerName, err.Name)
}

// ErrNoPendingRepoTransfer is an error type for repositories without a pending
// transfer request
type ErrNoPendingRepoTransfer struct {
RepoID int64
}

func (e ErrNoPendingRepoTransfer) Error() string {
return fmt.Sprintf("repository doesn't have a pending transfer [repo_id: %d]", e.RepoID)
}

// IsErrNoPendingTransfer is an error type when a repository has no pending
// transfers
func IsErrNoPendingTransfer(err error) bool {
_, ok := err.(ErrNoPendingRepoTransfer)
return ok
}

// ErrRepoTransferInProgress represents the state of a repository that has an
// ongoing transfer
type ErrRepoTransferInProgress struct {
Uname string
Name string
}

// IsErrRepoTransferInProgress checks if an error is a ErrRepoTransferInProgress.
func IsErrRepoTransferInProgress(err error) bool {
_, ok := err.(ErrRepoTransferInProgress)
return ok
}

func (err ErrRepoTransferInProgress) Error() string {
return fmt.Sprintf("repository is already being transferred [uname: %s, name: %s]", err.Uname, err.Name)
}

// ErrRepoAlreadyExist represents a "RepoAlreadyExist" kind of error.
type ErrRepoAlreadyExist struct {
Uname string
Expand Down
8 changes: 8 additions & 0 deletions models/fixtures/repo_transfer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-
id: 1
user_id: 3
recipient_id: 1
status: 1
repo_id: 3
created_unix: 1553610671
updated_unix: 1553610671
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ var migrations = []Migration{
NewMigration("Refix merge base for merged pull requests", refixMergeBase),
// v135 -> 136
NewMigration("Add OrgID column to Labels table", addOrgIDLabelColumn),
// V136 -> 137
NewMigration("create repo transfer table", addRepoTransfer),
}

// Migrate database to current version
Expand Down
26 changes: 26 additions & 0 deletions models/migrations/v136.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/xorm"
)

func addRepoTransfer(x *xorm.Engine) error {
type RepoTransfer struct {
ID int64 `xorm:"pk autoincr"`
UserID int64
RecipientID int64
RepoID int64
CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL updated"`
Status bool
TeamIDs []int64
}

return x.Sync(new(RepoTransfer))
}
1 change: 1 addition & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func init() {
new(Task),
new(LanguageStat),
new(EmailHash),
new(RepoTransfer),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
26 changes: 26 additions & 0 deletions models/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const (
NotificationSourcePullRequest
// NotificationSourceCommit is a notification of a commit
NotificationSourceCommit
// NotificationSourceRepoTransfer is a notification for a repository
// transfer
NotificationSourceRepoTransfer
)

// Notification represents a notification
Expand Down Expand Up @@ -116,6 +119,29 @@ func GetNotifications(opts FindNotificationOptions) (NotificationList, error) {
return getNotifications(x, opts)
}

// CreateRepoTransferNotification creates notification for the user a repository was transferred to
func CreateRepoTransferNotification(doerID, recipientID int64, repo *Repository) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}

notification := &Notification{
UserID: recipientID,
RepoID: repo.ID,
Status: NotificationStatusUnread,
UpdatedBy: doerID,
Source: NotificationSourceRepoTransfer,
}

if _, err := sess.Insert(notification); err != nil {
return err
}

return sess.Commit()
}

// CreateOrUpdateIssueNotifications creates an issue notification
// for each watcher, or updates it if already exists
func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error {
Expand Down
129 changes: 0 additions & 129 deletions models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -1117,135 +1117,6 @@ func IncrementRepoForkNum(ctx DBContext, repoID int64) error {
return err
}

// TransferOwnership transfers all corresponding setting from old user to new one.
Copy link
Member Author

Choose a reason for hiding this comment

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

Should in case anyone is looking at this, it was moved to the newly created repo_transfer.go file https://github.com/go-gitea/gitea/pull/5744/files#diff-4a00b23928d7e63417b6a39f3052ec23R126

func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error {
newOwner, err := GetUserByName(newOwnerName)
if err != nil {
return fmt.Errorf("get new owner '%s': %v", newOwnerName, err)
}

// Check if new owner has repository with same name.
has, err := IsRepositoryExist(newOwner, repo.Name)
if err != nil {
return fmt.Errorf("IsRepositoryExist: %v", err)
} else if has {
return ErrRepoAlreadyExist{newOwnerName, repo.Name}
}

sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return fmt.Errorf("sess.Begin: %v", err)
}

oldOwner := repo.Owner

// Note: we have to set value here to make sure recalculate accesses is based on
// new owner.
repo.OwnerID = newOwner.ID
repo.Owner = newOwner
repo.OwnerName = newOwner.Name

// Update repository.
if _, err := sess.ID(repo.ID).Update(repo); err != nil {
return fmt.Errorf("update owner: %v", err)
}

// Remove redundant collaborators.
collaborators, err := repo.getCollaborators(sess, ListOptions{})
if err != nil {
return fmt.Errorf("getCollaborators: %v", err)
}

// Dummy object.
collaboration := &Collaboration{RepoID: repo.ID}
for _, c := range collaborators {
if c.ID != newOwner.ID {
isMember, err := isOrganizationMember(sess, newOwner.ID, c.ID)
if err != nil {
return fmt.Errorf("IsOrgMember: %v", err)
} else if !isMember {
continue
}
}
collaboration.UserID = c.ID
if _, err = sess.Delete(collaboration); err != nil {
return fmt.Errorf("remove collaborator '%d': %v", c.ID, err)
}
}

// Remove old team-repository relations.
if oldOwner.IsOrganization() {
if err = oldOwner.removeOrgRepo(sess, repo.ID); err != nil {
return fmt.Errorf("removeOrgRepo: %v", err)
}
}

if newOwner.IsOrganization() {
if err := newOwner.GetTeams(&SearchTeamOptions{}); err != nil {
return fmt.Errorf("GetTeams: %v", err)
}
for _, t := range newOwner.Teams {
if t.IncludesAllRepositories {
if err := t.addRepository(sess, repo); err != nil {
return fmt.Errorf("addRepository: %v", err)
}
}
}
} else if err = repo.recalculateAccesses(sess); err != nil {
// Organization called this in addRepository method.
return fmt.Errorf("recalculateAccesses: %v", err)
}

// Update repository count.
if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos+1 WHERE id=?", newOwner.ID); err != nil {
return fmt.Errorf("increase new owner repository count: %v", err)
} else if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", oldOwner.ID); err != nil {
return fmt.Errorf("decrease old owner repository count: %v", err)
}

if err = watchRepo(sess, doer.ID, repo.ID, true); err != nil {
return fmt.Errorf("watchRepo: %v", err)
}

// Remove watch for organization.
if oldOwner.IsOrganization() {
if err = watchRepo(sess, oldOwner.ID, repo.ID, false); err != nil {
return fmt.Errorf("watchRepo [false]: %v", err)
}
}

// Rename remote repository to new path and delete local copy.
dir := UserPath(newOwner.Name)

if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("Failed to create dir %s: %v", dir, err)
}

if err = os.Rename(RepoPath(oldOwner.Name, repo.Name), RepoPath(newOwner.Name, repo.Name)); err != nil {
return fmt.Errorf("rename repository directory: %v", err)
}

// Rename remote wiki repository to new path and delete local copy.
wikiPath := WikiPath(oldOwner.Name, repo.Name)
if com.IsExist(wikiPath) {
if err = os.Rename(wikiPath, WikiPath(newOwner.Name, repo.Name)); err != nil {
return fmt.Errorf("rename repository wiki: %v", err)
}
}

// If there was previously a redirect at this location, remove it.
if err = deleteRepoRedirect(sess, newOwner.ID, repo.Name); err != nil {
return fmt.Errorf("delete repo redirect: %v", err)
}

if err := NewRepoRedirect(DBContext{sess}, oldOwner.ID, repo.ID, repo.Name, repo.Name); err != nil {
return fmt.Errorf("NewRepoRedirect: %v", err)
}

return sess.Commit()
}

// ChangeRepositoryName changes all corresponding setting from old repository name to new one.
func ChangeRepositoryName(doer *User, repo *Repository, newRepoName string) (err error) {
oldRepoName := repo.Name
Expand Down
36 changes: 36 additions & 0 deletions models/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,42 @@ func TestUploadBigAvatar(t *testing.T) {
assert.Error(t, err)
}

func TestRepositoryTransfer(t *testing.T) {

assert.NoError(t, PrepareTestDatabase())

doer := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
repo := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository)

transfer, err := GetPendingRepositoryTransfer(repo)
assert.Error(t, err)
assert.Nil(t, transfer)
assert.True(t, IsErrNoPendingTransfer(err))

user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)

assert.NoError(t, StartRepositoryTransfer(doer, user2, repo, nil))

transfer, err = GetPendingRepositoryTransfer(repo)
assert.Nil(t, err)
assert.NoError(t, transfer.LoadAttributes())
assert.Equal(t, "user2", transfer.Recipient.Name)

user6 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)

// Only transfer can be started at any given time
err = StartRepositoryTransfer(doer, user6, repo, nil)
assert.Error(t, err)
assert.True(t, IsErrRepoTransferInProgress(err))

// Unknown user
err = StartRepositoryTransfer(doer, &User{ID: 1000, LowerName: "user1000"}, repo, nil)
assert.Error(t, err)

// Cancel transfer
assert.NoError(t, CancelRepositoryTransfer(transfer))
}

func TestDeleteAvatar(t *testing.T) {

// Generate image
Expand Down
Loading