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

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