gitea源码

editor.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "bytes"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "path"
  10. "strings"
  11. git_model "code.gitea.io/gitea/models/git"
  12. "code.gitea.io/gitea/models/issues"
  13. "code.gitea.io/gitea/models/unit"
  14. "code.gitea.io/gitea/modules/charset"
  15. "code.gitea.io/gitea/modules/git"
  16. "code.gitea.io/gitea/modules/httplib"
  17. "code.gitea.io/gitea/modules/log"
  18. "code.gitea.io/gitea/modules/markup"
  19. "code.gitea.io/gitea/modules/setting"
  20. "code.gitea.io/gitea/modules/templates"
  21. "code.gitea.io/gitea/modules/util"
  22. "code.gitea.io/gitea/modules/web"
  23. "code.gitea.io/gitea/services/context"
  24. "code.gitea.io/gitea/services/context/upload"
  25. "code.gitea.io/gitea/services/forms"
  26. files_service "code.gitea.io/gitea/services/repository/files"
  27. )
  28. const (
  29. tplEditFile templates.TplName = "repo/editor/edit"
  30. tplEditDiffPreview templates.TplName = "repo/editor/diff_preview"
  31. tplDeleteFile templates.TplName = "repo/editor/delete"
  32. tplUploadFile templates.TplName = "repo/editor/upload"
  33. tplPatchFile templates.TplName = "repo/editor/patch"
  34. tplCherryPick templates.TplName = "repo/editor/cherry_pick"
  35. editorCommitChoiceDirect string = "direct"
  36. editorCommitChoiceNewBranch string = "commit-to-new-branch"
  37. )
  38. func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
  39. cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
  40. if cleanedTreePath != ctx.Repo.TreePath {
  41. redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
  42. if ctx.Req.URL.RawQuery != "" {
  43. redirectTo += "?" + ctx.Req.URL.RawQuery
  44. }
  45. ctx.Redirect(redirectTo)
  46. return nil
  47. }
  48. commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
  49. if err != nil {
  50. ctx.ServerError("PrepareCommitFormOptions", err)
  51. return nil
  52. }
  53. if commitFormOptions.NeedFork {
  54. ForkToEdit(ctx)
  55. return nil
  56. }
  57. if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() {
  58. ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable")
  59. ctx.NotFound(nil)
  60. }
  61. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
  62. ctx.Data["TreePath"] = ctx.Repo.TreePath
  63. ctx.Data["CommitFormOptions"] = commitFormOptions
  64. // for online editor
  65. ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
  66. ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
  67. ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
  68. ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
  69. // form fields
  70. ctx.Data["commit_summary"] = ""
  71. ctx.Data["commit_message"] = ""
  72. ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
  73. ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo)
  74. ctx.Data["last_commit"] = ctx.Repo.CommitID
  75. return commitFormOptions
  76. }
  77. func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
  78. // show the tree path fields in the "breadcrumb" and help users to edit the target tree path
  79. ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/"))
  80. }
  81. type preparedEditorCommitForm[T any] struct {
  82. form T
  83. commonForm *forms.CommitCommonForm
  84. CommitFormOptions *context.CommitFormOptions
  85. OldBranchName string
  86. NewBranchName string
  87. GitCommitter *files_service.IdentityOptions
  88. }
  89. func (f *preparedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
  90. commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage)
  91. if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
  92. commitMessage += "\n\n" + body
  93. }
  94. return commitMessage
  95. }
  96. func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] {
  97. form := web.GetForm(ctx).(T)
  98. if ctx.HasError() {
  99. ctx.JSONError(ctx.GetErrMsg())
  100. return nil
  101. }
  102. commonForm := form.GetCommitCommonForm()
  103. commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
  104. commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
  105. if err != nil {
  106. ctx.ServerError("PrepareCommitFormOptions", err)
  107. return nil
  108. }
  109. if commitFormOptions.NeedFork {
  110. // It shouldn't happen, because we should have done the checks in the "GET" request. But just in case.
  111. ctx.JSONError(ctx.Locale.TrString("error.not_found"))
  112. return nil
  113. }
  114. // check commit behavior
  115. fromBaseBranch := ctx.FormString("from_base_branch")
  116. commitToNewBranch := commonForm.CommitChoice == editorCommitChoiceNewBranch || fromBaseBranch != ""
  117. targetBranchName := util.Iif(commitToNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
  118. if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch {
  119. ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName))
  120. return nil
  121. }
  122. if !issues.CanMaintainerWriteToBranch(ctx, ctx.Repo.Permission, targetBranchName, ctx.Doer) {
  123. ctx.NotFound(nil)
  124. return nil
  125. }
  126. // Committer user info
  127. gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, commonForm.CommitEmail)
  128. if !valid {
  129. ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email"))
  130. return nil
  131. }
  132. if commitToNewBranch {
  133. // if target branch exists, we should stop
  134. targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName)
  135. if err != nil {
  136. ctx.ServerError("IsBranchExist", err)
  137. return nil
  138. } else if targetBranchExists {
  139. if fromBaseBranch != "" {
  140. ctx.JSONError(ctx.Tr("repo.editor.fork_branch_exists", targetBranchName))
  141. } else {
  142. ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", targetBranchName))
  143. }
  144. return nil
  145. }
  146. }
  147. oldBranchName := ctx.Repo.BranchName
  148. if fromBaseBranch != "" {
  149. err = editorPushBranchToForkedRepository(ctx, ctx.Doer, ctx.Repo.Repository.BaseRepo, fromBaseBranch, commitFormOptions.TargetRepo, targetBranchName)
  150. if err != nil {
  151. log.Error("Unable to editorPushBranchToForkedRepository: %v", err)
  152. ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName))
  153. return nil
  154. }
  155. // we have pushed the base branch as the new branch, now we need to commit the changes directly to the new branch
  156. oldBranchName = targetBranchName
  157. }
  158. return &preparedEditorCommitForm[T]{
  159. form: form,
  160. commonForm: commonForm,
  161. CommitFormOptions: commitFormOptions,
  162. OldBranchName: oldBranchName,
  163. NewBranchName: targetBranchName,
  164. GitCommitter: gitCommitter,
  165. }
  166. }
  167. // redirectForCommitChoice redirects after committing the edit to a branch
  168. func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) {
  169. // when editing a file in a PR, it should return to the origin location
  170. if returnURI := ctx.FormString("return_uri"); returnURI != "" && httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
  171. ctx.JSONRedirect(returnURI)
  172. return
  173. }
  174. if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch {
  175. // Redirect to a pull request when possible
  176. redirectToPullRequest := false
  177. repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName
  178. if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest {
  179. redirectToPullRequest = true
  180. baseBranch = repo.BaseRepo.DefaultBranch
  181. headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
  182. repo = repo.BaseRepo
  183. } else if repo.UnitEnabled(ctx, unit.TypePullRequests) {
  184. redirectToPullRequest = true
  185. }
  186. if redirectToPullRequest {
  187. ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
  188. return
  189. }
  190. }
  191. // redirect to the newly updated file
  192. redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
  193. ctx.JSONRedirect(redirectTo)
  194. }
  195. func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) {
  196. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
  197. if err != nil {
  198. HandleGitError(ctx, "GetTreeEntryByPath", err)
  199. return nil, nil, nil
  200. }
  201. // No way to edit a directory online.
  202. if entry.IsDir() {
  203. ctx.NotFound(nil)
  204. return nil, nil, nil
  205. }
  206. blob := entry.Blob()
  207. buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
  208. if err != nil {
  209. if git.IsErrNotExist(err) {
  210. ctx.NotFound(err)
  211. } else {
  212. ctx.ServerError("getFileReader", err)
  213. }
  214. return nil, nil, nil
  215. }
  216. if fInfo.isLFSFile() {
  217. lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
  218. if err != nil {
  219. _ = dataRc.Close()
  220. ctx.ServerError("GetTreePathLock", err)
  221. return nil, nil, nil
  222. } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
  223. _ = dataRc.Close()
  224. ctx.NotFound(nil)
  225. return nil, nil, nil
  226. }
  227. }
  228. return buf, dataRc, fInfo
  229. }
  230. func EditFile(ctx *context.Context) {
  231. editorAction := ctx.PathParam("editor_action")
  232. isNewFile := editorAction == "_new"
  233. ctx.Data["IsNewFile"] = isNewFile
  234. // Check if the filename (and additional path) is specified in the querystring
  235. // (filename is a misnomer, but kept for compatibility with GitHub)
  236. urlQuery := ctx.Req.URL.Query()
  237. queryFilename := urlQuery.Get("filename")
  238. if queryFilename != "" {
  239. newTreePath := path.Join(ctx.Repo.TreePath, queryFilename)
  240. redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(newTreePath))
  241. urlQuery.Del("filename")
  242. if newQueryParams := urlQuery.Encode(); newQueryParams != "" {
  243. redirectTo += "?" + newQueryParams
  244. }
  245. ctx.Redirect(redirectTo)
  246. return
  247. }
  248. // on the "New File" page, we should add an empty path field to make end users could input a new name
  249. prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath))
  250. prepareEditorCommitFormOptions(ctx, editorAction)
  251. if ctx.Written() {
  252. return
  253. }
  254. if !isNewFile {
  255. prefetch, dataRc, fInfo := editFileOpenExisting(ctx)
  256. if ctx.Written() {
  257. return
  258. }
  259. defer dataRc.Close()
  260. ctx.Data["FileSize"] = fInfo.fileSize
  261. // Only some file types are editable online as text.
  262. if fInfo.isLFSFile() {
  263. ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
  264. } else if !fInfo.st.IsRepresentableAsText() {
  265. ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
  266. } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
  267. ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file")
  268. }
  269. if ctx.Data["NotEditableReason"] == nil {
  270. buf, err := io.ReadAll(io.MultiReader(bytes.NewReader(prefetch), dataRc))
  271. if err != nil {
  272. ctx.ServerError("ReadAll", err)
  273. return
  274. }
  275. if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
  276. ctx.Data["FileContent"] = string(buf)
  277. } else {
  278. ctx.Data["FileContent"] = content
  279. }
  280. }
  281. }
  282. ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath)
  283. ctx.HTML(http.StatusOK, tplEditFile)
  284. }
  285. func EditFilePost(ctx *context.Context) {
  286. editorAction := ctx.PathParam("editor_action")
  287. isNewFile := editorAction == "_new"
  288. parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
  289. if ctx.Written() {
  290. return
  291. }
  292. defaultCommitMessage := util.Iif(isNewFile, ctx.Locale.TrString("repo.editor.add", parsed.form.TreePath), ctx.Locale.TrString("repo.editor.update", parsed.form.TreePath))
  293. var operation string
  294. if isNewFile {
  295. operation = "create"
  296. } else if parsed.form.Content.Has() {
  297. // The form content only has data if the file is representable as text, is not too large and not in lfs.
  298. operation = "update"
  299. } else if ctx.Repo.TreePath != parsed.form.TreePath {
  300. // If it doesn't have data, the only possible operation is a "rename"
  301. operation = "rename"
  302. } else {
  303. // It should never happen, just in case
  304. ctx.JSONError(ctx.Tr("error.occurred"))
  305. return
  306. }
  307. _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
  308. LastCommitID: parsed.form.LastCommit,
  309. OldBranch: parsed.OldBranchName,
  310. NewBranch: parsed.NewBranchName,
  311. Message: parsed.GetCommitMessage(defaultCommitMessage),
  312. Files: []*files_service.ChangeRepoFile{
  313. {
  314. Operation: operation,
  315. FromTreePath: ctx.Repo.TreePath,
  316. TreePath: parsed.form.TreePath,
  317. ContentReader: strings.NewReader(strings.ReplaceAll(parsed.form.Content.Value(), "\r", "")),
  318. },
  319. },
  320. Signoff: parsed.form.Signoff,
  321. Author: parsed.GitCommitter,
  322. Committer: parsed.GitCommitter,
  323. })
  324. if err != nil {
  325. editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
  326. return
  327. }
  328. redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
  329. }
  330. // DeleteFile render delete file page
  331. func DeleteFile(ctx *context.Context) {
  332. prepareEditorCommitFormOptions(ctx, "_delete")
  333. if ctx.Written() {
  334. return
  335. }
  336. ctx.Data["PageIsDelete"] = true
  337. ctx.HTML(http.StatusOK, tplDeleteFile)
  338. }
  339. // DeleteFilePost response for deleting file
  340. func DeleteFilePost(ctx *context.Context) {
  341. parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
  342. if ctx.Written() {
  343. return
  344. }
  345. treePath := ctx.Repo.TreePath
  346. _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
  347. LastCommitID: parsed.form.LastCommit,
  348. OldBranch: parsed.OldBranchName,
  349. NewBranch: parsed.NewBranchName,
  350. Files: []*files_service.ChangeRepoFile{
  351. {
  352. Operation: "delete",
  353. TreePath: treePath,
  354. },
  355. },
  356. Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
  357. Signoff: parsed.form.Signoff,
  358. Author: parsed.GitCommitter,
  359. Committer: parsed.GitCommitter,
  360. })
  361. if err != nil {
  362. editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
  363. return
  364. }
  365. ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
  366. redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
  367. redirectForCommitChoice(ctx, parsed, redirectTreePath)
  368. }
  369. func UploadFile(ctx *context.Context) {
  370. ctx.Data["PageIsUpload"] = true
  371. prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
  372. opts := prepareEditorCommitFormOptions(ctx, "_upload")
  373. if ctx.Written() {
  374. return
  375. }
  376. upload.AddUploadContextForRepo(ctx, opts.TargetRepo)
  377. ctx.HTML(http.StatusOK, tplUploadFile)
  378. }
  379. func UploadFilePost(ctx *context.Context) {
  380. parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
  381. if ctx.Written() {
  382. return
  383. }
  384. defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/"))
  385. err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
  386. LastCommitID: parsed.form.LastCommit,
  387. OldBranch: parsed.OldBranchName,
  388. NewBranch: parsed.NewBranchName,
  389. TreePath: parsed.form.TreePath,
  390. Message: parsed.GetCommitMessage(defaultCommitMessage),
  391. Files: parsed.form.Files,
  392. Signoff: parsed.form.Signoff,
  393. Author: parsed.GitCommitter,
  394. Committer: parsed.GitCommitter,
  395. })
  396. if err != nil {
  397. editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
  398. return
  399. }
  400. redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
  401. }