[FEAT] Show follow symlink button
- When a user goes opens a symlink file in Forgejo, the file would be rendered with the path of the symlink as content. - Add a button that is shown when the user opens a *valid* symlink file, which means that the symlink must have an valid path to an existent file and after 999 follows isn't a symlink anymore. - Return the relative path from the `FollowLink` functions, because Git really doesn't want to tell where an file is located based on the blob ID. - Adds integration tests.
This commit is contained in:
parent
0bba571f5b
commit
c63b52c126
6 changed files with 84 additions and 18 deletions
|
@ -174,7 +174,7 @@ func Int64sToStrings(ints []int64) []string {
|
||||||
func EntryIcon(entry *git.TreeEntry) string {
|
func EntryIcon(entry *git.TreeEntry) string {
|
||||||
switch {
|
switch {
|
||||||
case entry.IsLink():
|
case entry.IsLink():
|
||||||
te, err := entry.FollowLink()
|
te, _, err := entry.FollowLink()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug(err.Error())
|
log.Debug(err.Error())
|
||||||
return "file-symlink-file"
|
return "file-symlink-file"
|
||||||
|
|
|
@ -23,15 +23,15 @@ func (te *TreeEntry) Type() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// FollowLink returns the entry pointed to by a symlink
|
// FollowLink returns the entry pointed to by a symlink
|
||||||
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) {
|
||||||
if !te.IsLink() {
|
if !te.IsLink() {
|
||||||
return nil, ErrBadLink{te.Name(), "not a symlink"}
|
return nil, "", ErrBadLink{te.Name(), "not a symlink"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// read the link
|
// read the link
|
||||||
r, err := te.Blob().DataAsync()
|
r, err := te.Blob().DataAsync()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
closed := false
|
closed := false
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -42,7 +42,7 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
||||||
buf := make([]byte, te.Size())
|
buf := make([]byte, te.Size())
|
||||||
_, err = io.ReadFull(r, buf)
|
_, err = io.ReadFull(r, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
_ = r.Close()
|
_ = r.Close()
|
||||||
closed = true
|
closed = true
|
||||||
|
@ -56,33 +56,35 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return nil, ErrBadLink{te.Name(), "points outside of repo"}
|
return nil, "", ErrBadLink{te.Name(), "points outside of repo"}
|
||||||
}
|
}
|
||||||
|
|
||||||
target, err := t.GetTreeEntryByPath(lnk)
|
target, err := t.GetTreeEntryByPath(lnk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsErrNotExist(err) {
|
if IsErrNotExist(err) {
|
||||||
return nil, ErrBadLink{te.Name(), "broken link"}
|
return nil, "", ErrBadLink{te.Name(), "broken link"}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
return target, nil
|
return target, lnk, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FollowLinks returns the entry ultimately pointed to by a symlink
|
// FollowLinks returns the entry ultimately pointed to by a symlink
|
||||||
func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
|
func (te *TreeEntry) FollowLinks() (*TreeEntry, string, error) {
|
||||||
if !te.IsLink() {
|
if !te.IsLink() {
|
||||||
return nil, ErrBadLink{te.Name(), "not a symlink"}
|
return nil, "", ErrBadLink{te.Name(), "not a symlink"}
|
||||||
}
|
}
|
||||||
entry := te
|
entry := te
|
||||||
|
entryLink := ""
|
||||||
for i := 0; i < 999; i++ {
|
for i := 0; i < 999; i++ {
|
||||||
if entry.IsLink() {
|
if entry.IsLink() {
|
||||||
next, err := entry.FollowLink()
|
next, link, err := entry.FollowLink()
|
||||||
|
entryLink = link
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
if next.ID == entry.ID {
|
if next.ID == entry.ID {
|
||||||
return nil, ErrBadLink{
|
return nil, "", ErrBadLink{
|
||||||
entry.Name(),
|
entry.Name(),
|
||||||
"recursive link",
|
"recursive link",
|
||||||
}
|
}
|
||||||
|
@ -93,12 +95,12 @@ func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if entry.IsLink() {
|
if entry.IsLink() {
|
||||||
return nil, ErrBadLink{
|
return nil, "", ErrBadLink{
|
||||||
te.Name(),
|
te.Name(),
|
||||||
"too many levels of symbolic links",
|
"too many levels of symbolic links",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return entry, nil
|
return entry, entryLink, 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
|
||||||
|
|
|
@ -1205,6 +1205,7 @@ tag = Tag
|
||||||
released_this = released this
|
released_this = released this
|
||||||
file.title = %s at %s
|
file.title = %s at %s
|
||||||
file_raw = Raw
|
file_raw = Raw
|
||||||
|
file_follow = Follow Symlink
|
||||||
file_history = History
|
file_history = History
|
||||||
file_view_source = View Source
|
file_view_source = View Source
|
||||||
file_view_rendered = View Rendered
|
file_view_rendered = View Rendered
|
||||||
|
|
|
@ -114,7 +114,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
|
||||||
log.Debug("Potential readme file: %s", entry.Name())
|
log.Debug("Potential readme file: %s", 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()
|
target, _, err := entry.FollowLinks()
|
||||||
if err != nil && !git.IsErrBadLink(err) {
|
if err != nil && !git.IsErrBadLink(err) {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
|
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
|
||||||
|
@ -267,7 +267,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
|
||||||
func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
|
func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
|
||||||
target := readmeFile
|
target := readmeFile
|
||||||
if readmeFile != nil && readmeFile.IsLink() {
|
if readmeFile != nil && readmeFile.IsLink() {
|
||||||
target, _ = readmeFile.FollowLinks()
|
target, _, _ = readmeFile.FollowLinks()
|
||||||
}
|
}
|
||||||
if target == nil {
|
if target == nil {
|
||||||
// if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
|
// if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
|
||||||
|
@ -391,6 +391,15 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
ctx.Data["FileName"] = blob.Name()
|
ctx.Data["FileName"] = blob.Name()
|
||||||
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
||||||
|
|
||||||
|
if entry.IsLink() {
|
||||||
|
_, link, err := entry.FollowLinks()
|
||||||
|
// Errors should be allowed, because this shouldn't
|
||||||
|
// block rendering invalid symlink files.
|
||||||
|
if err == nil {
|
||||||
|
ctx.Data["SymlinkURL"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
|
commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetCommitByPath", err)
|
ctx.ServerError("GetCommitByPath", err)
|
||||||
|
|
|
@ -43,6 +43,9 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not .ReadmeInList}}
|
{{if not .ReadmeInList}}
|
||||||
<div class="ui buttons gt-mr-2">
|
<div class="ui buttons gt-mr-2">
|
||||||
|
{{if .SymlinkURL}}
|
||||||
|
<a class="ui mini basic button" href="{{$.SymlinkURL}}" data-kind="follow-symlink">{{ctx.Locale.Tr "repo.file_follow"}}</a>
|
||||||
|
{{end}}
|
||||||
<a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
|
<a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
|
||||||
{{if not .IsViewCommit}}
|
{{if not .IsViewCommit}}
|
||||||
<a class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
|
<a class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
|
||||||
|
|
|
@ -961,3 +961,54 @@ func TestRepoFilesList(t *testing.T) {
|
||||||
assert.EqualValues(t, []string{"Charlie", "alpha", "Beta", "delta", "licensa", "LICENSE", "licensz", "README.md", "zEta"}, filesList)
|
assert.EqualValues(t, []string{"Charlie", "alpha", "Beta", "delta", "licensa", "LICENSE", "licensz", "README.md", "zEta"}, filesList)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepoFollowSymlink(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
|
||||||
|
assertCase := func(t *testing.T, url, expectedSymlinkURL string, shouldExist bool) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", url)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
symlinkURL, ok := htmlDoc.Find(".file-actions .button[data-kind='follow-symlink']").Attr("href")
|
||||||
|
if shouldExist {
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.EqualValues(t, expectedSymlinkURL, symlinkURL)
|
||||||
|
} else {
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Normal", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
assertCase(t, "/user2/readme-test/src/branch/symlink/README.md?display=source", "/user2/readme-test/src/branch/symlink/some/other/path/awefulcake.txt", true)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Normal", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
assertCase(t, "/user2/readme-test/src/branch/symlink/some/README.txt", "/user2/readme-test/src/branch/symlink/some/other/path/awefulcake.txt", true)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Normal", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
assertCase(t, "/user2/readme-test/src/branch/symlink/up/back/down/down/README.md", "/user2/readme-test/src/branch/symlink/down/side/../left/right/../reelmein", true)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Broken symlink", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
assertCase(t, "/user2/readme-test/src/branch/fallbacks-broken-symlinks/docs/README", "", false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Loop symlink", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
assertCase(t, "/user2/readme-test/src/branch/symlink-loop/README.md", "", false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Not a symlink", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
assertCase(t, "/user2/readme-test/src/branch/master/README.md", "", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue