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