mirror of
https://github.com/go-gitea/gitea
synced 2024-12-27 02:54:27 +00:00
219 lines
7.1 KiB
Go
219 lines
7.1 KiB
Go
|
// 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"
|
||
|
|
||
|
"code.gitea.io/gitea/models/renderhelper"
|
||
|
"code.gitea.io/gitea/modules/base"
|
||
|
"code.gitea.io/gitea/modules/charset"
|
||
|
"code.gitea.io/gitea/modules/git"
|
||
|
"code.gitea.io/gitea/modules/log"
|
||
|
"code.gitea.io/gitea/modules/markup"
|
||
|
"code.gitea.io/gitea/modules/setting"
|
||
|
"code.gitea.io/gitea/modules/util"
|
||
|
"code.gitea.io/gitea/services/context"
|
||
|
)
|
||
|
|
||
|
// locate a README for a tree in one of the supported paths.
|
||
|
//
|
||
|
// entries is passed to reduce calls to ListEntries(), so
|
||
|
// this has precondition:
|
||
|
//
|
||
|
// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
|
||
|
//
|
||
|
// FIXME: There has to be a more efficient way of doing this
|
||
|
func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
|
||
|
// Create a list of extensions in priority order
|
||
|
// 1. Markdown files - with and without localisation - 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)
|
||
|
|
||
|
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
|
||
|
for _, entry := range entries {
|
||
|
if tryWellKnownDirs && entry.IsDir() {
|
||
|
// as a special case for the top-level repo introduction README,
|
||
|
// fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ...
|
||
|
// (note that docsEntries is ignored unless we are at the root)
|
||
|
lowerName := strings.ToLower(entry.Name())
|
||
|
switch lowerName {
|
||
|
case "docs":
|
||
|
if entry.Name() == "docs" || docsEntries[0] == nil {
|
||
|
docsEntries[0] = entry
|
||
|
}
|
||
|
case ".gitea":
|
||
|
if entry.Name() == ".gitea" || docsEntries[1] == nil {
|
||
|
docsEntries[1] = entry
|
||
|
}
|
||
|
case ".github":
|
||
|
if entry.Name() == ".github" || docsEntries[2] == nil {
|
||
|
docsEntries[2] = entry
|
||
|
}
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
|
||
|
log.Debug("Potential readme file: %s", entry.Name())
|
||
|
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
|
||
|
if entry.IsLink() {
|
||
|
target, err := entry.FollowLinks()
|
||
|
if err != nil && !git.IsErrBadLink(err) {
|
||
|
return "", nil, err
|
||
|
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
|
||
|
readmeFiles[i] = entry
|
||
|
}
|
||
|
} else {
|
||
|
readmeFiles[i] = entry
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
var readmeFile *git.TreeEntry
|
||
|
for _, f := range readmeFiles {
|
||
|
if f != nil {
|
||
|
readmeFile = f
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ctx.Repo.TreePath == "" && readmeFile == nil {
|
||
|
for _, subTreeEntry := range docsEntries {
|
||
|
if subTreeEntry == nil {
|
||
|
continue
|
||
|
}
|
||
|
subTree := subTreeEntry.Tree()
|
||
|
if subTree == nil {
|
||
|
// this should be impossible; if subTreeEntry exists so should this.
|
||
|
continue
|
||
|
}
|
||
|
childEntries, err := subTree.ListEntries()
|
||
|
if err != nil {
|
||
|
return "", nil, err
|
||
|
}
|
||
|
|
||
|
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false)
|
||
|
if err != nil && !git.IsErrNotExist(err) {
|
||
|
return "", nil, err
|
||
|
}
|
||
|
if readmeFile != nil {
|
||
|
return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return "", readmeFile, 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) {
|
||
|
target := readmeFile
|
||
|
if readmeFile != nil && readmeFile.IsLink() {
|
||
|
target, _ = readmeFile.FollowLinks()
|
||
|
}
|
||
|
if target == nil {
|
||
|
// if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
|
||
|
// simply skip rendering the README
|
||
|
return
|
||
|
}
|
||
|
|
||
|
ctx.Data["RawFileLink"] = ""
|
||
|
ctx.Data["ReadmeInList"] = true
|
||
|
ctx.Data["ReadmeExist"] = true
|
||
|
ctx.Data["FileIsSymlink"] = readmeFile.IsLink()
|
||
|
|
||
|
buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob())
|
||
|
if err != nil {
|
||
|
ctx.ServerError("getFileReader", err)
|
||
|
return
|
||
|
}
|
||
|
defer dataRc.Close()
|
||
|
|
||
|
ctx.Data["FileIsText"] = fInfo.isTextFile
|
||
|
ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name())
|
||
|
ctx.Data["FileSize"] = fInfo.fileSize
|
||
|
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.isTextFile {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if fInfo.fileSize >= 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
|
||
|
ctx.Data["IsTextFile"] = true
|
||
|
return
|
||
|
}
|
||
|
|
||
|
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||
|
|
||
|
if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" {
|
||
|
ctx.Data["IsMarkup"] = true
|
||
|
ctx.Data["MarkupType"] = markupType
|
||
|
|
||
|
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||
|
CurrentRefPath: ctx.Repo.BranchNameSubURL(),
|
||
|
CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder),
|
||
|
}).
|
||
|
WithMarkupType(markupType).
|
||
|
WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
|
||
|
|
||
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, 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, "IsMarkup")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ctx.Data["IsMarkup"] != true {
|
||
|
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.CanEnableEditor(ctx, ctx.Doer) {
|
||
|
ctx.Data["CanEditReadmeFile"] = true
|
||
|
}
|
||
|
}
|