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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  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. // format: int64
  51. // - name: page
  52. // in: query
  53. // description: page number of results to return (1-based)
  54. // type: integer
  55. // - name: limit
  56. // in: query
  57. // description: page size of results, maximum page size is 50
  58. // type: integer
  59. // - name: mode
  60. // in: query
  61. // description: type of repository to search for. Supported values are
  62. // "fork", "source", "mirror" and "collaborative"
  63. // type: string
  64. // - name: exclusive
  65. // in: query
  66. // description: if `uid` is given, search only for repos that the user owns
  67. // type: boolean
  68. // - name: sort
  69. // in: query
  70. // description: sort repos by attribute. Supported values are
  71. // "alpha", "created", "updated", "size", and "id".
  72. // Default is "alpha"
  73. // type: string
  74. // - name: order
  75. // in: query
  76. // description: sort order, either "asc" (ascending) or "desc" (descending).
  77. // Default is "asc", ignored if "sort" is not specified.
  78. // type: string
  79. // responses:
  80. // "200":
  81. // "$ref": "#/responses/SearchResults"
  82. // "422":
  83. // "$ref": "#/responses/validationError"
  84. opts := &models.SearchRepoOptions{
  85. Keyword: strings.Trim(ctx.Query("q"), " "),
  86. OwnerID: ctx.QueryInt64("uid"),
  87. Page: ctx.QueryInt("page"),
  88. PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")),
  89. TopicOnly: ctx.QueryBool("topic"),
  90. Collaborate: util.OptionalBoolNone,
  91. }
  92. if ctx.QueryBool("exclusive") {
  93. opts.Collaborate = util.OptionalBoolFalse
  94. }
  95. var mode = ctx.Query("mode")
  96. switch mode {
  97. case "source":
  98. opts.Fork = util.OptionalBoolFalse
  99. opts.Mirror = util.OptionalBoolFalse
  100. case "fork":
  101. opts.Fork = util.OptionalBoolTrue
  102. case "mirror":
  103. opts.Mirror = util.OptionalBoolTrue
  104. case "collaborative":
  105. opts.Mirror = util.OptionalBoolFalse
  106. opts.Collaborate = util.OptionalBoolTrue
  107. case "":
  108. default:
  109. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
  110. return
  111. }
  112. var sortMode = ctx.Query("sort")
  113. if len(sortMode) > 0 {
  114. var sortOrder = ctx.Query("order")
  115. if len(sortOrder) == 0 {
  116. sortOrder = "asc"
  117. }
  118. if searchModeMap, ok := searchOrderByMap[sortOrder]; ok {
  119. if orderBy, ok := searchModeMap[sortMode]; ok {
  120. opts.OrderBy = orderBy
  121. } else {
  122. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
  123. return
  124. }
  125. } else {
  126. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
  127. return
  128. }
  129. }
  130. var err error
  131. if opts.OwnerID > 0 {
  132. var repoOwner *models.User
  133. if ctx.User != nil && ctx.User.ID == opts.OwnerID {
  134. repoOwner = ctx.User
  135. } else {
  136. repoOwner, err = models.GetUserByID(opts.OwnerID)
  137. if err != nil {
  138. ctx.JSON(500, api.SearchError{
  139. OK: false,
  140. Error: err.Error(),
  141. })
  142. return
  143. }
  144. }
  145. if repoOwner.IsOrganization() {
  146. opts.Collaborate = util.OptionalBoolFalse
  147. }
  148. // Check visibility.
  149. if ctx.IsSigned {
  150. if ctx.User.ID == repoOwner.ID {
  151. opts.Private = true
  152. } else if repoOwner.IsOrganization() {
  153. opts.Private, err = repoOwner.IsOwnedBy(ctx.User.ID)
  154. if err != nil {
  155. ctx.JSON(500, api.SearchError{
  156. OK: false,
  157. Error: err.Error(),
  158. })
  159. return
  160. }
  161. }
  162. }
  163. }
  164. repos, count, err := models.SearchRepositoryByName(opts)
  165. if err != nil {
  166. ctx.JSON(500, api.SearchError{
  167. OK: false,
  168. Error: err.Error(),
  169. })
  170. return
  171. }
  172. var userID int64
  173. if ctx.IsSigned {
  174. userID = ctx.User.ID
  175. }
  176. results := make([]*api.Repository, len(repos))
  177. for i, repo := range repos {
  178. if err = repo.GetOwner(); err != nil {
  179. ctx.JSON(500, api.SearchError{
  180. OK: false,
  181. Error: err.Error(),
  182. })
  183. return
  184. }
  185. accessMode, err := models.AccessLevel(userID, repo)
  186. if err != nil {
  187. ctx.JSON(500, api.SearchError{
  188. OK: false,
  189. Error: err.Error(),
  190. })
  191. }
  192. results[i] = repo.APIFormat(accessMode)
  193. }
  194. ctx.SetLinkHeader(int(count), setting.API.MaxResponseItems)
  195. ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", count))
  196. ctx.JSON(200, api.SearchResults{
  197. OK: true,
  198. Data: results,
  199. })
  200. }
  201. // CreateUserRepo create a repository for a user
  202. func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateRepoOption) {
  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 !ctx.User.IsAdmin {
  289. isOwner, err := org.IsOwnedBy(ctx.User.ID)
  290. if err != nil {
  291. ctx.ServerError("IsOwnedBy", err)
  292. return
  293. } else if !isOwner {
  294. ctx.Error(403, "", "Given user is not owner of organization.")
  295. return
  296. }
  297. }
  298. CreateUserRepo(ctx, org, opt)
  299. }
  300. // Migrate migrate remote git repository to gitea
  301. func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) {
  302. // swagger:operation POST /repos/migrate repository repoMigrate
  303. // ---
  304. // summary: Migrate a remote git repository
  305. // consumes:
  306. // - application/json
  307. // produces:
  308. // - application/json
  309. // parameters:
  310. // - name: body
  311. // in: body
  312. // schema:
  313. // "$ref": "#/definitions/MigrateRepoForm"
  314. // responses:
  315. // "201":
  316. // "$ref": "#/responses/Repository"
  317. ctxUser := ctx.User
  318. // Not equal means context user is an organization,
  319. // or is another user/organization if current user is admin.
  320. if form.UID != ctxUser.ID {
  321. org, err := models.GetUserByID(form.UID)
  322. if err != nil {
  323. if models.IsErrUserNotExist(err) {
  324. ctx.Error(422, "", err)
  325. } else {
  326. ctx.Error(500, "GetUserByID", err)
  327. }
  328. return
  329. }
  330. ctxUser = org
  331. }
  332. if ctx.HasError() {
  333. ctx.Error(422, "", ctx.GetErrMsg())
  334. return
  335. }
  336. if !ctx.User.IsAdmin {
  337. if !ctxUser.IsOrganization() && ctx.User.ID != ctxUser.ID {
  338. ctx.Error(403, "", "Given user is not an organization.")
  339. return
  340. }
  341. if ctxUser.IsOrganization() {
  342. // Check ownership of organization.
  343. isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID)
  344. if err != nil {
  345. ctx.Error(500, "IsOwnedBy", err)
  346. return
  347. } else if !isOwner {
  348. ctx.Error(403, "", "Given user is not owner of organization.")
  349. return
  350. }
  351. }
  352. }
  353. remoteAddr, err := form.ParseRemoteAddr(ctx.User)
  354. if err != nil {
  355. if models.IsErrInvalidCloneAddr(err) {
  356. addrErr := err.(models.ErrInvalidCloneAddr)
  357. switch {
  358. case addrErr.IsURLError:
  359. ctx.Error(422, "", err)
  360. case addrErr.IsPermissionDenied:
  361. ctx.Error(422, "", "You are not allowed to import local repositories.")
  362. case addrErr.IsInvalidPath:
  363. ctx.Error(422, "", "Invalid local path, it does not exist or not a directory.")
  364. default:
  365. ctx.Error(500, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error())
  366. }
  367. } else {
  368. ctx.Error(500, "ParseRemoteAddr", err)
  369. }
  370. return
  371. }
  372. repo, err := models.MigrateRepository(ctx.User, ctxUser, models.MigrateRepoOptions{
  373. Name: form.RepoName,
  374. Description: form.Description,
  375. IsPrivate: form.Private || setting.Repository.ForcePrivate,
  376. IsMirror: form.Mirror,
  377. RemoteAddr: remoteAddr,
  378. })
  379. if err != nil {
  380. err = util.URLSanitizedError(err, remoteAddr)
  381. if repo != nil {
  382. if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil {
  383. log.Error(4, "DeleteRepository: %v", errDelete)
  384. }
  385. }
  386. ctx.Error(500, "MigrateRepository", err)
  387. return
  388. }
  389. log.Trace("Repository migrated: %s/%s", ctxUser.Name, form.RepoName)
  390. ctx.JSON(201, repo.APIFormat(models.AccessModeAdmin))
  391. }
  392. // Get one repository
  393. func Get(ctx *context.APIContext) {
  394. // swagger:operation GET /repos/{owner}/{repo} repository repoGet
  395. // ---
  396. // summary: Get a repository
  397. // produces:
  398. // - application/json
  399. // parameters:
  400. // - name: owner
  401. // in: path
  402. // description: owner of the repo
  403. // type: string
  404. // required: true
  405. // - name: repo
  406. // in: path
  407. // description: name of the repo
  408. // type: string
  409. // required: true
  410. // responses:
  411. // "200":
  412. // "$ref": "#/responses/Repository"
  413. ctx.JSON(200, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode))
  414. }
  415. // GetByID returns a single Repository
  416. func GetByID(ctx *context.APIContext) {
  417. // swagger:operation GET /repositories/{id} repository repoGetByID
  418. // ---
  419. // summary: Get a repository by id
  420. // produces:
  421. // - application/json
  422. // parameters:
  423. // - name: id
  424. // in: path
  425. // description: id of the repo to get
  426. // type: integer
  427. // format: int64
  428. // required: true
  429. // responses:
  430. // "200":
  431. // "$ref": "#/responses/Repository"
  432. repo, err := models.GetRepositoryByID(ctx.ParamsInt64(":id"))
  433. if err != nil {
  434. if models.IsErrRepoNotExist(err) {
  435. ctx.Status(404)
  436. } else {
  437. ctx.Error(500, "GetRepositoryByID", err)
  438. }
  439. return
  440. }
  441. access, err := models.AccessLevel(ctx.User.ID, repo)
  442. if err != nil {
  443. ctx.Error(500, "AccessLevel", err)
  444. return
  445. } else if access < models.AccessModeRead {
  446. ctx.Status(404)
  447. return
  448. }
  449. ctx.JSON(200, repo.APIFormat(access))
  450. }
  451. // Delete one repository
  452. func Delete(ctx *context.APIContext) {
  453. // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
  454. // ---
  455. // summary: Delete a repository
  456. // produces:
  457. // - application/json
  458. // parameters:
  459. // - name: owner
  460. // in: path
  461. // description: owner of the repo to delete
  462. // type: string
  463. // required: true
  464. // - name: repo
  465. // in: path
  466. // description: name of the repo to delete
  467. // type: string
  468. // required: true
  469. // responses:
  470. // "204":
  471. // "$ref": "#/responses/empty"
  472. // "403":
  473. // "$ref": "#/responses/forbidden"
  474. if !ctx.Repo.IsAdmin() {
  475. ctx.Error(403, "", "Must have admin rights")
  476. return
  477. }
  478. owner := ctx.Repo.Owner
  479. repo := ctx.Repo.Repository
  480. if owner.IsOrganization() {
  481. isOwner, err := owner.IsOwnedBy(ctx.User.ID)
  482. if err != nil {
  483. ctx.Error(500, "IsOwnedBy", err)
  484. return
  485. } else if !isOwner {
  486. ctx.Error(403, "", "Given user is not owner of organization.")
  487. return
  488. }
  489. }
  490. if err := models.DeleteRepository(ctx.User, owner.ID, repo.ID); err != nil {
  491. ctx.Error(500, "DeleteRepository", err)
  492. return
  493. }
  494. log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
  495. ctx.Status(204)
  496. }
  497. // MirrorSync adds a mirrored repository to the sync queue
  498. func MirrorSync(ctx *context.APIContext) {
  499. // swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
  500. // ---
  501. // summary: Sync a mirrored repository
  502. // produces:
  503. // - application/json
  504. // parameters:
  505. // - name: owner
  506. // in: path
  507. // description: owner of the repo to sync
  508. // type: string
  509. // required: true
  510. // - name: repo
  511. // in: path
  512. // description: name of the repo to sync
  513. // type: string
  514. // required: true
  515. // responses:
  516. // "200":
  517. // "$ref": "#/responses/empty"
  518. repo := ctx.Repo.Repository
  519. if !ctx.Repo.IsWriter() {
  520. ctx.Error(403, "MirrorSync", "Must have write access")
  521. }
  522. go models.MirrorQueue.Add(repo.ID)
  523. ctx.Status(200)
  524. }
  525. // TopicSearch search for creating topic
  526. func TopicSearch(ctx *context.Context) {
  527. // swagger:operation GET /topics/search repository topicSearch
  528. // ---
  529. // summary: search topics via keyword
  530. // produces:
  531. // - application/json
  532. // parameters:
  533. // - name: q
  534. // in: query
  535. // description: keywords to search
  536. // required: true
  537. // type: string
  538. // responses:
  539. // "200":
  540. // "$ref": "#/responses/Repository"
  541. if ctx.User == nil {
  542. ctx.JSON(403, map[string]interface{}{
  543. "message": "Only owners could change the topics.",
  544. })
  545. return
  546. }
  547. kw := ctx.Query("q")
  548. topics, err := models.FindTopics(&models.FindTopicOptions{
  549. Keyword: kw,
  550. Limit: 10,
  551. })
  552. if err != nil {
  553. log.Error(2, "SearchTopics failed: %v", err)
  554. ctx.JSON(500, map[string]interface{}{
  555. "message": "Search topics failed.",
  556. })
  557. return
  558. }
  559. ctx.JSON(200, map[string]interface{}{
  560. "topics": topics,
  561. })
  562. }