snapshot of https://github.com/go-gitea/gitea.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped
This commit is contained in:
226
routers/web/repo/view_readme.go
Normal file
226
routers/web/repo/view_readme.go
Normal file
@@ -0,0 +1,226 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/renderhelper"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/modules/charset"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// locate a README for a tree in one of the supported paths.
|
||||
// entries are passed to reduce calls to ListEntries(), so this has precondition:
|
||||
//
|
||||
// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
|
||||
//
|
||||
// this function is tested by integration test ViewRepoDirectoryReadme
|
||||
func findReadmeFileInRepoTree(ctx *context.Context, treePath string, tree *git.TreeEntry, rootSubEntries []*git.TreeEntry) (subFolder string, _ *git.TreeEntry, err error) {
|
||||
gitRepo := ctx.Repo.GitRepo
|
||||
var dirEntries []*git.TreeEntry
|
||||
if treePath == "" {
|
||||
// only try the special sub-folders when visiting the repo root
|
||||
wellKnownSubDirs := findReadmeWellKnownSubDirs(rootSubEntries)
|
||||
dirEntries = []*git.TreeEntry{wellKnownSubDirs.entryGitea, wellKnownSubDirs.entryGitHub, tree, wellKnownSubDirs.entryDocs}
|
||||
} else {
|
||||
dirEntries = []*git.TreeEntry{tree}
|
||||
}
|
||||
|
||||
for _, dirEntry := range dirEntries {
|
||||
if dirEntry == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var dirSubEntries []*git.TreeEntry
|
||||
if dirEntry == tree {
|
||||
subFolder, dirSubEntries = "", rootSubEntries
|
||||
} else {
|
||||
subFolder = dirEntry.Name()
|
||||
dirSubEntries, err = dirEntry.Tree(ctx, gitRepo).ListEntries(ctx, gitRepo)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
found := findReadmeFileInEntries(ctx, path.Join(treePath, subFolder), dirSubEntries)
|
||||
if found != nil {
|
||||
return subFolder, found, nil
|
||||
}
|
||||
}
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
func findReadmeWellKnownSubDirs(entries []*git.TreeEntry) (ret struct{ entryGitea, entryGitHub, entryDocs *git.TreeEntry }) {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
lowerName := strings.ToLower(entry.Name())
|
||||
switch lowerName {
|
||||
case ".gitea":
|
||||
if entry.Name() == ".gitea" || ret.entryGitea == nil {
|
||||
ret.entryGitea = entry
|
||||
}
|
||||
case ".github":
|
||||
if entry.Name() == ".github" || ret.entryGitHub == nil {
|
||||
ret.entryGitHub = entry
|
||||
}
|
||||
case "docs":
|
||||
if entry.Name() == "docs" || ret.entryDocs == nil {
|
||||
ret.entryDocs = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func findReadmeFileInEntries(ctx *context.Context, parentPath string, entries []*git.TreeEntry) *git.TreeEntry {
|
||||
// Create a list of extensions in priority order
|
||||
// 1. Markdown files - with and without localization - e.g. README.en-us.md or README.md
|
||||
// 2. Txt files - e.g. README.txt
|
||||
// 3. No extension - e.g. README
|
||||
exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
|
||||
extCount := len(exts)
|
||||
readmeFiles := make([]*git.TreeEntry, extCount+1) // ext weight can be len(exts), so here "+1"
|
||||
|
||||
for _, entry := range entries {
|
||||
extWeight, ok := util.IsReadmeFileExtension(entry.Name(), exts...)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fullPath := path.Join(parentPath, entry.Name())
|
||||
if readmeFiles[extWeight] == nil || base.NaturalSortCompare(readmeFiles[extWeight].Name(), entry.Blob(ctx.Repo.GitRepo).Name()) < 0 {
|
||||
if entry.IsLink() {
|
||||
res, err := git.EntryFollowLinks(ctx, ctx.Repo.GitRepo, ctx.Repo.Commit, fullPath, entry)
|
||||
if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) {
|
||||
readmeFiles[extWeight] = entry
|
||||
}
|
||||
} else {
|
||||
readmeFiles[extWeight] = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range readmeFiles {
|
||||
if f != nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// localizedExtensions prepends the provided language code with and without a
|
||||
// regional identifier to the provided extension.
|
||||
// Note: the language code will always be lower-cased, if a region is present it must be separated with a `-`
|
||||
// Note: ext should be prefixed with a `.`
|
||||
func localizedExtensions(ext, languageCode string) (localizedExts []string) {
|
||||
if len(languageCode) < 1 {
|
||||
return []string{ext}
|
||||
}
|
||||
|
||||
lowerLangCode := "." + strings.ToLower(languageCode)
|
||||
|
||||
if strings.Contains(lowerLangCode, "-") {
|
||||
underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_")
|
||||
indexOfDash := strings.Index(lowerLangCode, "-")
|
||||
// e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md]
|
||||
return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext}
|
||||
}
|
||||
|
||||
// e.g. [.en.md, .md]
|
||||
return []string{lowerLangCode + ext, ext}
|
||||
}
|
||||
|
||||
func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
|
||||
if readmeFile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
readmeFullPath := path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())
|
||||
readmeTargetEntry := readmeFile
|
||||
if readmeFile.IsLink() {
|
||||
if res, err := git.EntryFollowLinks(ctx, ctx.Repo.GitRepo, ctx.Repo.Commit, readmeFullPath, readmeFile); err == nil {
|
||||
readmeTargetEntry = res.TargetEntry
|
||||
} else {
|
||||
readmeTargetEntry = nil // if we cannot resolve the symlink, we cannot render the readme, ignore the error
|
||||
}
|
||||
}
|
||||
if readmeTargetEntry == nil {
|
||||
return // if no valid README entry found, skip rendering the README
|
||||
}
|
||||
|
||||
ctx.Data["RawFileLink"] = ""
|
||||
ctx.Data["ReadmeInList"] = path.Join(subfolder, readmeFile.Name()) // the relative path to the readme file to the current tree path
|
||||
ctx.Data["ReadmeExist"] = true
|
||||
ctx.Data["FileIsSymlink"] = readmeFile.IsLink()
|
||||
|
||||
buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, readmeTargetEntry.Blob(ctx.Repo.GitRepo))
|
||||
if err != nil {
|
||||
ctx.ServerError("getFileReader", err)
|
||||
return
|
||||
}
|
||||
defer dataRc.Close()
|
||||
|
||||
ctx.Data["FileIsText"] = fInfo.st.IsText()
|
||||
ctx.Data["FileTreePath"] = readmeFullPath
|
||||
ctx.Data["FileSize"] = fInfo.blobOrLfsSize
|
||||
ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
|
||||
|
||||
if fInfo.isLFSFile() {
|
||||
filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name()))
|
||||
ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
|
||||
}
|
||||
|
||||
if !fInfo.st.IsText() {
|
||||
return
|
||||
}
|
||||
|
||||
if fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize {
|
||||
// Pretend that this is a normal text file to display 'This file is too large to be shown'
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
return
|
||||
}
|
||||
|
||||
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||||
CurrentRefSubURL: ctx.Repo.RefTypeNameSubURL(),
|
||||
CurrentTreePath: path.Dir(readmeFullPath),
|
||||
}).WithRelativePath(readmeFullPath)
|
||||
renderer := rctx.DetectMarkupRenderer(buf)
|
||||
if renderer != nil {
|
||||
ctx.Data["RenderAsMarkup"] = "markup-inplace"
|
||||
ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
|
||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, rd)
|
||||
if err != nil {
|
||||
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
|
||||
delete(ctx.Data, "RenderAsMarkup")
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Data["RenderAsMarkup"] == nil {
|
||||
ctx.Data["IsPlainText"] = true
|
||||
content, err := io.ReadAll(rd)
|
||||
if err != nil {
|
||||
log.Error("Read readme content failed: %v", err)
|
||||
}
|
||||
contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content))
|
||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
|
||||
}
|
||||
|
||||
if !fInfo.isLFSFile() && ctx.Repo.Repository.CanContentChange() {
|
||||
ctx.Data["CanEditReadmeFile"] = true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user