You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

repo.go 16 kB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
10 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
10 years ago
10 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "fmt"
  8. "net/http"
  9. "strings"
  10. "code.gitea.io/gitea/models"
  11. "code.gitea.io/gitea/modules/auth"
  12. "code.gitea.io/gitea/modules/context"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/setting"
  15. "code.gitea.io/gitea/modules/util"
  16. "code.gitea.io/gitea/routers/api/v1/convert"
  17. api "code.gitea.io/sdk/gitea"
  18. )
  19. var searchOrderByMap = map[string]map[string]models.SearchOrderBy{
  20. "asc": {
  21. "alpha": models.SearchOrderByAlphabetically,
  22. "created": models.SearchOrderByOldest,
  23. "updated": models.SearchOrderByLeastUpdated,
  24. "size": models.SearchOrderBySize,
  25. "id": models.SearchOrderByID,
  26. },
  27. "desc": {
  28. "alpha": models.SearchOrderByAlphabeticallyReverse,
  29. "created": models.SearchOrderByNewest,
  30. "updated": models.SearchOrderByRecentUpdated,
  31. "size": models.SearchOrderBySizeReverse,
  32. "id": models.SearchOrderByIDReverse,
  33. },
  34. }
  35. // Search repositories via options
  36. func Search(ctx *context.APIContext) {
  37. // swagger:operation GET /repos/search repository repoSearch
  38. // ---
  39. // summary: Search for repositories
  40. // produces:
  41. // - application/json
  42. // parameters:
  43. // - name: q
  44. // in: query
  45. // description: keyword
  46. // type: string
  47. // - name: uid
  48. // in: query
  49. // description: search only for repos that the user with the given id owns or contributes to
  50. // type: integer
  51. // format: int64
  52. // - name: page
  53. // in: query
  54. // description: page number of results to return (1-based)
  55. // type: integer
  56. // - name: limit
  57. // in: query
  58. // description: page size of results, maximum page size is 50
  59. // type: integer
  60. // - name: mode
  61. // in: query
  62. // description: type of repository to search for. Supported values are
  63. // "fork", "source", "mirror" and "collaborative"
  64. // type: string
  65. // - name: exclusive
  66. // in: query
  67. // description: if `uid` is given, search only for repos that the user owns
  68. // type: boolean
  69. // - name: sort
  70. // in: query
  71. // description: sort repos by attribute. Supported values are
  72. // "alpha", "created", "updated", "size", and "id".
  73. // Default is "alpha"
  74. // type: string
  75. // - name: order
  76. // in: query
  77. // description: sort order, either "asc" (ascending) or "desc" (descending).
  78. // Default is "asc", ignored if "sort" is not specified.
  79. // type: string
  80. // responses:
  81. // "200":
  82. // "$ref": "#/responses/SearchResults"
  83. // "422":
  84. // "$ref": "#/responses/validationError"
  85. opts := &models.SearchRepoOptions{
  86. Keyword: strings.Trim(ctx.Query("q"), " "),
  87. OwnerID: ctx.QueryInt64("uid"),
  88. Page: ctx.QueryInt("page"),
  89. PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")),
  90. TopicOnly: ctx.QueryBool("topic"),
  91. Collaborate: util.OptionalBoolNone,
  92. }
  93. if ctx.QueryBool("exclusive") {
  94. opts.Collaborate = util.OptionalBoolFalse
  95. }
  96. var mode = ctx.Query("mode")
  97. switch mode {
  98. case "source":
  99. opts.Fork = util.OptionalBoolFalse
  100. opts.Mirror = util.OptionalBoolFalse
  101. case "fork":
  102. opts.Fork = util.OptionalBoolTrue
  103. case "mirror":
  104. opts.Mirror = util.OptionalBoolTrue
  105. case "collaborative":
  106. opts.Mirror = util.OptionalBoolFalse
  107. opts.Collaborate = util.OptionalBoolTrue
  108. case "":
  109. default:
  110. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
  111. return
  112. }
  113. var sortMode = ctx.Query("sort")
  114. if len(sortMode) > 0 {
  115. var sortOrder = ctx.Query("order")
  116. if len(sortOrder) == 0 {
  117. sortOrder = "asc"
  118. }
  119. if searchModeMap, ok := searchOrderByMap[sortOrder]; ok {
  120. if orderBy, ok := searchModeMap[sortMode]; ok {
  121. opts.OrderBy = orderBy
  122. } else {
  123. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
  124. return
  125. }
  126. } else {
  127. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
  128. return
  129. }
  130. }
  131. var err error
  132. if opts.OwnerID > 0 {
  133. var repoOwner *models.User
  134. if ctx.User != nil && ctx.User.ID == opts.OwnerID {
  135. repoOwner = ctx.User
  136. } else {
  137. repoOwner, err = models.GetUserByID(opts.OwnerID)
  138. if err != nil {
  139. ctx.JSON(500, api.SearchError{
  140. OK: false,
  141. Error: err.Error(),
  142. })
  143. return
  144. }
  145. }
  146. if repoOwner.IsOrganization() {
  147. opts.Collaborate = util.OptionalBoolFalse
  148. }
  149. // Check visibility.
  150. if ctx.IsSigned {
  151. if ctx.User.ID == repoOwner.ID {
  152. opts.Private = true
  153. } else if repoOwner.IsOrganization() {
  154. opts.Private, err = repoOwner.IsOwnedBy(ctx.User.ID)
  155. if err != nil {
  156. ctx.JSON(500, api.SearchError{
  157. OK: false,
  158. Error: err.Error(),
  159. })
  160. return
  161. }
  162. }
  163. }
  164. }
  165. repos, count, err := models.SearchRepositoryByName(opts)
  166. if err != nil {
  167. ctx.JSON(500, api.SearchError{
  168. OK: false,
  169. Error: err.Error(),
  170. })
  171. return
  172. }
  173. results := make([]*api.Repository, len(repos))
  174. for i, repo := range repos {
  175. if err = repo.GetOwner(); err != nil {
  176. ctx.JSON(500, api.SearchError{
  177. OK: false,
  178. Error: err.Error(),
  179. })
  180. return
  181. }
  182. accessMode, err := models.AccessLevel(ctx.User, repo)
  183. if err != nil {
  184. ctx.JSON(500, api.SearchError{
  185. OK: false,
  186. Error: err.Error(),
  187. })
  188. }
  189. results[i] = repo.APIFormat(accessMode)
  190. }
  191. ctx.SetLinkHeader(int(count), setting.API.MaxResponseItems)
  192. ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", count))
  193. ctx.JSON(200, api.SearchResults{
  194. OK: true,
  195. Data: results,
  196. })
  197. }
  198. // CreateUserRepo create a repository for a user
  199. func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateRepoOption) {
  200. if opt.AutoInit && opt.Readme == "" {
  201. opt.Readme = "Default"
  202. }
  203. repo, err := models.CreateRepository(ctx.User, owner, models.CreateRepoOptions{
  204. Name: opt.Name,
  205. Description: opt.Description,
  206. Gitignores: opt.Gitignores,
  207. License: opt.License,
  208. Readme: opt.Readme,
  209. IsPrivate: opt.Private,
  210. AutoInit: opt.AutoInit,
  211. })
  212. if err != nil {
  213. if models.IsErrRepoAlreadyExist(err) {
  214. ctx.Error(409, "", "The repository with the same name already exists.")
  215. } else if models.IsErrNameReserved(err) ||
  216. models.IsErrNamePatternNotAllowed(err) {
  217. ctx.Error(422, "", err)
  218. } else {
  219. if repo != nil {
  220. if err = models.DeleteRepository(ctx.User, ctx.User.ID, repo.ID); err != nil {
  221. log.Error(4, "DeleteRepository: %v", err)
  222. }
  223. }
  224. ctx.Error(500, "CreateRepository", err)
  225. }
  226. return
  227. }
  228. ctx.JSON(201, repo.APIFormat(models.AccessModeOwner))
  229. }
  230. // Create one repository of mine
  231. func Create(ctx *context.APIContext, opt api.CreateRepoOption) {
  232. // swagger:operation POST /user/repos repository user createCurrentUserRepo
  233. // ---
  234. // summary: Create a repository
  235. // consumes:
  236. // - application/json
  237. // produces:
  238. // - application/json
  239. // parameters:
  240. // - name: body
  241. // in: body
  242. // schema:
  243. // "$ref": "#/definitions/CreateRepoOption"
  244. // responses:
  245. // "201":
  246. // "$ref": "#/responses/Repository"
  247. if ctx.User.IsOrganization() {
  248. // Shouldn't reach this condition, but just in case.
  249. ctx.Error(422, "", "not allowed creating repository for organization")
  250. return
  251. }
  252. CreateUserRepo(ctx, ctx.User, opt)
  253. }
  254. // CreateOrgRepo create one repository of the organization
  255. func CreateOrgRepo(ctx *context.APIContext, opt api.CreateRepoOption) {
  256. // swagger:operation POST /org/{org}/repos organization createOrgRepo
  257. // ---
  258. // summary: Create a repository in an organization
  259. // consumes:
  260. // - application/json
  261. // produces:
  262. // - application/json
  263. // parameters:
  264. // - name: org
  265. // in: path
  266. // description: name of organization
  267. // type: string
  268. // required: true
  269. // - name: body
  270. // in: body
  271. // schema:
  272. // "$ref": "#/definitions/CreateRepoOption"
  273. // responses:
  274. // "201":
  275. // "$ref": "#/responses/Repository"
  276. // "422":
  277. // "$ref": "#/responses/validationError"
  278. // "403":
  279. // "$ref": "#/responses/forbidden"
  280. org, err := models.GetOrgByName(ctx.Params(":org"))
  281. if err != nil {
  282. if models.IsErrOrgNotExist(err) {
  283. ctx.Error(422, "", err)
  284. } else {
  285. ctx.Error(500, "GetOrgByName", err)
  286. }
  287. return
  288. }
  289. if !models.HasOrgVisible(org, ctx.User) {
  290. ctx.NotFound("HasOrgVisible", nil)
  291. return
  292. }
  293. if !ctx.User.IsAdmin {
  294. isOwner, err := org.IsOwnedBy(ctx.User.ID)
  295. if err != nil {
  296. ctx.ServerError("IsOwnedBy", err)
  297. return
  298. } else if !isOwner {
  299. ctx.Error(403, "", "Given user is not owner of organization.")
  300. return
  301. }
  302. }
  303. CreateUserRepo(ctx, org, opt)
  304. }
  305. // Migrate migrate remote git repository to gitea
  306. func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) {
  307. // swagger:operation POST /repos/migrate repository repoMigrate
  308. // ---
  309. // summary: Migrate a remote git repository
  310. // consumes:
  311. // - application/json
  312. // produces:
  313. // - application/json
  314. // parameters:
  315. // - name: body
  316. // in: body
  317. // schema:
  318. // "$ref": "#/definitions/MigrateRepoForm"
  319. // responses:
  320. // "201":
  321. // "$ref": "#/responses/Repository"
  322. ctxUser := ctx.User
  323. // Not equal means context user is an organization,
  324. // or is another user/organization if current user is admin.
  325. if form.UID != ctxUser.ID {
  326. org, err := models.GetUserByID(form.UID)
  327. if err != nil {
  328. if models.IsErrUserNotExist(err) {
  329. ctx.Error(422, "", err)
  330. } else {
  331. ctx.Error(500, "GetUserByID", err)
  332. }
  333. return
  334. }
  335. ctxUser = org
  336. }
  337. if ctx.HasError() {
  338. ctx.Error(422, "", ctx.GetErrMsg())
  339. return
  340. }
  341. if !ctx.User.IsAdmin {
  342. if !ctxUser.IsOrganization() && ctx.User.ID != ctxUser.ID {
  343. ctx.Error(403, "", "Given user is not an organization.")
  344. return
  345. }
  346. if ctxUser.IsOrganization() {
  347. // Check ownership of organization.
  348. isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID)
  349. if err != nil {
  350. ctx.Error(500, "IsOwnedBy", err)
  351. return
  352. } else if !isOwner {
  353. ctx.Error(403, "", "Given user is not owner of organization.")
  354. return
  355. }
  356. }
  357. }
  358. remoteAddr, err := form.ParseRemoteAddr(ctx.User)
  359. if err != nil {
  360. if models.IsErrInvalidCloneAddr(err) {
  361. addrErr := err.(models.ErrInvalidCloneAddr)
  362. switch {
  363. case addrErr.IsURLError:
  364. ctx.Error(422, "", err)
  365. case addrErr.IsPermissionDenied:
  366. ctx.Error(422, "", "You are not allowed to import local repositories.")
  367. case addrErr.IsInvalidPath:
  368. ctx.Error(422, "", "Invalid local path, it does not exist or not a directory.")
  369. default:
  370. ctx.Error(500, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error())
  371. }
  372. } else {
  373. ctx.Error(500, "ParseRemoteAddr", err)
  374. }
  375. return
  376. }
  377. repo, err := models.MigrateRepository(ctx.User, ctxUser, models.MigrateRepoOptions{
  378. Name: form.RepoName,
  379. Description: form.Description,
  380. IsPrivate: form.Private || setting.Repository.ForcePrivate,
  381. IsMirror: form.Mirror,
  382. RemoteAddr: remoteAddr,
  383. })
  384. if err != nil {
  385. if models.IsErrRepoAlreadyExist(err) {
  386. ctx.Error(409, "", "The repository with the same name already exists.")
  387. return
  388. }
  389. err = util.URLSanitizedError(err, remoteAddr)
  390. if repo != nil {
  391. if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil {
  392. log.Error(4, "DeleteRepository: %v", errDelete)
  393. }
  394. }
  395. ctx.Error(500, "MigrateRepository", err)
  396. return
  397. }
  398. log.Trace("Repository migrated: %s/%s", ctxUser.Name, form.RepoName)
  399. ctx.JSON(201, repo.APIFormat(models.AccessModeAdmin))
  400. }
  401. // Get one repository
  402. func Get(ctx *context.APIContext) {
  403. // swagger:operation GET /repos/{owner}/{repo} repository repoGet
  404. // ---
  405. // summary: Get a repository
  406. // produces:
  407. // - application/json
  408. // parameters:
  409. // - name: owner
  410. // in: path
  411. // description: owner of the repo
  412. // type: string
  413. // required: true
  414. // - name: repo
  415. // in: path
  416. // description: name of the repo
  417. // type: string
  418. // required: true
  419. // responses:
  420. // "200":
  421. // "$ref": "#/responses/Repository"
  422. ctx.JSON(200, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode))
  423. }
  424. // GetByID returns a single Repository
  425. func GetByID(ctx *context.APIContext) {
  426. // swagger:operation GET /repositories/{id} repository repoGetByID
  427. // ---
  428. // summary: Get a repository by id
  429. // produces:
  430. // - application/json
  431. // parameters:
  432. // - name: id
  433. // in: path
  434. // description: id of the repo to get
  435. // type: integer
  436. // format: int64
  437. // required: true
  438. // responses:
  439. // "200":
  440. // "$ref": "#/responses/Repository"
  441. repo, err := models.GetRepositoryByID(ctx.ParamsInt64(":id"))
  442. if err != nil {
  443. if models.IsErrRepoNotExist(err) {
  444. ctx.Status(404)
  445. } else {
  446. ctx.Error(500, "GetRepositoryByID", err)
  447. }
  448. return
  449. }
  450. perm, err := models.GetUserRepoPermission(repo, ctx.User)
  451. if err != nil {
  452. ctx.Error(500, "AccessLevel", err)
  453. return
  454. } else if !perm.HasAccess() {
  455. ctx.Status(404)
  456. return
  457. }
  458. ctx.JSON(200, repo.APIFormat(perm.AccessMode))
  459. }
  460. // Delete one repository
  461. func Delete(ctx *context.APIContext) {
  462. // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
  463. // ---
  464. // summary: Delete a repository
  465. // produces:
  466. // - application/json
  467. // parameters:
  468. // - name: owner
  469. // in: path
  470. // description: owner of the repo to delete
  471. // type: string
  472. // required: true
  473. // - name: repo
  474. // in: path
  475. // description: name of the repo to delete
  476. // type: string
  477. // required: true
  478. // responses:
  479. // "204":
  480. // "$ref": "#/responses/empty"
  481. // "403":
  482. // "$ref": "#/responses/forbidden"
  483. owner := ctx.Repo.Owner
  484. repo := ctx.Repo.Repository
  485. if owner.IsOrganization() && !ctx.User.IsAdmin {
  486. isOwner, err := owner.IsOwnedBy(ctx.User.ID)
  487. if err != nil {
  488. ctx.Error(500, "IsOwnedBy", err)
  489. return
  490. } else if !isOwner {
  491. ctx.Error(403, "", "Given user is not owner of organization.")
  492. return
  493. }
  494. }
  495. if err := models.DeleteRepository(ctx.User, owner.ID, repo.ID); err != nil {
  496. ctx.Error(500, "DeleteRepository", err)
  497. return
  498. }
  499. log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
  500. ctx.Status(204)
  501. }
  502. // MirrorSync adds a mirrored repository to the sync queue
  503. func MirrorSync(ctx *context.APIContext) {
  504. // swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
  505. // ---
  506. // summary: Sync a mirrored repository
  507. // produces:
  508. // - application/json
  509. // parameters:
  510. // - name: owner
  511. // in: path
  512. // description: owner of the repo to sync
  513. // type: string
  514. // required: true
  515. // - name: repo
  516. // in: path
  517. // description: name of the repo to sync
  518. // type: string
  519. // required: true
  520. // responses:
  521. // "200":
  522. // "$ref": "#/responses/empty"
  523. repo := ctx.Repo.Repository
  524. if !ctx.Repo.CanWrite(models.UnitTypeCode) {
  525. ctx.Error(403, "MirrorSync", "Must have write access")
  526. }
  527. go models.MirrorQueue.Add(repo.ID)
  528. ctx.Status(200)
  529. }
  530. // TopicSearch search for creating topic
  531. func TopicSearch(ctx *context.Context) {
  532. // swagger:operation GET /topics/search repository topicSearch
  533. // ---
  534. // summary: search topics via keyword
  535. // produces:
  536. // - application/json
  537. // parameters:
  538. // - name: q
  539. // in: query
  540. // description: keywords to search
  541. // required: true
  542. // type: string
  543. // responses:
  544. // "200":
  545. // "$ref": "#/responses/Repository"
  546. if ctx.User == nil {
  547. ctx.JSON(403, map[string]interface{}{
  548. "message": "Only owners could change the topics.",
  549. })
  550. return
  551. }
  552. kw := ctx.Query("q")
  553. topics, err := models.FindTopics(&models.FindTopicOptions{
  554. Keyword: kw,
  555. Limit: 10,
  556. })
  557. if err != nil {
  558. log.Error(2, "SearchTopics failed: %v", err)
  559. ctx.JSON(500, map[string]interface{}{
  560. "message": "Search topics failed.",
  561. })
  562. return
  563. }
  564. ctx.JSON(200, map[string]interface{}{
  565. "topics": topics,
  566. })
  567. }