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

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