mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-03 05:39:39 +00:00
feat(repo): prioritize well-known READMEs and optimize discovery (#38532)
This PR adjusts the repository README discovery logic to mirror GitHub's priority order, while retaining support for Gitea's specific `.gitea/` directory. Previously, Gitea would prioritize a `README.md` at the root of the repository over any READMEs in `.gitea/` or `.github/`. With this change, well-known subdirectories are evaluated first when viewing the repository root. **New Priority Order:** 1. `.gitea/README.md` 2. `.github/README.md` 3. `/README.md` (root directory) 4. `docs/README.md` This allows repository maintainers to use `.github/README.md` (or `.gitea/README.md`) as the repository homepage without having to remove or rename generic root `/README.md` files required by package managers (like NPM, Crates.io, Docker, etc.). --------- Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
+11
-11
@@ -268,11 +268,11 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
|
||||
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
|
||||
}
|
||||
|
||||
func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
|
||||
func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) (*git.TreeEntry, git.Entries) {
|
||||
tree, err := ctx.Repo.Commit.SubTree(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
HandleGitError(ctx, "Repo.Commit.SubTree", err)
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TODO: LAST-COMMIT-ASYNC-LOADING: search this keyword to see more details
|
||||
@@ -283,20 +283,20 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !entry.IsDir() {
|
||||
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
allEntries, err := tree.ListEntries(ctx, ctx.Repo.GitRepo)
|
||||
subEntries, err := tree.ListEntries(ctx, ctx.Repo.GitRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListEntries", err)
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
allEntries.CustomSort(base.NaturalSortCompare)
|
||||
subEntries.CustomSort(base.NaturalSortCompare)
|
||||
|
||||
commitInfoCtx := gocontext.Context(ctx)
|
||||
if timeout > 0 {
|
||||
@@ -305,10 +305,10 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.GitRepo, ctx.Repo.Commit, ctx.Repo.TreePath)
|
||||
files, latestCommit, err := subEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.GitRepo, ctx.Repo.Commit, ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommitsInfo", err)
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
{ // this block is for testing purpose only
|
||||
@@ -340,9 +340,9 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
||||
}
|
||||
|
||||
if !loadLatestCommitData(ctx, latestCommit) {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
return allEntries
|
||||
return entry, subEntries
|
||||
}
|
||||
|
||||
// RenderUserCards render a page show users according the input template
|
||||
|
||||
@@ -141,7 +141,7 @@ func prepareHomeSidebarLicenses(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func prepareToRenderDirectory(ctx *context.Context) {
|
||||
entries := renderDirectoryFiles(ctx, 1*time.Second)
|
||||
treeEntry, subEntries := renderDirectoryFiles(ctx, 1*time.Second)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -151,12 +151,11 @@ func prepareToRenderDirectory(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefFullName.ShortName())
|
||||
}
|
||||
|
||||
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, ctx.Repo.TreePath, entries, true)
|
||||
subfolder, readmeFile, err := findReadmeFileInRepoTree(ctx, ctx.Repo.TreePath, treeEntry, subEntries)
|
||||
if err != nil {
|
||||
ctx.ServerError("findReadmeFileInEntries", err)
|
||||
ctx.ServerError("findReadmeFileInRepo", err)
|
||||
return
|
||||
}
|
||||
|
||||
prepareToRenderReadmeFile(ctx, subfolder, readmeFile)
|
||||
}
|
||||
|
||||
@@ -356,7 +355,8 @@ func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) b
|
||||
return false
|
||||
}
|
||||
if treePathEntry.IsLink() {
|
||||
if res, err := git.EntryFollowLinks(ctx, ctx.Repo.GitRepo, ctx.Repo.Commit, ctx.Repo.TreePath, treePathEntry); err == nil {
|
||||
res, err := git.EntryFollowLinks(ctx, ctx.Repo.GitRepo, ctx.Repo.Commit, ctx.Repo.TreePath, treePathEntry)
|
||||
if err == nil {
|
||||
redirect := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(res.TargetFullPath) + "?" + ctx.Req.URL.RawQuery
|
||||
ctx.Redirect(redirect)
|
||||
return true
|
||||
|
||||
@@ -24,95 +24,101 @@ import (
|
||||
)
|
||||
|
||||
// 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 are 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/, .gitea/ or .github/)
|
||||
// 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 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
|
||||
}
|
||||
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 localisation - e.g. README.en-us.md or README.md
|
||||
// 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)
|
||||
readmeFiles := make([]*git.TreeEntry, extCount+1) // ext weight can be len(exts), so here "+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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var readmeFile *git.TreeEntry
|
||||
for _, f := range readmeFiles {
|
||||
if f != nil {
|
||||
readmeFile = f
|
||||
break
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Repo.TreePath == "" && readmeFile == nil {
|
||||
for _, subTreeEntry := range docsEntries {
|
||||
if subTreeEntry == nil {
|
||||
continue
|
||||
}
|
||||
subTree := subTreeEntry.Tree(ctx, 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
|
||||
return nil
|
||||
}
|
||||
|
||||
// localizedExtensions prepends the provided language code with and without a
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFindReadmeFileInEntriesWithSymlinkInSubfolder(t *testing.T) {
|
||||
for _, subdir := range []string{".github", ".gitea", "docs"} {
|
||||
t.Run(subdir, func(t *testing.T) {
|
||||
repoPath := t.TempDir()
|
||||
stdin := fmt.Sprintf(`commit refs/heads/master
|
||||
author Test <test@example.com> 1700000000 +0000
|
||||
committer Test <test@example.com> 1700000000 +0000
|
||||
data <<EOT
|
||||
initial
|
||||
EOT
|
||||
M 100644 inline target.md
|
||||
data <<EOT
|
||||
target-content
|
||||
EOT
|
||||
M 120000 inline %s/README.md
|
||||
data 12
|
||||
../target.md
|
||||
`, subdir)
|
||||
|
||||
var err error
|
||||
err = gitcmd.NewCommand("init", "--bare", ".").WithDir(repoPath).RunWithStderr(t.Context())
|
||||
require.NoError(t, err)
|
||||
err = gitcmd.NewCommand("fast-import").WithDir(repoPath).WithStdinBytes([]byte(stdin)).RunWithStderr(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
gitRepo, err := git.OpenRepositoryLocal(repoPath)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetBranchCommit(t.Context(), "master")
|
||||
require.NoError(t, err)
|
||||
|
||||
entries, err := commit.Tree().ListEntries(t.Context(), gitRepo)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, _ := contexttest.MockContext(t, "/")
|
||||
ctx.Repo.Commit = commit
|
||||
ctx.Repo.GitRepo = gitRepo
|
||||
foundDir, foundReadme, err := findReadmeFileInEntries(ctx, "", entries, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, foundReadme)
|
||||
|
||||
assert.Equal(t, subdir, foundDir)
|
||||
assert.Equal(t, "README.md", foundReadme.Name())
|
||||
assert.True(t, foundReadme.IsLink())
|
||||
|
||||
// Verify that it can follow the link
|
||||
res, err := git.EntryFollowLinks(t.Context(), gitRepo, commit, path.Join(foundDir, foundReadme.Name()), foundReadme)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "target.md", res.TargetFullPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user