code.gitea.io/gitea@v1.22.3/routers/web/repo/repo.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2020 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package repo
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"net/http"
    11  	"slices"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/models"
    15  	"code.gitea.io/gitea/models/db"
    16  	git_model "code.gitea.io/gitea/models/git"
    17  	"code.gitea.io/gitea/models/organization"
    18  	access_model "code.gitea.io/gitea/models/perm/access"
    19  	repo_model "code.gitea.io/gitea/models/repo"
    20  	"code.gitea.io/gitea/models/unit"
    21  	user_model "code.gitea.io/gitea/models/user"
    22  	"code.gitea.io/gitea/modules/base"
    23  	"code.gitea.io/gitea/modules/cache"
    24  	"code.gitea.io/gitea/modules/git"
    25  	"code.gitea.io/gitea/modules/log"
    26  	"code.gitea.io/gitea/modules/optional"
    27  	repo_module "code.gitea.io/gitea/modules/repository"
    28  	"code.gitea.io/gitea/modules/setting"
    29  	"code.gitea.io/gitea/modules/storage"
    30  	api "code.gitea.io/gitea/modules/structs"
    31  	"code.gitea.io/gitea/modules/util"
    32  	"code.gitea.io/gitea/modules/web"
    33  	"code.gitea.io/gitea/services/context"
    34  	"code.gitea.io/gitea/services/convert"
    35  	"code.gitea.io/gitea/services/forms"
    36  	repo_service "code.gitea.io/gitea/services/repository"
    37  	archiver_service "code.gitea.io/gitea/services/repository/archiver"
    38  	commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
    39  )
    40  
    41  const (
    42  	tplCreate       base.TplName = "repo/create"
    43  	tplAlertDetails base.TplName = "base/alert_details"
    44  )
    45  
    46  // MustBeNotEmpty render when a repo is a empty git dir
    47  func MustBeNotEmpty(ctx *context.Context) {
    48  	if ctx.Repo.Repository.IsEmpty {
    49  		ctx.NotFound("MustBeNotEmpty", nil)
    50  	}
    51  }
    52  
    53  // MustBeEditable check that repo can be edited
    54  func MustBeEditable(ctx *context.Context) {
    55  	if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
    56  		ctx.NotFound("", nil)
    57  		return
    58  	}
    59  }
    60  
    61  // MustBeAbleToUpload check that repo can be uploaded to
    62  func MustBeAbleToUpload(ctx *context.Context) {
    63  	if !setting.Repository.Upload.Enabled {
    64  		ctx.NotFound("", nil)
    65  	}
    66  }
    67  
    68  func CommitInfoCache(ctx *context.Context) {
    69  	var err error
    70  	ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
    71  	if err != nil {
    72  		ctx.ServerError("GetBranchCommit", err)
    73  		return
    74  	}
    75  	ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
    76  	if err != nil {
    77  		ctx.ServerError("GetCommitsCount", err)
    78  		return
    79  	}
    80  	ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
    81  	ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
    82  }
    83  
    84  func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
    85  	orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
    86  	if err != nil {
    87  		ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
    88  		return nil
    89  	}
    90  
    91  	if !ctx.Doer.IsAdmin {
    92  		orgsAvailable := []*organization.Organization{}
    93  		for i := 0; i < len(orgs); i++ {
    94  			if orgs[i].CanCreateRepo() {
    95  				orgsAvailable = append(orgsAvailable, orgs[i])
    96  			}
    97  		}
    98  		ctx.Data["Orgs"] = orgsAvailable
    99  	} else {
   100  		ctx.Data["Orgs"] = orgs
   101  	}
   102  
   103  	// Not equal means current user is an organization.
   104  	if uid == ctx.Doer.ID || uid == 0 {
   105  		return ctx.Doer
   106  	}
   107  
   108  	org, err := user_model.GetUserByID(ctx, uid)
   109  	if user_model.IsErrUserNotExist(err) {
   110  		return ctx.Doer
   111  	}
   112  
   113  	if err != nil {
   114  		ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %w", uid, err))
   115  		return nil
   116  	}
   117  
   118  	// Check ownership of organization.
   119  	if !org.IsOrganization() {
   120  		ctx.Error(http.StatusForbidden)
   121  		return nil
   122  	}
   123  	if !ctx.Doer.IsAdmin {
   124  		canCreate, err := organization.OrgFromUser(org).CanCreateOrgRepo(ctx, ctx.Doer.ID)
   125  		if err != nil {
   126  			ctx.ServerError("CanCreateOrgRepo", err)
   127  			return nil
   128  		} else if !canCreate {
   129  			ctx.Error(http.StatusForbidden)
   130  			return nil
   131  		}
   132  	} else {
   133  		ctx.Data["Orgs"] = orgs
   134  	}
   135  	return org
   136  }
   137  
   138  func getRepoPrivate(ctx *context.Context) bool {
   139  	switch strings.ToLower(setting.Repository.DefaultPrivate) {
   140  	case setting.RepoCreatingLastUserVisibility:
   141  		return ctx.Doer.LastRepoVisibility
   142  	case setting.RepoCreatingPrivate:
   143  		return true
   144  	case setting.RepoCreatingPublic:
   145  		return false
   146  	default:
   147  		return ctx.Doer.LastRepoVisibility
   148  	}
   149  }
   150  
   151  // Create render creating repository page
   152  func Create(ctx *context.Context) {
   153  	ctx.Data["Title"] = ctx.Tr("new_repo")
   154  
   155  	// Give default value for template to render.
   156  	ctx.Data["Gitignores"] = repo_module.Gitignores
   157  	ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
   158  	ctx.Data["Licenses"] = repo_module.Licenses
   159  	ctx.Data["Readmes"] = repo_module.Readmes
   160  	ctx.Data["readme"] = "Default"
   161  	ctx.Data["private"] = getRepoPrivate(ctx)
   162  	ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
   163  	ctx.Data["default_branch"] = setting.Repository.DefaultBranch
   164  
   165  	ctxUser := checkContextUser(ctx, ctx.FormInt64("org"))
   166  	if ctx.Written() {
   167  		return
   168  	}
   169  	ctx.Data["ContextUser"] = ctxUser
   170  
   171  	ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select")
   172  	templateID := ctx.FormInt64("template_id")
   173  	if templateID > 0 {
   174  		templateRepo, err := repo_model.GetRepositoryByID(ctx, templateID)
   175  		if err == nil && access_model.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) {
   176  			ctx.Data["repo_template"] = templateID
   177  			ctx.Data["repo_template_name"] = templateRepo.Name
   178  		}
   179  	}
   180  
   181  	ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
   182  	ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
   183  	ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats
   184  	ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat
   185  
   186  	ctx.HTML(http.StatusOK, tplCreate)
   187  }
   188  
   189  func handleCreateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl base.TplName, form any) {
   190  	switch {
   191  	case repo_model.IsErrReachLimitOfRepo(err):
   192  		maxCreationLimit := owner.MaxCreationLimit()
   193  		msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
   194  		ctx.RenderWithErr(msg, tpl, form)
   195  	case repo_model.IsErrRepoAlreadyExist(err):
   196  		ctx.Data["Err_RepoName"] = true
   197  		ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
   198  	case repo_model.IsErrRepoFilesAlreadyExist(err):
   199  		ctx.Data["Err_RepoName"] = true
   200  		switch {
   201  		case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
   202  			ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
   203  		case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
   204  			ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
   205  		case setting.Repository.AllowDeleteOfUnadoptedRepositories:
   206  			ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
   207  		default:
   208  			ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
   209  		}
   210  	case db.IsErrNameReserved(err):
   211  		ctx.Data["Err_RepoName"] = true
   212  		ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
   213  	case db.IsErrNamePatternNotAllowed(err):
   214  		ctx.Data["Err_RepoName"] = true
   215  		ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
   216  	default:
   217  		ctx.ServerError(name, err)
   218  	}
   219  }
   220  
   221  // CreatePost response for creating repository
   222  func CreatePost(ctx *context.Context) {
   223  	form := web.GetForm(ctx).(*forms.CreateRepoForm)
   224  	ctx.Data["Title"] = ctx.Tr("new_repo")
   225  
   226  	ctx.Data["Gitignores"] = repo_module.Gitignores
   227  	ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
   228  	ctx.Data["Licenses"] = repo_module.Licenses
   229  	ctx.Data["Readmes"] = repo_module.Readmes
   230  
   231  	ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
   232  	ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
   233  
   234  	ctxUser := checkContextUser(ctx, form.UID)
   235  	if ctx.Written() {
   236  		return
   237  	}
   238  	ctx.Data["ContextUser"] = ctxUser
   239  
   240  	if ctx.HasError() {
   241  		ctx.HTML(http.StatusOK, tplCreate)
   242  		return
   243  	}
   244  
   245  	var repo *repo_model.Repository
   246  	var err error
   247  	if form.RepoTemplate > 0 {
   248  		opts := repo_service.GenerateRepoOptions{
   249  			Name:            form.RepoName,
   250  			Description:     form.Description,
   251  			Private:         form.Private || setting.Repository.ForcePrivate,
   252  			GitContent:      form.GitContent,
   253  			Topics:          form.Topics,
   254  			GitHooks:        form.GitHooks,
   255  			Webhooks:        form.Webhooks,
   256  			Avatar:          form.Avatar,
   257  			IssueLabels:     form.Labels,
   258  			ProtectedBranch: form.ProtectedBranch,
   259  		}
   260  
   261  		if !opts.IsValid() {
   262  			ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form)
   263  			return
   264  		}
   265  
   266  		templateRepo := getRepository(ctx, form.RepoTemplate)
   267  		if ctx.Written() {
   268  			return
   269  		}
   270  
   271  		if !templateRepo.IsTemplate {
   272  			ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form)
   273  			return
   274  		}
   275  
   276  		repo, err = repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, templateRepo, opts)
   277  		if err == nil {
   278  			log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
   279  			ctx.Redirect(repo.Link())
   280  			return
   281  		}
   282  	} else {
   283  		repo, err = repo_service.CreateRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{
   284  			Name:             form.RepoName,
   285  			Description:      form.Description,
   286  			Gitignores:       form.Gitignores,
   287  			IssueLabels:      form.IssueLabels,
   288  			License:          form.License,
   289  			Readme:           form.Readme,
   290  			IsPrivate:        form.Private || setting.Repository.ForcePrivate,
   291  			DefaultBranch:    form.DefaultBranch,
   292  			AutoInit:         form.AutoInit,
   293  			IsTemplate:       form.Template,
   294  			TrustModel:       repo_model.DefaultTrustModel,
   295  			ObjectFormatName: form.ObjectFormatName,
   296  		})
   297  		if err == nil {
   298  			log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
   299  			ctx.Redirect(repo.Link())
   300  			return
   301  		}
   302  	}
   303  
   304  	handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form)
   305  }
   306  
   307  const (
   308  	tplWatchUnwatch base.TplName = "repo/watch_unwatch"
   309  	tplStarUnstar   base.TplName = "repo/star_unstar"
   310  )
   311  
   312  // Action response for actions to a repository
   313  func Action(ctx *context.Context) {
   314  	var err error
   315  	switch ctx.Params(":action") {
   316  	case "watch":
   317  		err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
   318  	case "unwatch":
   319  		err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
   320  	case "star":
   321  		err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
   322  	case "unstar":
   323  		err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
   324  	case "accept_transfer":
   325  		err = acceptOrRejectRepoTransfer(ctx, true)
   326  	case "reject_transfer":
   327  		err = acceptOrRejectRepoTransfer(ctx, false)
   328  	case "desc": // FIXME: this is not used
   329  		if !ctx.Repo.IsOwner() {
   330  			ctx.Error(http.StatusNotFound)
   331  			return
   332  		}
   333  
   334  		ctx.Repo.Repository.Description = ctx.FormString("desc")
   335  		ctx.Repo.Repository.Website = ctx.FormString("site")
   336  		err = repo_service.UpdateRepository(ctx, ctx.Repo.Repository, false)
   337  	}
   338  
   339  	if err != nil {
   340  		if errors.Is(err, user_model.ErrBlockedUser) {
   341  			ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
   342  		} else {
   343  			ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
   344  			return
   345  		}
   346  	}
   347  
   348  	switch ctx.Params(":action") {
   349  	case "watch", "unwatch":
   350  		ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
   351  	case "star", "unstar":
   352  		ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
   353  	}
   354  
   355  	switch ctx.Params(":action") {
   356  	case "watch", "unwatch", "star", "unstar":
   357  		// we have to reload the repository because NumStars or NumWatching (used in the templates) has just changed
   358  		ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
   359  		if err != nil {
   360  			ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
   361  			return
   362  		}
   363  	}
   364  
   365  	switch ctx.Params(":action") {
   366  	case "watch", "unwatch":
   367  		ctx.HTML(http.StatusOK, tplWatchUnwatch)
   368  		return
   369  	case "star", "unstar":
   370  		ctx.HTML(http.StatusOK, tplStarUnstar)
   371  		return
   372  	}
   373  
   374  	ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
   375  }
   376  
   377  func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
   378  	repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	if err := repoTransfer.LoadAttributes(ctx); err != nil {
   384  		return err
   385  	}
   386  
   387  	if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) {
   388  		return errors.New("user does not have enough permissions")
   389  	}
   390  
   391  	if accept {
   392  		if ctx.Repo.GitRepo != nil {
   393  			ctx.Repo.GitRepo.Close()
   394  			ctx.Repo.GitRepo = nil
   395  		}
   396  
   397  		if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
   398  			return err
   399  		}
   400  		ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
   401  	} else {
   402  		if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
   403  			return err
   404  		}
   405  		ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
   406  	}
   407  
   408  	ctx.Redirect(ctx.Repo.Repository.Link())
   409  	return nil
   410  }
   411  
   412  // RedirectDownload return a file based on the following infos:
   413  func RedirectDownload(ctx *context.Context) {
   414  	var (
   415  		vTag     = ctx.Params("vTag")
   416  		fileName = ctx.Params("fileName")
   417  	)
   418  	tagNames := []string{vTag}
   419  	curRepo := ctx.Repo.Repository
   420  	releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
   421  		IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases),
   422  		RepoID:        curRepo.ID,
   423  		TagNames:      tagNames,
   424  	})
   425  	if err != nil {
   426  		ctx.ServerError("RedirectDownload", err)
   427  		return
   428  	}
   429  	if len(releases) == 1 {
   430  		release := releases[0]
   431  		att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName)
   432  		if err != nil {
   433  			ctx.Error(http.StatusNotFound)
   434  			return
   435  		}
   436  		if att != nil {
   437  			ServeAttachment(ctx, att.UUID)
   438  			return
   439  		}
   440  	} else if len(releases) == 0 && vTag == "latest" {
   441  		// GitHub supports the alias "latest" for the latest release
   442  		// We only fetch the latest release if the tag is "latest" and no release with the tag "latest" exists
   443  		release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
   444  		if err != nil {
   445  			ctx.Error(http.StatusNotFound)
   446  			return
   447  		}
   448  		att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName)
   449  		if err != nil {
   450  			ctx.Error(http.StatusNotFound)
   451  			return
   452  		}
   453  		if att != nil {
   454  			ServeAttachment(ctx, att.UUID)
   455  			return
   456  		}
   457  	}
   458  	ctx.Error(http.StatusNotFound)
   459  }
   460  
   461  // Download an archive of a repository
   462  func Download(ctx *context.Context) {
   463  	uri := ctx.Params("*")
   464  	aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
   465  	if err != nil {
   466  		if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
   467  			ctx.Error(http.StatusBadRequest, err.Error())
   468  		} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
   469  			ctx.Error(http.StatusNotFound, err.Error())
   470  		} else {
   471  			ctx.ServerError("archiver_service.NewRequest", err)
   472  		}
   473  		return
   474  	}
   475  
   476  	archiver, err := aReq.Await(ctx)
   477  	if err != nil {
   478  		ctx.ServerError("archiver.Await", err)
   479  		return
   480  	}
   481  
   482  	download(ctx, aReq.GetArchiveName(), archiver)
   483  }
   484  
   485  func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) {
   486  	downloadName := ctx.Repo.Repository.Name + "-" + archiveName
   487  
   488  	// Add nix format link header so tarballs lock correctly:
   489  	// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
   490  	ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.tar.gz?rev=%s>; rel="immutable"`,
   491  		ctx.Repo.Repository.APIURL(),
   492  		archiver.CommitID, archiver.CommitID))
   493  
   494  	rPath := archiver.RelativePath()
   495  	if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
   496  		// If we have a signed url (S3, object storage), redirect to this directly.
   497  		u, err := storage.RepoArchives.URL(rPath, downloadName)
   498  		if u != nil && err == nil {
   499  			ctx.Redirect(u.String())
   500  			return
   501  		}
   502  	}
   503  
   504  	// If we have matched and access to release or issue
   505  	fr, err := storage.RepoArchives.Open(rPath)
   506  	if err != nil {
   507  		ctx.ServerError("Open", err)
   508  		return
   509  	}
   510  	defer fr.Close()
   511  
   512  	ctx.ServeContent(fr, &context.ServeHeaderOptions{
   513  		Filename:     downloadName,
   514  		LastModified: archiver.CreatedUnix.AsLocalTime(),
   515  	})
   516  }
   517  
   518  // InitiateDownload will enqueue an archival request, as needed.  It may submit
   519  // a request that's already in-progress, but the archiver service will just
   520  // kind of drop it on the floor if this is the case.
   521  func InitiateDownload(ctx *context.Context) {
   522  	uri := ctx.Params("*")
   523  	aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
   524  	if err != nil {
   525  		ctx.ServerError("archiver_service.NewRequest", err)
   526  		return
   527  	}
   528  	if aReq == nil {
   529  		ctx.Error(http.StatusNotFound)
   530  		return
   531  	}
   532  
   533  	archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
   534  	if err != nil {
   535  		ctx.ServerError("archiver_service.StartArchive", err)
   536  		return
   537  	}
   538  	if archiver == nil || archiver.Status != repo_model.ArchiverReady {
   539  		if err := archiver_service.StartArchive(aReq); err != nil {
   540  			ctx.ServerError("archiver_service.StartArchive", err)
   541  			return
   542  		}
   543  	}
   544  
   545  	var completed bool
   546  	if archiver != nil && archiver.Status == repo_model.ArchiverReady {
   547  		completed = true
   548  	}
   549  
   550  	ctx.JSON(http.StatusOK, map[string]any{
   551  		"complete": completed,
   552  	})
   553  }
   554  
   555  // SearchRepo repositories via options
   556  func SearchRepo(ctx *context.Context) {
   557  	page := ctx.FormInt("page")
   558  	if page <= 0 {
   559  		page = 1
   560  	}
   561  	opts := &repo_model.SearchRepoOptions{
   562  		ListOptions: db.ListOptions{
   563  			Page:     page,
   564  			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
   565  		},
   566  		Actor:              ctx.Doer,
   567  		Keyword:            ctx.FormTrim("q"),
   568  		OwnerID:            ctx.FormInt64("uid"),
   569  		PriorityOwnerID:    ctx.FormInt64("priority_owner_id"),
   570  		TeamID:             ctx.FormInt64("team_id"),
   571  		TopicOnly:          ctx.FormBool("topic"),
   572  		Collaborate:        optional.None[bool](),
   573  		Private:            ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
   574  		Template:           optional.None[bool](),
   575  		StarredByID:        ctx.FormInt64("starredBy"),
   576  		IncludeDescription: ctx.FormBool("includeDesc"),
   577  	}
   578  
   579  	if ctx.FormString("template") != "" {
   580  		opts.Template = optional.Some(ctx.FormBool("template"))
   581  	}
   582  
   583  	if ctx.FormBool("exclusive") {
   584  		opts.Collaborate = optional.Some(false)
   585  	}
   586  
   587  	mode := ctx.FormString("mode")
   588  	switch mode {
   589  	case "source":
   590  		opts.Fork = optional.Some(false)
   591  		opts.Mirror = optional.Some(false)
   592  	case "fork":
   593  		opts.Fork = optional.Some(true)
   594  	case "mirror":
   595  		opts.Mirror = optional.Some(true)
   596  	case "collaborative":
   597  		opts.Mirror = optional.Some(false)
   598  		opts.Collaborate = optional.Some(true)
   599  	case "":
   600  	default:
   601  		ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode))
   602  		return
   603  	}
   604  
   605  	if ctx.FormString("archived") != "" {
   606  		opts.Archived = optional.Some(ctx.FormBool("archived"))
   607  	}
   608  
   609  	if ctx.FormString("is_private") != "" {
   610  		opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
   611  	}
   612  
   613  	sortMode := ctx.FormString("sort")
   614  	if len(sortMode) > 0 {
   615  		sortOrder := ctx.FormString("order")
   616  		if len(sortOrder) == 0 {
   617  			sortOrder = "asc"
   618  		}
   619  		if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok {
   620  			if orderBy, ok := searchModeMap[sortMode]; ok {
   621  				opts.OrderBy = orderBy
   622  			} else {
   623  				ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: \"%s\"", sortMode))
   624  				return
   625  			}
   626  		} else {
   627  			ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: \"%s\"", sortOrder))
   628  			return
   629  		}
   630  	}
   631  
   632  	// To improve performance when only the count is requested
   633  	if ctx.FormBool("count_only") {
   634  		if count, err := repo_model.CountRepository(ctx, opts); err != nil {
   635  			log.Error("CountRepository: %v", err)
   636  			ctx.JSON(http.StatusInternalServerError, nil) // frontend JS doesn't handle error response (same as below)
   637  		} else {
   638  			ctx.SetTotalCountHeader(count)
   639  			ctx.JSONOK()
   640  		}
   641  		return
   642  	}
   643  
   644  	repos, count, err := repo_model.SearchRepository(ctx, opts)
   645  	if err != nil {
   646  		log.Error("SearchRepository: %v", err)
   647  		ctx.JSON(http.StatusInternalServerError, nil)
   648  		return
   649  	}
   650  
   651  	ctx.SetTotalCountHeader(count)
   652  
   653  	latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos)
   654  	if err != nil {
   655  		log.Error("FindReposLastestCommitStatuses: %v", err)
   656  		ctx.JSON(http.StatusInternalServerError, nil)
   657  		return
   658  	}
   659  
   660  	results := make([]*repo_service.WebSearchRepository, len(repos))
   661  	for i, repo := range repos {
   662  		results[i] = &repo_service.WebSearchRepository{
   663  			Repository: &api.Repository{
   664  				ID:       repo.ID,
   665  				FullName: repo.FullName(),
   666  				Fork:     repo.IsFork,
   667  				Private:  repo.IsPrivate,
   668  				Template: repo.IsTemplate,
   669  				Mirror:   repo.IsMirror,
   670  				Stars:    repo.NumStars,
   671  				HTMLURL:  repo.HTMLURL(),
   672  				Link:     repo.Link(),
   673  				Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
   674  			},
   675  		}
   676  
   677  		if latestCommitStatuses[i] != nil {
   678  			results[i].LatestCommitStatus = latestCommitStatuses[i]
   679  			results[i].LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(ctx.Locale)
   680  		}
   681  	}
   682  
   683  	ctx.JSON(http.StatusOK, repo_service.WebSearchResults{
   684  		OK:   true,
   685  		Data: results,
   686  	})
   687  }
   688  
   689  type branchTagSearchResponse struct {
   690  	Results []string `json:"results"`
   691  }
   692  
   693  // GetBranchesList get branches for current repo'
   694  func GetBranchesList(ctx *context.Context) {
   695  	branchOpts := git_model.FindBranchOptions{
   696  		RepoID:          ctx.Repo.Repository.ID,
   697  		IsDeletedBranch: optional.Some(false),
   698  		ListOptions:     db.ListOptionsAll,
   699  	}
   700  	branches, err := git_model.FindBranchNames(ctx, branchOpts)
   701  	if err != nil {
   702  		ctx.JSON(http.StatusInternalServerError, err)
   703  		return
   704  	}
   705  	resp := &branchTagSearchResponse{}
   706  	// always put default branch on the top if it exists
   707  	if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
   708  		branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
   709  		branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
   710  	}
   711  	resp.Results = branches
   712  	ctx.JSON(http.StatusOK, resp)
   713  }
   714  
   715  // GetTagList get tag list for current repo
   716  func GetTagList(ctx *context.Context) {
   717  	tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
   718  	if err != nil {
   719  		ctx.JSON(http.StatusInternalServerError, err)
   720  		return
   721  	}
   722  	resp := &branchTagSearchResponse{}
   723  	resp.Results = tags
   724  	ctx.JSON(http.StatusOK, resp)
   725  }
   726  
   727  func PrepareBranchList(ctx *context.Context) {
   728  	branchOpts := git_model.FindBranchOptions{
   729  		RepoID:          ctx.Repo.Repository.ID,
   730  		IsDeletedBranch: optional.Some(false),
   731  		ListOptions:     db.ListOptionsAll,
   732  	}
   733  	brs, err := git_model.FindBranchNames(ctx, branchOpts)
   734  	if err != nil {
   735  		ctx.ServerError("GetBranches", err)
   736  		return
   737  	}
   738  	// always put default branch on the top if it exists
   739  	if slices.Contains(brs, ctx.Repo.Repository.DefaultBranch) {
   740  		brs = util.SliceRemoveAll(brs, ctx.Repo.Repository.DefaultBranch)
   741  		brs = append([]string{ctx.Repo.Repository.DefaultBranch}, brs...)
   742  	}
   743  	ctx.Data["Branches"] = brs
   744  }