You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

repo.go 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2020 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package repo
  5. import (
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "strings"
  10. "code.gitea.io/gitea/models"
  11. "code.gitea.io/gitea/models/db"
  12. git_model "code.gitea.io/gitea/models/git"
  13. "code.gitea.io/gitea/models/organization"
  14. access_model "code.gitea.io/gitea/models/perm/access"
  15. repo_model "code.gitea.io/gitea/models/repo"
  16. "code.gitea.io/gitea/models/unit"
  17. user_model "code.gitea.io/gitea/models/user"
  18. "code.gitea.io/gitea/modules/base"
  19. "code.gitea.io/gitea/modules/cache"
  20. "code.gitea.io/gitea/modules/context"
  21. "code.gitea.io/gitea/modules/git"
  22. "code.gitea.io/gitea/modules/log"
  23. repo_module "code.gitea.io/gitea/modules/repository"
  24. "code.gitea.io/gitea/modules/setting"
  25. "code.gitea.io/gitea/modules/storage"
  26. api "code.gitea.io/gitea/modules/structs"
  27. "code.gitea.io/gitea/modules/util"
  28. "code.gitea.io/gitea/modules/web"
  29. "code.gitea.io/gitea/services/convert"
  30. "code.gitea.io/gitea/services/forms"
  31. repo_service "code.gitea.io/gitea/services/repository"
  32. archiver_service "code.gitea.io/gitea/services/repository/archiver"
  33. )
  34. const (
  35. tplCreate base.TplName = "repo/create"
  36. tplAlertDetails base.TplName = "base/alert_details"
  37. )
  38. // MustBeNotEmpty render when a repo is a empty git dir
  39. func MustBeNotEmpty(ctx *context.Context) {
  40. if ctx.Repo.Repository.IsEmpty {
  41. ctx.NotFound("MustBeNotEmpty", nil)
  42. }
  43. }
  44. // MustBeEditable check that repo can be edited
  45. func MustBeEditable(ctx *context.Context) {
  46. if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
  47. ctx.NotFound("", nil)
  48. return
  49. }
  50. }
  51. // MustBeAbleToUpload check that repo can be uploaded to
  52. func MustBeAbleToUpload(ctx *context.Context) {
  53. if !setting.Repository.Upload.Enabled {
  54. ctx.NotFound("", nil)
  55. }
  56. }
  57. func CommitInfoCache(ctx *context.Context) {
  58. var err error
  59. ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
  60. if err != nil {
  61. ctx.ServerError("GetBranchCommit", err)
  62. return
  63. }
  64. ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
  65. if err != nil {
  66. ctx.ServerError("GetCommitsCount", err)
  67. return
  68. }
  69. ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
  70. ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
  71. }
  72. func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
  73. orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx.Doer.ID)
  74. if err != nil {
  75. ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
  76. return nil
  77. }
  78. if !ctx.Doer.IsAdmin {
  79. orgsAvailable := []*organization.Organization{}
  80. for i := 0; i < len(orgs); i++ {
  81. if orgs[i].CanCreateRepo() {
  82. orgsAvailable = append(orgsAvailable, orgs[i])
  83. }
  84. }
  85. ctx.Data["Orgs"] = orgsAvailable
  86. } else {
  87. ctx.Data["Orgs"] = orgs
  88. }
  89. // Not equal means current user is an organization.
  90. if uid == ctx.Doer.ID || uid == 0 {
  91. return ctx.Doer
  92. }
  93. org, err := user_model.GetUserByID(ctx, uid)
  94. if user_model.IsErrUserNotExist(err) {
  95. return ctx.Doer
  96. }
  97. if err != nil {
  98. ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %w", uid, err))
  99. return nil
  100. }
  101. // Check ownership of organization.
  102. if !org.IsOrganization() {
  103. ctx.Error(http.StatusForbidden)
  104. return nil
  105. }
  106. if !ctx.Doer.IsAdmin {
  107. canCreate, err := organization.OrgFromUser(org).CanCreateOrgRepo(ctx.Doer.ID)
  108. if err != nil {
  109. ctx.ServerError("CanCreateOrgRepo", err)
  110. return nil
  111. } else if !canCreate {
  112. ctx.Error(http.StatusForbidden)
  113. return nil
  114. }
  115. } else {
  116. ctx.Data["Orgs"] = orgs
  117. }
  118. return org
  119. }
  120. func getRepoPrivate(ctx *context.Context) bool {
  121. switch strings.ToLower(setting.Repository.DefaultPrivate) {
  122. case setting.RepoCreatingLastUserVisibility:
  123. return ctx.Doer.LastRepoVisibility
  124. case setting.RepoCreatingPrivate:
  125. return true
  126. case setting.RepoCreatingPublic:
  127. return false
  128. default:
  129. return ctx.Doer.LastRepoVisibility
  130. }
  131. }
  132. // Create render creating repository page
  133. func Create(ctx *context.Context) {
  134. ctx.Data["Title"] = ctx.Tr("new_repo")
  135. // Give default value for template to render.
  136. ctx.Data["Gitignores"] = repo_module.Gitignores
  137. ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
  138. ctx.Data["Licenses"] = repo_module.Licenses
  139. ctx.Data["Readmes"] = repo_module.Readmes
  140. ctx.Data["readme"] = "Default"
  141. ctx.Data["private"] = getRepoPrivate(ctx)
  142. ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
  143. ctx.Data["default_branch"] = setting.Repository.DefaultBranch
  144. ctxUser := checkContextUser(ctx, ctx.FormInt64("org"))
  145. if ctx.Written() {
  146. return
  147. }
  148. ctx.Data["ContextUser"] = ctxUser
  149. ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select")
  150. templateID := ctx.FormInt64("template_id")
  151. if templateID > 0 {
  152. templateRepo, err := repo_model.GetRepositoryByID(ctx, templateID)
  153. if err == nil && access_model.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) {
  154. ctx.Data["repo_template"] = templateID
  155. ctx.Data["repo_template_name"] = templateRepo.Name
  156. }
  157. }
  158. ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
  159. ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
  160. ctx.HTML(http.StatusOK, tplCreate)
  161. }
  162. func handleCreateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl base.TplName, form any) {
  163. switch {
  164. case repo_model.IsErrReachLimitOfRepo(err):
  165. maxCreationLimit := owner.MaxCreationLimit()
  166. msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
  167. ctx.RenderWithErr(msg, tpl, form)
  168. case repo_model.IsErrRepoAlreadyExist(err):
  169. ctx.Data["Err_RepoName"] = true
  170. ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
  171. case repo_model.IsErrRepoFilesAlreadyExist(err):
  172. ctx.Data["Err_RepoName"] = true
  173. switch {
  174. case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
  175. ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
  176. case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
  177. ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
  178. case setting.Repository.AllowDeleteOfUnadoptedRepositories:
  179. ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
  180. default:
  181. ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
  182. }
  183. case db.IsErrNameReserved(err):
  184. ctx.Data["Err_RepoName"] = true
  185. ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
  186. case db.IsErrNamePatternNotAllowed(err):
  187. ctx.Data["Err_RepoName"] = true
  188. ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
  189. default:
  190. ctx.ServerError(name, err)
  191. }
  192. }
  193. // CreatePost response for creating repository
  194. func CreatePost(ctx *context.Context) {
  195. form := web.GetForm(ctx).(*forms.CreateRepoForm)
  196. ctx.Data["Title"] = ctx.Tr("new_repo")
  197. ctx.Data["Gitignores"] = repo_module.Gitignores
  198. ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
  199. ctx.Data["Licenses"] = repo_module.Licenses
  200. ctx.Data["Readmes"] = repo_module.Readmes
  201. ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
  202. ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
  203. ctxUser := checkContextUser(ctx, form.UID)
  204. if ctx.Written() {
  205. return
  206. }
  207. ctx.Data["ContextUser"] = ctxUser
  208. if ctx.HasError() {
  209. ctx.HTML(http.StatusOK, tplCreate)
  210. return
  211. }
  212. var repo *repo_model.Repository
  213. var err error
  214. if form.RepoTemplate > 0 {
  215. opts := repo_module.GenerateRepoOptions{
  216. Name: form.RepoName,
  217. Description: form.Description,
  218. Private: form.Private,
  219. GitContent: form.GitContent,
  220. Topics: form.Topics,
  221. GitHooks: form.GitHooks,
  222. Webhooks: form.Webhooks,
  223. Avatar: form.Avatar,
  224. IssueLabels: form.Labels,
  225. }
  226. if !opts.IsValid() {
  227. ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form)
  228. return
  229. }
  230. templateRepo := getRepository(ctx, form.RepoTemplate)
  231. if ctx.Written() {
  232. return
  233. }
  234. if !templateRepo.IsTemplate {
  235. ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form)
  236. return
  237. }
  238. repo, err = repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, templateRepo, opts)
  239. if err == nil {
  240. log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
  241. ctx.Redirect(repo.Link())
  242. return
  243. }
  244. } else {
  245. repo, err = repo_service.CreateRepository(ctx, ctx.Doer, ctxUser, repo_module.CreateRepoOptions{
  246. Name: form.RepoName,
  247. Description: form.Description,
  248. Gitignores: form.Gitignores,
  249. IssueLabels: form.IssueLabels,
  250. License: form.License,
  251. Readme: form.Readme,
  252. IsPrivate: form.Private || setting.Repository.ForcePrivate,
  253. DefaultBranch: form.DefaultBranch,
  254. AutoInit: form.AutoInit,
  255. IsTemplate: form.Template,
  256. TrustModel: repo_model.ToTrustModel(form.TrustModel),
  257. })
  258. if err == nil {
  259. log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
  260. ctx.Redirect(repo.Link())
  261. return
  262. }
  263. }
  264. handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form)
  265. }
  266. // Action response for actions to a repository
  267. func Action(ctx *context.Context) {
  268. var err error
  269. switch ctx.Params(":action") {
  270. case "watch":
  271. err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
  272. case "unwatch":
  273. err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
  274. case "star":
  275. err = repo_model.StarRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, true)
  276. case "unstar":
  277. err = repo_model.StarRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, false)
  278. case "accept_transfer":
  279. err = acceptOrRejectRepoTransfer(ctx, true)
  280. case "reject_transfer":
  281. err = acceptOrRejectRepoTransfer(ctx, false)
  282. case "desc": // FIXME: this is not used
  283. if !ctx.Repo.IsOwner() {
  284. ctx.Error(http.StatusNotFound)
  285. return
  286. }
  287. ctx.Repo.Repository.Description = ctx.FormString("desc")
  288. ctx.Repo.Repository.Website = ctx.FormString("site")
  289. err = repo_service.UpdateRepository(ctx, ctx.Repo.Repository, false)
  290. }
  291. if err != nil {
  292. ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
  293. return
  294. }
  295. ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
  296. }
  297. func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
  298. repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
  299. if err != nil {
  300. return err
  301. }
  302. if err := repoTransfer.LoadAttributes(ctx); err != nil {
  303. return err
  304. }
  305. if !repoTransfer.CanUserAcceptTransfer(ctx.Doer) {
  306. return errors.New("user does not have enough permissions")
  307. }
  308. if accept {
  309. if ctx.Repo.GitRepo != nil {
  310. ctx.Repo.GitRepo.Close()
  311. ctx.Repo.GitRepo = nil
  312. }
  313. if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
  314. return err
  315. }
  316. ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
  317. } else {
  318. if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
  319. return err
  320. }
  321. ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
  322. }
  323. ctx.Redirect(ctx.Repo.Repository.Link())
  324. return nil
  325. }
  326. // RedirectDownload return a file based on the following infos:
  327. func RedirectDownload(ctx *context.Context) {
  328. var (
  329. vTag = ctx.Params("vTag")
  330. fileName = ctx.Params("fileName")
  331. )
  332. tagNames := []string{vTag}
  333. curRepo := ctx.Repo.Repository
  334. releases, err := repo_model.GetReleasesByRepoIDAndNames(ctx, curRepo.ID, tagNames)
  335. if err != nil {
  336. if repo_model.IsErrAttachmentNotExist(err) {
  337. ctx.Error(http.StatusNotFound)
  338. return
  339. }
  340. ctx.ServerError("RedirectDownload", err)
  341. return
  342. }
  343. if len(releases) == 1 {
  344. release := releases[0]
  345. att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName)
  346. if err != nil {
  347. ctx.Error(http.StatusNotFound)
  348. return
  349. }
  350. if att != nil {
  351. ServeAttachment(ctx, att.UUID)
  352. return
  353. }
  354. }
  355. ctx.Error(http.StatusNotFound)
  356. }
  357. // Download an archive of a repository
  358. func Download(ctx *context.Context) {
  359. uri := ctx.Params("*")
  360. aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
  361. if err != nil {
  362. if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
  363. ctx.Error(http.StatusBadRequest, err.Error())
  364. } else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
  365. ctx.Error(http.StatusNotFound, err.Error())
  366. } else {
  367. ctx.ServerError("archiver_service.NewRequest", err)
  368. }
  369. return
  370. }
  371. archiver, err := aReq.Await(ctx)
  372. if err != nil {
  373. ctx.ServerError("archiver.Await", err)
  374. return
  375. }
  376. download(ctx, aReq.GetArchiveName(), archiver)
  377. }
  378. func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) {
  379. downloadName := ctx.Repo.Repository.Name + "-" + archiveName
  380. rPath := archiver.RelativePath()
  381. if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
  382. // If we have a signed url (S3, object storage), redirect to this directly.
  383. u, err := storage.RepoArchives.URL(rPath, downloadName)
  384. if u != nil && err == nil {
  385. ctx.Redirect(u.String())
  386. return
  387. }
  388. }
  389. // If we have matched and access to release or issue
  390. fr, err := storage.RepoArchives.Open(rPath)
  391. if err != nil {
  392. ctx.ServerError("Open", err)
  393. return
  394. }
  395. defer fr.Close()
  396. ctx.ServeContent(fr, &context.ServeHeaderOptions{
  397. Filename: downloadName,
  398. LastModified: archiver.CreatedUnix.AsLocalTime(),
  399. })
  400. }
  401. // InitiateDownload will enqueue an archival request, as needed. It may submit
  402. // a request that's already in-progress, but the archiver service will just
  403. // kind of drop it on the floor if this is the case.
  404. func InitiateDownload(ctx *context.Context) {
  405. uri := ctx.Params("*")
  406. aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
  407. if err != nil {
  408. ctx.ServerError("archiver_service.NewRequest", err)
  409. return
  410. }
  411. if aReq == nil {
  412. ctx.Error(http.StatusNotFound)
  413. return
  414. }
  415. archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
  416. if err != nil {
  417. ctx.ServerError("archiver_service.StartArchive", err)
  418. return
  419. }
  420. if archiver == nil || archiver.Status != repo_model.ArchiverReady {
  421. if err := archiver_service.StartArchive(aReq); err != nil {
  422. ctx.ServerError("archiver_service.StartArchive", err)
  423. return
  424. }
  425. }
  426. var completed bool
  427. if archiver != nil && archiver.Status == repo_model.ArchiverReady {
  428. completed = true
  429. }
  430. ctx.JSON(http.StatusOK, map[string]any{
  431. "complete": completed,
  432. })
  433. }
  434. // SearchRepo repositories via options
  435. func SearchRepo(ctx *context.Context) {
  436. opts := &repo_model.SearchRepoOptions{
  437. ListOptions: db.ListOptions{
  438. Page: ctx.FormInt("page"),
  439. PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
  440. },
  441. Actor: ctx.Doer,
  442. Keyword: ctx.FormTrim("q"),
  443. OwnerID: ctx.FormInt64("uid"),
  444. PriorityOwnerID: ctx.FormInt64("priority_owner_id"),
  445. TeamID: ctx.FormInt64("team_id"),
  446. TopicOnly: ctx.FormBool("topic"),
  447. Collaborate: util.OptionalBoolNone,
  448. Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
  449. Template: util.OptionalBoolNone,
  450. StarredByID: ctx.FormInt64("starredBy"),
  451. IncludeDescription: ctx.FormBool("includeDesc"),
  452. }
  453. if ctx.FormString("template") != "" {
  454. opts.Template = util.OptionalBoolOf(ctx.FormBool("template"))
  455. }
  456. if ctx.FormBool("exclusive") {
  457. opts.Collaborate = util.OptionalBoolFalse
  458. }
  459. mode := ctx.FormString("mode")
  460. switch mode {
  461. case "source":
  462. opts.Fork = util.OptionalBoolFalse
  463. opts.Mirror = util.OptionalBoolFalse
  464. case "fork":
  465. opts.Fork = util.OptionalBoolTrue
  466. case "mirror":
  467. opts.Mirror = util.OptionalBoolTrue
  468. case "collaborative":
  469. opts.Mirror = util.OptionalBoolFalse
  470. opts.Collaborate = util.OptionalBoolTrue
  471. case "":
  472. default:
  473. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode))
  474. return
  475. }
  476. if ctx.FormString("archived") != "" {
  477. opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived"))
  478. }
  479. if ctx.FormString("is_private") != "" {
  480. opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private"))
  481. }
  482. sortMode := ctx.FormString("sort")
  483. if len(sortMode) > 0 {
  484. sortOrder := ctx.FormString("order")
  485. if len(sortOrder) == 0 {
  486. sortOrder = "asc"
  487. }
  488. if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok {
  489. if orderBy, ok := searchModeMap[sortMode]; ok {
  490. opts.OrderBy = orderBy
  491. } else {
  492. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: \"%s\"", sortMode))
  493. return
  494. }
  495. } else {
  496. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: \"%s\"", sortOrder))
  497. return
  498. }
  499. }
  500. var err error
  501. repos, count, err := repo_model.SearchRepository(ctx, opts)
  502. if err != nil {
  503. ctx.JSON(http.StatusInternalServerError, api.SearchError{
  504. OK: false,
  505. Error: err.Error(),
  506. })
  507. return
  508. }
  509. ctx.SetTotalCountHeader(count)
  510. // To improve performance when only the count is requested
  511. if ctx.FormBool("count_only") {
  512. return
  513. }
  514. // collect the latest commit of each repo
  515. // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
  516. repoIDsToLatestCommitSHAs := make(map[int64]string, len(repos))
  517. for _, repo := range repos {
  518. commitID, err := repo_service.GetBranchCommitID(ctx, repo, repo.DefaultBranch)
  519. if err != nil {
  520. continue
  521. }
  522. repoIDsToLatestCommitSHAs[repo.ID] = commitID
  523. }
  524. // call the database O(1) times to get the commit statuses for all repos
  525. repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptions{})
  526. if err != nil {
  527. log.Error("GetLatestCommitStatusForPairs: %v", err)
  528. return
  529. }
  530. results := make([]*repo_service.WebSearchRepository, len(repos))
  531. for i, repo := range repos {
  532. results[i] = &repo_service.WebSearchRepository{
  533. Repository: &api.Repository{
  534. ID: repo.ID,
  535. FullName: repo.FullName(),
  536. Fork: repo.IsFork,
  537. Private: repo.IsPrivate,
  538. Template: repo.IsTemplate,
  539. Mirror: repo.IsMirror,
  540. Stars: repo.NumStars,
  541. HTMLURL: repo.HTMLURL(),
  542. Link: repo.Link(),
  543. Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
  544. },
  545. LatestCommitStatus: git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]),
  546. }
  547. }
  548. ctx.JSON(http.StatusOK, repo_service.WebSearchResults{
  549. OK: true,
  550. Data: results,
  551. })
  552. }