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 22KB

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