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 28 kB

11 years ago
Better logging (#6038) (#6095) * Panic don't fatal on create new logger Fixes #5854 Signed-off-by: Andrew Thornton <art27@cantab.net> * partial broken * Update the logging infrastrcture Signed-off-by: Andrew Thornton <art27@cantab.net> * Reset the skip levels for Fatal and Error Signed-off-by: Andrew Thornton <art27@cantab.net> * broken ncsa * More log.Error fixes Signed-off-by: Andrew Thornton <art27@cantab.net> * Remove nal * set log-levels to lowercase * Make console_test test all levels * switch to lowercased levels * OK now working * Fix vetting issues * Fix lint * Fix tests * change default logging to match current gitea * Improve log testing Signed-off-by: Andrew Thornton <art27@cantab.net> * reset error skip levels to 0 * Update documentation and access logger configuration * Redirect the router log back to gitea if redirect macaron log but also allow setting the log level - i.e. TRACE * Fix broken level caching * Refactor the router log * Add Router logger * Add colorizing options * Adjust router colors * Only create logger if they will be used * update app.ini.sample * rename Attribute ColorAttribute * Change from white to green for function * Set fatal/error levels * Restore initial trace logger * Fix Trace arguments in modules/auth/auth.go * Properly handle XORMLogger * Improve admin/config page * fix fmt * Add auto-compression of old logs * Update error log levels * Remove the unnecessary skip argument from Error, Fatal and Critical * Add stacktrace support * Fix tests * Remove x/sync from vendors? * Add stderr option to console logger * Use filepath.ToSlash to protect against Windows in tests * Remove prefixed underscores from names in colors.go * Remove not implemented database logger This was removed from Gogs on 4 Mar 2016 but left in the configuration since then. * Ensure that log paths are relative to ROOT_PATH * use path.Join * rename jsonConfig to logConfig * Rename "config" to "jsonConfig" to make it clearer * Requested changes * Requested changes: XormLogger * Try to color the windows terminal If successful default to colorizing the console logs * fixup * Colorize initially too * update vendor * Colorize logs on default and remove if this is not a colorizing logger * Fix documentation * fix test * Use go-isatty to detect if on windows we are on msys or cygwin * Fix spelling mistake * Add missing vendors * More changes * Rationalise the ANSI writer protection * Adjust colors on advice from @0x5c * Make Flags a comma separated list * Move to use the windows constant for ENABLE_VIRTUAL_TERMINAL_PROCESSING * Ensure matching is done on the non-colored message - to simpify EXPRESSION
6 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
10 years ago
10 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920
  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/migrations"
  15. "code.gitea.io/gitea/modules/notification"
  16. "code.gitea.io/gitea/modules/setting"
  17. api "code.gitea.io/gitea/modules/structs"
  18. "code.gitea.io/gitea/modules/util"
  19. "code.gitea.io/gitea/modules/validation"
  20. "code.gitea.io/gitea/routers/api/v1/convert"
  21. mirror_service "code.gitea.io/gitea/services/mirror"
  22. )
  23. var searchOrderByMap = map[string]map[string]models.SearchOrderBy{
  24. "asc": {
  25. "alpha": models.SearchOrderByAlphabetically,
  26. "created": models.SearchOrderByOldest,
  27. "updated": models.SearchOrderByLeastUpdated,
  28. "size": models.SearchOrderBySize,
  29. "id": models.SearchOrderByID,
  30. },
  31. "desc": {
  32. "alpha": models.SearchOrderByAlphabeticallyReverse,
  33. "created": models.SearchOrderByNewest,
  34. "updated": models.SearchOrderByRecentUpdated,
  35. "size": models.SearchOrderBySizeReverse,
  36. "id": models.SearchOrderByIDReverse,
  37. },
  38. }
  39. // Search repositories via options
  40. func Search(ctx *context.APIContext) {
  41. // swagger:operation GET /repos/search repository repoSearch
  42. // ---
  43. // summary: Search for repositories
  44. // produces:
  45. // - application/json
  46. // parameters:
  47. // - name: q
  48. // in: query
  49. // description: keyword
  50. // type: string
  51. // - name: topic
  52. // in: query
  53. // description: Limit search to repositories with keyword as topic
  54. // type: boolean
  55. // - name: includeDesc
  56. // in: query
  57. // description: include search of keyword within repository description
  58. // type: boolean
  59. // - name: uid
  60. // in: query
  61. // description: search only for repos that the user with the given id owns or contributes to
  62. // type: integer
  63. // format: int64
  64. // - name: starredBy
  65. // in: query
  66. // description: search only for repos that the user with the given id has starred
  67. // type: integer
  68. // format: int64
  69. // - name: private
  70. // in: query
  71. // description: include private repositories this user has access to (defaults to true)
  72. // type: boolean
  73. // - name: page
  74. // in: query
  75. // description: page number of results to return (1-based)
  76. // type: integer
  77. // - name: limit
  78. // in: query
  79. // description: page size of results, maximum page size is 50
  80. // type: integer
  81. // - name: mode
  82. // in: query
  83. // description: type of repository to search for. Supported values are
  84. // "fork", "source", "mirror" and "collaborative"
  85. // type: string
  86. // - name: exclusive
  87. // in: query
  88. // description: if `uid` is given, search only for repos that the user owns
  89. // type: boolean
  90. // - name: sort
  91. // in: query
  92. // description: sort repos by attribute. Supported values are
  93. // "alpha", "created", "updated", "size", and "id".
  94. // Default is "alpha"
  95. // type: string
  96. // - name: order
  97. // in: query
  98. // description: sort order, either "asc" (ascending) or "desc" (descending).
  99. // Default is "asc", ignored if "sort" is not specified.
  100. // type: string
  101. // responses:
  102. // "200":
  103. // "$ref": "#/responses/SearchResults"
  104. // "422":
  105. // "$ref": "#/responses/validationError"
  106. opts := &models.SearchRepoOptions{
  107. Keyword: strings.Trim(ctx.Query("q"), " "),
  108. OwnerID: ctx.QueryInt64("uid"),
  109. Page: ctx.QueryInt("page"),
  110. PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")),
  111. TopicOnly: ctx.QueryBool("topic"),
  112. Collaborate: util.OptionalBoolNone,
  113. Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")),
  114. UserIsAdmin: ctx.IsUserSiteAdmin(),
  115. UserID: ctx.Data["SignedUserID"].(int64),
  116. StarredByID: ctx.QueryInt64("starredBy"),
  117. IncludeDescription: ctx.QueryBool("includeDesc"),
  118. }
  119. if ctx.QueryBool("exclusive") {
  120. opts.Collaborate = util.OptionalBoolFalse
  121. }
  122. var mode = ctx.Query("mode")
  123. switch mode {
  124. case "source":
  125. opts.Fork = util.OptionalBoolFalse
  126. opts.Mirror = util.OptionalBoolFalse
  127. case "fork":
  128. opts.Fork = util.OptionalBoolTrue
  129. case "mirror":
  130. opts.Mirror = util.OptionalBoolTrue
  131. case "collaborative":
  132. opts.Mirror = util.OptionalBoolFalse
  133. opts.Collaborate = util.OptionalBoolTrue
  134. case "":
  135. default:
  136. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
  137. return
  138. }
  139. var sortMode = ctx.Query("sort")
  140. if len(sortMode) > 0 {
  141. var sortOrder = ctx.Query("order")
  142. if len(sortOrder) == 0 {
  143. sortOrder = "asc"
  144. }
  145. if searchModeMap, ok := searchOrderByMap[sortOrder]; ok {
  146. if orderBy, ok := searchModeMap[sortMode]; ok {
  147. opts.OrderBy = orderBy
  148. } else {
  149. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
  150. return
  151. }
  152. } else {
  153. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
  154. return
  155. }
  156. }
  157. var err error
  158. repos, count, err := models.SearchRepository(opts)
  159. if err != nil {
  160. ctx.JSON(500, api.SearchError{
  161. OK: false,
  162. Error: err.Error(),
  163. })
  164. return
  165. }
  166. results := make([]*api.Repository, len(repos))
  167. for i, repo := range repos {
  168. if err = repo.GetOwner(); err != nil {
  169. ctx.JSON(500, api.SearchError{
  170. OK: false,
  171. Error: err.Error(),
  172. })
  173. return
  174. }
  175. accessMode, err := models.AccessLevel(ctx.User, repo)
  176. if err != nil {
  177. ctx.JSON(500, api.SearchError{
  178. OK: false,
  179. Error: err.Error(),
  180. })
  181. }
  182. results[i] = repo.APIFormat(accessMode)
  183. }
  184. ctx.SetLinkHeader(int(count), setting.API.MaxResponseItems)
  185. ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", count))
  186. ctx.JSON(200, api.SearchResults{
  187. OK: true,
  188. Data: results,
  189. })
  190. }
  191. // CreateUserRepo create a repository for a user
  192. func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateRepoOption) {
  193. if opt.AutoInit && opt.Readme == "" {
  194. opt.Readme = "Default"
  195. }
  196. repo, err := models.CreateRepository(ctx.User, owner, models.CreateRepoOptions{
  197. Name: opt.Name,
  198. Description: opt.Description,
  199. IssueLabels: opt.IssueLabels,
  200. Gitignores: opt.Gitignores,
  201. License: opt.License,
  202. Readme: opt.Readme,
  203. IsPrivate: opt.Private,
  204. AutoInit: opt.AutoInit,
  205. })
  206. if err != nil {
  207. if models.IsErrRepoAlreadyExist(err) {
  208. ctx.Error(409, "", "The repository with the same name already exists.")
  209. } else if models.IsErrNameReserved(err) ||
  210. models.IsErrNamePatternNotAllowed(err) {
  211. ctx.Error(422, "", err)
  212. } else {
  213. if repo != nil {
  214. if err = models.DeleteRepository(ctx.User, ctx.User.ID, repo.ID); err != nil {
  215. log.Error("DeleteRepository: %v", err)
  216. }
  217. }
  218. ctx.Error(500, "CreateRepository", err)
  219. }
  220. return
  221. }
  222. notification.NotifyCreateRepository(ctx.User, owner, repo)
  223. ctx.JSON(201, repo.APIFormat(models.AccessModeOwner))
  224. }
  225. // Create one repository of mine
  226. func Create(ctx *context.APIContext, opt api.CreateRepoOption) {
  227. // swagger:operation POST /user/repos repository user createCurrentUserRepo
  228. // ---
  229. // summary: Create a repository
  230. // consumes:
  231. // - application/json
  232. // produces:
  233. // - application/json
  234. // parameters:
  235. // - name: body
  236. // in: body
  237. // schema:
  238. // "$ref": "#/definitions/CreateRepoOption"
  239. // responses:
  240. // "201":
  241. // "$ref": "#/responses/Repository"
  242. // "409":
  243. // description: The repository with the same name already exists.
  244. // "422":
  245. // "$ref": "#/responses/validationError"
  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. var opts = migrations.MigrateOptions{
  377. RemoteURL: remoteAddr,
  378. Name: form.RepoName,
  379. Description: form.Description,
  380. Private: form.Private || setting.Repository.ForcePrivate,
  381. Mirror: form.Mirror,
  382. AuthUsername: form.AuthUsername,
  383. AuthPassword: form.AuthPassword,
  384. Wiki: form.Wiki,
  385. Issues: form.Issues,
  386. Milestones: form.Milestones,
  387. Labels: form.Labels,
  388. Comments: true,
  389. PullRequests: form.PullRequests,
  390. Releases: form.Releases,
  391. }
  392. if opts.Mirror {
  393. opts.Issues = false
  394. opts.Milestones = false
  395. opts.Labels = false
  396. opts.Comments = false
  397. opts.PullRequests = false
  398. opts.Releases = false
  399. }
  400. repo, err := migrations.MigrateRepository(ctx.User, ctxUser.Name, opts)
  401. if err == nil {
  402. notification.NotifyCreateRepository(ctx.User, ctxUser, repo)
  403. log.Trace("Repository migrated: %s/%s", ctxUser.Name, form.RepoName)
  404. ctx.JSON(201, repo.APIFormat(models.AccessModeAdmin))
  405. return
  406. }
  407. switch {
  408. case models.IsErrRepoAlreadyExist(err):
  409. ctx.Error(409, "", "The repository with the same name already exists.")
  410. case migrations.IsRateLimitError(err):
  411. ctx.Error(422, "", "Remote visit addressed rate limitation.")
  412. case migrations.IsTwoFactorAuthError(err):
  413. ctx.Error(422, "", "Remote visit required two factors authentication.")
  414. case models.IsErrReachLimitOfRepo(err):
  415. ctx.Error(422, "", fmt.Sprintf("You have already reached your limit of %d repositories.", ctxUser.MaxCreationLimit()))
  416. case models.IsErrNameReserved(err):
  417. ctx.Error(422, "", fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name))
  418. case models.IsErrNamePatternNotAllowed(err):
  419. ctx.Error(422, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern))
  420. default:
  421. err = util.URLSanitizedError(err, remoteAddr)
  422. if strings.Contains(err.Error(), "Authentication failed") ||
  423. strings.Contains(err.Error(), "Bad credentials") ||
  424. strings.Contains(err.Error(), "could not read Username") {
  425. ctx.Error(422, "", fmt.Sprintf("Authentication failed: %v.", err))
  426. } else if strings.Contains(err.Error(), "fatal:") {
  427. ctx.Error(422, "", fmt.Sprintf("Migration failed: %v.", err))
  428. } else {
  429. ctx.Error(500, "MigrateRepository", err)
  430. }
  431. }
  432. }
  433. // Get one repository
  434. func Get(ctx *context.APIContext) {
  435. // swagger:operation GET /repos/{owner}/{repo} repository repoGet
  436. // ---
  437. // summary: Get a repository
  438. // produces:
  439. // - application/json
  440. // parameters:
  441. // - name: owner
  442. // in: path
  443. // description: owner of the repo
  444. // type: string
  445. // required: true
  446. // - name: repo
  447. // in: path
  448. // description: name of the repo
  449. // type: string
  450. // required: true
  451. // responses:
  452. // "200":
  453. // "$ref": "#/responses/Repository"
  454. ctx.JSON(200, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode))
  455. }
  456. // GetByID returns a single Repository
  457. func GetByID(ctx *context.APIContext) {
  458. // swagger:operation GET /repositories/{id} repository repoGetByID
  459. // ---
  460. // summary: Get a repository by id
  461. // produces:
  462. // - application/json
  463. // parameters:
  464. // - name: id
  465. // in: path
  466. // description: id of the repo to get
  467. // type: integer
  468. // format: int64
  469. // required: true
  470. // responses:
  471. // "200":
  472. // "$ref": "#/responses/Repository"
  473. repo, err := models.GetRepositoryByID(ctx.ParamsInt64(":id"))
  474. if err != nil {
  475. if models.IsErrRepoNotExist(err) {
  476. ctx.NotFound()
  477. } else {
  478. ctx.Error(500, "GetRepositoryByID", err)
  479. }
  480. return
  481. }
  482. perm, err := models.GetUserRepoPermission(repo, ctx.User)
  483. if err != nil {
  484. ctx.Error(500, "AccessLevel", err)
  485. return
  486. } else if !perm.HasAccess() {
  487. ctx.NotFound()
  488. return
  489. }
  490. ctx.JSON(200, repo.APIFormat(perm.AccessMode))
  491. }
  492. // Edit edit repository properties
  493. func Edit(ctx *context.APIContext, opts api.EditRepoOption) {
  494. // swagger:operation PATCH /repos/{owner}/{repo} repository repoEdit
  495. // ---
  496. // summary: Edit a repository's properties. Only fields that are set will be changed.
  497. // produces:
  498. // - application/json
  499. // parameters:
  500. // - name: owner
  501. // in: path
  502. // description: owner of the repo to edit
  503. // type: string
  504. // required: true
  505. // - name: repo
  506. // in: path
  507. // description: name of the repo to edit
  508. // type: string
  509. // required: true
  510. // required: true
  511. // - name: body
  512. // in: body
  513. // description: "Properties of a repo that you can edit"
  514. // schema:
  515. // "$ref": "#/definitions/EditRepoOption"
  516. // responses:
  517. // "200":
  518. // "$ref": "#/responses/Repository"
  519. // "403":
  520. // "$ref": "#/responses/forbidden"
  521. // "422":
  522. // "$ref": "#/responses/validationError"
  523. if err := updateBasicProperties(ctx, opts); err != nil {
  524. return
  525. }
  526. if err := updateRepoUnits(ctx, opts); err != nil {
  527. return
  528. }
  529. if opts.Archived != nil {
  530. if err := updateRepoArchivedState(ctx, opts); err != nil {
  531. return
  532. }
  533. }
  534. ctx.JSON(http.StatusOK, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode))
  535. }
  536. // updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility
  537. func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) error {
  538. owner := ctx.Repo.Owner
  539. repo := ctx.Repo.Repository
  540. oldRepoName := repo.Name
  541. newRepoName := repo.Name
  542. if opts.Name != nil {
  543. newRepoName = *opts.Name
  544. }
  545. // Check if repository name has been changed and not just a case change
  546. if repo.LowerName != strings.ToLower(newRepoName) {
  547. if err := models.ChangeRepositoryName(ctx.Repo.Owner, repo.Name, newRepoName); err != nil {
  548. switch {
  549. case models.IsErrRepoAlreadyExist(err):
  550. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err)
  551. case models.IsErrNameReserved(err):
  552. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err)
  553. case models.IsErrNamePatternNotAllowed(err):
  554. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(models.ErrNamePatternNotAllowed).Pattern), err)
  555. default:
  556. ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err)
  557. }
  558. return err
  559. }
  560. err := models.NewRepoRedirect(ctx.Repo.Owner.ID, repo.ID, repo.Name, newRepoName)
  561. if err != nil {
  562. ctx.Error(http.StatusUnprocessableEntity, "NewRepoRedirect", err)
  563. return err
  564. }
  565. if err := models.RenameRepoAction(ctx.User, oldRepoName, repo); err != nil {
  566. log.Error("RenameRepoAction: %v", err)
  567. ctx.Error(http.StatusInternalServerError, "RenameRepoActions", err)
  568. return err
  569. }
  570. log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
  571. }
  572. // Update the name in the repo object for the response
  573. repo.Name = newRepoName
  574. repo.LowerName = strings.ToLower(newRepoName)
  575. if opts.Description != nil {
  576. repo.Description = *opts.Description
  577. }
  578. if opts.Website != nil {
  579. repo.Website = *opts.Website
  580. }
  581. visibilityChanged := false
  582. if opts.Private != nil {
  583. // Visibility of forked repository is forced sync with base repository.
  584. if repo.IsFork {
  585. *opts.Private = repo.BaseRepo.IsPrivate
  586. }
  587. visibilityChanged = repo.IsPrivate != *opts.Private
  588. // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
  589. if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.User.IsAdmin {
  590. err := fmt.Errorf("cannot change private repository to public")
  591. ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err)
  592. return err
  593. }
  594. repo.IsPrivate = *opts.Private
  595. }
  596. if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
  597. ctx.Error(http.StatusInternalServerError, "UpdateRepository", err)
  598. return err
  599. }
  600. log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name)
  601. return nil
  602. }
  603. // updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings
  604. func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
  605. owner := ctx.Repo.Owner
  606. repo := ctx.Repo.Repository
  607. var units []models.RepoUnit
  608. for _, tp := range models.MustRepoUnits {
  609. units = append(units, models.RepoUnit{
  610. RepoID: repo.ID,
  611. Type: tp,
  612. Config: new(models.UnitConfig),
  613. })
  614. }
  615. if opts.HasIssues == nil {
  616. // If HasIssues setting not touched, rewrite existing repo unit
  617. if unit, err := repo.GetUnit(models.UnitTypeIssues); err == nil {
  618. units = append(units, *unit)
  619. } else if unit, err := repo.GetUnit(models.UnitTypeExternalTracker); err == nil {
  620. units = append(units, *unit)
  621. }
  622. } else if *opts.HasIssues {
  623. if opts.ExternalTracker != nil {
  624. // Check that values are valid
  625. if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) {
  626. err := fmt.Errorf("External tracker URL not valid")
  627. ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL", err)
  628. return err
  629. }
  630. if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) {
  631. err := fmt.Errorf("External tracker URL format not valid")
  632. ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL format", err)
  633. return err
  634. }
  635. units = append(units, models.RepoUnit{
  636. RepoID: repo.ID,
  637. Type: models.UnitTypeExternalTracker,
  638. Config: &models.ExternalTrackerConfig{
  639. ExternalTrackerURL: opts.ExternalTracker.ExternalTrackerURL,
  640. ExternalTrackerFormat: opts.ExternalTracker.ExternalTrackerFormat,
  641. ExternalTrackerStyle: opts.ExternalTracker.ExternalTrackerStyle,
  642. },
  643. })
  644. } else {
  645. // Default to built-in tracker
  646. var config *models.IssuesConfig
  647. if opts.InternalTracker != nil {
  648. config = &models.IssuesConfig{
  649. EnableTimetracker: opts.InternalTracker.EnableTimeTracker,
  650. AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
  651. EnableDependencies: opts.InternalTracker.EnableIssueDependencies,
  652. }
  653. } else if unit, err := repo.GetUnit(models.UnitTypeIssues); err != nil {
  654. // Unit type doesn't exist so we make a new config file with default values
  655. config = &models.IssuesConfig{
  656. EnableTimetracker: true,
  657. AllowOnlyContributorsToTrackTime: true,
  658. EnableDependencies: true,
  659. }
  660. } else {
  661. config = unit.IssuesConfig()
  662. }
  663. units = append(units, models.RepoUnit{
  664. RepoID: repo.ID,
  665. Type: models.UnitTypeIssues,
  666. Config: config,
  667. })
  668. }
  669. }
  670. if opts.HasWiki == nil {
  671. // If HasWiki setting not touched, rewrite existing repo unit
  672. if unit, err := repo.GetUnit(models.UnitTypeWiki); err == nil {
  673. units = append(units, *unit)
  674. } else if unit, err := repo.GetUnit(models.UnitTypeExternalWiki); err == nil {
  675. units = append(units, *unit)
  676. }
  677. } else if *opts.HasWiki {
  678. if opts.ExternalWiki != nil {
  679. // Check that values are valid
  680. if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) {
  681. err := fmt.Errorf("External wiki URL not valid")
  682. ctx.Error(http.StatusUnprocessableEntity, "", "Invalid external wiki URL")
  683. return err
  684. }
  685. units = append(units, models.RepoUnit{
  686. RepoID: repo.ID,
  687. Type: models.UnitTypeExternalWiki,
  688. Config: &models.ExternalWikiConfig{
  689. ExternalWikiURL: opts.ExternalWiki.ExternalWikiURL,
  690. },
  691. })
  692. } else {
  693. config := &models.UnitConfig{}
  694. units = append(units, models.RepoUnit{
  695. RepoID: repo.ID,
  696. Type: models.UnitTypeWiki,
  697. Config: config,
  698. })
  699. }
  700. }
  701. if opts.HasPullRequests == nil {
  702. // If HasPullRequest setting not touched, rewrite existing repo unit
  703. if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil {
  704. units = append(units, *unit)
  705. }
  706. } else if *opts.HasPullRequests {
  707. // We do allow setting individual PR settings through the API, so
  708. // we get the config settings and then set them
  709. // if those settings were provided in the opts.
  710. unit, err := repo.GetUnit(models.UnitTypePullRequests)
  711. var config *models.PullRequestsConfig
  712. if err != nil {
  713. // Unit type doesn't exist so we make a new config file with default values
  714. config = &models.PullRequestsConfig{
  715. IgnoreWhitespaceConflicts: false,
  716. AllowMerge: true,
  717. AllowRebase: true,
  718. AllowRebaseMerge: true,
  719. AllowSquash: true,
  720. }
  721. } else {
  722. config = unit.PullRequestsConfig()
  723. }
  724. if opts.IgnoreWhitespaceConflicts != nil {
  725. config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
  726. }
  727. if opts.AllowMerge != nil {
  728. config.AllowMerge = *opts.AllowMerge
  729. }
  730. if opts.AllowRebase != nil {
  731. config.AllowRebase = *opts.AllowRebase
  732. }
  733. if opts.AllowRebaseMerge != nil {
  734. config.AllowRebaseMerge = *opts.AllowRebaseMerge
  735. }
  736. if opts.AllowSquash != nil {
  737. config.AllowSquash = *opts.AllowSquash
  738. }
  739. units = append(units, models.RepoUnit{
  740. RepoID: repo.ID,
  741. Type: models.UnitTypePullRequests,
  742. Config: config,
  743. })
  744. }
  745. if err := models.UpdateRepositoryUnits(repo, units); err != nil {
  746. ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err)
  747. return err
  748. }
  749. log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name)
  750. return nil
  751. }
  752. // updateRepoArchivedState updates repo's archive state
  753. func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) error {
  754. repo := ctx.Repo.Repository
  755. // archive / un-archive
  756. if opts.Archived != nil {
  757. if repo.IsMirror {
  758. err := fmt.Errorf("repo is a mirror, cannot archive/un-archive")
  759. ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
  760. return err
  761. }
  762. if *opts.Archived {
  763. if err := repo.SetArchiveRepoState(*opts.Archived); err != nil {
  764. log.Error("Tried to archive a repo: %s", err)
  765. ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
  766. return err
  767. }
  768. log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  769. } else {
  770. if err := repo.SetArchiveRepoState(*opts.Archived); err != nil {
  771. log.Error("Tried to un-archive a repo: %s", err)
  772. ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
  773. return err
  774. }
  775. log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  776. }
  777. }
  778. return nil
  779. }
  780. // Delete one repository
  781. func Delete(ctx *context.APIContext) {
  782. // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
  783. // ---
  784. // summary: Delete a repository
  785. // produces:
  786. // - application/json
  787. // parameters:
  788. // - name: owner
  789. // in: path
  790. // description: owner of the repo to delete
  791. // type: string
  792. // required: true
  793. // - name: repo
  794. // in: path
  795. // description: name of the repo to delete
  796. // type: string
  797. // required: true
  798. // responses:
  799. // "204":
  800. // "$ref": "#/responses/empty"
  801. // "403":
  802. // "$ref": "#/responses/forbidden"
  803. owner := ctx.Repo.Owner
  804. repo := ctx.Repo.Repository
  805. if owner.IsOrganization() && !ctx.User.IsAdmin {
  806. isOwner, err := owner.IsOwnedBy(ctx.User.ID)
  807. if err != nil {
  808. ctx.Error(500, "IsOwnedBy", err)
  809. return
  810. } else if !isOwner {
  811. ctx.Error(403, "", "Given user is not owner of organization.")
  812. return
  813. }
  814. }
  815. if err := models.DeleteRepository(ctx.User, owner.ID, repo.ID); err != nil {
  816. ctx.Error(500, "DeleteRepository", err)
  817. return
  818. }
  819. log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
  820. ctx.Status(204)
  821. }
  822. // MirrorSync adds a mirrored repository to the sync queue
  823. func MirrorSync(ctx *context.APIContext) {
  824. // swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
  825. // ---
  826. // summary: Sync a mirrored repository
  827. // produces:
  828. // - application/json
  829. // parameters:
  830. // - name: owner
  831. // in: path
  832. // description: owner of the repo to sync
  833. // type: string
  834. // required: true
  835. // - name: repo
  836. // in: path
  837. // description: name of the repo to sync
  838. // type: string
  839. // required: true
  840. // responses:
  841. // "200":
  842. // "$ref": "#/responses/empty"
  843. repo := ctx.Repo.Repository
  844. if !ctx.Repo.CanWrite(models.UnitTypeCode) {
  845. ctx.Error(403, "MirrorSync", "Must have write access")
  846. }
  847. mirror_service.StartToMirror(repo.ID)
  848. ctx.Status(200)
  849. }