mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
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]>
329 lines
11 KiB
Go
329 lines
11 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
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/git"
|
|
"github.com/hanzoai/git/modules/json"
|
|
"github.com/hanzoai/git/modules/lfs"
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/structs"
|
|
"github.com/hanzoai/git/modules/templates"
|
|
"github.com/hanzoai/git/modules/util"
|
|
"github.com/hanzoai/git/modules/web"
|
|
"github.com/hanzoai/git/services/context"
|
|
"github.com/hanzoai/git/services/forms"
|
|
"github.com/hanzoai/git/services/migrations"
|
|
repo_service "github.com/hanzoai/git/services/repository"
|
|
"github.com/hanzoai/git/services/task"
|
|
)
|
|
|
|
const (
|
|
tplMigrate templates.TplName = "repo/migrate/migrate"
|
|
)
|
|
|
|
// Migrate render migration of repository page
|
|
func Migrate(ctx *context.Context) {
|
|
if setting.Repository.DisableMigrations {
|
|
ctx.HTTPError(http.StatusForbidden, "Migrate: the site administrator has disabled migrations")
|
|
return
|
|
}
|
|
|
|
serviceType := structs.GitServiceType(ctx.FormInt("service_type"))
|
|
|
|
setMigrationContextData(ctx, serviceType)
|
|
|
|
if serviceType == 0 {
|
|
ctx.Data["Org"] = ctx.FormString("org")
|
|
ctx.Data["Mirror"] = ctx.FormString("mirror")
|
|
|
|
ctx.HTML(http.StatusOK, tplMigrate)
|
|
return
|
|
}
|
|
|
|
ctx.Data["private"] = getRepoPrivate(ctx)
|
|
ctx.Data["mirror"] = ctx.FormString("mirror") == "1"
|
|
ctx.Data["lfs"] = ctx.FormString("lfs") == "1"
|
|
ctx.Data["wiki"] = ctx.FormString("wiki") == "1"
|
|
ctx.Data["milestones"] = ctx.FormString("milestones") == "1"
|
|
ctx.Data["labels"] = ctx.FormString("labels") == "1"
|
|
ctx.Data["issues"] = ctx.FormString("issues") == "1"
|
|
ctx.Data["pull_requests"] = ctx.FormString("pull_requests") == "1"
|
|
ctx.Data["releases"] = ctx.FormString("releases") == "1"
|
|
|
|
ctxUser := checkContextUser(ctx, ctx.FormInt64("org"))
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Data["ContextUser"] = ctxUser
|
|
|
|
ctx.HTML(http.StatusOK, templates.TplName("repo/migrate/"+serviceType.Name()))
|
|
}
|
|
|
|
func handleMigrateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl templates.TplName, form *forms.MigrateRepoForm) {
|
|
if setting.Repository.DisableMigrations {
|
|
ctx.HTTPError(http.StatusForbidden, "MigrateError: the site administrator has disabled migrations")
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case migrations.IsRateLimitError(err):
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.visit_rate_limit"), tpl, form)
|
|
case migrations.IsTwoFactorAuthError(err):
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.2fa_auth_required"), tpl, form)
|
|
case repo_model.IsErrReachLimitOfRepo(err):
|
|
maxCreationLimit := owner.MaxCreationLimit()
|
|
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
|
|
ctx.RenderWithErrDeprecated(msg, tpl, form)
|
|
case repo_model.IsErrRepoAlreadyExist(err):
|
|
ctx.Data["Err_RepoName"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tpl, form)
|
|
case repo_model.IsErrRepoFilesAlreadyExist(err):
|
|
ctx.Data["Err_RepoName"] = true
|
|
switch {
|
|
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
|
|
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
|
|
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
|
|
default:
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form)
|
|
}
|
|
case db.IsErrNameReserved(err):
|
|
ctx.Data["Err_RepoName"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
|
|
case db.IsErrNamePatternNotAllowed(err):
|
|
ctx.Data["Err_RepoName"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
|
|
default:
|
|
err = util.SanitizeErrorCredentialURLs(err)
|
|
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.RenderWithErrDeprecated(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
|
|
} else if strings.Contains(err.Error(), "fatal:") {
|
|
ctx.Data["Err_CloneAddr"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
|
|
} else {
|
|
ctx.ServerError(name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl templates.TplName, form *forms.MigrateRepoForm) {
|
|
if git.IsErrInvalidCloneAddr(err) {
|
|
addrErr := err.(*git.ErrInvalidCloneAddr)
|
|
switch {
|
|
case addrErr.IsProtocolInvalid:
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form)
|
|
case addrErr.IsURLError:
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", addrErr.Host), tpl, form)
|
|
case addrErr.IsPermissionDenied:
|
|
if addrErr.LocalPath {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied"), tpl, form)
|
|
} else {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form)
|
|
}
|
|
case addrErr.IsInvalidPath:
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
|
|
default:
|
|
log.Error("Error whilst updating url: %v", err)
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", "unknown"), tpl, form)
|
|
}
|
|
} else {
|
|
log.Error("Error whilst updating url: %v", err)
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", "unknown"), tpl, form)
|
|
}
|
|
}
|
|
|
|
// MigratePost response for migrating from external git repository
|
|
func MigratePost(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.MigrateRepoForm)
|
|
if setting.Repository.DisableMigrations {
|
|
ctx.HTTPError(http.StatusForbidden, "MigratePost: the site administrator has disabled migrations")
|
|
return
|
|
}
|
|
|
|
if form.Mirror && setting.Mirror.DisableNewPull {
|
|
ctx.HTTPError(http.StatusBadRequest, "MigratePost: the site administrator has disabled creation of new mirrors")
|
|
return
|
|
}
|
|
|
|
setMigrationContextData(ctx, form.Service)
|
|
|
|
ctxUser := checkContextUser(ctx, form.UID)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Data["ContextUser"] = ctxUser
|
|
|
|
tpl := templates.TplName("repo/migrate/" + form.Service.Name())
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tpl)
|
|
return
|
|
}
|
|
|
|
remoteAddr, err := git.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
|
|
if err == nil {
|
|
err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.Doer)
|
|
}
|
|
if err != nil {
|
|
ctx.Data["Err_CloneAddr"] = true
|
|
handleMigrateRemoteAddrError(ctx, err, tpl, form)
|
|
return
|
|
}
|
|
|
|
form.LFS = form.LFS && setting.LFS.StartServer
|
|
|
|
if form.LFS && len(form.LFSEndpoint) > 0 {
|
|
ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
|
|
if ep == nil {
|
|
ctx.Data["Err_LFSEndpoint"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form)
|
|
return
|
|
}
|
|
err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
|
|
if err != nil {
|
|
ctx.Data["Err_LFSEndpoint"] = true
|
|
handleMigrateRemoteAddrError(ctx, err, tpl, form)
|
|
return
|
|
}
|
|
}
|
|
|
|
opts := migrations.MigrateOptions{
|
|
OriginalURL: form.CloneAddr,
|
|
GitServiceType: form.Service,
|
|
CloneAddr: remoteAddr,
|
|
RepoName: form.RepoName,
|
|
Description: form.Description,
|
|
Private: form.Private || setting.Repository.ForcePrivate,
|
|
Mirror: form.Mirror,
|
|
LFS: form.LFS,
|
|
LFSEndpoint: form.LFSEndpoint,
|
|
AuthUsername: form.AuthUsername,
|
|
AuthPassword: form.AuthPassword,
|
|
AuthToken: form.AuthToken,
|
|
Wiki: form.Wiki,
|
|
Issues: form.Issues,
|
|
Milestones: form.Milestones,
|
|
Labels: form.Labels,
|
|
Comments: form.Issues || form.PullRequests,
|
|
PullRequests: form.PullRequests,
|
|
Releases: form.Releases,
|
|
}
|
|
if opts.Mirror {
|
|
opts.Issues = false
|
|
opts.Milestones = false
|
|
opts.Labels = false
|
|
opts.Comments = false
|
|
opts.PullRequests = false
|
|
opts.Releases = false
|
|
}
|
|
if form.Service == structs.CodeCommitService {
|
|
opts.AWSAccessKeyID = form.AWSAccessKeyID
|
|
opts.AWSSecretAccessKey = form.AWSSecretAccessKey
|
|
}
|
|
|
|
err = repo_service.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
|
|
if err != nil {
|
|
handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
|
|
return
|
|
}
|
|
|
|
err = task.MigrateRepository(ctx, ctx.Doer, ctxUser, opts)
|
|
if err == nil {
|
|
ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(opts.RepoName))
|
|
return
|
|
}
|
|
|
|
handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
|
|
}
|
|
|
|
func setMigrationContextData(ctx *context.Context, serviceType structs.GitServiceType) {
|
|
ctx.Data["Title"] = ctx.Tr("new_migrate")
|
|
|
|
ctx.Data["LFSActive"] = setting.LFS.StartServer
|
|
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
|
|
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
|
|
|
|
// Plain git should be first
|
|
ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...)
|
|
ctx.Data["service"] = serviceType
|
|
}
|
|
|
|
func MigrateRetryPost(ctx *context.Context) {
|
|
if err := task.RetryMigrateTask(ctx, ctx.Repo.Repository.ID); err != nil {
|
|
log.Error("Retry task failed: %v", err)
|
|
ctx.ServerError("task.RetryMigrateTask", err)
|
|
return
|
|
}
|
|
ctx.JSONOK()
|
|
}
|
|
|
|
func MigrateCancelPost(ctx *context.Context) {
|
|
migratingTask, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID)
|
|
if err != nil {
|
|
log.Error("GetMigratingTask: %v", err)
|
|
ctx.Redirect(ctx.Repo.Repository.Link())
|
|
return
|
|
}
|
|
if migratingTask.Status == structs.TaskStatusRunning {
|
|
taskUpdate := &admin_model.Task{ID: migratingTask.ID, Status: structs.TaskStatusFailed, Message: "canceled"}
|
|
if err = taskUpdate.UpdateCols(ctx, "status", "message"); err != nil {
|
|
ctx.ServerError("task.UpdateCols", err)
|
|
return
|
|
}
|
|
}
|
|
ctx.Redirect(ctx.Repo.Repository.Link())
|
|
}
|
|
|
|
// MigrateStatus returns migrate task's status
|
|
func MigrateStatus(ctx *context.Context) {
|
|
task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID)
|
|
if err != nil {
|
|
if admin_model.IsErrTaskDoesNotExist(err) {
|
|
ctx.JSON(http.StatusNotFound, map[string]any{
|
|
"err": "task does not exist or you do not have access to this task",
|
|
})
|
|
return
|
|
}
|
|
log.Error("GetMigratingTask: %v", err)
|
|
ctx.JSON(http.StatusInternalServerError, map[string]any{
|
|
"err": http.StatusText(http.StatusInternalServerError),
|
|
})
|
|
return
|
|
}
|
|
|
|
message := task.Message
|
|
|
|
if task.Message != "" && task.Message[0] == '{' {
|
|
// assume message is actually a translatable string
|
|
var translatableMessage admin_model.TranslatableMessage
|
|
if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil {
|
|
translatableMessage = admin_model.TranslatableMessage{
|
|
Format: "migrate.migrating_failed.error",
|
|
Args: []any{task.Message},
|
|
}
|
|
}
|
|
message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...)
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, map[string]any{
|
|
"status": task.Status,
|
|
"message": message,
|
|
})
|
|
}
|