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

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