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