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

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