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