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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  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. models.IsErrNameReserved(err) ||
  215. models.IsErrNamePatternNotAllowed(err) {
  216. ctx.Error(422, "", err)
  217. } else {
  218. if repo != nil {
  219. if err = models.DeleteRepository(ctx.User, ctx.User.ID, repo.ID); err != nil {
  220. log.Error(4, "DeleteRepository: %v", err)
  221. }
  222. }
  223. ctx.Error(500, "CreateRepository", err)
  224. }
  225. return
  226. }
  227. ctx.JSON(201, repo.APIFormat(models.AccessModeOwner))
  228. }
  229. // Create one repository of mine
  230. func Create(ctx *context.APIContext, opt api.CreateRepoOption) {
  231. // swagger:operation POST /user/repos repository user createCurrentUserRepo
  232. // ---
  233. // summary: Create a repository
  234. // consumes:
  235. // - application/json
  236. // produces:
  237. // - application/json
  238. // parameters:
  239. // - name: body
  240. // in: body
  241. // schema:
  242. // "$ref": "#/definitions/CreateRepoOption"
  243. // responses:
  244. // "201":
  245. // "$ref": "#/responses/Repository"
  246. if ctx.User.IsOrganization() {
  247. // Shouldn't reach this condition, but just in case.
  248. ctx.Error(422, "", "not allowed creating repository for organization")
  249. return
  250. }
  251. CreateUserRepo(ctx, ctx.User, opt)
  252. }
  253. // CreateOrgRepo create one repository of the organization
  254. func CreateOrgRepo(ctx *context.APIContext, opt api.CreateRepoOption) {
  255. // swagger:operation POST /org/{org}/repos organization createOrgRepo
  256. // ---
  257. // summary: Create a repository in an organization
  258. // consumes:
  259. // - application/json
  260. // produces:
  261. // - application/json
  262. // parameters:
  263. // - name: org
  264. // in: path
  265. // description: name of organization
  266. // type: string
  267. // required: true
  268. // - name: body
  269. // in: body
  270. // schema:
  271. // "$ref": "#/definitions/CreateRepoOption"
  272. // responses:
  273. // "201":
  274. // "$ref": "#/responses/Repository"
  275. // "422":
  276. // "$ref": "#/responses/validationError"
  277. // "403":
  278. // "$ref": "#/responses/forbidden"
  279. org, err := models.GetOrgByName(ctx.Params(":org"))
  280. if err != nil {
  281. if models.IsErrOrgNotExist(err) {
  282. ctx.Error(422, "", err)
  283. } else {
  284. ctx.Error(500, "GetOrgByName", err)
  285. }
  286. return
  287. }
  288. if !models.HasOrgVisible(org, ctx.User) {
  289. ctx.NotFound("HasOrgVisible", nil)
  290. return
  291. }
  292. if !ctx.User.IsAdmin {
  293. isOwner, err := org.IsOwnedBy(ctx.User.ID)
  294. if err != nil {
  295. ctx.ServerError("IsOwnedBy", err)
  296. return
  297. } else if !isOwner {
  298. ctx.Error(403, "", "Given user is not owner of organization.")
  299. return
  300. }
  301. }
  302. CreateUserRepo(ctx, org, opt)
  303. }
  304. // Migrate migrate remote git repository to gitea
  305. func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) {
  306. // swagger:operation POST /repos/migrate repository repoMigrate
  307. // ---
  308. // summary: Migrate a remote git repository
  309. // consumes:
  310. // - application/json
  311. // produces:
  312. // - application/json
  313. // parameters:
  314. // - name: body
  315. // in: body
  316. // schema:
  317. // "$ref": "#/definitions/MigrateRepoForm"
  318. // responses:
  319. // "201":
  320. // "$ref": "#/responses/Repository"
  321. ctxUser := ctx.User
  322. // Not equal means context user is an organization,
  323. // or is another user/organization if current user is admin.
  324. if form.UID != ctxUser.ID {
  325. org, err := models.GetUserByID(form.UID)
  326. if err != nil {
  327. if models.IsErrUserNotExist(err) {
  328. ctx.Error(422, "", err)
  329. } else {
  330. ctx.Error(500, "GetUserByID", err)
  331. }
  332. return
  333. }
  334. ctxUser = org
  335. }
  336. if ctx.HasError() {
  337. ctx.Error(422, "", ctx.GetErrMsg())
  338. return
  339. }
  340. if !ctx.User.IsAdmin {
  341. if !ctxUser.IsOrganization() && ctx.User.ID != ctxUser.ID {
  342. ctx.Error(403, "", "Given user is not an organization.")
  343. return
  344. }
  345. if ctxUser.IsOrganization() {
  346. // Check ownership of organization.
  347. isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID)
  348. if err != nil {
  349. ctx.Error(500, "IsOwnedBy", err)
  350. return
  351. } else if !isOwner {
  352. ctx.Error(403, "", "Given user is not owner of organization.")
  353. return
  354. }
  355. }
  356. }
  357. remoteAddr, err := form.ParseRemoteAddr(ctx.User)
  358. if err != nil {
  359. if models.IsErrInvalidCloneAddr(err) {
  360. addrErr := err.(models.ErrInvalidCloneAddr)
  361. switch {
  362. case addrErr.IsURLError:
  363. ctx.Error(422, "", err)
  364. case addrErr.IsPermissionDenied:
  365. ctx.Error(422, "", "You are not allowed to import local repositories.")
  366. case addrErr.IsInvalidPath:
  367. ctx.Error(422, "", "Invalid local path, it does not exist or not a directory.")
  368. default:
  369. ctx.Error(500, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error())
  370. }
  371. } else {
  372. ctx.Error(500, "ParseRemoteAddr", err)
  373. }
  374. return
  375. }
  376. repo, err := models.MigrateRepository(ctx.User, ctxUser, models.MigrateRepoOptions{
  377. Name: form.RepoName,
  378. Description: form.Description,
  379. IsPrivate: form.Private || setting.Repository.ForcePrivate,
  380. IsMirror: form.Mirror,
  381. RemoteAddr: remoteAddr,
  382. })
  383. if err != nil {
  384. if models.IsErrRepoAlreadyExist(err) {
  385. ctx.Error(409, "", "The repository with the same name already exists.")
  386. return
  387. }
  388. err = util.URLSanitizedError(err, remoteAddr)
  389. if repo != nil {
  390. if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil {
  391. log.Error(4, "DeleteRepository: %v", errDelete)
  392. }
  393. }
  394. ctx.Error(500, "MigrateRepository", err)
  395. return
  396. }
  397. log.Trace("Repository migrated: %s/%s", ctxUser.Name, form.RepoName)
  398. ctx.JSON(201, repo.APIFormat(models.AccessModeAdmin))
  399. }
  400. // Get one repository
  401. func Get(ctx *context.APIContext) {
  402. // swagger:operation GET /repos/{owner}/{repo} repository repoGet
  403. // ---
  404. // summary: Get a repository
  405. // produces:
  406. // - application/json
  407. // parameters:
  408. // - name: owner
  409. // in: path
  410. // description: owner of the repo
  411. // type: string
  412. // required: true
  413. // - name: repo
  414. // in: path
  415. // description: name of the repo
  416. // type: string
  417. // required: true
  418. // responses:
  419. // "200":
  420. // "$ref": "#/responses/Repository"
  421. ctx.JSON(200, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode))
  422. }
  423. // GetByID returns a single Repository
  424. func GetByID(ctx *context.APIContext) {
  425. // swagger:operation GET /repositories/{id} repository repoGetByID
  426. // ---
  427. // summary: Get a repository by id
  428. // produces:
  429. // - application/json
  430. // parameters:
  431. // - name: id
  432. // in: path
  433. // description: id of the repo to get
  434. // type: integer
  435. // format: int64
  436. // required: true
  437. // responses:
  438. // "200":
  439. // "$ref": "#/responses/Repository"
  440. repo, err := models.GetRepositoryByID(ctx.ParamsInt64(":id"))
  441. if err != nil {
  442. if models.IsErrRepoNotExist(err) {
  443. ctx.Status(404)
  444. } else {
  445. ctx.Error(500, "GetRepositoryByID", err)
  446. }
  447. return
  448. }
  449. perm, err := models.GetUserRepoPermission(repo, ctx.User)
  450. if err != nil {
  451. ctx.Error(500, "AccessLevel", err)
  452. return
  453. } else if !perm.HasAccess() {
  454. ctx.Status(404)
  455. return
  456. }
  457. ctx.JSON(200, repo.APIFormat(perm.AccessMode))
  458. }
  459. // Delete one repository
  460. func Delete(ctx *context.APIContext) {
  461. // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
  462. // ---
  463. // summary: Delete a repository
  464. // produces:
  465. // - application/json
  466. // parameters:
  467. // - name: owner
  468. // in: path
  469. // description: owner of the repo to delete
  470. // type: string
  471. // required: true
  472. // - name: repo
  473. // in: path
  474. // description: name of the repo to delete
  475. // type: string
  476. // required: true
  477. // responses:
  478. // "204":
  479. // "$ref": "#/responses/empty"
  480. // "403":
  481. // "$ref": "#/responses/forbidden"
  482. owner := ctx.Repo.Owner
  483. repo := ctx.Repo.Repository
  484. if owner.IsOrganization() && !ctx.User.IsAdmin {
  485. isOwner, err := owner.IsOwnedBy(ctx.User.ID)
  486. if err != nil {
  487. ctx.Error(500, "IsOwnedBy", err)
  488. return
  489. } else if !isOwner {
  490. ctx.Error(403, "", "Given user is not owner of organization.")
  491. return
  492. }
  493. }
  494. if err := models.DeleteRepository(ctx.User, owner.ID, repo.ID); err != nil {
  495. ctx.Error(500, "DeleteRepository", err)
  496. return
  497. }
  498. log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
  499. ctx.Status(204)
  500. }
  501. // MirrorSync adds a mirrored repository to the sync queue
  502. func MirrorSync(ctx *context.APIContext) {
  503. // swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
  504. // ---
  505. // summary: Sync a mirrored repository
  506. // produces:
  507. // - application/json
  508. // parameters:
  509. // - name: owner
  510. // in: path
  511. // description: owner of the repo to sync
  512. // type: string
  513. // required: true
  514. // - name: repo
  515. // in: path
  516. // description: name of the repo to sync
  517. // type: string
  518. // required: true
  519. // responses:
  520. // "200":
  521. // "$ref": "#/responses/empty"
  522. repo := ctx.Repo.Repository
  523. if !ctx.Repo.CanWrite(models.UnitTypeCode) {
  524. ctx.Error(403, "MirrorSync", "Must have write access")
  525. }
  526. go models.MirrorQueue.Add(repo.ID)
  527. ctx.Status(200)
  528. }
  529. // TopicSearch search for creating topic
  530. func TopicSearch(ctx *context.Context) {
  531. // swagger:operation GET /topics/search repository topicSearch
  532. // ---
  533. // summary: search topics via keyword
  534. // produces:
  535. // - application/json
  536. // parameters:
  537. // - name: q
  538. // in: query
  539. // description: keywords to search
  540. // required: true
  541. // type: string
  542. // responses:
  543. // "200":
  544. // "$ref": "#/responses/Repository"
  545. if ctx.User == nil {
  546. ctx.JSON(403, map[string]interface{}{
  547. "message": "Only owners could change the topics.",
  548. })
  549. return
  550. }
  551. kw := ctx.Query("q")
  552. topics, err := models.FindTopics(&models.FindTopicOptions{
  553. Keyword: kw,
  554. Limit: 10,
  555. })
  556. if err != nil {
  557. log.Error(2, "SearchTopics failed: %v", err)
  558. ctx.JSON(500, map[string]interface{}{
  559. "message": "Search topics failed.",
  560. })
  561. return
  562. }
  563. ctx.JSON(200, map[string]interface{}{
  564. "topics": topics,
  565. })
  566. }