mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-21 13:55:27 +02: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:
42
modules/git/fastimport.go
Normal file
42
modules/git/fastimport.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
type FastImportFile struct {
|
||||
Mode EntryMode
|
||||
Path string
|
||||
Content string
|
||||
}
|
||||
|
||||
type FastImportCommit struct {
|
||||
Ref string
|
||||
Message string
|
||||
Files []FastImportFile
|
||||
}
|
||||
|
||||
// ForceFastImport is for mainly for testing purpose
|
||||
func ForceFastImport(ctx context.Context, repo RepositoryFacade, commits []FastImportCommit) error {
|
||||
var buf bytes.Buffer
|
||||
for i, c := range commits {
|
||||
_, _ = fmt.Fprintf(&buf, "reset %s\n", c.Ref)
|
||||
_, _ = fmt.Fprintf(&buf, "commit %s\nmark :%d\ncommitter Gitea <gitea@example.com> 1500000000 +0000\n", c.Ref, i+1)
|
||||
_, _ = fmt.Fprintf(&buf, "data %d\n%s\n", len(c.Message), c.Message)
|
||||
for _, f := range c.Files {
|
||||
_, _ = fmt.Fprintf(&buf, "M %s inline %s\ndata %d\n%s\n", f.Mode.String(), f.Path, len(f.Content), f.Content)
|
||||
}
|
||||
}
|
||||
buf.WriteString("done\n")
|
||||
_, _, err := gitcmd.NewCommand("fast-import").AddArguments("--force", "--done").
|
||||
WithRepo(repo).WithStdinBytes(buf.Bytes()).
|
||||
RunStdString(ctx)
|
||||
return err
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -156,6 +157,7 @@ var AllowSkipExternalService = sync.OnceValue(func() bool {
|
||||
})
|
||||
|
||||
type TestingT interface {
|
||||
Context() context.Context
|
||||
Helper()
|
||||
Skipf(format string, args ...any)
|
||||
Errorf(format string, args ...any)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
ea9ef877d1d88af76682d8798418081264f10cfc refs/heads/fallbacks
|
||||
0d4c14db927c9ffba01fa7e126cc748b5c02c01e refs/heads/fallbacks2
|
||||
c66d5b07c2063d3268707f22226c708b589574ef refs/heads/fallbacks3
|
||||
89f8426e9eb5eff35c09b3565836c8f8e15d0ce9 refs/heads/fallbacks4
|
||||
b0e902496eae435ad03c92a5d479f916ef2d4893 refs/heads/fallbacks5
|
||||
84a5500b5cc040b11daf53fc42c542a99589dc76 refs/heads/fallbacks6
|
||||
cf406a96e416d7de5c4c1bbfffdd672300c822bf refs/heads/fallbacks7
|
||||
0d6ac644b969e9199915a492da9dba08c179fd23 refs/heads/fallbacks8
|
||||
5038febc0c57215beb3748d7ae4091a25a4acc93 refs/heads/fallbacks9
|
||||
9134e1f178ca4cccf1a197142646f2d7627e8cd5 refs/heads/i18n
|
||||
744d2441e55bc0010d6b340d303f0106a627ad29 refs/heads/master
|
||||
3c492566170b057e962c025515ab38bbd7444077 refs/heads/plain
|
||||
3882d6373a0882a6739b3cd9b24d21c630621234 refs/heads/sp-ace
|
||||
bf5ed898252eaa50dcc01108ed4417c3ea98a294 refs/heads/special-subdir-.gitea
|
||||
c03543573ab088ce1cf7090a387d2be621426234 refs/heads/special-subdir-.github
|
||||
e75957ad9b7e6ed16dda183529ec283db0bbc5fe refs/heads/special-subdir-docs
|
||||
46f5d5ab33d701642e08c713fab42af89fdd4fea refs/heads/special-subdir-nested
|
||||
9c0f872256b839c2b97ec22fd348d87b14045513 refs/heads/subdir
|
||||
d7a854fff61e45b98234d7aa79ecbcb1619cd3dd refs/heads/symlink
|
||||
30b9c0ed4b1039dbd99f3fb537b84ca507e0549d refs/heads/symlink-loop
|
||||
41489b7be5c2244d2b7b524dcb31caf3bd1f9ccc refs/heads/txt
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
x<01><>;<0E>0@<40>s
|
||||
_<EFBFBD>*N<><4E>H<08><>1q<01>u(<28>?<3F>t<EFBFBD><74>T<EFBFBD>0<><30>==<3D><><EFBFBD>Q<EFBFBD><05>+<2B>*4<><34>d<EFBFBD><64>h<EFBFBD>S<EFBFBD>η.z<><7A><EFBFBD><0E>͙Z3<5A><33>ct<><74>0'<27>As<41>5h<08>zL<7A>=D<><44>B<EFBFBD>\cx-ݴ<><DDB4>!O<><4F><EFBFBD><EFBFBD><EFBFBD><02>q<EFBFBD><71><EFBFBD><EFBFBD><EFBFBD><<3C><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 0<>T<EFBFBD>装<EFBFBD><E8A385><EFBFBD>5<EFBFBD>=-<2D><><EFBFBD><7F><EFBFBD>U s<>7,O<>#<23>M<EFBFBD>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
x<01><>M<0E> <10>aל<61><0B>0&Ƹs<C6B8><73>ئ<>'<27>.<2E><><EFBFBD>x<02><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>4]<5D><1A><>.)<29>D<EFBFBD><44>Q<EFBFBD>|@b6Xbd<62><64>}<7D>2+b<>%<25>T<> <1F>I>g<> 2<>7Q<37><51><EFBFBD>. (c<><63><EFBFBD><EFBFBD><EFBFBD>"o<1D><><EFBFBD>n<EFBFBD>M<EFBFBD><<3C>[6<>_^橼<>Z<1A><>T<07><>U<EFBFBD><19> <20><><EFBFBD>n<EFBFBD><6E>
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
x<01><>;<0E>0@<40>s
|
||||
_<EFBFBD>*N뤕bcc<63><02><>Њ<EFBFBD>Tҁ<54>S!N<><4E><EFBFBD><EFBFBD><EFBFBD>d<1E>><3E><><EFBFBD>!<21><><10>քLu<>Ul<55>#q<><71><EFBFBD><EFBFBD>l<EFBFBD>Q<EFBFBD>,<2C>ꔡ<EFBFBD><EA94A1>lCBn$6<><36>X<EFBFBD>Dɹ<44>bbҖR0<><30><EFBFBD><EFBFBD><EFBFBD>y<EFBFBD>[/O<>n<EFBFBD><6E><EFBFBD>
|
||||
<EFBFBD>i<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<0F>1<0F><>Ї@<40>e<EFBFBD>p<EFBFBD>d<EFBFBD><64><EFBFBD>iޭ<69>殯<0C><>!<21><<3C><07><>N<EFBFBD>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
x<01><>M
|
||||
1<0C>a<EFBFBD>=E.<2E><>m<EFBFBD><6D> <20>Νwh<77><68><EFBFBD><EFBFBD>#<23>"<22><>A<<3C><>g<EFBFBD>~|eǾ<>#<23>jU:<3A><><EFBFBD><05>$%<25>9o<39><6F>{<7B>9F <09>գQdsOU<4F><06>H<1C><>rA<72>(<28>=<3D>x<EFBFBD><78>E<EFBFBD><45><EFBFBD>$nkҳ]<5D>
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>\ҫV<D2AB>M7<><0F>yx<79>mؔ<6D><07>1<06>-<2D>1 <20><>}ږ<><DA96>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
P pack-8933bd634b76f8154310cccb52537a0195e43166.pack
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,22 +0,0 @@
|
||||
# pack-refs with: peeled fully-peeled sorted
|
||||
ea9ef877d1d88af76682d8798418081264f10cfc refs/heads/fallbacks
|
||||
0d4c14db927c9ffba01fa7e126cc748b5c02c01e refs/heads/fallbacks2
|
||||
c66d5b07c2063d3268707f22226c708b589574ef refs/heads/fallbacks3
|
||||
89f8426e9eb5eff35c09b3565836c8f8e15d0ce9 refs/heads/fallbacks4
|
||||
b0e902496eae435ad03c92a5d479f916ef2d4893 refs/heads/fallbacks5
|
||||
84a5500b5cc040b11daf53fc42c542a99589dc76 refs/heads/fallbacks6
|
||||
cf406a96e416d7de5c4c1bbfffdd672300c822bf refs/heads/fallbacks7
|
||||
0d6ac644b969e9199915a492da9dba08c179fd23 refs/heads/fallbacks8
|
||||
5038febc0c57215beb3748d7ae4091a25a4acc93 refs/heads/fallbacks9
|
||||
9134e1f178ca4cccf1a197142646f2d7627e8cd5 refs/heads/i18n
|
||||
744d2441e55bc0010d6b340d303f0106a627ad29 refs/heads/master
|
||||
3c492566170b057e962c025515ab38bbd7444077 refs/heads/plain
|
||||
3882d6373a0882a6739b3cd9b24d21c630621234 refs/heads/sp-ace
|
||||
bf5ed898252eaa50dcc01108ed4417c3ea98a294 refs/heads/special-subdir-.gitea
|
||||
c03543573ab088ce1cf7090a387d2be621426234 refs/heads/special-subdir-.github
|
||||
e75957ad9b7e6ed16dda183529ec283db0bbc5fe refs/heads/special-subdir-docs
|
||||
46f5d5ab33d701642e08c713fab42af89fdd4fea refs/heads/special-subdir-nested
|
||||
9c0f872256b839c2b97ec22fd348d87b14045513 refs/heads/subdir
|
||||
d7a854fff61e45b98234d7aa79ecbcb1619cd3dd refs/heads/symlink
|
||||
30b9c0ed4b1039dbd99f3fb537b84ca507e0549d refs/heads/symlink-loop
|
||||
41489b7be5c2244d2b7b524dcb31caf3bd1f9ccc refs/heads/txt
|
||||
@@ -1 +0,0 @@
|
||||
fe495ea336f079ef2bed68648d0ba9a37cdbd4aa
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//nolint:govet // disable "composites: gitea.dev/modules/git.FastImportFile struct literal uses unkeyed fields"
|
||||
package integration
|
||||
|
||||
import (
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/util"
|
||||
@@ -308,10 +310,92 @@ func testViewRepoDirectory(t *testing.T) {
|
||||
func testViewRepoDirectoryReadme(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
|
||||
repo56 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user2.ID, Name: "readme-test"})
|
||||
|
||||
const regular, symlink = git.EntryModeBlob, git.EntryModeSymlink
|
||||
|
||||
allGitea := []git.FastImportFile{
|
||||
{regular, ".gitea/README.en.md", "This is .gitea/README.en.md"},
|
||||
{regular, ".gitea/README.md", "This is .gitea/README.md"},
|
||||
{regular, ".gitea/README", "This is .gitea/README"},
|
||||
}
|
||||
allGithub := []git.FastImportFile{
|
||||
{regular, ".github/README.en.md", "This is .github/README.en.md"},
|
||||
{regular, ".github/README.md", "This is .github/README.md"},
|
||||
{regular, ".github/README", "This is .github/README"},
|
||||
}
|
||||
allRoot := []git.FastImportFile{
|
||||
{regular, "README.en.md", "This is README.en.md"},
|
||||
{regular, "README.md", "This is README.md"},
|
||||
{regular, "README", "This is README"},
|
||||
}
|
||||
allDocs := []git.FastImportFile{
|
||||
{regular, "docs/README.en.md", "This is docs/README.en.md"},
|
||||
{regular, "docs/README.md", "This is docs/README.md"},
|
||||
{regular, "docs/README", "This is docs/README"},
|
||||
}
|
||||
|
||||
combineFiles := func(slices ...[]git.FastImportFile) (res []git.FastImportFile) {
|
||||
for _, s := range slices {
|
||||
res = append(res, s...)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
err := git.ForceFastImport(t.Context(), repo56.CodeStorageRepo(), []git.FastImportCommit{
|
||||
{Ref: "refs/heads/master", Message: "init master", Files: []git.FastImportFile{{regular, "README.md", "The cake is a lie."}}},
|
||||
{Ref: "refs/heads/txt", Message: "init txt", Files: []git.FastImportFile{{regular, "README.txt", "My spoon is too big."}}},
|
||||
{Ref: "refs/heads/plain", Message: "init plain", Files: []git.FastImportFile{{regular, "README", "Birken my stocks gee howdy"}}},
|
||||
{Ref: "refs/heads/i18n", Message: "init i18n", Files: []git.FastImportFile{{regular, "README.zh.md", "你好世界"}}},
|
||||
{Ref: "refs/heads/subdir", Message: "init subdir", Files: []git.FastImportFile{{regular, "libcake/README.md", "Four pints of sugar."}}},
|
||||
{Ref: "refs/heads/special-subdir-docs", Message: "init special-subdir-docs", Files: []git.FastImportFile{{regular, "docs/README.md", "This is in docs/"}}},
|
||||
{Ref: "refs/heads/special-subdir-.gitea", Message: "init special-subdir-.gitea", Files: []git.FastImportFile{{regular, ".gitea/README.md", "This is in .gitea/"}}},
|
||||
{Ref: "refs/heads/special-subdir-.github", Message: "init special-subdir-.github", Files: []git.FastImportFile{{regular, ".github/README.md", "This is in .github/"}}},
|
||||
{Ref: "refs/heads/special-subdir-nested", Message: "init special-subdir-nested", Files: []git.FastImportFile{
|
||||
{regular, ".gitea/docs/README.md", "This is in docs/"},
|
||||
{regular, "subproject/.github/README.md", "This is in .github/"},
|
||||
}},
|
||||
{Ref: "refs/heads/symlink", Message: "init symlink", Files: []git.FastImportFile{
|
||||
{symlink, ".github/README.md", "../some/other/path/awefulcake.txt"},
|
||||
{symlink, "some/README.txt", "other/path/awefulcake.txt"},
|
||||
{regular, "some/other/path/awefulcake.txt", "This is in some/other/path"},
|
||||
{symlink, "trampoline", "up/back/down/down"},
|
||||
{symlink, "up/back/down/down/README.md", "../../../../up/down/left/reelmein"},
|
||||
{regular, "up/down/left/reelmein", "It's a me, mario"},
|
||||
}},
|
||||
{Ref: "refs/heads/symlink-loop", Message: "init symlink-loop", Files: []git.FastImportFile{
|
||||
{symlink, "README.md", "trampoline"},
|
||||
{symlink, "some/README.txt", "other/path/awefulcake.txt"},
|
||||
{symlink, "some/other/path/awefulcake.txt", "../../../README.md"},
|
||||
{symlink, "trampoline", "README.md"},
|
||||
}},
|
||||
{Ref: "refs/heads/sp-ace", Message: "init sp-ace", Files: []git.FastImportFile{{regular, "read me", "The cake is a lie."}}},
|
||||
{Ref: "refs/heads/fallbacks", Message: "init fallbacks", Files: combineFiles(allGitea, allGithub, allRoot, allDocs)},
|
||||
{Ref: "refs/heads/fallbacks2", Message: "init fallbacks2", Files: combineFiles(allGitea[1:], allGithub, allRoot, allDocs)},
|
||||
{Ref: "refs/heads/fallbacks3", Message: "init fallbacks3", Files: combineFiles(allGitea[2:], allGithub, allRoot, allDocs)},
|
||||
{Ref: "refs/heads/fallbacks4", Message: "init fallbacks4", Files: combineFiles(allGithub, allRoot, allDocs)},
|
||||
{Ref: "refs/heads/fallbacks5", Message: "init fallbacks5", Files: combineFiles(allGithub[1:], allRoot, allDocs)},
|
||||
{Ref: "refs/heads/fallbacks6", Message: "init fallbacks6", Files: combineFiles(allGithub[2:], allRoot, allDocs)},
|
||||
{Ref: "refs/heads/fallbacks7", Message: "init fallbacks7", Files: combineFiles(allRoot, allDocs)},
|
||||
{Ref: "refs/heads/fallbacks8", Message: "init fallbacks8", Files: combineFiles(allRoot[1:], allDocs)},
|
||||
{Ref: "refs/heads/fallbacks9", Message: "init fallbacks9", Files: combineFiles(allRoot[2:], allDocs)},
|
||||
{Ref: "refs/heads/fallbacks10", Message: "init fallbacks10", Files: combineFiles(allDocs)},
|
||||
{Ref: "refs/heads/fallbacks11", Message: "init fallbacks11", Files: combineFiles(allDocs[1:])},
|
||||
{Ref: "refs/heads/fallbacks12", Message: "init fallbacks12", Files: combineFiles(allDocs[2:])},
|
||||
{Ref: "refs/heads/fallbacks-broken-symlinks", Message: "init fallbacks-broken-symlinks", Files: []git.FastImportFile{
|
||||
{symlink, ".gitea/README.md", "non-existent-file"},
|
||||
{symlink, ".github/README.md", "non-existent-file"},
|
||||
{symlink, "README.md", "non-existent-file"},
|
||||
{regular, "docs/README", "This is docs/README"},
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// there are many combinations:
|
||||
// - READMEs can be .md, .txt, or have no extension
|
||||
// - READMEs can be tagged with a language and even a country code
|
||||
// - READMEs can be stored in docs/, .gitea/, or .github/
|
||||
// - READMEs can be stored in "docs/", ".gitea/", or ".github/"
|
||||
// - READMEs can be symlinks to other files
|
||||
// - READMEs can be broken symlinks which should not render
|
||||
//
|
||||
@@ -344,7 +428,7 @@ func testViewRepoDirectoryReadme(t *testing.T) {
|
||||
check("md", "/user2/readme-test/src/branch/master/", "README.md", "markdown", "The cake is a lie.")
|
||||
check("txt", "/user2/readme-test/src/branch/txt/", "README.txt", "plain-text", "My spoon is too big.")
|
||||
check("plain", "/user2/readme-test/src/branch/plain/", "README", "plain-text", "Birken my stocks gee howdy")
|
||||
check("i18n", "/user2/readme-test/src/branch/i18n/", "README.zh.md", "markdown", "蛋糕是一个谎言")
|
||||
check("i18n", "/user2/readme-test/src/branch/i18n/", "README.zh.md", "markdown", "你好世界")
|
||||
|
||||
// using HEAD ref
|
||||
check("branch-HEAD", "/user2/readme-test/src/branch/HEAD/", "README.md", "markdown", "The cake is a lie.")
|
||||
@@ -357,33 +441,32 @@ func testViewRepoDirectoryReadme(t *testing.T) {
|
||||
check(".gitea", "/user2/readme-test/src/branch/special-subdir-.gitea/", ".gitea/README.md", "markdown", "This is in .gitea/")
|
||||
check(".github", "/user2/readme-test/src/branch/special-subdir-.github/", ".github/README.md", "markdown", "This is in .github/")
|
||||
|
||||
// symlinks
|
||||
// symlinks are subtle:
|
||||
// - they should be able to handle going a reasonable number of times up and down in the tree
|
||||
// - they shouldn't get stuck on link cycles
|
||||
// - they should determine the filetype based on the name of the link, not the target
|
||||
check("symlink", "/user2/readme-test/src/branch/symlink/", "README.md", "markdown", "This is in some/other/path")
|
||||
check("symlink", "/user2/readme-test/src/branch/symlink/", ".github/README.md", "markdown", "This is in some/other/path")
|
||||
check("symlink-multiple", "/user2/readme-test/src/branch/symlink/some/", "README.txt", "plain-text", "This is in some/other/path")
|
||||
check("symlink-up-and-down", "/user2/readme-test/src/branch/symlink/up/back/down/down", "README.md", "markdown", "It's a me, mario")
|
||||
|
||||
// testing fallback rules
|
||||
// READMEs are searched in this order:
|
||||
// - [README.zh-cn.md, README.zh_cn.md, README.zh.md, README_zh.md, README.md, README.txt, README,
|
||||
// docs/README.zh-cn.md, docs/README.zh_cn.md, docs/README.zh.md, docs/README_zh.md, docs/README.md, docs/README.txt, docs/README,
|
||||
// .gitea/README.zh-cn.md, .gitea/README.zh_cn.md, .gitea/README.zh.md, .gitea/README_zh.md, .gitea/README.md, .gitea/README.txt, .gitea/README,
|
||||
|
||||
// .github/README.zh-cn.md, .github/README.zh_cn.md, .github/README.zh.md, .github/README_zh.md, .github/README.md, .github/README.txt, .github/README]
|
||||
// - directories: .gitea -> .github -> (root) -> docs
|
||||
// - extensions: longer to shorter, non-English to English, e.g.: ".zh-cn.md" -> ".zh.md" -> "_zh.md" -> "en.md" -> ".md" -> (no-ext)
|
||||
// and a broken/looped symlink counts as not existing at all and should be skipped.
|
||||
// again, this doesn't cover all cases, but it covers a few
|
||||
check("fallback/top", "/user2/readme-test/src/branch/fallbacks/", "README.en.md", "markdown", "This is README.en.md")
|
||||
check("fallback/2", "/user2/readme-test/src/branch/fallbacks2/", "README.md", "markdown", "This is README.md")
|
||||
check("fallback/3", "/user2/readme-test/src/branch/fallbacks3/", "README", "plain-text", "This is README")
|
||||
check("fallback/4", "/user2/readme-test/src/branch/fallbacks4/", "docs/README.en.md", "markdown", "This is docs/README.en.md")
|
||||
check("fallback/5", "/user2/readme-test/src/branch/fallbacks5/", "docs/README.md", "markdown", "This is docs/README.md")
|
||||
check("fallback/6", "/user2/readme-test/src/branch/fallbacks6/", "docs/README", "plain-text", "This is docs/README")
|
||||
check("fallback/7", "/user2/readme-test/src/branch/fallbacks7/", ".gitea/README.en.md", "markdown", "This is .gitea/README.en.md")
|
||||
check("fallback/8", "/user2/readme-test/src/branch/fallbacks8/", ".gitea/README.md", "markdown", "This is .gitea/README.md")
|
||||
check("fallback/9", "/user2/readme-test/src/branch/fallbacks9/", ".gitea/README", "plain-text", "This is .gitea/README")
|
||||
check("fallback/top", "/user2/readme-test/src/branch/fallbacks/", ".gitea/README.en.md", "markdown", "This is .gitea/README.en.md")
|
||||
check("fallback/2", "/user2/readme-test/src/branch/fallbacks2/", ".gitea/README.md", "markdown", "This is .gitea/README.md")
|
||||
check("fallback/3", "/user2/readme-test/src/branch/fallbacks3/", ".gitea/README", "plain-text", "This is .gitea/README")
|
||||
check("fallback/4", "/user2/readme-test/src/branch/fallbacks4/", ".github/README.en.md", "markdown", "This is .github/README.en.md")
|
||||
check("fallback/5", "/user2/readme-test/src/branch/fallbacks5/", ".github/README.md", "markdown", "This is .github/README.md")
|
||||
check("fallback/6", "/user2/readme-test/src/branch/fallbacks6/", ".github/README", "plain-text", "This is .github/README")
|
||||
check("fallback/7", "/user2/readme-test/src/branch/fallbacks7/", "README.en.md", "markdown", "This is README.en.md")
|
||||
check("fallback/8", "/user2/readme-test/src/branch/fallbacks8/", "README.md", "markdown", "This is README.md")
|
||||
check("fallback/9", "/user2/readme-test/src/branch/fallbacks9/", "README", "plain-text", "This is README")
|
||||
check("fallback/10", "/user2/readme-test/src/branch/fallbacks10/", "docs/README.en.md", "markdown", "This is docs/README.en.md")
|
||||
check("fallback/11", "/user2/readme-test/src/branch/fallbacks11/", "docs/README.md", "markdown", "This is docs/README.md")
|
||||
check("fallback/12", "/user2/readme-test/src/branch/fallbacks12/", "docs/README", "plain-text", "This is docs/README")
|
||||
|
||||
// this case tests that broken symlinks count as missing files, instead of rendering their contents
|
||||
check("fallbacks-broken-symlinks", "/user2/readme-test/src/branch/fallbacks-broken-symlinks/", "docs/README", "plain-text", "This is docs/README")
|
||||
@@ -407,24 +490,34 @@ func testViewRepoDirectoryReadme(t *testing.T) {
|
||||
})
|
||||
}
|
||||
missing("sp-ace", "/user2/readme-test/src/branch/sp-ace/")
|
||||
missing("nested-special", "/user2/readme-test/src/branch/special-subdir-nested/subproject") // the special subdirs should only trigger on the repo root
|
||||
missing("nested-special", "/user2/readme-test/src/branch/special-subdir-nested/subproject") // the special sub-dirs should only trigger on the repo root
|
||||
missing("special-subdir-nested", "/user2/readme-test/src/branch/special-subdir-nested/")
|
||||
missing("symlink-loop", "/user2/readme-test/src/branch/symlink-loop/")
|
||||
}
|
||||
|
||||
func testViewRepoSymlink(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequest(t, "GET", "/user2/readme-test/src/branch/symlink")
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 2, ID: 1})
|
||||
err := git.ForceFastImport(t.Context(), repo.CodeStorageRepo(), []git.FastImportCommit{
|
||||
{
|
||||
Ref: "refs/heads/symlink", Message: "test", Files: []git.FastImportFile{
|
||||
{git.EntryModeSymlink, "README.md", "some/other/path/awefulcake.txt"},
|
||||
{git.EntryModeBlob, "some/other/path/awefulcake.txt", "text content"},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/symlink")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
AssertHTMLElement(t, htmlDoc, ".entry-symbol-link", true)
|
||||
followSymbolLinkHref := htmlDoc.Find(".entry-symbol-link").AttrOr("href", "")
|
||||
require.Equal(t, "/user2/readme-test/src/branch/symlink/README.md?follow_symlink=1", followSymbolLinkHref)
|
||||
require.Equal(t, "/user2/repo1/src/branch/symlink/README.md?follow_symlink=1", followSymbolLinkHref)
|
||||
|
||||
req = NewRequest(t, "GET", followSymbolLinkHref)
|
||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
assert.Equal(t, "/user2/readme-test/src/branch/symlink/some/other/path/awefulcake.txt?follow_symlink=1", resp.Header().Get("Location"))
|
||||
assert.Equal(t, "/user2/repo1/src/branch/symlink/some/other/path/awefulcake.txt?follow_symlink=1", resp.Header().Get("Location"))
|
||||
}
|
||||
|
||||
func testMarkDownReadmeImage(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user