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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "fmt"
  8. "net/http"
  9. "strings"
  10. "time"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/models/db"
  13. "code.gitea.io/gitea/models/perm"
  14. repo_model "code.gitea.io/gitea/models/repo"
  15. unit_model "code.gitea.io/gitea/models/unit"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "code.gitea.io/gitea/modules/context"
  18. "code.gitea.io/gitea/modules/convert"
  19. "code.gitea.io/gitea/modules/git"
  20. "code.gitea.io/gitea/modules/log"
  21. "code.gitea.io/gitea/modules/setting"
  22. api "code.gitea.io/gitea/modules/structs"
  23. "code.gitea.io/gitea/modules/util"
  24. "code.gitea.io/gitea/modules/validation"
  25. "code.gitea.io/gitea/modules/web"
  26. "code.gitea.io/gitea/routers/api/v1/utils"
  27. repo_service "code.gitea.io/gitea/services/repository"
  28. )
  29. var searchOrderByMap = map[string]map[string]db.SearchOrderBy{
  30. "asc": {
  31. "alpha": db.SearchOrderByAlphabetically,
  32. "created": db.SearchOrderByOldest,
  33. "updated": db.SearchOrderByLeastUpdated,
  34. "size": db.SearchOrderBySize,
  35. "id": db.SearchOrderByID,
  36. },
  37. "desc": {
  38. "alpha": db.SearchOrderByAlphabeticallyReverse,
  39. "created": db.SearchOrderByNewest,
  40. "updated": db.SearchOrderByRecentUpdated,
  41. "size": db.SearchOrderBySizeReverse,
  42. "id": db.SearchOrderByIDReverse,
  43. },
  44. }
  45. // Search repositories via options
  46. func Search(ctx *context.APIContext) {
  47. // swagger:operation GET /repos/search repository repoSearch
  48. // ---
  49. // summary: Search for repositories
  50. // produces:
  51. // - application/json
  52. // parameters:
  53. // - name: q
  54. // in: query
  55. // description: keyword
  56. // type: string
  57. // - name: topic
  58. // in: query
  59. // description: Limit search to repositories with keyword as topic
  60. // type: boolean
  61. // - name: includeDesc
  62. // in: query
  63. // description: include search of keyword within repository description
  64. // type: boolean
  65. // - name: uid
  66. // in: query
  67. // description: search only for repos that the user with the given id owns or contributes to
  68. // type: integer
  69. // format: int64
  70. // - name: priority_owner_id
  71. // in: query
  72. // description: repo owner to prioritize in the results
  73. // type: integer
  74. // format: int64
  75. // - name: team_id
  76. // in: query
  77. // description: search only for repos that belong to the given team id
  78. // type: integer
  79. // format: int64
  80. // - name: starredBy
  81. // in: query
  82. // description: search only for repos that the user with the given id has starred
  83. // type: integer
  84. // format: int64
  85. // - name: private
  86. // in: query
  87. // description: include private repositories this user has access to (defaults to true)
  88. // type: boolean
  89. // - name: is_private
  90. // in: query
  91. // description: show only pubic, private or all repositories (defaults to all)
  92. // type: boolean
  93. // - name: template
  94. // in: query
  95. // description: include template repositories this user has access to (defaults to true)
  96. // type: boolean
  97. // - name: archived
  98. // in: query
  99. // description: show only archived, non-archived or all repositories (defaults to all)
  100. // type: boolean
  101. // - name: mode
  102. // in: query
  103. // description: type of repository to search for. Supported values are
  104. // "fork", "source", "mirror" and "collaborative"
  105. // type: string
  106. // - name: exclusive
  107. // in: query
  108. // description: if `uid` is given, search only for repos that the user owns
  109. // type: boolean
  110. // - name: sort
  111. // in: query
  112. // description: sort repos by attribute. Supported values are
  113. // "alpha", "created", "updated", "size", and "id".
  114. // Default is "alpha"
  115. // type: string
  116. // - name: order
  117. // in: query
  118. // description: sort order, either "asc" (ascending) or "desc" (descending).
  119. // Default is "asc", ignored if "sort" is not specified.
  120. // type: string
  121. // - name: page
  122. // in: query
  123. // description: page number of results to return (1-based)
  124. // type: integer
  125. // - name: limit
  126. // in: query
  127. // description: page size of results
  128. // type: integer
  129. // responses:
  130. // "200":
  131. // "$ref": "#/responses/SearchResults"
  132. // "422":
  133. // "$ref": "#/responses/validationError"
  134. opts := &models.SearchRepoOptions{
  135. ListOptions: utils.GetListOptions(ctx),
  136. Actor: ctx.User,
  137. Keyword: ctx.FormTrim("q"),
  138. OwnerID: ctx.FormInt64("uid"),
  139. PriorityOwnerID: ctx.FormInt64("priority_owner_id"),
  140. TeamID: ctx.FormInt64("team_id"),
  141. TopicOnly: ctx.FormBool("topic"),
  142. Collaborate: util.OptionalBoolNone,
  143. Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
  144. Template: util.OptionalBoolNone,
  145. StarredByID: ctx.FormInt64("starredBy"),
  146. IncludeDescription: ctx.FormBool("includeDesc"),
  147. }
  148. if ctx.FormString("template") != "" {
  149. opts.Template = util.OptionalBoolOf(ctx.FormBool("template"))
  150. }
  151. if ctx.FormBool("exclusive") {
  152. opts.Collaborate = util.OptionalBoolFalse
  153. }
  154. var mode = ctx.FormString("mode")
  155. switch mode {
  156. case "source":
  157. opts.Fork = util.OptionalBoolFalse
  158. opts.Mirror = util.OptionalBoolFalse
  159. case "fork":
  160. opts.Fork = util.OptionalBoolTrue
  161. case "mirror":
  162. opts.Mirror = util.OptionalBoolTrue
  163. case "collaborative":
  164. opts.Mirror = util.OptionalBoolFalse
  165. opts.Collaborate = util.OptionalBoolTrue
  166. case "":
  167. default:
  168. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
  169. return
  170. }
  171. if ctx.FormString("archived") != "" {
  172. opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived"))
  173. }
  174. if ctx.FormString("is_private") != "" {
  175. opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private"))
  176. }
  177. var sortMode = ctx.FormString("sort")
  178. if len(sortMode) > 0 {
  179. var sortOrder = ctx.FormString("order")
  180. if len(sortOrder) == 0 {
  181. sortOrder = "asc"
  182. }
  183. if searchModeMap, ok := searchOrderByMap[sortOrder]; ok {
  184. if orderBy, ok := searchModeMap[sortMode]; ok {
  185. opts.OrderBy = orderBy
  186. } else {
  187. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
  188. return
  189. }
  190. } else {
  191. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
  192. return
  193. }
  194. }
  195. var err error
  196. repos, count, err := models.SearchRepository(opts)
  197. if err != nil {
  198. ctx.JSON(http.StatusInternalServerError, api.SearchError{
  199. OK: false,
  200. Error: err.Error(),
  201. })
  202. return
  203. }
  204. results := make([]*api.Repository, len(repos))
  205. for i, repo := range repos {
  206. if err = repo.GetOwner(db.DefaultContext); err != nil {
  207. ctx.JSON(http.StatusInternalServerError, api.SearchError{
  208. OK: false,
  209. Error: err.Error(),
  210. })
  211. return
  212. }
  213. accessMode, err := models.AccessLevel(ctx.User, repo)
  214. if err != nil {
  215. ctx.JSON(http.StatusInternalServerError, api.SearchError{
  216. OK: false,
  217. Error: err.Error(),
  218. })
  219. }
  220. results[i] = convert.ToRepo(repo, accessMode)
  221. }
  222. ctx.SetLinkHeader(int(count), opts.PageSize)
  223. ctx.SetTotalCountHeader(count)
  224. ctx.JSON(http.StatusOK, api.SearchResults{
  225. OK: true,
  226. Data: results,
  227. })
  228. }
  229. // CreateUserRepo create a repository for a user
  230. func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.CreateRepoOption) {
  231. if opt.AutoInit && opt.Readme == "" {
  232. opt.Readme = "Default"
  233. }
  234. repo, err := repo_service.CreateRepository(ctx.User, owner, models.CreateRepoOptions{
  235. Name: opt.Name,
  236. Description: opt.Description,
  237. IssueLabels: opt.IssueLabels,
  238. Gitignores: opt.Gitignores,
  239. License: opt.License,
  240. Readme: opt.Readme,
  241. IsPrivate: opt.Private,
  242. AutoInit: opt.AutoInit,
  243. DefaultBranch: opt.DefaultBranch,
  244. TrustModel: repo_model.ToTrustModel(opt.TrustModel),
  245. IsTemplate: opt.Template,
  246. })
  247. if err != nil {
  248. if models.IsErrRepoAlreadyExist(err) {
  249. ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
  250. } else if db.IsErrNameReserved(err) ||
  251. db.IsErrNamePatternNotAllowed(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(repo.ID)
  260. if err != nil {
  261. ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err)
  262. }
  263. ctx.JSON(http.StatusCreated, convert.ToRepo(repo, 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. // "409":
  283. // description: The repository with the same name already exists.
  284. // "422":
  285. // "$ref": "#/responses/validationError"
  286. opt := web.GetForm(ctx).(*api.CreateRepoOption)
  287. if ctx.User.IsOrganization() {
  288. // Shouldn't reach this condition, but just in case.
  289. ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization")
  290. return
  291. }
  292. CreateUserRepo(ctx, ctx.User, *opt)
  293. }
  294. // Generate Create a repository using a template
  295. func Generate(ctx *context.APIContext) {
  296. // swagger:operation POST /repos/{template_owner}/{template_repo}/generate repository generateRepo
  297. // ---
  298. // summary: Create a repository using a template
  299. // consumes:
  300. // - application/json
  301. // produces:
  302. // - application/json
  303. // parameters:
  304. // - name: template_owner
  305. // in: path
  306. // description: name of the template repository owner
  307. // type: string
  308. // required: true
  309. // - name: template_repo
  310. // in: path
  311. // description: name of the template repository
  312. // type: string
  313. // required: true
  314. // - name: body
  315. // in: body
  316. // schema:
  317. // "$ref": "#/definitions/GenerateRepoOption"
  318. // responses:
  319. // "201":
  320. // "$ref": "#/responses/Repository"
  321. // "403":
  322. // "$ref": "#/responses/forbidden"
  323. // "404":
  324. // "$ref": "#/responses/notFound"
  325. // "409":
  326. // description: The repository with the same name already exists.
  327. // "422":
  328. // "$ref": "#/responses/validationError"
  329. form := web.GetForm(ctx).(*api.GenerateRepoOption)
  330. if !ctx.Repo.Repository.IsTemplate {
  331. ctx.Error(http.StatusUnprocessableEntity, "", "this is not a template repo")
  332. return
  333. }
  334. if ctx.User.IsOrganization() {
  335. ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization")
  336. return
  337. }
  338. opts := models.GenerateRepoOptions{
  339. Name: form.Name,
  340. Description: form.Description,
  341. Private: form.Private,
  342. GitContent: form.GitContent,
  343. Topics: form.Topics,
  344. GitHooks: form.GitHooks,
  345. Webhooks: form.Webhooks,
  346. Avatar: form.Avatar,
  347. IssueLabels: form.Labels,
  348. }
  349. if !opts.IsValid() {
  350. ctx.Error(http.StatusUnprocessableEntity, "", "must select at least one template item")
  351. return
  352. }
  353. ctxUser := ctx.User
  354. var err error
  355. if form.Owner != ctxUser.Name {
  356. ctxUser, err = user_model.GetUserByName(form.Owner)
  357. if err != nil {
  358. if user_model.IsErrUserNotExist(err) {
  359. ctx.JSON(http.StatusNotFound, map[string]interface{}{
  360. "error": "request owner `" + form.Owner + "` does not exist",
  361. })
  362. return
  363. }
  364. ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
  365. return
  366. }
  367. if !ctx.User.IsAdmin && !ctxUser.IsOrganization() {
  368. ctx.Error(http.StatusForbidden, "", "Only admin can generate repository for other user.")
  369. return
  370. }
  371. if !ctx.User.IsAdmin {
  372. canCreate, err := models.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx.User.ID)
  373. if err != nil {
  374. ctx.ServerError("CanCreateOrgRepo", err)
  375. return
  376. } else if !canCreate {
  377. ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.")
  378. return
  379. }
  380. }
  381. }
  382. repo, err := repo_service.GenerateRepository(ctx.User, ctxUser, ctx.Repo.Repository, opts)
  383. if err != nil {
  384. if models.IsErrRepoAlreadyExist(err) {
  385. ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
  386. } else if db.IsErrNameReserved(err) ||
  387. db.IsErrNamePatternNotAllowed(err) {
  388. ctx.Error(http.StatusUnprocessableEntity, "", err)
  389. } else {
  390. ctx.Error(http.StatusInternalServerError, "CreateRepository", err)
  391. }
  392. return
  393. }
  394. log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
  395. ctx.JSON(http.StatusCreated, convert.ToRepo(repo, perm.AccessModeOwner))
  396. }
  397. // CreateOrgRepoDeprecated create one repository of the organization
  398. func CreateOrgRepoDeprecated(ctx *context.APIContext) {
  399. // swagger:operation POST /org/{org}/repos organization createOrgRepoDeprecated
  400. // ---
  401. // summary: Create a repository in an organization
  402. // deprecated: true
  403. // consumes:
  404. // - application/json
  405. // produces:
  406. // - application/json
  407. // parameters:
  408. // - name: org
  409. // in: path
  410. // description: name of organization
  411. // type: string
  412. // required: true
  413. // - name: body
  414. // in: body
  415. // schema:
  416. // "$ref": "#/definitions/CreateRepoOption"
  417. // responses:
  418. // "201":
  419. // "$ref": "#/responses/Repository"
  420. // "422":
  421. // "$ref": "#/responses/validationError"
  422. // "403":
  423. // "$ref": "#/responses/forbidden"
  424. CreateOrgRepo(ctx)
  425. }
  426. // CreateOrgRepo create one repository of the organization
  427. func CreateOrgRepo(ctx *context.APIContext) {
  428. // swagger:operation POST /orgs/{org}/repos organization createOrgRepo
  429. // ---
  430. // summary: Create a repository in an organization
  431. // consumes:
  432. // - application/json
  433. // produces:
  434. // - application/json
  435. // parameters:
  436. // - name: org
  437. // in: path
  438. // description: name of organization
  439. // type: string
  440. // required: true
  441. // - name: body
  442. // in: body
  443. // schema:
  444. // "$ref": "#/definitions/CreateRepoOption"
  445. // responses:
  446. // "201":
  447. // "$ref": "#/responses/Repository"
  448. // "404":
  449. // "$ref": "#/responses/notFound"
  450. // "403":
  451. // "$ref": "#/responses/forbidden"
  452. opt := web.GetForm(ctx).(*api.CreateRepoOption)
  453. org, err := models.GetOrgByName(ctx.Params(":org"))
  454. if err != nil {
  455. if models.IsErrOrgNotExist(err) {
  456. ctx.Error(http.StatusUnprocessableEntity, "", err)
  457. } else {
  458. ctx.Error(http.StatusInternalServerError, "GetOrgByName", err)
  459. }
  460. return
  461. }
  462. if !models.HasOrgOrUserVisible(org.AsUser(), ctx.User) {
  463. ctx.NotFound("HasOrgOrUserVisible", nil)
  464. return
  465. }
  466. if !ctx.User.IsAdmin {
  467. canCreate, err := org.CanCreateOrgRepo(ctx.User.ID)
  468. if err != nil {
  469. ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err)
  470. return
  471. } else if !canCreate {
  472. ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.")
  473. return
  474. }
  475. }
  476. CreateUserRepo(ctx, org.AsUser(), *opt)
  477. }
  478. // Get one repository
  479. func Get(ctx *context.APIContext) {
  480. // swagger:operation GET /repos/{owner}/{repo} repository repoGet
  481. // ---
  482. // summary: Get a repository
  483. // produces:
  484. // - application/json
  485. // parameters:
  486. // - name: owner
  487. // in: path
  488. // description: owner of the repo
  489. // type: string
  490. // required: true
  491. // - name: repo
  492. // in: path
  493. // description: name of the repo
  494. // type: string
  495. // required: true
  496. // responses:
  497. // "200":
  498. // "$ref": "#/responses/Repository"
  499. ctx.JSON(http.StatusOK, convert.ToRepo(ctx.Repo.Repository, ctx.Repo.AccessMode))
  500. }
  501. // GetByID returns a single Repository
  502. func GetByID(ctx *context.APIContext) {
  503. // swagger:operation GET /repositories/{id} repository repoGetByID
  504. // ---
  505. // summary: Get a repository by id
  506. // produces:
  507. // - application/json
  508. // parameters:
  509. // - name: id
  510. // in: path
  511. // description: id of the repo to get
  512. // type: integer
  513. // format: int64
  514. // required: true
  515. // responses:
  516. // "200":
  517. // "$ref": "#/responses/Repository"
  518. repo, err := repo_model.GetRepositoryByID(ctx.ParamsInt64(":id"))
  519. if err != nil {
  520. if repo_model.IsErrRepoNotExist(err) {
  521. ctx.NotFound()
  522. } else {
  523. ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err)
  524. }
  525. return
  526. }
  527. perm, err := models.GetUserRepoPermission(repo, ctx.User)
  528. if err != nil {
  529. ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
  530. return
  531. } else if !perm.HasAccess() {
  532. ctx.NotFound()
  533. return
  534. }
  535. ctx.JSON(http.StatusOK, convert.ToRepo(repo, perm.AccessMode))
  536. }
  537. // Edit edit repository properties
  538. func Edit(ctx *context.APIContext) {
  539. // swagger:operation PATCH /repos/{owner}/{repo} repository repoEdit
  540. // ---
  541. // summary: Edit a repository's properties. Only fields that are set will be changed.
  542. // produces:
  543. // - application/json
  544. // parameters:
  545. // - name: owner
  546. // in: path
  547. // description: owner of the repo to edit
  548. // type: string
  549. // required: true
  550. // - name: repo
  551. // in: path
  552. // description: name of the repo to edit
  553. // type: string
  554. // required: true
  555. // required: true
  556. // - name: body
  557. // in: body
  558. // description: "Properties of a repo that you can edit"
  559. // schema:
  560. // "$ref": "#/definitions/EditRepoOption"
  561. // responses:
  562. // "200":
  563. // "$ref": "#/responses/Repository"
  564. // "403":
  565. // "$ref": "#/responses/forbidden"
  566. // "422":
  567. // "$ref": "#/responses/validationError"
  568. opts := *web.GetForm(ctx).(*api.EditRepoOption)
  569. if err := updateBasicProperties(ctx, opts); err != nil {
  570. return
  571. }
  572. if err := updateRepoUnits(ctx, opts); err != nil {
  573. return
  574. }
  575. if opts.Archived != nil {
  576. if err := updateRepoArchivedState(ctx, opts); err != nil {
  577. return
  578. }
  579. }
  580. if opts.MirrorInterval != nil {
  581. if err := updateMirrorInterval(ctx, opts); err != nil {
  582. return
  583. }
  584. }
  585. repo, err := repo_model.GetRepositoryByID(ctx.Repo.Repository.ID)
  586. if err != nil {
  587. ctx.InternalServerError(err)
  588. return
  589. }
  590. ctx.JSON(http.StatusOK, convert.ToRepo(repo, ctx.Repo.AccessMode))
  591. }
  592. // updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility
  593. func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) error {
  594. owner := ctx.Repo.Owner
  595. repo := ctx.Repo.Repository
  596. newRepoName := repo.Name
  597. if opts.Name != nil {
  598. newRepoName = *opts.Name
  599. }
  600. // Check if repository name has been changed and not just a case change
  601. if repo.LowerName != strings.ToLower(newRepoName) {
  602. if err := repo_service.ChangeRepositoryName(ctx.User, repo, newRepoName); err != nil {
  603. switch {
  604. case models.IsErrRepoAlreadyExist(err):
  605. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err)
  606. case db.IsErrNameReserved(err):
  607. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err)
  608. case db.IsErrNamePatternNotAllowed(err):
  609. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(db.ErrNamePatternNotAllowed).Pattern), err)
  610. default:
  611. ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err)
  612. }
  613. return err
  614. }
  615. log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
  616. }
  617. // Update the name in the repo object for the response
  618. repo.Name = newRepoName
  619. repo.LowerName = strings.ToLower(newRepoName)
  620. if opts.Description != nil {
  621. repo.Description = *opts.Description
  622. }
  623. if opts.Website != nil {
  624. repo.Website = *opts.Website
  625. }
  626. visibilityChanged := false
  627. if opts.Private != nil {
  628. // Visibility of forked repository is forced sync with base repository.
  629. if repo.IsFork {
  630. if err := repo.GetBaseRepo(); err != nil {
  631. ctx.Error(http.StatusInternalServerError, "Unable to load base repository", err)
  632. return err
  633. }
  634. *opts.Private = repo.BaseRepo.IsPrivate
  635. }
  636. visibilityChanged = repo.IsPrivate != *opts.Private
  637. // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
  638. if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.User.IsAdmin {
  639. err := fmt.Errorf("cannot change private repository to public")
  640. ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err)
  641. return err
  642. }
  643. repo.IsPrivate = *opts.Private
  644. }
  645. if opts.Template != nil {
  646. repo.IsTemplate = *opts.Template
  647. }
  648. if ctx.Repo.GitRepo == nil && !repo.IsEmpty {
  649. var err error
  650. ctx.Repo.GitRepo, err = git.OpenRepository(ctx.Repo.Repository.RepoPath())
  651. if err != nil {
  652. ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err)
  653. return err
  654. }
  655. defer ctx.Repo.GitRepo.Close()
  656. }
  657. // Default branch only updated if changed and exist or the repository is empty
  658. if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) {
  659. if !repo.IsEmpty {
  660. if err := ctx.Repo.GitRepo.SetDefaultBranch(*opts.DefaultBranch); err != nil {
  661. if !git.IsErrUnsupportedVersion(err) {
  662. ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err)
  663. return err
  664. }
  665. }
  666. }
  667. repo.DefaultBranch = *opts.DefaultBranch
  668. }
  669. if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
  670. ctx.Error(http.StatusInternalServerError, "UpdateRepository", err)
  671. return err
  672. }
  673. log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name)
  674. return nil
  675. }
  676. // updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings
  677. func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
  678. owner := ctx.Repo.Owner
  679. repo := ctx.Repo.Repository
  680. var units []repo_model.RepoUnit
  681. var deleteUnitTypes []unit_model.Type
  682. if opts.HasIssues != nil {
  683. if *opts.HasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
  684. // Check that values are valid
  685. if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) {
  686. err := fmt.Errorf("External tracker URL not valid")
  687. ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL", err)
  688. return err
  689. }
  690. if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) {
  691. err := fmt.Errorf("External tracker URL format not valid")
  692. ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL format", err)
  693. return err
  694. }
  695. units = append(units, repo_model.RepoUnit{
  696. RepoID: repo.ID,
  697. Type: unit_model.TypeExternalTracker,
  698. Config: &repo_model.ExternalTrackerConfig{
  699. ExternalTrackerURL: opts.ExternalTracker.ExternalTrackerURL,
  700. ExternalTrackerFormat: opts.ExternalTracker.ExternalTrackerFormat,
  701. ExternalTrackerStyle: opts.ExternalTracker.ExternalTrackerStyle,
  702. },
  703. })
  704. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
  705. } else if *opts.HasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() {
  706. // Default to built-in tracker
  707. var config *repo_model.IssuesConfig
  708. if opts.InternalTracker != nil {
  709. config = &repo_model.IssuesConfig{
  710. EnableTimetracker: opts.InternalTracker.EnableTimeTracker,
  711. AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
  712. EnableDependencies: opts.InternalTracker.EnableIssueDependencies,
  713. }
  714. } else if unit, err := repo.GetUnit(unit_model.TypeIssues); err != nil {
  715. // Unit type doesn't exist so we make a new config file with default values
  716. config = &repo_model.IssuesConfig{
  717. EnableTimetracker: true,
  718. AllowOnlyContributorsToTrackTime: true,
  719. EnableDependencies: true,
  720. }
  721. } else {
  722. config = unit.IssuesConfig()
  723. }
  724. units = append(units, repo_model.RepoUnit{
  725. RepoID: repo.ID,
  726. Type: unit_model.TypeIssues,
  727. Config: config,
  728. })
  729. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
  730. } else if !*opts.HasIssues {
  731. if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
  732. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
  733. }
  734. if !unit_model.TypeIssues.UnitGlobalDisabled() {
  735. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
  736. }
  737. }
  738. }
  739. if opts.HasWiki != nil {
  740. if *opts.HasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
  741. // Check that values are valid
  742. if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) {
  743. err := fmt.Errorf("External wiki URL not valid")
  744. ctx.Error(http.StatusUnprocessableEntity, "", "Invalid external wiki URL")
  745. return err
  746. }
  747. units = append(units, repo_model.RepoUnit{
  748. RepoID: repo.ID,
  749. Type: unit_model.TypeExternalWiki,
  750. Config: &repo_model.ExternalWikiConfig{
  751. ExternalWikiURL: opts.ExternalWiki.ExternalWikiURL,
  752. },
  753. })
  754. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
  755. } else if *opts.HasWiki && opts.ExternalWiki == nil && !unit_model.TypeWiki.UnitGlobalDisabled() {
  756. config := &repo_model.UnitConfig{}
  757. units = append(units, repo_model.RepoUnit{
  758. RepoID: repo.ID,
  759. Type: unit_model.TypeWiki,
  760. Config: config,
  761. })
  762. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
  763. } else if !*opts.HasWiki {
  764. if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
  765. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
  766. }
  767. if !unit_model.TypeWiki.UnitGlobalDisabled() {
  768. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
  769. }
  770. }
  771. }
  772. if opts.HasPullRequests != nil {
  773. if *opts.HasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() {
  774. // We do allow setting individual PR settings through the API, so
  775. // we get the config settings and then set them
  776. // if those settings were provided in the opts.
  777. unit, err := repo.GetUnit(unit_model.TypePullRequests)
  778. var config *repo_model.PullRequestsConfig
  779. if err != nil {
  780. // Unit type doesn't exist so we make a new config file with default values
  781. config = &repo_model.PullRequestsConfig{
  782. IgnoreWhitespaceConflicts: false,
  783. AllowMerge: true,
  784. AllowRebase: true,
  785. AllowRebaseMerge: true,
  786. AllowSquash: true,
  787. AllowManualMerge: true,
  788. AutodetectManualMerge: false,
  789. DefaultDeleteBranchAfterMerge: false,
  790. DefaultMergeStyle: repo_model.MergeStyleMerge,
  791. }
  792. } else {
  793. config = unit.PullRequestsConfig()
  794. }
  795. if opts.IgnoreWhitespaceConflicts != nil {
  796. config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
  797. }
  798. if opts.AllowMerge != nil {
  799. config.AllowMerge = *opts.AllowMerge
  800. }
  801. if opts.AllowRebase != nil {
  802. config.AllowRebase = *opts.AllowRebase
  803. }
  804. if opts.AllowRebaseMerge != nil {
  805. config.AllowRebaseMerge = *opts.AllowRebaseMerge
  806. }
  807. if opts.AllowSquash != nil {
  808. config.AllowSquash = *opts.AllowSquash
  809. }
  810. if opts.AllowManualMerge != nil {
  811. config.AllowManualMerge = *opts.AllowManualMerge
  812. }
  813. if opts.AutodetectManualMerge != nil {
  814. config.AutodetectManualMerge = *opts.AutodetectManualMerge
  815. }
  816. if opts.DefaultDeleteBranchAfterMerge != nil {
  817. config.DefaultDeleteBranchAfterMerge = *opts.DefaultDeleteBranchAfterMerge
  818. }
  819. if opts.DefaultMergeStyle != nil {
  820. config.DefaultMergeStyle = repo_model.MergeStyle(*opts.DefaultMergeStyle)
  821. }
  822. units = append(units, repo_model.RepoUnit{
  823. RepoID: repo.ID,
  824. Type: unit_model.TypePullRequests,
  825. Config: config,
  826. })
  827. } else if !*opts.HasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() {
  828. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
  829. }
  830. }
  831. if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() {
  832. if *opts.HasProjects {
  833. units = append(units, repo_model.RepoUnit{
  834. RepoID: repo.ID,
  835. Type: unit_model.TypeProjects,
  836. })
  837. } else {
  838. deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
  839. }
  840. }
  841. if err := models.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
  842. ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err)
  843. return err
  844. }
  845. log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name)
  846. return nil
  847. }
  848. // updateRepoArchivedState updates repo's archive state
  849. func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) error {
  850. repo := ctx.Repo.Repository
  851. // archive / un-archive
  852. if opts.Archived != nil {
  853. if repo.IsMirror {
  854. err := fmt.Errorf("repo is a mirror, cannot archive/un-archive")
  855. ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
  856. return err
  857. }
  858. if *opts.Archived {
  859. if err := models.SetArchiveRepoState(repo, *opts.Archived); err != nil {
  860. log.Error("Tried to archive a repo: %s", err)
  861. ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
  862. return err
  863. }
  864. log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  865. } else {
  866. if err := models.SetArchiveRepoState(repo, *opts.Archived); err != nil {
  867. log.Error("Tried to un-archive a repo: %s", err)
  868. ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
  869. return err
  870. }
  871. log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  872. }
  873. }
  874. return nil
  875. }
  876. // updateMirrorInterval updates the repo's mirror Interval
  877. func updateMirrorInterval(ctx *context.APIContext, opts api.EditRepoOption) error {
  878. repo := ctx.Repo.Repository
  879. if opts.MirrorInterval != nil {
  880. if !repo.IsMirror {
  881. err := fmt.Errorf("repo is not a mirror, can not change mirror interval")
  882. ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
  883. return err
  884. }
  885. mirror, err := repo_model.GetMirrorByRepoID(repo.ID)
  886. if err != nil {
  887. log.Error("Failed to get mirror: %s", err)
  888. ctx.Error(http.StatusInternalServerError, "MirrorInterval", err)
  889. return err
  890. }
  891. if interval, err := time.ParseDuration(*opts.MirrorInterval); err == nil {
  892. mirror.Interval = interval
  893. mirror.Repo = repo
  894. if err := repo_model.UpdateMirror(mirror); err != nil {
  895. log.Error("Failed to Set Mirror Interval: %s", err)
  896. ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
  897. return err
  898. }
  899. log.Trace("Repository %s/%s Mirror Interval was Updated to %s", ctx.Repo.Owner.Name, repo.Name, interval)
  900. } else {
  901. log.Error("Wrong format for MirrorInternal Sent: %s", err)
  902. ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
  903. return err
  904. }
  905. }
  906. return nil
  907. }
  908. // Delete one repository
  909. func Delete(ctx *context.APIContext) {
  910. // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
  911. // ---
  912. // summary: Delete a repository
  913. // produces:
  914. // - application/json
  915. // parameters:
  916. // - name: owner
  917. // in: path
  918. // description: owner of the repo to delete
  919. // type: string
  920. // required: true
  921. // - name: repo
  922. // in: path
  923. // description: name of the repo to delete
  924. // type: string
  925. // required: true
  926. // responses:
  927. // "204":
  928. // "$ref": "#/responses/empty"
  929. // "403":
  930. // "$ref": "#/responses/forbidden"
  931. owner := ctx.Repo.Owner
  932. repo := ctx.Repo.Repository
  933. canDelete, err := models.CanUserDelete(repo, ctx.User)
  934. if err != nil {
  935. ctx.Error(http.StatusInternalServerError, "CanUserDelete", err)
  936. return
  937. } else if !canDelete {
  938. ctx.Error(http.StatusForbidden, "", "Given user is not owner of organization.")
  939. return
  940. }
  941. if ctx.Repo.GitRepo != nil {
  942. ctx.Repo.GitRepo.Close()
  943. }
  944. if err := repo_service.DeleteRepository(ctx.User, repo, true); err != nil {
  945. ctx.Error(http.StatusInternalServerError, "DeleteRepository", err)
  946. return
  947. }
  948. log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
  949. ctx.Status(http.StatusNoContent)
  950. }
  951. // GetIssueTemplates returns the issue templates for a repository
  952. func GetIssueTemplates(ctx *context.APIContext) {
  953. // swagger:operation GET /repos/{owner}/{repo}/issue_templates repository repoGetIssueTemplates
  954. // ---
  955. // summary: Get available issue templates for a repository
  956. // produces:
  957. // - application/json
  958. // parameters:
  959. // - name: owner
  960. // in: path
  961. // description: owner of the repo
  962. // type: string
  963. // required: true
  964. // - name: repo
  965. // in: path
  966. // description: name of the repo
  967. // type: string
  968. // required: true
  969. // responses:
  970. // "200":
  971. // "$ref": "#/responses/IssueTemplates"
  972. ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch())
  973. }