mirror of
https://github.com/go-gitea/gitea
synced 2025-01-31 03:57:54 +01:00
ee6f508799
If the request isn't complete, this endpoint will now submit the request and wait for completion using the new API. This may still be susceptible to timeouts for larger repos, but other endpoints now exist that the web interface will use to negotiate its way through larger archive processes.
548 lines
16 KiB
Go
548 lines
16 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// 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 repo
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/models"
|
|
"code.gitea.io/gitea/modules/auth"
|
|
"code.gitea.io/gitea/modules/base"
|
|
"code.gitea.io/gitea/modules/context"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/migrations"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/task"
|
|
"code.gitea.io/gitea/modules/util"
|
|
archiver_service "code.gitea.io/gitea/services/archiver"
|
|
repo_service "code.gitea.io/gitea/services/repository"
|
|
)
|
|
|
|
const (
|
|
tplCreate base.TplName = "repo/create"
|
|
tplMigrate base.TplName = "repo/migrate"
|
|
)
|
|
|
|
// MustBeNotEmpty render when a repo is a empty git dir
|
|
func MustBeNotEmpty(ctx *context.Context) {
|
|
if ctx.Repo.Repository.IsEmpty {
|
|
ctx.NotFound("MustBeNotEmpty", nil)
|
|
}
|
|
}
|
|
|
|
// MustBeEditable check that repo can be edited
|
|
func MustBeEditable(ctx *context.Context) {
|
|
if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
|
|
ctx.NotFound("", nil)
|
|
return
|
|
}
|
|
}
|
|
|
|
// MustBeAbleToUpload check that repo can be uploaded to
|
|
func MustBeAbleToUpload(ctx *context.Context) {
|
|
if !setting.Repository.Upload.Enabled {
|
|
ctx.NotFound("", nil)
|
|
}
|
|
}
|
|
|
|
func checkContextUser(ctx *context.Context, uid int64) *models.User {
|
|
orgs, err := models.GetOrgsCanCreateRepoByUserID(ctx.User.ID)
|
|
if err != nil {
|
|
ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
|
|
return nil
|
|
}
|
|
|
|
if !ctx.User.IsAdmin {
|
|
orgsAvailable := []*models.User{}
|
|
for i := 0; i < len(orgs); i++ {
|
|
if orgs[i].CanCreateRepo() {
|
|
orgsAvailable = append(orgsAvailable, orgs[i])
|
|
}
|
|
}
|
|
ctx.Data["Orgs"] = orgsAvailable
|
|
} else {
|
|
ctx.Data["Orgs"] = orgs
|
|
}
|
|
|
|
// Not equal means current user is an organization.
|
|
if uid == ctx.User.ID || uid == 0 {
|
|
return ctx.User
|
|
}
|
|
|
|
org, err := models.GetUserByID(uid)
|
|
if models.IsErrUserNotExist(err) {
|
|
return ctx.User
|
|
}
|
|
|
|
if err != nil {
|
|
ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %v", uid, err))
|
|
return nil
|
|
}
|
|
|
|
// Check ownership of organization.
|
|
if !org.IsOrganization() {
|
|
ctx.Error(403)
|
|
return nil
|
|
}
|
|
if !ctx.User.IsAdmin {
|
|
canCreate, err := org.CanCreateOrgRepo(ctx.User.ID)
|
|
if err != nil {
|
|
ctx.ServerError("CanCreateOrgRepo", err)
|
|
return nil
|
|
} else if !canCreate {
|
|
ctx.Error(403)
|
|
return nil
|
|
}
|
|
} else {
|
|
ctx.Data["Orgs"] = orgs
|
|
}
|
|
return org
|
|
}
|
|
|
|
func getRepoPrivate(ctx *context.Context) bool {
|
|
switch strings.ToLower(setting.Repository.DefaultPrivate) {
|
|
case setting.RepoCreatingLastUserVisibility:
|
|
return ctx.User.LastRepoVisibility
|
|
case setting.RepoCreatingPrivate:
|
|
return true
|
|
case setting.RepoCreatingPublic:
|
|
return false
|
|
default:
|
|
return ctx.User.LastRepoVisibility
|
|
}
|
|
}
|
|
|
|
// Create render creating repository page
|
|
func Create(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("new_repo")
|
|
|
|
// Give default value for template to render.
|
|
ctx.Data["Gitignores"] = models.Gitignores
|
|
ctx.Data["LabelTemplates"] = models.LabelTemplates
|
|
ctx.Data["Licenses"] = models.Licenses
|
|
ctx.Data["Readmes"] = models.Readmes
|
|
ctx.Data["readme"] = "Default"
|
|
ctx.Data["private"] = getRepoPrivate(ctx)
|
|
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
|
|
|
|
ctxUser := checkContextUser(ctx, ctx.QueryInt64("org"))
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Data["ContextUser"] = ctxUser
|
|
|
|
ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select")
|
|
templateID := ctx.QueryInt64("template_id")
|
|
if templateID > 0 {
|
|
templateRepo, err := models.GetRepositoryByID(templateID)
|
|
if err == nil && templateRepo.CheckUnitUser(ctxUser.ID, ctxUser.IsAdmin, models.UnitTypeCode) {
|
|
ctx.Data["repo_template"] = templateID
|
|
ctx.Data["repo_template_name"] = templateRepo.Name
|
|
}
|
|
}
|
|
|
|
if !ctx.User.CanCreateRepo() {
|
|
ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", ctx.User.MaxCreationLimit()), tplCreate, nil)
|
|
} else {
|
|
ctx.HTML(200, tplCreate)
|
|
}
|
|
}
|
|
|
|
func handleCreateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form interface{}) {
|
|
switch {
|
|
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:
|
|
ctx.ServerError(name, err)
|
|
}
|
|
}
|
|
|
|
// CreatePost response for creating repository
|
|
func CreatePost(ctx *context.Context, form auth.CreateRepoForm) {
|
|
ctx.Data["Title"] = ctx.Tr("new_repo")
|
|
|
|
ctx.Data["Gitignores"] = models.Gitignores
|
|
ctx.Data["LabelTemplates"] = models.LabelTemplates
|
|
ctx.Data["Licenses"] = models.Licenses
|
|
ctx.Data["Readmes"] = models.Readmes
|
|
|
|
ctxUser := checkContextUser(ctx, form.UID)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Data["ContextUser"] = ctxUser
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(200, tplCreate)
|
|
return
|
|
}
|
|
|
|
var repo *models.Repository
|
|
var err error
|
|
if form.RepoTemplate > 0 {
|
|
opts := models.GenerateRepoOptions{
|
|
Name: form.RepoName,
|
|
Description: form.Description,
|
|
Private: form.Private,
|
|
GitContent: form.GitContent,
|
|
Topics: form.Topics,
|
|
GitHooks: form.GitHooks,
|
|
Webhooks: form.Webhooks,
|
|
Avatar: form.Avatar,
|
|
IssueLabels: form.Labels,
|
|
}
|
|
|
|
if !opts.IsValid() {
|
|
ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form)
|
|
return
|
|
}
|
|
|
|
templateRepo := getRepository(ctx, form.RepoTemplate)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
if !templateRepo.IsTemplate {
|
|
ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form)
|
|
return
|
|
}
|
|
|
|
repo, err = repo_service.GenerateRepository(ctx.User, ctxUser, templateRepo, opts)
|
|
if err == nil {
|
|
log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
|
|
ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + repo.Name)
|
|
return
|
|
}
|
|
} else {
|
|
repo, err = repo_service.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{
|
|
Name: form.RepoName,
|
|
Description: form.Description,
|
|
Gitignores: form.Gitignores,
|
|
IssueLabels: form.IssueLabels,
|
|
License: form.License,
|
|
Readme: form.Readme,
|
|
IsPrivate: form.Private || setting.Repository.ForcePrivate,
|
|
DefaultBranch: form.DefaultBranch,
|
|
AutoInit: form.AutoInit,
|
|
})
|
|
if err == nil {
|
|
log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
|
|
ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + repo.Name)
|
|
return
|
|
}
|
|
}
|
|
|
|
handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form)
|
|
}
|
|
|
|
// Migrate render migration of repository page
|
|
func Migrate(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("new_migrate")
|
|
ctx.Data["private"] = getRepoPrivate(ctx)
|
|
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
|
|
ctx.Data["mirror"] = ctx.Query("mirror") == "1"
|
|
ctx.Data["wiki"] = ctx.Query("wiki") == "1"
|
|
ctx.Data["milestones"] = ctx.Query("milestones") == "1"
|
|
ctx.Data["labels"] = ctx.Query("labels") == "1"
|
|
ctx.Data["issues"] = ctx.Query("issues") == "1"
|
|
ctx.Data["pull_requests"] = ctx.Query("pull_requests") == "1"
|
|
ctx.Data["releases"] = ctx.Query("releases") == "1"
|
|
ctx.Data["LFSActive"] = setting.LFS.StartServer
|
|
|
|
ctxUser := checkContextUser(ctx, ctx.QueryInt64("org"))
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Data["ContextUser"] = ctxUser
|
|
|
|
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")
|
|
|
|
ctxUser := checkContextUser(ctx, form.UID)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Data["ContextUser"] = ctxUser
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(200, tplMigrate)
|
|
return
|
|
}
|
|
|
|
remoteAddr, err := form.ParseRemoteAddr(ctx.User)
|
|
if err != nil {
|
|
if models.IsErrInvalidCloneAddr(err) {
|
|
ctx.Data["Err_CloneAddr"] = true
|
|
addrErr := err.(models.ErrInvalidCloneAddr)
|
|
switch {
|
|
case addrErr.IsURLError:
|
|
ctx.RenderWithErr(ctx.Tr("form.url_error"), tplMigrate, &form)
|
|
case addrErr.IsPermissionDenied:
|
|
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplMigrate, &form)
|
|
case addrErr.IsInvalidPath:
|
|
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplMigrate, &form)
|
|
default:
|
|
ctx.ServerError("Unknown error", err)
|
|
}
|
|
} else {
|
|
ctx.ServerError("ParseRemoteAddr", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
var gitServiceType = structs.PlainGitService
|
|
u, err := url.Parse(form.CloneAddr)
|
|
if err == nil && strings.EqualFold(u.Host, "github.com") {
|
|
gitServiceType = structs.GithubService
|
|
}
|
|
|
|
var opts = migrations.MigrateOptions{
|
|
OriginalURL: form.CloneAddr,
|
|
GitServiceType: gitServiceType,
|
|
CloneAddr: remoteAddr,
|
|
RepoName: form.RepoName,
|
|
Description: form.Description,
|
|
Private: form.Private || setting.Repository.ForcePrivate,
|
|
Mirror: form.Mirror,
|
|
AuthUsername: form.AuthUsername,
|
|
AuthPassword: form.AuthPassword,
|
|
Wiki: form.Wiki,
|
|
Issues: form.Issues,
|
|
Milestones: form.Milestones,
|
|
Labels: form.Labels,
|
|
Comments: true,
|
|
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
|
|
}
|
|
|
|
err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName)
|
|
if err != nil {
|
|
handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form)
|
|
return
|
|
}
|
|
|
|
err = task.MigrateRepository(ctx.User, ctxUser, opts)
|
|
if err == nil {
|
|
ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.RepoName)
|
|
return
|
|
}
|
|
|
|
handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form)
|
|
}
|
|
|
|
// Action response for actions to a repository
|
|
func Action(ctx *context.Context) {
|
|
var err error
|
|
switch ctx.Params(":action") {
|
|
case "watch":
|
|
err = models.WatchRepo(ctx.User.ID, ctx.Repo.Repository.ID, true)
|
|
case "unwatch":
|
|
err = models.WatchRepo(ctx.User.ID, ctx.Repo.Repository.ID, false)
|
|
case "star":
|
|
err = models.StarRepo(ctx.User.ID, ctx.Repo.Repository.ID, true)
|
|
case "unstar":
|
|
err = models.StarRepo(ctx.User.ID, ctx.Repo.Repository.ID, false)
|
|
case "desc": // FIXME: this is not used
|
|
if !ctx.Repo.IsOwner() {
|
|
ctx.Error(404)
|
|
return
|
|
}
|
|
|
|
ctx.Repo.Repository.Description = ctx.Query("desc")
|
|
ctx.Repo.Repository.Website = ctx.Query("site")
|
|
err = models.UpdateRepository(ctx.Repo.Repository, false)
|
|
}
|
|
|
|
if err != nil {
|
|
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
|
|
return
|
|
}
|
|
|
|
ctx.RedirectToFirst(ctx.Query("redirect_to"), ctx.Repo.RepoLink)
|
|
}
|
|
|
|
// RedirectDownload return a file based on the following infos:
|
|
func RedirectDownload(ctx *context.Context) {
|
|
var (
|
|
vTag = ctx.Params("vTag")
|
|
fileName = ctx.Params("fileName")
|
|
)
|
|
tagNames := []string{vTag}
|
|
curRepo := ctx.Repo.Repository
|
|
releases, err := models.GetReleasesByRepoIDAndNames(models.DefaultDBContext(), curRepo.ID, tagNames)
|
|
if err != nil {
|
|
if models.IsErrAttachmentNotExist(err) {
|
|
ctx.Error(404)
|
|
return
|
|
}
|
|
ctx.ServerError("RedirectDownload", err)
|
|
return
|
|
}
|
|
if len(releases) == 1 {
|
|
release := releases[0]
|
|
att, err := models.GetAttachmentByReleaseIDFileName(release.ID, fileName)
|
|
if err != nil {
|
|
ctx.Error(404)
|
|
return
|
|
}
|
|
if att != nil {
|
|
ctx.Redirect(att.DownloadURL())
|
|
return
|
|
}
|
|
}
|
|
ctx.Error(404)
|
|
}
|
|
|
|
// DownloadStatus checks the status of a download, because archiving may take a
|
|
// while. It does so by creating an archive request from the archiver service,
|
|
// then just examining the completion status.
|
|
func DownloadStatus(ctx *context.Context) {
|
|
uri := ctx.Params("*")
|
|
aReq := archiver_service.DeriveRequestFrom(ctx, uri)
|
|
|
|
if aReq == nil {
|
|
ctx.Error(404)
|
|
return
|
|
}
|
|
|
|
complete := aReq.IsComplete()
|
|
ctx.JSON(200, map[string]interface{}{
|
|
"archiving": !complete,
|
|
"complete": complete,
|
|
})
|
|
}
|
|
|
|
// Download an archive of a repository
|
|
func Download(ctx *context.Context) {
|
|
uri := ctx.Params("*")
|
|
aReq := archiver_service.DeriveRequestFrom(ctx, uri)
|
|
|
|
downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName()
|
|
if !aReq.IsComplete() {
|
|
aReq = archiver_service.ArchiveRepository(aReq)
|
|
archiver_service.LockQueue()
|
|
for !aReq.IsComplete() {
|
|
archiver_service.WaitForCompletion()
|
|
}
|
|
archiver_service.UnlockQueue()
|
|
}
|
|
|
|
if aReq.IsComplete() {
|
|
ctx.ServeFile(aReq.GetArchivePath(), downloadName)
|
|
} else {
|
|
ctx.Error(404)
|
|
}
|
|
}
|
|
|
|
// InitiateDownload will enqueue an archival request, as needed. It may submit
|
|
// a request that's already in-progress, but the archiver service will just
|
|
// kind of drop it on the floor if this is the case.
|
|
func InitiateDownload(ctx *context.Context) {
|
|
uri := ctx.Params("*")
|
|
aReq := archiver_service.DeriveRequestFrom(ctx, uri)
|
|
|
|
if aReq == nil {
|
|
return
|
|
}
|
|
|
|
complete := aReq.IsComplete()
|
|
if !complete {
|
|
aReq = archiver_service.ArchiveRepository(aReq)
|
|
// As with the standard Download, we'll wait up to two seconds for the request
|
|
// to be completed. The difference is that we'll never download the file from a POST
|
|
// request, only indicate the current status. If we did manage to complete the request
|
|
// in this timeframe, the download will proceed with no further overhead.
|
|
timeout := time.Now().Add(2 * time.Second)
|
|
for {
|
|
if aReq.IsComplete() || time.Now().After(timeout) {
|
|
break
|
|
}
|
|
}
|
|
|
|
complete = aReq.IsComplete()
|
|
}
|
|
|
|
ctx.JSON(200, map[string]interface{}{
|
|
"archiving": !complete,
|
|
"complete": complete,
|
|
})
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
}
|