Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

155 lines
4.9 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package task
import (
"context"
"errors"
"fmt"
"strings"
"time"
admin_model "github.com/hanzoai/git/models/admin"
"github.com/hanzoai/git/models/db"
repo_model "github.com/hanzoai/git/models/repo"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/graceful"
"github.com/hanzoai/git/modules/json"
"github.com/hanzoai/git/modules/log"
"github.com/hanzoai/git/modules/migration"
"github.com/hanzoai/git/modules/process"
"github.com/hanzoai/git/modules/structs"
"github.com/hanzoai/git/modules/timeutil"
"github.com/hanzoai/git/modules/util"
"github.com/hanzoai/git/services/migrations"
notify_service "github.com/hanzoai/git/services/notify"
)
func handleCreateError(owner *user_model.User, err error) error {
switch {
case repo_model.IsErrReachLimitOfRepo(err):
return fmt.Errorf("you have already reached your limit of %d repositories", owner.MaxCreationLimit())
case repo_model.IsErrRepoAlreadyExist(err):
return errors.New("the repository name is already used")
case db.IsErrNameReserved(err):
return fmt.Errorf("the repository name '%s' is reserved", err.(db.ErrNameReserved).Name)
case db.IsErrNamePatternNotAllowed(err):
return fmt.Errorf("the pattern '%s' is not allowed in a repository name", err.(db.ErrNamePatternNotAllowed).Pattern)
default:
return err
}
}
func runMigrateTask(ctx context.Context, t *admin_model.Task) (err error) {
defer func(ctx context.Context) {
if e := recover(); e != nil {
err = fmt.Errorf("PANIC whilst trying to do migrate task: %v", e)
log.Error("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 {
err = admin_model.FinishMigrateTask(ctx, t)
if err == nil {
notify_service.MigrateRepository(ctx, t.Doer, t.Owner, t.Repo)
return
}
log.Error("FinishMigrateTask[%d] by DoerID[%d] to RepoID[%d] for OwnerID[%d] failed: %v", t.ID, t.DoerID, t.RepoID, t.OwnerID, err)
}
log.Error("runMigrateTask[%d] by DoerID[%d] to RepoID[%d] for OwnerID[%d] failed: %v", t.ID, t.DoerID, t.RepoID, t.OwnerID, err)
t.EndTime = timeutil.TimeStampNow()
t.Status = structs.TaskStatusFailed
t.Message = err.Error()
if err := t.UpdateCols(ctx, "status", "message", "end_time"); err != nil {
log.Error("Task UpdateCols failed: %v", err)
}
// then, do not delete the repository, otherwise the users won't be able to see the last error
}(graceful.GetManager().ShutdownContext()) // even if the parent ctx is canceled, this defer-function still needs to update the task record in database
if err = t.LoadRepo(ctx); err != nil {
return err
}
// if repository is ready, then just finish the task
if t.Repo.Status == repo_model.RepositoryReady {
return nil
}
if err = t.LoadDoer(ctx); err != nil {
return err
}
if err = t.LoadOwner(ctx); err != nil {
return err
}
var opts *migration.MigrateOptions
opts, err = t.MigrateConfig()
if err != nil {
return err
}
opts.MigrateToRepoID = t.RepoID
pm := process.GetManager()
ctx, cancel, finished := pm.AddContext(graceful.GetManager().ShutdownContext(), fmt.Sprintf("MigrateTask: %s/%s", t.Owner.Name, opts.RepoName))
defer finished()
t.StartTime = timeutil.TimeStampNow()
t.Status = structs.TaskStatusRunning
if err = t.UpdateCols(ctx, "start_time", "status"); err != nil {
return err
}
// check whether the task should be canceled, this goroutine is also managed by process manager
go func() {
for {
select {
case <-time.After(2 * time.Second):
case <-ctx.Done():
return
}
task, _ := admin_model.GetMigratingTask(ctx, t.RepoID)
if task != nil && task.Status != structs.TaskStatusRunning {
log.Debug("MigrateTask[%d] by DoerID[%d] to RepoID[%d] for OwnerID[%d] is canceled due to status is not 'running'", t.ID, t.DoerID, t.RepoID, t.OwnerID)
cancel()
return
}
}
}()
t.Repo, err = migrations.MigrateRepository(ctx, t.Doer, t.Owner.Name, *opts, func(format string, args ...any) {
message := admin_model.TranslatableMessage{
Format: format,
Args: args,
}
bs, _ := json.Marshal(message)
t.Message = string(bs)
_ = t.UpdateCols(ctx, "message")
})
if err == nil {
log.Trace("Repository migrated [%d]: %s/%s", t.Repo.ID, t.Owner.Name, t.Repo.Name)
return nil
}
if repo_model.IsErrRepoAlreadyExist(err) {
return errors.New("the repository name is already used")
}
// remoteAddr may contain credentials, so we sanitize it
err = util.SanitizeErrorCredentialURLs(err)
if strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "could not read Username") {
return fmt.Errorf("authentication failed: %w", err)
} else if strings.Contains(err.Error(), "fatal:") {
return fmt.Errorf("migration failed: %w", err)
}
// do not be tempted to coalesce this line with the return
err = handleCreateError(t.Owner, err)
return err
}