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

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