From f2a3abc683ad4b2177b7c7c6160a2c0b4316120a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 13 Oct 2019 21:23:14 +0800 Subject: [PATCH] Move migrating repository from frontend to backend (#6200) * move migrating to backend * add loading image when migrating and fix tests * fix format * fix lint * add redis task queue support and improve docs * add redis vendor * fix vet * add database migrations and fix app.ini sample * add comments for task section on app.ini.sample * Update models/migrations/v84.go Co-Authored-By: lunny * Update models/repo.go Co-Authored-By: lunny * move migrating to backend * add loading image when migrating and fix tests * fix fmt * add redis task queue support and improve docs * fix fixtures * fix fixtures * fix duplicate function on index.js * fix tests * rename repository statuses * check if repository is being create when SSH request * fix lint * fix template * some improvements * fix template * unified migrate options * fix lint * fix loading page * refactor * When gitea restart, don't restart the running tasks because we may have servel gitea instances, that may break the migration * fix js * Update models/repo.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Update docs/content/doc/advanced/config-cheat-sheet.en-us.md Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * fix tests * rename ErrTaskIsNotExist to ErrTaskDoesNotExist * delete release after add one on tests to make it run happy * fix tests * fix tests * improve codes * fix lint * fix lint * fix migrations --- .gitignore | 1 + custom/conf/app.ini.sample | 9 + .../doc/advanced/config-cheat-sheet.en-us.md | 7 + .../doc/advanced/config-cheat-sheet.zh-cn.md | 7 + models/fixtures/repository.yml | 43 +++- models/migrations/migrations.go | 2 + models/migrations/v99.go | 34 +++ models/models.go | 1 + models/repo.go | 85 ++++--- models/task.go | 240 ++++++++++++++++++ modules/context/repo.go | 35 +-- modules/migrations/base/options.go | 21 +- modules/migrations/gitea.go | 36 ++- modules/migrations/gitea_test.go | 7 +- modules/migrations/github.go | 4 +- modules/migrations/migrate.go | 12 +- modules/setting/setting.go | 1 + modules/setting/task.go | 25 ++ modules/structs/repo.go | 16 +- modules/structs/task.go | 34 +++ modules/task/migrate.go | 120 +++++++++ modules/task/queue.go | 14 + modules/task/queue_channel.go | 48 ++++ modules/task/queue_redis.go | 130 ++++++++++ modules/task/task.go | 66 +++++ options/locale/locale_en-US.ini | 2 + public/img/loading.png | Bin 0 -> 18713 bytes public/js/index.js | 36 +++ routers/api/v1/repo/repo.go | 4 +- routers/init.go | 4 + routers/private/serv.go | 9 + routers/repo/repo.go | 103 +++++--- routers/repo/view.go | 30 +++ routers/routes/routes.go | 2 + services/mirror/mirror_test.go | 29 ++- templates/repo/header.tmpl | 166 ++++++------ templates/repo/migrating.tmpl | 31 +++ 37 files changed, 1192 insertions(+), 222 deletions(-) create mode 100644 models/migrations/v99.go create mode 100644 models/task.go create mode 100644 modules/setting/task.go create mode 100644 modules/structs/task.go create mode 100644 modules/task/migrate.go create mode 100644 modules/task/queue.go create mode 100644 modules/task/queue_channel.go create mode 100644 modules/task/queue_redis.go create mode 100644 modules/task/task.go create mode 100644 public/img/loading.png create mode 100644 templates/repo/migrating.tmpl diff --git a/.gitignore b/.gitignore index fa6cbb454b..773b4726c0 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,4 @@ prime/ *.snap *.snap-build *_source.tar.bz2 +.DS_Store \ No newline at end of file diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 9bfddc97e8..dd14089d2b 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -808,3 +808,12 @@ IS_INPUT_FILE = false ENABLED = false ; If you want to add authorization, specify a token here TOKEN = + +[task] +; Task queue type, could be `channel` or `redis`. +QUEUE_TYPE = channel +; Task queue length, available only when `QUEUE_TYPE` is `channel`. +QUEUE_LENGTH = 1000 +; Task queue connction string, available only when `QUEUE_TYPE` is `redis`. +; If there is a password of redis, use `addrs=127.0.0.1:6379 password=123 db=0`. +QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0" \ No newline at end of file diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 198cff6f04..ed34be032b 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -514,9 +514,16 @@ Two special environment variables are passed to the render command: - `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths. ## Time (`time`) + - `FORMAT`: Time format to diplay on UI. i.e. RFC1123 or 2006-01-02 15:04:05 - `DEFAULT_UI_LOCATION`: Default location of time on the UI, so that we can display correct user's time on UI. i.e. Shanghai/Asia +## Task (`task`) + +- `QUEUE_TYPE`: **channel**: Task queue type, could be `channel` or `redis`. +- `QUEUE_LENGTH`: **1000**: Task queue length, available only when `QUEUE_TYPE` is `channel`. +- `QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Task queue connection string, available only when `QUEUE_TYPE` is `redis`. If there redis needs a password, use `addrs=127.0.0.1:6379 password=123 db=0`. + ## Other (`other`) - `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer. diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index 541d66f4e9..01ba821a47 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -241,9 +241,16 @@ IS_INPUT_FILE = false - IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。 ## Time (`time`) + - `FORMAT`: 显示在界面上的时间格式。比如: RFC1123 或者 2006-01-02 15:04:05 - `DEFAULT_UI_LOCATION`: 默认显示在界面上的时区,默认为本地时区。比如: Asia/Shanghai +## Task (`task`) + +- `QUEUE_TYPE`: **channel**: 任务队列类型,可以为 `channel` 或 `redis`。 +- `QUEUE_LENGTH`: **1000**: 任务队列长度,当 `QUEUE_TYPE` 为 `channel` 时有效。 +- `QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: 任务队列连接字符串,当 `QUEUE_TYPE` 为 `redis` 时有效。如果redis有密码,则可以 `addrs=127.0.0.1:6379 password=123 db=0`。 + ## Other (`other`) - `SHOW_FOOTER_BRANDING`: 为真则在页面底部显示Gitea的字样。 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 2e38c5e1dd..cf7d24c6cd 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -11,6 +11,7 @@ num_milestones: 3 num_closed_milestones: 1 num_watches: 3 + status: 0 - id: 2 @@ -24,6 +25,7 @@ num_closed_pulls: 0 num_stars: 1 close_issues_via_commit_in_any_branch: true + status: 0 - id: 3 @@ -36,6 +38,7 @@ num_pulls: 0 num_closed_pulls: 0 num_watches: 0 + status: 0 - id: 4 @@ -48,6 +51,7 @@ num_pulls: 0 num_closed_pulls: 0 num_stars: 1 + status: 0 - id: 5 @@ -61,6 +65,7 @@ num_closed_pulls: 0 num_watches: 0 is_mirror: true + status: 0 - id: 6 @@ -73,6 +78,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 7 @@ -85,6 +91,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 8 @@ -97,6 +104,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 9 @@ -109,6 +117,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 10 @@ -122,6 +131,7 @@ num_closed_pulls: 0 is_mirror: false num_forks: 1 + status: 0 - id: 11 @@ -135,6 +145,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 12 @@ -147,6 +158,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 13 @@ -159,6 +171,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 14 @@ -172,6 +185,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 15 @@ -179,6 +193,7 @@ lower_name: repo15 name: repo15 is_empty: true + status: 0 - id: 16 @@ -191,6 +206,7 @@ num_pulls: 0 num_closed_pulls: 0 num_watches: 0 + status: 0 - id: 17 @@ -205,6 +221,7 @@ num_watches: 0 is_mirror: false is_fork: false + status: 0 - id: 18 @@ -218,6 +235,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 19 @@ -231,6 +249,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 20 @@ -244,6 +263,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 21 @@ -257,6 +277,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 22 @@ -270,6 +291,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 23 @@ -283,6 +305,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 24 @@ -296,6 +319,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 25 @@ -310,6 +334,7 @@ num_watches: 0 is_mirror: true is_fork: false + status: 0 - id: 26 @@ -324,6 +349,7 @@ num_watches: 0 is_mirror: true is_fork: false + status: 0 - id: 27 @@ -339,6 +365,7 @@ is_mirror: true num_forks: 1 is_fork: false + status: 0 - id: 28 @@ -354,6 +381,7 @@ is_mirror: true num_forks: 1 is_fork: false + status: 0 - id: 29 @@ -368,6 +396,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: true + status: 0 - id: 30 @@ -382,6 +411,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: true + status: 0 - id: 31 @@ -392,6 +422,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 32 # org public repo @@ -403,6 +434,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 33 @@ -410,6 +442,7 @@ lower_name: utf8 name: utf8 is_private: false + status: 0 - id: 34 @@ -421,6 +454,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 35 @@ -432,6 +466,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 36 @@ -443,6 +478,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 37 @@ -454,6 +490,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 38 @@ -465,6 +502,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 39 @@ -476,6 +514,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 40 @@ -487,6 +526,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 41 @@ -519,4 +559,5 @@ num_stars: 0 num_forks: 0 num_issues: 0 - is_mirror: false \ No newline at end of file + is_mirror: false + status: 0 diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index e14437a04b..ef5cd377a6 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -252,6 +252,8 @@ var migrations = []Migration{ NewMigration("add repo_admin_change_team_access to user", addRepoAdminChangeTeamAccessColumnForUser), // v98 -> v99 NewMigration("add original author name and id on migrated release", addOriginalAuthorOnMigratedReleases), + // v99 -> v100 + NewMigration("add task table and status column for repository table", addTaskTable), } // Migrate database to current version diff --git a/models/migrations/v99.go b/models/migrations/v99.go new file mode 100644 index 0000000000..3eb287af6c --- /dev/null +++ b/models/migrations/v99.go @@ -0,0 +1,34 @@ +// 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 ( + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/go-xorm/xorm" +) + +func addTaskTable(x *xorm.Engine) error { + type Task struct { + ID int64 + DoerID int64 `xorm:"index"` // operator + OwnerID int64 `xorm:"index"` // repo owner id, when creating, the repoID maybe zero + RepoID int64 `xorm:"index"` + Type structs.TaskType + Status structs.TaskStatus `xorm:"index"` + StartTime timeutil.TimeStamp + EndTime timeutil.TimeStamp + PayloadContent string `xorm:"TEXT"` + Errors string `xorm:"TEXT"` // if task failed, saved the error reason + Created timeutil.TimeStamp `xorm:"created"` + } + + type Repository struct { + Status int `xorm:"NOT NULL DEFAULT 0"` + } + + return x.Sync2(new(Task), new(Repository)) +} diff --git a/models/models.go b/models/models.go index e802a35a77..ea550cb839 100644 --- a/models/models.go +++ b/models/models.go @@ -112,6 +112,7 @@ func init() { new(OAuth2Application), new(OAuth2AuthorizationCode), new(OAuth2Grant), + new(Task), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/repo.go b/models/repo.go index 8db527477b..23b1c2ef52 100644 --- a/models/repo.go +++ b/models/repo.go @@ -126,6 +126,15 @@ func NewRepoContext() { RemoveAllWithNotice("Clean up repository temporary data", filepath.Join(setting.AppDataPath, "tmp")) } +// RepositoryStatus defines the status of repository +type RepositoryStatus int + +// all kinds of RepositoryStatus +const ( + RepositoryReady RepositoryStatus = iota // a normal repository + RepositoryBeingMigrated // repository is migrating +) + // Repository represents a git repository. type Repository struct { ID int64 `xorm:"pk autoincr"` @@ -156,9 +165,9 @@ type Repository struct { IsPrivate bool `xorm:"INDEX"` IsEmpty bool `xorm:"INDEX"` IsArchived bool `xorm:"INDEX"` - - IsMirror bool `xorm:"INDEX"` - *Mirror `xorm:"-"` + IsMirror bool `xorm:"INDEX"` + *Mirror `xorm:"-"` + Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"` ExternalMetas map[string]string `xorm:"-"` Units []*RepoUnit `xorm:"-"` @@ -197,6 +206,16 @@ func (repo *Repository) ColorFormat(s fmt.State) { repo.Name) } +// IsBeingMigrated indicates that repository is being migtated +func (repo *Repository) IsBeingMigrated() bool { + return repo.Status == RepositoryBeingMigrated +} + +// IsBeingCreated indicates that repository is being migrated or forked +func (repo *Repository) IsBeingCreated() bool { + return repo.IsBeingMigrated() +} + // AfterLoad is invoked from XORM after setting the values of all fields of this object. func (repo *Repository) AfterLoad() { // FIXME: use models migration to solve all at once. @@ -884,18 +903,6 @@ func (repo *Repository) CloneLink() (cl *CloneLink) { return repo.cloneLink(x, false) } -// MigrateRepoOptions contains the repository migrate options -type MigrateRepoOptions struct { - Name string - Description string - OriginalURL string - IsPrivate bool - IsMirror bool - RemoteAddr string - Wiki bool // include wiki repository - SyncReleasesWithTags bool // sync releases from tags -} - /* GitHub, GitLab, Gogs: *.wiki.git BitBucket: *.git/wiki @@ -915,20 +922,28 @@ func wikiRemoteURL(remote string) string { return "" } -// MigrateRepository migrates an existing repository from other project hosting. -func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, error) { - repo, err := CreateRepository(doer, u, CreateRepoOptions{ - Name: opts.Name, - Description: opts.Description, - OriginalURL: opts.OriginalURL, - IsPrivate: opts.IsPrivate, - IsMirror: opts.IsMirror, - }) - if err != nil { - return nil, err +// CheckCreateRepository check if could created a repository +func CheckCreateRepository(doer, u *User, name string) error { + if !doer.CanCreateRepo() { + return ErrReachLimitOfRepo{u.MaxRepoCreation} } - repoPath := RepoPath(u.Name, opts.Name) + if err := IsUsableRepoName(name); err != nil { + return err + } + + has, err := isRepositoryExist(x, u, name) + if err != nil { + return fmt.Errorf("IsRepositoryExist: %v", err) + } else if has { + return ErrRepoAlreadyExist{u.Name, name} + } + return nil +} + +// MigrateRepositoryGitData starts migrating git related data after created migrating repository +func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts api.MigrateRepoOption) (*Repository, error) { + repoPath := RepoPath(u.Name, opts.RepoName) if u.IsOrganization() { t, err := u.GetOwnerTeam() @@ -942,11 +957,12 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second - if err := os.RemoveAll(repoPath); err != nil { + var err error + if err = os.RemoveAll(repoPath); err != nil { return repo, fmt.Errorf("Failed to remove %s: %v", repoPath, err) } - if err = git.Clone(opts.RemoteAddr, repoPath, git.CloneRepoOptions{ + if err = git.Clone(opts.CloneAddr, repoPath, git.CloneRepoOptions{ Mirror: true, Quiet: true, Timeout: migrateTimeout, @@ -955,8 +971,8 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err } if opts.Wiki { - wikiPath := WikiPath(u.Name, opts.Name) - wikiRemotePath := wikiRemoteURL(opts.RemoteAddr) + wikiPath := WikiPath(u.Name, opts.RepoName) + wikiRemotePath := wikiRemoteURL(opts.CloneAddr) if len(wikiRemotePath) > 0 { if err := os.RemoveAll(wikiPath); err != nil { return repo, fmt.Errorf("Failed to remove %s: %v", wikiPath, err) @@ -986,7 +1002,7 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err return repo, fmt.Errorf("git.IsEmpty: %v", err) } - if opts.SyncReleasesWithTags && !repo.IsEmpty { + if !opts.Releases && !repo.IsEmpty { // Try to get HEAD branch and set it as default branch. headBranch, err := gitRepo.GetHEADBranch() if err != nil { @@ -1005,7 +1021,7 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err log.Error("Failed to update size for repository: %v", err) } - if opts.IsMirror { + if opts.Mirror { if _, err = x.InsertOne(&Mirror{ RepoID: repo.ID, Interval: setting.Mirror.DefaultInterval, @@ -1143,6 +1159,7 @@ type CreateRepoOptions struct { IsPrivate bool IsMirror bool AutoInit bool + Status RepositoryStatus } func getRepoInitFile(tp, name string) ([]byte, error) { @@ -1410,6 +1427,7 @@ func CreateRepository(doer, u *User, opts CreateRepoOptions) (_ *Repository, err IsPrivate: opts.IsPrivate, IsFsckEnabled: !opts.IsMirror, CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, + Status: opts.Status, } sess := x.NewSession() @@ -1856,6 +1874,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error { &CommitStatus{RepoID: repoID}, &RepoIndexerStatus{RepoID: repoID}, &Comment{RefRepoID: repoID}, + &Task{RepoID: repoID}, ); err != nil { return fmt.Errorf("deleteBeans: %v", err) } diff --git a/models/task.go b/models/task.go new file mode 100644 index 0000000000..cb878d387c --- /dev/null +++ b/models/task.go @@ -0,0 +1,240 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "encoding/json" + "fmt" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/migrations/base" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" +) + +// Task represents a task +type Task struct { + ID int64 + DoerID int64 `xorm:"index"` // operator + Doer *User `xorm:"-"` + OwnerID int64 `xorm:"index"` // repo owner id, when creating, the repoID maybe zero + Owner *User `xorm:"-"` + RepoID int64 `xorm:"index"` + Repo *Repository `xorm:"-"` + Type structs.TaskType + Status structs.TaskStatus `xorm:"index"` + StartTime timeutil.TimeStamp + EndTime timeutil.TimeStamp + PayloadContent string `xorm:"TEXT"` + Errors string `xorm:"TEXT"` // if task failed, saved the error reason + Created timeutil.TimeStamp `xorm:"created"` +} + +// LoadRepo loads repository of the task +func (task *Task) LoadRepo() error { + return task.loadRepo(x) +} + +func (task *Task) loadRepo(e Engine) error { + if task.Repo != nil { + return nil + } + var repo Repository + has, err := e.ID(task.RepoID).Get(&repo) + if err != nil { + return err + } else if !has { + return ErrRepoNotExist{ + ID: task.RepoID, + } + } + task.Repo = &repo + return nil +} + +// LoadDoer loads do user +func (task *Task) LoadDoer() error { + if task.Doer != nil { + return nil + } + + var doer User + has, err := x.ID(task.DoerID).Get(&doer) + if err != nil { + return err + } else if !has { + return ErrUserNotExist{ + UID: task.DoerID, + } + } + task.Doer = &doer + + return nil +} + +// LoadOwner loads owner user +func (task *Task) LoadOwner() error { + if task.Owner != nil { + return nil + } + + var owner User + has, err := x.ID(task.OwnerID).Get(&owner) + if err != nil { + return err + } else if !has { + return ErrUserNotExist{ + UID: task.OwnerID, + } + } + task.Owner = &owner + + return nil +} + +// UpdateCols updates some columns +func (task *Task) UpdateCols(cols ...string) error { + _, err := x.ID(task.ID).Cols(cols...).Update(task) + return err +} + +// MigrateConfig returns task config when migrate repository +func (task *Task) MigrateConfig() (*structs.MigrateRepoOption, error) { + if task.Type == structs.TaskTypeMigrateRepo { + var opts structs.MigrateRepoOption + err := json.Unmarshal([]byte(task.PayloadContent), &opts) + if err != nil { + return nil, err + } + return &opts, nil + } + return nil, fmt.Errorf("Task type is %s, not Migrate Repo", task.Type.Name()) +} + +// ErrTaskDoesNotExist represents a "TaskDoesNotExist" kind of error. +type ErrTaskDoesNotExist struct { + ID int64 + RepoID int64 + Type structs.TaskType +} + +// IsErrTaskDoesNotExist checks if an error is a ErrTaskIsNotExist. +func IsErrTaskDoesNotExist(err error) bool { + _, ok := err.(ErrTaskDoesNotExist) + return ok +} + +func (err ErrTaskDoesNotExist) Error() string { + return fmt.Sprintf("task is not exist [id: %d, repo_id: %d, type: %d]", + err.ID, err.RepoID, err.Type) +} + +// GetMigratingTask returns the migrating task by repo's id +func GetMigratingTask(repoID int64) (*Task, error) { + var task = Task{ + RepoID: repoID, + Type: structs.TaskTypeMigrateRepo, + } + has, err := x.Get(&task) + if err != nil { + return nil, err + } else if !has { + return nil, ErrTaskDoesNotExist{0, repoID, task.Type} + } + return &task, nil +} + +// FindTaskOptions find all tasks +type FindTaskOptions struct { + Status int +} + +// ToConds generates conditions for database operation. +func (opts FindTaskOptions) ToConds() builder.Cond { + var cond = builder.NewCond() + if opts.Status >= 0 { + cond = cond.And(builder.Eq{"status": opts.Status}) + } + return cond +} + +// FindTasks find all tasks +func FindTasks(opts FindTaskOptions) ([]*Task, error) { + var tasks = make([]*Task, 0, 10) + err := x.Where(opts.ToConds()).Find(&tasks) + return tasks, err +} + +func createTask(e Engine, task *Task) error { + _, err := e.Insert(task) + return err +} + +// CreateMigrateTask creates a migrate task +func CreateMigrateTask(doer, u *User, opts base.MigrateOptions) (*Task, error) { + bs, err := json.Marshal(&opts) + if err != nil { + return nil, err + } + + var task = Task{ + DoerID: doer.ID, + OwnerID: u.ID, + Type: structs.TaskTypeMigrateRepo, + Status: structs.TaskStatusQueue, + PayloadContent: string(bs), + } + + if err := createTask(x, &task); err != nil { + return nil, err + } + + repo, err := CreateRepository(doer, u, CreateRepoOptions{ + Name: opts.RepoName, + Description: opts.Description, + OriginalURL: opts.CloneAddr, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: RepositoryBeingMigrated, + }) + if err != nil { + task.EndTime = timeutil.TimeStampNow() + task.Status = structs.TaskStatusFailed + err2 := task.UpdateCols("end_time", "status") + if err2 != nil { + log.Error("UpdateCols Failed: %v", err2.Error()) + } + return nil, err + } + + task.RepoID = repo.ID + if err = task.UpdateCols("repo_id"); err != nil { + return nil, err + } + + return &task, nil +} + +// FinishMigrateTask updates database when migrate task finished +func FinishMigrateTask(task *Task) error { + task.Status = structs.TaskStatusFinished + task.EndTime = timeutil.TimeStampNow() + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + if _, err := sess.ID(task.ID).Cols("status", "end_time").Update(task); err != nil { + return err + } + task.Repo.Status = RepositoryReady + if _, err := sess.ID(task.RepoID).Cols("status").Update(task.Repo); err != nil { + return err + } + + return sess.Commit() +} diff --git a/modules/context/repo.go b/modules/context/repo.go index 3caf583f83..f4af19a0e8 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -146,6 +146,9 @@ func (r *Repository) FileExists(path string, branch string) (bool, error) { // GetEditorconfig returns the .editorconfig definition if found in the // HEAD of the default repo branch. func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) { + if r.GitRepo == nil { + return nil, nil + } commit, err := r.GitRepo.GetBranchCommit(r.Repository.DefaultBranch) if err != nil { return nil, err @@ -358,12 +361,6 @@ func RepoAssignment() macaron.Handler { return } - gitRepo, err := git.OpenRepository(models.RepoPath(userName, repoName)) - if err != nil { - ctx.ServerError("RepoAssignment Invalid repo "+models.RepoPath(userName, repoName), err) - return - } - ctx.Repo.GitRepo = gitRepo ctx.Repo.RepoLink = repo.Link() ctx.Data["RepoLink"] = ctx.Repo.RepoLink ctx.Data["RepoRelPath"] = ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name @@ -373,13 +370,6 @@ func RepoAssignment() macaron.Handler { ctx.Data["RepoExternalIssuesLink"] = unit.ExternalTrackerConfig().ExternalTrackerURL } - tags, err := ctx.Repo.GitRepo.GetTags() - if err != nil { - ctx.ServerError("GetTags", err) - return - } - ctx.Data["Tags"] = tags - count, err := models.GetReleaseCountByRepoID(ctx.Repo.Repository.ID, models.FindReleasesOptions{ IncludeDrafts: false, IncludeTags: true, @@ -425,12 +415,25 @@ func RepoAssignment() macaron.Handler { } // repo is empty and display enable - if ctx.Repo.Repository.IsEmpty { + if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBeingCreated() { ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch return } - ctx.Data["TagName"] = ctx.Repo.TagName + gitRepo, err := git.OpenRepository(models.RepoPath(userName, repoName)) + if err != nil { + ctx.ServerError("RepoAssignment Invalid repo "+models.RepoPath(userName, repoName), err) + return + } + ctx.Repo.GitRepo = gitRepo + + tags, err := ctx.Repo.GitRepo.GetTags() + if err != nil { + ctx.ServerError("GetTags", err) + return + } + ctx.Data["Tags"] = tags + brs, err := ctx.Repo.GitRepo.GetBranches() if err != nil { ctx.ServerError("GetBranches", err) @@ -439,6 +442,8 @@ func RepoAssignment() macaron.Handler { ctx.Data["Branches"] = brs ctx.Data["BranchesCount"] = len(brs) + ctx.Data["TagName"] = ctx.Repo.TagName + // If not branch selected, try default one. // If default branch doesn't exists, fall back to some other branch. if len(ctx.Repo.BranchName) == 0 { diff --git a/modules/migrations/base/options.go b/modules/migrations/base/options.go index ba7fdc6815..2d180b61d9 100644 --- a/modules/migrations/base/options.go +++ b/modules/migrations/base/options.go @@ -5,22 +5,7 @@ package base -// MigrateOptions defines the way a repository gets migrated -type MigrateOptions struct { - RemoteURL string - AuthUsername string - AuthPassword string - Name string - Description string - OriginalURL string +import "code.gitea.io/gitea/modules/structs" - Wiki bool - Issues bool - Milestones bool - Labels bool - Releases bool - Comments bool - PullRequests bool - Private bool - Mirror bool -} +// MigrateOptions defines the way a repository gets migrated +type MigrateOptions = structs.MigrateRepoOption diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go index 1edac47a6e..ab3b0b9f69 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" gouuid "github.com/satori/go.uuid" @@ -90,16 +91,33 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate remoteAddr = u.String() } - r, err := models.MigrateRepository(g.doer, owner, models.MigrateRepoOptions{ - Name: g.repoName, - Description: repo.Description, - OriginalURL: repo.OriginalURL, - IsMirror: repo.IsMirror, - RemoteAddr: remoteAddr, - IsPrivate: repo.IsPrivate, - Wiki: opts.Wiki, - SyncReleasesWithTags: !opts.Releases, // if didn't get releases, then sync them from tags + var r *models.Repository + if opts.MigrateToRepoID <= 0 { + r, err = models.CreateRepository(g.doer, owner, models.CreateRepoOptions{ + Name: g.repoName, + Description: repo.Description, + OriginalURL: repo.OriginalURL, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: models.RepositoryBeingMigrated, + }) + } else { + r, err = models.GetRepositoryByID(opts.MigrateToRepoID) + } + if err != nil { + return err + } + + 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 }) + g.repo = r if err != nil { return err diff --git a/modules/migrations/gitea_test.go b/modules/migrations/gitea_test.go index 88a3a6d218..73c119a15d 100644 --- a/modules/migrations/gitea_test.go +++ b/modules/migrations/gitea_test.go @@ -10,6 +10,7 @@ import ( "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -29,9 +30,9 @@ func TestGiteaUploadRepo(t *testing.T) { uploader = NewGiteaLocalUploader(user, user.Name, repoName) ) - err := migrateRepository(downloader, uploader, MigrateOptions{ - RemoteURL: "https://github.com/go-xorm/builder", - Name: repoName, + err := migrateRepository(downloader, uploader, structs.MigrateRepoOption{ + CloneAddr: "https://github.com/go-xorm/builder", + RepoName: repoName, AuthUsername: "", Wiki: true, diff --git a/modules/migrations/github.go b/modules/migrations/github.go index 754f98941c..1c5d96c03d 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -34,7 +34,7 @@ type GithubDownloaderV3Factory struct { // Match returns ture if the migration remote URL matched this downloader factory func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error) { - u, err := url.Parse(opts.RemoteURL) + u, err := url.Parse(opts.CloneAddr) if err != nil { return false, err } @@ -44,7 +44,7 @@ func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error // New returns a Downloader related to this factory according MigrateOptions func (f *GithubDownloaderV3Factory) New(opts base.MigrateOptions) (base.Downloader, error) { - u, err := url.Parse(opts.RemoteURL) + u, err := url.Parse(opts.CloneAddr) if err != nil { return nil, err } diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index 27782cb940..3f5c0d1118 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -6,6 +6,8 @@ package migrations import ( + "fmt" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" @@ -27,7 +29,7 @@ func RegisterDownloaderFactory(factory base.DownloaderFactory) { func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOptions) (*models.Repository, error) { var ( downloader base.Downloader - uploader = NewGiteaLocalUploader(doer, ownerName, opts.Name) + uploader = NewGiteaLocalUploader(doer, ownerName, opts.RepoName) ) for _, factory := range factories { @@ -50,14 +52,18 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt opts.Comments = false opts.Issues = false opts.PullRequests = false - downloader = NewPlainGitDownloader(ownerName, opts.Name, opts.RemoteURL) - log.Trace("Will migrate from git: %s", opts.RemoteURL) + downloader = NewPlainGitDownloader(ownerName, opts.RepoName, opts.CloneAddr) + log.Trace("Will migrate from git: %s", opts.CloneAddr) } if err := migrateRepository(downloader, uploader, opts); err != nil { if err1 := uploader.Rollback(); err1 != nil { log.Error("rollback failed: %v", err1) } + + if err2 := models.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.CloneAddr, err)); err2 != nil { + log.Error("create respotiry notice failed: ", err2) + } return nil, err } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 5e476854b2..8c61bdbb77 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1043,4 +1043,5 @@ func NewServices() { newNotifyMailService() newWebhookService() newIndexerService() + newTaskService() } diff --git a/modules/setting/task.go b/modules/setting/task.go new file mode 100644 index 0000000000..97704d4a4d --- /dev/null +++ b/modules/setting/task.go @@ -0,0 +1,25 @@ +// 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 setting + +var ( + // Task settings + Task = struct { + QueueType string + QueueLength int + QueueConnStr string + }{ + QueueType: ChannelQueueType, + QueueLength: 1000, + QueueConnStr: "addrs=127.0.0.1:6379 db=0", + } +) + +func newTaskService() { + sec := Cfg.Section("task") + Task.QueueType = sec.Key("QUEUE_TYPE").MustString(ChannelQueueType) + Task.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000) + Task.QueueConnStr = sec.Key("QUEUE_CONN_STR").MustString("addrs=127.0.0.1:6379 db=0") +} diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 87396d6ce9..57f1768a0b 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -162,8 +162,16 @@ type MigrateRepoOption struct { // required: true UID int `json:"uid" binding:"Required"` // required: true - RepoName string `json:"repo_name" binding:"Required"` - Mirror bool `json:"mirror"` - Private bool `json:"private"` - Description string `json:"description"` + RepoName string `json:"repo_name" binding:"Required"` + Mirror bool `json:"mirror"` + Private bool `json:"private"` + Description string `json:"description"` + Wiki bool + Issues bool + Milestones bool + Labels bool + Releases bool + Comments bool + PullRequests bool + MigrateToRepoID int64 } diff --git a/modules/structs/task.go b/modules/structs/task.go new file mode 100644 index 0000000000..e83d0437ce --- /dev/null +++ b/modules/structs/task.go @@ -0,0 +1,34 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package structs + +// TaskType defines task type +type TaskType int + +// all kinds of task types +const ( + TaskTypeMigrateRepo TaskType = iota // migrate repository from external or local disk +) + +// Name returns the task type name +func (taskType TaskType) Name() string { + switch taskType { + case TaskTypeMigrateRepo: + return "Migrate Repository" + } + return "" +} + +// TaskStatus defines task status +type TaskStatus int + +// enumerate all the kinds of task status +const ( + TaskStatusQueue TaskStatus = iota // 0 task is queue + TaskStatusRunning // 1 task is running + TaskStatusStopped // 2 task is stopped + TaskStatusFailed // 3 task is failed + TaskStatusFinished // 4 task is finished +) diff --git a/modules/task/migrate.go b/modules/task/migrate.go new file mode 100644 index 0000000000..5d15a506d7 --- /dev/null +++ b/modules/task/migrate.go @@ -0,0 +1,120 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package task + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/migrations" + "code.gitea.io/gitea/modules/notification" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +func handleCreateError(owner *models.User, err error, name string) error { + switch { + case models.IsErrReachLimitOfRepo(err): + return fmt.Errorf("You have already reached your limit of %d repositories", owner.MaxCreationLimit()) + case models.IsErrRepoAlreadyExist(err): + return errors.New("The repository name is already used") + case models.IsErrNameReserved(err): + return fmt.Errorf("The repository name '%s' is reserved", err.(models.ErrNameReserved).Name) + case models.IsErrNamePatternNotAllowed(err): + return fmt.Errorf("The pattern '%s' is not allowed in a repository name", err.(models.ErrNamePatternNotAllowed).Pattern) + default: + return err + } +} + +func runMigrateTask(t *models.Task) (err error) { + defer func() { + if e := recover(); e != nil { + var buf bytes.Buffer + fmt.Fprintf(&buf, "Handler crashed with error: %v", log.Stack(2)) + + err = errors.New(buf.String()) + } + + if err == nil { + err = models.FinishMigrateTask(t) + if err == nil { + notification.NotifyMigrateRepository(t.Doer, t.Owner, t.Repo) + return + } + + log.Error("FinishMigrateTask failed: %s", err.Error()) + } + + t.EndTime = timeutil.TimeStampNow() + t.Status = structs.TaskStatusFailed + t.Errors = err.Error() + if err := t.UpdateCols("status", "errors", "end_time"); err != nil { + log.Error("Task UpdateCols failed: %s", err.Error()) + } + + if t.Repo != nil { + if errDelete := models.DeleteRepository(t.Doer, t.OwnerID, t.Repo.ID); errDelete != nil { + log.Error("DeleteRepository: %v", errDelete) + } + } + }() + + if err := t.LoadRepo(); err != nil { + return err + } + + // if repository is ready, then just finsih the task + if t.Repo.Status == models.RepositoryReady { + return nil + } + + if err := t.LoadDoer(); err != nil { + return err + } + if err := t.LoadOwner(); err != nil { + return err + } + t.StartTime = timeutil.TimeStampNow() + t.Status = structs.TaskStatusRunning + if err := t.UpdateCols("start_time", "status"); err != nil { + return err + } + + var opts *structs.MigrateRepoOption + opts, err = t.MigrateConfig() + if err != nil { + return err + } + + opts.MigrateToRepoID = t.RepoID + repo, err := migrations.MigrateRepository(t.Doer, t.Owner.Name, *opts) + if err == nil { + notification.NotifyMigrateRepository(t.Doer, t.Owner, repo) + + log.Trace("Repository migrated [%d]: %s/%s", repo.ID, t.Owner.Name, repo.Name) + return nil + } + + if models.IsErrRepoAlreadyExist(err) { + return errors.New("The repository name is already used") + } + + // remoteAddr may contain credentials, so we sanitize it + err = util.URLSanitizedError(err, opts.CloneAddr) + if strings.Contains(err.Error(), "Authentication failed") || + strings.Contains(err.Error(), "could not read Username") { + return fmt.Errorf("Authentication failed: %v", err.Error()) + } else if strings.Contains(err.Error(), "fatal:") { + return fmt.Errorf("Migration failed: %v", err.Error()) + } + + return handleCreateError(t.Owner, err, "MigratePost") +} diff --git a/modules/task/queue.go b/modules/task/queue.go new file mode 100644 index 0000000000..ddee0b3d46 --- /dev/null +++ b/modules/task/queue.go @@ -0,0 +1,14 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package task + +import "code.gitea.io/gitea/models" + +// Queue defines an interface to run task queue +type Queue interface { + Run() error + Push(*models.Task) error + Stop() +} diff --git a/modules/task/queue_channel.go b/modules/task/queue_channel.go new file mode 100644 index 0000000000..da541f4755 --- /dev/null +++ b/modules/task/queue_channel.go @@ -0,0 +1,48 @@ +// 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 task + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" +) + +var ( + _ Queue = &ChannelQueue{} +) + +// ChannelQueue implements +type ChannelQueue struct { + queue chan *models.Task +} + +// NewChannelQueue create a memory channel queue +func NewChannelQueue(queueLen int) *ChannelQueue { + return &ChannelQueue{ + queue: make(chan *models.Task, queueLen), + } +} + +// Run starts to run the queue +func (c *ChannelQueue) Run() error { + for task := range c.queue { + err := Run(task) + if err != nil { + log.Error("Run task failed: %s", err.Error()) + } + } + return nil +} + +// Push will push the task ID to queue +func (c *ChannelQueue) Push(task *models.Task) error { + c.queue <- task + return nil +} + +// Stop stop the queue +func (c *ChannelQueue) Stop() { + close(c.queue) +} diff --git a/modules/task/queue_redis.go b/modules/task/queue_redis.go new file mode 100644 index 0000000000..127de0cdbf --- /dev/null +++ b/modules/task/queue_redis.go @@ -0,0 +1,130 @@ +// 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 task + +import ( + "encoding/json" + "errors" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + + "github.com/go-redis/redis" +) + +var ( + _ Queue = &RedisQueue{} +) + +type redisClient interface { + RPush(key string, args ...interface{}) *redis.IntCmd + LPop(key string) *redis.StringCmd + Ping() *redis.StatusCmd +} + +// RedisQueue redis queue +type RedisQueue struct { + client redisClient + queueName string + closeChan chan bool +} + +func parseConnStr(connStr string) (addrs, password string, dbIdx int, err error) { + fields := strings.Fields(connStr) + for _, f := range fields { + items := strings.SplitN(f, "=", 2) + if len(items) < 2 { + continue + } + switch strings.ToLower(items[0]) { + case "addrs": + addrs = items[1] + case "password": + password = items[1] + case "db": + dbIdx, err = strconv.Atoi(items[1]) + if err != nil { + return + } + } + } + return +} + +// NewRedisQueue creates single redis or cluster redis queue +func NewRedisQueue(addrs string, password string, dbIdx int) (*RedisQueue, error) { + dbs := strings.Split(addrs, ",") + var queue = RedisQueue{ + queueName: "task_queue", + closeChan: make(chan bool), + } + if len(dbs) == 0 { + return nil, errors.New("no redis host found") + } else if len(dbs) == 1 { + queue.client = redis.NewClient(&redis.Options{ + Addr: strings.TrimSpace(dbs[0]), // use default Addr + Password: password, // no password set + DB: dbIdx, // use default DB + }) + } else { + // cluster will ignore db + queue.client = redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: dbs, + Password: password, + }) + } + if err := queue.client.Ping().Err(); err != nil { + return nil, err + } + return &queue, nil +} + +// Run starts to run the queue +func (r *RedisQueue) Run() error { + for { + select { + case <-r.closeChan: + return nil + case <-time.After(time.Millisecond * 100): + } + + bs, err := r.client.LPop(r.queueName).Bytes() + if err != nil { + if err != redis.Nil { + log.Error("LPop failed: %v", err) + } + time.Sleep(time.Millisecond * 100) + continue + } + + var task models.Task + err = json.Unmarshal(bs, &task) + if err != nil { + log.Error("Unmarshal task failed: %s", err.Error()) + } else { + err = Run(&task) + if err != nil { + log.Error("Run task failed: %s", err.Error()) + } + } + } +} + +// Push implements Queue +func (r *RedisQueue) Push(task *models.Task) error { + bs, err := json.Marshal(task) + if err != nil { + return err + } + return r.client.RPush(r.queueName, bs).Err() +} + +// Stop stop the queue +func (r *RedisQueue) Stop() { + r.closeChan <- true +} diff --git a/modules/task/task.go b/modules/task/task.go new file mode 100644 index 0000000000..64744afe7a --- /dev/null +++ b/modules/task/task.go @@ -0,0 +1,66 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package task + +import ( + "fmt" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/migrations/base" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" +) + +// taskQueue is a global queue of tasks +var taskQueue Queue + +// Run a task +func Run(t *models.Task) error { + switch t.Type { + case structs.TaskTypeMigrateRepo: + return runMigrateTask(t) + default: + return fmt.Errorf("Unknow task type: %d", t.Type) + } +} + +// Init will start the service to get all unfinished tasks and run them +func Init() error { + switch setting.Task.QueueType { + case setting.ChannelQueueType: + taskQueue = NewChannelQueue(setting.Task.QueueLength) + case setting.RedisQueueType: + var err error + addrs, pass, idx, err := parseConnStr(setting.Task.QueueConnStr) + if err != nil { + return err + } + taskQueue, err = NewRedisQueue(addrs, pass, idx) + if err != nil { + return err + } + default: + return fmt.Errorf("Unsupported task queue type: %v", setting.Task.QueueType) + } + + go func() { + if err := taskQueue.Run(); err != nil { + log.Error("taskQueue.Run end failed: %v", err) + } + }() + + return nil +} + +// MigrateRepository add migration repository to task +func MigrateRepository(doer, u *models.User, opts base.MigrateOptions) error { + task, err := models.CreateMigrateTask(doer, u, opts) + if err != nil { + return err + } + + return taskQueue.Push(task) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ca09b6120d..e6c5839a64 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -633,6 +633,8 @@ migrate.lfs_mirror_unsupported = Mirroring LFS objects is not supported - use 'g migrate.migrate_items_options = When migrating from github, input a username and migration options will be displayed. migrated_from = Migrated from %[2]s migrated_from_fake = Migrated From %[1]s +migrate.migrating = Migrating from %s ... +migrate.migrating_failed = Migrating from %s failed. mirror_from = mirror of forked_from = forked from diff --git a/public/img/loading.png b/public/img/loading.png new file mode 100644 index 0000000000000000000000000000000000000000..aac702cfd6d010abb22f4ebd8f011b606075ef62 GIT binary patch literal 18713 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 literal 0 HcmV?d00001 diff --git a/public/js/index.js b/public/js/index.js index 8a85ad9157..3b15ad8f18 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -241,6 +241,41 @@ function updateIssuesMeta(url, action, issueIds, elementId) { }) } +function initRepoStatusChecker() { + const migrating = $("#repo_migrating"); + $('#repo_migrating_failed').hide(); + if (migrating) { + const repo_name = migrating.attr('repo'); + if (typeof repo_name === 'undefined') { + return + } + $.ajax({ + type: "GET", + url: suburl +"/"+repo_name+"/status", + data: { + "_csrf": csrf, + }, + complete: function(xhr) { + if (xhr.status == 200) { + if (xhr.responseJSON) { + if (xhr.responseJSON["status"] == 0) { + location.reload(); + return + } + + setTimeout(function () { + initRepoStatusChecker() + }, 2000); + return + } + } + $('#repo_migrating_progress').hide(); + $('#repo_migrating_failed').show(); + } + }) + } +} + function initReactionSelector(parent) { let reactions = ''; if (!parent) { @@ -2219,6 +2254,7 @@ $(document).ready(function () { initIssueList(); initWipTitle(); initPullRequestReview(); + initRepoStatusChecker(); // Repo clone url. if ($('#repo-clone-url').length > 0) { diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index d8b06862a5..08c0635bc3 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -398,8 +398,8 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { } var opts = migrations.MigrateOptions{ - RemoteURL: remoteAddr, - Name: form.RepoName, + CloneAddr: remoteAddr, + RepoName: form.RepoName, Description: form.Description, Private: form.Private || setting.Repository.ForcePrivate, Mirror: form.Mirror, diff --git a/routers/init.go b/routers/init.go index 1efddcfaa6..c37bbeb6b0 100644 --- a/routers/init.go +++ b/routers/init.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/ssh" + "code.gitea.io/gitea/modules/task" "code.gitea.io/gitea/services/mailer" mirror_service "code.gitea.io/gitea/services/mirror" @@ -102,6 +103,9 @@ func GlobalInit() { mirror_service.InitSyncMirrors() models.InitDeliverHooks() models.InitTestPullRequests() + if err := task.Init(); err != nil { + log.Fatal("Failed to initialize task scheduler: %v", err) + } } if setting.EnableSQLite3 { log.Info("SQLite3 Supported") diff --git a/routers/private/serv.go b/routers/private/serv.go index 71c0f6ea2c..c4508b4cb5 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -119,6 +119,15 @@ func ServCommand(ctx *macaron.Context) { repo.OwnerName = ownerName results.RepoID = repo.ID + if repo.IsBeingCreated() { + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": "Repository is being created, you could retry after it finished", + }) + return + } + // We can shortcut at this point if the repo is a mirror if mode > models.AccessModeRead && repo.IsMirror { ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ diff --git a/routers/repo/repo.go b/routers/repo/repo.go index b67384d721..bfd0c771b0 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/migrations" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/task" "code.gitea.io/gitea/modules/util" "github.com/unknwon/com" @@ -133,8 +134,6 @@ func Create(ctx *context.Context) { func handleCreateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form interface{}) { switch { - case migrations.IsRateLimitError(err): - ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form) case models.IsErrReachLimitOfRepo(err): ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form) case models.IsErrRepoAlreadyExist(err): @@ -221,6 +220,40 @@ func Migrate(ctx *context.Context) { ctx.HTML(200, tplMigrate) } +func handleMigrateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form *auth.MigrateRepoForm) { + switch { + case migrations.IsRateLimitError(err): + ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form) + case migrations.IsTwoFactorAuthError(err): + ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form) + case models.IsErrReachLimitOfRepo(err): + ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form) + case models.IsErrRepoAlreadyExist(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + case models.IsErrNameReserved(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form) + case models.IsErrNamePatternNotAllowed(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form) + default: + remoteAddr, _ := form.ParseRemoteAddr(owner) + err = util.URLSanitizedError(err, remoteAddr) + if strings.Contains(err.Error(), "Authentication failed") || + strings.Contains(err.Error(), "Bad credentials") || + strings.Contains(err.Error(), "could not read Username") { + ctx.Data["Err_Auth"] = true + ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form) + } else if strings.Contains(err.Error(), "fatal:") { + ctx.Data["Err_CloneAddr"] = true + ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form) + } else { + ctx.ServerError(name, err) + } + } +} + // MigratePost response for migrating from external git repository func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { ctx.Data["Title"] = ctx.Tr("new_migrate") @@ -258,8 +291,8 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { } var opts = migrations.MigrateOptions{ - RemoteURL: remoteAddr, - Name: form.RepoName, + CloneAddr: remoteAddr, + RepoName: form.RepoName, Description: form.Description, Private: form.Private || setting.Repository.ForcePrivate, Mirror: form.Mirror, @@ -282,47 +315,19 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { opts.Releases = false } - repo, err := migrations.MigrateRepository(ctx.User, ctxUser.Name, opts) - if err == nil { - notification.NotifyCreateRepository(ctx.User, ctxUser, repo) - - log.Trace("Repository migrated [%d]: %s/%s successfully", repo.ID, ctxUser.Name, form.RepoName) - ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + form.RepoName) + err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName) + if err != nil { + handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) return } - switch { - case models.IsErrReachLimitOfRepo(err): - ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", ctxUser.MaxCreationLimit()), tplMigrate, &form) - case models.IsErrNameReserved(err): - ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplMigrate, &form) - case models.IsErrRepoAlreadyExist(err): - ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplMigrate, &form) - case models.IsErrNamePatternNotAllowed(err): - ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplMigrate, &form) - case migrations.IsRateLimitError(err): - ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tplMigrate, &form) - case migrations.IsTwoFactorAuthError(err): - ctx.Data["Err_Auth"] = true - ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tplMigrate, &form) - default: - // remoteAddr may contain credentials, so we sanitize it - err = util.URLSanitizedError(err, remoteAddr) - if strings.Contains(err.Error(), "Authentication failed") || - strings.Contains(err.Error(), "Bad credentials") || - strings.Contains(err.Error(), "could not read Username") { - ctx.Data["Err_Auth"] = true - ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tplMigrate, &form) - } else if strings.Contains(err.Error(), "fatal:") { - ctx.Data["Err_CloneAddr"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tplMigrate, &form) - } else { - ctx.ServerError("MigratePost", err) - } + err = task.MigrateRepository(ctx.User, ctxUser, opts) + if err == nil { + ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.RepoName) + return } + + handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) } // Action response for actions to a repository @@ -460,3 +465,19 @@ func Download(ctx *context.Context) { ctx.ServeFile(archivePath, ctx.Repo.Repository.Name+"-"+refName+ext) } + +// Status returns repository's status +func Status(ctx *context.Context) { + task, err := models.GetMigratingTask(ctx.Repo.Repository.ID) + if err != nil { + ctx.JSON(500, map[string]interface{}{ + "err": err, + }) + return + } + + ctx.JSON(200, map[string]interface{}{ + "status": ctx.Repo.Repository.Status, + "err": task.Errors, + }) +} diff --git a/routers/repo/view.go b/routers/repo/view.go index 1967b511ca..c4e6a69220 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -11,6 +11,7 @@ import ( "fmt" gotemplate "html/template" "io/ioutil" + "net/url" "path" "strings" @@ -31,6 +32,7 @@ const ( tplRepoHome base.TplName = "repo/home" tplWatchers base.TplName = "repo/watchers" tplForks base.TplName = "repo/forks" + tplMigrating base.TplName = "repo/migrating" ) func renderDirectory(ctx *context.Context, treeLink string) { @@ -356,9 +358,37 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st } } +func safeURL(address string) string { + u, err := url.Parse(address) + if err != nil { + return address + } + u.User = nil + return u.String() +} + // Home render repository home page func Home(ctx *context.Context) { if len(ctx.Repo.Units) > 0 { + if ctx.Repo.Repository.IsBeingCreated() { + task, err := models.GetMigratingTask(ctx.Repo.Repository.ID) + if err != nil { + ctx.ServerError("models.GetMigratingTask", err) + return + } + cfg, err := task.MigrateConfig() + if err != nil { + ctx.ServerError("task.MigrateConfig", err) + return + } + + ctx.Data["Repo"] = ctx.Repo + ctx.Data["MigrateTask"] = task + ctx.Data["CloneAddr"] = safeURL(cfg.CloneAddr) + ctx.HTML(200, tplMigrating) + return + } + var firstUnit *models.Unit for _, repoUnit := range ctx.Repo.Units { if repoUnit.Type == models.UnitTypeCode { diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 11f2029226..8dfcdb9c9b 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -845,6 +845,8 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/archive/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.Download) + m.Get("/status", reqRepoCodeReader, repo.Status) + m.Group("/branches", func() { m.Get("", repo.Branches) }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index 76bd4c72f7..9ad11b7265 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/structs" release_service "code.gitea.io/gitea/services/release" "github.com/stretchr/testify/assert" @@ -26,16 +27,26 @@ func TestRelease_MirrorDelete(t *testing.T) { repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) repoPath := models.RepoPath(user.Name, repo.Name) - migrationOptions := models.MigrateRepoOptions{ - Name: "test_mirror", - Description: "Test mirror", - IsPrivate: false, - IsMirror: true, - RemoteAddr: repoPath, - Wiki: true, - SyncReleasesWithTags: true, + opts := structs.MigrateRepoOption{ + RepoName: "test_mirror", + Description: "Test mirror", + Private: false, + Mirror: true, + CloneAddr: repoPath, + Wiki: true, + Releases: false, } - mirror, err := models.MigrateRepository(user, user, migrationOptions) + + mirrorRepo, err := models.CreateRepository(user, user, models.CreateRepoOptions{ + Name: opts.RepoName, + Description: opts.Description, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: models.RepositoryBeingMigrated, + }) + assert.NoError(t, err) + + mirror, err := models.MigrateRepositoryGitData(user, user, mirrorRepo, opts) assert.NoError(t, err) gitRepo, err := git.OpenRepository(repoPath) diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index fc7f1b660c..9fb3e32899 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -16,93 +16,95 @@ {{if .IsMirror}}
{{$.i18n.Tr "repo.mirror_from"}} {{MirrorAddress $.Mirror}}
{{end}} {{if .IsFork}}
{{$.i18n.Tr "repo.forked_from"}} {{SubStr .BaseRepo.RelLink 1 -1}}
{{end}} - +{{end}} +
+ {{if not .Repository.IsBeingCreated}} + -
- -{{end}} - -
- + {{end}}
- diff --git a/templates/repo/migrating.tmpl b/templates/repo/migrating.tmpl new file mode 100644 index 0000000000..34031d5653 --- /dev/null +++ b/templates/repo/migrating.tmpl @@ -0,0 +1,31 @@ +{{template "base/head" .}} +
+ {{template "repo/header" .}} +
+
+
+ {{template "base/alert" .}} +
+
+
+
+ +
+
+
+
+
+
+

{{.i18n.Tr "repo.migrate.migrating" .CloneAddr | Safe}}

+
+
+

{{.i18n.Tr "repo.migrate.migrating_failed" .CloneAddr | Safe}}

+
+
+
+
+
+
+
+
+{{template "base/footer" .}}