mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-03 03:18:34 +00:00
Follow file symlinks in the UI to their target (#28835)
Symlinks are followed when you click on a link next to an entry, either until a file has been found or until we know that the link is dead. When the link cannot be accessed, we fall back to the current behavior of showing the document containing the target. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
a94e472788
commit
8dbf13b1cb
@ -6,17 +6,17 @@ package fileicon
|
||||
import "code.gitea.io/gitea/modules/git"
|
||||
|
||||
type EntryInfo struct {
|
||||
FullName string
|
||||
BaseName string
|
||||
EntryMode git.EntryMode
|
||||
SymlinkToMode git.EntryMode
|
||||
IsOpen bool
|
||||
}
|
||||
|
||||
func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo {
|
||||
ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
|
||||
func EntryInfoFromGitTreeEntry(commit *git.Commit, fullPath string, gitEntry *git.TreeEntry) *EntryInfo {
|
||||
ret := &EntryInfo{BaseName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
|
||||
if gitEntry.IsLink() {
|
||||
if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() {
|
||||
ret.SymlinkToMode = te.Mode()
|
||||
if res, err := git.EntryFollowLink(commit, fullPath, gitEntry); err == nil && res.TargetEntry.IsDir() {
|
||||
ret.SymlinkToMode = res.TargetEntry.Mode()
|
||||
}
|
||||
}
|
||||
return ret
|
||||
|
@ -5,7 +5,6 @@ package fileicon
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -134,7 +133,7 @@ func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string {
|
||||
return "folder-git"
|
||||
}
|
||||
|
||||
fileNameLower := strings.ToLower(path.Base(entry.FullName))
|
||||
fileNameLower := strings.ToLower(entry.BaseName)
|
||||
if entry.EntryMode.IsDir() {
|
||||
if s, ok := m.rules.FolderNames[fileNameLower]; ok {
|
||||
return s
|
||||
|
@ -20,8 +20,8 @@ func TestMain(m *testing.M) {
|
||||
func TestFindIconName(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
p := fileicon.DefaultMaterialIconProvider()
|
||||
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob}))
|
||||
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob}))
|
||||
assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob}))
|
||||
assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob}))
|
||||
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.php", EntryMode: git.EntryModeBlob}))
|
||||
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.PHP", EntryMode: git.EntryModeBlob}))
|
||||
assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.js", EntryMode: git.EntryModeBlob}))
|
||||
assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.vba", EntryMode: git.EntryModeBlob}))
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ import (
|
||||
|
||||
// Commit represents a git commit.
|
||||
type Commit struct {
|
||||
Tree
|
||||
Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache"
|
||||
|
||||
ID ObjectID // The ID of this commit object
|
||||
Author *Signature
|
||||
Committer *Signature
|
||||
|
@ -32,22 +32,6 @@ func (err ErrNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrSymlinkUnresolved entry.FollowLink error
|
||||
type ErrSymlinkUnresolved struct {
|
||||
Name string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (err ErrSymlinkUnresolved) Error() string {
|
||||
return fmt.Sprintf("%s: %s", err.Name, err.Message)
|
||||
}
|
||||
|
||||
// IsErrSymlinkUnresolved if some error is ErrSymlinkUnresolved
|
||||
func IsErrSymlinkUnresolved(err error) bool {
|
||||
_, ok := err.(ErrSymlinkUnresolved)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ErrBranchNotExist represents a "BranchNotExist" kind of error.
|
||||
type ErrBranchNotExist struct {
|
||||
Name string
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// GetTreeEntryByPath get the tree entries according the sub dir
|
||||
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
||||
func (t *Tree) GetTreeEntryByPath(relpath string) (_ *TreeEntry, err error) {
|
||||
if len(relpath) == 0 {
|
||||
return &TreeEntry{
|
||||
ptree: t,
|
||||
@ -21,27 +21,25 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FIXME: This should probably use git cat-file --batch to be a bit more efficient
|
||||
relpath = path.Clean(relpath)
|
||||
parts := strings.Split(relpath, "/")
|
||||
var err error
|
||||
|
||||
tree := t
|
||||
for i, name := range parts {
|
||||
if i == len(parts)-1 {
|
||||
entries, err := tree.ListEntries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range entries {
|
||||
if v.Name() == name {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tree, err = tree.SubTree(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, name := range parts[:len(parts)-1] {
|
||||
tree, err = tree.SubTree(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
name := parts[len(parts)-1]
|
||||
entries, err := tree.ListEntries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range entries {
|
||||
if v.Name() == name {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNotExist{"", relpath}
|
||||
|
@ -5,7 +5,7 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@ -24,77 +24,57 @@ func (te *TreeEntry) Type() string {
|
||||
}
|
||||
}
|
||||
|
||||
// FollowLink returns the entry pointed to by a symlink
|
||||
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
||||
if !te.IsLink() {
|
||||
return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"}
|
||||
}
|
||||
|
||||
// read the link
|
||||
r, err := te.Blob().DataAsync()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
closed := false
|
||||
defer func() {
|
||||
if !closed {
|
||||
_ = r.Close()
|
||||
}
|
||||
}()
|
||||
buf := make([]byte, te.Size())
|
||||
_, err = io.ReadFull(r, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = r.Close()
|
||||
closed = true
|
||||
|
||||
lnk := string(buf)
|
||||
t := te.ptree
|
||||
|
||||
// traverse up directories
|
||||
for ; t != nil && strings.HasPrefix(lnk, "../"); lnk = lnk[3:] {
|
||||
t = t.ptree
|
||||
}
|
||||
|
||||
if t == nil {
|
||||
return nil, ErrSymlinkUnresolved{te.Name(), "points outside of repo"}
|
||||
}
|
||||
|
||||
target, err := t.GetTreeEntryByPath(lnk)
|
||||
if err != nil {
|
||||
if IsErrNotExist(err) {
|
||||
return nil, ErrSymlinkUnresolved{te.Name(), "broken link"}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return target, nil
|
||||
type EntryFollowResult struct {
|
||||
SymlinkContent string
|
||||
TargetFullPath string
|
||||
TargetEntry *TreeEntry
|
||||
}
|
||||
|
||||
// FollowLinks returns the entry ultimately pointed to by a symlink
|
||||
func (te *TreeEntry) FollowLinks(optLimit ...int) (*TreeEntry, error) {
|
||||
func EntryFollowLink(commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) {
|
||||
if !te.IsLink() {
|
||||
return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"}
|
||||
return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q is not a symlink", fullPath)
|
||||
}
|
||||
|
||||
// git's filename max length is 4096, hopefully a link won't be longer than multiple of that
|
||||
const maxSymlinkSize = 20 * 4096
|
||||
if te.Blob().Size() > maxSymlinkSize {
|
||||
return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q content exceeds symlink limit", fullPath)
|
||||
}
|
||||
|
||||
link, err := te.Blob().GetBlobContent(maxSymlinkSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.HasPrefix(link, "/") {
|
||||
// It's said that absolute path will be stored as is in Git
|
||||
return &EntryFollowResult{SymlinkContent: link}, util.ErrorWrap(util.ErrUnprocessableContent, "%q is an absolute symlink", fullPath)
|
||||
}
|
||||
|
||||
targetFullPath := path.Join(path.Dir(fullPath), link)
|
||||
targetEntry, err := commit.GetTreeEntryByPath(targetFullPath)
|
||||
if err != nil {
|
||||
return &EntryFollowResult{SymlinkContent: link}, err
|
||||
}
|
||||
return &EntryFollowResult{SymlinkContent: link, TargetFullPath: targetFullPath, TargetEntry: targetEntry}, nil
|
||||
}
|
||||
|
||||
func EntryFollowLinks(commit *Commit, firstFullPath string, firstTreeEntry *TreeEntry, optLimit ...int) (res *EntryFollowResult, err error) {
|
||||
limit := util.OptionalArg(optLimit, 10)
|
||||
entry := te
|
||||
treeEntry, fullPath := firstTreeEntry, firstFullPath
|
||||
for range limit {
|
||||
if !entry.IsLink() {
|
||||
res, err = EntryFollowLink(commit, fullPath, treeEntry)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
treeEntry, fullPath = res.TargetEntry, res.TargetFullPath
|
||||
if !treeEntry.IsLink() {
|
||||
break
|
||||
}
|
||||
next, err := entry.FollowLink()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if next.ID == entry.ID {
|
||||
return nil, ErrSymlinkUnresolved{entry.Name(), "recursive link"}
|
||||
}
|
||||
entry = next
|
||||
}
|
||||
if entry.IsLink() {
|
||||
return nil, ErrSymlinkUnresolved{te.Name(), "too many levels of symbolic links"}
|
||||
if treeEntry.IsLink() {
|
||||
return res, util.ErrorWrap(util.ErrUnprocessableContent, "%q has too many links", firstFullPath)
|
||||
}
|
||||
return entry, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// returns the Tree pointed to by this TreeEntry, or nil if this is not a tree
|
||||
|
76
modules/git/tree_entry_common_test.go
Normal file
76
modules/git/tree_entry_common_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFollowLink(t *testing.T) {
|
||||
r, err := openRepositoryWithDefaultContext("tests/repos/repo1_bare")
|
||||
require.NoError(t, err)
|
||||
defer r.Close()
|
||||
|
||||
commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123")
|
||||
require.NoError(t, err)
|
||||
|
||||
// get the symlink
|
||||
{
|
||||
lnkFullPath := "foo/bar/link_to_hello"
|
||||
lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, lnk.IsLink())
|
||||
|
||||
// should be able to dereference to target
|
||||
res, err := EntryFollowLink(commit, lnkFullPath, lnk)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hello", res.TargetEntry.Name())
|
||||
assert.Equal(t, "foo/nar/hello", res.TargetFullPath)
|
||||
assert.False(t, res.TargetEntry.IsLink())
|
||||
assert.Equal(t, "b14df6442ea5a1b382985a6549b85d435376c351", res.TargetEntry.ID.String())
|
||||
}
|
||||
|
||||
{
|
||||
// should error when called on a normal file
|
||||
entry, err := commit.Tree.GetTreeEntryByPath("file1.txt")
|
||||
require.NoError(t, err)
|
||||
res, err := EntryFollowLink(commit, "file1.txt", entry)
|
||||
assert.ErrorIs(t, err, util.ErrUnprocessableContent)
|
||||
assert.Nil(t, res)
|
||||
}
|
||||
|
||||
{
|
||||
// should error for broken links
|
||||
entry, err := commit.Tree.GetTreeEntryByPath("foo/broken_link")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, entry.IsLink())
|
||||
res, err := EntryFollowLink(commit, "foo/broken_link", entry)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
assert.Equal(t, "nar/broken_link", res.SymlinkContent)
|
||||
}
|
||||
|
||||
{
|
||||
// should error for external links
|
||||
entry, err := commit.Tree.GetTreeEntryByPath("foo/outside_repo")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, entry.IsLink())
|
||||
res, err := EntryFollowLink(commit, "foo/outside_repo", entry)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
assert.Equal(t, "../../outside_repo", res.SymlinkContent)
|
||||
}
|
||||
|
||||
{
|
||||
// testing fix for short link bug
|
||||
entry, err := commit.Tree.GetTreeEntryByPath("foo/link_short")
|
||||
require.NoError(t, err)
|
||||
res, err := EntryFollowLink(commit, "foo/link_short", entry)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
assert.Equal(t, "a", res.SymlinkContent)
|
||||
}
|
||||
}
|
@ -19,16 +19,12 @@ type TreeEntry struct {
|
||||
gogitTreeEntry *object.TreeEntry
|
||||
ptree *Tree
|
||||
|
||||
size int64
|
||||
sized bool
|
||||
fullName string
|
||||
size int64
|
||||
sized bool
|
||||
}
|
||||
|
||||
// Name returns the name of the entry
|
||||
func (te *TreeEntry) Name() string {
|
||||
if te.fullName != "" {
|
||||
return te.fullName
|
||||
}
|
||||
return te.gogitTreeEntry.Name
|
||||
}
|
||||
|
||||
@ -55,7 +51,7 @@ func (te *TreeEntry) Size() int64 {
|
||||
return te.size
|
||||
}
|
||||
|
||||
// IsSubModule if the entry is a sub module
|
||||
// IsSubModule if the entry is a submodule
|
||||
func (te *TreeEntry) IsSubModule() bool {
|
||||
return te.gogitTreeEntry.Mode == filemode.Submodule
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ type EntryMode int
|
||||
// one of these.
|
||||
const (
|
||||
// EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of
|
||||
// added the base commit will not have the file in its tree so a mode of 0o000000 is used.
|
||||
// when adding the base commit doesn't have the file in its tree, a mode of 0o000000 is used.
|
||||
EntryModeNoEntry EntryMode = 0o000000
|
||||
|
||||
EntryModeBlob EntryMode = 0o100644
|
||||
@ -30,7 +30,7 @@ func (e EntryMode) String() string {
|
||||
return strconv.FormatInt(int64(e), 8)
|
||||
}
|
||||
|
||||
// IsSubModule if the entry is a sub module
|
||||
// IsSubModule if the entry is a submodule
|
||||
func (e EntryMode) IsSubModule() bool {
|
||||
return e == EntryModeCommit
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func (te *TreeEntry) Size() int64 {
|
||||
return te.size
|
||||
}
|
||||
|
||||
// IsSubModule if the entry is a sub module
|
||||
// IsSubModule if the entry is a submodule
|
||||
func (te *TreeEntry) IsSubModule() bool {
|
||||
return te.entryMode.IsSubModule()
|
||||
}
|
||||
|
@ -53,50 +53,3 @@ func TestEntriesCustomSort(t *testing.T) {
|
||||
assert.Equal(t, "bcd", entries[6].Name())
|
||||
assert.Equal(t, "abc", entries[7].Name())
|
||||
}
|
||||
|
||||
func TestFollowLink(t *testing.T) {
|
||||
r, err := openRepositoryWithDefaultContext("tests/repos/repo1_bare")
|
||||
assert.NoError(t, err)
|
||||
defer r.Close()
|
||||
|
||||
commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// get the symlink
|
||||
lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, lnk.IsLink())
|
||||
|
||||
// should be able to dereference to target
|
||||
target, err := lnk.FollowLink()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", target.Name())
|
||||
assert.False(t, target.IsLink())
|
||||
assert.Equal(t, "b14df6442ea5a1b382985a6549b85d435376c351", target.ID.String())
|
||||
|
||||
// should error when called on normal file
|
||||
target, err = commit.Tree.GetTreeEntryByPath("file1.txt")
|
||||
assert.NoError(t, err)
|
||||
_, err = target.FollowLink()
|
||||
assert.EqualError(t, err, "file1.txt: not a symlink")
|
||||
|
||||
// should error for broken links
|
||||
target, err = commit.Tree.GetTreeEntryByPath("foo/broken_link")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, target.IsLink())
|
||||
_, err = target.FollowLink()
|
||||
assert.EqualError(t, err, "broken_link: broken link")
|
||||
|
||||
// should error for external links
|
||||
target, err = commit.Tree.GetTreeEntryByPath("foo/outside_repo")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, target.IsLink())
|
||||
_, err = target.FollowLink()
|
||||
assert.EqualError(t, err, "outside_repo: points outside of repo")
|
||||
|
||||
// testing fix for short link bug
|
||||
target, err = commit.Tree.GetTreeEntryByPath("foo/link_short")
|
||||
assert.NoError(t, err)
|
||||
_, err = target.FollowLink()
|
||||
assert.EqualError(t, err, "link_short: broken link")
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
||||
seen := map[plumbing.Hash]bool{}
|
||||
walker := object.NewTreeWalker(t.gogitTree, true, seen)
|
||||
for {
|
||||
fullName, entry, err := walker.Next()
|
||||
_, entry, err := walker.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
@ -84,7 +84,6 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
||||
ID: ParseGogitHash(entry.Hash),
|
||||
gogitTreeEntry: &entry,
|
||||
ptree: t,
|
||||
fullName: fullName,
|
||||
}
|
||||
entries = append(entries, convertedEntry)
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ var (
|
||||
ErrNotExist = errors.New("resource does not exist") // also implies HTTP 404
|
||||
ErrAlreadyExist = errors.New("resource already exists") // also implies HTTP 409
|
||||
|
||||
// ErrUnprocessableContent implies HTTP 422, syntax of the request content was correct,
|
||||
// but server was unable to process the contained instructions
|
||||
// ErrUnprocessableContent implies HTTP 422, the syntax of the request content is correct,
|
||||
// but the server is unable to process the contained instructions
|
||||
ErrUnprocessableContent = errors.New("unprocessable content")
|
||||
)
|
||||
|
||||
|
@ -2782,6 +2782,7 @@ topic.done = Done
|
||||
topic.count_prompt = You cannot select more than 25 topics
|
||||
topic.format_prompt = Topics must start with a letter or number, can include dashes ('-') and dots ('.'), can be up to 35 characters long. Letters must be lowercase.
|
||||
|
||||
find_file.follow_symlink= Follow this symlink to where it is pointing at
|
||||
find_file.go_to_file = Go to file
|
||||
find_file.no_matching = No matching file found
|
||||
|
||||
|
@ -6,6 +6,7 @@ package repo
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
pull_model "code.gitea.io/gitea/models/pull"
|
||||
@ -111,7 +112,7 @@ func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTr
|
||||
item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
|
||||
item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
|
||||
item.NameHash = git.HashFilePathForWebUI(item.FullName)
|
||||
item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{FullName: file.HeadPath, EntryMode: file.HeadMode})
|
||||
item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{BaseName: path.Base(file.HeadPath), EntryMode: file.HeadMode})
|
||||
|
||||
switch file.HeadMode {
|
||||
case git.EntryModeTree:
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -260,7 +261,9 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
|
||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||
fileIcons := map[string]template.HTML{}
|
||||
for _, f := range files {
|
||||
fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFromGitTreeEntry(f.Entry))
|
||||
fullPath := path.Join(ctx.Repo.TreePath, f.Entry.Name())
|
||||
entryInfo := fileicon.EntryInfoFromGitTreeEntry(ctx.Repo.Commit, fullPath, f.Entry)
|
||||
fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
|
||||
}
|
||||
fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
|
||||
ctx.Data["FileIcons"] = fileIcons
|
||||
|
@ -143,7 +143,7 @@ func prepareToRenderDirectory(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
|
||||
}
|
||||
|
||||
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
|
||||
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, ctx.Repo.TreePath, entries, true)
|
||||
if err != nil {
|
||||
ctx.ServerError("findReadmeFileInEntries", err)
|
||||
return
|
||||
@ -377,8 +377,8 @@ func prepareHomeTreeSideBarSwitch(ctx *context.Context) {
|
||||
|
||||
func redirectSrcToRaw(ctx *context.Context) bool {
|
||||
// GitHub redirects a tree path with "?raw=1" to the raw path
|
||||
// It is useful to embed some raw contents into markdown files,
|
||||
// then viewing the markdown in "src" path could embed the raw content correctly.
|
||||
// It is useful to embed some raw contents into Markdown files,
|
||||
// then viewing the Markdown in "src" path could embed the raw content correctly.
|
||||
if ctx.Repo.TreePath != "" && ctx.FormBool("raw") {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath))
|
||||
return true
|
||||
@ -386,6 +386,20 @@ func redirectSrcToRaw(ctx *context.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) bool {
|
||||
if ctx.Repo.TreePath == "" || !ctx.FormBool("follow_symlink") {
|
||||
return false
|
||||
}
|
||||
if treePathEntry.IsLink() {
|
||||
if res, err := git.EntryFollowLinks(ctx.Repo.Commit, ctx.Repo.TreePath, treePathEntry); err == nil {
|
||||
redirect := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(res.TargetFullPath) + "?" + ctx.Req.URL.RawQuery
|
||||
ctx.Redirect(redirect)
|
||||
return true
|
||||
} // else: don't handle the links we cannot resolve, so ignore the error
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Home render repository home page
|
||||
func Home(ctx *context.Context) {
|
||||
if handleRepoHomeFeed(ctx) {
|
||||
@ -394,6 +408,7 @@ func Home(ctx *context.Context) {
|
||||
if redirectSrcToRaw(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check whether the repo is viewable: not in migration, and the code unit should be enabled
|
||||
// Ideally the "feed" logic should be after this, but old code did so, so keep it as-is.
|
||||
checkHomeCodeViewable(ctx)
|
||||
@ -424,6 +439,10 @@ func Home(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if redirectFollowSymlink(ctx, entry) {
|
||||
return
|
||||
}
|
||||
|
||||
// prepare the tree path
|
||||
var treeNames, paths []string
|
||||
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
||||
|
@ -32,15 +32,7 @@ import (
|
||||
// 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)
|
||||
|
||||
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/)
|
||||
for _, entry := range entries {
|
||||
if tryWellKnownDirs && entry.IsDir() {
|
||||
@ -62,16 +54,23 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
|
||||
docsEntries[2] = entry
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
log.Debug("Potential readme file: %s", entry.Name())
|
||||
fullPath := path.Join(parentDir, 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.IsErrSymlinkUnresolved(err) {
|
||||
return "", nil, err
|
||||
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
|
||||
res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry)
|
||||
if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) {
|
||||
readmeFiles[i] = entry
|
||||
}
|
||||
} else {
|
||||
@ -80,6 +79,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var readmeFile *git.TreeEntry
|
||||
for _, f := range readmeFiles {
|
||||
if f != nil {
|
||||
@ -103,7 +103,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false)
|
||||
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, parentDir, childEntries, false)
|
||||
if err != nil && !git.IsErrNotExist(err) {
|
||||
return "", nil, err
|
||||
}
|
||||
@ -139,22 +139,29 @@ func localizedExtensions(ext, languageCode string) (localizedExts []string) {
|
||||
}
|
||||
|
||||
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
|
||||
if readmeFile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
readmeFullPath := path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())
|
||||
readmeTargetEntry := readmeFile
|
||||
if readmeFile.IsLink() {
|
||||
if res, err := git.EntryFollowLinks(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, target.Blob())
|
||||
buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, readmeTargetEntry.Blob())
|
||||
if err != nil {
|
||||
ctx.ServerError("getFileReader", err)
|
||||
return
|
||||
@ -162,7 +169,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
|
||||
defer dataRc.Close()
|
||||
|
||||
ctx.Data["FileIsText"] = fInfo.st.IsText()
|
||||
ctx.Data["FileTreePath"] = path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())
|
||||
ctx.Data["FileTreePath"] = readmeFullPath
|
||||
ctx.Data["FileSize"] = fInfo.fileSize
|
||||
ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
|
||||
|
||||
@ -189,10 +196,10 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
||||
CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder),
|
||||
CurrentTreePath: path.Dir(readmeFullPath),
|
||||
}).
|
||||
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).
|
||||
WithRelativePath(readmeFullPath)
|
||||
|
||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
|
||||
if err != nil {
|
||||
|
@ -161,7 +161,7 @@ func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.Re
|
||||
FullPath: path.Join(parentDir, entry.Name()),
|
||||
}
|
||||
|
||||
entryInfo := fileicon.EntryInfoFromGitTreeEntry(entry)
|
||||
entryInfo := fileicon.EntryInfoFromGitTreeEntry(commit, node.FullPath, entry)
|
||||
node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
|
||||
if entryInfo.EntryMode.IsDir() {
|
||||
entryInfo.IsOpen = true
|
||||
|
@ -41,6 +41,9 @@
|
||||
</a>
|
||||
{{else}}
|
||||
<a class="entry-name" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a>
|
||||
{{if $entry.IsLink}}
|
||||
<a class="entry-symbol-link flex-text-inline" data-tooltip-content title="{{ctx.Locale.Tr "repo.find_file.follow_symlink"}}" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}?follow_symlink=1">{{svg "octicon-link" 12}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepoView(t *testing.T) {
|
||||
@ -41,6 +42,7 @@ func TestRepoView(t *testing.T) {
|
||||
t.Run("BlameFileInRepo", testBlameFileInRepo)
|
||||
t.Run("ViewRepoDirectory", testViewRepoDirectory)
|
||||
t.Run("ViewRepoDirectoryReadme", testViewRepoDirectoryReadme)
|
||||
t.Run("ViewRepoSymlink", testViewRepoSymlink)
|
||||
t.Run("MarkDownReadmeImage", testMarkDownReadmeImage)
|
||||
t.Run("MarkDownReadmeImageSubfolder", testMarkDownReadmeImageSubfolder)
|
||||
t.Run("GeneratedSourceLink", testGeneratedSourceLink)
|
||||
@ -412,6 +414,21 @@ func testViewRepoDirectoryReadme(t *testing.T) {
|
||||
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")
|
||||
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)
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
func testMarkDownReadmeImage(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user