gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "errors"
  6. "fmt"
  7. "html/template"
  8. "net/http"
  9. "path"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "code.gitea.io/gitea/models/db"
  14. git_model "code.gitea.io/gitea/models/git"
  15. repo_model "code.gitea.io/gitea/models/repo"
  16. unit_model "code.gitea.io/gitea/models/unit"
  17. user_model "code.gitea.io/gitea/models/user"
  18. "code.gitea.io/gitea/modules/git"
  19. "code.gitea.io/gitea/modules/gitrepo"
  20. "code.gitea.io/gitea/modules/htmlutil"
  21. "code.gitea.io/gitea/modules/httplib"
  22. "code.gitea.io/gitea/modules/log"
  23. repo_module "code.gitea.io/gitea/modules/repository"
  24. "code.gitea.io/gitea/modules/setting"
  25. "code.gitea.io/gitea/modules/svg"
  26. "code.gitea.io/gitea/modules/util"
  27. "code.gitea.io/gitea/routers/web/feed"
  28. "code.gitea.io/gitea/services/context"
  29. repo_service "code.gitea.io/gitea/services/repository"
  30. )
  31. func checkOutdatedBranch(ctx *context.Context) {
  32. if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) {
  33. return
  34. }
  35. // get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName`
  36. commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
  37. if err != nil {
  38. log.Error("GetBranchCommitID: %v", err)
  39. // Don't return an error page, as it can be rechecked the next time the user opens the page.
  40. return
  41. }
  42. dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName)
  43. if err != nil {
  44. log.Error("GetBranch: %v", err)
  45. // Don't return an error page, as it can be rechecked the next time the user opens the page.
  46. return
  47. }
  48. if dbBranch.CommitID != commit.ID.String() {
  49. ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true)
  50. }
  51. }
  52. func prepareHomeSidebarRepoTopics(ctx *context.Context) {
  53. topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
  54. RepoID: ctx.Repo.Repository.ID,
  55. })
  56. if err != nil {
  57. ctx.ServerError("models.FindTopics", err)
  58. return
  59. }
  60. ctx.Data["Topics"] = topics
  61. }
  62. func prepareOpenWithEditorApps(ctx *context.Context) {
  63. var tmplApps []map[string]any
  64. apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
  65. if len(apps) == 0 {
  66. apps = setting.DefaultOpenWithEditorApps()
  67. }
  68. for _, app := range apps {
  69. schema, _, _ := strings.Cut(app.OpenURL, ":")
  70. var iconHTML template.HTML
  71. if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
  72. iconHTML = svg.RenderHTML("gitea-"+schema, 16)
  73. } else {
  74. iconHTML = svg.RenderHTML("gitea-git", 16) // TODO: it could support user's customized icon in the future
  75. }
  76. tmplApps = append(tmplApps, map[string]any{
  77. "DisplayName": app.DisplayName,
  78. "OpenURL": app.OpenURL,
  79. "IconHTML": iconHTML,
  80. })
  81. }
  82. ctx.Data["OpenWithEditorApps"] = tmplApps
  83. }
  84. func prepareHomeSidebarCitationFile(entry *git.TreeEntry) func(ctx *context.Context) {
  85. return func(ctx *context.Context) {
  86. if entry.Name() != "" {
  87. return
  88. }
  89. tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
  90. if err != nil {
  91. HandleGitError(ctx, "Repo.Commit.SubTree", err)
  92. return
  93. }
  94. allEntries, err := tree.ListEntries()
  95. if err != nil {
  96. ctx.ServerError("ListEntries", err)
  97. return
  98. }
  99. for _, entry := range allEntries {
  100. if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" {
  101. // Read Citation file contents
  102. if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
  103. log.Error("checkCitationFile: GetBlobContent: %v", err)
  104. } else {
  105. ctx.Data["CitiationExist"] = true
  106. ctx.PageData["citationFileContent"] = content
  107. break
  108. }
  109. }
  110. }
  111. }
  112. }
  113. func prepareHomeSidebarLicenses(ctx *context.Context) {
  114. repoLicenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository)
  115. if err != nil {
  116. ctx.ServerError("GetRepoLicenses", err)
  117. return
  118. }
  119. ctx.Data["DetectedRepoLicenses"] = repoLicenses.StringList()
  120. ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
  121. }
  122. func prepareToRenderDirectory(ctx *context.Context) {
  123. entries := renderDirectoryFiles(ctx, 1*time.Second)
  124. if ctx.Written() {
  125. return
  126. }
  127. if ctx.Repo.TreePath != "" {
  128. ctx.Data["HideRepoInfo"] = true
  129. ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
  130. }
  131. subfolder, readmeFile, err := findReadmeFileInEntries(ctx, ctx.Repo.TreePath, entries, true)
  132. if err != nil {
  133. ctx.ServerError("findReadmeFileInEntries", err)
  134. return
  135. }
  136. prepareToRenderReadmeFile(ctx, subfolder, readmeFile)
  137. }
  138. func prepareHomeSidebarLanguageStats(ctx *context.Context) {
  139. langs, err := repo_model.GetTopLanguageStats(ctx, ctx.Repo.Repository, 5)
  140. if err != nil {
  141. ctx.ServerError("Repo.GetTopLanguageStats", err)
  142. return
  143. }
  144. ctx.Data["LanguageStats"] = langs
  145. }
  146. func prepareHomeSidebarLatestRelease(ctx *context.Context) {
  147. if !ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeReleases) {
  148. return
  149. }
  150. release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
  151. if err != nil && !repo_model.IsErrReleaseNotExist(err) {
  152. ctx.ServerError("GetLatestReleaseByRepoID", err)
  153. return
  154. }
  155. if release != nil {
  156. if err = release.LoadAttributes(ctx); err != nil {
  157. ctx.ServerError("release.LoadAttributes", err)
  158. return
  159. }
  160. ctx.Data["LatestRelease"] = release
  161. }
  162. }
  163. func prepareUpstreamDivergingInfo(ctx *context.Context) {
  164. if !ctx.Repo.Repository.IsFork || !ctx.Repo.RefFullName.IsBranch() || ctx.Repo.TreePath != "" {
  165. return
  166. }
  167. upstreamDivergingInfo, err := repo_service.GetUpstreamDivergingInfo(ctx, ctx.Repo.Repository, ctx.Repo.BranchName)
  168. if err != nil {
  169. if !errors.Is(err, util.ErrNotExist) && !errors.Is(err, util.ErrInvalidArgument) {
  170. log.Error("GetUpstreamDivergingInfo: %v", err)
  171. }
  172. return
  173. }
  174. ctx.Data["UpstreamDivergingInfo"] = upstreamDivergingInfo
  175. }
  176. func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) {
  177. if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status {
  178. return
  179. }
  180. ctx.Repo.Repository.IsEmpty = empty
  181. if ctx.Repo.Repository.Status == repo_model.RepositoryReady || ctx.Repo.Repository.Status == repo_model.RepositoryBroken {
  182. ctx.Repo.Repository.Status = status // only handle ready and broken status, leave other status as-is
  183. }
  184. if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, ctx.Repo.Repository, "is_empty", "status"); err != nil {
  185. ctx.ServerError("updateContextRepoEmptyAndStatus: UpdateRepositoryCols", err)
  186. return
  187. }
  188. }
  189. func handleRepoEmptyOrBroken(ctx *context.Context) {
  190. showEmpty := true
  191. if ctx.Repo.GitRepo == nil {
  192. // in case the repo really exists and works, but the status was incorrectly marked as "broken", we need to open and check it again
  193. ctx.Repo.GitRepo, _ = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
  194. }
  195. if ctx.Repo.GitRepo != nil {
  196. reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
  197. if err != nil {
  198. showEmpty = true // the repo is broken
  199. updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryBroken)
  200. log.Error("GitRepo.IsEmpty: %v", err)
  201. ctx.Flash.Error(ctx.Tr("error.occurred"), true)
  202. } else if reallyEmpty {
  203. showEmpty = true // the repo is really empty
  204. updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
  205. } else if branches, _, _ := ctx.Repo.GitRepo.GetBranchNames(0, 1); len(branches) == 0 {
  206. showEmpty = true // it is not really empty, but there is no branch
  207. // at the moment, other repo units like "actions" are not able to handle such case,
  208. // so we just mark the repo as empty to prevent from displaying these units.
  209. ctx.Data["RepoHasContentsWithoutBranch"] = true
  210. updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
  211. } else {
  212. // the repo is actually not empty and has branches, need to update the database later
  213. showEmpty = false
  214. }
  215. }
  216. if showEmpty {
  217. ctx.HTML(http.StatusOK, tplRepoEMPTY)
  218. return
  219. }
  220. // The repo is not really empty, so we should update the model in database, such problem may be caused by:
  221. // 1) an error occurs during pushing/receiving.
  222. // 2) the user replaces an empty git repo manually.
  223. updateContextRepoEmptyAndStatus(ctx, false, repo_model.RepositoryReady)
  224. if err := repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil {
  225. ctx.ServerError("UpdateRepoSize", err)
  226. return
  227. }
  228. // the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values
  229. link := ctx.Link
  230. if ctx.Req.URL.RawQuery != "" {
  231. link += "?" + ctx.Req.URL.RawQuery
  232. }
  233. ctx.Redirect(link)
  234. }
  235. func isViewHomeOnlyContent(ctx *context.Context) bool {
  236. return ctx.FormBool("only_content")
  237. }
  238. func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.CommitSubmoduleFile) {
  239. submoduleWebLink := commitSubmoduleFile.SubmoduleWebLinkTree(ctx)
  240. if submoduleWebLink == nil {
  241. ctx.Data["NotFoundPrompt"] = ctx.Repo.TreePath
  242. ctx.NotFound(nil)
  243. return
  244. }
  245. redirectLink := submoduleWebLink.CommitWebLink
  246. if isViewHomeOnlyContent(ctx) {
  247. ctx.Resp.Header().Set("Content-Type", "text/html; charset=utf-8")
  248. _, _ = ctx.Resp.Write([]byte(htmlutil.HTMLFormat(`<a href="%s">%s</a>`, redirectLink, redirectLink)))
  249. } else if !httplib.IsCurrentGiteaSiteURL(ctx, redirectLink) {
  250. // don't auto-redirect to external URL, to avoid open redirect or phishing
  251. ctx.Data["NotFoundPrompt"] = redirectLink
  252. ctx.NotFound(nil)
  253. } else {
  254. ctx.Redirect(submoduleWebLink.CommitWebLink)
  255. }
  256. }
  257. func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
  258. return func(ctx *context.Context) {
  259. if entry.IsSubModule() {
  260. commitSubmoduleFile, err := git.GetCommitInfoSubmoduleFile(ctx.Repo.RepoLink, ctx.Repo.TreePath, ctx.Repo.Commit, entry.ID)
  261. if err != nil {
  262. HandleGitError(ctx, "prepareToRenderDirOrFile: GetCommitInfoSubmoduleFile", err)
  263. return
  264. }
  265. handleRepoViewSubmodule(ctx, commitSubmoduleFile)
  266. } else if entry.IsDir() {
  267. prepareToRenderDirectory(ctx)
  268. } else {
  269. prepareFileView(ctx, entry)
  270. }
  271. }
  272. }
  273. func handleRepoHomeFeed(ctx *context.Context) bool {
  274. if !setting.Other.EnableFeed {
  275. return false
  276. }
  277. isFeed, showFeedType := feed.GetFeedType(ctx.PathParam("reponame"), ctx.Req)
  278. if !isFeed {
  279. return false
  280. }
  281. if ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType) {
  282. feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
  283. } else if ctx.Repo.TreePath == "" {
  284. feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
  285. } else {
  286. feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
  287. }
  288. return true
  289. }
  290. func prepareHomeTreeSideBarSwitch(ctx *context.Context) {
  291. showFileTree := true
  292. if ctx.Doer != nil {
  293. v, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyCodeViewShowFileTree, "true")
  294. if err != nil {
  295. log.Error("GetUserSetting: %v", err)
  296. } else {
  297. showFileTree, _ = strconv.ParseBool(v)
  298. }
  299. }
  300. ctx.Data["UserSettingCodeViewShowFileTree"] = showFileTree
  301. }
  302. func redirectSrcToRaw(ctx *context.Context) bool {
  303. // GitHub redirects a tree path with "?raw=1" to the raw path
  304. // It is useful to embed some raw contents into Markdown files,
  305. // then viewing the Markdown in "src" path could embed the raw content correctly.
  306. if ctx.Repo.TreePath != "" && ctx.FormBool("raw") {
  307. ctx.Redirect(ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath))
  308. return true
  309. }
  310. return false
  311. }
  312. func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) bool {
  313. if ctx.Repo.TreePath == "" || !ctx.FormBool("follow_symlink") {
  314. return false
  315. }
  316. if treePathEntry.IsLink() {
  317. if res, err := git.EntryFollowLinks(ctx.Repo.Commit, ctx.Repo.TreePath, treePathEntry); err == nil {
  318. redirect := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(res.TargetFullPath) + "?" + ctx.Req.URL.RawQuery
  319. ctx.Redirect(redirect)
  320. return true
  321. } // else: don't handle the links we cannot resolve, so ignore the error
  322. }
  323. return false
  324. }
  325. // Home render repository home page
  326. func Home(ctx *context.Context) {
  327. if handleRepoHomeFeed(ctx) {
  328. return
  329. }
  330. if redirectSrcToRaw(ctx) {
  331. return
  332. }
  333. // Check whether the repo is viewable: not in migration, and the code unit should be enabled
  334. // Ideally the "feed" logic should be after this, but old code did so, so keep it as-is.
  335. checkHomeCodeViewable(ctx)
  336. if ctx.Written() {
  337. return
  338. }
  339. title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
  340. if ctx.Repo.Repository.Description != "" {
  341. title += ": " + ctx.Repo.Repository.Description
  342. }
  343. ctx.Data["Title"] = title
  344. ctx.Data["PageIsViewCode"] = true
  345. ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons
  346. if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
  347. // empty or broken repositories need to be handled differently
  348. handleRepoEmptyOrBroken(ctx)
  349. return
  350. }
  351. prepareHomeTreeSideBarSwitch(ctx)
  352. // get the current git entry which doer user is currently looking at.
  353. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
  354. if err != nil {
  355. HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
  356. return
  357. }
  358. if redirectFollowSymlink(ctx, entry) {
  359. return
  360. }
  361. // prepare the tree path
  362. var treeNames, paths []string
  363. branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
  364. treeLink := branchLink
  365. if ctx.Repo.TreePath != "" {
  366. treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  367. treeNames = strings.Split(ctx.Repo.TreePath, "/")
  368. for i := range treeNames {
  369. paths = append(paths, strings.Join(treeNames[:i+1], "/"))
  370. }
  371. ctx.Data["HasParentPath"] = true
  372. if len(paths)-2 >= 0 {
  373. ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
  374. }
  375. }
  376. ctx.Data["Paths"] = paths
  377. ctx.Data["TreeLink"] = treeLink
  378. ctx.Data["TreeNames"] = treeNames
  379. ctx.Data["BranchLink"] = branchLink
  380. // some UI components are only shown when the tree path is root
  381. isTreePathRoot := ctx.Repo.TreePath == ""
  382. prepareFuncs := []func(*context.Context){
  383. prepareOpenWithEditorApps,
  384. prepareHomeSidebarRepoTopics,
  385. checkOutdatedBranch,
  386. prepareToRenderDirOrFile(entry),
  387. prepareRecentlyPushedNewBranches,
  388. }
  389. if isTreePathRoot {
  390. prepareFuncs = append(prepareFuncs,
  391. prepareUpstreamDivergingInfo,
  392. prepareHomeSidebarLicenses,
  393. prepareHomeSidebarCitationFile(entry),
  394. prepareHomeSidebarLanguageStats,
  395. prepareHomeSidebarLatestRelease,
  396. )
  397. }
  398. for _, prepare := range prepareFuncs {
  399. prepare(ctx)
  400. if ctx.Written() {
  401. return
  402. }
  403. }
  404. if isViewHomeOnlyContent(ctx) {
  405. ctx.HTML(http.StatusOK, tplRepoViewContent)
  406. } else if len(treeNames) != 0 {
  407. ctx.HTML(http.StatusOK, tplRepoView)
  408. } else {
  409. ctx.HTML(http.StatusOK, tplRepoHome)
  410. }
  411. }
  412. func RedirectRepoTreeToSrc(ctx *context.Context) {
  413. // Redirect "/owner/repo/tree/*" requests to "/owner/repo/src/*",
  414. // then use the deprecated "/src/*" handler to guess the ref type and render a file list page.
  415. // This is done intentionally so that Gitea's repo URL structure matches other forges (GitHub/GitLab) provide,
  416. // allowing us to construct submodule URLs across forges easily.
  417. // For example, when viewing a submodule, we can simply construct the link as:
  418. // * "https://gitea/owner/repo/tree/{CommitID}"
  419. // * "https://github/owner/repo/tree/{CommitID}"
  420. // * "https://gitlab/owner/repo/tree/{CommitID}"
  421. // Then no matter which forge the submodule is using, the link works.
  422. redirect := ctx.Repo.RepoLink + "/src/" + ctx.PathParamRaw("*")
  423. if ctx.Req.URL.RawQuery != "" {
  424. redirect += "?" + ctx.Req.URL.RawQuery
  425. }
  426. ctx.Redirect(redirect)
  427. }
  428. func RedirectRepoBlobToCommit(ctx *context.Context) {
  429. // redirect "/owner/repo/blob/*" requests to "/owner/repo/src/commit/*"
  430. // just like GitHub: browse files of a commit by "https://github/owner/repo/blob/{CommitID}"
  431. // TODO: maybe we could guess more types to redirect to the related pages in the future
  432. redirect := ctx.Repo.RepoLink + "/src/commit/" + ctx.PathParamRaw("*")
  433. if ctx.Req.URL.RawQuery != "" {
  434. redirect += "?" + ctx.Req.URL.RawQuery
  435. }
  436. ctx.Redirect(redirect)
  437. }