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

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