* 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.21.12.1
| @@ -690,6 +690,11 @@ SCHEDULE = @every 24h | |||||
| ; or only create new users if UPDATE_EXISTING is set to false | ; or only create new users if UPDATE_EXISTING is set to false | ||||
| UPDATE_EXISTING = true | 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] | [git] | ||||
| ; The path of git executable. If empty, Gitea searches through the PATH environment. | ; The path of git executable. If empty, Gitea searches through the PATH environment. | ||||
| PATH = | 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. | - `RUN_AT_START`: **true**: Run repository statistics check at start time. | ||||
| - `SCHEDULE`: **@every 24h**: Cron syntax for scheduling repository statistics check. | - `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`) | ## Git (`git`) | ||||
| - `PATH`: **""**: The path of git executable. If empty, Gitea searches through the PATH environment. | - `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`) | ### Cron - Repository Statistics Check (`cron.check_repo_stats`) | ||||
| - `RUN_AT_START`: 是否启动时自动运行仓库统计。 | - `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`) | ## Git (`git`) | ||||
| @@ -4,13 +4,34 @@ | |||||
| package models | 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 | // ExternalLoginUser makes the connecting between some existing user and additional external login sources | ||||
| type ExternalLoginUser struct { | 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 | // GetExternalLogin checks if a externalID in loginSourceID scope already exists | ||||
| @@ -32,23 +53,15 @@ func ListAccountLinks(user *User) ([]*ExternalLoginUser, error) { | |||||
| return externalAccounts, nil | 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 { | if err != nil { | ||||
| return err | return err | ||||
| } else if has { | } else if has { | ||||
| return ErrExternalLoginUserAlreadyExist{gothUser.UserID, user.ID, loginSource.ID} | |||||
| return ErrExternalLoginUserAlreadyExist{externalLoginUser.ExternalID, user.ID, externalLoginUser.LoginSourceID} | |||||
| } | } | ||||
| _, err = x.Insert(externalLoginUser) | _, err = x.Insert(externalLoginUser) | ||||
| @@ -72,3 +85,97 @@ func removeAllAccountLinks(e Engine, user *User) error { | |||||
| _, err := e.Delete(&ExternalLoginUser{UserID: user.ID}) | _, err := e.Delete(&ExternalLoginUser{UserID: user.ID}) | ||||
| return err | 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/base" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/structs" | |||||
| api "code.gitea.io/gitea/modules/structs" | api "code.gitea.io/gitea/modules/structs" | ||||
| "code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
| "code.gitea.io/gitea/modules/util" | "code.gitea.io/gitea/modules/util" | ||||
| @@ -32,7 +33,7 @@ type Issue struct { | |||||
| PosterID int64 `xorm:"INDEX"` | PosterID int64 `xorm:"INDEX"` | ||||
| Poster *User `xorm:"-"` | Poster *User `xorm:"-"` | ||||
| OriginalAuthor string | OriginalAuthor string | ||||
| OriginalAuthorID int64 | |||||
| OriginalAuthorID int64 `xorm:"index"` | |||||
| Title string `xorm:"name"` | Title string `xorm:"name"` | ||||
| Content string `xorm:"TEXT"` | Content string `xorm:"TEXT"` | ||||
| RenderedContent string `xorm:"-"` | RenderedContent string `xorm:"-"` | ||||
| @@ -1947,3 +1948,16 @@ func (issue *Issue) ResolveMentionsByVisibility(ctx DBContext, doer *User, menti | |||||
| return | 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/log" | ||||
| "code.gitea.io/gitea/modules/markup/markdown" | "code.gitea.io/gitea/modules/markup/markdown" | ||||
| "code.gitea.io/gitea/modules/references" | "code.gitea.io/gitea/modules/references" | ||||
| "code.gitea.io/gitea/modules/structs" | |||||
| api "code.gitea.io/gitea/modules/structs" | api "code.gitea.io/gitea/modules/structs" | ||||
| "code.gitea.io/gitea/modules/timeutil" | "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) { | func FetchCodeComments(issue *Issue, currentUser *User) (CodeComments, error) { | ||||
| return fetchCodeComments(x, issue, currentUser) | 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), | NewMigration("add original author name and id on migrated release", addOriginalAuthorOnMigratedReleases), | ||||
| // v99 -> v100 | // v99 -> v100 | ||||
| NewMigration("add task table and status column for repository table", addTaskTable), | 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 | // 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/git" | ||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/structs" | |||||
| api "code.gitea.io/gitea/modules/structs" | api "code.gitea.io/gitea/modules/structs" | ||||
| "code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
| @@ -366,3 +367,16 @@ func SyncReleasesWithTags(repo *Repository, gitRepo *git.Repository) error { | |||||
| } | } | ||||
| return nil | 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/options" | ||||
| "code.gitea.io/gitea/modules/process" | "code.gitea.io/gitea/modules/process" | ||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/structs" | |||||
| api "code.gitea.io/gitea/modules/structs" | api "code.gitea.io/gitea/modules/structs" | ||||
| "code.gitea.io/gitea/modules/sync" | "code.gitea.io/gitea/modules/sync" | ||||
| "code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
| @@ -137,16 +138,17 @@ const ( | |||||
| // Repository represents a git repository. | // Repository represents a git repository. | ||||
| type Repository struct { | 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 | NumWatches int | ||||
| NumStars int | NumStars int | ||||
| @@ -10,6 +10,7 @@ import ( | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/migrations" | |||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/sync" | "code.gitea.io/gitea/modules/sync" | ||||
| mirror_service "code.gitea.io/gitea/services/mirror" | mirror_service "code.gitea.io/gitea/services/mirror" | ||||
| @@ -18,12 +19,13 @@ import ( | |||||
| ) | ) | ||||
| const ( | 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() | var c = cron.New() | ||||
| @@ -117,6 +119,15 @@ func NewContext() { | |||||
| go WithUnique(deletedBranchesCleanup, models.RemoveOldDeletedBranches)() | 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() | c.Start() | ||||
| } | } | ||||
| @@ -5,6 +5,8 @@ | |||||
| package base | package base | ||||
| import "code.gitea.io/gitea/modules/structs" | |||||
| // Downloader downloads the site repo informations | // Downloader downloads the site repo informations | ||||
| type Downloader interface { | type Downloader interface { | ||||
| GetRepoInfo() (*Repository, error) | GetRepoInfo() (*Repository, error) | ||||
| @@ -21,4 +23,5 @@ type Downloader interface { | |||||
| type DownloaderFactory interface { | type DownloaderFactory interface { | ||||
| Match(opts MigrateOptions) (bool, error) | Match(opts MigrateOptions) (bool, error) | ||||
| New(opts MigrateOptions) (Downloader, error) | New(opts MigrateOptions) (Downloader, error) | ||||
| GitServiceType() structs.GitServiceType | |||||
| } | } | ||||
| @@ -34,15 +34,17 @@ var ( | |||||
| // GiteaLocalUploader implements an Uploader to gitea sites | // GiteaLocalUploader implements an Uploader to gitea sites | ||||
| type GiteaLocalUploader struct { | 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 | // NewGiteaLocalUploader creates an gitea Uploader via gitea API v1 | ||||
| @@ -52,6 +54,7 @@ func NewGiteaLocalUploader(doer *models.User, repoOwner, repoName string) *Gitea | |||||
| repoOwner: repoOwner, | repoOwner: repoOwner, | ||||
| repoName: repoName, | repoName: repoName, | ||||
| prHeadCache: make(map[string]struct{}), | 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{ | 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 | g.repo = r | ||||
| @@ -193,20 +198,38 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { | |||||
| var rels = make([]*models.Release, 0, len(releases)) | var rels = make([]*models.Release, 0, len(releases)) | ||||
| for _, release := range releases { | for _, release := range releases { | ||||
| var rel = models.Release{ | 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 | // calc NumCommits | ||||
| @@ -284,20 +307,39 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { | |||||
| } | } | ||||
| var is = models.Issue{ | 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 { | if issue.Closed != nil { | ||||
| is.ClosedUnix = timeutil.TimeStamp(issue.Closed.Unix()) | is.ClosedUnix = timeutil.TimeStamp(issue.Closed.Unix()) | ||||
| } | } | ||||
| @@ -331,15 +373,35 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { | |||||
| issueID = issueIDStr.(int64) | 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 | // TODO: Reactions | ||||
| } | } | ||||
| @@ -355,6 +417,28 @@ func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error | |||||
| if err != nil { | if err != nil { | ||||
| return err | 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) | gprs = append(gprs, gpr) | ||||
| } | } | ||||
| if err := models.InsertPullRequests(gprs...); err != nil { | if err := models.InsertPullRequests(gprs...); err != nil { | ||||
| @@ -460,6 +544,40 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR | |||||
| head = pr.Head.Ref | 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{ | var pullRequest = models.PullRequest{ | ||||
| HeadRepoID: g.repo.ID, | HeadRepoID: g.repo.ID, | ||||
| HeadBranch: head, | HeadBranch: head, | ||||
| @@ -470,22 +588,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR | |||||
| Index: pr.Number, | Index: pr.Number, | ||||
| HasMerged: pr.Merged, | 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 { | if pullRequest.Issue.IsClosed && pr.Closed != nil { | ||||
| @@ -14,6 +14,7 @@ import ( | |||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/migrations/base" | "code.gitea.io/gitea/modules/migrations/base" | ||||
| "code.gitea.io/gitea/modules/structs" | |||||
| "github.com/google/go-github/v24/github" | "github.com/google/go-github/v24/github" | ||||
| "golang.org/x/oauth2" | "golang.org/x/oauth2" | ||||
| @@ -39,7 +40,7 @@ func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error | |||||
| return false, err | 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 | // 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 | 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 | // GithubDownloaderV3 implements a Downloader interface to get repository informations | ||||
| // from github via APIv3 | // from github via APIv3 | ||||
| type GithubDownloaderV3 struct { | type GithubDownloaderV3 struct { | ||||
| @@ -11,6 +11,7 @@ import ( | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/migrations/base" | "code.gitea.io/gitea/modules/migrations/base" | ||||
| "code.gitea.io/gitea/modules/structs" | |||||
| ) | ) | ||||
| // MigrateOptions is equal to base.MigrateOptions | // MigrateOptions is equal to base.MigrateOptions | ||||
| @@ -30,6 +31,7 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt | |||||
| var ( | var ( | ||||
| downloader base.Downloader | downloader base.Downloader | ||||
| uploader = NewGiteaLocalUploader(doer, ownerName, opts.RepoName) | uploader = NewGiteaLocalUploader(doer, ownerName, opts.RepoName) | ||||
| theFactory base.DownloaderFactory | |||||
| ) | ) | ||||
| for _, factory := range factories { | for _, factory := range factories { | ||||
| @@ -40,6 +42,7 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt | |||||
| if err != nil { | if err != nil { | ||||
| return nil, err | return nil, err | ||||
| } | } | ||||
| theFactory = factory | |||||
| break | break | ||||
| } | } | ||||
| } | } | ||||
| @@ -52,10 +55,14 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt | |||||
| opts.Comments = false | opts.Comments = false | ||||
| opts.Issues = false | opts.Issues = false | ||||
| opts.PullRequests = false | opts.PullRequests = false | ||||
| opts.GitServiceType = structs.PlainGitService | |||||
| downloader = NewPlainGitDownloader(ownerName, opts.RepoName, opts.CloneAddr) | downloader = NewPlainGitDownloader(ownerName, opts.RepoName, opts.CloneAddr) | ||||
| log.Trace("Will migrate from git: %s", 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 err := migrateRepository(downloader, uploader, opts); err != nil { | ||||
| if err1 := uploader.Rollback(); err1 != nil { | if err1 := uploader.Rollback(); err1 != nil { | ||||
| log.Error("rollback failed: %v", err1) | 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 | Schedule string | ||||
| OlderThan time.Duration | OlderThan time.Duration | ||||
| } `ini:"cron.deleted_branches_cleanup"` | } `ini:"cron.deleted_branches_cleanup"` | ||||
| UpdateMigrationPosterID struct { | |||||
| Schedule string | |||||
| } `ini:"cron.update_migration_poster_id"` | |||||
| }{ | }{ | ||||
| UpdateMirror: struct { | UpdateMirror: struct { | ||||
| Enabled bool | Enabled bool | ||||
| @@ -114,6 +117,11 @@ var ( | |||||
| Schedule: "@every 24h", | Schedule: "@every 24h", | ||||
| OlderThan: 24 * time.Hour, | OlderThan: 24 * time.Hour, | ||||
| }, | }, | ||||
| UpdateMigrationPosterID: struct { | |||||
| Schedule string | |||||
| }{ | |||||
| Schedule: "@every 24h", | |||||
| }, | |||||
| } | } | ||||
| ) | ) | ||||
| @@ -153,6 +153,43 @@ type EditRepoOption struct { | |||||
| Archived *bool `json:"archived,omitempty"` | 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 | // MigrateRepoOption options for migrating a repository from an external service | ||||
| type MigrateRepoOption struct { | type MigrateRepoOption struct { | ||||
| // required: true | // required: true | ||||
| @@ -166,6 +203,8 @@ type MigrateRepoOption struct { | |||||
| Mirror bool `json:"mirror"` | Mirror bool `json:"mirror"` | ||||
| Private bool `json:"private"` | Private bool `json:"private"` | ||||
| Description string `json:"description"` | Description string `json:"description"` | ||||
| OriginalURL string | |||||
| GitServiceType GitServiceType | |||||
| Wiki bool | Wiki bool | ||||
| Issues bool | Issues bool | ||||
| Milestones bool | Milestones bool | ||||
| @@ -8,6 +8,7 @@ package repo | |||||
| import ( | import ( | ||||
| "fmt" | "fmt" | ||||
| "net/http" | "net/http" | ||||
| "net/url" | |||||
| "strings" | "strings" | ||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| @@ -17,6 +18,7 @@ import ( | |||||
| "code.gitea.io/gitea/modules/migrations" | "code.gitea.io/gitea/modules/migrations" | ||||
| "code.gitea.io/gitea/modules/notification" | "code.gitea.io/gitea/modules/notification" | ||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/structs" | |||||
| api "code.gitea.io/gitea/modules/structs" | api "code.gitea.io/gitea/modules/structs" | ||||
| "code.gitea.io/gitea/modules/util" | "code.gitea.io/gitea/modules/util" | ||||
| "code.gitea.io/gitea/modules/validation" | "code.gitea.io/gitea/modules/validation" | ||||
| @@ -397,21 +399,28 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { | |||||
| return | 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{ | 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 { | if opts.Mirror { | ||||
| opts.Issues = false | opts.Issues = false | ||||
| @@ -21,6 +21,7 @@ import ( | |||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
| "code.gitea.io/gitea/modules/util" | "code.gitea.io/gitea/modules/util" | ||||
| "code.gitea.io/gitea/services/externalaccount" | |||||
| "code.gitea.io/gitea/services/mailer" | "code.gitea.io/gitea/services/mailer" | ||||
| "gitea.com/macaron/captcha" | "gitea.com/macaron/captcha" | ||||
| @@ -277,7 +278,7 @@ func TwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) { | |||||
| return | return | ||||
| } | } | ||||
| err = models.LinkAccountToUser(u, gothUser.(goth.User)) | |||||
| err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User)) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("UserSignIn", err) | ctx.ServerError("UserSignIn", err) | ||||
| return | return | ||||
| @@ -452,7 +453,7 @@ func U2FSign(ctx *context.Context, signResp u2f.SignResponse) { | |||||
| return | return | ||||
| } | } | ||||
| err = models.LinkAccountToUser(user, gothUser.(goth.User)) | |||||
| err = externalaccount.LinkAccountToUser(user, gothUser.(goth.User)) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("UserSignIn", err) | ctx.ServerError("UserSignIn", err) | ||||
| return | return | ||||
| @@ -601,36 +602,42 @@ func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context | |||||
| // Instead, redirect them to the 2FA authentication page. | // Instead, redirect them to the 2FA authentication page. | ||||
| _, err = models.GetTwoFactorByUID(u.ID) | _, err = models.GetTwoFactorByUID(u.ID) | ||||
| if err != nil { | 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 | return | ||||
| } | } | ||||
| @@ -675,7 +682,7 @@ func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Requ | |||||
| } | } | ||||
| if hasUser { | if hasUser { | ||||
| return user, goth.User{}, nil | |||||
| return user, gothUser, nil | |||||
| } | } | ||||
| // search in external linked users | // search in external linked users | ||||
| @@ -689,7 +696,7 @@ func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Requ | |||||
| } | } | ||||
| if hasUser { | if hasUser { | ||||
| user, err = models.GetUserByID(externalLoginUser.UserID) | user, err = models.GetUserByID(externalLoginUser.UserID) | ||||
| return user, goth.User{}, err | |||||
| return user, gothUser, err | |||||
| } | } | ||||
| // no user found to login | // 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. | // Instead, redirect them to the 2FA authentication page. | ||||
| _, err = models.GetTwoFactorByUID(u.ID) | _, err = models.GetTwoFactorByUID(u.ID) | ||||
| if err != nil { | 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) | ctx.ServerError("UserLinkAccount", err) | ||||
| return | |||||
| } | } | ||||
| handleSignIn(ctx, u, signInForm.Remember) | |||||
| return | 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 | // Send confirmation email | ||||
| if setting.Service.RegisterEmailConfirm && u.ID > 1 { | if setting.Service.RegisterEmailConfirm && u.ID > 1 { | ||||
| mailer.SendActivateAccountMail(ctx.Locale, u) | 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 | |||||
| } | |||||