Update migrated repositories' issues/comments/prs poster id if user has a github external user saved (#7751)
* 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 message
This commit is contained in:
parent
ba201aaa44
commit
e3e44a59d0
21 changed files with 740 additions and 159 deletions
|
@ -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"`
|
ExternalID string `xorm:"pk NOT NULL"`
|
||||||
UserID int64 `xorm:"INDEX NOT NULL"`
|
UserID int64 `xorm:"INDEX NOT NULL"`
|
||||||
LoginSourceID int64 `xorm:"pk 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
|
// LinkExternalToUser link the external user to the user
|
||||||
func LinkAccountToUser(user *User, gothUser goth.User) error {
|
func LinkExternalToUser(user *User, externalLoginUser *ExternalLoginUser) error {
|
||||||
loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider)
|
has, err := x.Where("external_id=? AND login_source_id=?", externalLoginUser.ExternalID, externalLoginUser.LoginSourceID).
|
||||||
if err != nil {
|
NoAutoCondition().
|
||||||
return err
|
Exist(externalLoginUser)
|
||||||
}
|
|
||||||
|
|
||||||
externalLoginUser := &ExternalLoginUser{
|
|
||||||
ExternalID: gothUser.UserID,
|
|
||||||
UserID: user.ID,
|
|
||||||
LoginSourceID: loginSource.ID,
|
|
||||||
}
|
|
||||||
has, err := x.Get(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
|
||||||
|
|
83
models/migrations/v100.go
Normal file
83
models/migrations/v100.go
Normal file
|
@ -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"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
OwnerID int64 `xorm:"UNIQUE(s) index"`
|
OwnerID int64 `xorm:"UNIQUE(s) index"`
|
||||||
OwnerName string `xorm:"-"`
|
OwnerName string `xorm:"-"`
|
||||||
Owner *User `xorm:"-"`
|
Owner *User `xorm:"-"`
|
||||||
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
||||||
Name string `xorm:"INDEX NOT NULL"`
|
Name string `xorm:"INDEX NOT NULL"`
|
||||||
Description string `xorm:"TEXT"`
|
Description string `xorm:"TEXT"`
|
||||||
Website string `xorm:"VARCHAR(2048)"`
|
Website string `xorm:"VARCHAR(2048)"`
|
||||||
OriginalURL string `xorm:"VARCHAR(2048)"`
|
OriginalServiceType structs.GitServiceType `xorm:"index"`
|
||||||
DefaultBranch string
|
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"
|
mirrorUpdate = "mirror_update"
|
||||||
gitFsck = "git_fsck"
|
gitFsck = "git_fsck"
|
||||||
checkRepos = "check_repos"
|
checkRepos = "check_repos"
|
||||||
archiveCleanup = "archive_cleanup"
|
archiveCleanup = "archive_cleanup"
|
||||||
syncExternalUsers = "sync_external_users"
|
syncExternalUsers = "sync_external_users"
|
||||||
deletedBranchesCleanup = "deleted_branches_cleanup"
|
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
|
doer *models.User
|
||||||
repoOwner string
|
repoOwner string
|
||||||
repoName string
|
repoName string
|
||||||
repo *models.Repository
|
repo *models.Repository
|
||||||
labels sync.Map
|
labels sync.Map
|
||||||
milestones sync.Map
|
milestones sync.Map
|
||||||
issues sync.Map
|
issues sync.Map
|
||||||
gitRepo *git.Repository
|
gitRepo *git.Repository
|
||||||
prHeadCache map[string]struct{}
|
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,
|
RepoName: g.repoName,
|
||||||
Description: repo.Description,
|
Description: repo.Description,
|
||||||
Mirror: repo.IsMirror,
|
OriginalURL: repo.OriginalURL,
|
||||||
CloneAddr: remoteAddr,
|
GitServiceType: opts.GitServiceType,
|
||||||
Private: repo.IsPrivate,
|
Mirror: repo.IsMirror,
|
||||||
Wiki: opts.Wiki,
|
CloneAddr: remoteAddr,
|
||||||
Releases: opts.Releases, // if didn't get releases, then sync them from tags
|
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,
|
RepoID: g.repo.ID,
|
||||||
PublisherID: g.doer.ID,
|
TagName: release.TagName,
|
||||||
TagName: release.TagName,
|
LowerTagName: strings.ToLower(release.TagName),
|
||||||
LowerTagName: strings.ToLower(release.TagName),
|
Target: release.TargetCommitish,
|
||||||
Target: release.TargetCommitish,
|
Title: release.Name,
|
||||||
Title: release.Name,
|
Sha1: release.TargetCommitish,
|
||||||
Sha1: release.TargetCommitish,
|
Note: release.Body,
|
||||||
Note: release.Body,
|
IsDraft: release.Draft,
|
||||||
IsDraft: release.Draft,
|
IsPrerelease: release.Prerelease,
|
||||||
IsPrerelease: release.Prerelease,
|
IsTag: false,
|
||||||
IsTag: false,
|
CreatedUnix: timeutil.TimeStamp(release.Created.Unix()),
|
||||||
CreatedUnix: timeutil.TimeStamp(release.Created.Unix()),
|
}
|
||||||
OriginalAuthor: release.PublisherName,
|
|
||||||
OriginalAuthorID: release.PublisherID,
|
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,
|
RepoID: g.repo.ID,
|
||||||
Repo: g.repo,
|
Repo: g.repo,
|
||||||
Index: issue.Number,
|
Index: issue.Number,
|
||||||
PosterID: g.doer.ID,
|
Title: issue.Title,
|
||||||
OriginalAuthor: issue.PosterName,
|
Content: issue.Content,
|
||||||
OriginalAuthorID: issue.PosterID,
|
IsClosed: issue.State == "closed",
|
||||||
Title: issue.Title,
|
IsLocked: issue.IsLocked,
|
||||||
Content: issue.Content,
|
MilestoneID: milestoneID,
|
||||||
IsClosed: issue.State == "closed",
|
Labels: labels,
|
||||||
IsLocked: issue.IsLocked,
|
CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()),
|
||||||
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{
|
userid, ok := g.userMap[comment.PosterID]
|
||||||
IssueID: issueID,
|
tp := g.gitServiceType.Name()
|
||||||
Type: models.CommentTypeComment,
|
if !ok && tp != "" {
|
||||||
PosterID: g.doer.ID,
|
var err error
|
||||||
OriginalAuthor: comment.PosterName,
|
userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", comment.PosterID))
|
||||||
OriginalAuthorID: comment.PosterID,
|
if err != nil {
|
||||||
Content: comment.Content,
|
log.Error("GetUserIDByExternalUserID: %v", err)
|
||||||
CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()),
|
}
|
||||||
})
|
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{
|
Issue: &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()),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
59
modules/migrations/update.go
Normal file
59
modules/migrations/update.go
Normal file
|
@ -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,
|
CloneAddr: remoteAddr,
|
||||||
RepoName: form.RepoName,
|
RepoName: form.RepoName,
|
||||||
Description: form.Description,
|
Description: form.Description,
|
||||||
Private: form.Private || setting.Repository.ForcePrivate,
|
Private: form.Private || setting.Repository.ForcePrivate,
|
||||||
Mirror: form.Mirror,
|
Mirror: form.Mirror,
|
||||||
AuthUsername: form.AuthUsername,
|
AuthUsername: form.AuthUsername,
|
||||||
AuthPassword: form.AuthPassword,
|
AuthPassword: form.AuthPassword,
|
||||||
Wiki: form.Wiki,
|
Wiki: form.Wiki,
|
||||||
Issues: form.Issues,
|
Issues: form.Issues,
|
||||||
Milestones: form.Milestones,
|
Milestones: form.Milestones,
|
||||||
Labels: form.Labels,
|
Labels: form.Labels,
|
||||||
Comments: true,
|
Comments: true,
|
||||||
PullRequests: form.PullRequests,
|
PullRequests: form.PullRequests,
|
||||||
Releases: form.Releases,
|
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) {
|
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// Register last login
|
|
||||||
u.SetLastLogin()
|
|
||||||
if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
|
|
||||||
ctx.ServerError("UpdateUserCols", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + "/")
|
|
||||||
} else {
|
|
||||||
ctx.ServerError("UserSignIn", err)
|
ctx.ServerError("UserSignIn", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Register last login
|
||||||
|
u.SetLastLogin()
|
||||||
|
if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
|
||||||
|
ctx.ServerError("UpdateUserCols", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
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 {
|
|
||||||
ctx.ServerError("UserLinkAccount", 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
|
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)
|
||||||
|
|
66
services/externalaccount/user.go
Normal file
66
services/externalaccount/user.go
Normal file
|
@ -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
|
||||||
|
}
|
Reference in a new issue