mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
221 lines
7.5 KiB
Go
221 lines
7.5 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/models/renderhelper"
|
|
"github.com/hanzoai/git/modules/base"
|
|
"github.com/hanzoai/git/modules/charset"
|
|
"github.com/hanzoai/git/modules/git"
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/util"
|
|
"github.com/hanzoai/git/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, parentDir string, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
|
|
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .hanzo/ 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, .hanzo/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 ".hanzo":
|
|
if entry.Name() == ".hanzo" || docsEntries[1] == nil {
|
|
docsEntries[1] = entry
|
|
}
|
|
case ".github":
|
|
if entry.Name() == ".github" || docsEntries[2] == nil {
|
|
docsEntries[2] = entry
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
for _, entry := range entries {
|
|
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
|
|
fullPath := path.Join(parentDir, entry.Name())
|
|
if readmeFiles[i] == nil || base.NaturalSortCompare(readmeFiles[i].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[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(ctx.Repo.GitRepo)
|
|
if subTree == nil {
|
|
// this should be impossible; if subTreeEntry exists so should this.
|
|
continue
|
|
}
|
|
childEntries, err := subTree.ListEntries(ctx, ctx.Repo.GitRepo)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, path.Join(parentDir, subTreeEntry.Name()), 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) {
|
|
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.CanEnableEditor() {
|
|
ctx.Data["CanEditReadmeFile"] = true
|
|
}
|
|
}
|