From b8d27629b447cde92113f5d24b66aace4ae298c1 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 27 Feb 2019 23:14:35 +0800 Subject: [PATCH 01/42] move migrating to backend --- models/models.go | 1 + models/repo.go | 69 ++++++--- models/task.go | 244 ++++++++++++++++++++++++++++++++ modules/context/repo.go | 35 +++-- modules/migrations/gitea.go | 3 +- modules/setting/task.go | 22 +++ modules/structs/task.go | 36 +++++ modules/task/migrate.go | 117 +++++++++++++++ modules/task/queue.go | 13 ++ modules/task/queue_channel.go | 48 +++++++ modules/task/task.go | 66 +++++++++ options/locale/locale_en-US.ini | 1 + public/js/index.js | 32 ++++- routers/init.go | 4 + routers/repo/repo.go | 56 ++++---- routers/repo/view.go | 20 +++ routers/routes/routes.go | 2 + templates/repo/header.tmpl | 2 +- templates/repo/migrating.tmpl | 16 +++ 19 files changed, 722 insertions(+), 65 deletions(-) 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/task.go create mode 100644 templates/repo/migrating.tmpl diff --git a/models/models.go b/models/models.go index e802a35a7770..ea550cb839fe 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 8db527477b03..9db4d3ed00ee 100644 --- a/models/repo.go +++ b/models/repo.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/sync" "code.gitea.io/gitea/modules/timeutil" @@ -126,6 +127,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 ( + RepositoryCreated RepositoryStatus = iota // a normal repository + RepositoryCreating // repository is migrating or forking +) + // Repository represents a git repository. type Repository struct { ID int64 `xorm:"pk autoincr"` @@ -156,9 +166,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 ExternalMetas map[string]string `xorm:"-"` Units []*RepoUnit `xorm:"-"` @@ -197,6 +207,11 @@ func (repo *Repository) ColorFormat(s fmt.State) { repo.Name) } +// IsCreating indicates that repository is creating +func (repo *Repository) IsCreating() bool { + return repo.Status == RepositoryCreating +} + // 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 +899,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,19 +918,44 @@ func wikiRemoteURL(remote string) string { return "" } -// MigrateRepository migrates an existing repository from other project hosting. -func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, error) { +// CheckCreateRepository check if could created a repository +func CheckCreateRepository(doer, u *User, name string) error { + if !doer.CanCreateRepo() { + return ErrReachLimitOfRepo{u.MaxRepoCreation} + } + + 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 +} + +// MigrateRepository migrates a existing repository from other project hosting. +func MigrateRepository(doer, u *User, opts structs.MigrateRepoOptions) (*Repository, error) { repo, err := CreateRepository(doer, u, CreateRepoOptions{ Name: opts.Name, Description: opts.Description, OriginalURL: opts.OriginalURL, IsPrivate: opts.IsPrivate, IsMirror: opts.IsMirror, + Status: RepositoryCreating, }) if err != nil { return nil, err } + return MigrateRepositoryGitData(doer, u, repo, opts) +} + +// MigrateRepositoryGitData starts migrating git related data after created migrating repository +func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts structs.MigrateRepoOptions) (*Repository, error) { repoPath := RepoPath(u.Name, opts.Name) if u.IsOrganization() { @@ -942,7 +970,8 @@ 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) } @@ -1006,6 +1035,7 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err } if opts.IsMirror { + if _, err = x.InsertOne(&Mirror{ RepoID: repo.ID, Interval: setting.Mirror.DefaultInterval, @@ -1143,6 +1173,7 @@ type CreateRepoOptions struct { IsPrivate bool IsMirror bool AutoInit bool + Status RepositoryStatus } func getRepoInitFile(tp, name string) ([]byte, error) { @@ -1410,6 +1441,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 +1888,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 000000000000..ff58465ce1a3 --- /dev/null +++ b/models/task.go @@ -0,0 +1,244 @@ +// 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 + 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.MigrateRepoOptions, error) { + if task.Type == structs.TaskTypeMigrateRepo { + var opts structs.MigrateRepoOptions + 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) +} + +// ErrTaskIsNotExist represents a "TaskIsNotExist" kind of error. +type ErrTaskIsNotExist struct { + ID int64 + RepoID int64 + Type structs.TaskType +} + +// IsErrTaskNotExist checks if an error is a ErrTaskIsNotExist. +func IsErrTaskNotExist(err error) bool { + _, ok := err.(ErrTaskIsNotExist) + return ok +} + +func (err ErrTaskIsNotExist) 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, ErrTaskIsNotExist{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 +} + +// CreateTask creates a task +func CreateTask(task *Task) error { + return createTask(x, task) +} + +// 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.Name, + Description: opts.Description, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: RepositoryCreating, + }) + 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 = RepositoryCreated + 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 3caf583f836a..7dddbdb383bf 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.IsCreating() { 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/gitea.go b/modules/migrations/gitea.go index 1edac47a6ece..7fad4c95a498 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,7 +91,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate remoteAddr = u.String() } - r, err := models.MigrateRepository(g.doer, owner, models.MigrateRepoOptions{ + r, err := models.MigrateRepository(g.doer, owner, structs.MigrateRepoOptions{ Name: g.repoName, Description: repo.Description, OriginalURL: repo.OriginalURL, diff --git a/modules/setting/task.go b/modules/setting/task.go new file mode 100644 index 000000000000..adcb24fa6721 --- /dev/null +++ b/modules/setting/task.go @@ -0,0 +1,22 @@ +// 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 + }{ + QueueType: ChannelQueueType, + QueueLength: 1000, + } +) + +func newTaskService() { + sec := Cfg.Section("task") + Task.QueueType = sec.Key("QUEUE_TYPE").MustString(ChannelQueueType) + Task.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000) +} diff --git a/modules/structs/task.go b/modules/structs/task.go new file mode 100644 index 000000000000..de490e31b059 --- /dev/null +++ b/modules/structs/task.go @@ -0,0 +1,36 @@ +// 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 +) + +// TaskStatus defines task status +type TaskStatus int + +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 +) + +// 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 +} diff --git a/modules/task/migrate.go b/modules/task/migrate.go new file mode 100644 index 000000000000..c97def22ed59 --- /dev/null +++ b/modules/task/migrate.go @@ -0,0 +1,117 @@ +// 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" + "runtime" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "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) error { + opts, err := t.MigrateConfig() + if err != nil { + return err + } + + defer func() { + if e := recover(); e != nil { + var buf bytes.Buffer + fmt.Fprintf(&buf, "Handler crashed with error: %v", e) + + for i := 1; ; i++ { + _, file, line, ok := runtime.Caller(i) + if !ok { + break + } else { + fmt.Fprintf(&buf, "\n") + } + fmt.Fprintf(&buf, "%v:%v", file, line) + } + + err = errors.New(buf.String()) + } + + if err != nil { + 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()) + } else if t.Repo != nil { + if errDelete := models.DeleteRepository(t.Doer, t.OwnerID, t.Repo.ID); errDelete != nil { + log.Error("DeleteRepository: %v", errDelete) + } + } + } else { + if err := models.FinishMigrateTask(t); err != nil { + log.Error("Task UpdateCols failed: %s", err.Error()) + } else { + notification.NotifyMigrateRepository(t.Doer, t.Owner, t.Repo) + } + } + }() + + if err := t.LoadRepo(); err != nil { + return err + } + + 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 + } + + repo, err := models.MigrateRepositoryGitData(t.Doer, t.Owner, t.Repo, *opts) + if err == nil { + 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.RemoteAddr) + 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 000000000000..99986fc7ed6f --- /dev/null +++ b/modules/task/queue.go @@ -0,0 +1,13 @@ +// 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 +} diff --git a/modules/task/queue_channel.go b/modules/task/queue_channel.go new file mode 100644 index 000000000000..b3a2dd6a18d3 --- /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 ( + "time" + + "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 { + select { + case task := <-c.queue: + err := Run(task) + if err != nil { + log.Error("Run task failed: %s", err.Error()) + } + case <-time.After(time.Millisecond * 100): + } + } +} + +// Push will push the task ID to queue +func (c *ChannelQueue) Push(task *models.Task) error { + c.queue <- task + return nil +} diff --git a/modules/task/task.go b/modules/task/task.go new file mode 100644 index 000000000000..270a79c4a6f2 --- /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/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 { + var err error + switch setting.Task.QueueType { + case setting.ChannelQueueType: + taskQueue = NewChannelQueue(setting.Task.QueueLength) + default: + return fmt.Errorf("Unsupported indexer queue type: %v", setting.Task.QueueType) + } + + go taskQueue.Run() + + tasks, err := models.FindTasks(models.FindTaskOptions{ + Status: int(structs.TaskStatusRunning), + }) + + if err != nil { + return fmt.Errorf("DeliverHooks: %v", err.Error()) + } + + // Update hook task status. + for _, t := range tasks { + if err := taskQueue.Push(t); err != nil { + return fmt.Errorf("Run Task: %v", err.Error()) + } + } + 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 ca09b6120d71..78d3a2a30c58 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -633,6 +633,7 @@ 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 ... mirror_from = mirror of forked_from = forked from diff --git a/public/js/index.js b/public/js/index.js index 8a85ad91579e..8923dcbc0e51 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -238,7 +238,36 @@ function updateIssuesMeta(url, action, issueIds, elementId) { }, success: resolve }) - }) + } +} + +function initRepoStatusChecker() { + console.log("initRepoStatusChecker") + var migrating = $("#repo_migrating"); + if (migrating) { + var repo_name = migrating.attr('repo'); + if (typeof repo_nane === 'undefined') { + return + } + $.ajax({ + type: "GET", + url: suburl +"/"+repo_name+"/status", + data: { + "_csrf": csrf, + } + }).done(function(resp) { + if (resp) { + if (resp["status"] == 0) { + location.reload(); + return + } + + setTimeout(function () { + initRepoStatusChecker() + }, 2000); + } + }) + } } function initReactionSelector(parent) { @@ -2219,6 +2248,7 @@ $(document).ready(function () { initIssueList(); initWipTitle(); initPullRequestReview(); + initRepoStatusChecker(); // Repo clone url. if ($('#repo-clone-url').length > 0) { diff --git a/routers/init.go b/routers/init.go index 1efddcfaa6c6..e630a1e3cf9a 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: %v", err) + } } if setting.EnableSQLite3 { log.Info("SQLite3 Supported") diff --git a/routers/repo/repo.go b/routers/repo/repo.go index b67384d72193..ab92e0027ede 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" @@ -282,47 +283,37 @@ 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) - return - } + err = models.CheckCreateRepository(ctx.User, ctxUser, opts.Name) + if err != nil { + if models.IsErrRepoAlreadyExist(err) { + ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), 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) + return } 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) + return } + + handleCreateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) + return } + + err = task.MigrateRepository(ctx.User, ctxUser, opts) + if err == nil { + ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.Name) + return + } + ctx.ServerError("MigrateRepository", err) } // Action response for actions to a repository @@ -460,3 +451,10 @@ func Download(ctx *context.Context) { ctx.ServeFile(archivePath, ctx.Repo.Repository.Name+"-"+refName+ext) } + +// Status returns repository's status +func Status(ctx *context.Context) { + ctx.JSON(200, map[string]interface{}{ + "status": ctx.Repo.Repository.Status, + }) +} diff --git a/routers/repo/view.go b/routers/repo/view.go index 1967b511ca4f..8fb50d2fcedc 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -31,6 +31,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) { @@ -359,6 +360,25 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st // Home render repository home page func Home(ctx *context.Context) { if len(ctx.Repo.Units) > 0 { + if ctx.Repo.Repository.IsCreating() { + 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["MigrateConfig"] = cfg + 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 11f2029226a7..8dfcdb9c9b64 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/templates/repo/header.tmpl b/templates/repo/header.tmpl index fc7f1b660ca6..288c51c6797e 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -47,7 +47,7 @@ {{end}} - +{{if not .Repository.IsCreating}}
+{{template "base/footer" .}} From b8d9e8c40440019dfc61fadba7c1de2c63bb59e2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 28 Feb 2019 19:35:42 +0800 Subject: [PATCH 02/42] add loading image when migrating and fix tests --- options/locale/locale_en-US.ini | 2 +- public/img/loading.png | Bin 0 -> 33407 bytes services/mirror/mirror_test.go | 3 +- templates/repo/header.tmpl | 52 +++++++++++++++++--------------- templates/repo/migrating.tmpl | 18 +++++++++-- 5 files changed, 45 insertions(+), 30 deletions(-) create mode 100644 public/img/loading.png diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 78d3a2a30c58..af56583d4266 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -633,7 +633,7 @@ 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 = Migrating from %s ... 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..4f65305d27fce36aa3b222aa07c56ecf1e700a55 GIT binary patch literal 33407 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.i18n.Tr "repo.mirror_from"}} {{MirrorAddress $.Mirror}}
{{end}} {{if .IsFork}}
{{$.i18n.Tr "repo.forked_from"}} {{SubStr .BaseRepo.RelLink 1 -1}}
{{end}} - {{end}} diff --git a/templates/repo/migrating.tmpl b/templates/repo/migrating.tmpl index 92e0235565bc..46a95dda1108 100644 --- a/templates/repo/migrating.tmpl +++ b/templates/repo/migrating.tmpl @@ -5,9 +5,21 @@
{{template "base/alert" .}} -
- {{.i18n.Tr "repo.migrate.migrating" .MigrateConfig.RemoteAddr}} -
+
+
+
+
+ +
+
+
+
+
+
+

{{.i18n.Tr "repo.migrate.migrating" .MigrateConfig.RemoteAddr | Safe}}

+
+
+
From 258afa42847d9ed5becfcce7247a58c5792adf1d Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 28 Feb 2019 19:45:59 +0800 Subject: [PATCH 03/42] fix format --- models/task.go | 2 +- modules/structs/task.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/models/task.go b/models/task.go index ff58465ce1a3..a4117bd0d649 100644 --- a/models/task.go +++ b/models/task.go @@ -112,7 +112,7 @@ func (task *Task) MigrateConfig() (*structs.MigrateRepoOptions, error) { } return &opts, nil } - return nil, fmt.Errorf("Task type is %s, not Migrate Repo", task.Type) + return nil, fmt.Errorf("Task type is %s, not Migrate Repo", task.Type.Name()) } // ErrTaskIsNotExist represents a "TaskIsNotExist" kind of error. diff --git a/modules/structs/task.go b/modules/structs/task.go index de490e31b059..f5fdca98352d 100644 --- a/modules/structs/task.go +++ b/modules/structs/task.go @@ -12,6 +12,15 @@ 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 From 58d35b06de4b794873ded4a37a272a63a1cccda3 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 28 Feb 2019 20:01:37 +0800 Subject: [PATCH 04/42] fix lint --- modules/structs/task.go | 1 + modules/task/migrate.go | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/structs/task.go b/modules/structs/task.go index f5fdca98352d..92f2a7f073ee 100644 --- a/modules/structs/task.go +++ b/modules/structs/task.go @@ -24,6 +24,7 @@ func (taskType TaskType) Name() string { // 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 diff --git a/modules/task/migrate.go b/modules/task/migrate.go index c97def22ed59..9b6cc60de9d4 100644 --- a/modules/task/migrate.go +++ b/modules/task/migrate.go @@ -22,13 +22,13 @@ import ( 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()) + 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.") + 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) + 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) + return fmt.Errorf("The pattern '%s' is not allowed in a repository name", err.(models.ErrNamePatternNotAllowed).Pattern) default: return err } @@ -101,7 +101,7 @@ func runMigrateTask(t *models.Task) error { } if models.IsErrRepoAlreadyExist(err) { - return errors.New("The repository name is already used.") + return errors.New("The repository name is already used") } // remoteAddr may contain credentials, so we sanitize it From 7771acb5af9391db3a24b3bb48ca40db37e554c1 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 1 Mar 2019 11:39:04 +0800 Subject: [PATCH 05/42] add redis task queue support and improve docs --- .gitignore | 1 + custom/conf/app.ini.sample | 5 + .../doc/advanced/config-cheat-sheet.en-us.md | 7 ++ .../doc/advanced/config-cheat-sheet.zh-cn.md | 7 ++ modules/setting/task.go | 11 +- modules/task/queue_redis.go | 119 ++++++++++++++++++ modules/task/task.go | 11 +- public/img/loading.png | Bin 33407 -> 18713 bytes public/js/index.js | 5 +- templates/repo/migrating.tmpl | 2 +- 10 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 modules/task/queue_redis.go diff --git a/.gitignore b/.gitignore index fa6cbb454b5c..773b4726c0ae 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 9bfddc97e8f2..44b3b4df9c84 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -808,3 +808,8 @@ IS_INPUT_FILE = false ENABLED = false ; If you want to add authorization, specify a token here TOKEN = + +[task] +QUEUE_TYPE = redis +QUEUE_LENGTH = 1000 +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 198cff6f0494..20a416d0c95f 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 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`. + ## 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 541d66f4e9b6..01ba821a47a3 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/modules/setting/task.go b/modules/setting/task.go index adcb24fa6721..97704d4a4da6 100644 --- a/modules/setting/task.go +++ b/modules/setting/task.go @@ -7,11 +7,13 @@ package setting var ( // Task settings Task = struct { - QueueType string - QueueLength int + QueueType string + QueueLength int + QueueConnStr string }{ - QueueType: ChannelQueueType, - QueueLength: 1000, + QueueType: ChannelQueueType, + QueueLength: 1000, + QueueConnStr: "addrs=127.0.0.1:6379 db=0", } ) @@ -19,4 +21,5 @@ 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/task/queue_redis.go b/modules/task/queue_redis.go new file mode 100644 index 000000000000..12a6ec3245bf --- /dev/null +++ b/modules/task/queue_redis.go @@ -0,0 +1,119 @@ +// 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 +} + +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", + } + 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 +} + +func (r *RedisQueue) Run() error { + for { + 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()) + } + } + + time.Sleep(time.Millisecond * 100) + } + return nil +} + +// 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() +} diff --git a/modules/task/task.go b/modules/task/task.go index 270a79c4a6f2..a80e3963ce40 100644 --- a/modules/task/task.go +++ b/modules/task/task.go @@ -32,8 +32,17 @@ func Init() error { switch setting.Task.QueueType { case setting.ChannelQueueType: taskQueue = NewChannelQueue(setting.Task.QueueLength) + case setting.RedisQueueType: + 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 indexer queue type: %v", setting.Task.QueueType) + return fmt.Errorf("Unsupported task queue type: %v", setting.Task.QueueType) } go taskQueue.Run() diff --git a/public/img/loading.png b/public/img/loading.png index 4f65305d27fce36aa3b222aa07c56ecf1e700a55..aac702cfd6d010abb22f4ebd8f011b606075ef62 100644 GIT binary patch literal 18713 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 literal 33407 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
-
+
From ac357dcafe6277b185454407e2da2c12d7a55692 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 1 Mar 2019 13:01:37 +0800 Subject: [PATCH 06/42] add redis vendor --- models/task.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/models/task.go b/models/task.go index a4117bd0d649..bdc359aedb22 100644 --- a/models/task.go +++ b/models/task.go @@ -26,7 +26,7 @@ type Task struct { RepoID int64 `xorm:"index"` Repo *Repository `xorm:"-"` Type structs.TaskType - Status structs.TaskStatus + Status structs.TaskStatus `xorm:"index"` StartTime timeutil.TimeStamp EndTime timeutil.TimeStamp PayloadContent string `xorm:"TEXT"` @@ -174,11 +174,6 @@ func createTask(e Engine, task *Task) error { return err } -// CreateTask creates a task -func CreateTask(task *Task) error { - return createTask(x, task) -} - // CreateMigrateTask creates a migrate task func CreateMigrateTask(doer, u *User, opts base.MigrateOptions) (*Task, error) { bs, err := json.Marshal(&opts) From dff614fc51d4608b258cc1f1df893313fe496305 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 1 Mar 2019 13:17:24 +0800 Subject: [PATCH 07/42] fix vet --- modules/task/queue_redis.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/task/queue_redis.go b/modules/task/queue_redis.go index 12a6ec3245bf..30f5f4229a10 100644 --- a/modules/task/queue_redis.go +++ b/modules/task/queue_redis.go @@ -82,6 +82,7 @@ func NewRedisQueue(addrs string, password string, dbIdx int) (*RedisQueue, error return &queue, nil } +// Run starts to run the queue func (r *RedisQueue) Run() error { for { bs, err := r.client.LPop(r.queueName).Bytes() @@ -106,7 +107,6 @@ func (r *RedisQueue) Run() error { time.Sleep(time.Millisecond * 100) } - return nil } // Push implements Queue From 75f2aa366696ca5e0ed1761ba66a6e76d5dbd85f Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 3 Apr 2019 22:08:31 +0800 Subject: [PATCH 08/42] add database migrations and fix app.ini sample --- custom/conf/app.ini.sample | 2 +- models/migrations/migrations.go | 2 ++ models/migrations/v99.go | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 models/migrations/v99.go diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 44b3b4df9c84..7eee53e39a9a 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -810,6 +810,6 @@ ENABLED = false TOKEN = [task] -QUEUE_TYPE = redis +QUEUE_TYPE = channel QUEUE_LENGTH = 1000 QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0" \ No newline at end of file diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index e14437a04b35..ef5cd377a6c1 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 000000000000..367925ffe064 --- /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 + } + + return x.Sync2(new(Task), new(Repository)) +} From 585fa50f970165b3a914d577571453742031f4e4 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 3 Apr 2019 22:49:29 +0800 Subject: [PATCH 09/42] add comments for task section on app.ini.sample --- custom/conf/app.ini.sample | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 7eee53e39a9a..dd14089d2b06 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -810,6 +810,10 @@ ENABLED = false 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 From fffcc0e42ca317bc04b4d946f17e92103886a82e Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Thu, 4 Apr 2019 22:28:51 +0800 Subject: [PATCH 10/42] Update models/migrations/v84.go Co-Authored-By: lunny --- models/migrations/v90.go | 1 + 1 file changed, 1 insertion(+) diff --git a/models/migrations/v90.go b/models/migrations/v90.go index 09aceae2f9e4..580552dd3acc 100644 --- a/models/migrations/v90.go +++ b/models/migrations/v90.go @@ -12,6 +12,7 @@ func changeSomeColumnsLengthOfRepo(x *xorm.Engine) error { Description string `xorm:"TEXT"` Website string `xorm:"VARCHAR(2048)"` OriginalURL string `xorm:"VARCHAR(2048)"` + Status int `xorm:"NOT NULL DEFAULT 0"` } return x.Sync2(new(Repository)) From 8a29d8a4c5afee6e8fcd9990cca57da64cf4da29 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Thu, 4 Apr 2019 22:29:10 +0800 Subject: [PATCH 11/42] Update models/repo.go Co-Authored-By: lunny --- models/repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/repo.go b/models/repo.go index 9db4d3ed00ee..19bb4dfeca34 100644 --- a/models/repo.go +++ b/models/repo.go @@ -168,7 +168,7 @@ type Repository struct { IsArchived bool `xorm:"INDEX"` IsMirror bool `xorm:"INDEX"` *Mirror `xorm:"-"` - Status RepositoryStatus + Status RepositoryStatus `xorm:"NOT NULL"` ExternalMetas map[string]string `xorm:"-"` Units []*RepoUnit `xorm:"-"` From 40a6d644c64fef1852686a964bde88f908d3bc85 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 27 Feb 2019 23:14:35 +0800 Subject: [PATCH 12/42] move migrating to backend --- models/repo.go | 2 +- public/js/index.js | 31 ++++++++++++++++++++++++++++++- templates/repo/migrating.tmpl | 2 +- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/models/repo.go b/models/repo.go index 19bb4dfeca34..21e3088114ba 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1441,7 +1441,7 @@ func CreateRepository(doer, u *User, opts CreateRepoOptions) (_ *Repository, err IsPrivate: opts.IsPrivate, IsFsckEnabled: !opts.IsMirror, CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, - Status: opts.Status, + Status: opts.Status, } sess := x.NewSession() diff --git a/public/js/index.js b/public/js/index.js index d44783801f9d..2b598d385a67 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -238,7 +238,36 @@ function updateIssuesMeta(url, action, issueIds, elementId) { }, success: resolve }) - }) + } +} + +function initRepoStatusChecker() { + console.log("initRepoStatusChecker") + var migrating = $("#repo_migrating"); + if (migrating) { + var repo_name = migrating.attr('repo'); + if (typeof repo_nane === 'undefined') { + return + } + $.ajax({ + type: "GET", + url: suburl +"/"+repo_name+"/status", + data: { + "_csrf": csrf, + } + }).done(function(resp) { + if (resp) { + if (resp["status"] == 0) { + location.reload(); + return + } + + setTimeout(function () { + initRepoStatusChecker() + }, 2000); + } + }) + } } function initRepoStatusChecker() { diff --git a/templates/repo/migrating.tmpl b/templates/repo/migrating.tmpl index c6b64d78a132..0bc1be3161d7 100644 --- a/templates/repo/migrating.tmpl +++ b/templates/repo/migrating.tmpl @@ -16,7 +16,7 @@
-

{{.i18n.Tr "repo.migrate.migrating" .MigrateConfig.RemoteAddr | Safe}}

+

{{.i18n.Tr "repo.migrate.migrating" .MigrateConfig.RemoteAddr}}

From 2bba6a92a9eead1e57934ec3a874a8bfe4d81c57 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 28 Feb 2019 19:35:42 +0800 Subject: [PATCH 13/42] add loading image when migrating and fix tests --- templates/repo/migrating.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/migrating.tmpl b/templates/repo/migrating.tmpl index 0bc1be3161d7..c6b64d78a132 100644 --- a/templates/repo/migrating.tmpl +++ b/templates/repo/migrating.tmpl @@ -16,7 +16,7 @@
-

{{.i18n.Tr "repo.migrate.migrating" .MigrateConfig.RemoteAddr}}

+

{{.i18n.Tr "repo.migrate.migrating" .MigrateConfig.RemoteAddr | Safe}}

From 2b1626d04ad251776bf5b910fe23d67b926ab384 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 28 Feb 2019 20:05:03 +0800 Subject: [PATCH 14/42] fix fmt --- models/repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/repo.go b/models/repo.go index 21e3088114ba..19bb4dfeca34 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1441,7 +1441,7 @@ func CreateRepository(doer, u *User, opts CreateRepoOptions) (_ *Repository, err IsPrivate: opts.IsPrivate, IsFsckEnabled: !opts.IsMirror, CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, - Status: opts.Status, + Status: opts.Status, } sess := x.NewSession() From 6b6ece76350c7d0a77568c18fd637fdbf91a112b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 1 Mar 2019 11:39:04 +0800 Subject: [PATCH 15/42] add redis task queue support and improve docs --- public/js/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/js/index.js b/public/js/index.js index 2b598d385a67..97692e9935f2 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -238,15 +238,14 @@ function updateIssuesMeta(url, action, issueIds, elementId) { }, success: resolve }) - } + }) } function initRepoStatusChecker() { - console.log("initRepoStatusChecker") var migrating = $("#repo_migrating"); if (migrating) { var repo_name = migrating.attr('repo'); - if (typeof repo_nane === 'undefined') { + if (typeof repo_name === 'undefined') { return } $.ajax({ From cf24a79aa475bc8b66bf7d16cbb59ecf797d4143 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 7 Apr 2019 09:03:16 +0800 Subject: [PATCH 16/42] fix fixtures --- models/fixtures/repository.yml | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 2e38c5e1dd6e..37714c34008b 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 @@ -443,6 +477,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 37 From 2643529529f2afd4f3b40de09e11dd3215219d9a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 12 Apr 2019 16:32:34 +0800 Subject: [PATCH 17/42] fix fixtures --- models/fixtures/repository.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 37714c34008b..875976f9c135 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -466,6 +466,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 36 From e018d40b7e8199b12054caadc64cfcd8c0940603 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 26 May 2019 20:25:41 +0800 Subject: [PATCH 18/42] fix duplicate function on index.js --- public/js/index.js | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/public/js/index.js b/public/js/index.js index 97692e9935f2..d44783801f9d 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -269,34 +269,6 @@ function initRepoStatusChecker() { } } -function initRepoStatusChecker() { - var migrating = $("#repo_migrating"); - if (migrating) { - var repo_name = migrating.attr('repo'); - if (typeof repo_name === 'undefined') { - return - } - $.ajax({ - type: "GET", - url: suburl +"/"+repo_name+"/status", - data: { - "_csrf": csrf, - } - }).done(function(resp) { - if (resp) { - if (resp["status"] == 0) { - location.reload(); - return - } - - setTimeout(function () { - initRepoStatusChecker() - }, 2000); - } - }) - } -} - function initReactionSelector(parent) { let reactions = ''; if (!parent) { From fe4557b68043067cfcd39ed34b7089a51a7bfbb4 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 26 May 2019 20:53:34 +0800 Subject: [PATCH 19/42] fix tests --- models/fixtures/repository.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 875976f9c135..cf7d24c6cdb3 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -490,6 +490,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 38 @@ -501,6 +502,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 39 @@ -512,6 +514,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 40 @@ -523,6 +526,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 41 @@ -555,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 From 8187434e98a53179a24b93ce5415da1f9c55ed3b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 31 May 2019 15:08:31 +0800 Subject: [PATCH 20/42] rename repository statuses --- models/repo.go | 17 +++++++++++------ models/task.go | 4 ++-- modules/context/repo.go | 2 +- routers/repo/view.go | 2 +- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/models/repo.go b/models/repo.go index 19bb4dfeca34..7de139cb695b 100644 --- a/models/repo.go +++ b/models/repo.go @@ -132,8 +132,8 @@ type RepositoryStatus int // all kinds of RepositoryStatus const ( - RepositoryCreated RepositoryStatus = iota // a normal repository - RepositoryCreating // repository is migrating or forking + RepositoryReady RepositoryStatus = iota // a normal repository + RepositoryBeingMigrated // repository is migrating ) // Repository represents a git repository. @@ -207,9 +207,14 @@ func (repo *Repository) ColorFormat(s fmt.State) { repo.Name) } -// IsCreating indicates that repository is creating -func (repo *Repository) IsCreating() bool { - return repo.Status == RepositoryCreating +// IsBeingMigrated indicates that repository is being migtated +func (repo *Repository) IsBeingMigrated() bool { + return repo.Status == RepositoryBeingMigrated +} + +// IsBeingCreated indicates that repository is being migtated 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. @@ -945,7 +950,7 @@ func MigrateRepository(doer, u *User, opts structs.MigrateRepoOptions) (*Reposit OriginalURL: opts.OriginalURL, IsPrivate: opts.IsPrivate, IsMirror: opts.IsMirror, - Status: RepositoryCreating, + Status: RepositoryBeingMigrated, }) if err != nil { return nil, err diff --git a/models/task.go b/models/task.go index bdc359aedb22..40326b63c37f 100644 --- a/models/task.go +++ b/models/task.go @@ -198,7 +198,7 @@ func CreateMigrateTask(doer, u *User, opts base.MigrateOptions) (*Task, error) { Description: opts.Description, IsPrivate: opts.Private, IsMirror: opts.Mirror, - Status: RepositoryCreating, + Status: RepositoryBeingMigrated, }) if err != nil { task.EndTime = timeutil.TimeStampNow() @@ -230,7 +230,7 @@ func FinishMigrateTask(task *Task) error { if _, err := sess.ID(task.ID).Cols("status", "end_time").Update(task); err != nil { return err } - task.Repo.Status = RepositoryCreated + task.Repo.Status = RepositoryBeingMigrated if _, err := sess.ID(task.RepoID).Cols("status").Update(task.Repo); err != nil { return err } diff --git a/modules/context/repo.go b/modules/context/repo.go index 7dddbdb383bf..f4af19a0e835 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -415,7 +415,7 @@ func RepoAssignment() macaron.Handler { } // repo is empty and display enable - if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsCreating() { + if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBeingCreated() { ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch return } diff --git a/routers/repo/view.go b/routers/repo/view.go index 8fb50d2fcedc..bea8c7a70521 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -360,7 +360,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st // Home render repository home page func Home(ctx *context.Context) { if len(ctx.Repo.Units) > 0 { - if ctx.Repo.Repository.IsCreating() { + if ctx.Repo.Repository.IsBeingCreated() { task, err := models.GetMigratingTask(ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("models.GetMigratingTask", err) From 10caeebfac3e34458e8e5a7271407beba81f0bba Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 1 Jun 2019 10:27:34 +0800 Subject: [PATCH 21/42] check if repository is being create when SSH request --- routers/private/serv.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/routers/private/serv.go b/routers/private/serv.go index 71c0f6ea2c48..c4508b4cb5e0 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{}{ From 8abee04e5add2044f524251dcf9ed666f5b0ff18 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 18 Jun 2019 12:27:55 +0800 Subject: [PATCH 22/42] fix lint --- modules/setting/setting.go | 1 + modules/task/migrate.go | 14 ++++++++------ modules/task/task.go | 7 ++++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 5e476854b229..8c61bdbb7719 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/task/migrate.go b/modules/task/migrate.go index 9b6cc60de9d4..3a04b65160a4 100644 --- a/modules/task/migrate.go +++ b/modules/task/migrate.go @@ -69,13 +69,15 @@ func runMigrateTask(t *models.Task) error { log.Error("DeleteRepository: %v", errDelete) } } - } else { - if err := models.FinishMigrateTask(t); err != nil { - log.Error("Task UpdateCols failed: %s", err.Error()) - } else { - notification.NotifyMigrateRepository(t.Doer, t.Owner, t.Repo) - } + return } + + if err := models.FinishMigrateTask(t); err != nil { + log.Error("Task UpdateCols failed: %s", err.Error()) + return + } + + notification.NotifyMigrateRepository(t.Doer, t.Owner, t.Repo) }() if err := t.LoadRepo(); err != nil { diff --git a/modules/task/task.go b/modules/task/task.go index a80e3963ce40..2ea1133ef028 100644 --- a/modules/task/task.go +++ b/modules/task/task.go @@ -8,6 +8,7 @@ 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" @@ -45,7 +46,11 @@ func Init() error { return fmt.Errorf("Unsupported task queue type: %v", setting.Task.QueueType) } - go taskQueue.Run() + go func() { + if err := taskQueue.Run(); err != nil { + log.Error("taskQueue.Run end failed: %v", err) + } + }() tasks, err := models.FindTasks(models.FindTaskOptions{ Status: int(structs.TaskStatusRunning), From a1b539cbc2d3f318c654465b1a5a8a00edf3090a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 18 Jun 2019 15:23:33 +0800 Subject: [PATCH 23/42] fix template --- templates/repo/header.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index a6ed5f5c6d51..6edd605a08bd 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -16,7 +16,7 @@ {{if .IsMirror}}
{{$.i18n.Tr "repo.mirror_from"}} {{MirrorAddress $.Mirror}}
{{end}} {{if .IsFork}}
{{$.i18n.Tr "repo.forked_from"}} {{SubStr .BaseRepo.RelLink 1 -1}}
{{end}}
- {{if not .IsCreating}} + {{if not .IsBeingCreated}} {{end}} -{{if not .Repository.IsCreating}} +{{if not .Repository.IsBeingCreated}}
-
+{{end}} \ No newline at end of file From 26d82d3072d2718ea506a19f34f569030bb742b5 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 11 Jul 2019 00:27:10 +0800 Subject: [PATCH 26/42] unified migrate options --- models/repo.go | 11 +++++------ models/task.go | 4 ++-- modules/migrations/base/options.go | 21 +++------------------ modules/migrations/gitea.go | 16 ++++++++-------- modules/migrations/migrate.go | 4 ++-- modules/structs/task.go | 26 +++++++++++++++++--------- modules/task/migrate.go | 2 +- routers/api/v1/repo/repo.go | 6 +++--- routers/repo/repo.go | 6 +++--- templates/repo/header.tmpl | 3 ++- templates/repo/migrating.tmpl | 2 +- 11 files changed, 47 insertions(+), 54 deletions(-) diff --git a/models/repo.go b/models/repo.go index 28eecd35a184..3e29df605190 100644 --- a/models/repo.go +++ b/models/repo.go @@ -32,7 +32,6 @@ import ( "code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/sync" "code.gitea.io/gitea/modules/timeutil" @@ -943,7 +942,7 @@ func CheckCreateRepository(doer, u *User, name string) error { } // MigrateRepository migrates a existing repository from other project hosting. -func MigrateRepository(doer, u *User, opts structs.MigrateRepoOptions) (*Repository, error) { +func MigrateRepository(doer, u *User, opts api.MigrateRepoOptions) (*Repository, error) { repo, err := CreateRepository(doer, u, CreateRepoOptions{ Name: opts.Name, Description: opts.Description, @@ -960,7 +959,7 @@ func MigrateRepository(doer, u *User, opts structs.MigrateRepoOptions) (*Reposit } // MigrateRepositoryGitData starts migrating git related data after created migrating repository -func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts structs.MigrateRepoOptions) (*Repository, error) { +func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts api.MigrateRepoOptions) (*Repository, error) { repoPath := RepoPath(u.Name, opts.Name) if u.IsOrganization() { @@ -980,7 +979,7 @@ func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts structs.Migr 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.RemoteURL, repoPath, git.CloneRepoOptions{ Mirror: true, Quiet: true, Timeout: migrateTimeout, @@ -990,7 +989,7 @@ func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts structs.Migr if opts.Wiki { wikiPath := WikiPath(u.Name, opts.Name) - wikiRemotePath := wikiRemoteURL(opts.RemoteAddr) + wikiRemotePath := wikiRemoteURL(opts.RemoteURL) if len(wikiRemotePath) > 0 { if err := os.RemoveAll(wikiPath); err != nil { return repo, fmt.Errorf("Failed to remove %s: %v", wikiPath, err) @@ -1020,7 +1019,7 @@ func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts structs.Migr 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 { diff --git a/models/task.go b/models/task.go index 40326b63c37f..6fd8cfc9a163 100644 --- a/models/task.go +++ b/models/task.go @@ -196,8 +196,8 @@ func CreateMigrateTask(doer, u *User, opts base.MigrateOptions) (*Task, error) { repo, err := CreateRepository(doer, u, CreateRepoOptions{ Name: opts.Name, Description: opts.Description, - IsPrivate: opts.Private, - IsMirror: opts.Mirror, + IsPrivate: opts.IsPrivate, + IsMirror: opts.IsMirror, Status: RepositoryBeingMigrated, }) if err != nil { diff --git a/modules/migrations/base/options.go b/modules/migrations/base/options.go index ba7fdc68156a..26387169c221 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.MigrateRepoOptions diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go index 7fad4c95a498..e4351500cb1c 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -92,14 +92,14 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate } r, err := models.MigrateRepository(g.doer, owner, structs.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 + Name: g.repoName, + Description: repo.Description, + OriginalURL: repo.OriginalURL, + IsMirror: repo.IsMirror, + RemoteURL: remoteAddr, + IsPrivate: repo.IsPrivate, + Wiki: opts.Wiki, + Releases: opts.Releases, // if didn't get releases, then sync them from tags }) g.repo = r if err != nil { diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index 27782cb94034..7f2e3b4e987a 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -72,8 +72,8 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts if err != nil { return err } - repo.IsPrivate = opts.Private - repo.IsMirror = opts.Mirror + repo.IsPrivate = opts.IsPrivate + repo.IsMirror = opts.IsMirror if opts.Description != "" { repo.Description = opts.Description } diff --git a/modules/structs/task.go b/modules/structs/task.go index 92f2a7f073ee..d6383930bbbf 100644 --- a/modules/structs/task.go +++ b/modules/structs/task.go @@ -33,14 +33,22 @@ const ( TaskStatusFinished // 4 task is finished ) -// MigrateRepoOptions contains the repository migrate options +// MigrateRepoOptions defines the way a repository gets migrated 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 + Name string + Description string + OriginalURL string + RemoteURL string + AuthUsername string + AuthPassword string + + Wiki bool + Issues bool + Milestones bool + Labels bool + Releases bool + Comments bool + PullRequests bool + IsPrivate bool + IsMirror bool } diff --git a/modules/task/migrate.go b/modules/task/migrate.go index e7d9b411d8b6..dfdedd445779 100644 --- a/modules/task/migrate.go +++ b/modules/task/migrate.go @@ -100,7 +100,7 @@ func runMigrateTask(t *models.Task) error { } // remoteAddr may contain credentials, so we sanitize it - err = util.URLSanitizedError(err, opts.RemoteAddr) + err = util.URLSanitizedError(err, opts.RemoteURL) if strings.Contains(err.Error(), "Authentication failed") || strings.Contains(err.Error(), "could not read Username") { return fmt.Errorf("Authentication failed: %v", err.Error()) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index d8b06862a5ea..d56d0d6771a1 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -401,8 +401,8 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { RemoteURL: remoteAddr, Name: form.RepoName, Description: form.Description, - Private: form.Private || setting.Repository.ForcePrivate, - Mirror: form.Mirror, + IsPrivate: form.Private || setting.Repository.ForcePrivate, + IsMirror: form.Mirror, AuthUsername: form.AuthUsername, AuthPassword: form.AuthPassword, Wiki: form.Wiki, @@ -413,7 +413,7 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { PullRequests: form.PullRequests, Releases: form.Releases, } - if opts.Mirror { + if opts.IsMirror { opts.Issues = false opts.Milestones = false opts.Labels = false diff --git a/routers/repo/repo.go b/routers/repo/repo.go index ab92e0027ede..ca884075dc3a 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -262,8 +262,8 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { RemoteURL: remoteAddr, Name: form.RepoName, Description: form.Description, - Private: form.Private || setting.Repository.ForcePrivate, - Mirror: form.Mirror, + IsPrivate: form.Private || setting.Repository.ForcePrivate, + IsMirror: form.Mirror, AuthUsername: form.AuthUsername, AuthPassword: form.AuthPassword, Wiki: form.Wiki, @@ -274,7 +274,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { PullRequests: form.PullRequests, Releases: form.Releases, } - if opts.Mirror { + if opts.IsMirror { opts.Issues = false opts.Milestones = false opts.Labels = false diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 0b72fe1e0167..e655d8b451ab 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -105,6 +105,7 @@ {{end}}
+{{end}}
+ -{{end}} \ No newline at end of file diff --git a/templates/repo/migrating.tmpl b/templates/repo/migrating.tmpl index c6b64d78a132..be7ab9eb24b1 100644 --- a/templates/repo/migrating.tmpl +++ b/templates/repo/migrating.tmpl @@ -16,7 +16,7 @@
-

{{.i18n.Tr "repo.migrate.migrating" .MigrateConfig.RemoteAddr | Safe}}

+

{{.i18n.Tr "repo.migrate.migrating" .MigrateConfig.RemoteURL | Safe}}

From a1b174426ae55158a71178768f0120aab6e44ba2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 11 Jul 2019 09:38:41 +0800 Subject: [PATCH 27/42] fix lint --- modules/migrations/gitea_test.go | 7 ++++--- services/mirror/mirror_test.go | 14 +++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/modules/migrations/gitea_test.go b/modules/migrations/gitea_test.go index 88a3a6d2189d..5cf05c9370d6 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,7 +30,7 @@ func TestGiteaUploadRepo(t *testing.T) { uploader = NewGiteaLocalUploader(user, user.Name, repoName) ) - err := migrateRepository(downloader, uploader, MigrateOptions{ + err := migrateRepository(downloader, uploader, structs.MigrateRepoOptions{ RemoteURL: "https://github.com/go-xorm/builder", Name: repoName, AuthUsername: "", @@ -41,8 +42,8 @@ func TestGiteaUploadRepo(t *testing.T) { Releases: true, Comments: true, PullRequests: true, - Private: true, - Mirror: false, + IsPrivate: true, + IsMirror: false, }) assert.NoError(t, err) diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index 4e563793e034..72532a5f6e95 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -28,13 +28,13 @@ func TestRelease_MirrorDelete(t *testing.T) { repoPath := models.RepoPath(user.Name, repo.Name) migrationOptions := structs.MigrateRepoOptions{ - Name: "test_mirror", - Description: "Test mirror", - IsPrivate: false, - IsMirror: true, - RemoteAddr: repoPath, - Wiki: true, - SyncReleasesWithTags: true, + Name: "test_mirror", + Description: "Test mirror", + IsPrivate: false, + IsMirror: true, + RemoteURL: repoPath, + Wiki: true, + Releases: false, } mirror, err := models.MigrateRepository(user, user, migrationOptions) assert.NoError(t, err) From 5384a12be0893fe50703350f008c76261f2a7da6 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 11 Jul 2019 10:22:43 +0800 Subject: [PATCH 28/42] fix loading page --- routers/repo/view.go | 12 ++++- templates/repo/header.tmpl | 94 +++++++++++++++++------------------ templates/repo/migrating.tmpl | 2 +- 3 files changed, 58 insertions(+), 50 deletions(-) diff --git a/routers/repo/view.go b/routers/repo/view.go index bea8c7a70521..37296fb5a527 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" @@ -357,6 +358,15 @@ 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 { @@ -374,7 +384,7 @@ func Home(ctx *context.Context) { ctx.Data["Repo"] = ctx.Repo ctx.Data["MigrateTask"] = task - ctx.Data["MigrateConfig"] = cfg + ctx.Data["RemoteURL"] = safeURL(cfg.RemoteURL) ctx.HTML(200, tplMigrating) return } diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index e655d8b451ab..e84fc3504c2b 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -49,63 +49,61 @@ {{end}} -{{if not .Repository.IsBeingCreated}}
- -{{end}} -
+ {{end}} + {{template "custom/extra_tabs" .}} + + {{if .Permission.IsAdmin}} + + {{end}} +
+ {{end}} + diff --git a/templates/repo/migrating.tmpl b/templates/repo/migrating.tmpl index be7ab9eb24b1..3bfdbc20a8ed 100644 --- a/templates/repo/migrating.tmpl +++ b/templates/repo/migrating.tmpl @@ -16,7 +16,7 @@
-

{{.i18n.Tr "repo.migrate.migrating" .MigrateConfig.RemoteURL | Safe}}

+

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

From 19d10ee3993061c34c3f6a431e5e0b60eb800a74 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 11 Jul 2019 11:56:43 +0800 Subject: [PATCH 29/42] refactor --- models/repo.go | 29 ++++-------------- models/task.go | 12 ++++---- modules/migrations/base/options.go | 2 +- modules/migrations/gitea.go | 20 +++++++++--- modules/migrations/gitea_test.go | 10 +++--- modules/migrations/github.go | 4 +-- modules/migrations/migrate.go | 10 +++--- modules/structs/repo.go | 15 ++++++--- modules/structs/task.go | 20 ------------ modules/task/migrate.go | 49 +++++++++++++++--------------- routers/api/v1/repo/repo.go | 10 +++--- routers/repo/repo.go | 14 ++++----- routers/repo/view.go | 2 +- templates/repo/header.tmpl | 1 + templates/repo/migrating.tmpl | 2 +- 15 files changed, 91 insertions(+), 109 deletions(-) diff --git a/models/repo.go b/models/repo.go index 3e29df605190..ba9b6b076646 100644 --- a/models/repo.go +++ b/models/repo.go @@ -941,26 +941,9 @@ func CheckCreateRepository(doer, u *User, name string) error { return nil } -// MigrateRepository migrates a existing repository from other project hosting. -func MigrateRepository(doer, u *User, opts api.MigrateRepoOptions) (*Repository, error) { - repo, err := CreateRepository(doer, u, CreateRepoOptions{ - Name: opts.Name, - Description: opts.Description, - OriginalURL: opts.OriginalURL, - IsPrivate: opts.IsPrivate, - IsMirror: opts.IsMirror, - Status: RepositoryBeingMigrated, - }) - if err != nil { - return nil, err - } - - return MigrateRepositoryGitData(doer, u, repo, opts) -} - // MigrateRepositoryGitData starts migrating git related data after created migrating repository -func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts api.MigrateRepoOptions) (*Repository, error) { - repoPath := RepoPath(u.Name, opts.Name) +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() @@ -979,7 +962,7 @@ func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts api.MigrateR return repo, fmt.Errorf("Failed to remove %s: %v", repoPath, err) } - if err = git.Clone(opts.RemoteURL, repoPath, git.CloneRepoOptions{ + if err = git.Clone(opts.CloneAddr, repoPath, git.CloneRepoOptions{ Mirror: true, Quiet: true, Timeout: migrateTimeout, @@ -988,8 +971,8 @@ func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts api.MigrateR } if opts.Wiki { - wikiPath := WikiPath(u.Name, opts.Name) - wikiRemotePath := wikiRemoteURL(opts.RemoteURL) + 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) @@ -1038,7 +1021,7 @@ func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts api.MigrateR 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, diff --git a/models/task.go b/models/task.go index 6fd8cfc9a163..8139998cfeef 100644 --- a/models/task.go +++ b/models/task.go @@ -103,9 +103,9 @@ func (task *Task) UpdateCols(cols ...string) error { } // MigrateConfig returns task config when migrate repository -func (task *Task) MigrateConfig() (*structs.MigrateRepoOptions, error) { +func (task *Task) MigrateConfig() (*structs.MigrateRepoOption, error) { if task.Type == structs.TaskTypeMigrateRepo { - var opts structs.MigrateRepoOptions + var opts structs.MigrateRepoOption err := json.Unmarshal([]byte(task.PayloadContent), &opts) if err != nil { return nil, err @@ -194,10 +194,10 @@ func CreateMigrateTask(doer, u *User, opts base.MigrateOptions) (*Task, error) { } repo, err := CreateRepository(doer, u, CreateRepoOptions{ - Name: opts.Name, + Name: opts.RepoName, Description: opts.Description, - IsPrivate: opts.IsPrivate, - IsMirror: opts.IsMirror, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, Status: RepositoryBeingMigrated, }) if err != nil { @@ -230,7 +230,7 @@ func FinishMigrateTask(task *Task) error { if _, err := sess.ID(task.ID).Cols("status", "end_time").Update(task); err != nil { return err } - task.Repo.Status = RepositoryBeingMigrated + task.Repo.Status = RepositoryReady if _, err := sess.ID(task.RepoID).Cols("status").Update(task.Repo); err != nil { return err } diff --git a/modules/migrations/base/options.go b/modules/migrations/base/options.go index 26387169c221..2d180b61d955 100644 --- a/modules/migrations/base/options.go +++ b/modules/migrations/base/options.go @@ -8,4 +8,4 @@ package base import "code.gitea.io/gitea/modules/structs" // MigrateOptions defines the way a repository gets migrated -type MigrateOptions = structs.MigrateRepoOptions +type MigrateOptions = structs.MigrateRepoOption diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go index e4351500cb1c..bb56ee83521f 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -91,16 +91,28 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate remoteAddr = u.String() } - r, err := models.MigrateRepository(g.doer, owner, structs.MigrateRepoOptions{ + r, err := models.CreateRepository(g.doer, owner, models.CreateRepoOptions{ Name: g.repoName, Description: repo.Description, OriginalURL: repo.OriginalURL, - IsMirror: repo.IsMirror, - RemoteURL: remoteAddr, - IsPrivate: repo.IsPrivate, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: models.RepositoryBeingMigrated, + }) + 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 5cf05c9370d6..73c119a15de7 100644 --- a/modules/migrations/gitea_test.go +++ b/modules/migrations/gitea_test.go @@ -30,9 +30,9 @@ func TestGiteaUploadRepo(t *testing.T) { uploader = NewGiteaLocalUploader(user, user.Name, repoName) ) - err := migrateRepository(downloader, uploader, structs.MigrateRepoOptions{ - 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, @@ -42,8 +42,8 @@ func TestGiteaUploadRepo(t *testing.T) { Releases: true, Comments: true, PullRequests: true, - IsPrivate: true, - IsMirror: false, + Private: true, + Mirror: false, }) assert.NoError(t, err) diff --git a/modules/migrations/github.go b/modules/migrations/github.go index 754f98941c17..1c5d96c03d47 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 7f2e3b4e987a..1ce3a3bc8612 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -27,7 +27,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,8 +50,8 @@ 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 { @@ -72,8 +72,8 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts if err != nil { return err } - repo.IsPrivate = opts.IsPrivate - repo.IsMirror = opts.IsMirror + repo.IsPrivate = opts.Private + repo.IsMirror = opts.Mirror if opts.Description != "" { repo.Description = opts.Description } diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 87396d6ce99a..a535a675af91 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -162,8 +162,15 @@ 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 } diff --git a/modules/structs/task.go b/modules/structs/task.go index d6383930bbbf..e83d0437ceff 100644 --- a/modules/structs/task.go +++ b/modules/structs/task.go @@ -32,23 +32,3 @@ const ( TaskStatusFailed // 3 task is failed TaskStatusFinished // 4 task is finished ) - -// MigrateRepoOptions defines the way a repository gets migrated -type MigrateRepoOptions struct { - Name string - Description string - OriginalURL string - RemoteURL string - AuthUsername string - AuthPassword string - - Wiki bool - Issues bool - Milestones bool - Labels bool - Releases bool - Comments bool - PullRequests bool - IsPrivate bool - IsMirror bool -} diff --git a/modules/task/migrate.go b/modules/task/migrate.go index dfdedd445779..9b99f1c40af9 100644 --- a/modules/task/migrate.go +++ b/modules/task/migrate.go @@ -33,12 +33,7 @@ func handleCreateError(owner *models.User, err error, name string) error { } } -func runMigrateTask(t *models.Task) error { - opts, err := t.MigrateConfig() - if err != nil { - return err - } - +func runMigrateTask(t *models.Task) (err error) { defer func() { if e := recover(); e != nil { var buf bytes.Buffer @@ -47,28 +42,36 @@ func runMigrateTask(t *models.Task) error { err = errors.New(buf.String()) } - if err != nil { - 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()) - } else if t.Repo != nil { - if errDelete := models.DeleteRepository(t.Doer, t.OwnerID, t.Repo.ID); errDelete != nil { - log.Error("DeleteRepository: %v", errDelete) - } + if err == nil { + err = models.FinishMigrateTask(t) + if err == nil { + notification.NotifyMigrateRepository(t.Doer, t.Owner, t.Repo) + return } - return + + log.Error("FinishMigrateTask failed: %s", err.Error()) } - if err := models.FinishMigrateTask(t); err != nil { + 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()) - return } - notification.NotifyMigrateRepository(t.Doer, t.Owner, t.Repo) + if t.Repo != nil { + if errDelete := models.DeleteRepository(t.Doer, t.OwnerID, t.Repo.ID); errDelete != nil { + log.Error("DeleteRepository: %v", errDelete) + } + } }() + var opts *structs.MigrateRepoOption + opts, err = t.MigrateConfig() + if err != nil { + return err + } + if err := t.LoadRepo(); err != nil { return err } @@ -85,10 +88,6 @@ func runMigrateTask(t *models.Task) error { return err } - if t.Repo.IsBeingCreated() { - return fmt.Errorf("Repository %s/%s is being created, task ignored", t.Owner.Name, t.Repo.Name) - } - repo, err := models.MigrateRepositoryGitData(t.Doer, t.Owner, t.Repo, *opts) if err == nil { log.Trace("Repository migrated [%d]: %s/%s", repo.ID, t.Owner.Name, repo.Name) @@ -100,7 +99,7 @@ func runMigrateTask(t *models.Task) error { } // remoteAddr may contain credentials, so we sanitize it - err = util.URLSanitizedError(err, opts.RemoteURL) + 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()) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index d56d0d6771a1..08c0635bc312 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -398,11 +398,11 @@ 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, - IsPrivate: form.Private || setting.Repository.ForcePrivate, - IsMirror: form.Mirror, + Private: form.Private || setting.Repository.ForcePrivate, + Mirror: form.Mirror, AuthUsername: form.AuthUsername, AuthPassword: form.AuthPassword, Wiki: form.Wiki, @@ -413,7 +413,7 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { PullRequests: form.PullRequests, Releases: form.Releases, } - if opts.IsMirror { + if opts.Mirror { opts.Issues = false opts.Milestones = false opts.Labels = false diff --git a/routers/repo/repo.go b/routers/repo/repo.go index ca884075dc3a..1eac66739300 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -259,11 +259,11 @@ 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, - IsPrivate: form.Private || setting.Repository.ForcePrivate, - IsMirror: form.Mirror, + Private: form.Private || setting.Repository.ForcePrivate, + Mirror: form.Mirror, AuthUsername: form.AuthUsername, AuthPassword: form.AuthPassword, Wiki: form.Wiki, @@ -274,7 +274,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { PullRequests: form.PullRequests, Releases: form.Releases, } - if opts.IsMirror { + if opts.Mirror { opts.Issues = false opts.Milestones = false opts.Labels = false @@ -283,7 +283,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { opts.Releases = false } - err = models.CheckCreateRepository(ctx.User, ctxUser, opts.Name) + err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName) if err != nil { if models.IsErrRepoAlreadyExist(err) { ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplMigrate, &form) @@ -310,7 +310,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { err = task.MigrateRepository(ctx.User, ctxUser, opts) if err == nil { - ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.Name) + ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.RepoName) return } ctx.ServerError("MigrateRepository", err) diff --git a/routers/repo/view.go b/routers/repo/view.go index 37296fb5a527..c4e6a69220aa 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -384,7 +384,7 @@ func Home(ctx *context.Context) { ctx.Data["Repo"] = ctx.Repo ctx.Data["MigrateTask"] = task - ctx.Data["RemoteURL"] = safeURL(cfg.RemoteURL) + ctx.Data["CloneAddr"] = safeURL(cfg.CloneAddr) ctx.HTML(200, tplMigrating) return } diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index e84fc3504c2b..9fb3e32899ae 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -106,4 +106,5 @@ {{end}} +
diff --git a/templates/repo/migrating.tmpl b/templates/repo/migrating.tmpl index 3bfdbc20a8ed..d0128e2fb3e8 100644 --- a/templates/repo/migrating.tmpl +++ b/templates/repo/migrating.tmpl @@ -16,7 +16,7 @@
-

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

+

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

From 38b67266a7259c7b1f7308c0b4f772cfed8fbd79 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 11 Jul 2019 20:10:03 +0800 Subject: [PATCH 30/42] When gitea restart, don't restart the running tasks because we may have servel gitea instances, that may break the migration --- modules/task/migrate.go | 15 ++++++++++----- modules/task/task.go | 16 +--------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/modules/task/migrate.go b/modules/task/migrate.go index 9b99f1c40af9..66edf01f5873 100644 --- a/modules/task/migrate.go +++ b/modules/task/migrate.go @@ -66,14 +66,13 @@ func runMigrateTask(t *models.Task) (err error) { } }() - var opts *structs.MigrateRepoOption - opts, err = t.MigrateConfig() - if err != nil { + if err := t.LoadRepo(); err != nil { return err } - 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 { @@ -88,6 +87,12 @@ func runMigrateTask(t *models.Task) (err error) { return err } + var opts *structs.MigrateRepoOption + opts, err = t.MigrateConfig() + if err != nil { + return err + } + repo, err := models.MigrateRepositoryGitData(t.Doer, t.Owner, t.Repo, *opts) if err == nil { log.Trace("Repository migrated [%d]: %s/%s", repo.ID, t.Owner.Name, repo.Name) diff --git a/modules/task/task.go b/modules/task/task.go index 2ea1133ef028..64744afe7a4c 100644 --- a/modules/task/task.go +++ b/modules/task/task.go @@ -29,11 +29,11 @@ func Run(t *models.Task) error { // Init will start the service to get all unfinished tasks and run them func Init() error { - var err 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 @@ -52,20 +52,6 @@ func Init() error { } }() - tasks, err := models.FindTasks(models.FindTaskOptions{ - Status: int(structs.TaskStatusRunning), - }) - - if err != nil { - return fmt.Errorf("DeliverHooks: %v", err.Error()) - } - - // Update hook task status. - for _, t := range tasks { - if err := taskQueue.Push(t); err != nil { - return fmt.Errorf("Run Task: %v", err.Error()) - } - } return nil } From 237c73a1fc9b306202ff8276d8d06ab4b74ace71 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 28 Jul 2019 11:10:38 +0800 Subject: [PATCH 31/42] fix js --- public/js/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/index.js b/public/js/index.js index d44783801f9d..98c1e047e3f3 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -242,9 +242,9 @@ function updateIssuesMeta(url, action, issueIds, elementId) { } function initRepoStatusChecker() { - var migrating = $("#repo_migrating"); + const migrating = $("#repo_migrating"); if (migrating) { - var repo_name = migrating.attr('repo'); + const repo_name = migrating.attr('repo'); if (typeof repo_name === 'undefined') { return } From 4ef33d6593e4ba3f71106013ea572e5c9b89dc7b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 17 Sep 2019 08:50:11 +0800 Subject: [PATCH 32/42] fix tests --- models/migrations/v90.go | 1 - services/mirror/mirror_test.go | 18 ++++++-- services/release/release_test.go | 77 ++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/models/migrations/v90.go b/models/migrations/v90.go index 580552dd3acc..09aceae2f9e4 100644 --- a/models/migrations/v90.go +++ b/models/migrations/v90.go @@ -12,7 +12,6 @@ func changeSomeColumnsLengthOfRepo(x *xorm.Engine) error { Description string `xorm:"TEXT"` Website string `xorm:"VARCHAR(2048)"` OriginalURL string `xorm:"VARCHAR(2048)"` - Status int `xorm:"NOT NULL DEFAULT 0"` } return x.Sync2(new(Repository)) diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index 72532a5f6e95..d2e0d86b1dcb 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -27,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 := structs.MigrateRepoOptions{ - Name: "test_mirror", + opts := structs.MigrateRepoOptions{ + RepoName: "test_mirror", Description: "Test mirror", IsPrivate: false, IsMirror: true, - RemoteURL: repoPath, + CloneAddr: repoPath, Wiki: true, Releases: false, } - mirror, err := models.MigrateRepository(user, user, migrationOptions) + + repo, 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, repo, opts) assert.NoError(t, err) gitRepo, err := git.OpenRepository(repoPath) diff --git a/services/release/release_test.go b/services/release/release_test.go index d30dfee2865c..a641ba9bace6 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -100,3 +100,80 @@ func TestRelease_Create(t *testing.T) { IsTag: true, }, nil)) } +<<<<<<< HEAD +======= + +func TestRelease_MirrorDelete(t *testing.T) { + assert.NoError(t, models.PrepareTestDatabase()) + + user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) + repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) + repoPath := models.RepoPath(user.Name, repo.Name) + opts := structs.MigrateRepoOption{ + RepoName: "test_mirror", + Description: "Test mirror", + Private: false, + Mirror: true, + CloneAddr: repoPath, + Wiki: true, + Releases: false, + } + + repo, 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, repo, opts) + assert.NoError(t, err) + + gitRepo, err := git.OpenRepository(repoPath) + assert.NoError(t, err) + + findOptions := models.FindReleasesOptions{IncludeDrafts: true, IncludeTags: true} + initCount, err := models.GetReleaseCountByRepoID(mirror.ID, findOptions) + assert.NoError(t, err) + assert.EqualValues(t, 1, initCount) + + assert.NoError(t, CreateRelease(gitRepo, &models.Release{ + RepoID: repo.ID, + PublisherID: user.ID, + TagName: "v0.2", + Target: "master", + Title: "v0.2 is released", + Note: "v0.2 is released", + IsDraft: false, + IsPrerelease: false, + IsTag: true, + }, nil)) + + err = mirror.GetMirror() + assert.NoError(t, err) + + ok := models.RunMirrorSync(mirror.Mirror) + assert.True(t, ok) + + count, err := models.GetReleaseCountByRepoID(mirror.ID, findOptions) + assert.NoError(t, err) + assert.EqualValues(t, initCount+1, count) + + release, err := models.GetRelease(repo.ID, "v0.2") + assert.NoError(t, err) + assert.NoError(t, models.DeleteReleaseByID(release.ID, user, true)) + + rels, err := models.GetReleasesByRepoID(mirror.ID, findOptions, 0, 100) + assert.NoError(t, err) + assert.EqualValues(t, initCount, len(rels)) + + ok = models.RunMirrorSync(mirror.Mirror) + assert.True(t, ok) + + count, err = models.GetReleaseCountByRepoID(mirror.ID, findOptions) + assert.NoError(t, err) + assert.EqualValues(t, initCount+1, count) +} +>>>>>>> fix tests From e4adc77cf7bf2f585c434fe5442cd1985376a370 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 17 Sep 2019 08:38:49 +0800 Subject: [PATCH 33/42] Update models/repo.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> --- models/repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/repo.go b/models/repo.go index ba9b6b076646..23b1c2ef52c4 100644 --- a/models/repo.go +++ b/models/repo.go @@ -211,7 +211,7 @@ func (repo *Repository) IsBeingMigrated() bool { return repo.Status == RepositoryBeingMigrated } -// IsBeingCreated indicates that repository is being migtated or forked +// IsBeingCreated indicates that repository is being migrated or forked func (repo *Repository) IsBeingCreated() bool { return repo.IsBeingMigrated() } From 6c6bde7f3e873f919b7d55b30e0a6d3e59d58d7b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 17 Sep 2019 08:39:03 +0800 Subject: [PATCH 34/42] Update docs/content/doc/advanced/config-cheat-sheet.en-us.md Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> --- docs/content/doc/advanced/config-cheat-sheet.en-us.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 20a416d0c95f..ed34be032bbd 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -522,7 +522,7 @@ Two special environment variables are passed to the render command: - `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 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**: 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`) From 220de6fc57487e7f43cd92e0ac8e10f40167a93e Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 21 Sep 2019 20:48:46 +0800 Subject: [PATCH 35/42] rename ErrTaskIsNotExist to ErrTaskDoesNotExist --- models/task.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/models/task.go b/models/task.go index 8139998cfeef..ff147544c419 100644 --- a/models/task.go +++ b/models/task.go @@ -115,20 +115,20 @@ func (task *Task) MigrateConfig() (*structs.MigrateRepoOption, error) { return nil, fmt.Errorf("Task type is %s, not Migrate Repo", task.Type.Name()) } -// ErrTaskIsNotExist represents a "TaskIsNotExist" kind of error. -type ErrTaskIsNotExist struct { +// ErrTaskDoesNotExist represents a "TaskDoesNotExist" kind of error. +type ErrTaskDoesNotExist struct { ID int64 RepoID int64 Type structs.TaskType } -// IsErrTaskNotExist checks if an error is a ErrTaskIsNotExist. -func IsErrTaskNotExist(err error) bool { - _, ok := err.(ErrTaskIsNotExist) +// IsErrTaskDoesNotExist checks if an error is a ErrTaskIsNotExist. +func IsErrTaskDoesNotExist(err error) bool { + _, ok := err.(ErrTaskDoesNotExist) return ok } -func (err ErrTaskIsNotExist) Error() string { +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) } @@ -143,7 +143,7 @@ func GetMigratingTask(repoID int64) (*Task, error) { if err != nil { return nil, err } else if !has { - return nil, ErrTaskIsNotExist{0, repoID, task.Type} + return nil, ErrTaskDoesNotExist{0, repoID, task.Type} } return &task, nil } From a1a1358af0823b79694c043e4d93916a6291c3d9 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 25 Sep 2019 09:26:15 +0800 Subject: [PATCH 36/42] delete release after add one on tests to make it run happy --- services/release/release_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/release/release_test.go b/services/release/release_test.go index a641ba9bace6..96caf492ff6e 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -175,5 +175,12 @@ func TestRelease_MirrorDelete(t *testing.T) { count, err = models.GetReleaseCountByRepoID(mirror.ID, findOptions) assert.NoError(t, err) assert.EqualValues(t, initCount+1, count) + + err = models.DeleteReleaseByID(release.ID, user, true) + assert.NoError(t, err) + + count, err = models.GetReleaseCountByRepoID(mirror.ID, findOptions) + assert.NoError(t, err) + assert.EqualValues(t, initCount, count) } >>>>>>> fix tests From 45ab0cd4718a2c768ad1b02602a5ba077f833d8c Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 4 Oct 2019 09:31:31 +0800 Subject: [PATCH 37/42] fix tests --- services/mirror/mirror_test.go | 6 +-- services/release/release_test.go | 84 -------------------------------- 2 files changed, 3 insertions(+), 87 deletions(-) diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index d2e0d86b1dcb..147a56406961 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -27,11 +27,11 @@ func TestRelease_MirrorDelete(t *testing.T) { repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) repoPath := models.RepoPath(user.Name, repo.Name) - opts := structs.MigrateRepoOptions{ + opts := structs.MigrateRepoOption{ RepoName: "test_mirror", Description: "Test mirror", - IsPrivate: false, - IsMirror: true, + Private: false, + Mirror: true, CloneAddr: repoPath, Wiki: true, Releases: false, diff --git a/services/release/release_test.go b/services/release/release_test.go index 96caf492ff6e..d30dfee2865c 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -100,87 +100,3 @@ func TestRelease_Create(t *testing.T) { IsTag: true, }, nil)) } -<<<<<<< HEAD -======= - -func TestRelease_MirrorDelete(t *testing.T) { - assert.NoError(t, models.PrepareTestDatabase()) - - user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) - repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) - repoPath := models.RepoPath(user.Name, repo.Name) - opts := structs.MigrateRepoOption{ - RepoName: "test_mirror", - Description: "Test mirror", - Private: false, - Mirror: true, - CloneAddr: repoPath, - Wiki: true, - Releases: false, - } - - repo, 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, repo, opts) - assert.NoError(t, err) - - gitRepo, err := git.OpenRepository(repoPath) - assert.NoError(t, err) - - findOptions := models.FindReleasesOptions{IncludeDrafts: true, IncludeTags: true} - initCount, err := models.GetReleaseCountByRepoID(mirror.ID, findOptions) - assert.NoError(t, err) - assert.EqualValues(t, 1, initCount) - - assert.NoError(t, CreateRelease(gitRepo, &models.Release{ - RepoID: repo.ID, - PublisherID: user.ID, - TagName: "v0.2", - Target: "master", - Title: "v0.2 is released", - Note: "v0.2 is released", - IsDraft: false, - IsPrerelease: false, - IsTag: true, - }, nil)) - - err = mirror.GetMirror() - assert.NoError(t, err) - - ok := models.RunMirrorSync(mirror.Mirror) - assert.True(t, ok) - - count, err := models.GetReleaseCountByRepoID(mirror.ID, findOptions) - assert.NoError(t, err) - assert.EqualValues(t, initCount+1, count) - - release, err := models.GetRelease(repo.ID, "v0.2") - assert.NoError(t, err) - assert.NoError(t, models.DeleteReleaseByID(release.ID, user, true)) - - rels, err := models.GetReleasesByRepoID(mirror.ID, findOptions, 0, 100) - assert.NoError(t, err) - assert.EqualValues(t, initCount, len(rels)) - - ok = models.RunMirrorSync(mirror.Mirror) - assert.True(t, ok) - - count, err = models.GetReleaseCountByRepoID(mirror.ID, findOptions) - assert.NoError(t, err) - assert.EqualValues(t, initCount+1, count) - - err = models.DeleteReleaseByID(release.ID, user, true) - assert.NoError(t, err) - - count, err = models.GetReleaseCountByRepoID(mirror.ID, findOptions) - assert.NoError(t, err) - assert.EqualValues(t, initCount, count) -} ->>>>>>> fix tests From 5e1888f14aaf530290a866d0a60353657be0081d Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 5 Oct 2019 10:04:10 +0800 Subject: [PATCH 38/42] fix tests --- services/mirror/mirror_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index 147a56406961..9ad11b726563 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -37,7 +37,7 @@ func TestRelease_MirrorDelete(t *testing.T) { Releases: false, } - repo, err := models.CreateRepository(user, user, models.CreateRepoOptions{ + mirrorRepo, err := models.CreateRepository(user, user, models.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, IsPrivate: opts.Private, @@ -46,7 +46,7 @@ func TestRelease_MirrorDelete(t *testing.T) { }) assert.NoError(t, err) - mirror, err := models.MigrateRepositoryGitData(user, user, repo, opts) + mirror, err := models.MigrateRepositoryGitData(user, user, mirrorRepo, opts) assert.NoError(t, err) gitRepo, err := git.OpenRepository(repoPath) From fb4e7f3397d5289c98f2da1ea2e635198d81513a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 12 Oct 2019 10:31:49 +0800 Subject: [PATCH 39/42] improve codes --- models/migrations/v99.go | 2 +- modules/migrations/migrate.go | 6 ++++ modules/task/queue.go | 1 + modules/task/queue_channel.go | 19 ++++++----- modules/task/queue_redis.go | 11 +++++++ routers/init.go | 2 +- routers/repo/repo.go | 60 +++++++++++++++++++++-------------- 7 files changed, 66 insertions(+), 35 deletions(-) diff --git a/models/migrations/v99.go b/models/migrations/v99.go index 367925ffe064..3eb287af6c96 100644 --- a/models/migrations/v99.go +++ b/models/migrations/v99.go @@ -27,7 +27,7 @@ func addTaskTable(x *xorm.Engine) error { } type Repository struct { - Status int + Status int `xorm:"NOT NULL DEFAULT 0"` } return x.Sync2(new(Task), new(Repository)) diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index 1ce3a3bc8612..3f5c0d1118c3 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" @@ -58,6 +60,10 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt 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/task/queue.go b/modules/task/queue.go index 99986fc7ed6f..ddee0b3d4627 100644 --- a/modules/task/queue.go +++ b/modules/task/queue.go @@ -10,4 +10,5 @@ import "code.gitea.io/gitea/models" type Queue interface { Run() error Push(*models.Task) error + Stop() } diff --git a/modules/task/queue_channel.go b/modules/task/queue_channel.go index b3a2dd6a18d3..0fe2bd28abd0 100644 --- a/modules/task/queue_channel.go +++ b/modules/task/queue_channel.go @@ -5,8 +5,6 @@ package task import ( - "time" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" ) @@ -29,16 +27,13 @@ func NewChannelQueue(queueLen int) *ChannelQueue { // Run starts to run the queue func (c *ChannelQueue) Run() error { - for { - select { - case task := <-c.queue: - err := Run(task) - if err != nil { - log.Error("Run task failed: %s", err.Error()) - } - case <-time.After(time.Millisecond * 100): + 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 @@ -46,3 +41,7 @@ func (c *ChannelQueue) Push(task *models.Task) error { c.queue <- task return nil } + +func (c *ChannelQueue) Stop() { + close(c.queue) +} diff --git a/modules/task/queue_redis.go b/modules/task/queue_redis.go index 30f5f4229a10..8b524b549fe3 100644 --- a/modules/task/queue_redis.go +++ b/modules/task/queue_redis.go @@ -31,6 +31,7 @@ type redisClient interface { type RedisQueue struct { client redisClient queueName string + closeChan chan bool } func parseConnStr(connStr string) (addrs, password string, dbIdx int, err error) { @@ -60,6 +61,7 @@ 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") @@ -85,6 +87,11 @@ func NewRedisQueue(addrs string, password string, dbIdx int) (*RedisQueue, error // Run starts to run the queue func (r *RedisQueue) Run() error { for { + select { + case <-r.closeChan: + return nil + } + bs, err := r.client.LPop(r.queueName).Bytes() if err != nil { if err != redis.Nil { @@ -117,3 +124,7 @@ func (r *RedisQueue) Push(task *models.Task) error { } return r.client.RPush(r.queueName, bs).Err() } + +func (r *RedisQueue) Stop() { + r.closeChan <- true +} diff --git a/routers/init.go b/routers/init.go index e630a1e3cf9a..c37bbeb6b08d 100644 --- a/routers/init.go +++ b/routers/init.go @@ -104,7 +104,7 @@ func GlobalInit() { models.InitDeliverHooks() models.InitTestPullRequests() if err := task.Init(); err != nil { - log.Fatal("Failed to initialize task: %v", err) + log.Fatal("Failed to initialize task scheduler: %v", err) } } if setting.EnableSQLite3 { diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 1eac66739300..1c87c72a8e37 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -134,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): @@ -222,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") @@ -285,26 +317,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName) if err != nil { - if models.IsErrRepoAlreadyExist(err) { - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplMigrate, &form) - return - } - - // remoteAddr may contain credentials, so we sanitize it - err = util.URLSanitizedError(err, remoteAddr) - - if strings.Contains(err.Error(), "Authentication failed") || - strings.Contains(err.Error(), "could not read Username") { - ctx.Data["Err_Auth"] = true - ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tplMigrate, &form) - return - } else if strings.Contains(err.Error(), "fatal:") { - ctx.Data["Err_CloneAddr"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tplMigrate, &form) - return - } - - handleCreateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) + handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) return } @@ -313,7 +326,8 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.RepoName) return } - ctx.ServerError("MigrateRepository", err) + + handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) } // Action response for actions to a repository From 80c9e8dd3b9a1282f712354e8b8e4a82a7f94682 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 12 Oct 2019 16:50:27 +0800 Subject: [PATCH 40/42] fix lint --- modules/task/queue_redis.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/task/queue_redis.go b/modules/task/queue_redis.go index 8b524b549fe3..3098c3f00698 100644 --- a/modules/task/queue_redis.go +++ b/modules/task/queue_redis.go @@ -90,6 +90,7 @@ func (r *RedisQueue) Run() error { select { case <-r.closeChan: return nil + case <-time.After(time.Millisecond * 100): } bs, err := r.client.LPop(r.queueName).Bytes() @@ -111,8 +112,6 @@ func (r *RedisQueue) Run() error { log.Error("Run task failed: %s", err.Error()) } } - - time.Sleep(time.Millisecond * 100) } } From 8edb889d73bdb0490d560ae27c96281a4b08c097 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 12 Oct 2019 23:48:47 +0800 Subject: [PATCH 41/42] fix lint --- modules/task/queue_channel.go | 1 + modules/task/queue_redis.go | 1 + 2 files changed, 2 insertions(+) diff --git a/modules/task/queue_channel.go b/modules/task/queue_channel.go index 0fe2bd28abd0..da541f47551f 100644 --- a/modules/task/queue_channel.go +++ b/modules/task/queue_channel.go @@ -42,6 +42,7 @@ func (c *ChannelQueue) Push(task *models.Task) error { 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 index 3098c3f00698..127de0cdbf1d 100644 --- a/modules/task/queue_redis.go +++ b/modules/task/queue_redis.go @@ -124,6 +124,7 @@ func (r *RedisQueue) Push(task *models.Task) error { return r.client.RPush(r.queueName, bs).Err() } +// Stop stop the queue func (r *RedisQueue) Stop() { r.closeChan <- true } From badbf9e015d1901246b17ba80f984554e4749df0 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 13 Oct 2019 17:32:10 +0800 Subject: [PATCH 42/42] fix migrations --- models/task.go | 1 + modules/migrations/gitea.go | 21 +++++++++++++-------- modules/structs/repo.go | 23 ++++++++++++----------- modules/task/migrate.go | 6 +++++- options/locale/locale_en-US.ini | 1 + public/js/index.js | 27 +++++++++++++++++---------- routers/repo/repo.go | 9 +++++++++ templates/repo/migrating.tmpl | 5 ++++- 8 files changed, 62 insertions(+), 31 deletions(-) diff --git a/models/task.go b/models/task.go index ff147544c419..cb878d387c12 100644 --- a/models/task.go +++ b/models/task.go @@ -196,6 +196,7 @@ func CreateMigrateTask(doer, u *User, opts base.MigrateOptions) (*Task, error) { repo, err := CreateRepository(doer, u, CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, + OriginalURL: opts.CloneAddr, IsPrivate: opts.Private, IsMirror: opts.Mirror, Status: RepositoryBeingMigrated, diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go index bb56ee83521f..ab3b0b9f694a 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -91,14 +91,19 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate remoteAddr = u.String() } - 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, - }) + 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 } diff --git a/modules/structs/repo.go b/modules/structs/repo.go index a535a675af91..57f1768a0b94 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -162,15 +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"` - Wiki bool - Issues bool - Milestones bool - Labels bool - Releases bool - Comments bool - PullRequests bool + 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/task/migrate.go b/modules/task/migrate.go index 66edf01f5873..5d15a506d793 100644 --- a/modules/task/migrate.go +++ b/modules/task/migrate.go @@ -12,6 +12,7 @@ import ( "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" @@ -93,8 +94,11 @@ func runMigrateTask(t *models.Task) (err error) { return err } - repo, err := models.MigrateRepositoryGitData(t.Doer, t.Owner, t.Repo, *opts) + 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 } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index af56583d4266..e6c5839a645c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -634,6 +634,7 @@ migrate.migrate_items_options = When migrating from github, input a username and 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/js/index.js b/public/js/index.js index 98c1e047e3f3..3b15ad8f1883 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -243,6 +243,7 @@ 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') { @@ -253,17 +254,23 @@ function initRepoStatusChecker() { url: suburl +"/"+repo_name+"/status", data: { "_csrf": csrf, - } - }).done(function(resp) { - if (resp) { - if (resp["status"] == 0) { - location.reload(); - return + }, + complete: function(xhr) { + if (xhr.status == 200) { + if (xhr.responseJSON) { + if (xhr.responseJSON["status"] == 0) { + location.reload(); + return + } + + setTimeout(function () { + initRepoStatusChecker() + }, 2000); + return + } } - - setTimeout(function () { - initRepoStatusChecker() - }, 2000); + $('#repo_migrating_progress').hide(); + $('#repo_migrating_failed').show(); } }) } diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 1c87c72a8e37..bfd0c771b058 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -468,7 +468,16 @@ func Download(ctx *context.Context) { // 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/templates/repo/migrating.tmpl b/templates/repo/migrating.tmpl index d0128e2fb3e8..34031d5653ef 100644 --- a/templates/repo/migrating.tmpl +++ b/templates/repo/migrating.tmpl @@ -15,9 +15,12 @@
-
+

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

+
+

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

+