gitea源码

view_readme.go 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "bytes"
  6. "encoding/base64"
  7. "fmt"
  8. "html/template"
  9. "io"
  10. "net/url"
  11. "path"
  12. "strings"
  13. "code.gitea.io/gitea/models/renderhelper"
  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/log"
  18. "code.gitea.io/gitea/modules/markup"
  19. "code.gitea.io/gitea/modules/setting"
  20. "code.gitea.io/gitea/modules/util"
  21. "code.gitea.io/gitea/services/context"
  22. )
  23. // locate a README for a tree in one of the supported paths.
  24. //
  25. // entries is passed to reduce calls to ListEntries(), so
  26. // this has precondition:
  27. //
  28. // entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
  29. //
  30. // FIXME: There has to be a more efficient way of doing this
  31. func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
  32. docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
  33. for _, entry := range entries {
  34. if tryWellKnownDirs && entry.IsDir() {
  35. // as a special case for the top-level repo introduction README,
  36. // fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ...
  37. // (note that docsEntries is ignored unless we are at the root)
  38. lowerName := strings.ToLower(entry.Name())
  39. switch lowerName {
  40. case "docs":
  41. if entry.Name() == "docs" || docsEntries[0] == nil {
  42. docsEntries[0] = entry
  43. }
  44. case ".gitea":
  45. if entry.Name() == ".gitea" || docsEntries[1] == nil {
  46. docsEntries[1] = entry
  47. }
  48. case ".github":
  49. if entry.Name() == ".github" || docsEntries[2] == nil {
  50. docsEntries[2] = entry
  51. }
  52. }
  53. }
  54. }
  55. // Create a list of extensions in priority order
  56. // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
  57. // 2. Txt files - e.g. README.txt
  58. // 3. No extension - e.g. README
  59. exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
  60. extCount := len(exts)
  61. readmeFiles := make([]*git.TreeEntry, extCount+1)
  62. for _, entry := range entries {
  63. if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
  64. fullPath := path.Join(parentDir, entry.Name())
  65. if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
  66. if entry.IsLink() {
  67. res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry)
  68. if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) {
  69. readmeFiles[i] = entry
  70. }
  71. } else {
  72. readmeFiles[i] = entry
  73. }
  74. }
  75. }
  76. }
  77. var readmeFile *git.TreeEntry
  78. for _, f := range readmeFiles {
  79. if f != nil {
  80. readmeFile = f
  81. break
  82. }
  83. }
  84. if ctx.Repo.TreePath == "" && readmeFile == nil {
  85. for _, subTreeEntry := range docsEntries {
  86. if subTreeEntry == nil {
  87. continue
  88. }
  89. subTree := subTreeEntry.Tree()
  90. if subTree == nil {
  91. // this should be impossible; if subTreeEntry exists so should this.
  92. continue
  93. }
  94. childEntries, err := subTree.ListEntries()
  95. if err != nil {
  96. return "", nil, err
  97. }
  98. subfolder, readmeFile, err := findReadmeFileInEntries(ctx, parentDir, childEntries, false)
  99. if err != nil && !git.IsErrNotExist(err) {
  100. return "", nil, err
  101. }
  102. if readmeFile != nil {
  103. return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil
  104. }
  105. }
  106. }
  107. return "", readmeFile, nil
  108. }
  109. // localizedExtensions prepends the provided language code with and without a
  110. // regional identifier to the provided extension.
  111. // Note: the language code will always be lower-cased, if a region is present it must be separated with a `-`
  112. // Note: ext should be prefixed with a `.`
  113. func localizedExtensions(ext, languageCode string) (localizedExts []string) {
  114. if len(languageCode) < 1 {
  115. return []string{ext}
  116. }
  117. lowerLangCode := "." + strings.ToLower(languageCode)
  118. if strings.Contains(lowerLangCode, "-") {
  119. underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_")
  120. indexOfDash := strings.Index(lowerLangCode, "-")
  121. // e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md]
  122. return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext}
  123. }
  124. // e.g. [.en.md, .md]
  125. return []string{lowerLangCode + ext, ext}
  126. }
  127. func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
  128. if readmeFile == nil {
  129. return
  130. }
  131. readmeFullPath := path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())
  132. readmeTargetEntry := readmeFile
  133. if readmeFile.IsLink() {
  134. if res, err := git.EntryFollowLinks(ctx.Repo.Commit, readmeFullPath, readmeFile); err == nil {
  135. readmeTargetEntry = res.TargetEntry
  136. } else {
  137. readmeTargetEntry = nil // if we cannot resolve the symlink, we cannot render the readme, ignore the error
  138. }
  139. }
  140. if readmeTargetEntry == nil {
  141. return // if no valid README entry found, skip rendering the README
  142. }
  143. ctx.Data["RawFileLink"] = ""
  144. ctx.Data["ReadmeInList"] = path.Join(subfolder, readmeFile.Name()) // the relative path to the readme file to the current tree path
  145. ctx.Data["ReadmeExist"] = true
  146. ctx.Data["FileIsSymlink"] = readmeFile.IsLink()
  147. buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, readmeTargetEntry.Blob())
  148. if err != nil {
  149. ctx.ServerError("getFileReader", err)
  150. return
  151. }
  152. defer dataRc.Close()
  153. ctx.Data["FileIsText"] = fInfo.st.IsText()
  154. ctx.Data["FileTreePath"] = readmeFullPath
  155. ctx.Data["FileSize"] = fInfo.fileSize
  156. ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
  157. if fInfo.isLFSFile() {
  158. filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name()))
  159. ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
  160. }
  161. if !fInfo.st.IsText() {
  162. return
  163. }
  164. if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
  165. // Pretend that this is a normal text file to display 'This file is too large to be shown'
  166. ctx.Data["IsFileTooLarge"] = true
  167. return
  168. }
  169. rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
  170. if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" {
  171. ctx.Data["IsMarkup"] = true
  172. ctx.Data["MarkupType"] = markupType
  173. rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
  174. CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
  175. CurrentTreePath: path.Dir(readmeFullPath),
  176. }).
  177. WithMarkupType(markupType).
  178. WithRelativePath(readmeFullPath)
  179. ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
  180. if err != nil {
  181. log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
  182. delete(ctx.Data, "IsMarkup")
  183. }
  184. }
  185. if ctx.Data["IsMarkup"] != true {
  186. ctx.Data["IsPlainText"] = true
  187. content, err := io.ReadAll(rd)
  188. if err != nil {
  189. log.Error("Read readme content failed: %v", err)
  190. }
  191. contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content))
  192. ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
  193. }
  194. if !fInfo.isLFSFile() && ctx.Repo.Repository.CanEnableEditor() {
  195. ctx.Data["CanEditReadmeFile"] = true
  196. }
  197. }