mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-19 15:40:50 +00:00
See https://codeberg.org/forgejo/forgejo/pulls/4801#issuecomment-5094525 and #8152 for more context. The current implementation is limited to self-hosted actions and buggy as soon as multiple repos are involved, like for the homepage (because each permission must be fetched individually). Ideally this feature should work for all kind of status (with some setting indicating which collaborator can access with status). Probably inside the `git_model.ParseCommitsWithStatus` function. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8177 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: oliverpool <git@olivier.pfad.fr> Co-committed-by: oliverpool <git@olivier.pfad.fr>
805 lines
24 KiB
Go
805 lines
24 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
|
|
"forgejo.org/models"
|
|
"forgejo.org/models/db"
|
|
git_model "forgejo.org/models/git"
|
|
"forgejo.org/models/organization"
|
|
access_model "forgejo.org/models/perm/access"
|
|
quota_model "forgejo.org/models/quota"
|
|
repo_model "forgejo.org/models/repo"
|
|
"forgejo.org/models/unit"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/modules/base"
|
|
"forgejo.org/modules/cache"
|
|
"forgejo.org/modules/git"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/optional"
|
|
repo_module "forgejo.org/modules/repository"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/storage"
|
|
api "forgejo.org/modules/structs"
|
|
"forgejo.org/modules/util"
|
|
"forgejo.org/modules/web"
|
|
"forgejo.org/services/context"
|
|
"forgejo.org/services/convert"
|
|
"forgejo.org/services/forms"
|
|
repo_service "forgejo.org/services/repository"
|
|
archiver_service "forgejo.org/services/repository/archiver"
|
|
commitstatus_service "forgejo.org/services/repository/commitstatus"
|
|
)
|
|
|
|
const (
|
|
tplCreate base.TplName = "repo/create"
|
|
tplAlertDetails base.TplName = "base/alert_details"
|
|
)
|
|
|
|
// 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 CommitInfoCache(ctx *context.Context) {
|
|
var err error
|
|
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
|
if err != nil {
|
|
ctx.ServerError("GetBranchCommit", err)
|
|
return
|
|
}
|
|
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
|
|
if err != nil {
|
|
ctx.ServerError("GetCommitsCount", err)
|
|
return
|
|
}
|
|
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
|
|
ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
|
|
}
|
|
|
|
func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
|
|
orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
|
|
if err != nil {
|
|
ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
|
|
return nil
|
|
}
|
|
|
|
if !ctx.Doer.IsAdmin {
|
|
orgsAvailable := []*organization.Organization{}
|
|
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.Doer.ID || uid == 0 {
|
|
return ctx.Doer
|
|
}
|
|
|
|
org, err := user_model.GetUserByID(ctx, uid)
|
|
if user_model.IsErrUserNotExist(err) {
|
|
return ctx.Doer
|
|
}
|
|
|
|
if err != nil {
|
|
ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %w", uid, err))
|
|
return nil
|
|
}
|
|
|
|
// Check ownership of organization.
|
|
if !org.IsOrganization() {
|
|
ctx.Error(http.StatusForbidden)
|
|
return nil
|
|
}
|
|
if !ctx.Doer.IsAdmin {
|
|
canCreate, err := organization.OrgFromUser(org).CanCreateOrgRepo(ctx, ctx.Doer.ID)
|
|
if err != nil {
|
|
ctx.ServerError("CanCreateOrgRepo", err)
|
|
return nil
|
|
} else if !canCreate {
|
|
ctx.Error(http.StatusForbidden)
|
|
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.Doer.LastRepoVisibility
|
|
case setting.RepoCreatingPrivate:
|
|
return true
|
|
case setting.RepoCreatingPublic:
|
|
return false
|
|
default:
|
|
return ctx.Doer.LastRepoVisibility
|
|
}
|
|
}
|
|
|
|
// Create render creating repository page
|
|
func Create(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("new_repo.title")
|
|
|
|
// Give default value for template to render.
|
|
ctx.Data["Gitignores"] = repo_module.Gitignores
|
|
ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
|
|
ctx.Data["Licenses"] = repo_module.Licenses
|
|
ctx.Data["Readmes"] = repo_module.Readmes
|
|
ctx.Data["readme"] = "Default"
|
|
ctx.Data["private"] = getRepoPrivate(ctx)
|
|
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
|
|
ctx.Data["default_branch"] = setting.Repository.DefaultBranch
|
|
|
|
ctxUser := checkContextUser(ctx, ctx.FormInt64("org"))
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Data["ContextUser"] = ctxUser
|
|
|
|
ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select")
|
|
templateID := ctx.FormInt64("template_id")
|
|
if templateID > 0 {
|
|
templateRepo, err := repo_model.GetRepositoryByID(ctx, templateID)
|
|
if err == nil && access_model.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) {
|
|
ctx.Data["repo_template"] = templateID
|
|
ctx.Data["repo_template_name"] = templateRepo.Name
|
|
}
|
|
}
|
|
|
|
ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
|
|
ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
|
|
ctx.Data["SupportedObjectFormats"] = git.SupportedObjectFormats
|
|
ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat
|
|
|
|
ctx.HTML(http.StatusOK, tplCreate)
|
|
}
|
|
|
|
func handleCreateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl base.TplName, form any) {
|
|
switch {
|
|
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.RenderWithErr(msg, tpl, form)
|
|
case repo_model.IsErrRepoAlreadyExist(err):
|
|
ctx.Data["Err_RepoName"] = true
|
|
ctx.RenderWithErr(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.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
|
|
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
|
|
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
|
|
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
|
|
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
|
|
default:
|
|
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
|
|
}
|
|
case db.IsErrNameReserved(err):
|
|
ctx.Data["Err_RepoName"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
|
|
case db.IsErrNamePatternNotAllowed(err):
|
|
ctx.Data["Err_RepoName"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
|
|
default:
|
|
ctx.ServerError(name, err)
|
|
}
|
|
}
|
|
|
|
// CreatePost response for creating repository
|
|
func CreatePost(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.CreateRepoForm)
|
|
ctx.Data["Title"] = ctx.Tr("new_repo.title")
|
|
|
|
ctx.Data["Gitignores"] = repo_module.Gitignores
|
|
ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
|
|
ctx.Data["Licenses"] = repo_module.Licenses
|
|
ctx.Data["Readmes"] = repo_module.Readmes
|
|
|
|
ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
|
|
ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
|
|
ctx.Data["SupportedObjectFormats"] = git.SupportedObjectFormats
|
|
ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat
|
|
|
|
ctxUser := checkContextUser(ctx, form.UID)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Data["ContextUser"] = ctxUser
|
|
|
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
|
|
return
|
|
}
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tplCreate)
|
|
return
|
|
}
|
|
|
|
var repo *repo_model.Repository
|
|
var err error
|
|
if form.RepoTemplate > 0 {
|
|
opts := repo_service.GenerateRepoOptions{
|
|
Name: form.RepoName,
|
|
Description: form.Description,
|
|
Private: form.Private || setting.Repository.ForcePrivate,
|
|
GitContent: form.GitContent,
|
|
Topics: form.Topics,
|
|
GitHooks: form.GitHooks,
|
|
Webhooks: form.Webhooks,
|
|
Avatar: form.Avatar,
|
|
IssueLabels: form.Labels,
|
|
ProtectedBranch: form.ProtectedBranch,
|
|
}
|
|
|
|
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, ctx.Doer, ctxUser, templateRepo, opts)
|
|
if err == nil {
|
|
log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
|
|
ctx.Redirect(repo.Link())
|
|
return
|
|
}
|
|
} else {
|
|
repo, err = repo_service.CreateRepository(ctx, ctx.Doer, ctxUser, repo_service.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,
|
|
IsTemplate: form.Template,
|
|
TrustModel: repo_model.DefaultTrustModel,
|
|
ObjectFormatName: form.ObjectFormatName,
|
|
})
|
|
if err == nil {
|
|
log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
|
|
ctx.Redirect(repo.Link())
|
|
return
|
|
}
|
|
}
|
|
|
|
handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form)
|
|
}
|
|
|
|
const (
|
|
tplWatchUnwatch base.TplName = "repo/watch_unwatch"
|
|
tplStarUnstar base.TplName = "repo/star_unstar"
|
|
)
|
|
|
|
func ActionWatch(watch bool) func(ctx *context.Context) {
|
|
return func(ctx *context.Context) {
|
|
err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, watch)
|
|
if err != nil {
|
|
ctx.ServerError(fmt.Sprintf("Action (watch, %t)", watch), err)
|
|
return
|
|
}
|
|
|
|
ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
|
|
|
// we have to reload the repository because NumStars or NumWatching (used in the templates) has just changed
|
|
ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
|
|
if err != nil {
|
|
ctx.ServerError(fmt.Sprintf("Action (watch, %t)", watch), err)
|
|
return
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, tplWatchUnwatch)
|
|
}
|
|
}
|
|
|
|
func ActionStar(star bool) func(ctx *context.Context) {
|
|
return func(ctx *context.Context) {
|
|
err := repo_service.StarRepoAndSendLikeActivities(ctx, *ctx.Doer, ctx.Repo.Repository.ID, star)
|
|
if err != nil {
|
|
ctx.ServerError(fmt.Sprintf("Action (star, %t)", star), err)
|
|
return
|
|
}
|
|
|
|
ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
|
|
|
// we have to reload the repository because NumStars or NumWatching (used in the templates) has just changed
|
|
ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
|
|
if err != nil {
|
|
ctx.ServerError(fmt.Sprintf("Action (star, %t)", star), err)
|
|
return
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, tplStarUnstar)
|
|
}
|
|
}
|
|
|
|
func ActionTransfer(accept bool) func(ctx *context.Context) {
|
|
return func(ctx *context.Context) {
|
|
var action string
|
|
if accept {
|
|
action = "accept_transfer"
|
|
} else {
|
|
action = "reject_transfer"
|
|
}
|
|
|
|
ok, err := acceptOrRejectRepoTransfer(ctx, accept)
|
|
if err != nil {
|
|
ctx.ServerError(fmt.Sprintf("Action (%s)", action), err)
|
|
return
|
|
}
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
|
|
}
|
|
}
|
|
|
|
func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) (bool, error) {
|
|
repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if err := repoTransfer.LoadAttributes(ctx); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) {
|
|
return false, errors.New("user does not have enough permissions")
|
|
}
|
|
|
|
if accept {
|
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctx.Doer.ID, ctx.Doer.Name) {
|
|
return false, nil
|
|
}
|
|
|
|
if ctx.Repo.GitRepo != nil {
|
|
ctx.Repo.GitRepo.Close()
|
|
ctx.Repo.GitRepo = nil
|
|
}
|
|
|
|
if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
|
|
return false, err
|
|
}
|
|
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
|
|
} else {
|
|
if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
|
|
return false, err
|
|
}
|
|
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
|
|
}
|
|
|
|
ctx.Redirect(ctx.Repo.Repository.Link())
|
|
return true, nil
|
|
}
|
|
|
|
// 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 := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
|
IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases),
|
|
RepoID: curRepo.ID,
|
|
TagNames: tagNames,
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("RedirectDownload", err)
|
|
return
|
|
}
|
|
if len(releases) == 1 {
|
|
release := releases[0]
|
|
att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName)
|
|
if err != nil {
|
|
ctx.Error(http.StatusNotFound)
|
|
return
|
|
}
|
|
if att != nil {
|
|
ServeAttachment(ctx, att.UUID)
|
|
return
|
|
}
|
|
} else if len(releases) == 0 && vTag == "latest" {
|
|
// GitHub supports the alias "latest" for the latest release
|
|
// We only fetch the latest release if the tag is "latest" and no release with the tag "latest" exists
|
|
release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
|
|
if err != nil {
|
|
ctx.Error(http.StatusNotFound)
|
|
return
|
|
}
|
|
att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName)
|
|
if err != nil {
|
|
ctx.Error(http.StatusNotFound)
|
|
return
|
|
}
|
|
if att != nil {
|
|
ServeAttachment(ctx, att.UUID)
|
|
return
|
|
}
|
|
}
|
|
ctx.Error(http.StatusNotFound)
|
|
}
|
|
|
|
// Download an archive of a repository
|
|
func Download(ctx *context.Context) {
|
|
uri := ctx.Params("*")
|
|
ext, tp, err := archiver_service.ParseFileName(uri)
|
|
if err != nil {
|
|
ctx.NotFound("ParseFileName", err)
|
|
return
|
|
}
|
|
aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp)
|
|
if err != nil {
|
|
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
|
|
ctx.Error(http.StatusBadRequest, err.Error())
|
|
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
|
|
ctx.Error(http.StatusNotFound, err.Error())
|
|
} else {
|
|
ctx.ServerError("archiver_service.NewRequest", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
archiver, err := aReq.Await(ctx)
|
|
if err != nil {
|
|
ctx.ServerError("archiver.Await", err)
|
|
return
|
|
}
|
|
|
|
download(ctx, aReq.GetArchiveName(), archiver)
|
|
}
|
|
|
|
func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) {
|
|
downloadName := ctx.Repo.Repository.Name + "-" + archiveName
|
|
|
|
// Add nix format link header so tarballs lock correctly:
|
|
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
|
|
ctx.Resp.Header().Add("Link", fmt.Sprintf("<%s/archive/%s.tar.gz?rev=%s>; rel=\"immutable\"",
|
|
ctx.Repo.Repository.APIURL(),
|
|
archiver.CommitID, archiver.CommitID))
|
|
|
|
rPath := archiver.RelativePath()
|
|
if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
|
|
// If we have a signed url (S3, object storage), redirect to this directly.
|
|
u, err := storage.RepoArchives.URL(rPath, downloadName, nil)
|
|
if u != nil && err == nil {
|
|
if archiver.ReleaseID != 0 {
|
|
err = repo_model.CountArchiveDownload(ctx, ctx.Repo.Repository.ID, archiver.ReleaseID, archiver.Type)
|
|
if err != nil {
|
|
ctx.ServerError("CountArchiveDownload", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx.Redirect(u.String())
|
|
return
|
|
}
|
|
}
|
|
|
|
// If we have matched and access to release or issue
|
|
fr, err := storage.RepoArchives.Open(rPath)
|
|
if err != nil {
|
|
ctx.ServerError("Open", err)
|
|
return
|
|
}
|
|
defer fr.Close()
|
|
|
|
if archiver.ReleaseID != 0 {
|
|
err = repo_model.CountArchiveDownload(ctx, ctx.Repo.Repository.ID, archiver.ReleaseID, archiver.Type)
|
|
if err != nil {
|
|
ctx.ServerError("CountArchiveDownload", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx.ServeContent(fr, &context.ServeHeaderOptions{
|
|
Filename: downloadName,
|
|
LastModified: archiver.CreatedUnix.AsLocalTime(),
|
|
})
|
|
}
|
|
|
|
// 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("*")
|
|
ext, tp, err := archiver_service.ParseFileName(uri)
|
|
if err != nil {
|
|
ctx.NotFound("ParseFileName", err)
|
|
return
|
|
}
|
|
aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp)
|
|
if err != nil {
|
|
ctx.ServerError("archiver_service.NewRequest", err)
|
|
return
|
|
}
|
|
if aReq == nil {
|
|
ctx.Error(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
|
|
if err != nil {
|
|
ctx.ServerError("archiver_service.StartArchive", err)
|
|
return
|
|
}
|
|
if archiver == nil || archiver.Status != repo_model.ArchiverReady {
|
|
if err := archiver_service.StartArchive(aReq); err != nil {
|
|
ctx.ServerError("archiver_service.StartArchive", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
var completed bool
|
|
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
|
|
completed = true
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, map[string]any{
|
|
"complete": completed,
|
|
})
|
|
}
|
|
|
|
// SearchRepo repositories via options
|
|
func SearchRepo(ctx *context.Context) {
|
|
page := ctx.FormInt("page")
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
opts := &repo_model.SearchRepoOptions{
|
|
ListOptions: db.ListOptions{
|
|
Page: page,
|
|
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
|
},
|
|
Actor: ctx.Doer,
|
|
Keyword: ctx.FormTrim("q"),
|
|
OwnerID: ctx.FormInt64("uid"),
|
|
PriorityOwnerID: ctx.FormInt64("priority_owner_id"),
|
|
TeamID: ctx.FormInt64("team_id"),
|
|
TopicOnly: ctx.FormBool("topic"),
|
|
Collaborate: optional.None[bool](),
|
|
Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
|
|
Template: optional.None[bool](),
|
|
StarredByID: ctx.FormInt64("starredBy"),
|
|
IncludeDescription: ctx.FormBool("includeDesc"),
|
|
}
|
|
|
|
if ctx.FormString("template") != "" {
|
|
opts.Template = optional.Some(ctx.FormBool("template"))
|
|
}
|
|
|
|
if ctx.FormBool("exclusive") {
|
|
opts.Collaborate = optional.Some(false)
|
|
}
|
|
|
|
mode := ctx.FormString("mode")
|
|
switch mode {
|
|
case "source":
|
|
opts.Fork = optional.Some(false)
|
|
opts.Mirror = optional.Some(false)
|
|
case "fork":
|
|
opts.Fork = optional.Some(true)
|
|
case "mirror":
|
|
opts.Mirror = optional.Some(true)
|
|
case "collaborative":
|
|
opts.Mirror = optional.Some(false)
|
|
opts.Collaborate = optional.Some(true)
|
|
case "":
|
|
default:
|
|
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode))
|
|
return
|
|
}
|
|
|
|
if ctx.FormString("archived") != "" {
|
|
opts.Archived = optional.Some(ctx.FormBool("archived"))
|
|
}
|
|
|
|
if ctx.FormString("is_private") != "" {
|
|
opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
|
|
}
|
|
|
|
sortMode := ctx.FormString("sort")
|
|
if len(sortMode) > 0 {
|
|
sortOrder := ctx.FormString("order")
|
|
if len(sortOrder) == 0 {
|
|
sortOrder = "asc"
|
|
}
|
|
if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok {
|
|
if orderBy, ok := searchModeMap[sortMode]; ok {
|
|
opts.OrderBy = orderBy
|
|
} else {
|
|
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: \"%s\"", sortMode))
|
|
return
|
|
}
|
|
} else {
|
|
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: \"%s\"", sortOrder))
|
|
return
|
|
}
|
|
}
|
|
|
|
// To improve performance when only the count is requested
|
|
if ctx.FormBool("count_only") {
|
|
if count, err := repo_model.CountRepository(ctx, opts); err != nil {
|
|
log.Error("CountRepository: %v", err)
|
|
ctx.JSON(http.StatusInternalServerError, nil) // frontend JS doesn't handle error response (same as below)
|
|
} else {
|
|
ctx.SetTotalCountHeader(count)
|
|
ctx.JSONOK()
|
|
}
|
|
return
|
|
}
|
|
|
|
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
|
if err != nil {
|
|
log.Error("SearchRepository: %v", err)
|
|
ctx.JSON(http.StatusInternalServerError, nil)
|
|
return
|
|
}
|
|
|
|
ctx.SetTotalCountHeader(count)
|
|
|
|
latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos)
|
|
if err != nil {
|
|
log.Error("FindReposLastestCommitStatuses: %v", err)
|
|
ctx.JSON(http.StatusInternalServerError, nil)
|
|
return
|
|
}
|
|
|
|
results := make([]*repo_service.WebSearchRepository, len(repos))
|
|
for i, repo := range repos {
|
|
results[i] = &repo_service.WebSearchRepository{
|
|
Repository: &api.Repository{
|
|
ID: repo.ID,
|
|
FullName: repo.FullName(),
|
|
Fork: repo.IsFork,
|
|
Private: repo.IsPrivate,
|
|
Template: repo.IsTemplate,
|
|
Mirror: repo.IsMirror,
|
|
Stars: repo.NumStars,
|
|
HTMLURL: repo.HTMLURL(),
|
|
Link: repo.Link(),
|
|
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
|
|
},
|
|
}
|
|
|
|
if latestCommitStatuses[i] != nil {
|
|
results[i].LatestCommitStatus = latestCommitStatuses[i]
|
|
results[i].LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(ctx.Locale)
|
|
}
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, repo_service.WebSearchResults{
|
|
OK: true,
|
|
Data: results,
|
|
})
|
|
}
|
|
|
|
type branchTagSearchResponse struct {
|
|
Results []string `json:"results"`
|
|
}
|
|
|
|
// GetBranchesList get branches for current repo'
|
|
func GetBranchesList(ctx *context.Context) {
|
|
branchOpts := git_model.FindBranchOptions{
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
IsDeletedBranch: optional.Some(false),
|
|
ListOptions: db.ListOptionsAll,
|
|
}
|
|
branches, err := git_model.FindBranchNames(ctx, branchOpts)
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
resp := &branchTagSearchResponse{}
|
|
// always put default branch on the top if it exists
|
|
if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
|
|
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
|
|
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
|
|
}
|
|
resp.Results = branches
|
|
ctx.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
// GetTagList get tag list for current repo
|
|
func GetTagList(ctx *context.Context) {
|
|
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
resp := &branchTagSearchResponse{}
|
|
resp.Results = tags
|
|
ctx.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
func PrepareBranchList(ctx *context.Context) {
|
|
branchOpts := git_model.FindBranchOptions{
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
IsDeletedBranch: optional.Some(false),
|
|
ListOptions: db.ListOptionsAll,
|
|
}
|
|
brs, err := git_model.FindBranchNames(ctx, branchOpts)
|
|
if err != nil {
|
|
ctx.ServerError("GetBranches", err)
|
|
return
|
|
}
|
|
// always put default branch on the top if it exists
|
|
if slices.Contains(brs, ctx.Repo.Repository.DefaultBranch) {
|
|
brs = util.SliceRemoveAll(brs, ctx.Repo.Repository.DefaultBranch)
|
|
brs = append([]string{ctx.Repo.Repository.DefaultBranch}, brs...)
|
|
}
|
|
ctx.Data["Branches"] = brs
|
|
}
|
|
|
|
func SyncFork(ctx *context.Context) {
|
|
redirectURL := fmt.Sprintf("%s/src/branch/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(ctx.Repo.BranchName))
|
|
branch := ctx.FormString("branch")
|
|
|
|
syncForkInfo, err := repo_service.GetSyncForkInfo(ctx, ctx.Repo.Repository, branch)
|
|
if err != nil {
|
|
ctx.ServerError("GetSyncForkInfo", err)
|
|
return
|
|
}
|
|
|
|
if !syncForkInfo.Allowed {
|
|
ctx.Redirect(redirectURL)
|
|
return
|
|
}
|
|
|
|
err = repo_service.SyncFork(ctx, ctx.Doer, ctx.Repo.Repository, branch)
|
|
if err != nil {
|
|
ctx.ServerError("SyncFork", err)
|
|
return
|
|
}
|
|
|
|
ctx.Redirect(redirectURL)
|
|
}
|