gitea源码

repo.go 39KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 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. "strconv"
  11. "strings"
  12. "time"
  13. activities_model "code.gitea.io/gitea/models/activities"
  14. "code.gitea.io/gitea/models/db"
  15. "code.gitea.io/gitea/models/organization"
  16. "code.gitea.io/gitea/models/perm"
  17. access_model "code.gitea.io/gitea/models/perm/access"
  18. repo_model "code.gitea.io/gitea/models/repo"
  19. unit_model "code.gitea.io/gitea/models/unit"
  20. user_model "code.gitea.io/gitea/models/user"
  21. "code.gitea.io/gitea/modules/gitrepo"
  22. "code.gitea.io/gitea/modules/label"
  23. "code.gitea.io/gitea/modules/log"
  24. "code.gitea.io/gitea/modules/optional"
  25. repo_module "code.gitea.io/gitea/modules/repository"
  26. "code.gitea.io/gitea/modules/setting"
  27. api "code.gitea.io/gitea/modules/structs"
  28. "code.gitea.io/gitea/modules/validation"
  29. "code.gitea.io/gitea/modules/web"
  30. "code.gitea.io/gitea/routers/api/v1/utils"
  31. actions_service "code.gitea.io/gitea/services/actions"
  32. "code.gitea.io/gitea/services/context"
  33. "code.gitea.io/gitea/services/convert"
  34. feed_service "code.gitea.io/gitea/services/feed"
  35. "code.gitea.io/gitea/services/issue"
  36. repo_service "code.gitea.io/gitea/services/repository"
  37. )
  38. // Search repositories via options
  39. func Search(ctx *context.APIContext) {
  40. // swagger:operation GET /repos/search repository repoSearch
  41. // ---
  42. // summary: Search for repositories
  43. // produces:
  44. // - application/json
  45. // parameters:
  46. // - name: q
  47. // in: query
  48. // description: keyword
  49. // type: string
  50. // - name: topic
  51. // in: query
  52. // description: Limit search to repositories with keyword as topic
  53. // type: boolean
  54. // - name: includeDesc
  55. // in: query
  56. // description: include search of keyword within repository description
  57. // type: boolean
  58. // - name: uid
  59. // in: query
  60. // description: search only for repos that the user with the given id owns or contributes to
  61. // type: integer
  62. // format: int64
  63. // - name: priority_owner_id
  64. // in: query
  65. // description: repo owner to prioritize in the results
  66. // type: integer
  67. // format: int64
  68. // - name: team_id
  69. // in: query
  70. // description: search only for repos that belong to the given team id
  71. // type: integer
  72. // format: int64
  73. // - name: starredBy
  74. // in: query
  75. // description: search only for repos that the user with the given id has starred
  76. // type: integer
  77. // format: int64
  78. // - name: private
  79. // in: query
  80. // description: include private repositories this user has access to (defaults to true)
  81. // type: boolean
  82. // - name: is_private
  83. // in: query
  84. // description: show only pubic, private or all repositories (defaults to all)
  85. // type: boolean
  86. // - name: template
  87. // in: query
  88. // description: include template repositories this user has access to (defaults to true)
  89. // type: boolean
  90. // - name: archived
  91. // in: query
  92. // description: show only archived, non-archived or all repositories (defaults to all)
  93. // type: boolean
  94. // - name: mode
  95. // in: query
  96. // description: type of repository to search for. Supported values are
  97. // "fork", "source", "mirror" and "collaborative"
  98. // type: string
  99. // - name: exclusive
  100. // in: query
  101. // description: if `uid` is given, search only for repos that the user owns
  102. // type: boolean
  103. // - name: sort
  104. // in: query
  105. // description: sort repos by attribute. Supported values are
  106. // "alpha", "created", "updated", "size", "git_size", "lfs_size", "stars", "forks" and "id".
  107. // Default is "alpha"
  108. // type: string
  109. // - name: order
  110. // in: query
  111. // description: sort order, either "asc" (ascending) or "desc" (descending).
  112. // Default is "asc", ignored if "sort" is not specified.
  113. // type: string
  114. // - name: page
  115. // in: query
  116. // description: page number of results to return (1-based)
  117. // type: integer
  118. // - name: limit
  119. // in: query
  120. // description: page size of results
  121. // type: integer
  122. // responses:
  123. // "200":
  124. // "$ref": "#/responses/SearchResults"
  125. // "422":
  126. // "$ref": "#/responses/validationError"
  127. private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
  128. if ctx.PublicOnly {
  129. private = false
  130. }
  131. opts := repo_model.SearchRepoOptions{
  132. ListOptions: utils.GetListOptions(ctx),
  133. Actor: ctx.Doer,
  134. Keyword: ctx.FormTrim("q"),
  135. OwnerID: ctx.FormInt64("uid"),
  136. PriorityOwnerID: ctx.FormInt64("priority_owner_id"),
  137. TeamID: ctx.FormInt64("team_id"),
  138. TopicOnly: ctx.FormBool("topic"),
  139. Collaborate: optional.None[bool](),
  140. Private: private,
  141. Template: optional.None[bool](),
  142. StarredByID: ctx.FormInt64("starredBy"),
  143. IncludeDescription: ctx.FormBool("includeDesc"),
  144. }
  145. if ctx.FormString("template") != "" {
  146. opts.Template = optional.Some(ctx.FormBool("template"))
  147. }
  148. if ctx.FormBool("exclusive") {
  149. opts.Collaborate = optional.Some(false)
  150. }
  151. mode := ctx.FormString("mode")
  152. switch mode {
  153. case "source":
  154. opts.Fork = optional.Some(false)
  155. opts.Mirror = optional.Some(false)
  156. case "fork":
  157. opts.Fork = optional.Some(true)
  158. case "mirror":
  159. opts.Mirror = optional.Some(true)
  160. case "collaborative":
  161. opts.Mirror = optional.Some(false)
  162. opts.Collaborate = optional.Some(true)
  163. case "":
  164. default:
  165. ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid search mode: \"%s\"", mode))
  166. return
  167. }
  168. if ctx.FormString("archived") != "" {
  169. opts.Archived = optional.Some(ctx.FormBool("archived"))
  170. }
  171. if ctx.FormString("is_private") != "" {
  172. opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
  173. }
  174. sortMode := ctx.FormString("sort")
  175. if len(sortMode) > 0 {
  176. sortOrder := ctx.FormString("order")
  177. if len(sortOrder) == 0 {
  178. sortOrder = "asc"
  179. }
  180. if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok {
  181. if orderBy, ok := searchModeMap[sortMode]; ok {
  182. opts.OrderBy = orderBy
  183. } else {
  184. ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
  185. return
  186. }
  187. } else {
  188. ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
  189. return
  190. }
  191. }
  192. repos, count, err := repo_model.SearchRepository(ctx, opts)
  193. if err != nil {
  194. ctx.JSON(http.StatusInternalServerError, api.SearchError{
  195. OK: false,
  196. Error: err.Error(),
  197. })
  198. return
  199. }
  200. results := make([]*api.Repository, len(repos))
  201. for i, repo := range repos {
  202. if err = repo.LoadOwner(ctx); err != nil {
  203. ctx.JSON(http.StatusInternalServerError, api.SearchError{
  204. OK: false,
  205. Error: err.Error(),
  206. })
  207. return
  208. }
  209. permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
  210. if err != nil {
  211. ctx.JSON(http.StatusInternalServerError, api.SearchError{
  212. OK: false,
  213. Error: err.Error(),
  214. })
  215. }
  216. results[i] = convert.ToRepo(ctx, repo, permission)
  217. }
  218. ctx.SetLinkHeader(int(count), opts.PageSize)
  219. ctx.SetTotalCountHeader(count)
  220. ctx.JSON(http.StatusOK, api.SearchResults{
  221. OK: true,
  222. Data: results,
  223. })
  224. }
  225. // CreateUserRepo create a repository for a user
  226. func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.CreateRepoOption) {
  227. if opt.AutoInit && opt.Readme == "" {
  228. opt.Readme = "Default"
  229. }
  230. // If the readme template does not exist, a 400 will be returned.
  231. if opt.AutoInit && len(opt.Readme) > 0 && !slices.Contains(repo_module.Readmes, opt.Readme) {
  232. ctx.APIError(http.StatusBadRequest, fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes))
  233. return
  234. }
  235. repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_service.CreateRepoOptions{
  236. Name: opt.Name,
  237. Description: opt.Description,
  238. IssueLabels: opt.IssueLabels,
  239. Gitignores: opt.Gitignores,
  240. License: opt.License,
  241. Readme: opt.Readme,
  242. IsPrivate: opt.Private || setting.Repository.ForcePrivate,
  243. AutoInit: opt.AutoInit,
  244. DefaultBranch: opt.DefaultBranch,
  245. TrustModel: repo_model.ToTrustModel(opt.TrustModel),
  246. IsTemplate: opt.Template,
  247. ObjectFormatName: opt.ObjectFormatName,
  248. })
  249. if err != nil {
  250. if repo_model.IsErrRepoAlreadyExist(err) {
  251. ctx.APIError(http.StatusConflict, "The repository with the same name already exists.")
  252. } else if db.IsErrNameReserved(err) ||
  253. db.IsErrNamePatternNotAllowed(err) ||
  254. label.IsErrTemplateLoad(err) {
  255. ctx.APIError(http.StatusUnprocessableEntity, err)
  256. } else {
  257. ctx.APIErrorInternal(err)
  258. }
  259. return
  260. }
  261. // reload repo from db to get a real state after creation
  262. repo, err = repo_model.GetRepositoryByID(ctx, repo.ID)
  263. if err != nil {
  264. ctx.APIErrorInternal(err)
  265. }
  266. ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}))
  267. }
  268. // Create one repository of mine
  269. func Create(ctx *context.APIContext) {
  270. // swagger:operation POST /user/repos repository user createCurrentUserRepo
  271. // ---
  272. // summary: Create a repository
  273. // consumes:
  274. // - application/json
  275. // produces:
  276. // - application/json
  277. // parameters:
  278. // - name: body
  279. // in: body
  280. // schema:
  281. // "$ref": "#/definitions/CreateRepoOption"
  282. // responses:
  283. // "201":
  284. // "$ref": "#/responses/Repository"
  285. // "400":
  286. // "$ref": "#/responses/error"
  287. // "409":
  288. // description: The repository with the same name already exists.
  289. // "422":
  290. // "$ref": "#/responses/validationError"
  291. opt := web.GetForm(ctx).(*api.CreateRepoOption)
  292. if ctx.Doer.IsOrganization() {
  293. // Shouldn't reach this condition, but just in case.
  294. ctx.APIError(http.StatusUnprocessableEntity, "not allowed creating repository for organization")
  295. return
  296. }
  297. CreateUserRepo(ctx, ctx.Doer, *opt)
  298. }
  299. // Generate Create a repository using a template
  300. func Generate(ctx *context.APIContext) {
  301. // swagger:operation POST /repos/{template_owner}/{template_repo}/generate repository generateRepo
  302. // ---
  303. // summary: Create a repository using a template
  304. // consumes:
  305. // - application/json
  306. // produces:
  307. // - application/json
  308. // parameters:
  309. // - name: template_owner
  310. // in: path
  311. // description: owner of the template repository
  312. // type: string
  313. // required: true
  314. // - name: template_repo
  315. // in: path
  316. // description: name of the template repository
  317. // type: string
  318. // required: true
  319. // - name: body
  320. // in: body
  321. // schema:
  322. // "$ref": "#/definitions/GenerateRepoOption"
  323. // responses:
  324. // "201":
  325. // "$ref": "#/responses/Repository"
  326. // "403":
  327. // "$ref": "#/responses/forbidden"
  328. // "404":
  329. // "$ref": "#/responses/notFound"
  330. // "409":
  331. // description: The repository with the same name already exists.
  332. // "422":
  333. // "$ref": "#/responses/validationError"
  334. form := web.GetForm(ctx).(*api.GenerateRepoOption)
  335. if !ctx.Repo.Repository.IsTemplate {
  336. ctx.APIError(http.StatusUnprocessableEntity, "this is not a template repo")
  337. return
  338. }
  339. if ctx.Doer.IsOrganization() {
  340. ctx.APIError(http.StatusUnprocessableEntity, "not allowed creating repository for organization")
  341. return
  342. }
  343. opts := repo_service.GenerateRepoOptions{
  344. Name: form.Name,
  345. DefaultBranch: form.DefaultBranch,
  346. Description: form.Description,
  347. Private: form.Private || setting.Repository.ForcePrivate,
  348. GitContent: form.GitContent,
  349. Topics: form.Topics,
  350. GitHooks: form.GitHooks,
  351. Webhooks: form.Webhooks,
  352. Avatar: form.Avatar,
  353. IssueLabels: form.Labels,
  354. ProtectedBranch: form.ProtectedBranch,
  355. }
  356. if !opts.IsValid() {
  357. ctx.APIError(http.StatusUnprocessableEntity, "must select at least one template item")
  358. return
  359. }
  360. ctxUser := ctx.Doer
  361. var err error
  362. if form.Owner != ctxUser.Name {
  363. ctxUser, err = user_model.GetUserByName(ctx, form.Owner)
  364. if err != nil {
  365. if user_model.IsErrUserNotExist(err) {
  366. ctx.JSON(http.StatusNotFound, map[string]any{
  367. "error": "request owner `" + form.Owner + "` does not exist",
  368. })
  369. return
  370. }
  371. ctx.APIErrorInternal(err)
  372. return
  373. }
  374. if !ctx.Doer.IsAdmin && !ctxUser.IsOrganization() {
  375. ctx.APIError(http.StatusForbidden, "Only admin can generate repository for other user.")
  376. return
  377. }
  378. if !ctx.Doer.IsAdmin {
  379. canCreate, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
  380. if err != nil {
  381. ctx.APIErrorInternal(err)
  382. return
  383. } else if !canCreate {
  384. ctx.APIError(http.StatusForbidden, "Given user is not allowed to create repository in organization.")
  385. return
  386. }
  387. }
  388. }
  389. repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, ctx.Repo.Repository, opts)
  390. if err != nil {
  391. if repo_model.IsErrRepoAlreadyExist(err) {
  392. ctx.APIError(http.StatusConflict, "The repository with the same name already exists.")
  393. } else if db.IsErrNameReserved(err) ||
  394. db.IsErrNamePatternNotAllowed(err) {
  395. ctx.APIError(http.StatusUnprocessableEntity, err)
  396. } else {
  397. ctx.APIErrorInternal(err)
  398. }
  399. return
  400. }
  401. log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
  402. ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}))
  403. }
  404. // CreateOrgRepoDeprecated create one repository of the organization
  405. func CreateOrgRepoDeprecated(ctx *context.APIContext) {
  406. // swagger:operation POST /org/{org}/repos organization createOrgRepoDeprecated
  407. // ---
  408. // summary: Create a repository in an organization
  409. // deprecated: true
  410. // consumes:
  411. // - application/json
  412. // produces:
  413. // - application/json
  414. // parameters:
  415. // - name: org
  416. // in: path
  417. // description: name of organization
  418. // type: string
  419. // required: true
  420. // - name: body
  421. // in: body
  422. // schema:
  423. // "$ref": "#/definitions/CreateRepoOption"
  424. // responses:
  425. // "201":
  426. // "$ref": "#/responses/Repository"
  427. // "422":
  428. // "$ref": "#/responses/validationError"
  429. // "403":
  430. // "$ref": "#/responses/forbidden"
  431. // "404":
  432. // "$ref": "#/responses/notFound"
  433. CreateOrgRepo(ctx)
  434. }
  435. // CreateOrgRepo create one repository of the organization
  436. func CreateOrgRepo(ctx *context.APIContext) {
  437. // swagger:operation POST /orgs/{org}/repos organization createOrgRepo
  438. // ---
  439. // summary: Create a repository in an organization
  440. // consumes:
  441. // - application/json
  442. // produces:
  443. // - application/json
  444. // parameters:
  445. // - name: org
  446. // in: path
  447. // description: name of organization
  448. // type: string
  449. // required: true
  450. // - name: body
  451. // in: body
  452. // schema:
  453. // "$ref": "#/definitions/CreateRepoOption"
  454. // responses:
  455. // "201":
  456. // "$ref": "#/responses/Repository"
  457. // "400":
  458. // "$ref": "#/responses/error"
  459. // "404":
  460. // "$ref": "#/responses/notFound"
  461. // "403":
  462. // "$ref": "#/responses/forbidden"
  463. opt := web.GetForm(ctx).(*api.CreateRepoOption)
  464. org, err := organization.GetOrgByName(ctx, ctx.PathParam("org"))
  465. if err != nil {
  466. if organization.IsErrOrgNotExist(err) {
  467. ctx.APIError(http.StatusUnprocessableEntity, err)
  468. } else {
  469. ctx.APIErrorInternal(err)
  470. }
  471. return
  472. }
  473. if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) {
  474. ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
  475. return
  476. }
  477. if !ctx.Doer.IsAdmin {
  478. canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
  479. if err != nil {
  480. ctx.APIErrorInternal(err)
  481. return
  482. } else if !canCreate {
  483. ctx.APIError(http.StatusForbidden, "Given user is not allowed to create repository in organization.")
  484. return
  485. }
  486. }
  487. CreateUserRepo(ctx, org.AsUser(), *opt)
  488. }
  489. // Get one repository
  490. func Get(ctx *context.APIContext) {
  491. // swagger:operation GET /repos/{owner}/{repo} repository repoGet
  492. // ---
  493. // summary: Get a repository
  494. // produces:
  495. // - application/json
  496. // parameters:
  497. // - name: owner
  498. // in: path
  499. // description: owner of the repo
  500. // type: string
  501. // required: true
  502. // - name: repo
  503. // in: path
  504. // description: name of the repo
  505. // type: string
  506. // required: true
  507. // responses:
  508. // "200":
  509. // "$ref": "#/responses/Repository"
  510. // "404":
  511. // "$ref": "#/responses/notFound"
  512. if err := ctx.Repo.Repository.LoadAttributes(ctx); err != nil {
  513. ctx.APIErrorInternal(err)
  514. return
  515. }
  516. ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission))
  517. }
  518. // GetByID returns a single Repository
  519. func GetByID(ctx *context.APIContext) {
  520. // swagger:operation GET /repositories/{id} repository repoGetByID
  521. // ---
  522. // summary: Get a repository by id
  523. // produces:
  524. // - application/json
  525. // parameters:
  526. // - name: id
  527. // in: path
  528. // description: id of the repo to get
  529. // type: integer
  530. // format: int64
  531. // required: true
  532. // responses:
  533. // "200":
  534. // "$ref": "#/responses/Repository"
  535. // "404":
  536. // "$ref": "#/responses/notFound"
  537. repo, err := repo_model.GetRepositoryByID(ctx, ctx.PathParamInt64("id"))
  538. if err != nil {
  539. if repo_model.IsErrRepoNotExist(err) {
  540. ctx.APIErrorNotFound()
  541. } else {
  542. ctx.APIErrorInternal(err)
  543. }
  544. return
  545. }
  546. permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
  547. if err != nil {
  548. ctx.APIErrorInternal(err)
  549. return
  550. } else if !permission.HasAnyUnitAccess() {
  551. ctx.APIErrorNotFound()
  552. return
  553. }
  554. ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission))
  555. }
  556. // Edit edit repository properties
  557. func Edit(ctx *context.APIContext) {
  558. // swagger:operation PATCH /repos/{owner}/{repo} repository repoEdit
  559. // ---
  560. // summary: Edit a repository's properties. Only fields that are set will be changed.
  561. // produces:
  562. // - application/json
  563. // parameters:
  564. // - name: owner
  565. // in: path
  566. // description: owner of the repo to edit
  567. // type: string
  568. // required: true
  569. // - name: repo
  570. // in: path
  571. // description: name of the repo to edit
  572. // type: string
  573. // required: true
  574. // - name: body
  575. // in: body
  576. // description: "Properties of a repo that you can edit"
  577. // schema:
  578. // "$ref": "#/definitions/EditRepoOption"
  579. // responses:
  580. // "200":
  581. // "$ref": "#/responses/Repository"
  582. // "403":
  583. // "$ref": "#/responses/forbidden"
  584. // "404":
  585. // "$ref": "#/responses/notFound"
  586. // "422":
  587. // "$ref": "#/responses/validationError"
  588. opts := *web.GetForm(ctx).(*api.EditRepoOption)
  589. if err := updateBasicProperties(ctx, opts); err != nil {
  590. return
  591. }
  592. if err := updateRepoUnits(ctx, opts); err != nil {
  593. return
  594. }
  595. if opts.Archived != nil {
  596. if err := updateRepoArchivedState(ctx, opts); err != nil {
  597. return
  598. }
  599. }
  600. if opts.MirrorInterval != nil || opts.EnablePrune != nil {
  601. if err := updateMirror(ctx, opts); err != nil {
  602. return
  603. }
  604. }
  605. repo, err := repo_model.GetRepositoryByID(ctx, ctx.Repo.Repository.ID)
  606. if err != nil {
  607. ctx.APIErrorInternal(err)
  608. return
  609. }
  610. ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, ctx.Repo.Permission))
  611. }
  612. // updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility
  613. func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) error {
  614. owner := ctx.Repo.Owner
  615. repo := ctx.Repo.Repository
  616. newRepoName := repo.Name
  617. if opts.Name != nil {
  618. newRepoName = *opts.Name
  619. }
  620. // Check if repository name has been changed and not just a case change
  621. if !strings.EqualFold(repo.LowerName, newRepoName) {
  622. if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
  623. switch {
  624. case repo_model.IsErrRepoAlreadyExist(err):
  625. ctx.APIError(http.StatusUnprocessableEntity, err)
  626. case db.IsErrNameReserved(err):
  627. ctx.APIError(http.StatusUnprocessableEntity, err)
  628. case db.IsErrNamePatternNotAllowed(err):
  629. ctx.APIError(http.StatusUnprocessableEntity, err)
  630. default:
  631. ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("ChangeRepositoryName: %w", err))
  632. }
  633. return err
  634. }
  635. log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
  636. }
  637. // Update the name in the repo object for the response
  638. repo.Name = newRepoName
  639. repo.LowerName = strings.ToLower(newRepoName)
  640. if opts.Description != nil {
  641. repo.Description = *opts.Description
  642. }
  643. if opts.Website != nil {
  644. repo.Website = *opts.Website
  645. }
  646. visibilityChanged := false
  647. if opts.Private != nil {
  648. // Visibility of forked repository is forced sync with base repository.
  649. if repo.IsFork {
  650. if err := repo.GetBaseRepo(ctx); err != nil {
  651. ctx.APIErrorInternal(err)
  652. return err
  653. }
  654. *opts.Private = repo.BaseRepo.IsPrivate
  655. }
  656. visibilityChanged = repo.IsPrivate != *opts.Private
  657. // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
  658. if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin {
  659. err := errors.New("cannot change private repository to public")
  660. ctx.APIError(http.StatusUnprocessableEntity, err)
  661. return err
  662. }
  663. repo.IsPrivate = *opts.Private
  664. }
  665. if opts.Template != nil {
  666. repo.IsTemplate = *opts.Template
  667. }
  668. if ctx.Repo.GitRepo == nil && !repo.IsEmpty {
  669. var err error
  670. ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
  671. if err != nil {
  672. ctx.APIErrorInternal(err)
  673. return err
  674. }
  675. }
  676. // Default branch only updated if changed and exist or the repository is empty
  677. updateRepoLicense := false
  678. if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, *opts.DefaultBranch)) {
  679. repo.DefaultBranch = *opts.DefaultBranch
  680. if !repo.IsEmpty {
  681. if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
  682. ctx.APIErrorInternal(err)
  683. return err
  684. }
  685. updateRepoLicense = true
  686. }
  687. }
  688. if err := repo_service.UpdateRepository(ctx, repo, visibilityChanged); err != nil {
  689. ctx.APIErrorInternal(err)
  690. return err
  691. }
  692. if updateRepoLicense {
  693. if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
  694. RepoID: ctx.Repo.Repository.ID,
  695. }); err != nil {
  696. ctx.APIErrorInternal(err)
  697. return err
  698. }
  699. }
  700. log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name)
  701. return nil
  702. }
  703. // updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings
  704. func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
  705. owner := ctx.Repo.Owner
  706. repo := ctx.Repo.Repository
  707. var units []repo_model.RepoUnit
  708. var deleteUnitTypes []unit_model.Type
  709. if opts.HasIssues != nil {
  710. if *opts.HasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
  711. // Check that values are valid
  712. if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) {
  713. err := errors.New("External tracker URL not valid")
  714. ctx.APIError(http.StatusUnprocessableEntity, err)
  715. return err
  716. }
  717. if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) {
  718. err := errors.New("External tracker URL format not valid")
  719. ctx.APIError(http.StatusUnprocessableEntity, err)
  720. return err
  721. }
  722. units = append(units, repo_model.RepoUnit{
  723. RepoID: repo.ID,
  724. Type: unit_model.TypeExternalTracker,
  725. Config: &repo_model.ExternalTrackerConfig{
  726. ExternalTrackerURL: opts.ExternalTracker.ExternalTrackerURL,
  727. ExternalTrackerFormat: opts.ExternalTracker.ExternalTrackerFormat,
  728. ExternalTrackerStyle: opts.ExternalTracker.ExternalTrackerStyle,
  729. ExternalTrackerRegexpPattern: opts.ExternalTracker.ExternalTrackerRegexpPattern,
  730. },
  731. })
  732. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
  733. } else if *opts.HasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() {
  734. // Default to built-in tracker
  735. var config *repo_model.IssuesConfig
  736. if opts.InternalTracker != nil {
  737. config = &repo_model.IssuesConfig{
  738. EnableTimetracker: opts.InternalTracker.EnableTimeTracker,
  739. AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
  740. EnableDependencies: opts.InternalTracker.EnableIssueDependencies,
  741. }
  742. } else if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err != nil {
  743. // Unit type doesn't exist so we make a new config file with default values
  744. config = &repo_model.IssuesConfig{
  745. EnableTimetracker: true,
  746. AllowOnlyContributorsToTrackTime: true,
  747. EnableDependencies: true,
  748. }
  749. } else {
  750. config = unit.IssuesConfig()
  751. }
  752. units = append(units, repo_model.RepoUnit{
  753. RepoID: repo.ID,
  754. Type: unit_model.TypeIssues,
  755. Config: config,
  756. })
  757. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
  758. } else if !*opts.HasIssues {
  759. if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
  760. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
  761. }
  762. if !unit_model.TypeIssues.UnitGlobalDisabled() {
  763. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
  764. }
  765. }
  766. }
  767. if opts.HasWiki != nil {
  768. if *opts.HasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
  769. // Check that values are valid
  770. if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) {
  771. err := errors.New("External wiki URL not valid")
  772. ctx.APIError(http.StatusUnprocessableEntity, "Invalid external wiki URL")
  773. return err
  774. }
  775. units = append(units, repo_model.RepoUnit{
  776. RepoID: repo.ID,
  777. Type: unit_model.TypeExternalWiki,
  778. Config: &repo_model.ExternalWikiConfig{
  779. ExternalWikiURL: opts.ExternalWiki.ExternalWikiURL,
  780. },
  781. })
  782. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
  783. } else if *opts.HasWiki && opts.ExternalWiki == nil && !unit_model.TypeWiki.UnitGlobalDisabled() {
  784. config := &repo_model.UnitConfig{}
  785. units = append(units, repo_model.RepoUnit{
  786. RepoID: repo.ID,
  787. Type: unit_model.TypeWiki,
  788. Config: config,
  789. })
  790. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
  791. } else if !*opts.HasWiki {
  792. if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
  793. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
  794. }
  795. if !unit_model.TypeWiki.UnitGlobalDisabled() {
  796. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
  797. }
  798. }
  799. }
  800. if opts.HasCode != nil && !unit_model.TypeCode.UnitGlobalDisabled() {
  801. if *opts.HasCode {
  802. units = append(units, repo_model.RepoUnit{
  803. RepoID: repo.ID,
  804. Type: unit_model.TypeCode,
  805. Config: &repo_model.UnitConfig{},
  806. })
  807. } else {
  808. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
  809. }
  810. }
  811. if opts.HasPullRequests != nil && !unit_model.TypePullRequests.UnitGlobalDisabled() {
  812. if *opts.HasPullRequests {
  813. // We do allow setting individual PR settings through the API, so
  814. // we get the config settings and then set them
  815. // if those settings were provided in the opts.
  816. unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests)
  817. var config *repo_model.PullRequestsConfig
  818. if err != nil {
  819. // Unit type doesn't exist so we make a new config file with default values
  820. config = &repo_model.PullRequestsConfig{
  821. IgnoreWhitespaceConflicts: false,
  822. AllowMerge: true,
  823. AllowRebase: true,
  824. AllowRebaseMerge: true,
  825. AllowSquash: true,
  826. AllowFastForwardOnly: true,
  827. AllowManualMerge: true,
  828. AutodetectManualMerge: false,
  829. AllowRebaseUpdate: true,
  830. DefaultDeleteBranchAfterMerge: false,
  831. DefaultMergeStyle: repo_model.MergeStyleMerge,
  832. DefaultAllowMaintainerEdit: false,
  833. }
  834. } else {
  835. config = unit.PullRequestsConfig()
  836. }
  837. if opts.IgnoreWhitespaceConflicts != nil {
  838. config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
  839. }
  840. if opts.AllowMerge != nil {
  841. config.AllowMerge = *opts.AllowMerge
  842. }
  843. if opts.AllowRebase != nil {
  844. config.AllowRebase = *opts.AllowRebase
  845. }
  846. if opts.AllowRebaseMerge != nil {
  847. config.AllowRebaseMerge = *opts.AllowRebaseMerge
  848. }
  849. if opts.AllowSquash != nil {
  850. config.AllowSquash = *opts.AllowSquash
  851. }
  852. if opts.AllowFastForwardOnly != nil {
  853. config.AllowFastForwardOnly = *opts.AllowFastForwardOnly
  854. }
  855. if opts.AllowManualMerge != nil {
  856. config.AllowManualMerge = *opts.AllowManualMerge
  857. }
  858. if opts.AutodetectManualMerge != nil {
  859. config.AutodetectManualMerge = *opts.AutodetectManualMerge
  860. }
  861. if opts.AllowRebaseUpdate != nil {
  862. config.AllowRebaseUpdate = *opts.AllowRebaseUpdate
  863. }
  864. if opts.DefaultDeleteBranchAfterMerge != nil {
  865. config.DefaultDeleteBranchAfterMerge = *opts.DefaultDeleteBranchAfterMerge
  866. }
  867. if opts.DefaultMergeStyle != nil {
  868. config.DefaultMergeStyle = repo_model.MergeStyle(*opts.DefaultMergeStyle)
  869. }
  870. if opts.DefaultAllowMaintainerEdit != nil {
  871. config.DefaultAllowMaintainerEdit = *opts.DefaultAllowMaintainerEdit
  872. }
  873. units = append(units, repo_model.RepoUnit{
  874. RepoID: repo.ID,
  875. Type: unit_model.TypePullRequests,
  876. Config: config,
  877. })
  878. } else {
  879. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
  880. }
  881. }
  882. if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() {
  883. if *opts.HasProjects {
  884. unit, err := repo.GetUnit(ctx, unit_model.TypeProjects)
  885. var config *repo_model.ProjectsConfig
  886. if err != nil {
  887. config = &repo_model.ProjectsConfig{
  888. ProjectsMode: repo_model.ProjectsModeAll,
  889. }
  890. } else {
  891. config = unit.ProjectsConfig()
  892. }
  893. if opts.ProjectsMode != nil {
  894. config.ProjectsMode = repo_model.ProjectsMode(*opts.ProjectsMode)
  895. }
  896. units = append(units, repo_model.RepoUnit{
  897. RepoID: repo.ID,
  898. Type: unit_model.TypeProjects,
  899. Config: config,
  900. })
  901. } else {
  902. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
  903. }
  904. }
  905. if opts.HasReleases != nil && !unit_model.TypeReleases.UnitGlobalDisabled() {
  906. if *opts.HasReleases {
  907. units = append(units, repo_model.RepoUnit{
  908. RepoID: repo.ID,
  909. Type: unit_model.TypeReleases,
  910. })
  911. } else {
  912. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
  913. }
  914. }
  915. if opts.HasPackages != nil && !unit_model.TypePackages.UnitGlobalDisabled() {
  916. if *opts.HasPackages {
  917. units = append(units, repo_model.RepoUnit{
  918. RepoID: repo.ID,
  919. Type: unit_model.TypePackages,
  920. })
  921. } else {
  922. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
  923. }
  924. }
  925. if opts.HasActions != nil && !unit_model.TypeActions.UnitGlobalDisabled() {
  926. if *opts.HasActions {
  927. units = append(units, repo_model.RepoUnit{
  928. RepoID: repo.ID,
  929. Type: unit_model.TypeActions,
  930. })
  931. } else {
  932. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions)
  933. }
  934. }
  935. if len(units)+len(deleteUnitTypes) > 0 {
  936. if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
  937. ctx.APIErrorInternal(err)
  938. return err
  939. }
  940. }
  941. log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name)
  942. return nil
  943. }
  944. // updateRepoArchivedState updates repo's archive state
  945. func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) error {
  946. repo := ctx.Repo.Repository
  947. // archive / un-archive
  948. if opts.Archived != nil {
  949. if repo.IsMirror {
  950. err := errors.New("repo is a mirror, cannot archive/un-archive")
  951. ctx.APIError(http.StatusUnprocessableEntity, err)
  952. return err
  953. }
  954. if *opts.Archived {
  955. if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil {
  956. log.Error("Tried to archive a repo: %s", err)
  957. ctx.APIErrorInternal(err)
  958. return err
  959. }
  960. if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil {
  961. log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
  962. }
  963. log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  964. } else {
  965. if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil {
  966. log.Error("Tried to un-archive a repo: %s", err)
  967. ctx.APIErrorInternal(err)
  968. return err
  969. }
  970. if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
  971. if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
  972. log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
  973. }
  974. }
  975. log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  976. }
  977. }
  978. return nil
  979. }
  980. // updateMirror updates a repo's mirror Interval and EnablePrune
  981. func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error {
  982. repo := ctx.Repo.Repository
  983. // Skip this update if the repo is not a mirror, do not return error.
  984. // Because reporting errors only makes the logic more complex&fragile, it doesn't really help end users.
  985. if !repo.IsMirror {
  986. return nil
  987. }
  988. // get the mirror from the repo
  989. mirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
  990. if err != nil {
  991. log.Error("Failed to get mirror: %s", err)
  992. ctx.APIErrorInternal(err)
  993. return err
  994. }
  995. // update MirrorInterval
  996. if opts.MirrorInterval != nil {
  997. // MirrorInterval should be a duration
  998. interval, err := time.ParseDuration(*opts.MirrorInterval)
  999. if err != nil {
  1000. log.Error("Wrong format for MirrorInternal Sent: %s", err)
  1001. ctx.APIError(http.StatusUnprocessableEntity, err)
  1002. return err
  1003. }
  1004. // Ensure the provided duration is not too short
  1005. if interval != 0 && interval < setting.Mirror.MinInterval {
  1006. err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval)
  1007. ctx.APIError(http.StatusUnprocessableEntity, err)
  1008. return err
  1009. }
  1010. mirror.Interval = interval
  1011. mirror.Repo = repo
  1012. mirror.ScheduleNextUpdate()
  1013. log.Trace("Repository %s Mirror[%d] Set Interval: %s NextUpdateUnix: %s", repo.FullName(), mirror.ID, interval, mirror.NextUpdateUnix)
  1014. }
  1015. // update EnablePrune
  1016. if opts.EnablePrune != nil {
  1017. mirror.EnablePrune = *opts.EnablePrune
  1018. log.Trace("Repository %s Mirror[%d] Set EnablePrune: %t", repo.FullName(), mirror.ID, mirror.EnablePrune)
  1019. }
  1020. // finally update the mirror in the DB
  1021. if err := repo_model.UpdateMirror(ctx, mirror); err != nil {
  1022. log.Error("Failed to Set Mirror Interval: %s", err)
  1023. ctx.APIError(http.StatusUnprocessableEntity, err)
  1024. return err
  1025. }
  1026. return nil
  1027. }
  1028. // Delete one repository
  1029. func Delete(ctx *context.APIContext) {
  1030. // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
  1031. // ---
  1032. // summary: Delete a repository
  1033. // produces:
  1034. // - application/json
  1035. // parameters:
  1036. // - name: owner
  1037. // in: path
  1038. // description: owner of the repo to delete
  1039. // type: string
  1040. // required: true
  1041. // - name: repo
  1042. // in: path
  1043. // description: name of the repo to delete
  1044. // type: string
  1045. // required: true
  1046. // responses:
  1047. // "204":
  1048. // "$ref": "#/responses/empty"
  1049. // "403":
  1050. // "$ref": "#/responses/forbidden"
  1051. // "404":
  1052. // "$ref": "#/responses/notFound"
  1053. owner := ctx.Repo.Owner
  1054. repo := ctx.Repo.Repository
  1055. canDelete, err := repo_module.CanUserDelete(ctx, repo, ctx.Doer)
  1056. if err != nil {
  1057. ctx.APIErrorInternal(err)
  1058. return
  1059. } else if !canDelete {
  1060. ctx.APIError(http.StatusForbidden, "Given user is not owner of organization.")
  1061. return
  1062. }
  1063. if ctx.Repo.GitRepo != nil {
  1064. ctx.Repo.GitRepo.Close()
  1065. }
  1066. if err := repo_service.DeleteRepository(ctx, ctx.Doer, repo, true); err != nil {
  1067. ctx.APIErrorInternal(err)
  1068. return
  1069. }
  1070. log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
  1071. ctx.Status(http.StatusNoContent)
  1072. }
  1073. // GetIssueTemplates returns the issue templates for a repository
  1074. func GetIssueTemplates(ctx *context.APIContext) {
  1075. // swagger:operation GET /repos/{owner}/{repo}/issue_templates repository repoGetIssueTemplates
  1076. // ---
  1077. // summary: Get available issue templates for a repository
  1078. // produces:
  1079. // - application/json
  1080. // parameters:
  1081. // - name: owner
  1082. // in: path
  1083. // description: owner of the repo
  1084. // type: string
  1085. // required: true
  1086. // - name: repo
  1087. // in: path
  1088. // description: name of the repo
  1089. // type: string
  1090. // required: true
  1091. // responses:
  1092. // "200":
  1093. // "$ref": "#/responses/IssueTemplates"
  1094. // "404":
  1095. // "$ref": "#/responses/notFound"
  1096. ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1097. if cnt := len(ret.TemplateErrors); cnt != 0 {
  1098. ctx.Resp.Header().Add("X-Gitea-Warning", "error occurs when parsing issue template: count="+strconv.Itoa(cnt))
  1099. }
  1100. ctx.JSON(http.StatusOK, ret.IssueTemplates)
  1101. }
  1102. // GetIssueConfig returns the issue config for a repo
  1103. func GetIssueConfig(ctx *context.APIContext) {
  1104. // swagger:operation GET /repos/{owner}/{repo}/issue_config repository repoGetIssueConfig
  1105. // ---
  1106. // summary: Returns the issue config for a repo
  1107. // produces:
  1108. // - application/json
  1109. // parameters:
  1110. // - name: owner
  1111. // in: path
  1112. // description: owner of the repo
  1113. // type: string
  1114. // required: true
  1115. // - name: repo
  1116. // in: path
  1117. // description: name of the repo
  1118. // type: string
  1119. // required: true
  1120. // responses:
  1121. // "200":
  1122. // "$ref": "#/responses/RepoIssueConfig"
  1123. // "404":
  1124. // "$ref": "#/responses/notFound"
  1125. issueConfig, _ := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1126. ctx.JSON(http.StatusOK, issueConfig)
  1127. }
  1128. // ValidateIssueConfig returns validation errors for the issue config
  1129. func ValidateIssueConfig(ctx *context.APIContext) {
  1130. // swagger:operation GET /repos/{owner}/{repo}/issue_config/validate repository repoValidateIssueConfig
  1131. // ---
  1132. // summary: Returns the validation information for a issue config
  1133. // produces:
  1134. // - application/json
  1135. // parameters:
  1136. // - name: owner
  1137. // in: path
  1138. // description: owner of the repo
  1139. // type: string
  1140. // required: true
  1141. // - name: repo
  1142. // in: path
  1143. // description: name of the repo
  1144. // type: string
  1145. // required: true
  1146. // responses:
  1147. // "200":
  1148. // "$ref": "#/responses/RepoIssueConfigValidation"
  1149. // "404":
  1150. // "$ref": "#/responses/notFound"
  1151. _, err := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1152. if err == nil {
  1153. ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""})
  1154. } else {
  1155. ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: false, Message: err.Error()})
  1156. }
  1157. }
  1158. func ListRepoActivityFeeds(ctx *context.APIContext) {
  1159. // swagger:operation GET /repos/{owner}/{repo}/activities/feeds repository repoListActivityFeeds
  1160. // ---
  1161. // summary: List a repository's activity feeds
  1162. // produces:
  1163. // - application/json
  1164. // parameters:
  1165. // - name: owner
  1166. // in: path
  1167. // description: owner of the repo
  1168. // type: string
  1169. // required: true
  1170. // - name: repo
  1171. // in: path
  1172. // description: name of the repo
  1173. // type: string
  1174. // required: true
  1175. // - name: date
  1176. // in: query
  1177. // description: the date of the activities to be found
  1178. // type: string
  1179. // format: date
  1180. // - name: page
  1181. // in: query
  1182. // description: page number of results to return (1-based)
  1183. // type: integer
  1184. // - name: limit
  1185. // in: query
  1186. // description: page size of results
  1187. // type: integer
  1188. // responses:
  1189. // "200":
  1190. // "$ref": "#/responses/ActivityFeedsList"
  1191. // "404":
  1192. // "$ref": "#/responses/notFound"
  1193. listOptions := utils.GetListOptions(ctx)
  1194. opts := activities_model.GetFeedsOptions{
  1195. RequestedRepo: ctx.Repo.Repository,
  1196. Actor: ctx.Doer,
  1197. IncludePrivate: true,
  1198. Date: ctx.FormString("date"),
  1199. ListOptions: listOptions,
  1200. }
  1201. feeds, count, err := feed_service.GetFeeds(ctx, opts)
  1202. if err != nil {
  1203. ctx.APIErrorInternal(err)
  1204. return
  1205. }
  1206. ctx.SetTotalCountHeader(count)
  1207. ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
  1208. }