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.

view.go 36KB

Git LFS support v2 (#122) * Import github.com/git-lfs/lfs-test-server as lfs module base Imported commit is 3968aac269a77b73924649b9412ae03f7ccd3198 Removed: Dockerfile CONTRIBUTING.md mgmt* script/ vendor/ kvlogger.go .dockerignore .gitignore README.md * Remove config, add JWT support from github.com/mgit-at/lfs-test-server Imported commit f0cdcc5a01599c5a955dc1bbf683bb4acecdba83 * Add LFS settings * Add LFS meta object model * Add LFS routes and initialization * Import github.com/dgrijalva/jwt-go into vendor/ * Adapt LFS module: handlers, routing, meta store * Move LFS routes to /user/repo/info/lfs/* * Add request header checks to LFS BatchHandler / PostHandler * Implement LFS basic authentication * Rework JWT secret generation / load * Implement LFS SSH token authentication with JWT Specification: https://github.com/github/git-lfs/tree/master/docs/api * Integrate LFS settings into install process * Remove LFS objects when repository is deleted Only removes objects from content store when deleted repo is the only referencing repository * Make LFS module stateless Fixes bug where LFS would not work after installation without restarting Gitea * Change 500 'Internal Server Error' to 400 'Bad Request' * Change sql query to xorm call * Remove unneeded type from LFS module * Change internal imports to code.gitea.io/gitea/ * Add Gitea authors copyright * Change basic auth realm to "gitea-lfs" * Add unique indexes to LFS model * Use xorm count function in LFS check on repository delete * Return io.ReadCloser from content store and close after usage * Add LFS info to runWeb() * Export LFS content store base path * LFS file download from UI * Work around git-lfs client issue with unauthenticated requests Returning a dummy Authorization header for unauthenticated requests lets git-lfs client skip asking for auth credentials See: https://github.com/github/git-lfs/issues/1088 * Fix unauthenticated UI downloads from public repositories * Authentication check order, Finish LFS file view logic * Ignore LFS hooks if installed for current OS user Fixes Gitea UI actions for repositories tracking LFS files. Checks for minimum needed git version by parsing the semantic version string. * Hide LFS metafile diff from commit view, marking as binary * Show LFS notice if file in commit view is tracked * Add notbefore/nbf JWT claim * Correct lint suggestions - comments for structs and functions - Add comments to LFS model - Function comment for GetRandomBytesAsBase64 - LFS server function comments and lint variable suggestion * Move secret generation code out of conditional Ensures no LFS code may run with an empty secret * Do not hand out JWT tokens if LFS server support is disabled
7 years ago
9 years ago
Git LFS support v2 (#122) * Import github.com/git-lfs/lfs-test-server as lfs module base Imported commit is 3968aac269a77b73924649b9412ae03f7ccd3198 Removed: Dockerfile CONTRIBUTING.md mgmt* script/ vendor/ kvlogger.go .dockerignore .gitignore README.md * Remove config, add JWT support from github.com/mgit-at/lfs-test-server Imported commit f0cdcc5a01599c5a955dc1bbf683bb4acecdba83 * Add LFS settings * Add LFS meta object model * Add LFS routes and initialization * Import github.com/dgrijalva/jwt-go into vendor/ * Adapt LFS module: handlers, routing, meta store * Move LFS routes to /user/repo/info/lfs/* * Add request header checks to LFS BatchHandler / PostHandler * Implement LFS basic authentication * Rework JWT secret generation / load * Implement LFS SSH token authentication with JWT Specification: https://github.com/github/git-lfs/tree/master/docs/api * Integrate LFS settings into install process * Remove LFS objects when repository is deleted Only removes objects from content store when deleted repo is the only referencing repository * Make LFS module stateless Fixes bug where LFS would not work after installation without restarting Gitea * Change 500 'Internal Server Error' to 400 'Bad Request' * Change sql query to xorm call * Remove unneeded type from LFS module * Change internal imports to code.gitea.io/gitea/ * Add Gitea authors copyright * Change basic auth realm to "gitea-lfs" * Add unique indexes to LFS model * Use xorm count function in LFS check on repository delete * Return io.ReadCloser from content store and close after usage * Add LFS info to runWeb() * Export LFS content store base path * LFS file download from UI * Work around git-lfs client issue with unauthenticated requests Returning a dummy Authorization header for unauthenticated requests lets git-lfs client skip asking for auth credentials See: https://github.com/github/git-lfs/issues/1088 * Fix unauthenticated UI downloads from public repositories * Authentication check order, Finish LFS file view logic * Ignore LFS hooks if installed for current OS user Fixes Gitea UI actions for repositories tracking LFS files. Checks for minimum needed git version by parsing the semantic version string. * Hide LFS metafile diff from commit view, marking as binary * Show LFS notice if file in commit view is tracked * Add notbefore/nbf JWT claim * Correct lint suggestions - comments for structs and functions - Add comments to LFS model - Function comment for GetRandomBytesAsBase64 - LFS server function comments and lint variable suggestion * Move secret generation code out of conditional Ensures no LFS code may run with an empty secret * Do not hand out JWT tokens if LFS server support is disabled
7 years ago
Git LFS support v2 (#122) * Import github.com/git-lfs/lfs-test-server as lfs module base Imported commit is 3968aac269a77b73924649b9412ae03f7ccd3198 Removed: Dockerfile CONTRIBUTING.md mgmt* script/ vendor/ kvlogger.go .dockerignore .gitignore README.md * Remove config, add JWT support from github.com/mgit-at/lfs-test-server Imported commit f0cdcc5a01599c5a955dc1bbf683bb4acecdba83 * Add LFS settings * Add LFS meta object model * Add LFS routes and initialization * Import github.com/dgrijalva/jwt-go into vendor/ * Adapt LFS module: handlers, routing, meta store * Move LFS routes to /user/repo/info/lfs/* * Add request header checks to LFS BatchHandler / PostHandler * Implement LFS basic authentication * Rework JWT secret generation / load * Implement LFS SSH token authentication with JWT Specification: https://github.com/github/git-lfs/tree/master/docs/api * Integrate LFS settings into install process * Remove LFS objects when repository is deleted Only removes objects from content store when deleted repo is the only referencing repository * Make LFS module stateless Fixes bug where LFS would not work after installation without restarting Gitea * Change 500 'Internal Server Error' to 400 'Bad Request' * Change sql query to xorm call * Remove unneeded type from LFS module * Change internal imports to code.gitea.io/gitea/ * Add Gitea authors copyright * Change basic auth realm to "gitea-lfs" * Add unique indexes to LFS model * Use xorm count function in LFS check on repository delete * Return io.ReadCloser from content store and close after usage * Add LFS info to runWeb() * Export LFS content store base path * LFS file download from UI * Work around git-lfs client issue with unauthenticated requests Returning a dummy Authorization header for unauthenticated requests lets git-lfs client skip asking for auth credentials See: https://github.com/github/git-lfs/issues/1088 * Fix unauthenticated UI downloads from public repositories * Authentication check order, Finish LFS file view logic * Ignore LFS hooks if installed for current OS user Fixes Gitea UI actions for repositories tracking LFS files. Checks for minimum needed git version by parsing the semantic version string. * Hide LFS metafile diff from commit view, marking as binary * Show LFS notice if file in commit view is tracked * Add notbefore/nbf JWT claim * Correct lint suggestions - comments for structs and functions - Add comments to LFS model - Function comment for GetRandomBytesAsBase64 - LFS server function comments and lint variable suggestion * Move secret generation code out of conditional Ensures no LFS code may run with an empty secret * Do not hand out JWT tokens if LFS server support is disabled
7 years ago
Fix elipsis button not working if the last commit loading is deferred (#29544) Before this change, if we had more than 200 entries being deferred in loading, the entire table would get replaced thus losing any event listeners attached to the elements within the table, such as the elipsis button and commit list with tippy. With this change we remove the previous javascript code that replaced the table and use htmx to replace the table. htmx attributes added: - `hx-indicator="tr.notready td.message span"`: attach the loading spinner to the files whose last commit is still being loaded - `hx-trigger="load"` trigger the request-replace behavior as soon as possible - `hx-swap="morph"`: use the idiomorph morphing algorithm, this is the thing that makes it so the elipsis button event listener is kept during the replacement, fixing the bug because we don't actually replace the table, only modifying it - `hx-post="{{.LastCommitLoaderURL}}"`: make a post request to this url to get the table with all of the commit information As part of this change I removed the handling of partial replacement in the case we have less than 200 "not ready" files. The first reason is that I couldn't make htmx replace only a subset of returned elements, the second reason is that we have a cache implemented in the backend already so the only cost added is that we query the cache a few times (which is sure to be populated due to the initial request), and the last reason is that since the last refactor of this functionality that removed jQuery we don't properly send the "not ready" entries as the backend expects `FormData` with `f[]` and we send a JSON with `f` so we always query for all rows anyway. # Before ![before](https://github.com/go-gitea/gitea/assets/20454870/482ebfec-66c5-40cc-9c1e-e3b3bfe1bbc1) # After ![after](https://github.com/go-gitea/gitea/assets/20454870/454c517e-3a4e-4006-a49f-99cc56e0fd60) --------- Signed-off-by: Yarden Shoham <git@yardenshoham.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2 months ago
Fix elipsis button not working if the last commit loading is deferred (#29544) Before this change, if we had more than 200 entries being deferred in loading, the entire table would get replaced thus losing any event listeners attached to the elements within the table, such as the elipsis button and commit list with tippy. With this change we remove the previous javascript code that replaced the table and use htmx to replace the table. htmx attributes added: - `hx-indicator="tr.notready td.message span"`: attach the loading spinner to the files whose last commit is still being loaded - `hx-trigger="load"` trigger the request-replace behavior as soon as possible - `hx-swap="morph"`: use the idiomorph morphing algorithm, this is the thing that makes it so the elipsis button event listener is kept during the replacement, fixing the bug because we don't actually replace the table, only modifying it - `hx-post="{{.LastCommitLoaderURL}}"`: make a post request to this url to get the table with all of the commit information As part of this change I removed the handling of partial replacement in the case we have less than 200 "not ready" files. The first reason is that I couldn't make htmx replace only a subset of returned elements, the second reason is that we have a cache implemented in the backend already so the only cost added is that we query the cache a few times (which is sure to be populated due to the initial request), and the last reason is that since the last refactor of this functionality that removed jQuery we don't properly send the "not ready" entries as the backend expects `FormData` with `f[]` and we send a JSON with `f` so we always query for all rows anyway. # Before ![before](https://github.com/go-gitea/gitea/assets/20454870/482ebfec-66c5-40cc-9c1e-e3b3bfe1bbc1) # After ![after](https://github.com/go-gitea/gitea/assets/20454870/454c517e-3a4e-4006-a49f-99cc56e0fd60) --------- Signed-off-by: Yarden Shoham <git@yardenshoham.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2 months ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168
  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. "bytes"
  7. gocontext "context"
  8. "encoding/base64"
  9. "fmt"
  10. "html/template"
  11. "image"
  12. "io"
  13. "net/http"
  14. "net/url"
  15. "path"
  16. "slices"
  17. "strings"
  18. "time"
  19. _ "image/gif" // for processing gif images
  20. _ "image/jpeg" // for processing jpeg images
  21. _ "image/png" // for processing png images
  22. activities_model "code.gitea.io/gitea/models/activities"
  23. admin_model "code.gitea.io/gitea/models/admin"
  24. asymkey_model "code.gitea.io/gitea/models/asymkey"
  25. "code.gitea.io/gitea/models/db"
  26. git_model "code.gitea.io/gitea/models/git"
  27. issue_model "code.gitea.io/gitea/models/issues"
  28. repo_model "code.gitea.io/gitea/models/repo"
  29. unit_model "code.gitea.io/gitea/models/unit"
  30. user_model "code.gitea.io/gitea/models/user"
  31. "code.gitea.io/gitea/modules/actions"
  32. "code.gitea.io/gitea/modules/base"
  33. "code.gitea.io/gitea/modules/charset"
  34. "code.gitea.io/gitea/modules/git"
  35. "code.gitea.io/gitea/modules/highlight"
  36. "code.gitea.io/gitea/modules/lfs"
  37. "code.gitea.io/gitea/modules/log"
  38. "code.gitea.io/gitea/modules/markup"
  39. repo_module "code.gitea.io/gitea/modules/repository"
  40. "code.gitea.io/gitea/modules/setting"
  41. "code.gitea.io/gitea/modules/structs"
  42. "code.gitea.io/gitea/modules/svg"
  43. "code.gitea.io/gitea/modules/typesniffer"
  44. "code.gitea.io/gitea/modules/util"
  45. "code.gitea.io/gitea/routers/web/feed"
  46. "code.gitea.io/gitea/services/context"
  47. issue_service "code.gitea.io/gitea/services/issue"
  48. files_service "code.gitea.io/gitea/services/repository/files"
  49. "github.com/nektos/act/pkg/model"
  50. _ "golang.org/x/image/bmp" // for processing bmp images
  51. _ "golang.org/x/image/webp" // for processing webp images
  52. )
  53. const (
  54. tplRepoEMPTY base.TplName = "repo/empty"
  55. tplRepoHome base.TplName = "repo/home"
  56. tplRepoViewList base.TplName = "repo/view_list"
  57. tplWatchers base.TplName = "repo/watchers"
  58. tplForks base.TplName = "repo/forks"
  59. tplMigrating base.TplName = "repo/migrate/migrating"
  60. )
  61. // locate a README for a tree in one of the supported paths.
  62. //
  63. // entries is passed to reduce calls to ListEntries(), so
  64. // this has precondition:
  65. //
  66. // entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
  67. //
  68. // FIXME: There has to be a more efficient way of doing this
  69. func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
  70. // Create a list of extensions in priority order
  71. // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
  72. // 2. Txt files - e.g. README.txt
  73. // 3. No extension - e.g. README
  74. exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
  75. extCount := len(exts)
  76. readmeFiles := make([]*git.TreeEntry, extCount+1)
  77. docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
  78. for _, entry := range entries {
  79. if tryWellKnownDirs && entry.IsDir() {
  80. // as a special case for the top-level repo introduction README,
  81. // fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ...
  82. // (note that docsEntries is ignored unless we are at the root)
  83. lowerName := strings.ToLower(entry.Name())
  84. switch lowerName {
  85. case "docs":
  86. if entry.Name() == "docs" || docsEntries[0] == nil {
  87. docsEntries[0] = entry
  88. }
  89. case ".gitea":
  90. if entry.Name() == ".gitea" || docsEntries[1] == nil {
  91. docsEntries[1] = entry
  92. }
  93. case ".github":
  94. if entry.Name() == ".github" || docsEntries[2] == nil {
  95. docsEntries[2] = entry
  96. }
  97. }
  98. continue
  99. }
  100. if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
  101. log.Debug("Potential readme file: %s", entry.Name())
  102. if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
  103. if entry.IsLink() {
  104. target, err := entry.FollowLinks()
  105. if err != nil && !git.IsErrBadLink(err) {
  106. return "", nil, err
  107. } else if target != nil && (target.IsExecutable() || target.IsRegular()) {
  108. readmeFiles[i] = entry
  109. }
  110. } else {
  111. readmeFiles[i] = entry
  112. }
  113. }
  114. }
  115. }
  116. var readmeFile *git.TreeEntry
  117. for _, f := range readmeFiles {
  118. if f != nil {
  119. readmeFile = f
  120. break
  121. }
  122. }
  123. if ctx.Repo.TreePath == "" && readmeFile == nil {
  124. for _, subTreeEntry := range docsEntries {
  125. if subTreeEntry == nil {
  126. continue
  127. }
  128. subTree := subTreeEntry.Tree()
  129. if subTree == nil {
  130. // this should be impossible; if subTreeEntry exists so should this.
  131. continue
  132. }
  133. var err error
  134. childEntries, err := subTree.ListEntries()
  135. if err != nil {
  136. return "", nil, err
  137. }
  138. subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false)
  139. if err != nil && !git.IsErrNotExist(err) {
  140. return "", nil, err
  141. }
  142. if readmeFile != nil {
  143. return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil
  144. }
  145. }
  146. }
  147. return "", readmeFile, nil
  148. }
  149. func renderDirectory(ctx *context.Context) {
  150. entries := renderDirectoryFiles(ctx, 1*time.Second)
  151. if ctx.Written() {
  152. return
  153. }
  154. if ctx.Repo.TreePath != "" {
  155. ctx.Data["HideRepoInfo"] = true
  156. ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
  157. }
  158. subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
  159. if err != nil {
  160. ctx.ServerError("findReadmeFileInEntries", err)
  161. return
  162. }
  163. renderReadmeFile(ctx, subfolder, readmeFile)
  164. }
  165. // localizedExtensions prepends the provided language code with and without a
  166. // regional identifier to the provided extension.
  167. // Note: the language code will always be lower-cased, if a region is present it must be separated with a `-`
  168. // Note: ext should be prefixed with a `.`
  169. func localizedExtensions(ext, languageCode string) (localizedExts []string) {
  170. if len(languageCode) < 1 {
  171. return []string{ext}
  172. }
  173. lowerLangCode := "." + strings.ToLower(languageCode)
  174. if strings.Contains(lowerLangCode, "-") {
  175. underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_")
  176. indexOfDash := strings.Index(lowerLangCode, "-")
  177. // e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md]
  178. return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext}
  179. }
  180. // e.g. [.en.md, .md]
  181. return []string{lowerLangCode + ext, ext}
  182. }
  183. type fileInfo struct {
  184. isTextFile bool
  185. isLFSFile bool
  186. fileSize int64
  187. lfsMeta *lfs.Pointer
  188. st typesniffer.SniffedType
  189. }
  190. func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) {
  191. dataRc, err := blob.DataAsync()
  192. if err != nil {
  193. return nil, nil, nil, err
  194. }
  195. buf := make([]byte, 1024)
  196. n, _ := util.ReadAtMost(dataRc, buf)
  197. buf = buf[:n]
  198. st := typesniffer.DetectContentType(buf)
  199. isTextFile := st.IsText()
  200. // FIXME: what happens when README file is an image?
  201. if !isTextFile || !setting.LFS.StartServer {
  202. return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
  203. }
  204. pointer, _ := lfs.ReadPointerFromBuffer(buf)
  205. if !pointer.IsValid() { // fallback to plain file
  206. return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
  207. }
  208. meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
  209. if err != nil && err != git_model.ErrLFSObjectNotExist { // fallback to plain file
  210. return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
  211. }
  212. dataRc.Close()
  213. if err != nil {
  214. return nil, nil, nil, err
  215. }
  216. dataRc, err = lfs.ReadMetaObject(pointer)
  217. if err != nil {
  218. return nil, nil, nil, err
  219. }
  220. buf = make([]byte, 1024)
  221. n, err = util.ReadAtMost(dataRc, buf)
  222. if err != nil {
  223. dataRc.Close()
  224. return nil, nil, nil, err
  225. }
  226. buf = buf[:n]
  227. st = typesniffer.DetectContentType(buf)
  228. return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
  229. }
  230. func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
  231. target := readmeFile
  232. if readmeFile != nil && readmeFile.IsLink() {
  233. target, _ = readmeFile.FollowLinks()
  234. }
  235. if target == nil {
  236. // if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
  237. // simply skip rendering the README
  238. return
  239. }
  240. ctx.Data["RawFileLink"] = ""
  241. ctx.Data["ReadmeInList"] = true
  242. ctx.Data["ReadmeExist"] = true
  243. ctx.Data["FileIsSymlink"] = readmeFile.IsLink()
  244. buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob())
  245. if err != nil {
  246. ctx.ServerError("getFileReader", err)
  247. return
  248. }
  249. defer dataRc.Close()
  250. ctx.Data["FileIsText"] = fInfo.isTextFile
  251. ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name())
  252. ctx.Data["IsLFSFile"] = fInfo.isLFSFile
  253. if fInfo.isLFSFile {
  254. filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name()))
  255. ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
  256. }
  257. if !fInfo.isTextFile {
  258. return
  259. }
  260. if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
  261. // Pretend that this is a normal text file to display 'This file is too large to be shown'
  262. ctx.Data["IsFileTooLarge"] = true
  263. ctx.Data["IsTextFile"] = true
  264. ctx.Data["FileSize"] = fInfo.fileSize
  265. return
  266. }
  267. rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
  268. if markupType := markup.Type(readmeFile.Name()); markupType != "" {
  269. ctx.Data["IsMarkup"] = true
  270. ctx.Data["MarkupType"] = markupType
  271. ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
  272. Ctx: ctx,
  273. RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
  274. Links: markup.Links{
  275. Base: ctx.Repo.RepoLink,
  276. BranchPath: ctx.Repo.BranchNameSubURL(),
  277. TreePath: path.Join(ctx.Repo.TreePath, subfolder),
  278. },
  279. Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
  280. GitRepo: ctx.Repo.GitRepo,
  281. }, rd)
  282. if err != nil {
  283. log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
  284. delete(ctx.Data, "IsMarkup")
  285. }
  286. }
  287. if ctx.Data["IsMarkup"] != true {
  288. ctx.Data["IsPlainText"] = true
  289. content, err := io.ReadAll(rd)
  290. if err != nil {
  291. log.Error("Read readme content failed: %v", err)
  292. }
  293. contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content))
  294. ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
  295. }
  296. if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
  297. ctx.Data["CanEditReadmeFile"] = true
  298. }
  299. }
  300. func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
  301. // Show latest commit info of repository in table header,
  302. // or of directory if not in root directory.
  303. ctx.Data["LatestCommit"] = latestCommit
  304. if latestCommit != nil {
  305. verification := asymkey_model.ParseCommitWithSignature(ctx, latestCommit)
  306. if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
  307. return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
  308. }, nil); err != nil {
  309. ctx.ServerError("CalculateTrustStatus", err)
  310. return false
  311. }
  312. ctx.Data["LatestCommitVerification"] = verification
  313. ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
  314. statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptions{ListAll: true})
  315. if err != nil {
  316. log.Error("GetLatestCommitStatus: %v", err)
  317. }
  318. ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(statuses)
  319. ctx.Data["LatestCommitStatuses"] = statuses
  320. }
  321. return true
  322. }
  323. func renderFile(ctx *context.Context, entry *git.TreeEntry) {
  324. ctx.Data["IsViewFile"] = true
  325. ctx.Data["HideRepoInfo"] = true
  326. blob := entry.Blob()
  327. buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
  328. if err != nil {
  329. ctx.ServerError("getFileReader", err)
  330. return
  331. }
  332. defer dataRc.Close()
  333. ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
  334. ctx.Data["FileIsSymlink"] = entry.IsLink()
  335. ctx.Data["FileName"] = blob.Name()
  336. ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  337. commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
  338. if err != nil {
  339. ctx.ServerError("GetCommitByPath", err)
  340. return
  341. }
  342. if !loadLatestCommitData(ctx, commit) {
  343. return
  344. }
  345. if ctx.Repo.TreePath == ".editorconfig" {
  346. _, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
  347. if editorconfigWarning != nil {
  348. ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error())
  349. }
  350. if editorconfigErr != nil {
  351. ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error())
  352. }
  353. } else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) {
  354. _, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit)
  355. if issueConfigErr != nil {
  356. ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error())
  357. }
  358. } else if actions.IsWorkflow(ctx.Repo.TreePath) {
  359. content, err := actions.GetContentFromEntry(entry)
  360. if err != nil {
  361. log.Error("actions.GetContentFromEntry: %v", err)
  362. }
  363. _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content))
  364. if workFlowErr != nil {
  365. ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
  366. }
  367. } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
  368. if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
  369. _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
  370. if len(warnings) > 0 {
  371. ctx.Data["FileWarning"] = strings.Join(warnings, "\n")
  372. }
  373. }
  374. }
  375. isDisplayingSource := ctx.FormString("display") == "source"
  376. isDisplayingRendered := !isDisplayingSource
  377. if fInfo.isLFSFile {
  378. ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  379. }
  380. isRepresentableAsText := fInfo.st.IsRepresentableAsText()
  381. if !isRepresentableAsText {
  382. // If we can't show plain text, always try to render.
  383. isDisplayingSource = false
  384. isDisplayingRendered = true
  385. }
  386. ctx.Data["IsLFSFile"] = fInfo.isLFSFile
  387. ctx.Data["FileSize"] = fInfo.fileSize
  388. ctx.Data["IsTextFile"] = fInfo.isTextFile
  389. ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
  390. ctx.Data["IsDisplayingSource"] = isDisplayingSource
  391. ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
  392. ctx.Data["IsExecutable"] = entry.IsExecutable()
  393. isTextSource := fInfo.isTextFile || isDisplayingSource
  394. ctx.Data["IsTextSource"] = isTextSource
  395. if isTextSource {
  396. ctx.Data["CanCopyContent"] = true
  397. }
  398. // Check LFS Lock
  399. lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
  400. ctx.Data["LFSLock"] = lfsLock
  401. if err != nil {
  402. ctx.ServerError("GetTreePathLock", err)
  403. return
  404. }
  405. if lfsLock != nil {
  406. u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
  407. if err != nil {
  408. ctx.ServerError("GetTreePathLock", err)
  409. return
  410. }
  411. ctx.Data["LFSLockOwner"] = u.Name
  412. ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
  413. ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
  414. }
  415. // Assume file is not editable first.
  416. if fInfo.isLFSFile {
  417. ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
  418. } else if !isRepresentableAsText {
  419. ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
  420. }
  421. switch {
  422. case isRepresentableAsText:
  423. if fInfo.st.IsSvgImage() {
  424. ctx.Data["IsImageFile"] = true
  425. ctx.Data["CanCopyContent"] = true
  426. ctx.Data["HasSourceRenderedToggle"] = true
  427. }
  428. if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
  429. ctx.Data["IsFileTooLarge"] = true
  430. break
  431. }
  432. rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
  433. shouldRenderSource := ctx.FormString("display") == "source"
  434. readmeExist := util.IsReadmeFileName(blob.Name())
  435. ctx.Data["ReadmeExist"] = readmeExist
  436. markupType := markup.Type(blob.Name())
  437. // If the markup is detected by custom markup renderer it should not be reset later on
  438. // to not pass it down to the render context.
  439. detected := false
  440. if markupType == "" {
  441. detected = true
  442. markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
  443. }
  444. if markupType != "" {
  445. ctx.Data["HasSourceRenderedToggle"] = true
  446. }
  447. if markupType != "" && !shouldRenderSource {
  448. ctx.Data["IsMarkup"] = true
  449. ctx.Data["MarkupType"] = markupType
  450. if !detected {
  451. markupType = ""
  452. }
  453. metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
  454. metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
  455. ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
  456. Ctx: ctx,
  457. Type: markupType,
  458. RelativePath: ctx.Repo.TreePath,
  459. Links: markup.Links{
  460. Base: ctx.Repo.RepoLink,
  461. BranchPath: ctx.Repo.BranchNameSubURL(),
  462. TreePath: path.Dir(ctx.Repo.TreePath),
  463. },
  464. Metas: metas,
  465. GitRepo: ctx.Repo.GitRepo,
  466. }, rd)
  467. if err != nil {
  468. ctx.ServerError("Render", err)
  469. return
  470. }
  471. // to prevent iframe load third-party url
  472. ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
  473. } else {
  474. buf, _ := io.ReadAll(rd)
  475. // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
  476. // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
  477. // Gitea uses the definition (like most modern editors):
  478. // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
  479. // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
  480. // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
  481. // This NumLines is only used for the display on the UI: "xxx lines"
  482. if len(buf) == 0 {
  483. ctx.Data["NumLines"] = 0
  484. } else {
  485. ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
  486. }
  487. ctx.Data["NumLinesSet"] = true
  488. language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
  489. if err != nil {
  490. log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
  491. }
  492. fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
  493. ctx.Data["LexerName"] = lexerName
  494. if err != nil {
  495. log.Error("highlight.File failed, fallback to plain text: %v", err)
  496. fileContent = highlight.PlainText(buf)
  497. }
  498. status := &charset.EscapeStatus{}
  499. statuses := make([]*charset.EscapeStatus, len(fileContent))
  500. for i, line := range fileContent {
  501. statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
  502. status = status.Or(statuses[i])
  503. }
  504. ctx.Data["EscapeStatus"] = status
  505. ctx.Data["FileContent"] = fileContent
  506. ctx.Data["LineEscapeStatus"] = statuses
  507. }
  508. if !fInfo.isLFSFile {
  509. if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
  510. if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
  511. ctx.Data["CanEditFile"] = false
  512. ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
  513. } else {
  514. ctx.Data["CanEditFile"] = true
  515. ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
  516. }
  517. } else if !ctx.Repo.IsViewBranch {
  518. ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
  519. } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
  520. ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
  521. }
  522. }
  523. case fInfo.st.IsPDF():
  524. ctx.Data["IsPDFFile"] = true
  525. case fInfo.st.IsVideo():
  526. ctx.Data["IsVideoFile"] = true
  527. case fInfo.st.IsAudio():
  528. ctx.Data["IsAudioFile"] = true
  529. case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
  530. ctx.Data["IsImageFile"] = true
  531. ctx.Data["CanCopyContent"] = true
  532. default:
  533. if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
  534. ctx.Data["IsFileTooLarge"] = true
  535. break
  536. }
  537. if markupType := markup.Type(blob.Name()); markupType != "" {
  538. rd := io.MultiReader(bytes.NewReader(buf), dataRc)
  539. ctx.Data["IsMarkup"] = true
  540. ctx.Data["MarkupType"] = markupType
  541. ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
  542. Ctx: ctx,
  543. RelativePath: ctx.Repo.TreePath,
  544. Links: markup.Links{
  545. Base: ctx.Repo.RepoLink,
  546. BranchPath: ctx.Repo.BranchNameSubURL(),
  547. TreePath: path.Dir(ctx.Repo.TreePath),
  548. },
  549. Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
  550. GitRepo: ctx.Repo.GitRepo,
  551. }, rd)
  552. if err != nil {
  553. ctx.ServerError("Render", err)
  554. return
  555. }
  556. }
  557. }
  558. if ctx.Repo.GitRepo != nil {
  559. checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID)
  560. if checker != nil {
  561. defer deferable()
  562. attrs, err := checker.CheckPath(ctx.Repo.TreePath)
  563. if err == nil {
  564. ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value()
  565. ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value()
  566. }
  567. }
  568. }
  569. if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() {
  570. img, _, err := image.DecodeConfig(bytes.NewReader(buf))
  571. if err == nil {
  572. // There are Image formats go can't decode
  573. // Instead of throwing an error in that case, we show the size only when we can decode
  574. ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
  575. }
  576. }
  577. if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
  578. if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
  579. ctx.Data["CanDeleteFile"] = false
  580. ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
  581. } else {
  582. ctx.Data["CanDeleteFile"] = true
  583. ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
  584. }
  585. } else if !ctx.Repo.IsViewBranch {
  586. ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
  587. } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
  588. ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
  589. }
  590. }
  591. func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
  592. markupRd, markupWr := io.Pipe()
  593. defer markupWr.Close()
  594. done := make(chan struct{})
  595. go func() {
  596. sb := &strings.Builder{}
  597. // We allow NBSP here this is rendered
  598. escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP)
  599. output = template.HTML(sb.String())
  600. close(done)
  601. }()
  602. err = markup.Render(renderCtx, input, markupWr)
  603. _ = markupWr.CloseWithError(err)
  604. <-done
  605. return escaped, output, err
  606. }
  607. func checkHomeCodeViewable(ctx *context.Context) {
  608. if len(ctx.Repo.Units) > 0 {
  609. if ctx.Repo.Repository.IsBeingCreated() {
  610. task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID)
  611. if err != nil {
  612. if admin_model.IsErrTaskDoesNotExist(err) {
  613. ctx.Data["Repo"] = ctx.Repo
  614. ctx.Data["CloneAddr"] = ""
  615. ctx.Data["Failed"] = true
  616. ctx.HTML(http.StatusOK, tplMigrating)
  617. return
  618. }
  619. ctx.ServerError("models.GetMigratingTask", err)
  620. return
  621. }
  622. cfg, err := task.MigrateConfig()
  623. if err != nil {
  624. ctx.ServerError("task.MigrateConfig", err)
  625. return
  626. }
  627. ctx.Data["Repo"] = ctx.Repo
  628. ctx.Data["MigrateTask"] = task
  629. ctx.Data["CloneAddr"], _ = util.SanitizeURL(cfg.CloneAddr)
  630. ctx.Data["Failed"] = task.Status == structs.TaskStatusFailed
  631. ctx.HTML(http.StatusOK, tplMigrating)
  632. return
  633. }
  634. if ctx.IsSigned {
  635. // Set repo notification-status read if unread
  636. if err := activities_model.SetRepoReadBy(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID); err != nil {
  637. ctx.ServerError("ReadBy", err)
  638. return
  639. }
  640. }
  641. var firstUnit *unit_model.Unit
  642. for _, repoUnit := range ctx.Repo.Units {
  643. if repoUnit.Type == unit_model.TypeCode {
  644. return
  645. }
  646. unit, ok := unit_model.Units[repoUnit.Type]
  647. if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) {
  648. firstUnit = &unit
  649. }
  650. }
  651. if firstUnit != nil {
  652. ctx.Redirect(fmt.Sprintf("%s%s", ctx.Repo.Repository.Link(), firstUnit.URI))
  653. return
  654. }
  655. }
  656. ctx.NotFound("Home", fmt.Errorf(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
  657. }
  658. func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
  659. if entry.Name() != "" {
  660. return
  661. }
  662. tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
  663. if err != nil {
  664. HandleGitError(ctx, "Repo.Commit.SubTree", err)
  665. return
  666. }
  667. allEntries, err := tree.ListEntries()
  668. if err != nil {
  669. ctx.ServerError("ListEntries", err)
  670. return
  671. }
  672. for _, entry := range allEntries {
  673. if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" {
  674. // Read Citation file contents
  675. if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
  676. log.Error("checkCitationFile: GetBlobContent: %v", err)
  677. } else {
  678. ctx.Data["CitiationExist"] = true
  679. ctx.PageData["citationFileContent"] = content
  680. break
  681. }
  682. }
  683. }
  684. }
  685. // Home render repository home page
  686. func Home(ctx *context.Context) {
  687. if setting.Other.EnableFeed {
  688. isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req)
  689. if isFeed {
  690. switch {
  691. case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType):
  692. feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
  693. case ctx.Repo.TreePath == "":
  694. feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
  695. case ctx.Repo.TreePath != "":
  696. feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
  697. }
  698. return
  699. }
  700. }
  701. checkHomeCodeViewable(ctx)
  702. if ctx.Written() {
  703. return
  704. }
  705. renderHomeCode(ctx)
  706. }
  707. // LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
  708. func LastCommit(ctx *context.Context) {
  709. checkHomeCodeViewable(ctx)
  710. if ctx.Written() {
  711. return
  712. }
  713. renderDirectoryFiles(ctx, 0)
  714. if ctx.Written() {
  715. return
  716. }
  717. var treeNames []string
  718. paths := make([]string, 0, 5)
  719. if len(ctx.Repo.TreePath) > 0 {
  720. treeNames = strings.Split(ctx.Repo.TreePath, "/")
  721. for i := range treeNames {
  722. paths = append(paths, strings.Join(treeNames[:i+1], "/"))
  723. }
  724. ctx.Data["HasParentPath"] = true
  725. if len(paths)-2 >= 0 {
  726. ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
  727. }
  728. }
  729. branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  730. ctx.Data["BranchLink"] = branchLink
  731. ctx.HTML(http.StatusOK, tplRepoViewList)
  732. }
  733. func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
  734. tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
  735. if err != nil {
  736. HandleGitError(ctx, "Repo.Commit.SubTree", err)
  737. return nil
  738. }
  739. ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  740. // Get current entry user currently looking at.
  741. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
  742. if err != nil {
  743. HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
  744. return nil
  745. }
  746. if !entry.IsDir() {
  747. HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
  748. return nil
  749. }
  750. allEntries, err := tree.ListEntries()
  751. if err != nil {
  752. ctx.ServerError("ListEntries", err)
  753. return nil
  754. }
  755. allEntries.CustomSort(base.NaturalSortLess)
  756. commitInfoCtx := gocontext.Context(ctx)
  757. if timeout > 0 {
  758. var cancel gocontext.CancelFunc
  759. commitInfoCtx, cancel = gocontext.WithTimeout(ctx, timeout)
  760. defer cancel()
  761. }
  762. files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
  763. if err != nil {
  764. ctx.ServerError("GetCommitsInfo", err)
  765. return nil
  766. }
  767. ctx.Data["Files"] = files
  768. for _, f := range files {
  769. if f.Commit == nil {
  770. ctx.Data["HasFilesWithoutLatestCommit"] = true
  771. break
  772. }
  773. }
  774. if !loadLatestCommitData(ctx, latestCommit) {
  775. return nil
  776. }
  777. branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  778. treeLink := branchLink
  779. if len(ctx.Repo.TreePath) > 0 {
  780. treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  781. }
  782. ctx.Data["TreeLink"] = treeLink
  783. ctx.Data["SSHDomain"] = setting.SSH.Domain
  784. return allEntries
  785. }
  786. func renderLanguageStats(ctx *context.Context) {
  787. langs, err := repo_model.GetTopLanguageStats(ctx, ctx.Repo.Repository, 5)
  788. if err != nil {
  789. ctx.ServerError("Repo.GetTopLanguageStats", err)
  790. return
  791. }
  792. ctx.Data["LanguageStats"] = langs
  793. }
  794. func renderRepoTopics(ctx *context.Context) {
  795. topics, _, err := repo_model.FindTopics(ctx, &repo_model.FindTopicOptions{
  796. RepoID: ctx.Repo.Repository.ID,
  797. })
  798. if err != nil {
  799. ctx.ServerError("models.FindTopics", err)
  800. return
  801. }
  802. ctx.Data["Topics"] = topics
  803. }
  804. func prepareOpenWithEditorApps(ctx *context.Context) {
  805. var tmplApps []map[string]any
  806. apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
  807. if len(apps) == 0 {
  808. apps = setting.DefaultOpenWithEditorApps()
  809. }
  810. for _, app := range apps {
  811. schema, _, _ := strings.Cut(app.OpenURL, ":")
  812. var iconHTML template.HTML
  813. if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
  814. iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "gt-mr-3")
  815. } else {
  816. iconHTML = svg.RenderHTML("gitea-git", 16, "gt-mr-3") // TODO: it could support user's customized icon in the future
  817. }
  818. tmplApps = append(tmplApps, map[string]any{
  819. "DisplayName": app.DisplayName,
  820. "OpenURL": app.OpenURL,
  821. "IconHTML": iconHTML,
  822. })
  823. }
  824. ctx.Data["OpenWithEditorApps"] = tmplApps
  825. }
  826. func renderHomeCode(ctx *context.Context) {
  827. ctx.Data["PageIsViewCode"] = true
  828. ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
  829. prepareOpenWithEditorApps(ctx)
  830. if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
  831. showEmpty := true
  832. var err error
  833. if ctx.Repo.GitRepo != nil {
  834. showEmpty, err = ctx.Repo.GitRepo.IsEmpty()
  835. if err != nil {
  836. log.Error("GitRepo.IsEmpty: %v", err)
  837. ctx.Repo.Repository.Status = repo_model.RepositoryBroken
  838. showEmpty = true
  839. ctx.Flash.Error(ctx.Tr("error.occurred"), true)
  840. }
  841. }
  842. if showEmpty {
  843. ctx.HTML(http.StatusOK, tplRepoEMPTY)
  844. return
  845. }
  846. // the repo is not really empty, so we should update the modal in database
  847. // such problem may be caused by:
  848. // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually
  849. // and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos.
  850. // it's possible for a repository to be non-empty by that flag but still 500
  851. // because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed.
  852. ctx.Repo.Repository.IsEmpty = false
  853. if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil {
  854. ctx.ServerError("UpdateRepositoryCols", err)
  855. return
  856. }
  857. if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil {
  858. ctx.ServerError("UpdateRepoSize", err)
  859. return
  860. }
  861. // the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values
  862. link := ctx.Link
  863. if ctx.Req.URL.RawQuery != "" {
  864. link += "?" + ctx.Req.URL.RawQuery
  865. }
  866. ctx.Redirect(link)
  867. return
  868. }
  869. title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
  870. if len(ctx.Repo.Repository.Description) > 0 {
  871. title += ": " + ctx.Repo.Repository.Description
  872. }
  873. ctx.Data["Title"] = title
  874. // Get Topics of this repo
  875. renderRepoTopics(ctx)
  876. if ctx.Written() {
  877. return
  878. }
  879. // Get current entry user currently looking at.
  880. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
  881. if err != nil {
  882. HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
  883. return
  884. }
  885. checkOutdatedBranch(ctx)
  886. checkCitationFile(ctx, entry)
  887. if ctx.Written() {
  888. return
  889. }
  890. renderLanguageStats(ctx)
  891. if ctx.Written() {
  892. return
  893. }
  894. if entry.IsDir() {
  895. renderDirectory(ctx)
  896. } else {
  897. renderFile(ctx, entry)
  898. }
  899. if ctx.Written() {
  900. return
  901. }
  902. if ctx.Doer != nil {
  903. if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil {
  904. ctx.ServerError("GetBaseRepo", err)
  905. return
  906. }
  907. showRecentlyPushedNewBranches := true
  908. if ctx.Repo.Repository.IsMirror ||
  909. !ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) {
  910. showRecentlyPushedNewBranches = false
  911. }
  912. if showRecentlyPushedNewBranches {
  913. ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch)
  914. if err != nil {
  915. ctx.ServerError("GetRecentlyPushedBranches", err)
  916. return
  917. }
  918. }
  919. }
  920. var treeNames []string
  921. paths := make([]string, 0, 5)
  922. if len(ctx.Repo.TreePath) > 0 {
  923. treeNames = strings.Split(ctx.Repo.TreePath, "/")
  924. for i := range treeNames {
  925. paths = append(paths, strings.Join(treeNames[:i+1], "/"))
  926. }
  927. ctx.Data["HasParentPath"] = true
  928. if len(paths)-2 >= 0 {
  929. ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
  930. }
  931. }
  932. ctx.Data["Paths"] = paths
  933. branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  934. treeLink := branchLink
  935. if len(ctx.Repo.TreePath) > 0 {
  936. treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  937. }
  938. ctx.Data["TreeLink"] = treeLink
  939. ctx.Data["TreeNames"] = treeNames
  940. ctx.Data["BranchLink"] = branchLink
  941. ctx.HTML(http.StatusOK, tplRepoHome)
  942. }
  943. func checkOutdatedBranch(ctx *context.Context) {
  944. if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) {
  945. return
  946. }
  947. // get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName`
  948. commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
  949. if err != nil {
  950. log.Error("GetBranchCommitID: %v", err)
  951. // Don't return an error page, as it can be rechecked the next time the user opens the page.
  952. return
  953. }
  954. dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName)
  955. if err != nil {
  956. log.Error("GetBranch: %v", err)
  957. // Don't return an error page, as it can be rechecked the next time the user opens the page.
  958. return
  959. }
  960. if dbBranch.CommitID != commit.ID.String() {
  961. ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true)
  962. }
  963. }
  964. // RenderUserCards render a page show users according the input template
  965. func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) {
  966. page := ctx.FormInt("page")
  967. if page <= 0 {
  968. page = 1
  969. }
  970. pager := context.NewPagination(total, setting.ItemsPerPage, page, 5)
  971. ctx.Data["Page"] = pager
  972. items, err := getter(db.ListOptions{
  973. Page: pager.Paginater.Current(),
  974. PageSize: setting.ItemsPerPage,
  975. })
  976. if err != nil {
  977. ctx.ServerError("getter", err)
  978. return
  979. }
  980. ctx.Data["Cards"] = items
  981. ctx.HTML(http.StatusOK, tpl)
  982. }
  983. // Watchers render repository's watch users
  984. func Watchers(ctx *context.Context) {
  985. ctx.Data["Title"] = ctx.Tr("repo.watchers")
  986. ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
  987. ctx.Data["PageIsWatchers"] = true
  988. RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, func(opts db.ListOptions) ([]*user_model.User, error) {
  989. return repo_model.GetRepoWatchers(ctx, ctx.Repo.Repository.ID, opts)
  990. }, tplWatchers)
  991. }
  992. // Stars render repository's starred users
  993. func Stars(ctx *context.Context) {
  994. ctx.Data["Title"] = ctx.Tr("repo.stargazers")
  995. ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
  996. ctx.Data["PageIsStargazers"] = true
  997. RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*user_model.User, error) {
  998. return repo_model.GetStargazers(ctx, ctx.Repo.Repository, opts)
  999. }, tplWatchers)
  1000. }
  1001. // Forks render repository's forked users
  1002. func Forks(ctx *context.Context) {
  1003. ctx.Data["Title"] = ctx.Tr("repo.forks")
  1004. page := ctx.FormInt("page")
  1005. if page <= 0 {
  1006. page = 1
  1007. }
  1008. pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.ItemsPerPage, page, 5)
  1009. ctx.Data["Page"] = pager
  1010. forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{
  1011. Page: pager.Paginater.Current(),
  1012. PageSize: setting.ItemsPerPage,
  1013. })
  1014. if err != nil {
  1015. ctx.ServerError("GetForks", err)
  1016. return
  1017. }
  1018. for _, fork := range forks {
  1019. if err = fork.LoadOwner(ctx); err != nil {
  1020. ctx.ServerError("LoadOwner", err)
  1021. return
  1022. }
  1023. }
  1024. ctx.Data["Forks"] = forks
  1025. ctx.HTML(http.StatusOK, tplForks)
  1026. }