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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  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. "net/url"
  10. "strings"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/auth"
  13. "code.gitea.io/gitea/modules/context"
  14. "code.gitea.io/gitea/modules/log"
  15. "code.gitea.io/gitea/modules/migrations"
  16. "code.gitea.io/gitea/modules/notification"
  17. "code.gitea.io/gitea/modules/setting"
  18. "code.gitea.io/gitea/modules/structs"
  19. api "code.gitea.io/gitea/modules/structs"
  20. "code.gitea.io/gitea/modules/util"
  21. "code.gitea.io/gitea/modules/validation"
  22. "code.gitea.io/gitea/routers/api/v1/convert"
  23. mirror_service "code.gitea.io/gitea/services/mirror"
  24. )
  25. var searchOrderByMap = map[string]map[string]models.SearchOrderBy{
  26. "asc": {
  27. "alpha": models.SearchOrderByAlphabetically,
  28. "created": models.SearchOrderByOldest,
  29. "updated": models.SearchOrderByLeastUpdated,
  30. "size": models.SearchOrderBySize,
  31. "id": models.SearchOrderByID,
  32. },
  33. "desc": {
  34. "alpha": models.SearchOrderByAlphabeticallyReverse,
  35. "created": models.SearchOrderByNewest,
  36. "updated": models.SearchOrderByRecentUpdated,
  37. "size": models.SearchOrderBySizeReverse,
  38. "id": models.SearchOrderByIDReverse,
  39. },
  40. }
  41. // Search repositories via options
  42. func Search(ctx *context.APIContext) {
  43. // swagger:operation GET /repos/search repository repoSearch
  44. // ---
  45. // summary: Search for repositories
  46. // produces:
  47. // - application/json
  48. // parameters:
  49. // - name: q
  50. // in: query
  51. // description: keyword
  52. // type: string
  53. // - name: topic
  54. // in: query
  55. // description: Limit search to repositories with keyword as topic
  56. // type: boolean
  57. // - name: includeDesc
  58. // in: query
  59. // description: include search of keyword within repository description
  60. // type: boolean
  61. // - name: uid
  62. // in: query
  63. // description: search only for repos that the user with the given id owns or contributes to
  64. // type: integer
  65. // format: int64
  66. // - name: starredBy
  67. // in: query
  68. // description: search only for repos that the user with the given id has starred
  69. // type: integer
  70. // format: int64
  71. // - name: private
  72. // in: query
  73. // description: include private repositories this user has access to (defaults to true)
  74. // type: boolean
  75. // - name: page
  76. // in: query
  77. // description: page number of results to return (1-based)
  78. // type: integer
  79. // - name: limit
  80. // in: query
  81. // description: page size of results, maximum page size is 50
  82. // type: integer
  83. // - name: mode
  84. // in: query
  85. // description: type of repository to search for. Supported values are
  86. // "fork", "source", "mirror" and "collaborative"
  87. // type: string
  88. // - name: exclusive
  89. // in: query
  90. // description: if `uid` is given, search only for repos that the user owns
  91. // type: boolean
  92. // - name: sort
  93. // in: query
  94. // description: sort repos by attribute. Supported values are
  95. // "alpha", "created", "updated", "size", and "id".
  96. // Default is "alpha"
  97. // type: string
  98. // - name: order
  99. // in: query
  100. // description: sort order, either "asc" (ascending) or "desc" (descending).
  101. // Default is "asc", ignored if "sort" is not specified.
  102. // type: string
  103. // responses:
  104. // "200":
  105. // "$ref": "#/responses/SearchResults"
  106. // "422":
  107. // "$ref": "#/responses/validationError"
  108. opts := &models.SearchRepoOptions{
  109. Keyword: strings.Trim(ctx.Query("q"), " "),
  110. OwnerID: ctx.QueryInt64("uid"),
  111. Page: ctx.QueryInt("page"),
  112. PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")),
  113. TopicOnly: ctx.QueryBool("topic"),
  114. Collaborate: util.OptionalBoolNone,
  115. Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")),
  116. UserIsAdmin: ctx.IsUserSiteAdmin(),
  117. UserID: ctx.Data["SignedUserID"].(int64),
  118. StarredByID: ctx.QueryInt64("starredBy"),
  119. IncludeDescription: ctx.QueryBool("includeDesc"),
  120. }
  121. if ctx.QueryBool("exclusive") {
  122. opts.Collaborate = util.OptionalBoolFalse
  123. }
  124. var mode = ctx.Query("mode")
  125. switch mode {
  126. case "source":
  127. opts.Fork = util.OptionalBoolFalse
  128. opts.Mirror = util.OptionalBoolFalse
  129. case "fork":
  130. opts.Fork = util.OptionalBoolTrue
  131. case "mirror":
  132. opts.Mirror = util.OptionalBoolTrue
  133. case "collaborative":
  134. opts.Mirror = util.OptionalBoolFalse
  135. opts.Collaborate = util.OptionalBoolTrue
  136. case "":
  137. default:
  138. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
  139. return
  140. }
  141. var sortMode = ctx.Query("sort")
  142. if len(sortMode) > 0 {
  143. var sortOrder = ctx.Query("order")
  144. if len(sortOrder) == 0 {
  145. sortOrder = "asc"
  146. }
  147. if searchModeMap, ok := searchOrderByMap[sortOrder]; ok {
  148. if orderBy, ok := searchModeMap[sortMode]; ok {
  149. opts.OrderBy = orderBy
  150. } else {
  151. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
  152. return
  153. }
  154. } else {
  155. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
  156. return
  157. }
  158. }
  159. var err error
  160. repos, count, err := models.SearchRepository(opts)
  161. if err != nil {
  162. ctx.JSON(500, api.SearchError{
  163. OK: false,
  164. Error: err.Error(),
  165. })
  166. return
  167. }
  168. results := make([]*api.Repository, len(repos))
  169. for i, repo := range repos {
  170. if err = repo.GetOwner(); err != nil {
  171. ctx.JSON(500, api.SearchError{
  172. OK: false,
  173. Error: err.Error(),
  174. })
  175. return
  176. }
  177. accessMode, err := models.AccessLevel(ctx.User, repo)
  178. if err != nil {
  179. ctx.JSON(500, api.SearchError{
  180. OK: false,
  181. Error: err.Error(),
  182. })
  183. }
  184. results[i] = repo.APIFormat(accessMode)
  185. }
  186. ctx.SetLinkHeader(int(count), setting.API.MaxResponseItems)
  187. ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", count))
  188. ctx.JSON(200, api.SearchResults{
  189. OK: true,
  190. Data: results,
  191. })
  192. }
  193. // CreateUserRepo create a repository for a user
  194. func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateRepoOption) {
  195. if opt.AutoInit && opt.Readme == "" {
  196. opt.Readme = "Default"
  197. }
  198. repo, err := models.CreateRepository(ctx.User, owner, models.CreateRepoOptions{
  199. Name: opt.Name,
  200. Description: opt.Description,
  201. IssueLabels: opt.IssueLabels,
  202. Gitignores: opt.Gitignores,
  203. License: opt.License,
  204. Readme: opt.Readme,
  205. IsPrivate: opt.Private,
  206. AutoInit: opt.AutoInit,
  207. })
  208. if err != nil {
  209. if models.IsErrRepoAlreadyExist(err) {
  210. ctx.Error(409, "", "The repository with the same name already exists.")
  211. } else if models.IsErrNameReserved(err) ||
  212. models.IsErrNamePatternNotAllowed(err) {
  213. ctx.Error(422, "", err)
  214. } else {
  215. if repo != nil {
  216. if err = models.DeleteRepository(ctx.User, ctx.User.ID, repo.ID); err != nil {
  217. log.Error("DeleteRepository: %v", err)
  218. }
  219. }
  220. ctx.Error(500, "CreateRepository", err)
  221. }
  222. return
  223. }
  224. notification.NotifyCreateRepository(ctx.User, owner, repo)
  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. // "409":
  245. // description: The repository with the same name already exists.
  246. // "422":
  247. // "$ref": "#/responses/validationError"
  248. if ctx.User.IsOrganization() {
  249. // Shouldn't reach this condition, but just in case.
  250. ctx.Error(422, "", "not allowed creating repository for organization")
  251. return
  252. }
  253. CreateUserRepo(ctx, ctx.User, opt)
  254. }
  255. // CreateOrgRepo create one repository of the organization
  256. func CreateOrgRepo(ctx *context.APIContext, opt api.CreateRepoOption) {
  257. // swagger:operation POST /org/{org}/repos organization createOrgRepo
  258. // ---
  259. // summary: Create a repository in an organization
  260. // consumes:
  261. // - application/json
  262. // produces:
  263. // - application/json
  264. // parameters:
  265. // - name: org
  266. // in: path
  267. // description: name of organization
  268. // type: string
  269. // required: true
  270. // - name: body
  271. // in: body
  272. // schema:
  273. // "$ref": "#/definitions/CreateRepoOption"
  274. // responses:
  275. // "201":
  276. // "$ref": "#/responses/Repository"
  277. // "422":
  278. // "$ref": "#/responses/validationError"
  279. // "403":
  280. // "$ref": "#/responses/forbidden"
  281. org, err := models.GetOrgByName(ctx.Params(":org"))
  282. if err != nil {
  283. if models.IsErrOrgNotExist(err) {
  284. ctx.Error(422, "", err)
  285. } else {
  286. ctx.Error(500, "GetOrgByName", err)
  287. }
  288. return
  289. }
  290. if !models.HasOrgVisible(org, ctx.User) {
  291. ctx.NotFound("HasOrgVisible", nil)
  292. return
  293. }
  294. if !ctx.User.IsAdmin {
  295. isOwner, err := org.IsOwnedBy(ctx.User.ID)
  296. if err != nil {
  297. ctx.ServerError("IsOwnedBy", err)
  298. return
  299. } else if !isOwner {
  300. ctx.Error(403, "", "Given user is not owner of organization.")
  301. return
  302. }
  303. }
  304. CreateUserRepo(ctx, org, opt)
  305. }
  306. // Migrate migrate remote git repository to gitea
  307. func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) {
  308. // swagger:operation POST /repos/migrate repository repoMigrate
  309. // ---
  310. // summary: Migrate a remote git repository
  311. // consumes:
  312. // - application/json
  313. // produces:
  314. // - application/json
  315. // parameters:
  316. // - name: body
  317. // in: body
  318. // schema:
  319. // "$ref": "#/definitions/MigrateRepoForm"
  320. // responses:
  321. // "201":
  322. // "$ref": "#/responses/Repository"
  323. ctxUser := ctx.User
  324. // Not equal means context user is an organization,
  325. // or is another user/organization if current user is admin.
  326. if form.UID != ctxUser.ID {
  327. org, err := models.GetUserByID(form.UID)
  328. if err != nil {
  329. if models.IsErrUserNotExist(err) {
  330. ctx.Error(422, "", err)
  331. } else {
  332. ctx.Error(500, "GetUserByID", err)
  333. }
  334. return
  335. }
  336. ctxUser = org
  337. }
  338. if ctx.HasError() {
  339. ctx.Error(422, "", ctx.GetErrMsg())
  340. return
  341. }
  342. if !ctx.User.IsAdmin {
  343. if !ctxUser.IsOrganization() && ctx.User.ID != ctxUser.ID {
  344. ctx.Error(403, "", "Given user is not an organization.")
  345. return
  346. }
  347. if ctxUser.IsOrganization() {
  348. // Check ownership of organization.
  349. isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID)
  350. if err != nil {
  351. ctx.Error(500, "IsOwnedBy", err)
  352. return
  353. } else if !isOwner {
  354. ctx.Error(403, "", "Given user is not owner of organization.")
  355. return
  356. }
  357. }
  358. }
  359. remoteAddr, err := form.ParseRemoteAddr(ctx.User)
  360. if err != nil {
  361. if models.IsErrInvalidCloneAddr(err) {
  362. addrErr := err.(models.ErrInvalidCloneAddr)
  363. switch {
  364. case addrErr.IsURLError:
  365. ctx.Error(422, "", err)
  366. case addrErr.IsPermissionDenied:
  367. ctx.Error(422, "", "You are not allowed to import local repositories.")
  368. case addrErr.IsInvalidPath:
  369. ctx.Error(422, "", "Invalid local path, it does not exist or not a directory.")
  370. default:
  371. ctx.Error(500, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error())
  372. }
  373. } else {
  374. ctx.Error(500, "ParseRemoteAddr", err)
  375. }
  376. return
  377. }
  378. var gitServiceType = structs.PlainGitService
  379. u, err := url.Parse(remoteAddr)
  380. if err == nil && strings.EqualFold(u.Host, "github.com") {
  381. gitServiceType = structs.GithubService
  382. }
  383. var opts = migrations.MigrateOptions{
  384. CloneAddr: remoteAddr,
  385. RepoName: form.RepoName,
  386. Description: form.Description,
  387. Private: form.Private || setting.Repository.ForcePrivate,
  388. Mirror: form.Mirror,
  389. AuthUsername: form.AuthUsername,
  390. AuthPassword: form.AuthPassword,
  391. Wiki: form.Wiki,
  392. Issues: form.Issues,
  393. Milestones: form.Milestones,
  394. Labels: form.Labels,
  395. Comments: true,
  396. PullRequests: form.PullRequests,
  397. Releases: form.Releases,
  398. GitServiceType: gitServiceType,
  399. }
  400. if opts.Mirror {
  401. opts.Issues = false
  402. opts.Milestones = false
  403. opts.Labels = false
  404. opts.Comments = false
  405. opts.PullRequests = false
  406. opts.Releases = false
  407. }
  408. repo, err := migrations.MigrateRepository(ctx.User, ctxUser.Name, opts)
  409. if err == nil {
  410. notification.NotifyCreateRepository(ctx.User, ctxUser, repo)
  411. log.Trace("Repository migrated: %s/%s", ctxUser.Name, form.RepoName)
  412. ctx.JSON(201, repo.APIFormat(models.AccessModeAdmin))
  413. return
  414. }
  415. switch {
  416. case models.IsErrRepoAlreadyExist(err):
  417. ctx.Error(409, "", "The repository with the same name already exists.")
  418. case migrations.IsRateLimitError(err):
  419. ctx.Error(422, "", "Remote visit addressed rate limitation.")
  420. case migrations.IsTwoFactorAuthError(err):
  421. ctx.Error(422, "", "Remote visit required two factors authentication.")
  422. case models.IsErrReachLimitOfRepo(err):
  423. ctx.Error(422, "", fmt.Sprintf("You have already reached your limit of %d repositories.", ctxUser.MaxCreationLimit()))
  424. case models.IsErrNameReserved(err):
  425. ctx.Error(422, "", fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name))
  426. case models.IsErrNamePatternNotAllowed(err):
  427. ctx.Error(422, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern))
  428. default:
  429. err = util.URLSanitizedError(err, remoteAddr)
  430. if strings.Contains(err.Error(), "Authentication failed") ||
  431. strings.Contains(err.Error(), "Bad credentials") ||
  432. strings.Contains(err.Error(), "could not read Username") {
  433. ctx.Error(422, "", fmt.Sprintf("Authentication failed: %v.", err))
  434. } else if strings.Contains(err.Error(), "fatal:") {
  435. ctx.Error(422, "", fmt.Sprintf("Migration failed: %v.", err))
  436. } else {
  437. ctx.Error(500, "MigrateRepository", err)
  438. }
  439. }
  440. }
  441. // Get one repository
  442. func Get(ctx *context.APIContext) {
  443. // swagger:operation GET /repos/{owner}/{repo} repository repoGet
  444. // ---
  445. // summary: Get a repository
  446. // produces:
  447. // - application/json
  448. // parameters:
  449. // - name: owner
  450. // in: path
  451. // description: owner of the repo
  452. // type: string
  453. // required: true
  454. // - name: repo
  455. // in: path
  456. // description: name of the repo
  457. // type: string
  458. // required: true
  459. // responses:
  460. // "200":
  461. // "$ref": "#/responses/Repository"
  462. ctx.JSON(200, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode))
  463. }
  464. // GetByID returns a single Repository
  465. func GetByID(ctx *context.APIContext) {
  466. // swagger:operation GET /repositories/{id} repository repoGetByID
  467. // ---
  468. // summary: Get a repository by id
  469. // produces:
  470. // - application/json
  471. // parameters:
  472. // - name: id
  473. // in: path
  474. // description: id of the repo to get
  475. // type: integer
  476. // format: int64
  477. // required: true
  478. // responses:
  479. // "200":
  480. // "$ref": "#/responses/Repository"
  481. repo, err := models.GetRepositoryByID(ctx.ParamsInt64(":id"))
  482. if err != nil {
  483. if models.IsErrRepoNotExist(err) {
  484. ctx.NotFound()
  485. } else {
  486. ctx.Error(500, "GetRepositoryByID", err)
  487. }
  488. return
  489. }
  490. perm, err := models.GetUserRepoPermission(repo, ctx.User)
  491. if err != nil {
  492. ctx.Error(500, "AccessLevel", err)
  493. return
  494. } else if !perm.HasAccess() {
  495. ctx.NotFound()
  496. return
  497. }
  498. ctx.JSON(200, repo.APIFormat(perm.AccessMode))
  499. }
  500. // Edit edit repository properties
  501. func Edit(ctx *context.APIContext, opts api.EditRepoOption) {
  502. // swagger:operation PATCH /repos/{owner}/{repo} repository repoEdit
  503. // ---
  504. // summary: Edit a repository's properties. Only fields that are set will be changed.
  505. // produces:
  506. // - application/json
  507. // parameters:
  508. // - name: owner
  509. // in: path
  510. // description: owner of the repo to edit
  511. // type: string
  512. // required: true
  513. // - name: repo
  514. // in: path
  515. // description: name of the repo to edit
  516. // type: string
  517. // required: true
  518. // required: true
  519. // - name: body
  520. // in: body
  521. // description: "Properties of a repo that you can edit"
  522. // schema:
  523. // "$ref": "#/definitions/EditRepoOption"
  524. // responses:
  525. // "200":
  526. // "$ref": "#/responses/Repository"
  527. // "403":
  528. // "$ref": "#/responses/forbidden"
  529. // "422":
  530. // "$ref": "#/responses/validationError"
  531. if err := updateBasicProperties(ctx, opts); err != nil {
  532. return
  533. }
  534. if err := updateRepoUnits(ctx, opts); err != nil {
  535. return
  536. }
  537. if opts.Archived != nil {
  538. if err := updateRepoArchivedState(ctx, opts); err != nil {
  539. return
  540. }
  541. }
  542. ctx.JSON(http.StatusOK, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode))
  543. }
  544. // updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility
  545. func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) error {
  546. owner := ctx.Repo.Owner
  547. repo := ctx.Repo.Repository
  548. oldRepoName := repo.Name
  549. newRepoName := repo.Name
  550. if opts.Name != nil {
  551. newRepoName = *opts.Name
  552. }
  553. // Check if repository name has been changed and not just a case change
  554. if repo.LowerName != strings.ToLower(newRepoName) {
  555. if err := models.ChangeRepositoryName(ctx.Repo.Owner, repo.Name, newRepoName); err != nil {
  556. switch {
  557. case models.IsErrRepoAlreadyExist(err):
  558. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err)
  559. case models.IsErrNameReserved(err):
  560. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err)
  561. case models.IsErrNamePatternNotAllowed(err):
  562. ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(models.ErrNamePatternNotAllowed).Pattern), err)
  563. default:
  564. ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err)
  565. }
  566. return err
  567. }
  568. err := models.NewRepoRedirect(ctx.Repo.Owner.ID, repo.ID, repo.Name, newRepoName)
  569. if err != nil {
  570. ctx.Error(http.StatusUnprocessableEntity, "NewRepoRedirect", err)
  571. return err
  572. }
  573. if err := models.RenameRepoAction(ctx.User, oldRepoName, repo); err != nil {
  574. log.Error("RenameRepoAction: %v", err)
  575. ctx.Error(http.StatusInternalServerError, "RenameRepoActions", err)
  576. return err
  577. }
  578. log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
  579. }
  580. // Update the name in the repo object for the response
  581. repo.Name = newRepoName
  582. repo.LowerName = strings.ToLower(newRepoName)
  583. if opts.Description != nil {
  584. repo.Description = *opts.Description
  585. }
  586. if opts.Website != nil {
  587. repo.Website = *opts.Website
  588. }
  589. visibilityChanged := false
  590. if opts.Private != nil {
  591. // Visibility of forked repository is forced sync with base repository.
  592. if repo.IsFork {
  593. *opts.Private = repo.BaseRepo.IsPrivate
  594. }
  595. visibilityChanged = repo.IsPrivate != *opts.Private
  596. // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
  597. if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.User.IsAdmin {
  598. err := fmt.Errorf("cannot change private repository to public")
  599. ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err)
  600. return err
  601. }
  602. repo.IsPrivate = *opts.Private
  603. }
  604. if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
  605. ctx.Error(http.StatusInternalServerError, "UpdateRepository", err)
  606. return err
  607. }
  608. log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name)
  609. return nil
  610. }
  611. // updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings
  612. func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
  613. owner := ctx.Repo.Owner
  614. repo := ctx.Repo.Repository
  615. var units []models.RepoUnit
  616. for _, tp := range models.MustRepoUnits {
  617. units = append(units, models.RepoUnit{
  618. RepoID: repo.ID,
  619. Type: tp,
  620. Config: new(models.UnitConfig),
  621. })
  622. }
  623. if opts.HasIssues == nil {
  624. // If HasIssues setting not touched, rewrite existing repo unit
  625. if unit, err := repo.GetUnit(models.UnitTypeIssues); err == nil {
  626. units = append(units, *unit)
  627. } else if unit, err := repo.GetUnit(models.UnitTypeExternalTracker); err == nil {
  628. units = append(units, *unit)
  629. }
  630. } else if *opts.HasIssues {
  631. if opts.ExternalTracker != nil {
  632. // Check that values are valid
  633. if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) {
  634. err := fmt.Errorf("External tracker URL not valid")
  635. ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL", err)
  636. return err
  637. }
  638. if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) {
  639. err := fmt.Errorf("External tracker URL format not valid")
  640. ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL format", err)
  641. return err
  642. }
  643. units = append(units, models.RepoUnit{
  644. RepoID: repo.ID,
  645. Type: models.UnitTypeExternalTracker,
  646. Config: &models.ExternalTrackerConfig{
  647. ExternalTrackerURL: opts.ExternalTracker.ExternalTrackerURL,
  648. ExternalTrackerFormat: opts.ExternalTracker.ExternalTrackerFormat,
  649. ExternalTrackerStyle: opts.ExternalTracker.ExternalTrackerStyle,
  650. },
  651. })
  652. } else {
  653. // Default to built-in tracker
  654. var config *models.IssuesConfig
  655. if opts.InternalTracker != nil {
  656. config = &models.IssuesConfig{
  657. EnableTimetracker: opts.InternalTracker.EnableTimeTracker,
  658. AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
  659. EnableDependencies: opts.InternalTracker.EnableIssueDependencies,
  660. }
  661. } else if unit, err := repo.GetUnit(models.UnitTypeIssues); err != nil {
  662. // Unit type doesn't exist so we make a new config file with default values
  663. config = &models.IssuesConfig{
  664. EnableTimetracker: true,
  665. AllowOnlyContributorsToTrackTime: true,
  666. EnableDependencies: true,
  667. }
  668. } else {
  669. config = unit.IssuesConfig()
  670. }
  671. units = append(units, models.RepoUnit{
  672. RepoID: repo.ID,
  673. Type: models.UnitTypeIssues,
  674. Config: config,
  675. })
  676. }
  677. }
  678. if opts.HasWiki == nil {
  679. // If HasWiki setting not touched, rewrite existing repo unit
  680. if unit, err := repo.GetUnit(models.UnitTypeWiki); err == nil {
  681. units = append(units, *unit)
  682. } else if unit, err := repo.GetUnit(models.UnitTypeExternalWiki); err == nil {
  683. units = append(units, *unit)
  684. }
  685. } else if *opts.HasWiki {
  686. if opts.ExternalWiki != nil {
  687. // Check that values are valid
  688. if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) {
  689. err := fmt.Errorf("External wiki URL not valid")
  690. ctx.Error(http.StatusUnprocessableEntity, "", "Invalid external wiki URL")
  691. return err
  692. }
  693. units = append(units, models.RepoUnit{
  694. RepoID: repo.ID,
  695. Type: models.UnitTypeExternalWiki,
  696. Config: &models.ExternalWikiConfig{
  697. ExternalWikiURL: opts.ExternalWiki.ExternalWikiURL,
  698. },
  699. })
  700. } else {
  701. config := &models.UnitConfig{}
  702. units = append(units, models.RepoUnit{
  703. RepoID: repo.ID,
  704. Type: models.UnitTypeWiki,
  705. Config: config,
  706. })
  707. }
  708. }
  709. if opts.HasPullRequests == nil {
  710. // If HasPullRequest setting not touched, rewrite existing repo unit
  711. if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil {
  712. units = append(units, *unit)
  713. }
  714. } else if *opts.HasPullRequests {
  715. // We do allow setting individual PR settings through the API, so
  716. // we get the config settings and then set them
  717. // if those settings were provided in the opts.
  718. unit, err := repo.GetUnit(models.UnitTypePullRequests)
  719. var config *models.PullRequestsConfig
  720. if err != nil {
  721. // Unit type doesn't exist so we make a new config file with default values
  722. config = &models.PullRequestsConfig{
  723. IgnoreWhitespaceConflicts: false,
  724. AllowMerge: true,
  725. AllowRebase: true,
  726. AllowRebaseMerge: true,
  727. AllowSquash: true,
  728. }
  729. } else {
  730. config = unit.PullRequestsConfig()
  731. }
  732. if opts.IgnoreWhitespaceConflicts != nil {
  733. config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
  734. }
  735. if opts.AllowMerge != nil {
  736. config.AllowMerge = *opts.AllowMerge
  737. }
  738. if opts.AllowRebase != nil {
  739. config.AllowRebase = *opts.AllowRebase
  740. }
  741. if opts.AllowRebaseMerge != nil {
  742. config.AllowRebaseMerge = *opts.AllowRebaseMerge
  743. }
  744. if opts.AllowSquash != nil {
  745. config.AllowSquash = *opts.AllowSquash
  746. }
  747. units = append(units, models.RepoUnit{
  748. RepoID: repo.ID,
  749. Type: models.UnitTypePullRequests,
  750. Config: config,
  751. })
  752. }
  753. if err := models.UpdateRepositoryUnits(repo, units); err != nil {
  754. ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err)
  755. return err
  756. }
  757. log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name)
  758. return nil
  759. }
  760. // updateRepoArchivedState updates repo's archive state
  761. func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) error {
  762. repo := ctx.Repo.Repository
  763. // archive / un-archive
  764. if opts.Archived != nil {
  765. if repo.IsMirror {
  766. err := fmt.Errorf("repo is a mirror, cannot archive/un-archive")
  767. ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
  768. return err
  769. }
  770. if *opts.Archived {
  771. if err := repo.SetArchiveRepoState(*opts.Archived); err != nil {
  772. log.Error("Tried to archive a repo: %s", err)
  773. ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
  774. return err
  775. }
  776. log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  777. } else {
  778. if err := repo.SetArchiveRepoState(*opts.Archived); err != nil {
  779. log.Error("Tried to un-archive a repo: %s", err)
  780. ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
  781. return err
  782. }
  783. log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  784. }
  785. }
  786. return nil
  787. }
  788. // Delete one repository
  789. func Delete(ctx *context.APIContext) {
  790. // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
  791. // ---
  792. // summary: Delete a repository
  793. // produces:
  794. // - application/json
  795. // parameters:
  796. // - name: owner
  797. // in: path
  798. // description: owner of the repo to delete
  799. // type: string
  800. // required: true
  801. // - name: repo
  802. // in: path
  803. // description: name of the repo to delete
  804. // type: string
  805. // required: true
  806. // responses:
  807. // "204":
  808. // "$ref": "#/responses/empty"
  809. // "403":
  810. // "$ref": "#/responses/forbidden"
  811. owner := ctx.Repo.Owner
  812. repo := ctx.Repo.Repository
  813. if owner.IsOrganization() && !ctx.User.IsAdmin {
  814. isOwner, err := owner.IsOwnedBy(ctx.User.ID)
  815. if err != nil {
  816. ctx.Error(500, "IsOwnedBy", err)
  817. return
  818. } else if !isOwner {
  819. ctx.Error(403, "", "Given user is not owner of organization.")
  820. return
  821. }
  822. }
  823. if err := models.DeleteRepository(ctx.User, owner.ID, repo.ID); err != nil {
  824. ctx.Error(500, "DeleteRepository", err)
  825. return
  826. }
  827. log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
  828. ctx.Status(204)
  829. }
  830. // MirrorSync adds a mirrored repository to the sync queue
  831. func MirrorSync(ctx *context.APIContext) {
  832. // swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
  833. // ---
  834. // summary: Sync a mirrored repository
  835. // produces:
  836. // - application/json
  837. // parameters:
  838. // - name: owner
  839. // in: path
  840. // description: owner of the repo to sync
  841. // type: string
  842. // required: true
  843. // - name: repo
  844. // in: path
  845. // description: name of the repo to sync
  846. // type: string
  847. // required: true
  848. // responses:
  849. // "200":
  850. // "$ref": "#/responses/empty"
  851. repo := ctx.Repo.Repository
  852. if !ctx.Repo.CanWrite(models.UnitTypeCode) {
  853. ctx.Error(403, "MirrorSync", "Must have write access")
  854. }
  855. mirror_service.StartToMirror(repo.ID)
  856. ctx.Status(200)
  857. }