THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

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.

911 lines
26KB

  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. "code.gitea.io/gitea/models"
  11. "code.gitea.io/gitea/modules/auth"
  12. "code.gitea.io/gitea/modules/context"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/migrations"
  15. "code.gitea.io/gitea/modules/notification"
  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/routers/api/v1/convert"
  20. )
  21. var searchOrderByMap = map[string]map[string]models.SearchOrderBy{
  22. "asc": {
  23. "alpha": models.SearchOrderByAlphabetically,
  24. "created": models.SearchOrderByOldest,
  25. "updated": models.SearchOrderByLeastUpdated,
  26. "size": models.SearchOrderBySize,
  27. "id": models.SearchOrderByID,
  28. },
  29. "desc": {
  30. "alpha": models.SearchOrderByAlphabeticallyReverse,
  31. "created": models.SearchOrderByNewest,
  32. "updated": models.SearchOrderByRecentUpdated,
  33. "size": models.SearchOrderBySizeReverse,
  34. "id": models.SearchOrderByIDReverse,
  35. },
  36. }
  37. // Search repositories via options
  38. func Search(ctx *context.APIContext) {
  39. // swagger:operation GET /repos/search repository repoSearch
  40. // ---
  41. // summary: Search for repositories
  42. // produces:
  43. // - application/json
  44. // parameters:
  45. // - name: q
  46. // in: query
  47. // description: keyword
  48. // type: string
  49. // - name: topic
  50. // in: query
  51. // description: Limit search to repositories with keyword as topic
  52. // type: boolean
  53. // - name: uid
  54. // in: query
  55. // description: search only for repos that the user with the given id owns or contributes to
  56. // type: integer
  57. // format: int64
  58. // - name: starredBy
  59. // in: query
  60. // description: search only for repos that the user with the given id has starred
  61. // type: integer
  62. // format: int64
  63. // - name: private
  64. // in: query
  65. // description: include private repositories this user has access to (defaults to true)
  66. // type: boolean
  67. // - name: page
  68. // in: query
  69. // description: page number of results to return (1-based)
  70. // type: integer
  71. // - name: limit
  72. // in: query
  73. // description: page size of results, maximum page size is 50
  74. // type: integer
  75. // - name: mode
  76. // in: query
  77. // description: type of repository to search for. Supported values are
  78. // "fork", "source", "mirror" and "collaborative"
  79. // type: string
  80. // - name: exclusive
  81. // in: query
  82. // description: if `uid` is given, search only for repos that the user owns
  83. // type: boolean
  84. // - name: sort
  85. // in: query
  86. // description: sort repos by attribute. Supported values are
  87. // "alpha", "created", "updated", "size", and "id".
  88. // Default is "alpha"
  89. // type: string
  90. // - name: order
  91. // in: query
  92. // description: sort order, either "asc" (ascending) or "desc" (descending).
  93. // Default is "asc", ignored if "sort" is not specified.
  94. // type: string
  95. // responses:
  96. // "200":
  97. // "$ref": "#/responses/SearchResults"
  98. // "422":
  99. // "$ref": "#/responses/validationError"
  100. opts := &models.SearchRepoOptions{
  101. Keyword: strings.Trim(ctx.Query("q"), " "),
  102. OwnerID: ctx.QueryInt64("uid"),
  103. Page: ctx.QueryInt("page"),
  104. PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")),
  105. TopicOnly: ctx.QueryBool("topic"),
  106. Collaborate: util.OptionalBoolNone,
  107. Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")),
  108. UserIsAdmin: ctx.IsUserSiteAdmin(),
  109. UserID: ctx.Data["SignedUserID"].(int64),
  110. StarredByID: ctx.QueryInt64("starredBy"),
  111. }
  112. if ctx.QueryBool("exclusive") {
  113. opts.Collaborate = util.OptionalBoolFalse
  114. }
  115. var mode = ctx.Query("mode")
  116. switch mode {
  117. case "source":
  118. opts.Fork = util.OptionalBoolFalse
  119. opts.Mirror = util.OptionalBoolFalse
  120. case "fork":
  121. opts.Fork = util.OptionalBoolTrue
  122. case "mirror":
  123. opts.Mirror = util.OptionalBoolTrue
  124. case "collaborative":
  125. opts.Mirror = util.OptionalBoolFalse
  126. opts.Collaborate = util.OptionalBoolTrue
  127. case "":
  128. default:
  129. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
  130. return
  131. }
  132. var sortMode = ctx.Query("sort")
  133. if len(sortMode) > 0 {
  134. var sortOrder = ctx.Query("order")
  135. if len(sortOrder) == 0 {
  136. sortOrder = "asc"
  137. }
  138. if searchModeMap, ok := searchOrderByMap[sortOrder]; ok {
  139. if orderBy, ok := searchModeMap[sortMode]; ok {
  140. opts.OrderBy = orderBy
  141. } else {
  142. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
  143. return
  144. }
  145. } else {
  146. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
  147. return
  148. }
  149. }
  150. var err error
  151. repos, count, err := models.SearchRepositoryByName(opts)
  152. if err != nil {
  153. ctx.JSON(500, api.SearchError{
  154. OK: false,
  155. Error: err.Error(),
  156. })
  157. return
  158. }
  159. results := make([]*api.Repository, len(repos))
  160. for i, repo := range repos {
  161. if err = repo.GetOwner(); err != nil {
  162. ctx.JSON(500, api.SearchError{
  163. OK: false,
  164. Error: err.Error(),
  165. })
  166. return
  167. }
  168. accessMode, err := models.AccessLevel(ctx.User, repo)
  169. if err != nil {
  170. ctx.JSON(500, api.SearchError{
  171. OK: false,
  172. Error: err.Error(),
  173. })
  174. }
  175. results[i] = repo.APIFormat(accessMode)
  176. }
  177. ctx.SetLinkHeader(int(count), setting.API.MaxResponseItems)
  178. ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", count))
  179. ctx.JSON(200, api.SearchResults{
  180. OK: true,
  181. Data: results,
  182. })
  183. }
  184. // CreateUserRepo create a repository for a user
  185. func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateRepoOption) {
  186. if opt.AutoInit && opt.Readme == "" {
  187. opt.Readme = "Default"
  188. }
  189. repo, err := models.CreateRepository(ctx.User, owner, models.CreateRepoOptions{
  190. Name: opt.Name,
  191. Description: opt.Description,
  192. Gitignores: opt.Gitignores,
  193. License: opt.License,
  194. Readme: opt.Readme,
  195. IsPrivate: opt.Private,
  196. AutoInit: opt.AutoInit,
  197. })
  198. if err != nil {
  199. if models.IsErrRepoAlreadyExist(err) {
  200. ctx.Error(409, "", "The repository with the same name already exists.")
  201. } else if models.IsErrNameReserved(err) ||
  202. models.IsErrNamePatternNotAllowed(err) {
  203. ctx.Error(422, "", err)
  204. } else {
  205. if repo != nil {
  206. if err = models.DeleteRepository(ctx.User, ctx.User.ID, repo.ID); err != nil {
  207. log.Error("DeleteRepository: %v", err)
  208. }
  209. }
  210. ctx.Error(500, "CreateRepository", err)
  211. }
  212. return
  213. }
  214. notification.NotifyCreateRepository(ctx.User, owner, repo)
  215. ctx.JSON(201, repo.APIFormat(models.AccessModeOwner))
  216. }
  217. // Create one repository of mine
  218. func Create(ctx *context.APIContext, opt api.CreateRepoOption) {
  219. // swagger:operation POST /user/repos repository user createCurrentUserRepo
  220. // ---
  221. // summary: Create a repository
  222. // consumes:
  223. // - application/json
  224. // produces:
  225. // - application/json
  226. // parameters:
  227. // - name: body
  228. // in: body
  229. // schema:
  230. // "$ref": "#/definitions/CreateRepoOption"
  231. // responses:
  232. // "201":
  233. // "$ref": "#/responses/Repository"
  234. // "409":
  235. // description: The repository with the same name already exists.
  236. // "422":
  237. // "$ref": "#/responses/validationError"
  238. if ctx.User.IsOrganization() {
  239. // Shouldn't reach this condition, but just in case.
  240. ctx.Error(422, "", "not allowed creating repository for organization")
  241. return
  242. }
  243. CreateUserRepo(ctx, ctx.User, opt)
  244. }
  245. // CreateOrgRepo create one repository of the organization
  246. func CreateOrgRepo(ctx *context.APIContext, opt api.CreateRepoOption) {
  247. // swagger:operation POST /org/{org}/repos organization createOrgRepo
  248. // ---
  249. // summary: Create a repository in an organization
  250. // consumes:
  251. // - application/json
  252. // produces:
  253. // - application/json
  254. // parameters:
  255. // - name: org
  256. // in: path
  257. // description: name of organization
  258. // type: string
  259. // required: true
  260. // - name: body
  261. // in: body
  262. // schema:
  263. // "$ref": "#/definitions/CreateRepoOption"
  264. // responses:
  265. // "201":
  266. // "$ref": "#/responses/Repository"
  267. // "422":
  268. // "$ref": "#/responses/validationError"
  269. // "403":
  270. // "$ref": "#/responses/forbidden"
  271. org, err := models.GetOrgByName(ctx.Params(":org"))
  272. if err != nil {
  273. if models.IsErrOrgNotExist(err) {
  274. ctx.Error(422, "", err)
  275. } else {
  276. ctx.Error(500, "GetOrgByName", err)
  277. }
  278. return
  279. }
  280. if !models.HasOrgVisible(org, ctx.User) {
  281. ctx.NotFound("HasOrgVisible", nil)
  282. return
  283. }
  284. if !ctx.User.IsAdmin {
  285. isOwner, err := org.IsOwnedBy(ctx.User.ID)
  286. if err != nil {
  287. ctx.ServerError("IsOwnedBy", err)
  288. return
  289. } else if !isOwner {
  290. ctx.Error(403, "", "Given user is not owner of organization.")
  291. return
  292. }
  293. }
  294. CreateUserRepo(ctx, org, opt)
  295. }
  296. // Migrate migrate remote git repository to gitea
  297. func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) {
  298. // swagger:operation POST /repos/migrate repository repoMigrate
  299. // ---
  300. // summary: Migrate a remote git repository
  301. // consumes:
  302. // - application/json
  303. // produces:
  304. // - application/json
  305. // parameters:
  306. // - name: body
  307. // in: body
  308. // schema:
  309. // "$ref": "#/definitions/MigrateRepoForm"
  310. // responses:
  311. // "201":
  312. // "$ref": "#/responses/Repository"
  313. ctxUser := ctx.User
  314. // Not equal means context user is an organization,
  315. // or is another user/organization if current user is admin.
  316. if form.UID != ctxUser.ID {
  317. org, err := models.GetUserByID(form.UID)
  318. if err != nil {
  319. if models.IsErrUserNotExist(err) {
  320. ctx.Error(422, "", err)
  321. } else {
  322. ctx.Error(500, "GetUserByID", err)
  323. }
  324. return
  325. }
  326. ctxUser = org
  327. }
  328. if ctx.HasError() {
  329. ctx.Error(422, "", ctx.GetErrMsg())
  330. return
  331. }
  332. if !ctx.User.IsAdmin {
  333. if !ctxUser.IsOrganization() && ctx.User.ID != ctxUser.ID {
  334. ctx.Error(403, "", "Given user is not an organization.")
  335. return
  336. }
  337. if ctxUser.IsOrganization() {
  338. // Check ownership of organization.
  339. isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID)
  340. if err != nil {
  341. ctx.Error(500, "IsOwnedBy", err)
  342. return
  343. } else if !isOwner {
  344. ctx.Error(403, "", "Given user is not owner of organization.")
  345. return
  346. }
  347. }
  348. }
  349. remoteAddr, err := form.ParseRemoteAddr(ctx.User)
  350. if err != nil {
  351. if models.IsErrInvalidCloneAddr(err) {
  352. addrErr := err.(models.ErrInvalidCloneAddr)
  353. switch {
  354. case addrErr.IsURLError:
  355. ctx.Error(422, "", err)
  356. case addrErr.IsPermissionDenied:
  357. ctx.Error(422, "", "You are not allowed to import local repositories.")
  358. case addrErr.IsInvalidPath:
  359. ctx.Error(422, "", "Invalid local path, it does not exist or not a directory.")
  360. default:
  361. ctx.Error(500, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error())
  362. }
  363. } else {
  364. ctx.Error(500, "ParseRemoteAddr", err)
  365. }
  366. return
  367. }
  368. var opts = migrations.MigrateOptions{
  369. RemoteURL: remoteAddr,
  370. Name: form.RepoName,
  371. Description: form.Description,
  372. Private: form.Private || setting.Repository.ForcePrivate,
  373. Mirror: form.Mirror,
  374. AuthUsername: form.AuthUsername,
  375. AuthPassword: form.AuthPassword,
  376. Wiki: form.Wiki,
  377. Issues: form.Issues,
  378. Milestones: form.Milestones,
  379. Labels: form.Labels,
  380. Comments: true,
  381. PullRequests: form.PullRequests,
  382. Releases: form.Releases,
  383. }
  384. if opts.Mirror {
  385. opts.Issues = false
  386. opts.Milestones = false
  387. opts.Labels = false
  388. opts.Comments = false
  389. opts.PullRequests = false
  390. opts.Releases = false
  391. }
  392. repo, err := migrations.MigrateRepository(ctx.User, ctxUser.Name, opts)
  393. if err == nil {
  394. notification.NotifyCreateRepository(ctx.User, ctxUser, repo)
  395. log.Trace("Repository migrated: %s/%s", ctxUser.Name, form.RepoName)
  396. ctx.JSON(201, repo.APIFormat(models.AccessModeAdmin))
  397. return
  398. }
  399. switch {
  400. case models.IsErrRepoAlreadyExist(err):
  401. ctx.Error(409, "", "The repository with the same name already exists.")
  402. case migrations.IsRateLimitError(err):
  403. ctx.Error(422, "", "Remote visit addressed rate limitation.")
  404. case migrations.IsTwoFactorAuthError(err):
  405. ctx.Error(422, "", "Remote visit required two factors authentication.")
  406. case models.IsErrReachLimitOfRepo(err):
  407. ctx.Error(422, "", fmt.Sprintf("You have already reached your limit of %d repositories.", ctxUser.MaxCreationLimit()))
  408. case models.IsErrNameReserved(err):
  409. ctx.Error(422, "", fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name))
  410. case models.IsErrNamePatternNotAllowed(err):
  411. ctx.Error(422, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern))
  412. default:
  413. err = util.URLSanitizedError(err, remoteAddr)
  414. if strings.Contains(err.Error(), "Authentication failed") ||
  415. strings.Contains(err.Error(), "Bad credentials") ||
  416. strings.Contains(err.Error(), "could not read Username") {
  417. ctx.Error(422, "", fmt.Sprintf("Authentication failed: %v.", err))
  418. } else if strings.Contains(err.Error(), "fatal:") {
  419. ctx.Error(422, "", fmt.Sprintf("Migration failed: %v.", err))
  420. } else {
  421. ctx.Error(500, "MigrateRepository", err)
  422. }
  423. }
  424. }
  425. // Get one repository
  426. func Get(ctx *context.APIContext) {
  427. // swagger:operation GET /repos/{owner}/{repo} repository repoGet
  428. // ---
  429. // summary: Get a repository
  430. // produces:
  431. // - application/json
  432. // parameters:
  433. // - name: owner
  434. // in: path
  435. // description: owner of the repo
  436. // type: string
  437. // required: true
  438. // - name: repo
  439. // in: path
  440. // description: name of the repo
  441. // type: string
  442. // required: true
  443. // responses:
  444. // "200":
  445. // "$ref": "#/responses/Repository"
  446. ctx.JSON(200, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode))
  447. }
  448. // GetByID returns a single Repository
  449. func GetByID(ctx *context.APIContext) {
  450. // swagger:operation GET /repositories/{id} repository repoGetByID
  451. // ---
  452. // summary: Get a repository by id
  453. // produces:
  454. // - application/json
  455. // parameters:
  456. // - name: id
  457. // in: path
  458. // description: id of the repo to get
  459. // type: integer
  460. // format: int64
  461. // required: true
  462. // responses:
  463. // "200":
  464. // "$ref": "#/responses/Repository"
  465. repo, err := models.GetRepositoryByID(ctx.ParamsInt64(":id"))
  466. if err != nil {
  467. if models.IsErrRepoNotExist(err) {
  468. ctx.NotFound()
  469. } else {
  470. ctx.Error(500, "GetRepositoryByID", err)
  471. }
  472. return
  473. }
  474. perm, err := models.GetUserRepoPermission(repo, ctx.User)
  475. if err != nil {
  476. ctx.Error(500, "AccessLevel", err)
  477. return
  478. } else if !perm.HasAccess() {
  479. ctx.NotFound()
  480. return
  481. }
  482. ctx.JSON(200, repo.APIFormat(perm.AccessMode))
  483. }
  484. // Edit edit repository properties
  485. func Edit(ctx *context.APIContext, opts api.EditRepoOption) {
  486. // swagger:operation PATCH /repos/{owner}/{repo} repository repoEdit
  487. // ---
  488. // summary: Edit a repository's properties. Only fields that are set will be changed.
  489. // produces:
  490. // - application/json
  491. // parameters:
  492. // - name: owner
  493. // in: path
  494. // description: owner of the repo to edit
  495. // type: string
  496. // required: true
  497. // - name: repo
  498. // in: path
  499. // description: name of the repo to edit
  500. // type: string
  501. // required: true
  502. // required: true
  503. // - name: body
  504. // in: body
  505. // description: "Properties of a repo that you can edit"
  506. // schema:
  507. // "$ref": "#/definitions/EditRepoOption"
  508. // responses:
  509. // "200":
  510. // "$ref": "#/responses/Repository"
  511. // "403":
  512. // "$ref": "#/responses/forbidden"
  513. // "422":
  514. // "$ref": "#/responses/validationError"
  515. if err := updateBasicProperties(ctx, opts); err != nil {
  516. return
  517. }
  518. if err := updateRepoUnits(ctx, opts); err != nil {
  519. return
  520. }
  521. if opts.Archived != nil {
  522. if err := updateRepoArchivedState(ctx, opts); err != nil {
  523. return
  524. }
  525. }
  526. ctx.JSON(http.StatusOK, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode))
  527. }
  528. // updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility
  529. func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) error {
  530. owner := ctx.Repo.Owner
  531. repo := ctx.Repo.Repository
  532. oldRepoName := repo.Name
  533. newRepoName := repo.Name
  534. if opts.Name != nil {
  535. newRepoName = *opts.Name
  536. }
  537. // Check if repository name has been changed and not just a case change
  538. if repo.LowerName != strings.ToLower(newRepoName) {
  539. if err := models.ChangeRepositoryName(ctx.Repo.Owner, repo.Name, newRepoName); err != nil {
  540. switch {
  541. case models.IsErrRepoAlreadyExist(err):
  542. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err)
  543. case models.IsErrNameReserved(err):
  544. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err)
  545. case models.IsErrNamePatternNotAllowed(err):
  546. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(models.ErrNamePatternNotAllowed).Pattern), err)
  547. default:
  548. ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err)
  549. }
  550. return err
  551. }
  552. err := models.NewRepoRedirect(ctx.Repo.Owner.ID, repo.ID, repo.Name, newRepoName)
  553. if err != nil {
  554. ctx.Error(http.StatusUnprocessableEntity, "NewRepoRedirect", err)
  555. return err
  556. }
  557. if err := models.RenameRepoAction(ctx.User, oldRepoName, repo); err != nil {
  558. log.Error("RenameRepoAction: %v", err)
  559. ctx.Error(http.StatusInternalServerError, "RenameRepoActions", err)
  560. return err
  561. }
  562. log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
  563. }
  564. // Update the name in the repo object for the response
  565. repo.Name = newRepoName
  566. repo.LowerName = strings.ToLower(newRepoName)
  567. if opts.Description != nil {
  568. repo.Description = *opts.Description
  569. }
  570. if opts.Website != nil {
  571. repo.Website = *opts.Website
  572. }
  573. visibilityChanged := false
  574. if opts.Private != nil {
  575. // Visibility of forked repository is forced sync with base repository.
  576. if repo.IsFork {
  577. *opts.Private = repo.BaseRepo.IsPrivate
  578. }
  579. visibilityChanged = repo.IsPrivate != *opts.Private
  580. // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
  581. if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.User.IsAdmin {
  582. err := fmt.Errorf("cannot change private repository to public")
  583. ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err)
  584. return err
  585. }
  586. repo.IsPrivate = *opts.Private
  587. }
  588. if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
  589. ctx.Error(http.StatusInternalServerError, "UpdateRepository", err)
  590. return err
  591. }
  592. log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name)
  593. return nil
  594. }
  595. // updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings
  596. func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
  597. owner := ctx.Repo.Owner
  598. repo := ctx.Repo.Repository
  599. var units []models.RepoUnit
  600. for _, tp := range models.MustRepoUnits {
  601. units = append(units, models.RepoUnit{
  602. RepoID: repo.ID,
  603. Type: tp,
  604. Config: new(models.UnitConfig),
  605. })
  606. }
  607. if opts.HasIssues == nil {
  608. // If HasIssues setting not touched, rewrite existing repo unit
  609. if unit, err := repo.GetUnit(models.UnitTypeIssues); err == nil {
  610. units = append(units, *unit)
  611. } else if unit, err := repo.GetUnit(models.UnitTypeExternalTracker); err == nil {
  612. units = append(units, *unit)
  613. }
  614. } else if *opts.HasIssues {
  615. // We don't currently allow setting individual issue settings through the API,
  616. // only can enable/disable issues, so when enabling issues,
  617. // we either get the existing config which means it was already enabled,
  618. // or create a new config since it doesn't exist.
  619. unit, err := repo.GetUnit(models.UnitTypeIssues)
  620. var config *models.IssuesConfig
  621. if err != nil {
  622. // Unit type doesn't exist so we make a new config file with default values
  623. config = &models.IssuesConfig{
  624. EnableTimetracker: true,
  625. AllowOnlyContributorsToTrackTime: true,
  626. EnableDependencies: true,
  627. }
  628. } else {
  629. config = unit.IssuesConfig()
  630. }
  631. units = append(units, models.RepoUnit{
  632. RepoID: repo.ID,
  633. Type: models.UnitTypeIssues,
  634. Config: config,
  635. })
  636. }
  637. if opts.HasWiki == nil {
  638. // If HasWiki setting not touched, rewrite existing repo unit
  639. if unit, err := repo.GetUnit(models.UnitTypeWiki); err == nil {
  640. units = append(units, *unit)
  641. } else if unit, err := repo.GetUnit(models.UnitTypeExternalWiki); err == nil {
  642. units = append(units, *unit)
  643. }
  644. } else if *opts.HasWiki {
  645. // We don't currently allow setting individual wiki settings through the API,
  646. // only can enable/disable the wiki, so when enabling the wiki,
  647. // we either get the existing config which means it was already enabled,
  648. // or create a new config since it doesn't exist.
  649. config := &models.UnitConfig{}
  650. units = append(units, models.RepoUnit{
  651. RepoID: repo.ID,
  652. Type: models.UnitTypeWiki,
  653. Config: config,
  654. })
  655. }
  656. if opts.HasPullRequests == nil {
  657. // If HasPullRequest setting not touched, rewrite existing repo unit
  658. if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil {
  659. units = append(units, *unit)
  660. }
  661. } else if *opts.HasPullRequests {
  662. // We do allow setting individual PR settings through the API, so
  663. // we get the config settings and then set them
  664. // if those settings were provided in the opts.
  665. unit, err := repo.GetUnit(models.UnitTypePullRequests)
  666. var config *models.PullRequestsConfig
  667. if err != nil {
  668. // Unit type doesn't exist so we make a new config file with default values
  669. config = &models.PullRequestsConfig{
  670. IgnoreWhitespaceConflicts: false,
  671. AllowMerge: true,
  672. AllowRebase: true,
  673. AllowRebaseMerge: true,
  674. AllowSquash: true,
  675. }
  676. } else {
  677. config = unit.PullRequestsConfig()
  678. }
  679. if opts.IgnoreWhitespaceConflicts != nil {
  680. config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
  681. }
  682. if opts.AllowMerge != nil {
  683. config.AllowMerge = *opts.AllowMerge
  684. }
  685. if opts.AllowRebase != nil {
  686. config.AllowRebase = *opts.AllowRebase
  687. }
  688. if opts.AllowRebaseMerge != nil {
  689. config.AllowRebaseMerge = *opts.AllowRebaseMerge
  690. }
  691. if opts.AllowSquash != nil {
  692. config.AllowSquash = *opts.AllowSquash
  693. }
  694. units = append(units, models.RepoUnit{
  695. RepoID: repo.ID,
  696. Type: models.UnitTypePullRequests,
  697. Config: config,
  698. })
  699. }
  700. if err := models.UpdateRepositoryUnits(repo, units); err != nil {
  701. ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err)
  702. return err
  703. }
  704. log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name)
  705. return nil
  706. }
  707. // updateRepoArchivedState updates repo's archive state
  708. func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) error {
  709. repo := ctx.Repo.Repository
  710. // archive / un-archive
  711. if opts.Archived != nil {
  712. if repo.IsMirror {
  713. err := fmt.Errorf("repo is a mirror, cannot archive/un-archive")
  714. ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
  715. return err
  716. }
  717. if *opts.Archived {
  718. if err := repo.SetArchiveRepoState(*opts.Archived); err != nil {
  719. log.Error("Tried to archive a repo: %s", err)
  720. ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
  721. return err
  722. }
  723. log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  724. } else {
  725. if err := repo.SetArchiveRepoState(*opts.Archived); err != nil {
  726. log.Error("Tried to un-archive a repo: %s", err)
  727. ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
  728. return err
  729. }
  730. log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  731. }
  732. }
  733. return nil
  734. }
  735. // Delete one repository
  736. func Delete(ctx *context.APIContext) {
  737. // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
  738. // ---
  739. // summary: Delete a repository
  740. // produces:
  741. // - application/json
  742. // parameters:
  743. // - name: owner
  744. // in: path
  745. // description: owner of the repo to delete
  746. // type: string
  747. // required: true
  748. // - name: repo
  749. // in: path
  750. // description: name of the repo to delete
  751. // type: string
  752. // required: true
  753. // responses:
  754. // "204":
  755. // "$ref": "#/responses/empty"
  756. // "403":
  757. // "$ref": "#/responses/forbidden"
  758. owner := ctx.Repo.Owner
  759. repo := ctx.Repo.Repository
  760. if owner.IsOrganization() && !ctx.User.IsAdmin {
  761. isOwner, err := owner.IsOwnedBy(ctx.User.ID)
  762. if err != nil {
  763. ctx.Error(500, "IsOwnedBy", err)
  764. return
  765. } else if !isOwner {
  766. ctx.Error(403, "", "Given user is not owner of organization.")
  767. return
  768. }
  769. }
  770. if err := models.DeleteRepository(ctx.User, owner.ID, repo.ID); err != nil {
  771. ctx.Error(500, "DeleteRepository", err)
  772. return
  773. }
  774. log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
  775. ctx.Status(204)
  776. }
  777. // MirrorSync adds a mirrored repository to the sync queue
  778. func MirrorSync(ctx *context.APIContext) {
  779. // swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
  780. // ---
  781. // summary: Sync a mirrored repository
  782. // produces:
  783. // - application/json
  784. // parameters:
  785. // - name: owner
  786. // in: path
  787. // description: owner of the repo to sync
  788. // type: string
  789. // required: true
  790. // - name: repo
  791. // in: path
  792. // description: name of the repo to sync
  793. // type: string
  794. // required: true
  795. // responses:
  796. // "200":
  797. // "$ref": "#/responses/empty"
  798. repo := ctx.Repo.Repository
  799. if !ctx.Repo.CanWrite(models.UnitTypeCode) {
  800. ctx.Error(403, "MirrorSync", "Must have write access")
  801. }
  802. go models.MirrorQueue.Add(repo.ID)
  803. ctx.Status(200)
  804. }
  805. // TopicSearch search for creating topic
  806. func TopicSearch(ctx *context.Context) {
  807. // swagger:operation GET /topics/search repository topicSearch
  808. // ---
  809. // summary: search topics via keyword
  810. // produces:
  811. // - application/json
  812. // parameters:
  813. // - name: q
  814. // in: query
  815. // description: keywords to search
  816. // required: true
  817. // type: string
  818. // responses:
  819. // "200":
  820. // "$ref": "#/responses/Repository"
  821. if ctx.User == nil {
  822. ctx.JSON(403, map[string]interface{}{
  823. "message": "Only owners could change the topics.",
  824. })
  825. return
  826. }
  827. kw := ctx.Query("q")
  828. topics, err := models.FindTopics(&models.FindTopicOptions{
  829. Keyword: kw,
  830. Limit: 10,
  831. })
  832. if err != nil {
  833. log.Error("SearchTopics failed: %v", err)
  834. ctx.JSON(500, map[string]interface{}{
  835. "message": "Search topics failed.",
  836. })
  837. return
  838. }
  839. ctx.JSON(200, map[string]interface{}{
  840. "topics": topics,
  841. })
  842. }