0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-21 21:04:40 +02:00

Merge branch 'main' into feature/enhanced-workflow-runs-api

This commit is contained in:
Brice Ruth 2025-06-30 21:27:38 -05:00 committed by GitHub
commit ac87911f07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 293 additions and 217 deletions

View File

@ -6,17 +6,17 @@ package fileicon
import "code.gitea.io/gitea/modules/git" import "code.gitea.io/gitea/modules/git"
type EntryInfo struct { type EntryInfo struct {
FullName string BaseName string
EntryMode git.EntryMode EntryMode git.EntryMode
SymlinkToMode git.EntryMode SymlinkToMode git.EntryMode
IsOpen bool IsOpen bool
} }
func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo { func EntryInfoFromGitTreeEntry(commit *git.Commit, fullPath string, gitEntry *git.TreeEntry) *EntryInfo {
ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()} ret := &EntryInfo{BaseName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
if gitEntry.IsLink() { if gitEntry.IsLink() {
if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() { if res, err := git.EntryFollowLink(commit, fullPath, gitEntry); err == nil && res.TargetEntry.IsDir() {
ret.SymlinkToMode = te.Mode() ret.SymlinkToMode = res.TargetEntry.Mode()
} }
} }
return ret return ret

View File

@ -5,7 +5,6 @@ package fileicon
import ( import (
"html/template" "html/template"
"path"
"strings" "strings"
"sync" "sync"
@ -134,7 +133,7 @@ func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string {
return "folder-git" return "folder-git"
} }
fileNameLower := strings.ToLower(path.Base(entry.FullName)) fileNameLower := strings.ToLower(entry.BaseName)
if entry.EntryMode.IsDir() { if entry.EntryMode.IsDir() {
if s, ok := m.rules.FolderNames[fileNameLower]; ok { if s, ok := m.rules.FolderNames[fileNameLower]; ok {
return s return s

View File

@ -20,8 +20,8 @@ func TestMain(m *testing.M) {
func TestFindIconName(t *testing.T) { func TestFindIconName(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
p := fileicon.DefaultMaterialIconProvider() 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{BaseName: "foo.php", EntryMode: git.EntryModeBlob}))
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "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{FullName: "foo.js", 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{FullName: "foo.vba", EntryMode: git.EntryModeBlob})) assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.vba", EntryMode: git.EntryModeBlob}))
} }

View File

@ -20,7 +20,8 @@ import (
// Commit represents a git commit. // Commit represents a git commit.
type Commit struct { 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 ID ObjectID // The ID of this commit object
Author *Signature Author *Signature
Committer *Signature Committer *Signature

View File

@ -32,22 +32,6 @@ func (err ErrNotExist) Unwrap() error {
return util.ErrNotExist 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. // ErrBranchNotExist represents a "BranchNotExist" kind of error.
type ErrBranchNotExist struct { type ErrBranchNotExist struct {
Name string Name string

View File

@ -11,7 +11,7 @@ import (
) )
// GetTreeEntryByPath get the tree entries according the sub dir // 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 { if len(relpath) == 0 {
return &TreeEntry{ return &TreeEntry{
ptree: t, ptree: t,
@ -21,27 +21,25 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
}, nil }, nil
} }
// FIXME: This should probably use git cat-file --batch to be a bit more efficient
relpath = path.Clean(relpath) relpath = path.Clean(relpath)
parts := strings.Split(relpath, "/") parts := strings.Split(relpath, "/")
var err error
tree := t tree := t
for i, name := range parts { for _, name := range parts[:len(parts)-1] {
if i == len(parts)-1 { tree, err = tree.SubTree(name)
entries, err := tree.ListEntries() if err != nil {
if err != nil { return nil, err
return nil, err }
} }
for _, v := range entries {
if v.Name() == name { name := parts[len(parts)-1]
return v, nil entries, err := tree.ListEntries()
} if err != nil {
} return nil, err
} else { }
tree, err = tree.SubTree(name) for _, v := range entries {
if err != nil { if v.Name() == name {
return nil, err return v, nil
}
} }
} }
return nil, ErrNotExist{"", relpath} return nil, ErrNotExist{"", relpath}

View File

@ -5,7 +5,7 @@
package git package git
import ( import (
"io" "path"
"sort" "sort"
"strings" "strings"
@ -24,77 +24,57 @@ func (te *TreeEntry) Type() string {
} }
} }
// FollowLink returns the entry pointed to by a symlink type EntryFollowResult struct {
func (te *TreeEntry) FollowLink() (*TreeEntry, error) { SymlinkContent string
if !te.IsLink() { TargetFullPath string
return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"} TargetEntry *TreeEntry
}
// 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
} }
// FollowLinks returns the entry ultimately pointed to by a symlink func EntryFollowLink(commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) {
func (te *TreeEntry) FollowLinks(optLimit ...int) (*TreeEntry, error) {
if !te.IsLink() { 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) limit := util.OptionalArg(optLimit, 10)
entry := te treeEntry, fullPath := firstTreeEntry, firstFullPath
for range limit { 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 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() { if treeEntry.IsLink() {
return nil, ErrSymlinkUnresolved{te.Name(), "too many levels of symbolic links"} 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 // returns the Tree pointed to by this TreeEntry, or nil if this is not a tree

View 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)
}
}

View File

@ -19,16 +19,12 @@ type TreeEntry struct {
gogitTreeEntry *object.TreeEntry gogitTreeEntry *object.TreeEntry
ptree *Tree ptree *Tree
size int64 size int64
sized bool sized bool
fullName string
} }
// Name returns the name of the entry // Name returns the name of the entry
func (te *TreeEntry) Name() string { func (te *TreeEntry) Name() string {
if te.fullName != "" {
return te.fullName
}
return te.gogitTreeEntry.Name return te.gogitTreeEntry.Name
} }
@ -55,7 +51,7 @@ func (te *TreeEntry) Size() int64 {
return te.size return te.size
} }
// IsSubModule if the entry is a sub module // IsSubModule if the entry is a submodule
func (te *TreeEntry) IsSubModule() bool { func (te *TreeEntry) IsSubModule() bool {
return te.gogitTreeEntry.Mode == filemode.Submodule return te.gogitTreeEntry.Mode == filemode.Submodule
} }

View File

@ -15,7 +15,7 @@ type EntryMode int
// one of these. // one of these.
const ( const (
// EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of // 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 EntryModeNoEntry EntryMode = 0o000000
EntryModeBlob EntryMode = 0o100644 EntryModeBlob EntryMode = 0o100644
@ -30,7 +30,7 @@ func (e EntryMode) String() string {
return strconv.FormatInt(int64(e), 8) 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 { func (e EntryMode) IsSubModule() bool {
return e == EntryModeCommit return e == EntryModeCommit
} }

View File

@ -57,7 +57,7 @@ func (te *TreeEntry) Size() int64 {
return te.size return te.size
} }
// IsSubModule if the entry is a sub module // IsSubModule if the entry is a submodule
func (te *TreeEntry) IsSubModule() bool { func (te *TreeEntry) IsSubModule() bool {
return te.entryMode.IsSubModule() return te.entryMode.IsSubModule()
} }

View File

@ -53,50 +53,3 @@ func TestEntriesCustomSort(t *testing.T) {
assert.Equal(t, "bcd", entries[6].Name()) assert.Equal(t, "bcd", entries[6].Name())
assert.Equal(t, "abc", entries[7].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")
}

View File

@ -69,7 +69,7 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
seen := map[plumbing.Hash]bool{} seen := map[plumbing.Hash]bool{}
walker := object.NewTreeWalker(t.gogitTree, true, seen) walker := object.NewTreeWalker(t.gogitTree, true, seen)
for { for {
fullName, entry, err := walker.Next() _, entry, err := walker.Next()
if err == io.EOF { if err == io.EOF {
break break
} }
@ -84,7 +84,6 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
ID: ParseGogitHash(entry.Hash), ID: ParseGogitHash(entry.Hash),
gogitTreeEntry: &entry, gogitTreeEntry: &entry,
ptree: t, ptree: t,
fullName: fullName,
} }
entries = append(entries, convertedEntry) entries = append(entries, convertedEntry)
} }

View File

@ -17,8 +17,8 @@ var (
ErrNotExist = errors.New("resource does not exist") // also implies HTTP 404 ErrNotExist = errors.New("resource does not exist") // also implies HTTP 404
ErrAlreadyExist = errors.New("resource already exists") // also implies HTTP 409 ErrAlreadyExist = errors.New("resource already exists") // also implies HTTP 409
// ErrUnprocessableContent implies HTTP 422, syntax of the request content was correct, // ErrUnprocessableContent implies HTTP 422, the syntax of the request content is correct,
// but server was unable to process the contained instructions // but the server is unable to process the contained instructions
ErrUnprocessableContent = errors.New("unprocessable content") ErrUnprocessableContent = errors.New("unprocessable content")
) )

View File

@ -2782,6 +2782,7 @@ topic.done = Done
topic.count_prompt = You cannot select more than 25 topics 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. 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.go_to_file = Go to file
find_file.no_matching = No matching file found find_file.no_matching = No matching file found

View File

@ -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_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_title=Cumaisc
pulls.cmd_instruction_merge_desc=Cumaisc na hathruithe agus nuashonrú ar Gitea. 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=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 …". 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 …".

View File

@ -420,8 +420,9 @@ remember_me=记住此设备
remember_me.compromised=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。 remember_me.compromised=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。
forgot_password_title=忘记密码 forgot_password_title=忘记密码
forgot_password=忘记密码? forgot_password=忘记密码?
need_account=需要一个帐户? need_account=需要一个帐户?
sign_up_now=还没账号?马上注册。 sign_up_tip=您正在系统中注册第一个帐户,它拥有管理员权限。请仔细记住您的用户名和密码。 如果您忘记了用户名或密码,请参阅 Gitea 文档以恢复账户。
sign_up_now=立即注册。
sign_up_successful=帐户创建成功。欢迎! sign_up_successful=帐户创建成功。欢迎!
confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 <b>%s</b>。请在下一个 %s 中检查您的收件箱以完成注册流程。 如果您的注册邮箱地址不正确,您可以重新登录并更改它。 confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 <b>%s</b>。请在下一个 %s 中检查您的收件箱以完成注册流程。 如果您的注册邮箱地址不正确,您可以重新登录并更改它。
must_change_password=更新您的密码 must_change_password=更新您的密码
@ -485,7 +486,7 @@ sspi_auth_failed=SSPI 认证失败
password_pwned=此密码出现在 <a target="_blank" rel="noopener noreferrer" href="%s">被盗密码</a> 列表上并且曾经被公开。 请使用另一个密码再试一次。 password_pwned=此密码出现在 <a target="_blank" rel="noopener noreferrer" href="%s">被盗密码</a> 列表上并且曾经被公开。 请使用另一个密码再试一次。
password_pwned_err=无法完成对 HaveIBeenPwned 的请求 password_pwned_err=无法完成对 HaveIBeenPwned 的请求
last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。 last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。
signin_passkey=使用密钥登录 signin_passkey=使用通行密钥登录
back_to_sign_in=返回登录页面 back_to_sign_in=返回登录页面
[mail] [mail]
@ -518,7 +519,7 @@ register_success=注册成功
issue_assigned.pull=@%[1]s 已将仓库 %[3]s 中的合并请求 %[2]s 指派给您 issue_assigned.pull=@%[1]s 已将仓库 %[3]s 中的合并请求 %[2]s 指派给您
issue_assigned.issue=@%[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.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_1=<b>@%[1]s</b> 推送了 %[3]d 个提交到 %[2]s
issue.action.push_n=<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 证书规则已关联到您的账号将允许完全访问您所有仓库。 principal_desc=这些 SSH 证书规则已关联到您的账号将允许完全访问您所有仓库。
gpg_desc=这些 GPG 公钥已经关联到您的账号。请妥善保管您的私钥因为他们将被用于认证提交。 gpg_desc=这些 GPG 公钥已经关联到您的账号。请妥善保管您的私钥因为他们将被用于认证提交。
ssh_helper=<strong>需要帮助?</strong> 请查看有关 <a href="%s">如何生成 SSH 密钥</a> 或 <a href="%s">常见 SSH 问题</a> 寻找答案。 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_key=增加 SSH 密钥
add_new_gpg_key=添加的 GPG 密钥 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' 开头 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=确实要永久删除此用户帐户吗? delete_account_desc=确实要永久删除此用户帐户吗?
email_notifications.enable=启用邮件通知 email_notifications.enable=启用邮件通知
email_notifications.onmention=只在被提到时邮件通知 email_notifications.onmention=仅被提及时通知
email_notifications.disable=停用邮件通知 email_notifications.disable=停用邮件通知
email_notifications.submit=邮件通知设置 email_notifications.submit=设置邮件通知
email_notifications.andyourown=和您自己的通知 email_notifications.andyourown=仅与您相关的通知
visibility=用户可见性 visibility=用户可见性
visibility.public=公开 visibility.public=公开
@ -1061,6 +1062,7 @@ fork_no_valid_owners=这个代码仓库无法被派生,因为没有有效的
fork.blocked_user=无法克隆仓库,因为您被仓库所有者屏蔽。 fork.blocked_user=无法克隆仓库,因为您被仓库所有者屏蔽。
use_template=使用此模板 use_template=使用此模板
open_with_editor=用 %s 打开 open_with_editor=用 %s 打开
download_zip=下载 ZIP download_zip=下载 ZIP
download_tar=下载 TAR.GZ download_tar=下载 TAR.GZ
download_bundle=下载 BUNDLE download_bundle=下载 BUNDLE
@ -1070,12 +1072,12 @@ repo_desc=描述
repo_desc_helper=输入简要描述 (可选) repo_desc_helper=输入简要描述 (可选)
repo_no_desc=无详细信息 repo_no_desc=无详细信息
repo_lang=语言 repo_lang=语言
repo_gitignore_helper=选择 .gitignore 模板 repo_gitignore_helper=选择 .gitignore 模板
repo_gitignore_helper_desc=从常见语言的模板列表中选择忽略跟踪的文件。默认情况下,由开发或构建工具生成的特殊文件都包含在 .gitignore 中。 repo_gitignore_helper_desc=从常见语言的模板列表中选择忽略跟踪的文件。默认情况下,由开发或构建工具生成的特殊文件都包含在 .gitignore 中。
issue_labels=工单标签 issue_labels=工单标签
issue_labels_helper=选择一个工单标签集 issue_labels_helper=选择一个工单标签集
license=授权许可 license=授权许可
license_helper=选择授权许可文件 license_helper=选择授权许可文件
license_helper_desc=许可证说明了其他人可以和不可以用您的代码做什么。不确定哪一个适合您的项目?见 <a target="_blank" rel="noopener noreferrer" href="%s">选择一个许可证</a> license_helper_desc=许可证说明了其他人可以和不可以用您的代码做什么。不确定哪一个适合您的项目?见 <a target="_blank" rel="noopener noreferrer" href="%s">选择一个许可证</a>
multiple_licenses=多许可证 multiple_licenses=多许可证
object_format=对象格式 object_format=对象格式
@ -1228,6 +1230,7 @@ migrate.migrating_issues=迁移工单
migrate.migrating_pulls=迁移合并请求 migrate.migrating_pulls=迁移合并请求
migrate.cancel_migrating_title=取消迁移 migrate.cancel_migrating_title=取消迁移
migrate.cancel_migrating_confirm=您想要取消此次迁移吗? migrate.cancel_migrating_confirm=您想要取消此次迁移吗?
migration_status=迁移状态
mirror_from=镜像自地址 mirror_from=镜像自地址
forked_from=派生自 forked_from=派生自
@ -1353,6 +1356,7 @@ editor.update=更新 %s
editor.delete=删除 %s editor.delete=删除 %s
editor.patch=应用补丁 editor.patch=应用补丁
editor.patching=打补丁: editor.patching=打补丁:
editor.fail_to_apply_patch=无法应用补丁
editor.new_patch=新补丁 editor.new_patch=新补丁
editor.commit_message_desc=添加一个可选的扩展描述... editor.commit_message_desc=添加一个可选的扩展描述...
editor.signoff_desc=在提交日志消息末尾添加签署人信息。 editor.signoff_desc=在提交日志消息末尾添加签署人信息。
@ -1372,6 +1376,7 @@ editor.branch_already_exists=此仓库已存在名为「%s」的分支。
editor.directory_is_a_file=目录名「%s」已作为文件名在此仓库中存在。 editor.directory_is_a_file=目录名「%s」已作为文件名在此仓库中存在。
editor.file_is_a_symlink=`「%s」是一个符号链接无法在 Web 编辑器中编辑` editor.file_is_a_symlink=`「%s」是一个符号链接无法在 Web 编辑器中编辑`
editor.filename_is_a_directory=文件名「%s」已作为目录名在此仓库中存在。 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_changed_while_editing=文件内容在您进行编辑时已经发生变动。<a target="_blank" rel="noopener noreferrer" href="%s">单击此处</a> 查看变动的具体内容,或者 <strong>再次提交</strong> 覆盖已发生的变动。
editor.file_already_exists=此仓库已经存在名为「%s」的文件。 editor.file_already_exists=此仓库已经存在名为「%s」的文件。
editor.commit_id_not_matching=提交 ID 与您开始编辑时的 ID 不匹配。请提交到补丁分支然后合并。 editor.commit_id_not_matching=提交 ID 与您开始编辑时的 ID 不匹配。请提交到补丁分支然后合并。
@ -1392,7 +1397,15 @@ editor.user_no_push_to_branch=用户不能推送到分支
editor.require_signed_commit=分支需要签名提交 editor.require_signed_commit=分支需要签名提交
editor.cherry_pick=拣选提交 %s 到: editor.cherry_pick=拣选提交 %s 到:
editor.revert=将 %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.desc=浏览代码修改历史
commits.commits=次代码提交 commits.commits=次代码提交
@ -1714,6 +1727,8 @@ issues.remove_time_estimate_at=删除预估时间 %s
issues.time_estimate_invalid=预计时间格式无效 issues.time_estimate_invalid=预计时间格式无效
issues.start_tracking_history=`开始工作 %s` issues.start_tracking_history=`开始工作 %s`
issues.tracker_auto_close=当此工单关闭时,自动停止计时器 issues.tracker_auto_close=当此工单关闭时,自动停止计时器
issues.stopwatch_already_stopped=此工单的计时器已经停止
issues.stopwatch_already_created=此工单的计时器已经存在
issues.tracking_already_started=`您已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!` issues.tracking_already_started=`您已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!`
issues.stop_tracking=停止计时器 issues.stop_tracking=停止计时器
issues.stop_tracking_history=工作 <b>%[1]s</b> 于 %[2]s 停止 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_checkout_desc=从您的仓库中检出一个新的分支并测试变更。
pulls.cmd_instruction_merge_title=合并 pulls.cmd_instruction_merge_title=合并
pulls.cmd_instruction_merge_desc=合并变更并更新到 Gitea 上 pulls.cmd_instruction_merge_desc=合并变更并更新到 Gitea 上
pulls.cmd_instruction_merge_warning=警告:此操作不能合并该合并请求,因为「自动检测手动合并」未启用
pulls.clear_merge_message=清除合并信息 pulls.clear_merge_message=清除合并信息
pulls.clear_merge_message_hint=清除合并消息只会删除提交消息内容,并保留生成的 Git 附加内容如「Co-Authored-By…」。 pulls.clear_merge_message_hint=清除合并消息只会删除提交消息内容,并保留生成的 Git 附加内容如「Co-Authored-By…」。
@ -2150,6 +2166,7 @@ settings.collaboration.write=可写权限
settings.collaboration.read=可读权限 settings.collaboration.read=可读权限
settings.collaboration.owner=所有者 settings.collaboration.owner=所有者
settings.collaboration.undefined=未定义 settings.collaboration.undefined=未定义
settings.collaboration.per_unit=单元权限
settings.hooks=Web 钩子 settings.hooks=Web 钩子
settings.githooks=管理 Git 钩子 settings.githooks=管理 Git 钩子
settings.basic_settings=基本设置 settings.basic_settings=基本设置
@ -2368,6 +2385,7 @@ settings.event_repository=仓库
settings.event_repository_desc=创建或删除仓库 settings.event_repository_desc=创建或删除仓库
settings.event_header_issue=工单事件 settings.event_header_issue=工单事件
settings.event_issues=工单 settings.event_issues=工单
settings.event_issues_desc=工单已打开、已关闭、已重新打开或已编辑。
settings.event_issue_assign=工单已指派 settings.event_issue_assign=工单已指派
settings.event_issue_assign_desc=工单已指派或取消指派。 settings.event_issue_assign_desc=工单已指派或取消指派。
settings.event_issue_label=工单增删标签 settings.event_issue_label=工单增删标签
@ -2378,6 +2396,7 @@ settings.event_issue_comment=工单评论
settings.event_issue_comment_desc=工单评论已创建、编辑或删除。 settings.event_issue_comment_desc=工单评论已创建、编辑或删除。
settings.event_header_pull_request=合并请求事件 settings.event_header_pull_request=合并请求事件
settings.event_pull_request=合并请求 settings.event_pull_request=合并请求
settings.event_pull_request_desc=合并请求已打开、关闭、重新打开或编辑。
settings.event_pull_request_assign=合并请求已指派 settings.event_pull_request_assign=合并请求已指派
settings.event_pull_request_assign_desc=合并请求已指派或取消指派。 settings.event_pull_request_assign_desc=合并请求已指派或取消指派。
settings.event_pull_request_label=合并请求增删标签 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_approvals=合并请求批准
settings.event_pull_request_merge=合并请求合并 settings.event_pull_request_merge=合并请求合并
settings.event_header_workflow=工作流程事件 settings.event_header_workflow=工作流程事件
settings.event_workflow_run=工作流运行
settings.event_workflow_run_desc=Gitea 工作流队列中、等待中、正在进行或已完成的任务。
settings.event_workflow_job=工作流任务 settings.event_workflow_job=工作流任务
settings.event_workflow_job_desc=Gitea 工作流队列中、等待中、正在进行或已完成的任务。 settings.event_workflow_job_desc=Gitea 工作流队列中、等待中、正在进行或已完成的任务。
settings.event_package=软件包 settings.event_package=软件包
@ -2773,7 +2794,7 @@ error.broken_git_hook=此仓库的 Git 钩子似乎已损坏。 请按照 <a tar
[graphs] [graphs]
component_loading=正在加载 %s... component_loading=正在加载 %s...
component_loading_failed=无法加载 %s component_loading_failed=无法加载 %s
component_loading_info=这可能需要一点 component_loading_info=这可能需要一点时间
component_failed_to_load=意外的错误发生了。 component_failed_to_load=意外的错误发生了。
code_frequency.what=代码频率 code_frequency.what=代码频率
contributors.what=贡献 contributors.what=贡献
@ -2802,6 +2823,7 @@ team_permission_desc=权限
team_unit_desc=允许访问仓库单元 team_unit_desc=允许访问仓库单元
team_unit_disabled=(已禁用) team_unit_disabled=(已禁用)
form.name_been_taken=组织名称「%s」已经被占用。
form.name_reserved=组织名称「%s」是保留的。 form.name_reserved=组织名称「%s」是保留的。
form.name_pattern_not_allowed=组织名中不允许使用「%s」格式。 form.name_pattern_not_allowed=组织名中不允许使用「%s」格式。
form.create_org_not_allowed=此账号禁止创建组织 form.create_org_not_allowed=此账号禁止创建组织
@ -2824,12 +2846,27 @@ settings.visibility.private_shortname=私有
settings.update_settings=更新组织设置 settings.update_settings=更新组织设置
settings.update_setting_success=组织设置已更新。 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.update_avatar_success=组织头像已经更新。
settings.delete=删除组织 settings.delete=删除组织
settings.delete_account=删除当前组织 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.confirm_delete_account=确认删除组织
settings.delete_failed=由于内部错误,删除组织失败
settings.delete_successful=组织 <b>%s</b> 已成功删除。
settings.hooks_desc=在此处添加的 Web 钩子将会应用到该组织下的 <strong>所有仓库</strong>。 settings.hooks_desc=在此处添加的 Web 钩子将会应用到该组织下的 <strong>所有仓库</strong>。
settings.labels_desc=添加能够被该组织下的 <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 ; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation
creation.description=组织描述 creation.description=组织描述
creation.name_placeholder=不区分大小写,仅限字母数字或下划线且不能以 GITEA_ 或 GITHUB_ 开头 creation.name_placeholder=不区分大小写,仅限字母数字或下划线且不能以 GITEA_ 或 GITHUB_ 开头
creation.value_placeholder=输入任何内容,开头和结尾的空白将会被忽略 creation.value_placeholder=输入任何内容,开头和结尾的空白将会被忽略
creation.description_placeholder=输入简短描述(可选) creation.description_placeholder=输入简短描述(可选)
save_success=密钥「%s」保存成功。 save_success=密钥「%s」保存成功。
save_failed=密钥保存失败。 save_failed=密钥保存失败。
@ -3806,6 +3843,7 @@ runs.no_runs=工作流尚未运行过。
runs.empty_commit_message=(空白的提交消息) runs.empty_commit_message=(空白的提交消息)
runs.expire_log_message=旧的日志已清除。 runs.expire_log_message=旧的日志已清除。
runs.delete=删除工作流运行 runs.delete=删除工作流运行
runs.cancel=取消工作流运行
runs.delete.description=您确定要永久删除此工作流运行吗?此操作无法撤消。 runs.delete.description=您确定要永久删除此工作流运行吗?此操作无法撤消。
runs.not_done=此工作流运行尚未完成。 runs.not_done=此工作流运行尚未完成。
runs.view_workflow_file=查看工作流文件 runs.view_workflow_file=查看工作流文件

View File

@ -6,6 +6,7 @@ package repo
import ( import (
"html/template" "html/template"
"net/http" "net/http"
"path"
"strings" "strings"
pull_model "code.gitea.io/gitea/models/pull" 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 := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
item.NameHash = git.HashFilePathForWebUI(item.FullName) 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 { switch file.HeadMode {
case git.EntryModeTree: case git.EntryModeTree:

View File

@ -12,6 +12,7 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"path"
"strings" "strings"
"time" "time"
@ -260,7 +261,9 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
renderedIconPool := fileicon.NewRenderedIconPool() renderedIconPool := fileicon.NewRenderedIconPool()
fileIcons := map[string]template.HTML{} fileIcons := map[string]template.HTML{}
for _, f := range files { 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()) fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.Data["FileIcons"] = fileIcons ctx.Data["FileIcons"] = fileIcons

View File

@ -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()) 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 { if err != nil {
ctx.ServerError("findReadmeFileInEntries", err) ctx.ServerError("findReadmeFileInEntries", err)
return return
@ -377,8 +377,8 @@ func prepareHomeTreeSideBarSwitch(ctx *context.Context) {
func redirectSrcToRaw(ctx *context.Context) bool { func redirectSrcToRaw(ctx *context.Context) bool {
// GitHub redirects a tree path with "?raw=1" to the raw path // GitHub redirects a tree path with "?raw=1" to the raw path
// It is useful to embed some raw contents into markdown files, // It is useful to embed some raw contents into Markdown files,
// then viewing the markdown in "src" path could embed the raw content correctly. // then viewing the Markdown in "src" path could embed the raw content correctly.
if ctx.Repo.TreePath != "" && ctx.FormBool("raw") { if ctx.Repo.TreePath != "" && ctx.FormBool("raw") {
ctx.Redirect(ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)) ctx.Redirect(ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath))
return true return true
@ -386,6 +386,20 @@ func redirectSrcToRaw(ctx *context.Context) bool {
return false 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 // Home render repository home page
func Home(ctx *context.Context) { func Home(ctx *context.Context) {
if handleRepoHomeFeed(ctx) { if handleRepoHomeFeed(ctx) {
@ -394,6 +408,7 @@ func Home(ctx *context.Context) {
if redirectSrcToRaw(ctx) { if redirectSrcToRaw(ctx) {
return return
} }
// Check whether the repo is viewable: not in migration, and the code unit should be enabled // 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. // Ideally the "feed" logic should be after this, but old code did so, so keep it as-is.
checkHomeCodeViewable(ctx) checkHomeCodeViewable(ctx)
@ -424,6 +439,10 @@ func Home(ctx *context.Context) {
return return
} }
if redirectFollowSymlink(ctx, entry) {
return
}
// prepare the tree path // prepare the tree path
var treeNames, paths []string var treeNames, paths []string
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()

View File

@ -32,15 +32,7 @@ import (
// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries() // entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
// //
// FIXME: There has to be a more efficient way of doing this // 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) { func findReadmeFileInEntries(ctx *context.Context, parentDir string, 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)
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/) docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
for _, entry := range entries { for _, entry := range entries {
if tryWellKnownDirs && entry.IsDir() { if tryWellKnownDirs && entry.IsDir() {
@ -62,16 +54,23 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
docsEntries[2] = entry 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 { 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 readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
if entry.IsLink() { if entry.IsLink() {
target, err := entry.FollowLinks() res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry)
if err != nil && !git.IsErrSymlinkUnresolved(err) { if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) {
return "", nil, err
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
readmeFiles[i] = entry readmeFiles[i] = entry
} }
} else { } else {
@ -80,6 +79,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
} }
} }
} }
var readmeFile *git.TreeEntry var readmeFile *git.TreeEntry
for _, f := range readmeFiles { for _, f := range readmeFiles {
if f != nil { if f != nil {
@ -103,7 +103,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
return "", nil, err return "", nil, err
} }
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false) subfolder, readmeFile, err := findReadmeFileInEntries(ctx, parentDir, childEntries, false)
if err != nil && !git.IsErrNotExist(err) { if err != nil && !git.IsErrNotExist(err) {
return "", nil, 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) { func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
target := readmeFile if readmeFile == nil {
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
return 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["RawFileLink"] = ""
ctx.Data["ReadmeInList"] = path.Join(subfolder, readmeFile.Name()) // the relative path to the readme file to the current tree path 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["ReadmeExist"] = true
ctx.Data["FileIsSymlink"] = readmeFile.IsLink() 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 { if err != nil {
ctx.ServerError("getFileReader", err) ctx.ServerError("getFileReader", err)
return return
@ -162,7 +169,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
defer dataRc.Close() defer dataRc.Close()
ctx.Data["FileIsText"] = fInfo.st.IsText() 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["FileSize"] = fInfo.fileSize
ctx.Data["IsLFSFile"] = fInfo.isLFSFile() 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{ rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder), CurrentTreePath: path.Dir(readmeFullPath),
}). }).
WithMarkupType(markupType). 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) ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
if err != nil { if err != nil {

View File

@ -161,7 +161,7 @@ func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.Re
FullPath: path.Join(parentDir, entry.Name()), FullPath: path.Join(parentDir, entry.Name()),
} }
entryInfo := fileicon.EntryInfoFromGitTreeEntry(entry) entryInfo := fileicon.EntryInfoFromGitTreeEntry(commit, node.FullPath, entry)
node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo) node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
if entryInfo.EntryMode.IsDir() { if entryInfo.EntryMode.IsDir() {
entryInfo.IsOpen = true entryInfo.IsOpen = true

View File

@ -41,6 +41,9 @@
</a> </a>
{{else}} {{else}}
<a class="entry-name" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a> <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}}
{{end}} {{end}}
</div> </div>

View File

@ -27,6 +27,7 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestRepoView(t *testing.T) { func TestRepoView(t *testing.T) {
@ -41,6 +42,7 @@ func TestRepoView(t *testing.T) {
t.Run("BlameFileInRepo", testBlameFileInRepo) t.Run("BlameFileInRepo", testBlameFileInRepo)
t.Run("ViewRepoDirectory", testViewRepoDirectory) t.Run("ViewRepoDirectory", testViewRepoDirectory)
t.Run("ViewRepoDirectoryReadme", testViewRepoDirectoryReadme) t.Run("ViewRepoDirectoryReadme", testViewRepoDirectoryReadme)
t.Run("ViewRepoSymlink", testViewRepoSymlink)
t.Run("MarkDownReadmeImage", testMarkDownReadmeImage) t.Run("MarkDownReadmeImage", testMarkDownReadmeImage)
t.Run("MarkDownReadmeImageSubfolder", testMarkDownReadmeImageSubfolder) t.Run("MarkDownReadmeImageSubfolder", testMarkDownReadmeImageSubfolder)
t.Run("GeneratedSourceLink", testGeneratedSourceLink) t.Run("GeneratedSourceLink", testGeneratedSourceLink)
@ -412,6 +414,21 @@ func testViewRepoDirectoryReadme(t *testing.T) {
missing("symlink-loop", "/user2/readme-test/src/branch/symlink-loop/") 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) { func testMarkDownReadmeImage(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()