mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-21 09:31:19 +02:00
Compare commits
2 Commits
a94e472788
...
f3364ec57f
Author | SHA1 | Date | |
---|---|---|---|
|
f3364ec57f | ||
|
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
|
||||
|
||||
|
@@ -1969,6 +1969,7 @@ pulls.cmd_instruction_checkout_title=Seiceáil
|
||||
pulls.cmd_instruction_checkout_desc=Ó stór tionscadail, seiceáil brainse nua agus déan tástáil ar na hathruithe.
|
||||
pulls.cmd_instruction_merge_title=Cumaisc
|
||||
pulls.cmd_instruction_merge_desc=Cumaisc na hathruithe agus nuashonrú ar Gitea.
|
||||
pulls.cmd_instruction_merge_warning=Rabhadh: Ní féidir iarratas tarraingthe cumaisc a dhéanamh leis an oibríocht seo mar nach bhfuil "autodetect manual merge" cumasaithe.
|
||||
pulls.clear_merge_message=Glan an teachtaireacht chumaisc
|
||||
pulls.clear_merge_message_hint=Má imrítear an teachtaireacht chumaisc ní bhainfear ach ábhar na teachtaireachta tiomanta agus coimeádfar leantóirí git ginte ar nós "Co-Authored-By …".
|
||||
|
||||
|
@@ -420,8 +420,9 @@ remember_me=记住此设备
|
||||
remember_me.compromised=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。
|
||||
forgot_password_title=忘记密码
|
||||
forgot_password=忘记密码?
|
||||
need_account=需要一个帐户?
|
||||
sign_up_now=还没账号?马上注册。
|
||||
need_account=需要一个帐户?
|
||||
sign_up_tip=您正在系统中注册第一个帐户,它拥有管理员权限。请仔细记住您的用户名和密码。 如果您忘记了用户名或密码,请参阅 Gitea 文档以恢复账户。
|
||||
sign_up_now=立即注册。
|
||||
sign_up_successful=帐户创建成功。欢迎!
|
||||
confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 <b>%s</b>。请在下一个 %s 中检查您的收件箱以完成注册流程。 如果您的注册邮箱地址不正确,您可以重新登录并更改它。
|
||||
must_change_password=更新您的密码
|
||||
@@ -485,7 +486,7 @@ sspi_auth_failed=SSPI 认证失败
|
||||
password_pwned=此密码出现在 <a target="_blank" rel="noopener noreferrer" href="%s">被盗密码</a> 列表上并且曾经被公开。 请使用另一个密码再试一次。
|
||||
password_pwned_err=无法完成对 HaveIBeenPwned 的请求
|
||||
last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。
|
||||
signin_passkey=使用密钥登录
|
||||
signin_passkey=使用通行密钥登录
|
||||
back_to_sign_in=返回登录页面
|
||||
|
||||
[mail]
|
||||
@@ -518,7 +519,7 @@ register_success=注册成功
|
||||
issue_assigned.pull=@%[1]s 已将仓库 %[3]s 中的合并请求 %[2]s 指派给您
|
||||
issue_assigned.issue=@%[1]s 已将仓库 %[3]s 中的工单 %[2]s 指派给您
|
||||
|
||||
issue.x_mentioned_you=<b>@%s</b> 提到了您:
|
||||
issue.x_mentioned_you=<b>@%s</b> 提及了您:
|
||||
issue.action.force_push=<b>%[1]s</b> 强制从 %[3]s 推送 <b>%[2]s</b> 至 [4]s。
|
||||
issue.action.push_1=<b>@%[1]s</b> 推送了 %[3]d 个提交到 %[2]s
|
||||
issue.action.push_n=<b>@%[1]s</b> 推送了 %[3]d 个提交到 %[2]s
|
||||
@@ -838,7 +839,7 @@ ssh_desc=这些 SSH 公钥已经关联到您的账号。相应的私钥拥有完
|
||||
principal_desc=这些 SSH 证书规则已关联到您的账号将允许完全访问您所有仓库。
|
||||
gpg_desc=这些 GPG 公钥已经关联到您的账号。请妥善保管您的私钥因为他们将被用于认证提交。
|
||||
ssh_helper=<strong>需要帮助?</strong> 请查看有关 <a href="%s">如何生成 SSH 密钥</a> 或 <a href="%s">常见 SSH 问题</a> 寻找答案。
|
||||
gpg_helper=<strong>需要帮助吗?</strong>看一看 GitHub <a href="%s">关于 GPG</a> 的指导。
|
||||
gpg_helper=<strong>需要帮助?</strong>看一看 GitHub <a href="%s">关于 GPG</a> 的指导。
|
||||
add_new_key=增加 SSH 密钥
|
||||
add_new_gpg_key=添加的 GPG 密钥
|
||||
key_content_ssh_placeholder=以 'ssh-ed25519'、 'ssh-rsa'、 'ecdsa-sha2-nistp256'、'ecdsa-sha2-nistp384'、'ecdsa-sha2-nistp521'、 'sk-ecdsa-sha2-nistp256@openssh.com' 或 'sk-ssh-ed25519@openssh.com' 开头
|
||||
@@ -1016,10 +1017,10 @@ delete_account_title=删除当前帐户
|
||||
delete_account_desc=确实要永久删除此用户帐户吗?
|
||||
|
||||
email_notifications.enable=启用邮件通知
|
||||
email_notifications.onmention=只在被提到时邮件通知
|
||||
email_notifications.onmention=仅被提及时通知
|
||||
email_notifications.disable=停用邮件通知
|
||||
email_notifications.submit=邮件通知设置
|
||||
email_notifications.andyourown=和您自己的通知
|
||||
email_notifications.submit=设置邮件通知
|
||||
email_notifications.andyourown=仅与您相关的通知
|
||||
|
||||
visibility=用户可见性
|
||||
visibility.public=公开
|
||||
@@ -1061,6 +1062,7 @@ fork_no_valid_owners=这个代码仓库无法被派生,因为没有有效的
|
||||
fork.blocked_user=无法克隆仓库,因为您被仓库所有者屏蔽。
|
||||
use_template=使用此模板
|
||||
open_with_editor=用 %s 打开
|
||||
|
||||
download_zip=下载 ZIP
|
||||
download_tar=下载 TAR.GZ
|
||||
download_bundle=下载 BUNDLE
|
||||
@@ -1070,12 +1072,12 @@ repo_desc=描述
|
||||
repo_desc_helper=输入简要描述 (可选)
|
||||
repo_no_desc=无详细信息
|
||||
repo_lang=语言
|
||||
repo_gitignore_helper=选择 .gitignore 模板。
|
||||
repo_gitignore_helper=选择 .gitignore 模板
|
||||
repo_gitignore_helper_desc=从常见语言的模板列表中选择忽略跟踪的文件。默认情况下,由开发或构建工具生成的特殊文件都包含在 .gitignore 中。
|
||||
issue_labels=工单标签
|
||||
issue_labels_helper=选择一个工单标签集
|
||||
license=授权许可
|
||||
license_helper=选择授权许可文件。
|
||||
license_helper=选择授权许可文件
|
||||
license_helper_desc=许可证说明了其他人可以和不可以用您的代码做什么。不确定哪一个适合您的项目?见 <a target="_blank" rel="noopener noreferrer" href="%s">选择一个许可证</a>
|
||||
multiple_licenses=多许可证
|
||||
object_format=对象格式
|
||||
@@ -1228,6 +1230,7 @@ migrate.migrating_issues=迁移工单
|
||||
migrate.migrating_pulls=迁移合并请求
|
||||
migrate.cancel_migrating_title=取消迁移
|
||||
migrate.cancel_migrating_confirm=您想要取消此次迁移吗?
|
||||
migration_status=迁移状态
|
||||
|
||||
mirror_from=镜像自地址
|
||||
forked_from=派生自
|
||||
@@ -1353,6 +1356,7 @@ editor.update=更新 %s
|
||||
editor.delete=删除 %s
|
||||
editor.patch=应用补丁
|
||||
editor.patching=打补丁:
|
||||
editor.fail_to_apply_patch=无法应用补丁
|
||||
editor.new_patch=新补丁
|
||||
editor.commit_message_desc=添加一个可选的扩展描述...
|
||||
editor.signoff_desc=在提交日志消息末尾添加签署人信息。
|
||||
@@ -1372,6 +1376,7 @@ editor.branch_already_exists=此仓库已存在名为「%s」的分支。
|
||||
editor.directory_is_a_file=目录名「%s」已作为文件名在此仓库中存在。
|
||||
editor.file_is_a_symlink=`「%s」是一个符号链接,无法在 Web 编辑器中编辑`
|
||||
editor.filename_is_a_directory=文件名「%s」已作为目录名在此仓库中存在。
|
||||
editor.file_modifying_no_longer_exists=正在修改的文件「%s」已不存在于此仓库。
|
||||
editor.file_changed_while_editing=文件内容在您进行编辑时已经发生变动。<a target="_blank" rel="noopener noreferrer" href="%s">单击此处</a> 查看变动的具体内容,或者 <strong>再次提交</strong> 覆盖已发生的变动。
|
||||
editor.file_already_exists=此仓库已经存在名为「%s」的文件。
|
||||
editor.commit_id_not_matching=提交 ID 与您开始编辑时的 ID 不匹配。请提交到补丁分支然后合并。
|
||||
@@ -1392,7 +1397,15 @@ editor.user_no_push_to_branch=用户不能推送到分支
|
||||
editor.require_signed_commit=分支需要签名提交
|
||||
editor.cherry_pick=拣选提交 %s 到:
|
||||
editor.revert=将 %s 还原到:
|
||||
editor.failed_to_commit=提交更改失败。
|
||||
editor.failed_to_commit_summary=错误信息:
|
||||
|
||||
editor.fork_create=派生仓库发起请求变更
|
||||
editor.fork_create_description=您不能直接编辑此仓库。您可以从此仓库派生,进行编辑并创建一个拉取请求。
|
||||
editor.fork_edit_description=您不能直接编辑此仓库。 更改将写入您的派生仓库 <b>%s</b>,以便您可以创建一个拉取请求。
|
||||
editor.fork_not_editable=你已经派生了这个仓库,但是你的分叉是不可编辑的。
|
||||
editor.fork_failed_to_push_branch=推送分支 %s 到仓库失败。
|
||||
editor.fork_branch_exists=分支 "%s" 已存在于您的派生仓库中,请选择一个新的分支名称。
|
||||
|
||||
commits.desc=浏览代码修改历史
|
||||
commits.commits=次代码提交
|
||||
@@ -1714,6 +1727,8 @@ issues.remove_time_estimate_at=删除预估时间 %s
|
||||
issues.time_estimate_invalid=预计时间格式无效
|
||||
issues.start_tracking_history=`开始工作 %s`
|
||||
issues.tracker_auto_close=当此工单关闭时,自动停止计时器
|
||||
issues.stopwatch_already_stopped=此工单的计时器已经停止
|
||||
issues.stopwatch_already_created=此工单的计时器已经存在
|
||||
issues.tracking_already_started=`您已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!`
|
||||
issues.stop_tracking=停止计时器
|
||||
issues.stop_tracking_history=工作 <b>%[1]s</b> 于 %[2]s 停止
|
||||
@@ -1955,6 +1970,7 @@ pulls.cmd_instruction_checkout_title=检出
|
||||
pulls.cmd_instruction_checkout_desc=从您的仓库中检出一个新的分支并测试变更。
|
||||
pulls.cmd_instruction_merge_title=合并
|
||||
pulls.cmd_instruction_merge_desc=合并变更并更新到 Gitea 上
|
||||
pulls.cmd_instruction_merge_warning=警告:此操作不能合并该合并请求,因为「自动检测手动合并」未启用
|
||||
pulls.clear_merge_message=清除合并信息
|
||||
pulls.clear_merge_message_hint=清除合并消息只会删除提交消息内容,并保留生成的 Git 附加内容,如「Co-Authored-By…」。
|
||||
|
||||
@@ -2150,6 +2166,7 @@ settings.collaboration.write=可写权限
|
||||
settings.collaboration.read=可读权限
|
||||
settings.collaboration.owner=所有者
|
||||
settings.collaboration.undefined=未定义
|
||||
settings.collaboration.per_unit=单元权限
|
||||
settings.hooks=Web 钩子
|
||||
settings.githooks=管理 Git 钩子
|
||||
settings.basic_settings=基本设置
|
||||
@@ -2368,6 +2385,7 @@ settings.event_repository=仓库
|
||||
settings.event_repository_desc=创建或删除仓库
|
||||
settings.event_header_issue=工单事件
|
||||
settings.event_issues=工单
|
||||
settings.event_issues_desc=工单已打开、已关闭、已重新打开或已编辑。
|
||||
settings.event_issue_assign=工单已指派
|
||||
settings.event_issue_assign_desc=工单已指派或取消指派。
|
||||
settings.event_issue_label=工单增删标签
|
||||
@@ -2378,6 +2396,7 @@ settings.event_issue_comment=工单评论
|
||||
settings.event_issue_comment_desc=工单评论已创建、编辑或删除。
|
||||
settings.event_header_pull_request=合并请求事件
|
||||
settings.event_pull_request=合并请求
|
||||
settings.event_pull_request_desc=合并请求已打开、关闭、重新打开或编辑。
|
||||
settings.event_pull_request_assign=合并请求已指派
|
||||
settings.event_pull_request_assign_desc=合并请求已指派或取消指派。
|
||||
settings.event_pull_request_label=合并请求增删标签
|
||||
@@ -2395,6 +2414,8 @@ settings.event_pull_request_review_request_desc=合并请求评审已请求或
|
||||
settings.event_pull_request_approvals=合并请求批准
|
||||
settings.event_pull_request_merge=合并请求合并
|
||||
settings.event_header_workflow=工作流程事件
|
||||
settings.event_workflow_run=工作流运行
|
||||
settings.event_workflow_run_desc=Gitea 工作流队列中、等待中、正在进行或已完成的任务。
|
||||
settings.event_workflow_job=工作流任务
|
||||
settings.event_workflow_job_desc=Gitea 工作流队列中、等待中、正在进行或已完成的任务。
|
||||
settings.event_package=软件包
|
||||
@@ -2773,7 +2794,7 @@ error.broken_git_hook=此仓库的 Git 钩子似乎已损坏。 请按照 <a tar
|
||||
[graphs]
|
||||
component_loading=正在加载 %s...
|
||||
component_loading_failed=无法加载 %s
|
||||
component_loading_info=这可能需要一点…
|
||||
component_loading_info=这可能需要一点时间…
|
||||
component_failed_to_load=意外的错误发生了。
|
||||
code_frequency.what=代码频率
|
||||
contributors.what=贡献
|
||||
@@ -2802,6 +2823,7 @@ team_permission_desc=权限
|
||||
team_unit_desc=允许访问仓库单元
|
||||
team_unit_disabled=(已禁用)
|
||||
|
||||
form.name_been_taken=组织名称「%s」已经被占用。
|
||||
form.name_reserved=组织名称「%s」是保留的。
|
||||
form.name_pattern_not_allowed=组织名中不允许使用「%s」格式。
|
||||
form.create_org_not_allowed=此账号禁止创建组织
|
||||
@@ -2824,12 +2846,27 @@ settings.visibility.private_shortname=私有
|
||||
settings.update_settings=更新组织设置
|
||||
settings.update_setting_success=组织设置已更新。
|
||||
|
||||
settings.rename=修改组织名称
|
||||
settings.rename_desc=更改组织名称同时会更改组织的 URL 地址并释放旧的名称。
|
||||
settings.rename_success=组织 %[1]s 已成功重命名为 %[2]s。
|
||||
settings.rename_no_change=组织名称没有变化。
|
||||
settings.rename_new_org_name=新组织名称
|
||||
settings.rename_failed=由于内部错误,重命名组织失败
|
||||
settings.rename_notices_1=此操作 <strong>无法</strong> 被回滚。
|
||||
settings.rename_notices_2=在被人使用前,旧名称将会被重定向。
|
||||
|
||||
settings.update_avatar_success=组织头像已经更新。
|
||||
settings.delete=删除组织
|
||||
settings.delete_account=删除当前组织
|
||||
settings.delete_prompt=删除操作会永久清除该组织的信息,并且 <strong>不可恢复</strong>!
|
||||
settings.delete_prompt=删除操作会永久清除该组织的信息,并且 <strong>无法</strong> 恢复!
|
||||
settings.name_confirm=输入组织名称以确认:
|
||||
settings.delete_notices_1=此操作 <strong>无法</strong> 被回滚。
|
||||
settings.delete_notices_2=此操作将永久删除 <strong>%s</strong> 的所有<strong>仓库</strong>,包括 Git 数据、 工单、评论、百科和协作者的操作权限。
|
||||
settings.delete_notices_3=此操作将永久删除 <strong>%s</strong> 的所有 <strong>软件包</strong>。
|
||||
settings.delete_notices_4=此操作将永久删除 <strong>%s</strong> 的所有 <strong>项目</strong>。
|
||||
settings.confirm_delete_account=确认删除组织
|
||||
settings.delete_failed=由于内部错误,删除组织失败
|
||||
settings.delete_successful=组织 <b>%s</b> 已成功删除。
|
||||
settings.hooks_desc=在此处添加的 Web 钩子将会应用到该组织下的 <strong>所有仓库</strong>。
|
||||
|
||||
settings.labels_desc=添加能够被该组织下的 <strong>所有仓库</strong> 的工单使用的标签。
|
||||
@@ -3720,8 +3757,8 @@ none=还没有密钥。
|
||||
; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation
|
||||
creation.description=组织描述
|
||||
creation.name_placeholder=不区分大小写,仅限字母数字或下划线且不能以 GITEA_ 或 GITHUB_ 开头
|
||||
creation.value_placeholder=输入任何内容,开头和结尾的空白将会被忽略。
|
||||
creation.description_placeholder=输入简短描述(可选)。
|
||||
creation.value_placeholder=输入任何内容,开头和结尾的空白将会被忽略
|
||||
creation.description_placeholder=输入简短描述(可选)
|
||||
|
||||
save_success=密钥「%s」保存成功。
|
||||
save_failed=密钥保存失败。
|
||||
@@ -3806,6 +3843,7 @@ runs.no_runs=工作流尚未运行过。
|
||||
runs.empty_commit_message=(空白的提交消息)
|
||||
runs.expire_log_message=旧的日志已清除。
|
||||
runs.delete=删除工作流运行
|
||||
runs.cancel=取消工作流运行
|
||||
runs.delete.description=您确定要永久删除此工作流运行吗?此操作无法撤消。
|
||||
runs.not_done=此工作流运行尚未完成。
|
||||
runs.view_workflow_file=查看工作流文件
|
||||
|
@@ -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)()
|
||||
|
||||
|
Reference in New Issue
Block a user