You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

530 lines
14 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  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. "encoding/base64"
  8. "net/http"
  9. "time"
  10. "code.gitea.io/gitea/models"
  11. "code.gitea.io/gitea/modules/context"
  12. "code.gitea.io/gitea/modules/git"
  13. "code.gitea.io/gitea/modules/repofiles"
  14. api "code.gitea.io/gitea/modules/structs"
  15. "code.gitea.io/gitea/routers/repo"
  16. )
  17. // GetRawFile get a file by path on a repository
  18. func GetRawFile(ctx *context.APIContext) {
  19. // swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
  20. // ---
  21. // summary: Get a file from a repository
  22. // produces:
  23. // - application/json
  24. // parameters:
  25. // - name: owner
  26. // in: path
  27. // description: owner of the repo
  28. // type: string
  29. // required: true
  30. // - name: repo
  31. // in: path
  32. // description: name of the repo
  33. // type: string
  34. // required: true
  35. // - name: filepath
  36. // in: path
  37. // description: filepath of the file to get
  38. // type: string
  39. // required: true
  40. // responses:
  41. // 200:
  42. // description: success
  43. // "404":
  44. // "$ref": "#/responses/notFound"
  45. if ctx.Repo.Repository.IsEmpty {
  46. ctx.NotFound()
  47. return
  48. }
  49. blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
  50. if err != nil {
  51. if git.IsErrNotExist(err) {
  52. ctx.NotFound()
  53. } else {
  54. ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
  55. }
  56. return
  57. }
  58. if err = repo.ServeBlob(ctx.Context, blob); err != nil {
  59. ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
  60. }
  61. }
  62. // GetArchive get archive of a repository
  63. func GetArchive(ctx *context.APIContext) {
  64. // swagger:operation GET /repos/{owner}/{repo}/archive/{archive} repository repoGetArchive
  65. // ---
  66. // summary: Get an archive of a repository
  67. // produces:
  68. // - application/json
  69. // parameters:
  70. // - name: owner
  71. // in: path
  72. // description: owner of the repo
  73. // type: string
  74. // required: true
  75. // - name: repo
  76. // in: path
  77. // description: name of the repo
  78. // type: string
  79. // required: true
  80. // - name: archive
  81. // in: path
  82. // description: archive to download, consisting of a git reference and archive
  83. // type: string
  84. // required: true
  85. // responses:
  86. // 200:
  87. // description: success
  88. // "404":
  89. // "$ref": "#/responses/notFound"
  90. repoPath := models.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))
  91. gitRepo, err := git.OpenRepository(repoPath)
  92. if err != nil {
  93. ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
  94. return
  95. }
  96. ctx.Repo.GitRepo = gitRepo
  97. defer gitRepo.Close()
  98. repo.Download(ctx.Context)
  99. }
  100. // GetEditorconfig get editor config of a repository
  101. func GetEditorconfig(ctx *context.APIContext) {
  102. // swagger:operation GET /repos/{owner}/{repo}/editorconfig/{filepath} repository repoGetEditorConfig
  103. // ---
  104. // summary: Get the EditorConfig definitions of a file in a repository
  105. // produces:
  106. // - application/json
  107. // parameters:
  108. // - name: owner
  109. // in: path
  110. // description: owner of the repo
  111. // type: string
  112. // required: true
  113. // - name: repo
  114. // in: path
  115. // description: name of the repo
  116. // type: string
  117. // required: true
  118. // - name: filepath
  119. // in: path
  120. // description: filepath of file to get
  121. // type: string
  122. // required: true
  123. // responses:
  124. // 200:
  125. // description: success
  126. // "404":
  127. // "$ref": "#/responses/notFound"
  128. ec, err := ctx.Repo.GetEditorconfig()
  129. if err != nil {
  130. if git.IsErrNotExist(err) {
  131. ctx.NotFound(err)
  132. } else {
  133. ctx.Error(http.StatusInternalServerError, "GetEditorconfig", err)
  134. }
  135. return
  136. }
  137. fileName := ctx.Params("filename")
  138. def, err := ec.GetDefinitionForFilename(fileName)
  139. if def == nil {
  140. ctx.NotFound(err)
  141. return
  142. }
  143. ctx.JSON(http.StatusOK, def)
  144. }
  145. // canWriteFiles returns true if repository is editable and user has proper access level.
  146. func canWriteFiles(r *context.Repository) bool {
  147. return r.Permission.CanWrite(models.UnitTypeCode) && !r.Repository.IsMirror && !r.Repository.IsArchived
  148. }
  149. // canReadFiles returns true if repository is readable and user has proper access level.
  150. func canReadFiles(r *context.Repository) bool {
  151. return r.Permission.CanRead(models.UnitTypeCode)
  152. }
  153. // CreateFile handles API call for creating a file
  154. func CreateFile(ctx *context.APIContext, apiOpts api.CreateFileOptions) {
  155. // swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
  156. // ---
  157. // summary: Create a file in a repository
  158. // consumes:
  159. // - application/json
  160. // produces:
  161. // - application/json
  162. // parameters:
  163. // - name: owner
  164. // in: path
  165. // description: owner of the repo
  166. // type: string
  167. // required: true
  168. // - name: repo
  169. // in: path
  170. // description: name of the repo
  171. // type: string
  172. // required: true
  173. // - name: filepath
  174. // in: path
  175. // description: path of the file to create
  176. // type: string
  177. // required: true
  178. // - name: body
  179. // in: body
  180. // required: true
  181. // schema:
  182. // "$ref": "#/definitions/CreateFileOptions"
  183. // responses:
  184. // "201":
  185. // "$ref": "#/responses/FileResponse"
  186. if apiOpts.BranchName == "" {
  187. apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
  188. }
  189. opts := &repofiles.UpdateRepoFileOptions{
  190. Content: apiOpts.Content,
  191. IsNewFile: true,
  192. Message: apiOpts.Message,
  193. TreePath: ctx.Params("*"),
  194. OldBranch: apiOpts.BranchName,
  195. NewBranch: apiOpts.NewBranchName,
  196. Committer: &repofiles.IdentityOptions{
  197. Name: apiOpts.Committer.Name,
  198. Email: apiOpts.Committer.Email,
  199. },
  200. Author: &repofiles.IdentityOptions{
  201. Name: apiOpts.Author.Name,
  202. Email: apiOpts.Author.Email,
  203. },
  204. Dates: &repofiles.CommitDateOptions{
  205. Author: apiOpts.Dates.Author,
  206. Committer: apiOpts.Dates.Committer,
  207. },
  208. }
  209. if opts.Dates.Author.IsZero() {
  210. opts.Dates.Author = time.Now()
  211. }
  212. if opts.Dates.Committer.IsZero() {
  213. opts.Dates.Committer = time.Now()
  214. }
  215. if opts.Message == "" {
  216. opts.Message = ctx.Tr("repo.editor.add", opts.TreePath)
  217. }
  218. if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
  219. ctx.Error(http.StatusInternalServerError, "CreateFile", err)
  220. } else {
  221. ctx.JSON(http.StatusCreated, fileResponse)
  222. }
  223. }
  224. // UpdateFile handles API call for updating a file
  225. func UpdateFile(ctx *context.APIContext, apiOpts api.UpdateFileOptions) {
  226. // swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
  227. // ---
  228. // summary: Update a file in a repository
  229. // consumes:
  230. // - application/json
  231. // produces:
  232. // - application/json
  233. // parameters:
  234. // - name: owner
  235. // in: path
  236. // description: owner of the repo
  237. // type: string
  238. // required: true
  239. // - name: repo
  240. // in: path
  241. // description: name of the repo
  242. // type: string
  243. // required: true
  244. // - name: filepath
  245. // in: path
  246. // description: path of the file to update
  247. // type: string
  248. // required: true
  249. // - name: body
  250. // in: body
  251. // required: true
  252. // schema:
  253. // "$ref": "#/definitions/UpdateFileOptions"
  254. // responses:
  255. // "200":
  256. // "$ref": "#/responses/FileResponse"
  257. if apiOpts.BranchName == "" {
  258. apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
  259. }
  260. opts := &repofiles.UpdateRepoFileOptions{
  261. Content: apiOpts.Content,
  262. SHA: apiOpts.SHA,
  263. IsNewFile: false,
  264. Message: apiOpts.Message,
  265. FromTreePath: apiOpts.FromPath,
  266. TreePath: ctx.Params("*"),
  267. OldBranch: apiOpts.BranchName,
  268. NewBranch: apiOpts.NewBranchName,
  269. Committer: &repofiles.IdentityOptions{
  270. Name: apiOpts.Committer.Name,
  271. Email: apiOpts.Committer.Email,
  272. },
  273. Author: &repofiles.IdentityOptions{
  274. Name: apiOpts.Author.Name,
  275. Email: apiOpts.Author.Email,
  276. },
  277. Dates: &repofiles.CommitDateOptions{
  278. Author: apiOpts.Dates.Author,
  279. Committer: apiOpts.Dates.Committer,
  280. },
  281. }
  282. if opts.Dates.Author.IsZero() {
  283. opts.Dates.Author = time.Now()
  284. }
  285. if opts.Dates.Committer.IsZero() {
  286. opts.Dates.Committer = time.Now()
  287. }
  288. if opts.Message == "" {
  289. opts.Message = ctx.Tr("repo.editor.update", opts.TreePath)
  290. }
  291. if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
  292. ctx.Error(http.StatusInternalServerError, "UpdateFile", err)
  293. } else {
  294. ctx.JSON(http.StatusOK, fileResponse)
  295. }
  296. }
  297. // Called from both CreateFile or UpdateFile to handle both
  298. func createOrUpdateFile(ctx *context.APIContext, opts *repofiles.UpdateRepoFileOptions) (*api.FileResponse, error) {
  299. if !canWriteFiles(ctx.Repo) {
  300. return nil, models.ErrUserDoesNotHaveAccessToRepo{
  301. UserID: ctx.User.ID,
  302. RepoName: ctx.Repo.Repository.LowerName,
  303. }
  304. }
  305. content, err := base64.StdEncoding.DecodeString(opts.Content)
  306. if err != nil {
  307. return nil, err
  308. }
  309. opts.Content = string(content)
  310. return repofiles.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, opts)
  311. }
  312. // DeleteFile Delete a fle in a repository
  313. func DeleteFile(ctx *context.APIContext, apiOpts api.DeleteFileOptions) {
  314. // swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
  315. // ---
  316. // summary: Delete a file in a repository
  317. // consumes:
  318. // - application/json
  319. // produces:
  320. // - application/json
  321. // parameters:
  322. // - name: owner
  323. // in: path
  324. // description: owner of the repo
  325. // type: string
  326. // required: true
  327. // - name: repo
  328. // in: path
  329. // description: name of the repo
  330. // type: string
  331. // required: true
  332. // - name: filepath
  333. // in: path
  334. // description: path of the file to delete
  335. // type: string
  336. // required: true
  337. // - name: body
  338. // in: body
  339. // required: true
  340. // schema:
  341. // "$ref": "#/definitions/DeleteFileOptions"
  342. // responses:
  343. // "200":
  344. // "$ref": "#/responses/FileDeleteResponse"
  345. // "400":
  346. // "$ref": "#/responses/error"
  347. // "403":
  348. // "$ref": "#/responses/error"
  349. // "404":
  350. // "$ref": "#/responses/error"
  351. if !canWriteFiles(ctx.Repo) {
  352. ctx.Error(http.StatusForbidden, "DeleteFile", models.ErrUserDoesNotHaveAccessToRepo{
  353. UserID: ctx.User.ID,
  354. RepoName: ctx.Repo.Repository.LowerName,
  355. })
  356. return
  357. }
  358. if apiOpts.BranchName == "" {
  359. apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
  360. }
  361. opts := &repofiles.DeleteRepoFileOptions{
  362. Message: apiOpts.Message,
  363. OldBranch: apiOpts.BranchName,
  364. NewBranch: apiOpts.NewBranchName,
  365. SHA: apiOpts.SHA,
  366. TreePath: ctx.Params("*"),
  367. Committer: &repofiles.IdentityOptions{
  368. Name: apiOpts.Committer.Name,
  369. Email: apiOpts.Committer.Email,
  370. },
  371. Author: &repofiles.IdentityOptions{
  372. Name: apiOpts.Author.Name,
  373. Email: apiOpts.Author.Email,
  374. },
  375. Dates: &repofiles.CommitDateOptions{
  376. Author: apiOpts.Dates.Author,
  377. Committer: apiOpts.Dates.Committer,
  378. },
  379. }
  380. if opts.Dates.Author.IsZero() {
  381. opts.Dates.Author = time.Now()
  382. }
  383. if opts.Dates.Committer.IsZero() {
  384. opts.Dates.Committer = time.Now()
  385. }
  386. if opts.Message == "" {
  387. opts.Message = ctx.Tr("repo.editor.delete", opts.TreePath)
  388. }
  389. if fileResponse, err := repofiles.DeleteRepoFile(ctx.Repo.Repository, ctx.User, opts); err != nil {
  390. if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
  391. ctx.Error(http.StatusNotFound, "DeleteFile", err)
  392. return
  393. } else if models.IsErrBranchAlreadyExists(err) ||
  394. models.IsErrFilenameInvalid(err) ||
  395. models.IsErrSHADoesNotMatch(err) ||
  396. models.IsErrCommitIDDoesNotMatch(err) ||
  397. models.IsErrSHAOrCommitIDNotProvided(err) {
  398. ctx.Error(http.StatusBadRequest, "DeleteFile", err)
  399. return
  400. } else if models.IsErrUserCannotCommit(err) {
  401. ctx.Error(http.StatusForbidden, "DeleteFile", err)
  402. return
  403. }
  404. ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
  405. } else {
  406. ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
  407. }
  408. }
  409. // GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
  410. func GetContents(ctx *context.APIContext) {
  411. // swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
  412. // ---
  413. // summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
  414. // produces:
  415. // - application/json
  416. // parameters:
  417. // - name: owner
  418. // in: path
  419. // description: owner of the repo
  420. // type: string
  421. // required: true
  422. // - name: repo
  423. // in: path
  424. // description: name of the repo
  425. // type: string
  426. // required: true
  427. // - name: filepath
  428. // in: path
  429. // description: path of the dir, file, symlink or submodule in the repo
  430. // type: string
  431. // required: true
  432. // - name: ref
  433. // in: query
  434. // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
  435. // type: string
  436. // required: false
  437. // responses:
  438. // "200":
  439. // "$ref": "#/responses/ContentsResponse"
  440. // "404":
  441. // "$ref": "#/responses/notFound"
  442. if !canReadFiles(ctx.Repo) {
  443. ctx.Error(http.StatusInternalServerError, "GetContentsOrList", models.ErrUserDoesNotHaveAccessToRepo{
  444. UserID: ctx.User.ID,
  445. RepoName: ctx.Repo.Repository.LowerName,
  446. })
  447. return
  448. }
  449. treePath := ctx.Params("*")
  450. ref := ctx.QueryTrim("ref")
  451. if fileList, err := repofiles.GetContentsOrList(ctx.Repo.Repository, treePath, ref); err != nil {
  452. if git.IsErrNotExist(err) {
  453. ctx.NotFound("GetContentsOrList", err)
  454. return
  455. }
  456. ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)
  457. } else {
  458. ctx.JSON(http.StatusOK, fileList)
  459. }
  460. }
  461. // GetContentsList Get the metadata of all the entries of the root dir
  462. func GetContentsList(ctx *context.APIContext) {
  463. // swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
  464. // ---
  465. // summary: Gets the metadata of all the entries of the root dir
  466. // produces:
  467. // - application/json
  468. // parameters:
  469. // - name: owner
  470. // in: path
  471. // description: owner of the repo
  472. // type: string
  473. // required: true
  474. // - name: repo
  475. // in: path
  476. // description: name of the repo
  477. // type: string
  478. // required: true
  479. // - name: ref
  480. // in: query
  481. // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
  482. // type: string
  483. // required: false
  484. // responses:
  485. // "200":
  486. // "$ref": "#/responses/ContentsListResponse"
  487. // "404":
  488. // "$ref": "#/responses/notFound"
  489. // same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
  490. GetContents(ctx)
  491. }