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

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