Skip to content

Update migrated repositories' issues/comments/prs poster id if user has a github external user saved #7751

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

Merged
merged 19 commits into from
Oct 14, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,11 @@ SCHEDULE = @every 24h
; or only create new users if UPDATE_EXISTING is set to false
UPDATE_EXISTING = true

; Update migrated repositories' issues and comments' posterid, it will always attempt synchronization when the instance starts.
[cron.update_migration_post_id]
; Interval as a duration between each synchronization. (default every 24h)
SCHEDULE = @every 24h

[git]
; The path of git executable. If empty, Gitea searches through the PATH environment.
PATH =
Expand Down
4 changes: 4 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,10 @@ NB: You must `REDIRECT_MACARON_LOG` and have `DISABLE_ROUTER_LOG` set to `false`
- `RUN_AT_START`: **true**: Run repository statistics check at start time.
- `SCHEDULE`: **@every 24h**: Cron syntax for scheduling repository statistics check.

### Cron - Update Migration Poster ID (`cron.update_migration_post_id`)

- `SCHEDULE`: **@every 24h** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts.

## Git (`git`)

- `PATH`: **""**: The path of git executable. If empty, Gitea searches through the PATH environment.
Expand Down
6 changes: 5 additions & 1 deletion docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,11 @@ menu:
### Cron - Repository Statistics Check (`cron.check_repo_stats`)

- `RUN_AT_START`: 是否启动时自动运行仓库统计。
- `SCHEDULE`: 藏亏统计时的Cron 语法,比如:`@every 24h`.
- `SCHEDULE`: 仓库统计时的Cron 语法,比如:`@every 24h`.

### Cron - Update Migration Poster ID (`cron.update_migration_post_id`)

- `SCHEDULE`: **@every 24h** : 每次同步的间隔时间。此任务总是在启动时自动进行。

## Git (`git`)

Expand Down
143 changes: 125 additions & 18 deletions models/external_login_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,34 @@

package models

import "github.com/markbates/goth"
import (
"time"

"code.gitea.io/gitea/modules/structs"

"github.com/markbates/goth"
"xorm.io/builder"
)

// ExternalLoginUser makes the connecting between some existing user and additional external login sources
type ExternalLoginUser struct {
ExternalID string `xorm:"pk NOT NULL"`
UserID int64 `xorm:"INDEX NOT NULL"`
LoginSourceID int64 `xorm:"pk NOT NULL"`
ExternalID string `xorm:"pk NOT NULL"`
UserID int64 `xorm:"INDEX NOT NULL"`
LoginSourceID int64 `xorm:"pk NOT NULL"`
RawData map[string]interface{} `xorm:"TEXT JSON"`
Provider string `xorm:"index VARCHAR(25)"`
Email string
Name string
FirstName string
LastName string
NickName string
Description string
AvatarURL string
Location string
AccessToken string
AccessTokenSecret string
RefreshToken string
ExpiresAt time.Time
}

// GetExternalLogin checks if a externalID in loginSourceID scope already exists
Expand All @@ -32,23 +53,15 @@ func ListAccountLinks(user *User) ([]*ExternalLoginUser, error) {
return externalAccounts, nil
}

// LinkAccountToUser link the gothUser to the user
func LinkAccountToUser(user *User, gothUser goth.User) error {
loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider)
if err != nil {
return err
}

externalLoginUser := &ExternalLoginUser{
ExternalID: gothUser.UserID,
UserID: user.ID,
LoginSourceID: loginSource.ID,
}
has, err := x.Get(externalLoginUser)
// LinkExternalToUser link the external user to the user
func LinkExternalToUser(user *User, externalLoginUser *ExternalLoginUser) error {
has, err := x.Where("external_id=? AND login_source_id=?", externalLoginUser.ExternalID, externalLoginUser.LoginSourceID).
NoAutoCondition().
Exist(externalLoginUser)
if err != nil {
return err
} else if has {
return ErrExternalLoginUserAlreadyExist{gothUser.UserID, user.ID, loginSource.ID}
return ErrExternalLoginUserAlreadyExist{externalLoginUser.ExternalID, user.ID, externalLoginUser.LoginSourceID}
}

_, err = x.Insert(externalLoginUser)
Expand All @@ -72,3 +85,97 @@ func removeAllAccountLinks(e Engine, user *User) error {
_, err := e.Delete(&ExternalLoginUser{UserID: user.ID})
return err
}

// GetUserIDByExternalUserID get user id according to provider and userID
func GetUserIDByExternalUserID(provider string, userID string) (int64, error) {
var id int64
_, err := x.Table("external_login_user").
Copy link
Member

Choose a reason for hiding this comment

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

Why bypass the ORM? Is there a limitation/advantage? Is it because it's not the PK?

Copy link
Member Author

Choose a reason for hiding this comment

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

Table("external_login_user") has a better performance than Table(new(ExternalLoginUser)).

Select("user_id").
Where("provider=?", provider).
And("external_id=?", userID).
Get(&id)
if err != nil {
return 0, err
}
return id, nil
}

// UpdateExternalUser updates external user's information
func UpdateExternalUser(user *User, gothUser goth.User) error {
loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider)
if err != nil {
return err
}
externalLoginUser := &ExternalLoginUser{
ExternalID: gothUser.UserID,
UserID: user.ID,
LoginSourceID: loginSource.ID,
RawData: gothUser.RawData,
Provider: gothUser.Provider,
Email: gothUser.Email,
Name: gothUser.Name,
FirstName: gothUser.FirstName,
LastName: gothUser.LastName,
NickName: gothUser.NickName,
Description: gothUser.Description,
AvatarURL: gothUser.AvatarURL,
Location: gothUser.Location,
AccessToken: gothUser.AccessToken,
AccessTokenSecret: gothUser.AccessTokenSecret,
RefreshToken: gothUser.RefreshToken,
ExpiresAt: gothUser.ExpiresAt,
}

has, err := x.Where("external_id=? AND login_source_id=?", gothUser.UserID, loginSource.ID).
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this check accomplished by the RETURNING of affected rows from the UPDATE? (Does xorm return that value? 🤔)

Copy link
Member Author

Choose a reason for hiding this comment

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

Update may return zero if no records actually changed in fact on some databases but some will returns values only the where conditions matched. So we can not depend on that.

NoAutoCondition().
Exist(externalLoginUser)
if err != nil {
return err
} else if !has {
return ErrExternalLoginUserNotExist{user.ID, loginSource.ID}
}

_, err = x.Where("external_id=? AND login_source_id=?", gothUser.UserID, loginSource.ID).AllCols().Update(externalLoginUser)
return err
}

// FindExternalUserOptions represents an options to find external users
type FindExternalUserOptions struct {
Provider string
Limit int
Start int
}

func (opts FindExternalUserOptions) toConds() builder.Cond {
var cond = builder.NewCond()
if len(opts.Provider) > 0 {
cond = cond.And(builder.Eq{"provider": opts.Provider})
}
return cond
}

// FindExternalUsersByProvider represents external users via provider
func FindExternalUsersByProvider(opts FindExternalUserOptions) ([]ExternalLoginUser, error) {
var users []ExternalLoginUser
err := x.Where(opts.toConds()).
Limit(opts.Limit, opts.Start).
Asc("id").
Find(&users)
if err != nil {
return nil, err
}
return users, nil
}

// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID
func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID, userID int64) error {
if err := UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil {
return err
}

if err := UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil {
return err
}

return UpdateReleasesMigrationsByType(tp, externalUserID, userID)
}
16 changes: 15 additions & 1 deletion models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
Expand All @@ -32,7 +33,7 @@ type Issue struct {
PosterID int64 `xorm:"INDEX"`
Poster *User `xorm:"-"`
OriginalAuthor string
OriginalAuthorID int64
OriginalAuthorID int64 `xorm:"index"`
Title string `xorm:"name"`
Content string `xorm:"TEXT"`
RenderedContent string `xorm:"-"`
Expand Down Expand Up @@ -1947,3 +1948,16 @@ func (issue *Issue) ResolveMentionsByVisibility(ctx DBContext, doer *User, menti

return
}

// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID
func UpdateIssuesMigrationsByType(gitServiceType structs.GitServiceType, originalAuthorID, posterID int64) error {
_, err := x.Table("issue").
Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
And("original_author_id = ?", originalAuthorID).
Update(map[string]interface{}{
"poster_id": posterID,
"original_author": "",
"original_author_id": 0,
Copy link
Member

Choose a reason for hiding this comment

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

This functions seems to be about clearing up the original author information, rather than update it. Which is it?

Copy link
Member Author

Choose a reason for hiding this comment

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

poster_id stores local user id and original_author_id stores github's user id.
This function will convert an issue's poster from an original author to a local user if we have already known his local user is related a github account. All related information are stored on external_login_user. So we could know which local user id is point to a github user id.

Copy link
Member

Choose a reason for hiding this comment

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

Of course, I missed the relevant line.

})
return err
}
21 changes: 21 additions & 0 deletions models/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/structs"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"

Expand Down Expand Up @@ -1022,3 +1023,23 @@ func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review
func FetchCodeComments(issue *Issue, currentUser *User) (CodeComments, error) {
return fetchCodeComments(x, issue, currentUser)
}

// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID, posterID int64) error {
_, err := x.Table("comment").
Where(builder.In("issue_id",
builder.Select("issue.id").
From("issue").
InnerJoin("repository", "issue.repo_id = repository.id").
Where(builder.Eq{
"repository.original_service_type": tp,
}),
)).
And("comment.original_author_id = ?", originalAuthorID).
Update(map[string]interface{}{
"poster_id": posterID,
"original_author": "",
"original_author_id": 0,
Copy link
Member

Choose a reason for hiding this comment

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

Same here

Copy link
Member Author

Choose a reason for hiding this comment

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

As above.

})
return err
}
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ var migrations = []Migration{
NewMigration("add original author name and id on migrated release", addOriginalAuthorOnMigratedReleases),
// v99 -> v100
NewMigration("add task table and status column for repository table", addTaskTable),
// v100 -> v101
NewMigration("update migration repositories' service type", updateMigrationServiceTypes),
}

// Migrate database to current version
Expand Down
83 changes: 83 additions & 0 deletions models/migrations/v100.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2019 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 (
"net/url"
"strings"
"time"

"github.com/go-xorm/xorm"
)

func updateMigrationServiceTypes(x *xorm.Engine) error {
type Repository struct {
ID int64
OriginalServiceType int `xorm:"index default(0)"`
OriginalURL string `xorm:"VARCHAR(2048)"`
}

if err := x.Sync2(new(Repository)); err != nil {
return err
}

var last int
const batchSize = 50
for {
var results = make([]Repository, 0, batchSize)
err := x.Where("original_url <> '' AND original_url IS NOT NULL").
And("original_service_type = 0 OR original_service_type IS NULL").
OrderBy("id").
Limit(batchSize, last).
Find(&results)
if err != nil {
return err
}
if len(results) == 0 {
break
}
last += len(results)

const PlainGitService = 1 // 1 plain git service
const GithubService = 2 // 2 github.com

for _, res := range results {
u, err := url.Parse(res.OriginalURL)
if err != nil {
return err
}
var serviceType = PlainGitService
if strings.EqualFold(u.Host, "github.com") {
serviceType = GithubService
}
_, err = x.Exec("UPDATE repository SET original_service_type = ? WHERE id = ?", serviceType, res.ID)
if err != nil {
return err
}
}
}

type ExternalLoginUser struct {
ExternalID string `xorm:"pk NOT NULL"`
UserID int64 `xorm:"INDEX NOT NULL"`
LoginSourceID int64 `xorm:"pk NOT NULL"`
RawData map[string]interface{} `xorm:"TEXT JSON"`
Provider string `xorm:"index VARCHAR(25)"`
Email string
Name string
FirstName string
LastName string
NickName string
Description string
AvatarURL string
Location string
AccessToken string
AccessTokenSecret string
RefreshToken string
ExpiresAt time.Time
}

return x.Sync2(new(ExternalLoginUser))
}
14 changes: 14 additions & 0 deletions models/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"

Expand Down Expand Up @@ -366,3 +367,16 @@ func SyncReleasesWithTags(repo *Repository, gitRepo *git.Repository) error {
}
return nil
}

// UpdateReleasesMigrationsByType updates all migrated repositories' releases from gitServiceType to replace originalAuthorID to posterID
func UpdateReleasesMigrationsByType(gitServiceType structs.GitServiceType, originalAuthorID, posterID int64) error {
_, err := x.Table("release").
Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
And("original_author_id = ?", originalAuthorID).
Update(map[string]interface{}{
"publisher_id": posterID,
"original_author": "",
"original_author_id": 0,
})
return err
}
Loading