* update migrated issues/comments when login as github * add get userid when migrating or login with github oauth2 * fix lint * add migrations for repository service type * fix build * remove unnecessary dependencies on migrations * add cron task to update migrations poster ids and fix posterid when migrating * fix lint * fix lint * improve code * fix lint * improve code * replace releases publish id to actual author id * fix import * fix bug * fix lint * fix rawdata definition * fix some bugs * fix error messagetags/v1.10.0-rc1
@@ -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 = |
@@ -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. |
@@ -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`) | |||
@@ -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 | |||
@@ -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) | |||
@@ -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"). | |||
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). | |||
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) | |||
} |
@@ -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" | |||
@@ -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:"-"` | |||
@@ -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, | |||
}) | |||
return err | |||
} |
@@ -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" | |||
@@ -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, | |||
}) | |||
return err | |||
} |
@@ -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 |
@@ -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)) | |||
} |
@@ -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" | |||
@@ -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 | |||
} |
@@ -32,6 +32,7 @@ import ( | |||
"code.gitea.io/gitea/modules/options" | |||
"code.gitea.io/gitea/modules/process" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/structs" | |||
api "code.gitea.io/gitea/modules/structs" | |||
"code.gitea.io/gitea/modules/sync" | |||
"code.gitea.io/gitea/modules/timeutil" | |||
@@ -137,16 +138,17 @@ const ( | |||
// Repository represents a git repository. | |||
type Repository struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
OwnerID int64 `xorm:"UNIQUE(s) index"` | |||
OwnerName string `xorm:"-"` | |||
Owner *User `xorm:"-"` | |||
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` | |||
Name string `xorm:"INDEX NOT NULL"` | |||
Description string `xorm:"TEXT"` | |||
Website string `xorm:"VARCHAR(2048)"` | |||
OriginalURL string `xorm:"VARCHAR(2048)"` | |||
DefaultBranch string | |||
ID int64 `xorm:"pk autoincr"` | |||
OwnerID int64 `xorm:"UNIQUE(s) index"` | |||
OwnerName string `xorm:"-"` | |||
Owner *User `xorm:"-"` | |||
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` | |||
Name string `xorm:"INDEX NOT NULL"` | |||
Description string `xorm:"TEXT"` | |||
Website string `xorm:"VARCHAR(2048)"` | |||
OriginalServiceType structs.GitServiceType `xorm:"index"` | |||
OriginalURL string `xorm:"VARCHAR(2048)"` | |||
DefaultBranch string | |||
NumWatches int | |||
NumStars int |
@@ -10,6 +10,7 @@ import ( | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/migrations" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/sync" | |||
mirror_service "code.gitea.io/gitea/services/mirror" | |||
@@ -18,12 +19,13 @@ import ( | |||
) | |||
const ( | |||
mirrorUpdate = "mirror_update" | |||
gitFsck = "git_fsck" | |||
checkRepos = "check_repos" | |||
archiveCleanup = "archive_cleanup" | |||
syncExternalUsers = "sync_external_users" | |||
deletedBranchesCleanup = "deleted_branches_cleanup" | |||
mirrorUpdate = "mirror_update" | |||
gitFsck = "git_fsck" | |||
checkRepos = "check_repos" | |||
archiveCleanup = "archive_cleanup" | |||
syncExternalUsers = "sync_external_users" | |||
deletedBranchesCleanup = "deleted_branches_cleanup" | |||
updateMigrationPosterID = "update_migration_post_id" | |||
) | |||
var c = cron.New() | |||
@@ -117,6 +119,15 @@ func NewContext() { | |||
go WithUnique(deletedBranchesCleanup, models.RemoveOldDeletedBranches)() | |||
} | |||
} | |||
entry, err = c.AddFunc("Update migrated repositories' issues and comments' posterid", setting.Cron.UpdateMigrationPosterID.Schedule, WithUnique(updateMigrationPosterID, migrations.UpdateMigrationPosterID)) | |||
if err != nil { | |||
log.Fatal("Cron[Update migrated repositories]: %v", err) | |||
} | |||
entry.Prev = time.Now() | |||
entry.ExecTimes++ | |||
go WithUnique(updateMigrationPosterID, migrations.UpdateMigrationPosterID)() | |||
c.Start() | |||
} | |||
@@ -5,6 +5,8 @@ | |||
package base | |||
import "code.gitea.io/gitea/modules/structs" | |||
// Downloader downloads the site repo informations | |||
type Downloader interface { | |||
GetRepoInfo() (*Repository, error) | |||
@@ -21,4 +23,5 @@ type Downloader interface { | |||
type DownloaderFactory interface { | |||
Match(opts MigrateOptions) (bool, error) | |||
New(opts MigrateOptions) (Downloader, error) | |||
GitServiceType() structs.GitServiceType | |||
} |
@@ -34,15 +34,17 @@ var ( | |||
// GiteaLocalUploader implements an Uploader to gitea sites | |||
type GiteaLocalUploader struct { | |||
doer *models.User | |||
repoOwner string | |||
repoName string | |||
repo *models.Repository | |||
labels sync.Map | |||
milestones sync.Map | |||
issues sync.Map | |||
gitRepo *git.Repository | |||
prHeadCache map[string]struct{} | |||
doer *models.User | |||
repoOwner string | |||
repoName string | |||
repo *models.Repository | |||
labels sync.Map | |||
milestones sync.Map | |||
issues sync.Map | |||
gitRepo *git.Repository | |||
prHeadCache map[string]struct{} | |||
userMap map[int64]int64 // external user id mapping to user id | |||
gitServiceType structs.GitServiceType | |||
} | |||
// NewGiteaLocalUploader creates an gitea Uploader via gitea API v1 | |||
@@ -52,6 +54,7 @@ func NewGiteaLocalUploader(doer *models.User, repoOwner, repoName string) *Gitea | |||
repoOwner: repoOwner, | |||
repoName: repoName, | |||
prHeadCache: make(map[string]struct{}), | |||
userMap: make(map[int64]int64), | |||
} | |||
} | |||
@@ -109,13 +112,15 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate | |||
} | |||
r, err = models.MigrateRepositoryGitData(g.doer, owner, r, structs.MigrateRepoOption{ | |||
RepoName: g.repoName, | |||
Description: repo.Description, | |||
Mirror: repo.IsMirror, | |||
CloneAddr: remoteAddr, | |||
Private: repo.IsPrivate, | |||
Wiki: opts.Wiki, | |||
Releases: opts.Releases, // if didn't get releases, then sync them from tags | |||
RepoName: g.repoName, | |||
Description: repo.Description, | |||
OriginalURL: repo.OriginalURL, | |||
GitServiceType: opts.GitServiceType, | |||
Mirror: repo.IsMirror, | |||
CloneAddr: remoteAddr, | |||
Private: repo.IsPrivate, | |||
Wiki: opts.Wiki, | |||
Releases: opts.Releases, // if didn't get releases, then sync them from tags | |||
}) | |||
g.repo = r | |||
@@ -193,20 +198,38 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { | |||
var rels = make([]*models.Release, 0, len(releases)) | |||
for _, release := range releases { | |||
var rel = models.Release{ | |||
RepoID: g.repo.ID, | |||
PublisherID: g.doer.ID, | |||
TagName: release.TagName, | |||
LowerTagName: strings.ToLower(release.TagName), | |||
Target: release.TargetCommitish, | |||
Title: release.Name, | |||
Sha1: release.TargetCommitish, | |||
Note: release.Body, | |||
IsDraft: release.Draft, | |||
IsPrerelease: release.Prerelease, | |||
IsTag: false, | |||
CreatedUnix: timeutil.TimeStamp(release.Created.Unix()), | |||
OriginalAuthor: release.PublisherName, | |||
OriginalAuthorID: release.PublisherID, | |||
RepoID: g.repo.ID, | |||
TagName: release.TagName, | |||
LowerTagName: strings.ToLower(release.TagName), | |||
Target: release.TargetCommitish, | |||
Title: release.Name, | |||
Sha1: release.TargetCommitish, | |||
Note: release.Body, | |||
IsDraft: release.Draft, | |||
IsPrerelease: release.Prerelease, | |||
IsTag: false, | |||
CreatedUnix: timeutil.TimeStamp(release.Created.Unix()), | |||
} | |||
userid, ok := g.userMap[release.PublisherID] | |||
tp := g.gitServiceType.Name() | |||
if !ok && tp != "" { | |||
var err error | |||
userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", release.PublisherID)) | |||
if err != nil { | |||
log.Error("GetUserIDByExternalUserID: %v", err) | |||
} | |||
if userid > 0 { | |||
g.userMap[release.PublisherID] = userid | |||
} | |||
} | |||
if userid > 0 { | |||
rel.PublisherID = userid | |||
} else { | |||
rel.PublisherID = g.doer.ID | |||
rel.OriginalAuthor = release.PublisherName | |||
rel.OriginalAuthorID = release.PublisherID | |||
} | |||
// calc NumCommits | |||
@@ -284,20 +307,39 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { | |||
} | |||
var is = models.Issue{ | |||
RepoID: g.repo.ID, | |||
Repo: g.repo, | |||
Index: issue.Number, | |||
PosterID: g.doer.ID, | |||
OriginalAuthor: issue.PosterName, | |||
OriginalAuthorID: issue.PosterID, | |||
Title: issue.Title, | |||
Content: issue.Content, | |||
IsClosed: issue.State == "closed", | |||
IsLocked: issue.IsLocked, | |||
MilestoneID: milestoneID, | |||
Labels: labels, | |||
CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()), | |||
RepoID: g.repo.ID, | |||
Repo: g.repo, | |||
Index: issue.Number, | |||
Title: issue.Title, | |||
Content: issue.Content, | |||
IsClosed: issue.State == "closed", | |||
IsLocked: issue.IsLocked, | |||
MilestoneID: milestoneID, | |||
Labels: labels, | |||
CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()), | |||
} | |||
userid, ok := g.userMap[issue.PosterID] | |||
tp := g.gitServiceType.Name() | |||
if !ok && tp != "" { | |||
var err error | |||
userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", issue.PosterID)) | |||
if err != nil { | |||
log.Error("GetUserIDByExternalUserID: %v", err) | |||
} | |||
if userid > 0 { | |||
g.userMap[issue.PosterID] = userid | |||
} | |||
} | |||
if userid > 0 { | |||
is.PosterID = userid | |||
} else { | |||
is.PosterID = g.doer.ID | |||
is.OriginalAuthor = issue.PosterName | |||
is.OriginalAuthorID = issue.PosterID | |||
} | |||
if issue.Closed != nil { | |||
is.ClosedUnix = timeutil.TimeStamp(issue.Closed.Unix()) | |||
} | |||
@@ -331,15 +373,35 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { | |||
issueID = issueIDStr.(int64) | |||
} | |||
cms = append(cms, &models.Comment{ | |||
IssueID: issueID, | |||
Type: models.CommentTypeComment, | |||
PosterID: g.doer.ID, | |||
OriginalAuthor: comment.PosterName, | |||
OriginalAuthorID: comment.PosterID, | |||
Content: comment.Content, | |||
CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()), | |||
}) | |||
userid, ok := g.userMap[comment.PosterID] | |||
tp := g.gitServiceType.Name() | |||
if !ok && tp != "" { | |||
var err error | |||
userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", comment.PosterID)) | |||
if err != nil { | |||
log.Error("GetUserIDByExternalUserID: %v", err) | |||
} | |||
if userid > 0 { | |||
g.userMap[comment.PosterID] = userid | |||
} | |||
} | |||
cm := models.Comment{ | |||
IssueID: issueID, | |||
Type: models.CommentTypeComment, | |||
Content: comment.Content, | |||
CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()), | |||
} | |||
if userid > 0 { | |||
cm.PosterID = userid | |||
} else { | |||
cm.PosterID = g.doer.ID | |||
cm.OriginalAuthor = comment.PosterName | |||
cm.OriginalAuthorID = comment.PosterID | |||
} | |||
cms = append(cms, &cm) | |||
// TODO: Reactions | |||
} | |||
@@ -355,6 +417,28 @@ func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error | |||
if err != nil { | |||
return err | |||
} | |||
userid, ok := g.userMap[pr.PosterID] | |||
tp := g.gitServiceType.Name() | |||
if !ok && tp != "" { | |||
var err error | |||
userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", pr.PosterID)) | |||
if err != nil { | |||
log.Error("GetUserIDByExternalUserID: %v", err) | |||
} | |||
if userid > 0 { | |||
g.userMap[pr.PosterID] = userid | |||
} | |||
} | |||
if userid > 0 { | |||
gpr.Issue.PosterID = userid | |||
} else { | |||
gpr.Issue.PosterID = g.doer.ID | |||
gpr.Issue.OriginalAuthor = pr.PosterName | |||
gpr.Issue.OriginalAuthorID = pr.PosterID | |||
} | |||
gprs = append(gprs, gpr) | |||
} | |||
if err := models.InsertPullRequests(gprs...); err != nil { | |||
@@ -460,6 +544,40 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR | |||
head = pr.Head.Ref | |||
} | |||
var issue = models.Issue{ | |||
RepoID: g.repo.ID, | |||
Repo: g.repo, | |||
Title: pr.Title, | |||
Index: pr.Number, | |||
Content: pr.Content, | |||
MilestoneID: milestoneID, | |||
IsPull: true, | |||
IsClosed: pr.State == "closed", | |||
IsLocked: pr.IsLocked, | |||
Labels: labels, | |||
CreatedUnix: timeutil.TimeStamp(pr.Created.Unix()), | |||
} | |||
userid, ok := g.userMap[pr.PosterID] | |||
if !ok { | |||
var err error | |||
userid, err = models.GetUserIDByExternalUserID("github", fmt.Sprintf("%v", pr.PosterID)) | |||
if err != nil { | |||
log.Error("GetUserIDByExternalUserID: %v", err) | |||
} | |||
if userid > 0 { | |||
g.userMap[pr.PosterID] = userid | |||
} | |||
} | |||
if userid > 0 { | |||
issue.PosterID = userid | |||
} else { | |||
issue.PosterID = g.doer.ID | |||
issue.OriginalAuthor = pr.PosterName | |||
issue.OriginalAuthorID = pr.PosterID | |||
} | |||
var pullRequest = models.PullRequest{ | |||
HeadRepoID: g.repo.ID, | |||
HeadBranch: head, | |||
@@ -470,22 +588,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR | |||
Index: pr.Number, | |||
HasMerged: pr.Merged, | |||
Issue: &models.Issue{ | |||
RepoID: g.repo.ID, | |||
Repo: g.repo, | |||
Title: pr.Title, | |||
Index: pr.Number, | |||
PosterID: g.doer.ID, | |||
OriginalAuthor: pr.PosterName, | |||
OriginalAuthorID: pr.PosterID, | |||
Content: pr.Content, | |||
MilestoneID: milestoneID, | |||
IsPull: true, | |||
IsClosed: pr.State == "closed", | |||
IsLocked: pr.IsLocked, | |||
Labels: labels, | |||
CreatedUnix: timeutil.TimeStamp(pr.Created.Unix()), | |||
}, | |||
Issue: &issue, | |||
} | |||
if pullRequest.Issue.IsClosed && pr.Closed != nil { |
@@ -14,6 +14,7 @@ import ( | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/migrations/base" | |||
"code.gitea.io/gitea/modules/structs" | |||
"github.com/google/go-github/v24/github" | |||
"golang.org/x/oauth2" | |||
@@ -39,7 +40,7 @@ func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error | |||
return false, err | |||
} | |||
return u.Host == "github.com" && opts.AuthUsername != "", nil | |||
return strings.EqualFold(u.Host, "github.com") && opts.AuthUsername != "", nil | |||
} | |||
// New returns a Downloader related to this factory according MigrateOptions | |||
@@ -58,6 +59,11 @@ func (f *GithubDownloaderV3Factory) New(opts base.MigrateOptions) (base.Download | |||
return NewGithubDownloaderV3(opts.AuthUsername, opts.AuthPassword, oldOwner, oldName), nil | |||
} | |||
// GitServiceType returns the type of git service | |||
func (f *GithubDownloaderV3Factory) GitServiceType() structs.GitServiceType { | |||
return structs.GithubService | |||
} | |||
// GithubDownloaderV3 implements a Downloader interface to get repository informations | |||
// from github via APIv3 | |||
type GithubDownloaderV3 struct { |
@@ -11,6 +11,7 @@ import ( | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/migrations/base" | |||
"code.gitea.io/gitea/modules/structs" | |||
) | |||
// MigrateOptions is equal to base.MigrateOptions | |||
@@ -30,6 +31,7 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt | |||
var ( | |||
downloader base.Downloader | |||
uploader = NewGiteaLocalUploader(doer, ownerName, opts.RepoName) | |||
theFactory base.DownloaderFactory | |||
) | |||
for _, factory := range factories { | |||
@@ -40,6 +42,7 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt | |||
if err != nil { | |||
return nil, err | |||
} | |||
theFactory = factory | |||
break | |||
} | |||
} | |||
@@ -52,10 +55,14 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt | |||
opts.Comments = false | |||
opts.Issues = false | |||
opts.PullRequests = false | |||
opts.GitServiceType = structs.PlainGitService | |||
downloader = NewPlainGitDownloader(ownerName, opts.RepoName, opts.CloneAddr) | |||
log.Trace("Will migrate from git: %s", opts.CloneAddr) | |||
} else if opts.GitServiceType == structs.NotMigrated { | |||
opts.GitServiceType = theFactory.GitServiceType() | |||
} | |||
uploader.gitServiceType = opts.GitServiceType | |||
if err := migrateRepository(downloader, uploader, opts); err != nil { | |||
if err1 := uploader.Rollback(); err1 != nil { | |||
log.Error("rollback failed: %v", err1) |
@@ -0,0 +1,59 @@ | |||
// 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 ( | |||
"strconv" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/structs" | |||
) | |||
// UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID | |||
func UpdateMigrationPosterID() { | |||
for _, gitService := range structs.SupportedFullGitService { | |||
if err := updateMigrationPosterIDByGitService(gitService); err != nil { | |||
log.Error("updateMigrationPosterIDByGitService failed: %v", err) | |||
} | |||
} | |||
} | |||
func updateMigrationPosterIDByGitService(tp structs.GitServiceType) error { | |||
provider := tp.Name() | |||
if len(provider) == 0 { | |||
return nil | |||
} | |||
const batchSize = 100 | |||
var start int | |||
for { | |||
users, err := models.FindExternalUsersByProvider(models.FindExternalUserOptions{ | |||
Provider: provider, | |||
Start: start, | |||
Limit: batchSize, | |||
}) | |||
if err != nil { | |||
return err | |||
} | |||
for _, user := range users { | |||
externalUserID, err := strconv.ParseInt(user.ExternalID, 10, 64) | |||
if err != nil { | |||
log.Warn("Parse externalUser %#v 's userID failed: %v", user, err) | |||
continue | |||
} | |||
if err := models.UpdateMigrationsByType(tp, externalUserID, user.UserID); err != nil { | |||
log.Error("UpdateMigrationsByType type %s external user id %v to local user id %v failed: %v", tp.Name(), user.ExternalID, user.UserID, err) | |||
} | |||
} | |||
if len(users) < batchSize { | |||
break | |||
} | |||
start += len(users) | |||
} | |||
return nil | |||
} |
@@ -49,6 +49,9 @@ var ( | |||
Schedule string | |||
OlderThan time.Duration | |||
} `ini:"cron.deleted_branches_cleanup"` | |||
UpdateMigrationPosterID struct { | |||
Schedule string | |||
} `ini:"cron.update_migration_poster_id"` | |||
}{ | |||
UpdateMirror: struct { | |||
Enabled bool | |||
@@ -114,6 +117,11 @@ var ( | |||
Schedule: "@every 24h", | |||
OlderThan: 24 * time.Hour, | |||
}, | |||
UpdateMigrationPosterID: struct { | |||
Schedule string | |||
}{ | |||
Schedule: "@every 24h", | |||
}, | |||
} | |||
) | |||
@@ -153,6 +153,43 @@ type EditRepoOption struct { | |||
Archived *bool `json:"archived,omitempty"` | |||
} | |||
// GitServiceType represents a git service | |||
type GitServiceType int | |||
// enumerate all GitServiceType | |||
const ( | |||
NotMigrated GitServiceType = iota // 0 not migrated from external sites | |||
PlainGitService // 1 plain git service | |||
GithubService // 2 github.com | |||
GiteaService // 3 gitea service | |||
GitlabService // 4 gitlab service | |||
GogsService // 5 gogs service | |||
) | |||
// Name represents the service type's name | |||
// WARNNING: the name have to be equal to that on goth's library | |||
func (gt GitServiceType) Name() string { | |||
switch gt { | |||
case GithubService: | |||
return "github" | |||
case GiteaService: | |||
return "gitea" | |||
case GitlabService: | |||
return "gitlab" | |||
case GogsService: | |||
return "gogs" | |||
} | |||
return "" | |||
} | |||
var ( | |||
// SupportedFullGitService represents all git services supported to migrate issues/labels/prs and etc. | |||
// TODO: add to this list after new git service added | |||
SupportedFullGitService = []GitServiceType{ | |||
GithubService, | |||
} | |||
) | |||
// MigrateRepoOption options for migrating a repository from an external service | |||
type MigrateRepoOption struct { | |||
// required: true | |||
@@ -166,6 +203,8 @@ type MigrateRepoOption struct { | |||
Mirror bool `json:"mirror"` | |||
Private bool `json:"private"` | |||
Description string `json:"description"` | |||
OriginalURL string | |||
GitServiceType GitServiceType | |||
Wiki bool | |||
Issues bool | |||
Milestones bool |
@@ -8,6 +8,7 @@ package repo | |||
import ( | |||
"fmt" | |||
"net/http" | |||
"net/url" | |||
"strings" | |||
"code.gitea.io/gitea/models" | |||
@@ -17,6 +18,7 @@ import ( | |||
"code.gitea.io/gitea/modules/migrations" | |||
"code.gitea.io/gitea/modules/notification" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/structs" | |||
api "code.gitea.io/gitea/modules/structs" | |||
"code.gitea.io/gitea/modules/util" | |||
"code.gitea.io/gitea/modules/validation" | |||
@@ -397,21 +399,28 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { | |||
return | |||
} | |||
var gitServiceType = structs.PlainGitService | |||
u, err := url.Parse(remoteAddr) | |||
if err == nil && strings.EqualFold(u.Host, "github.com") { | |||
gitServiceType = structs.GithubService | |||
} | |||
var opts = migrations.MigrateOptions{ | |||
CloneAddr: remoteAddr, | |||
RepoName: form.RepoName, | |||
Description: form.Description, | |||
Private: form.Private || setting.Repository.ForcePrivate, | |||
Mirror: form.Mirror, | |||
AuthUsername: form.AuthUsername, | |||
AuthPassword: form.AuthPassword, | |||
Wiki: form.Wiki, | |||
Issues: form.Issues, | |||
Milestones: form.Milestones, | |||
Labels: form.Labels, | |||
Comments: true, | |||
PullRequests: form.PullRequests, | |||
Releases: form.Releases, | |||
CloneAddr: remoteAddr, | |||
RepoName: form.RepoName, | |||
Description: form.Description, | |||
Private: form.Private || setting.Repository.ForcePrivate, | |||
Mirror: form.Mirror, | |||
AuthUsername: form.AuthUsername, | |||
AuthPassword: form.AuthPassword, | |||
Wiki: form.Wiki, | |||
Issues: form.Issues, | |||
Milestones: form.Milestones, | |||
Labels: form.Labels, | |||
Comments: true, | |||
PullRequests: form.PullRequests, | |||
Releases: form.Releases, | |||
GitServiceType: gitServiceType, | |||
} | |||
if opts.Mirror { | |||
opts.Issues = false |
@@ -21,6 +21,7 @@ import ( | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/timeutil" | |||
"code.gitea.io/gitea/modules/util" | |||
"code.gitea.io/gitea/services/externalaccount" | |||
"code.gitea.io/gitea/services/mailer" | |||
"gitea.com/macaron/captcha" | |||
@@ -277,7 +278,7 @@ func TwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) { | |||
return | |||
} | |||
err = models.LinkAccountToUser(u, gothUser.(goth.User)) | |||
err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User)) | |||
if err != nil { | |||
ctx.ServerError("UserSignIn", err) | |||
return | |||
@@ -452,7 +453,7 @@ func U2FSign(ctx *context.Context, signResp u2f.SignResponse) { | |||
return | |||
} | |||
err = models.LinkAccountToUser(user, gothUser.(goth.User)) | |||
err = externalaccount.LinkAccountToUser(user, gothUser.(goth.User)) | |||
if err != nil { | |||
ctx.ServerError("UserSignIn", err) | |||
return | |||
@@ -601,36 +602,42 @@ func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context | |||
// Instead, redirect them to the 2FA authentication page. | |||
_, err = models.GetTwoFactorByUID(u.ID) | |||
if err != nil { | |||
if models.IsErrTwoFactorNotEnrolled(err) { | |||
err = ctx.Session.Set("uid", u.ID) | |||
if err != nil { | |||
log.Error(fmt.Sprintf("Error setting session: %v", err)) | |||
} | |||
err = ctx.Session.Set("uname", u.Name) | |||
if err != nil { | |||
log.Error(fmt.Sprintf("Error setting session: %v", err)) | |||
} | |||
if !models.IsErrTwoFactorNotEnrolled(err) { | |||
ctx.ServerError("UserSignIn", err) | |||
return | |||
} | |||
// Clear whatever CSRF has right now, force to generate a new one | |||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true) | |||
err = ctx.Session.Set("uid", u.ID) | |||
if err != nil { | |||
log.Error(fmt.Sprintf("Error setting session: %v", err)) | |||
} | |||
err = ctx.Session.Set("uname", u.Name) | |||
if err != nil { | |||
log.Error(fmt.Sprintf("Error setting session: %v", err)) | |||
} | |||
// Register last login | |||
u.SetLastLogin() | |||
if err := models.UpdateUserCols(u, "last_login_unix"); err != nil { | |||
ctx.ServerError("UpdateUserCols", err) | |||
return | |||
} | |||
// Clear whatever CSRF has right now, force to generate a new one | |||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true) | |||
if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 { | |||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true) | |||
ctx.RedirectToFirst(redirectTo) | |||
return | |||
} | |||
// Register last login | |||
u.SetLastLogin() | |||
if err := models.UpdateUserCols(u, "last_login_unix"); err != nil { | |||
ctx.ServerError("UpdateUserCols", err) | |||
return | |||
} | |||
ctx.Redirect(setting.AppSubURL + "/") | |||
} else { | |||
ctx.ServerError("UserSignIn", err) | |||
// update external user information | |||
if err := models.UpdateExternalUser(u, gothUser); err != nil { | |||
log.Error("UpdateExternalUser failed: %v", err) | |||
} | |||
if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 { | |||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true) | |||
ctx.RedirectToFirst(redirectTo) | |||
return | |||
} | |||
ctx.Redirect(setting.AppSubURL + "/") | |||
return | |||
} | |||
@@ -675,7 +682,7 @@ func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Requ | |||
} | |||
if hasUser { | |||
return user, goth.User{}, nil | |||
return user, gothUser, nil | |||
} | |||
// search in external linked users | |||
@@ -689,7 +696,7 @@ func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Requ | |||
} | |||
if hasUser { | |||
user, err = models.GetUserByID(externalLoginUser.UserID) | |||
return user, goth.User{}, err | |||
return user, gothUser, err | |||
} | |||
// no user found to login | |||
@@ -789,16 +796,18 @@ func LinkAccountPostSignIn(ctx *context.Context, signInForm auth.SignInForm) { | |||
// Instead, redirect them to the 2FA authentication page. | |||
_, err = models.GetTwoFactorByUID(u.ID) | |||
if err != nil { | |||
if models.IsErrTwoFactorNotEnrolled(err) { | |||
err = models.LinkAccountToUser(u, gothUser.(goth.User)) | |||
if err != nil { | |||
ctx.ServerError("UserLinkAccount", err) | |||
} else { | |||
handleSignIn(ctx, u, signInForm.Remember) | |||
} | |||
} else { | |||
if !models.IsErrTwoFactorNotEnrolled(err) { | |||
ctx.ServerError("UserLinkAccount", err) | |||
return | |||
} | |||
err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User)) | |||
if err != nil { | |||
ctx.ServerError("UserLinkAccount", err) | |||
return | |||
} | |||
handleSignIn(ctx, u, signInForm.Remember) | |||
return | |||
} | |||
@@ -947,6 +956,11 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au | |||
} | |||
} | |||
// update external user information | |||
if err := models.UpdateExternalUser(u, gothUser.(goth.User)); err != nil { | |||
log.Error("UpdateExternalUser failed: %v", err) | |||
} | |||
// Send confirmation email | |||
if setting.Service.RegisterEmailConfirm && u.ID > 1 { | |||
mailer.SendActivateAccountMail(ctx.Locale, u) |
@@ -0,0 +1,66 @@ | |||
// 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 externalaccount | |||
import ( | |||
"strconv" | |||
"strings" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/structs" | |||
"github.com/markbates/goth" | |||
) | |||
// LinkAccountToUser link the gothUser to the user | |||
func LinkAccountToUser(user *models.User, gothUser goth.User) error { | |||
loginSource, err := models.GetActiveOAuth2LoginSourceByName(gothUser.Provider) | |||
if err != nil { | |||
return err | |||
} | |||
externalLoginUser := &models.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, | |||
} | |||
if err := models.LinkExternalToUser(user, externalLoginUser); err != nil { | |||
return err | |||
} | |||
externalID, err := strconv.ParseInt(externalLoginUser.ExternalID, 10, 64) | |||
if err != nil { | |||
return err | |||
} | |||
var tp structs.GitServiceType | |||
for _, s := range structs.SupportedFullGitService { | |||
if strings.EqualFold(s.Name(), gothUser.Provider) { | |||
tp = s | |||
break | |||
} | |||
} | |||
if tp.Name() != "" { | |||
return models.UpdateMigrationsByType(tp, externalID, user.ID) | |||
} | |||
return nil | |||
} |