* Store task errors following migrations and display them When migrate tasks fail store the error in the task table and ensure that they show on the status page. Fix #13242 Signed-off-by: Andrew Thornton <art27@cantab.net> * Update web_src/js/index.js * Hide the failed first Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: zeripath <art27@cantab.net>tags/v1.13.0-rc2
return &task, nil | return &task, nil | ||||
} | } | ||||
// GetMigratingTaskByID returns the migrating task by repo's id | |||||
func GetMigratingTaskByID(id, doerID int64) (*Task, *migration.MigrateOptions, error) { | |||||
var task = Task{ | |||||
ID: id, | |||||
DoerID: doerID, | |||||
Type: structs.TaskTypeMigrateRepo, | |||||
} | |||||
has, err := x.Get(&task) | |||||
if err != nil { | |||||
return nil, nil, err | |||||
} else if !has { | |||||
return nil, nil, ErrTaskDoesNotExist{id, 0, task.Type} | |||||
} | |||||
var opts migration.MigrateOptions | |||||
if err := json.Unmarshal([]byte(task.PayloadContent), &opts); err != nil { | |||||
return nil, nil, err | |||||
} | |||||
return &task, &opts, nil | |||||
} | |||||
// FindTaskOptions find all tasks | // FindTaskOptions find all tasks | ||||
type FindTaskOptions struct { | type FindTaskOptions struct { | ||||
Status int | Status int |
"code.gitea.io/gitea/modules/util" | "code.gitea.io/gitea/modules/util" | ||||
) | ) | ||||
func handleCreateError(owner *models.User, err error, name string) error { | |||||
func handleCreateError(owner *models.User, err error) error { | |||||
switch { | switch { | ||||
case models.IsErrReachLimitOfRepo(err): | 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()) | ||||
func runMigrateTask(t *models.Task) (err error) { | func runMigrateTask(t *models.Task) (err error) { | ||||
defer func() { | defer func() { | ||||
if e := recover(); e != nil { | if e := recover(); e != nil { | ||||
err = fmt.Errorf("PANIC whilst trying to do migrate task: %v\nStacktrace: %v", err, log.Stack(2)) | |||||
log.Critical("PANIC during runMigrateTask[%d] by DoerID[%d] to RepoID[%d] for OwnerID[%d]: %v", t.ID, t.DoerID, t.RepoID, t.OwnerID, err) | |||||
err = fmt.Errorf("PANIC whilst trying to do migrate task: %v", e) | |||||
log.Critical("PANIC during runMigrateTask[%d] by DoerID[%d] to RepoID[%d] for OwnerID[%d]: %v\nStacktrace: %v", t.ID, t.DoerID, t.RepoID, t.OwnerID, e, log.Stack(2)) | |||||
} | } | ||||
if err == nil { | if err == nil { | ||||
t.EndTime = timeutil.TimeStampNow() | t.EndTime = timeutil.TimeStampNow() | ||||
t.Status = structs.TaskStatusFailed | t.Status = structs.TaskStatusFailed | ||||
t.Errors = err.Error() | t.Errors = err.Error() | ||||
if err := t.UpdateCols("status", "errors", "end_time"); err != nil { | |||||
t.RepoID = 0 | |||||
if err := t.UpdateCols("status", "errors", "repo_id", "end_time"); err != nil { | |||||
log.Error("Task UpdateCols failed: %v", err) | log.Error("Task UpdateCols failed: %v", err) | ||||
} | } | ||||
} | } | ||||
}() | }() | ||||
if err := t.LoadRepo(); err != nil { | |||||
return err | |||||
if err = t.LoadRepo(); err != nil { | |||||
return | |||||
} | } | ||||
// if repository is ready, then just finsih the task | // if repository is ready, then just finsih the task | ||||
return nil | return nil | ||||
} | } | ||||
if err := t.LoadDoer(); err != nil { | |||||
return err | |||||
if err = t.LoadDoer(); err != nil { | |||||
return | |||||
} | } | ||||
if err := t.LoadOwner(); err != nil { | |||||
return err | |||||
if err = t.LoadOwner(); err != nil { | |||||
return | |||||
} | } | ||||
t.StartTime = timeutil.TimeStampNow() | t.StartTime = timeutil.TimeStampNow() | ||||
t.Status = structs.TaskStatusRunning | t.Status = structs.TaskStatusRunning | ||||
if err := t.UpdateCols("start_time", "status"); err != nil { | |||||
return err | |||||
if err = t.UpdateCols("start_time", "status"); err != nil { | |||||
return | |||||
} | } | ||||
var opts *migration.MigrateOptions | var opts *migration.MigrateOptions | ||||
opts, err = t.MigrateConfig() | opts, err = t.MigrateConfig() | ||||
if err != nil { | if err != nil { | ||||
return err | |||||
return | |||||
} | } | ||||
opts.MigrateToRepoID = t.RepoID | opts.MigrateToRepoID = t.RepoID | ||||
repo, err := migrations.MigrateRepository(graceful.GetManager().HammerContext(), t.Doer, t.Owner.Name, *opts) | |||||
var repo *models.Repository | |||||
repo, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), t.Doer, t.Owner.Name, *opts) | |||||
if err == nil { | if err == nil { | ||||
log.Trace("Repository migrated [%d]: %s/%s", repo.ID, t.Owner.Name, repo.Name) | log.Trace("Repository migrated [%d]: %s/%s", repo.ID, t.Owner.Name, repo.Name) | ||||
return nil | |||||
return | |||||
} | } | ||||
if models.IsErrRepoAlreadyExist(err) { | if models.IsErrRepoAlreadyExist(err) { | ||||
return errors.New("The repository name is already used") | |||||
err = errors.New("The repository name is already used") | |||||
return | |||||
} | } | ||||
// remoteAddr may contain credentials, so we sanitize it | // remoteAddr may contain credentials, so we sanitize it | ||||
return fmt.Errorf("Migration failed: %v", err.Error()) | return fmt.Errorf("Migration failed: %v", err.Error()) | ||||
} | } | ||||
return handleCreateError(t.Owner, err, "MigratePost") | |||||
// do not be tempted to coalesce this line with the return | |||||
err = handleCreateError(t.Owner, err) | |||||
return | |||||
} | } |
ctx.ServeFile(archivePath, ctx.Repo.Repository.Name+"-"+refName+ext) | ctx.ServeFile(archivePath, ctx.Repo.Repository.Name+"-"+refName+ext) | ||||
} | } | ||||
// Status returns repository's status | |||||
func Status(ctx *context.Context) { | |||||
task, err := models.GetMigratingTask(ctx.Repo.Repository.ID) | |||||
if err != nil { | |||||
ctx.JSON(500, map[string]interface{}{ | |||||
"err": err, | |||||
}) | |||||
return | |||||
} | |||||
ctx.JSON(200, map[string]interface{}{ | |||||
"status": ctx.Repo.Repository.Status, | |||||
"err": task.Errors, | |||||
}) | |||||
} |
m.Get("/forgot_password", user.ForgotPasswd) | m.Get("/forgot_password", user.ForgotPasswd) | ||||
m.Post("/forgot_password", user.ForgotPasswdPost) | m.Post("/forgot_password", user.ForgotPasswdPost) | ||||
m.Post("/logout", user.SignOut) | m.Post("/logout", user.SignOut) | ||||
m.Get("/task/:task", user.TaskStatus) | |||||
}) | }) | ||||
// ***** END: User ***** | // ***** END: User ***** | ||||
m.Get("/archive/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.Download) | m.Get("/archive/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.Download) | ||||
m.Get("/status", reqRepoCodeReader, repo.Status) | |||||
m.Group("/branches", func() { | m.Group("/branches", func() { | ||||
m.Get("", repo.Branches) | m.Get("", repo.Branches) | ||||
}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) | }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) |
// Copyright 2020 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 user | |||||
import ( | |||||
"code.gitea.io/gitea/models" | |||||
"code.gitea.io/gitea/modules/context" | |||||
) | |||||
// TaskStatus returns task's status | |||||
func TaskStatus(ctx *context.Context) { | |||||
task, opts, err := models.GetMigratingTaskByID(ctx.ParamsInt64("task"), ctx.User.ID) | |||||
if err != nil { | |||||
ctx.JSON(500, map[string]interface{}{ | |||||
"err": err, | |||||
}) | |||||
return | |||||
} | |||||
ctx.JSON(200, map[string]interface{}{ | |||||
"status": task.Status, | |||||
"err": task.Errors, | |||||
"repo-id": task.RepoID, | |||||
"repo-name": opts.RepoName, | |||||
"start": task.StartTime, | |||||
"end": task.EndTime, | |||||
}) | |||||
} |
{{template "base/alert" .}} | {{template "base/alert" .}} | ||||
<div class="home"> | <div class="home"> | ||||
<div class="ui stackable middle very relaxed page grid"> | <div class="ui stackable middle very relaxed page grid"> | ||||
<div id="repo_migrating" class="sixteen wide center aligned centered column" repo="{{.Repo.Repository.FullName}}"> | |||||
<div id="repo_migrating" class="sixteen wide center aligned centered column" task="{{.MigrateTask.ID}}"> | |||||
<div> | <div> | ||||
<img src="{{StaticUrlPrefix}}/img/loading.png"/> | <img src="{{StaticUrlPrefix}}/img/loading.png"/> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div id="repo_migrating_failed_image" class="sixteen wide center aligned centered column" style="display: none;"> | |||||
<div> | |||||
<img src="{{StaticUrlPrefix}}/img/failed.png"/> | |||||
</div> | |||||
</div> | |||||
</div> | </div> | ||||
<div class="ui stackable middle very relaxed page grid"> | <div class="ui stackable middle very relaxed page grid"> | ||||
<div class="sixteen wide center aligned centered column"> | <div class="sixteen wide center aligned centered column"> | ||||
</div> | </div> | ||||
<div id="repo_migrating_failed"> | <div id="repo_migrating_failed"> | ||||
<p>{{.i18n.Tr "repo.migrate.migrating_failed" .CloneAddr | Safe}}</p> | <p>{{.i18n.Tr "repo.migrate.migrating_failed" .CloneAddr | Safe}}</p> | ||||
<p id="repo_migrating_failed_error"></p> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> |
function initRepoStatusChecker() { | function initRepoStatusChecker() { | ||||
const migrating = $('#repo_migrating'); | const migrating = $('#repo_migrating'); | ||||
$('#repo_migrating_failed').hide(); | $('#repo_migrating_failed').hide(); | ||||
$('#repo_migrating_failed_image').hide(); | |||||
if (migrating) { | if (migrating) { | ||||
const repo_name = migrating.attr('repo'); | |||||
if (typeof repo_name === 'undefined') { | |||||
const task = migrating.attr('task'); | |||||
if (typeof task === 'undefined') { | |||||
return; | return; | ||||
} | } | ||||
$.ajax({ | $.ajax({ | ||||
type: 'GET', | type: 'GET', | ||||
url: `${AppSubUrl}/${repo_name}/status`, | |||||
url: `${AppSubUrl}/user/task/${task}`, | |||||
data: { | data: { | ||||
_csrf: csrf, | _csrf: csrf, | ||||
}, | }, | ||||
complete(xhr) { | complete(xhr) { | ||||
if (xhr.status === 200) { | if (xhr.status === 200) { | ||||
if (xhr.responseJSON) { | if (xhr.responseJSON) { | ||||
if (xhr.responseJSON.status === 0) { | |||||
if (xhr.responseJSON.status === 4) { | |||||
window.location.reload(); | window.location.reload(); | ||||
return; | return; | ||||
} else if (xhr.responseJSON.status === 3) { | |||||
$('#repo_migrating_progress').hide(); | |||||
$('#repo_migrating').hide(); | |||||
$('#repo_migrating_failed').show(); | |||||
$('#repo_migrating_failed_image').show(); | |||||
$('#repo_migrating_failed_error').text(xhr.responseJSON.err); | |||||
return; | |||||
} | } | ||||
setTimeout(() => { | setTimeout(() => { | ||||
initRepoStatusChecker(); | initRepoStatusChecker(); | ||||
}, 2000); | }, 2000); | ||||
} | } | ||||
} | } | ||||
$('#repo_migrating_progress').hide(); | $('#repo_migrating_progress').hide(); | ||||
$('#repo_migrating').hide(); | |||||
$('#repo_migrating_failed').show(); | $('#repo_migrating_failed').show(); | ||||
$('#repo_migrating_failed_image').show(); | |||||
} | } | ||||
}); | }); | ||||
} | } |