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:
commit
ac87911f07
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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}))
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
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
|
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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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")
|
|
||||||
}
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 …".
|
||||||
|
|
||||||
|
@ -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=查看工作流文件
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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)()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user