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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884
  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"] = nil
  376. } else {
  377. ctx.Data["File"] = diff.Files[0]
  378. }
  379. ctx.HTML(http.StatusOK, tplEditDiffPreview)
  380. }
  381. // DeleteFile render delete file page
  382. func DeleteFile(ctx *context.Context) {
  383. ctx.Data["PageIsDelete"] = true
  384. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  385. treePath := cleanUploadFileName(ctx.Repo.TreePath)
  386. if treePath != ctx.Repo.TreePath {
  387. ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
  388. return
  389. }
  390. ctx.Data["TreePath"] = treePath
  391. canCommit := renderCommitRights(ctx)
  392. ctx.Data["commit_summary"] = ""
  393. ctx.Data["commit_message"] = ""
  394. ctx.Data["last_commit"] = ctx.Repo.CommitID
  395. if canCommit {
  396. ctx.Data["commit_choice"] = frmCommitChoiceDirect
  397. } else {
  398. ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
  399. }
  400. ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
  401. ctx.HTML(http.StatusOK, tplDeleteFile)
  402. }
  403. // DeleteFilePost response for deleting file
  404. func DeleteFilePost(ctx *context.Context) {
  405. form := web.GetForm(ctx).(*forms.DeleteRepoFileForm)
  406. canCommit := renderCommitRights(ctx)
  407. branchName := ctx.Repo.BranchName
  408. if form.CommitChoice == frmCommitChoiceNewBranch {
  409. branchName = form.NewBranchName
  410. }
  411. ctx.Data["PageIsDelete"] = true
  412. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  413. ctx.Data["TreePath"] = ctx.Repo.TreePath
  414. ctx.Data["commit_summary"] = form.CommitSummary
  415. ctx.Data["commit_message"] = form.CommitMessage
  416. ctx.Data["commit_choice"] = form.CommitChoice
  417. ctx.Data["new_branch_name"] = form.NewBranchName
  418. ctx.Data["last_commit"] = ctx.Repo.CommitID
  419. if ctx.HasError() {
  420. ctx.HTML(http.StatusOK, tplDeleteFile)
  421. return
  422. }
  423. if branchName == ctx.Repo.BranchName && !canCommit {
  424. ctx.Data["Err_NewBranchName"] = true
  425. ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
  426. ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
  427. return
  428. }
  429. message := strings.TrimSpace(form.CommitSummary)
  430. if len(message) == 0 {
  431. message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
  432. }
  433. form.CommitMessage = strings.TrimSpace(form.CommitMessage)
  434. if len(form.CommitMessage) > 0 {
  435. message += "\n\n" + form.CommitMessage
  436. }
  437. if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
  438. LastCommitID: form.LastCommit,
  439. OldBranch: ctx.Repo.BranchName,
  440. NewBranch: branchName,
  441. Files: []*files_service.ChangeRepoFile{
  442. {
  443. Operation: "delete",
  444. TreePath: ctx.Repo.TreePath,
  445. },
  446. },
  447. Message: message,
  448. Signoff: form.Signoff,
  449. }); err != nil {
  450. // This is where we handle all the errors thrown by repofiles.DeleteRepoFile
  451. if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
  452. ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form)
  453. } else if models.IsErrFilenameInvalid(err) {
  454. ctx.Data["Err_TreePath"] = true
  455. ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form)
  456. } else if models.IsErrFilePathInvalid(err) {
  457. ctx.Data["Err_TreePath"] = true
  458. if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
  459. switch fileErr.Type {
  460. case git.EntryModeSymlink:
  461. ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form)
  462. case git.EntryModeTree:
  463. ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form)
  464. case git.EntryModeBlob:
  465. ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form)
  466. default:
  467. ctx.ServerError("DeleteRepoFile", err)
  468. }
  469. } else {
  470. ctx.ServerError("DeleteRepoFile", err)
  471. }
  472. } else if git.IsErrBranchNotExist(err) {
  473. // For when a user deletes a file to a branch that no longer exists
  474. if branchErr, ok := err.(git.ErrBranchNotExist); ok {
  475. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form)
  476. } else {
  477. ctx.Error(http.StatusInternalServerError, err.Error())
  478. }
  479. } else if git_model.IsErrBranchAlreadyExists(err) {
  480. // For when a user specifies a new branch that already exists
  481. if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
  482. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
  483. } else {
  484. ctx.Error(http.StatusInternalServerError, err.Error())
  485. }
  486. } else if models.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
  487. 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)
  488. } else if git.IsErrPushRejected(err) {
  489. errPushRej := err.(*git.ErrPushRejected)
  490. if len(errPushRej.Message) == 0 {
  491. ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
  492. } else {
  493. flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
  494. "Message": ctx.Tr("repo.editor.push_rejected"),
  495. "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
  496. "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
  497. })
  498. if err != nil {
  499. ctx.ServerError("DeleteFilePost.HTMLString", err)
  500. return
  501. }
  502. ctx.RenderWithErr(flashError, tplDeleteFile, &form)
  503. }
  504. } else {
  505. ctx.ServerError("DeleteRepoFile", err)
  506. }
  507. }
  508. ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
  509. treePath := path.Dir(ctx.Repo.TreePath)
  510. if treePath == "." {
  511. treePath = "" // the file deleted was in the root, so we return the user to the root directory
  512. }
  513. if len(treePath) > 0 {
  514. // Need to get the latest commit since it changed
  515. commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
  516. if err == nil && commit != nil {
  517. // We have the comment, now find what directory we can return the user to
  518. // (must have entries)
  519. treePath = GetClosestParentWithFiles(treePath, commit)
  520. } else {
  521. treePath = "" // otherwise return them to the root of the repo
  522. }
  523. }
  524. redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath)
  525. }
  526. // UploadFile render upload file page
  527. func UploadFile(ctx *context.Context) {
  528. ctx.Data["PageIsUpload"] = true
  529. upload.AddUploadContext(ctx, "repo")
  530. canCommit := renderCommitRights(ctx)
  531. treePath := cleanUploadFileName(ctx.Repo.TreePath)
  532. if treePath != ctx.Repo.TreePath {
  533. ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
  534. return
  535. }
  536. ctx.Repo.TreePath = treePath
  537. treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
  538. if len(treeNames) == 0 {
  539. // We must at least have one element for user to input.
  540. treeNames = []string{""}
  541. }
  542. ctx.Data["TreeNames"] = treeNames
  543. ctx.Data["TreePaths"] = treePaths
  544. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  545. ctx.Data["commit_summary"] = ""
  546. ctx.Data["commit_message"] = ""
  547. if canCommit {
  548. ctx.Data["commit_choice"] = frmCommitChoiceDirect
  549. } else {
  550. ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
  551. }
  552. ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
  553. ctx.HTML(http.StatusOK, tplUploadFile)
  554. }
  555. // UploadFilePost response for uploading file
  556. func UploadFilePost(ctx *context.Context) {
  557. form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
  558. ctx.Data["PageIsUpload"] = true
  559. upload.AddUploadContext(ctx, "repo")
  560. canCommit := renderCommitRights(ctx)
  561. oldBranchName := ctx.Repo.BranchName
  562. branchName := oldBranchName
  563. if form.CommitChoice == frmCommitChoiceNewBranch {
  564. branchName = form.NewBranchName
  565. }
  566. form.TreePath = cleanUploadFileName(form.TreePath)
  567. treeNames, treePaths := getParentTreeFields(form.TreePath)
  568. if len(treeNames) == 0 {
  569. // We must at least have one element for user to input.
  570. treeNames = []string{""}
  571. }
  572. ctx.Data["TreePath"] = form.TreePath
  573. ctx.Data["TreeNames"] = treeNames
  574. ctx.Data["TreePaths"] = treePaths
  575. ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)
  576. ctx.Data["commit_summary"] = form.CommitSummary
  577. ctx.Data["commit_message"] = form.CommitMessage
  578. ctx.Data["commit_choice"] = form.CommitChoice
  579. ctx.Data["new_branch_name"] = branchName
  580. if ctx.HasError() {
  581. ctx.HTML(http.StatusOK, tplUploadFile)
  582. return
  583. }
  584. if oldBranchName != branchName {
  585. if _, err := ctx.Repo.GitRepo.GetBranch(branchName); err == nil {
  586. ctx.Data["Err_NewBranchName"] = true
  587. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form)
  588. return
  589. }
  590. } else if !canCommit {
  591. ctx.Data["Err_NewBranchName"] = true
  592. ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
  593. ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form)
  594. return
  595. }
  596. if !ctx.Repo.Repository.IsEmpty {
  597. var newTreePath string
  598. for _, part := range treeNames {
  599. newTreePath = path.Join(newTreePath, part)
  600. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
  601. if err != nil {
  602. if git.IsErrNotExist(err) {
  603. break // Means there is no item with that name, so we're good
  604. }
  605. ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
  606. return
  607. }
  608. // User can only upload files to a directory, the directory name shouldn't be an existing file.
  609. if !entry.IsDir() {
  610. ctx.Data["Err_TreePath"] = true
  611. ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form)
  612. return
  613. }
  614. }
  615. }
  616. message := strings.TrimSpace(form.CommitSummary)
  617. if len(message) == 0 {
  618. dir := form.TreePath
  619. if dir == "" {
  620. dir = "/"
  621. }
  622. message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
  623. }
  624. form.CommitMessage = strings.TrimSpace(form.CommitMessage)
  625. if len(form.CommitMessage) > 0 {
  626. message += "\n\n" + form.CommitMessage
  627. }
  628. if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
  629. LastCommitID: ctx.Repo.CommitID,
  630. OldBranch: oldBranchName,
  631. NewBranch: branchName,
  632. TreePath: form.TreePath,
  633. Message: message,
  634. Files: form.Files,
  635. Signoff: form.Signoff,
  636. }); err != nil {
  637. if git_model.IsErrLFSFileLocked(err) {
  638. ctx.Data["Err_TreePath"] = true
  639. ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form)
  640. } else if models.IsErrFilenameInvalid(err) {
  641. ctx.Data["Err_TreePath"] = true
  642. ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
  643. } else if models.IsErrFilePathInvalid(err) {
  644. ctx.Data["Err_TreePath"] = true
  645. fileErr := err.(models.ErrFilePathInvalid)
  646. switch fileErr.Type {
  647. case git.EntryModeSymlink:
  648. ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form)
  649. case git.EntryModeTree:
  650. ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form)
  651. case git.EntryModeBlob:
  652. ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form)
  653. default:
  654. ctx.Error(http.StatusInternalServerError, err.Error())
  655. }
  656. } else if models.IsErrRepoFileAlreadyExists(err) {
  657. ctx.Data["Err_TreePath"] = true
  658. ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form)
  659. } else if git.IsErrBranchNotExist(err) {
  660. branchErr := err.(git.ErrBranchNotExist)
  661. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form)
  662. } else if git_model.IsErrBranchAlreadyExists(err) {
  663. // For when a user specifies a new branch that already exists
  664. ctx.Data["Err_NewBranchName"] = true
  665. branchErr := err.(git_model.ErrBranchAlreadyExists)
  666. ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
  667. } else if git.IsErrPushOutOfDate(err) {
  668. 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)
  669. } else if git.IsErrPushRejected(err) {
  670. errPushRej := err.(*git.ErrPushRejected)
  671. if len(errPushRej.Message) == 0 {
  672. ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
  673. } else {
  674. flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
  675. "Message": ctx.Tr("repo.editor.push_rejected"),
  676. "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
  677. "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
  678. })
  679. if err != nil {
  680. ctx.ServerError("UploadFilePost.HTMLString", err)
  681. return
  682. }
  683. ctx.RenderWithErr(flashError, tplUploadFile, &form)
  684. }
  685. } else {
  686. // os.ErrNotExist - upload file missing in the intervening time?!
  687. 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)
  688. ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form)
  689. }
  690. return
  691. }
  692. if ctx.Repo.Repository.IsEmpty {
  693. if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
  694. _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
  695. }
  696. }
  697. redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
  698. }
  699. func cleanUploadFileName(name string) string {
  700. // Rebase the filename
  701. name = util.PathJoinRel(name)
  702. // Git disallows any filenames to have a .git directory in them.
  703. for _, part := range strings.Split(name, "/") {
  704. if strings.ToLower(part) == ".git" {
  705. return ""
  706. }
  707. }
  708. return name
  709. }
  710. // UploadFileToServer upload file to server file dir not git
  711. func UploadFileToServer(ctx *context.Context) {
  712. file, header, err := ctx.Req.FormFile("file")
  713. if err != nil {
  714. ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
  715. return
  716. }
  717. defer file.Close()
  718. buf := make([]byte, 1024)
  719. n, _ := util.ReadAtMost(file, buf)
  720. if n > 0 {
  721. buf = buf[:n]
  722. }
  723. err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
  724. if err != nil {
  725. ctx.Error(http.StatusBadRequest, err.Error())
  726. return
  727. }
  728. name := cleanUploadFileName(header.Filename)
  729. if len(name) == 0 {
  730. ctx.Error(http.StatusInternalServerError, "Upload file name is invalid")
  731. return
  732. }
  733. upload, err := repo_model.NewUpload(ctx, name, buf, file)
  734. if err != nil {
  735. ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
  736. return
  737. }
  738. log.Trace("New file uploaded: %s", upload.UUID)
  739. ctx.JSON(http.StatusOK, map[string]string{
  740. "uuid": upload.UUID,
  741. })
  742. }
  743. // RemoveUploadFileFromServer remove file from server file dir
  744. func RemoveUploadFileFromServer(ctx *context.Context) {
  745. form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
  746. if len(form.File) == 0 {
  747. ctx.Status(http.StatusNoContent)
  748. return
  749. }
  750. if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil {
  751. ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
  752. return
  753. }
  754. log.Trace("Upload file removed: %s", form.File)
  755. ctx.Status(http.StatusNoContent)
  756. }
  757. // GetUniquePatchBranchName Gets a unique branch name for a new patch branch
  758. // It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
  759. // that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
  760. // type in the branch name themselves (will be an empty field)
  761. func GetUniquePatchBranchName(ctx *context.Context) string {
  762. prefix := ctx.Doer.LowerName + "-patch-"
  763. for i := 1; i <= 1000; i++ {
  764. branchName := fmt.Sprintf("%s%d", prefix, i)
  765. if _, err := ctx.Repo.GitRepo.GetBranch(branchName); err != nil {
  766. if git.IsErrBranchNotExist(err) {
  767. return branchName
  768. }
  769. log.Error("GetUniquePatchBranchName: %v", err)
  770. return ""
  771. }
  772. }
  773. return ""
  774. }
  775. // GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is
  776. // deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a
  777. // SubTree or it has no entries, we go up one dir and see if we can return the user to that listing.
  778. func GetClosestParentWithFiles(treePath string, commit *git.Commit) string {
  779. if len(treePath) == 0 || treePath == "." {
  780. return ""
  781. }
  782. // see if the tree has entries
  783. if tree, err := commit.SubTree(treePath); err != nil {
  784. // failed to get tree, going up a dir
  785. return GetClosestParentWithFiles(path.Dir(treePath), commit)
  786. } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
  787. // no files in this dir, going up a dir
  788. return GetClosestParentWithFiles(path.Dir(treePath), commit)
  789. }
  790. return treePath
  791. }