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

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