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.

editor.go 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "path"
  9. "strings"
  10. "code.gitea.io/gitea/models"
  11. git_model "code.gitea.io/gitea/models/git"
  12. repo_model "code.gitea.io/gitea/models/repo"
  13. "code.gitea.io/gitea/models/unit"
  14. "code.gitea.io/gitea/modules/base"
  15. "code.gitea.io/gitea/modules/charset"
  16. "code.gitea.io/gitea/modules/context"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/json"
  19. "code.gitea.io/gitea/modules/log"
  20. "code.gitea.io/gitea/modules/markup"
  21. "code.gitea.io/gitea/modules/setting"
  22. "code.gitea.io/gitea/modules/typesniffer"
  23. "code.gitea.io/gitea/modules/upload"
  24. "code.gitea.io/gitea/modules/util"
  25. "code.gitea.io/gitea/modules/web"
  26. "code.gitea.io/gitea/routers/utils"
  27. "code.gitea.io/gitea/services/forms"
  28. files_service "code.gitea.io/gitea/services/repository/files"
  29. )
  30. const (
  31. tplEditFile base.TplName = "repo/editor/edit"
  32. tplEditDiffPreview base.TplName = "repo/editor/diff_preview"
  33. tplDeleteFile base.TplName = "repo/editor/delete"
  34. tplUploadFile base.TplName = "repo/editor/upload"
  35. frmCommitChoiceDirect string = "direct"
  36. frmCommitChoiceNewBranch string = "commit-to-new-branch"
  37. )
  38. func canCreateBasePullRequest(ctx *context.Context) bool {
  39. baseRepo := ctx.Repo.Repository.BaseRepo
  40. return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests)
  41. }
  42. func renderCommitRights(ctx *context.Context) bool {
  43. canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer)
  44. if err != nil {
  45. log.Error("CanCommitToBranch: %v", err)
  46. }
  47. ctx.Data["CanCommitToBranch"] = canCommitToBranch
  48. ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx)
  49. return canCommitToBranch.CanCommitToBranch
  50. }
  51. // redirectForCommitChoice redirects after committing the edit to a branch
  52. func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) {
  53. if commitChoice == frmCommitChoiceNewBranch {
  54. // Redirect to a pull request when possible
  55. redirectToPullRequest := false
  56. repo := ctx.Repo.Repository
  57. baseBranch := ctx.Repo.BranchName
  58. headBranch := newBranchName
  59. if repo.UnitEnabled(ctx, unit.TypePullRequests) {
  60. redirectToPullRequest = true
  61. } else if canCreateBasePullRequest(ctx) {
  62. redirectToPullRequest = true
  63. baseBranch = repo.BaseRepo.DefaultBranch
  64. headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
  65. repo = repo.BaseRepo
  66. }
  67. if redirectToPullRequest {
  68. ctx.Redirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
  69. return
  70. }
  71. }
  72. // Redirect to viewing file or folder
  73. ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(newBranchName) + "/" + util.PathEscapeSegments(treePath))
  74. }
  75. // getParentTreeFields returns list of parent tree names and corresponding tree paths
  76. // based on given tree path.
  77. func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
  78. if len(treePath) == 0 {
  79. return treeNames, treePaths
  80. }
  81. treeNames = strings.Split(treePath, "/")
  82. treePaths = make([]string, len(treeNames))
  83. for i := range treeNames {
  84. treePaths[i] = strings.Join(treeNames[:i+1], "/")
  85. }
  86. return treeNames, treePaths
  87. }
  88. func editFile(ctx *context.Context, isNewFile bool) {
  89. ctx.Data["PageIsEdit"] = true
  90. ctx.Data["IsNewFile"] = isNewFile
  91. canCommit := renderCommitRights(ctx)
  92. treePath := cleanUploadFileName(ctx.Repo.TreePath)
  93. if treePath != ctx.Repo.TreePath {
  94. if isNewFile {
  95. ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
  96. } else {
  97. ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
  98. }
  99. return
  100. }
  101. // Check if the filename (and additional path) is specified in the querystring
  102. // (filename is a misnomer, but kept for compatibility with GitHub)
  103. filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename"))
  104. filePath = strings.Trim(filePath, "/")
  105. treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath))
  106. if !isNewFile {
  107. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
  108. if err != nil {
  109. HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
  110. return
  111. }
  112. // No way to edit a directory online.
  113. if entry.IsDir() {
  114. ctx.NotFound("entry.IsDir", nil)
  115. return
  116. }
  117. blob := entry.Blob()
  118. if blob.Size() >= setting.UI.MaxDisplayFileSize {
  119. ctx.NotFound("blob.Size", err)
  120. return
  121. }
  122. dataRc, err := blob.DataAsync()
  123. if err != nil {
  124. ctx.NotFound("blob.Data", err)
  125. return
  126. }
  127. defer dataRc.Close()
  128. ctx.Data["FileSize"] = blob.Size()
  129. ctx.Data["FileName"] = blob.Name()
  130. buf := make([]byte, 1024)
  131. n, _ := util.ReadAtMost(dataRc, buf)
  132. buf = buf[:n]
  133. // Only some file types are editable online as text.
  134. if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
  135. ctx.NotFound("typesniffer.IsRepresentableAsText", nil)
  136. return
  137. }
  138. d, _ := io.ReadAll(dataRc)
  139. buf = append(buf, d...)
  140. if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
  141. log.Error("ToUTF8: %v", err)
  142. ctx.Data["FileContent"] = string(buf)
  143. } else {
  144. ctx.Data["FileContent"] = content
  145. }
  146. } else {
  147. // Append filename from query, or empty string to allow user name the new file.
  148. treeNames = append(treeNames, fileName)
  149. }
  150. ctx.Data["TreeNames"] = treeNames
  151. ctx.Data["TreePaths"] = treePaths
  152. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  153. ctx.Data["commit_summary"] = ""
  154. ctx.Data["commit_message"] = ""
  155. if canCommit {
  156. ctx.Data["commit_choice"] = frmCommitChoiceDirect
  157. } else {
  158. ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
  159. }
  160. ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
  161. ctx.Data["last_commit"] = ctx.Repo.CommitID
  162. ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
  163. ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
  164. ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
  165. ctx.HTML(http.StatusOK, tplEditFile)
  166. }
  167. // GetEditorConfig returns a editorconfig JSON string for given treePath or "null"
  168. func GetEditorConfig(ctx *context.Context, treePath string) string {
  169. ec, _, err := ctx.Repo.GetEditorconfig()
  170. if err == nil {
  171. def, err := ec.GetDefinitionForFilename(treePath)
  172. if err == nil {
  173. jsonStr, _ := json.Marshal(def)
  174. return string(jsonStr)
  175. }
  176. }
  177. return "null"
  178. }
  179. // EditFile render edit file page
  180. func EditFile(ctx *context.Context) {
  181. editFile(ctx, false)
  182. }
  183. // NewFile render create file page
  184. func NewFile(ctx *context.Context) {
  185. editFile(ctx, true)
  186. }
  187. func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
  188. canCommit := renderCommitRights(ctx)
  189. treeNames, treePaths := getParentTreeFields(form.TreePath)
  190. branchName := ctx.Repo.BranchName
  191. if form.CommitChoice == frmCommitChoiceNewBranch {
  192. branchName = form.NewBranchName
  193. }
  194. ctx.Data["PageIsEdit"] = true
  195. ctx.Data["PageHasPosted"] = true
  196. ctx.Data["IsNewFile"] = isNewFile
  197. ctx.Data["TreePath"] = form.TreePath
  198. ctx.Data["TreeNames"] = treeNames
  199. ctx.Data["TreePaths"] = treePaths
  200. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
  201. ctx.Data["FileContent"] = form.Content
  202. ctx.Data["commit_summary"] = form.CommitSummary
  203. ctx.Data["commit_message"] = form.CommitMessage
  204. ctx.Data["commit_choice"] = form.CommitChoice
  205. ctx.Data["new_branch_name"] = form.NewBranchName
  206. ctx.Data["last_commit"] = ctx.Repo.CommitID
  207. ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
  208. ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
  209. ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
  210. if ctx.HasError() {
  211. ctx.HTML(http.StatusOK, tplEditFile)
  212. return
  213. }
  214. // Cannot commit to a an existing branch if user doesn't have rights
  215. if branchName == ctx.Repo.BranchName && !canCommit {
  216. ctx.Data["Err_NewBranchName"] = true
  217. ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
  218. ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
  219. return
  220. }
  221. // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
  222. // `message` will be both the summary and message combined
  223. message := strings.TrimSpace(form.CommitSummary)
  224. if len(message) == 0 {
  225. if isNewFile {
  226. message = ctx.Tr("repo.editor.add", form.TreePath)
  227. } else {
  228. message = ctx.Tr("repo.editor.update", form.TreePath)
  229. }
  230. }
  231. form.CommitMessage = strings.TrimSpace(form.CommitMessage)
  232. if len(form.CommitMessage) > 0 {
  233. message += "\n\n" + form.CommitMessage
  234. }
  235. operation := "update"
  236. if isNewFile {
  237. operation = "create"
  238. }
  239. if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
  240. LastCommitID: form.LastCommit,
  241. OldBranch: ctx.Repo.BranchName,
  242. NewBranch: branchName,
  243. Message: message,
  244. Files: []*files_service.ChangeRepoFile{
  245. {
  246. Operation: operation,
  247. FromTreePath: ctx.Repo.TreePath,
  248. TreePath: form.TreePath,
  249. ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
  250. },
  251. },
  252. Signoff: form.Signoff,
  253. }); err != nil {
  254. // This is where we handle all the errors thrown by files_service.ChangeRepoFiles
  255. if git.IsErrNotExist(err) {
  256. ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
  257. } else if git_model.IsErrLFSFileLocked(err) {
  258. ctx.Data["Err_TreePath"] = true
  259. ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form)
  260. } else if models.IsErrFilenameInvalid(err) {
  261. ctx.Data["Err_TreePath"] = true
  262. ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
  263. } else if models.IsErrFilePathInvalid(err) {
  264. ctx.Data["Err_TreePath"] = true
  265. if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
  266. switch fileErr.Type {
  267. case git.EntryModeSymlink:
  268. ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
  269. case git.EntryModeTree:
  270. ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
  271. case git.EntryModeBlob:
  272. ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
  273. default:
  274. ctx.Error(http.StatusInternalServerError, err.Error())
  275. }
  276. } else {
  277. ctx.Error(http.StatusInternalServerError, err.Error())
  278. }
  279. } else if models.IsErrRepoFileAlreadyExists(err) {
  280. ctx.Data["Err_TreePath"] = true
  281. ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
  282. } else if git.IsErrBranchNotExist(err) {
  283. // For when a user adds/updates a file to a branch that no longer exists
  284. if branchErr, ok := err.(git.ErrBranchNotExist); ok {
  285. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
  286. } else {
  287. ctx.Error(http.StatusInternalServerError, err.Error())
  288. }
  289. } else if git_model.IsErrBranchAlreadyExists(err) {
  290. // For when a user specifies a new branch that already exists
  291. ctx.Data["Err_NewBranchName"] = true
  292. if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
  293. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
  294. } else {
  295. ctx.Error(http.StatusInternalServerError, err.Error())
  296. }
  297. } else if models.IsErrCommitIDDoesNotMatch(err) {
  298. ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplEditFile, &form)
  299. } else if git.IsErrPushOutOfDate(err) {
  300. ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form)
  301. } else if git.IsErrPushRejected(err) {
  302. errPushRej := err.(*git.ErrPushRejected)
  303. if len(errPushRej.Message) == 0 {
  304. ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
  305. } else {
  306. flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  307. "Message": ctx.Tr("repo.editor.push_rejected"),
  308. "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
  309. "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
  310. })
  311. if err != nil {
  312. ctx.ServerError("editFilePost.HTMLString", err)
  313. return
  314. }
  315. ctx.RenderWithErr(flashError, tplEditFile, &form)
  316. }
  317. } else {
  318. flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  319. "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
  320. "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
  321. "Details": utils.SanitizeFlashErrorString(err.Error()),
  322. })
  323. if err != nil {
  324. ctx.ServerError("editFilePost.HTMLString", err)
  325. return
  326. }
  327. ctx.RenderWithErr(flashError, tplEditFile, &form)
  328. }
  329. }
  330. if ctx.Repo.Repository.IsEmpty {
  331. if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
  332. _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
  333. }
  334. }
  335. redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
  336. }
  337. // EditFilePost response for editing file
  338. func EditFilePost(ctx *context.Context) {
  339. form := web.GetForm(ctx).(*forms.EditRepoFileForm)
  340. editFilePost(ctx, *form, false)
  341. }
  342. // NewFilePost response for creating file
  343. func NewFilePost(ctx *context.Context) {
  344. form := web.GetForm(ctx).(*forms.EditRepoFileForm)
  345. editFilePost(ctx, *form, true)
  346. }
  347. // DiffPreviewPost render preview diff page
  348. func DiffPreviewPost(ctx *context.Context) {
  349. form := web.GetForm(ctx).(*forms.EditPreviewDiffForm)
  350. treePath := cleanUploadFileName(ctx.Repo.TreePath)
  351. if len(treePath) == 0 {
  352. ctx.Error(http.StatusInternalServerError, "file name to diff is invalid")
  353. return
  354. }
  355. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
  356. if err != nil {
  357. ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error())
  358. return
  359. } else if entry.IsDir() {
  360. ctx.Error(http.StatusUnprocessableEntity)
  361. return
  362. }
  363. diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
  364. if err != nil {
  365. ctx.Error(http.StatusInternalServerError, "GetDiffPreview: "+err.Error())
  366. return
  367. }
  368. if diff.NumFiles == 0 {
  369. ctx.PlainText(http.StatusOK, ctx.Tr("repo.editor.no_changes_to_show"))
  370. return
  371. }
  372. ctx.Data["File"] = diff.Files[0]
  373. ctx.HTML(http.StatusOK, tplEditDiffPreview)
  374. }
  375. // DeleteFile render delete file page
  376. func DeleteFile(ctx *context.Context) {
  377. ctx.Data["PageIsDelete"] = true
  378. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  379. treePath := cleanUploadFileName(ctx.Repo.TreePath)
  380. if treePath != ctx.Repo.TreePath {
  381. ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
  382. return
  383. }
  384. ctx.Data["TreePath"] = treePath
  385. canCommit := renderCommitRights(ctx)
  386. ctx.Data["commit_summary"] = ""
  387. ctx.Data["commit_message"] = ""
  388. ctx.Data["last_commit"] = ctx.Repo.CommitID
  389. if canCommit {
  390. ctx.Data["commit_choice"] = frmCommitChoiceDirect
  391. } else {
  392. ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
  393. }
  394. ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
  395. ctx.HTML(http.StatusOK, tplDeleteFile)
  396. }
  397. // DeleteFilePost response for deleting file
  398. func DeleteFilePost(ctx *context.Context) {
  399. form := web.GetForm(ctx).(*forms.DeleteRepoFileForm)
  400. canCommit := renderCommitRights(ctx)
  401. branchName := ctx.Repo.BranchName
  402. if form.CommitChoice == frmCommitChoiceNewBranch {
  403. branchName = form.NewBranchName
  404. }
  405. ctx.Data["PageIsDelete"] = true
  406. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  407. ctx.Data["TreePath"] = ctx.Repo.TreePath
  408. ctx.Data["commit_summary"] = form.CommitSummary
  409. ctx.Data["commit_message"] = form.CommitMessage
  410. ctx.Data["commit_choice"] = form.CommitChoice
  411. ctx.Data["new_branch_name"] = form.NewBranchName
  412. ctx.Data["last_commit"] = ctx.Repo.CommitID
  413. if ctx.HasError() {
  414. ctx.HTML(http.StatusOK, tplDeleteFile)
  415. return
  416. }
  417. if branchName == ctx.Repo.BranchName && !canCommit {
  418. ctx.Data["Err_NewBranchName"] = true
  419. ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
  420. ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
  421. return
  422. }
  423. message := strings.TrimSpace(form.CommitSummary)
  424. if len(message) == 0 {
  425. message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
  426. }
  427. form.CommitMessage = strings.TrimSpace(form.CommitMessage)
  428. if len(form.CommitMessage) > 0 {
  429. message += "\n\n" + form.CommitMessage
  430. }
  431. if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
  432. LastCommitID: form.LastCommit,
  433. OldBranch: ctx.Repo.BranchName,
  434. NewBranch: branchName,
  435. Files: []*files_service.ChangeRepoFile{
  436. {
  437. Operation: "delete",
  438. TreePath: ctx.Repo.TreePath,
  439. },
  440. },
  441. Message: message,
  442. Signoff: form.Signoff,
  443. }); err != nil {
  444. // This is where we handle all the errors thrown by repofiles.DeleteRepoFile
  445. if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
  446. ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form)
  447. } else if models.IsErrFilenameInvalid(err) {
  448. ctx.Data["Err_TreePath"] = true
  449. ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form)
  450. } else if models.IsErrFilePathInvalid(err) {
  451. ctx.Data["Err_TreePath"] = true
  452. if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
  453. switch fileErr.Type {
  454. case git.EntryModeSymlink:
  455. ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form)
  456. case git.EntryModeTree:
  457. ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form)
  458. case git.EntryModeBlob:
  459. ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form)
  460. default:
  461. ctx.ServerError("DeleteRepoFile", err)
  462. }
  463. } else {
  464. ctx.ServerError("DeleteRepoFile", err)
  465. }
  466. } else if git.IsErrBranchNotExist(err) {
  467. // For when a user deletes a file to a branch that no longer exists
  468. if branchErr, ok := err.(git.ErrBranchNotExist); ok {
  469. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form)
  470. } else {
  471. ctx.Error(http.StatusInternalServerError, err.Error())
  472. }
  473. } else if git_model.IsErrBranchAlreadyExists(err) {
  474. // For when a user specifies a new branch that already exists
  475. if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
  476. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
  477. } else {
  478. ctx.Error(http.StatusInternalServerError, err.Error())
  479. }
  480. } else if models.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
  481. ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form)
  482. } else if git.IsErrPushRejected(err) {
  483. errPushRej := err.(*git.ErrPushRejected)
  484. if len(errPushRej.Message) == 0 {
  485. ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
  486. } else {
  487. flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  488. "Message": ctx.Tr("repo.editor.push_rejected"),
  489. "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
  490. "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
  491. })
  492. if err != nil {
  493. ctx.ServerError("DeleteFilePost.HTMLString", err)
  494. return
  495. }
  496. ctx.RenderWithErr(flashError, tplDeleteFile, &form)
  497. }
  498. } else {
  499. ctx.ServerError("DeleteRepoFile", err)
  500. }
  501. }
  502. ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
  503. treePath := path.Dir(ctx.Repo.TreePath)
  504. if treePath == "." {
  505. treePath = "" // the file deleted was in the root, so we return the user to the root directory
  506. }
  507. if len(treePath) > 0 {
  508. // Need to get the latest commit since it changed
  509. commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
  510. if err == nil && commit != nil {
  511. // We have the comment, now find what directory we can return the user to
  512. // (must have entries)
  513. treePath = GetClosestParentWithFiles(treePath, commit)
  514. } else {
  515. treePath = "" // otherwise return them to the root of the repo
  516. }
  517. }
  518. redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath)
  519. }
  520. // UploadFile render upload file page
  521. func UploadFile(ctx *context.Context) {
  522. ctx.Data["PageIsUpload"] = true
  523. upload.AddUploadContext(ctx, "repo")
  524. canCommit := renderCommitRights(ctx)
  525. treePath := cleanUploadFileName(ctx.Repo.TreePath)
  526. if treePath != ctx.Repo.TreePath {
  527. ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
  528. return
  529. }
  530. ctx.Repo.TreePath = treePath
  531. treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
  532. if len(treeNames) == 0 {
  533. // We must at least have one element for user to input.
  534. treeNames = []string{""}
  535. }
  536. ctx.Data["TreeNames"] = treeNames
  537. ctx.Data["TreePaths"] = treePaths
  538. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  539. ctx.Data["commit_summary"] = ""
  540. ctx.Data["commit_message"] = ""
  541. if canCommit {
  542. ctx.Data["commit_choice"] = frmCommitChoiceDirect
  543. } else {
  544. ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
  545. }
  546. ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
  547. ctx.HTML(http.StatusOK, tplUploadFile)
  548. }
  549. // UploadFilePost response for uploading file
  550. func UploadFilePost(ctx *context.Context) {
  551. form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
  552. ctx.Data["PageIsUpload"] = true
  553. upload.AddUploadContext(ctx, "repo")
  554. canCommit := renderCommitRights(ctx)
  555. oldBranchName := ctx.Repo.BranchName
  556. branchName := oldBranchName
  557. if form.CommitChoice == frmCommitChoiceNewBranch {
  558. branchName = form.NewBranchName
  559. }
  560. form.TreePath = cleanUploadFileName(form.TreePath)
  561. treeNames, treePaths := getParentTreeFields(form.TreePath)
  562. if len(treeNames) == 0 {
  563. // We must at least have one element for user to input.
  564. treeNames = []string{""}
  565. }
  566. ctx.Data["TreePath"] = form.TreePath
  567. ctx.Data["TreeNames"] = treeNames
  568. ctx.Data["TreePaths"] = treePaths
  569. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)
  570. ctx.Data["commit_summary"] = form.CommitSummary
  571. ctx.Data["commit_message"] = form.CommitMessage
  572. ctx.Data["commit_choice"] = form.CommitChoice
  573. ctx.Data["new_branch_name"] = branchName
  574. if ctx.HasError() {
  575. ctx.HTML(http.StatusOK, tplUploadFile)
  576. return
  577. }
  578. if oldBranchName != branchName {
  579. if _, err := ctx.Repo.GitRepo.GetBranch(branchName); err == nil {
  580. ctx.Data["Err_NewBranchName"] = true
  581. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form)
  582. return
  583. }
  584. } else if !canCommit {
  585. ctx.Data["Err_NewBranchName"] = true
  586. ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
  587. ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form)
  588. return
  589. }
  590. if !ctx.Repo.Repository.IsEmpty {
  591. var newTreePath string
  592. for _, part := range treeNames {
  593. newTreePath = path.Join(newTreePath, part)
  594. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
  595. if err != nil {
  596. if git.IsErrNotExist(err) {
  597. break // Means there is no item with that name, so we're good
  598. }
  599. ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
  600. return
  601. }
  602. // User can only upload files to a directory, the directory name shouldn't be an existing file.
  603. if !entry.IsDir() {
  604. ctx.Data["Err_TreePath"] = true
  605. ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form)
  606. return
  607. }
  608. }
  609. }
  610. message := strings.TrimSpace(form.CommitSummary)
  611. if len(message) == 0 {
  612. dir := form.TreePath
  613. if dir == "" {
  614. dir = "/"
  615. }
  616. message = ctx.Tr("repo.editor.upload_files_to_dir", dir)
  617. }
  618. form.CommitMessage = strings.TrimSpace(form.CommitMessage)
  619. if len(form.CommitMessage) > 0 {
  620. message += "\n\n" + form.CommitMessage
  621. }
  622. if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
  623. LastCommitID: ctx.Repo.CommitID,
  624. OldBranch: oldBranchName,
  625. NewBranch: branchName,
  626. TreePath: form.TreePath,
  627. Message: message,
  628. Files: form.Files,
  629. Signoff: form.Signoff,
  630. }); err != nil {
  631. if git_model.IsErrLFSFileLocked(err) {
  632. ctx.Data["Err_TreePath"] = true
  633. ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form)
  634. } else if models.IsErrFilenameInvalid(err) {
  635. ctx.Data["Err_TreePath"] = true
  636. ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
  637. } else if models.IsErrFilePathInvalid(err) {
  638. ctx.Data["Err_TreePath"] = true
  639. fileErr := err.(models.ErrFilePathInvalid)
  640. switch fileErr.Type {
  641. case git.EntryModeSymlink:
  642. ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form)
  643. case git.EntryModeTree:
  644. ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form)
  645. case git.EntryModeBlob:
  646. ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form)
  647. default:
  648. ctx.Error(http.StatusInternalServerError, err.Error())
  649. }
  650. } else if models.IsErrRepoFileAlreadyExists(err) {
  651. ctx.Data["Err_TreePath"] = true
  652. ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form)
  653. } else if git.IsErrBranchNotExist(err) {
  654. branchErr := err.(git.ErrBranchNotExist)
  655. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form)
  656. } else if git_model.IsErrBranchAlreadyExists(err) {
  657. // For when a user specifies a new branch that already exists
  658. ctx.Data["Err_NewBranchName"] = true
  659. branchErr := err.(git_model.ErrBranchAlreadyExists)
  660. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
  661. } else if git.IsErrPushOutOfDate(err) {
  662. ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form)
  663. } else if git.IsErrPushRejected(err) {
  664. errPushRej := err.(*git.ErrPushRejected)
  665. if len(errPushRej.Message) == 0 {
  666. ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
  667. } else {
  668. flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  669. "Message": ctx.Tr("repo.editor.push_rejected"),
  670. "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
  671. "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
  672. })
  673. if err != nil {
  674. ctx.ServerError("UploadFilePost.HTMLString", err)
  675. return
  676. }
  677. ctx.RenderWithErr(flashError, tplUploadFile, &form)
  678. }
  679. } else {
  680. // os.ErrNotExist - upload file missing in the intervening time?!
  681. log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err)
  682. ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form)
  683. }
  684. return
  685. }
  686. if ctx.Repo.Repository.IsEmpty {
  687. if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
  688. _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
  689. }
  690. }
  691. redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
  692. }
  693. func cleanUploadFileName(name string) string {
  694. // Rebase the filename
  695. name = util.PathJoinRel(name)
  696. // Git disallows any filenames to have a .git directory in them.
  697. for _, part := range strings.Split(name, "/") {
  698. if strings.ToLower(part) == ".git" {
  699. return ""
  700. }
  701. }
  702. return name
  703. }
  704. // UploadFileToServer upload file to server file dir not git
  705. func UploadFileToServer(ctx *context.Context) {
  706. file, header, err := ctx.Req.FormFile("file")
  707. if err != nil {
  708. ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
  709. return
  710. }
  711. defer file.Close()
  712. buf := make([]byte, 1024)
  713. n, _ := util.ReadAtMost(file, buf)
  714. if n > 0 {
  715. buf = buf[:n]
  716. }
  717. err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
  718. if err != nil {
  719. ctx.Error(http.StatusBadRequest, err.Error())
  720. return
  721. }
  722. name := cleanUploadFileName(header.Filename)
  723. if len(name) == 0 {
  724. ctx.Error(http.StatusInternalServerError, "Upload file name is invalid")
  725. return
  726. }
  727. upload, err := repo_model.NewUpload(ctx, name, buf, file)
  728. if err != nil {
  729. ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
  730. return
  731. }
  732. log.Trace("New file uploaded: %s", upload.UUID)
  733. ctx.JSON(http.StatusOK, map[string]string{
  734. "uuid": upload.UUID,
  735. })
  736. }
  737. // RemoveUploadFileFromServer remove file from server file dir
  738. func RemoveUploadFileFromServer(ctx *context.Context) {
  739. form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
  740. if len(form.File) == 0 {
  741. ctx.Status(http.StatusNoContent)
  742. return
  743. }
  744. if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil {
  745. ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
  746. return
  747. }
  748. log.Trace("Upload file removed: %s", form.File)
  749. ctx.Status(http.StatusNoContent)
  750. }
  751. // GetUniquePatchBranchName Gets a unique branch name for a new patch branch
  752. // It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
  753. // that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
  754. // type in the branch name themselves (will be an empty field)
  755. func GetUniquePatchBranchName(ctx *context.Context) string {
  756. prefix := ctx.Doer.LowerName + "-patch-"
  757. for i := 1; i <= 1000; i++ {
  758. branchName := fmt.Sprintf("%s%d", prefix, i)
  759. if _, err := ctx.Repo.GitRepo.GetBranch(branchName); err != nil {
  760. if git.IsErrBranchNotExist(err) {
  761. return branchName
  762. }
  763. log.Error("GetUniquePatchBranchName: %v", err)
  764. return ""
  765. }
  766. }
  767. return ""
  768. }
  769. // GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is
  770. // deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a
  771. // SubTree or it has no entries, we go up one dir and see if we can return the user to that listing.
  772. func GetClosestParentWithFiles(treePath string, commit *git.Commit) string {
  773. if len(treePath) == 0 || treePath == "." {
  774. return ""
  775. }
  776. // see if the tree has entries
  777. if tree, err := commit.SubTree(treePath); err != nil {
  778. // failed to get tree, going up a dir
  779. return GetClosestParentWithFiles(path.Dir(treePath), commit)
  780. } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
  781. // no files in this dir, going up a dir
  782. return GetClosestParentWithFiles(path.Dir(treePath), commit)
  783. }
  784. return treePath
  785. }