gitea源码

view.go 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // Copyright 2014 The Gogs Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package repo
  5. import (
  6. gocontext "context"
  7. "errors"
  8. "fmt"
  9. "html/template"
  10. "io"
  11. "net/http"
  12. "net/url"
  13. "path"
  14. "strings"
  15. "time"
  16. _ "image/gif" // for processing gif images
  17. _ "image/jpeg" // for processing jpeg images
  18. _ "image/png" // for processing png images
  19. activities_model "code.gitea.io/gitea/models/activities"
  20. admin_model "code.gitea.io/gitea/models/admin"
  21. asymkey_model "code.gitea.io/gitea/models/asymkey"
  22. "code.gitea.io/gitea/models/db"
  23. git_model "code.gitea.io/gitea/models/git"
  24. repo_model "code.gitea.io/gitea/models/repo"
  25. unit_model "code.gitea.io/gitea/models/unit"
  26. user_model "code.gitea.io/gitea/models/user"
  27. "code.gitea.io/gitea/modules/base"
  28. "code.gitea.io/gitea/modules/charset"
  29. "code.gitea.io/gitea/modules/fileicon"
  30. "code.gitea.io/gitea/modules/git"
  31. "code.gitea.io/gitea/modules/lfs"
  32. "code.gitea.io/gitea/modules/log"
  33. "code.gitea.io/gitea/modules/markup"
  34. "code.gitea.io/gitea/modules/setting"
  35. "code.gitea.io/gitea/modules/structs"
  36. "code.gitea.io/gitea/modules/templates"
  37. "code.gitea.io/gitea/modules/typesniffer"
  38. "code.gitea.io/gitea/modules/util"
  39. asymkey_service "code.gitea.io/gitea/services/asymkey"
  40. "code.gitea.io/gitea/services/context"
  41. repo_service "code.gitea.io/gitea/services/repository"
  42. _ "golang.org/x/image/bmp" // for processing bmp images
  43. _ "golang.org/x/image/webp" // for processing webp images
  44. )
  45. const (
  46. tplRepoEMPTY templates.TplName = "repo/empty"
  47. tplRepoHome templates.TplName = "repo/home"
  48. tplRepoView templates.TplName = "repo/view"
  49. tplRepoViewContent templates.TplName = "repo/view_content"
  50. tplRepoViewList templates.TplName = "repo/view_list"
  51. tplWatchers templates.TplName = "repo/watchers"
  52. tplForks templates.TplName = "repo/forks"
  53. tplMigrating templates.TplName = "repo/migrate/migrating"
  54. )
  55. type fileInfo struct {
  56. fileSize int64
  57. lfsMeta *lfs.Pointer
  58. st typesniffer.SniffedType
  59. }
  60. func (fi *fileInfo) isLFSFile() bool {
  61. return fi.lfsMeta != nil && fi.lfsMeta.Oid != ""
  62. }
  63. func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []byte, dataRc io.ReadCloser, fi *fileInfo, err error) {
  64. dataRc, err = blob.DataAsync()
  65. if err != nil {
  66. return nil, nil, nil, err
  67. }
  68. const prefetchSize = lfs.MetaFileMaxSize
  69. buf = make([]byte, prefetchSize)
  70. n, _ := util.ReadAtMost(dataRc, buf)
  71. buf = buf[:n]
  72. fi = &fileInfo{fileSize: blob.Size(), st: typesniffer.DetectContentType(buf)}
  73. // FIXME: what happens when README file is an image?
  74. if !fi.st.IsText() || !setting.LFS.StartServer {
  75. return buf, dataRc, fi, nil
  76. }
  77. pointer, _ := lfs.ReadPointerFromBuffer(buf)
  78. if !pointer.IsValid() { // fallback to a plain file
  79. return buf, dataRc, fi, nil
  80. }
  81. meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
  82. if err != nil { // fallback to a plain file
  83. log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
  84. return buf, dataRc, fi, nil
  85. }
  86. // close the old dataRc and open the real LFS target
  87. _ = dataRc.Close()
  88. dataRc, err = lfs.ReadMetaObject(pointer)
  89. if err != nil {
  90. return nil, nil, nil, err
  91. }
  92. buf = make([]byte, prefetchSize)
  93. n, err = util.ReadAtMost(dataRc, buf)
  94. if err != nil {
  95. _ = dataRc.Close()
  96. return nil, nil, fi, err
  97. }
  98. buf = buf[:n]
  99. fi.st = typesniffer.DetectContentType(buf)
  100. fi.fileSize = blob.Size()
  101. fi.lfsMeta = &meta.Pointer
  102. return buf, dataRc, fi, nil
  103. }
  104. func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
  105. // Show latest commit info of repository in table header,
  106. // or of directory if not in root directory.
  107. ctx.Data["LatestCommit"] = latestCommit
  108. if latestCommit != nil {
  109. verification := asymkey_service.ParseCommitWithSignature(ctx, latestCommit)
  110. if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
  111. return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
  112. }, nil); err != nil {
  113. ctx.ServerError("CalculateTrustStatus", err)
  114. return false
  115. }
  116. ctx.Data["LatestCommitVerification"] = verification
  117. ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
  118. statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
  119. if err != nil {
  120. log.Error("GetLatestCommitStatus: %v", err)
  121. }
  122. if !ctx.Repo.CanRead(unit_model.TypeActions) {
  123. git_model.CommitStatusesHideActionsURL(ctx, statuses)
  124. }
  125. ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(statuses)
  126. ctx.Data["LatestCommitStatuses"] = statuses
  127. }
  128. return true
  129. }
  130. func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
  131. markupRd, markupWr := io.Pipe()
  132. defer markupWr.Close()
  133. done := make(chan struct{})
  134. go func() {
  135. sb := &strings.Builder{}
  136. // We allow NBSP here this is rendered
  137. escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP)
  138. output = template.HTML(sb.String())
  139. close(done)
  140. }()
  141. err = markup.Render(renderCtx, input, markupWr)
  142. _ = markupWr.CloseWithError(err)
  143. <-done
  144. return escaped, output, err
  145. }
  146. func checkHomeCodeViewable(ctx *context.Context) {
  147. if ctx.Repo.HasUnits() {
  148. if ctx.Repo.Repository.IsBeingCreated() {
  149. task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID)
  150. if err != nil {
  151. if admin_model.IsErrTaskDoesNotExist(err) {
  152. ctx.Data["Repo"] = ctx.Repo
  153. ctx.Data["CloneAddr"] = ""
  154. ctx.Data["Failed"] = true
  155. ctx.HTML(http.StatusOK, tplMigrating)
  156. return
  157. }
  158. ctx.ServerError("models.GetMigratingTask", err)
  159. return
  160. }
  161. cfg, err := task.MigrateConfig()
  162. if err != nil {
  163. ctx.ServerError("task.MigrateConfig", err)
  164. return
  165. }
  166. ctx.Data["Repo"] = ctx.Repo
  167. ctx.Data["MigrateTask"] = task
  168. ctx.Data["CloneAddr"], _ = util.SanitizeURL(cfg.CloneAddr)
  169. ctx.Data["Failed"] = task.Status == structs.TaskStatusFailed
  170. ctx.HTML(http.StatusOK, tplMigrating)
  171. return
  172. }
  173. if ctx.IsSigned {
  174. // Set repo notification-status read if unread
  175. if err := activities_model.SetRepoReadBy(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID); err != nil {
  176. ctx.ServerError("ReadBy", err)
  177. return
  178. }
  179. }
  180. var firstUnit *unit_model.Unit
  181. for _, repoUnitType := range ctx.Repo.Permission.ReadableUnitTypes() {
  182. if repoUnitType == unit_model.TypeCode {
  183. // we are doing this check in "code" unit related pages, so if the code unit is readable, no need to do any further redirection
  184. return
  185. }
  186. unit, ok := unit_model.Units[repoUnitType]
  187. if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) {
  188. firstUnit = &unit
  189. }
  190. }
  191. if firstUnit != nil {
  192. ctx.Redirect(fmt.Sprintf("%s%s", ctx.Repo.Repository.Link(), firstUnit.URI))
  193. return
  194. }
  195. }
  196. ctx.NotFound(errors.New(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
  197. }
  198. // LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
  199. func LastCommit(ctx *context.Context) {
  200. checkHomeCodeViewable(ctx)
  201. if ctx.Written() {
  202. return
  203. }
  204. renderDirectoryFiles(ctx, 0)
  205. if ctx.Written() {
  206. return
  207. }
  208. var treeNames []string
  209. paths := make([]string, 0, 5)
  210. if len(ctx.Repo.TreePath) > 0 {
  211. treeNames = strings.Split(ctx.Repo.TreePath, "/")
  212. for i := range treeNames {
  213. paths = append(paths, strings.Join(treeNames[:i+1], "/"))
  214. }
  215. ctx.Data["HasParentPath"] = true
  216. if len(paths)-2 >= 0 {
  217. ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
  218. }
  219. }
  220. branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
  221. ctx.Data["BranchLink"] = branchLink
  222. ctx.HTML(http.StatusOK, tplRepoViewList)
  223. }
  224. func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
  225. renderedIconPool := fileicon.NewRenderedIconPool()
  226. fileIcons := map[string]template.HTML{}
  227. for _, f := range files {
  228. fullPath := path.Join(ctx.Repo.TreePath, f.Entry.Name())
  229. entryInfo := fileicon.EntryInfoFromGitTreeEntry(ctx.Repo.Commit, fullPath, f.Entry)
  230. fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
  231. }
  232. fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
  233. ctx.Data["FileIcons"] = fileIcons
  234. ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
  235. }
  236. func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
  237. tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
  238. if err != nil {
  239. HandleGitError(ctx, "Repo.Commit.SubTree", err)
  240. return nil
  241. }
  242. ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  243. // Get current entry user currently looking at.
  244. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
  245. if err != nil {
  246. HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
  247. return nil
  248. }
  249. if !entry.IsDir() {
  250. HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
  251. return nil
  252. }
  253. allEntries, err := tree.ListEntries()
  254. if err != nil {
  255. ctx.ServerError("ListEntries", err)
  256. return nil
  257. }
  258. allEntries.CustomSort(base.NaturalSortLess)
  259. commitInfoCtx := gocontext.Context(ctx)
  260. if timeout > 0 {
  261. var cancel gocontext.CancelFunc
  262. commitInfoCtx, cancel = gocontext.WithTimeout(ctx, timeout)
  263. defer cancel()
  264. }
  265. files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.Commit, ctx.Repo.TreePath)
  266. if err != nil {
  267. ctx.ServerError("GetCommitsInfo", err)
  268. return nil
  269. }
  270. ctx.Data["Files"] = files
  271. prepareDirectoryFileIcons(ctx, files)
  272. for _, f := range files {
  273. if f.Commit == nil {
  274. ctx.Data["HasFilesWithoutLatestCommit"] = true
  275. break
  276. }
  277. }
  278. if !loadLatestCommitData(ctx, latestCommit) {
  279. return nil
  280. }
  281. branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
  282. treeLink := branchLink
  283. if len(ctx.Repo.TreePath) > 0 {
  284. treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  285. }
  286. ctx.Data["TreeLink"] = treeLink
  287. return allEntries
  288. }
  289. // RenderUserCards render a page show users according the input template
  290. func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl templates.TplName) {
  291. page := ctx.FormInt("page")
  292. if page <= 0 {
  293. page = 1
  294. }
  295. pager := context.NewPagination(total, setting.ItemsPerPage, page, 5)
  296. ctx.Data["Page"] = pager
  297. items, err := getter(db.ListOptions{
  298. Page: pager.Paginater.Current(),
  299. PageSize: setting.ItemsPerPage,
  300. })
  301. if err != nil {
  302. ctx.ServerError("getter", err)
  303. return
  304. }
  305. ctx.Data["Cards"] = items
  306. ctx.HTML(http.StatusOK, tpl)
  307. }
  308. // Watchers render repository's watch users
  309. func Watchers(ctx *context.Context) {
  310. ctx.Data["Title"] = ctx.Tr("repo.watchers")
  311. ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
  312. RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, func(opts db.ListOptions) ([]*user_model.User, error) {
  313. return repo_model.GetRepoWatchers(ctx, ctx.Repo.Repository.ID, opts)
  314. }, tplWatchers)
  315. }
  316. // Stars render repository's starred users
  317. func Stars(ctx *context.Context) {
  318. ctx.Data["Title"] = ctx.Tr("repo.stargazers")
  319. ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
  320. RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*user_model.User, error) {
  321. return repo_model.GetStargazers(ctx, ctx.Repo.Repository, opts)
  322. }, tplWatchers)
  323. }
  324. // Forks render repository's forked users
  325. func Forks(ctx *context.Context) {
  326. ctx.Data["Title"] = ctx.Tr("repo.forks")
  327. page := ctx.FormInt("page")
  328. if page <= 0 {
  329. page = 1
  330. }
  331. pageSize := setting.ItemsPerPage
  332. forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, db.ListOptions{
  333. Page: page,
  334. PageSize: pageSize,
  335. })
  336. if err != nil {
  337. ctx.ServerError("FindForks", err)
  338. return
  339. }
  340. if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil {
  341. ctx.ServerError("LoadAttributes", err)
  342. return
  343. }
  344. pager := context.NewPagination(int(total), pageSize, page, 5)
  345. ctx.Data["ShowRepoOwnerAvatar"] = true
  346. ctx.Data["ShowRepoOwnerOnList"] = true
  347. ctx.Data["Page"] = pager
  348. ctx.Data["Repos"] = forks
  349. ctx.HTML(http.StatusOK, tplForks)
  350. }