Skip to content

Follow file symlinks in the UI to their target #28835

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions modules/git/tree_blob_nogogit.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,24 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
relpath = path.Clean(relpath)
parts := strings.Split(relpath, "/")
var err error

tree := t
for i, name := range parts {
if i == len(parts)-1 {
entries, err := tree.ListEntries()
if err != nil {
return nil, err
}
for _, v := range entries {
if v.Name() == name {
return v, nil
}
}
} else {
tree, err = tree.SubTree(name)
if err != nil {
return nil, err
}
for _, name := range parts[:len(parts)-1] {
tree, err = tree.SubTree(name)
if err != nil {
return nil, err
}
}

name := parts[len(parts)-1]
entries, err := tree.ListEntries()
if err != nil {
return nil, err
}
for _, v := range entries {
if v.Name() == name {
v.fullName = relpath
return v, nil
}
}
return nil, ErrNotExist{"", relpath}
Expand Down
10 changes: 10 additions & 0 deletions modules/git/tree_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
return entry, nil
}

// TryFollowingLinks attempts to follow the symlinks of this entry to the origin
// If that fails, it defaults to the entry itself
func (te *TreeEntry) TryFollowingLinks() *TreeEntry {
newEntry, err := te.FollowLinks()
if err != nil {
return te
}
return newEntry
}

// returns the Tree pointed to by this TreeEntry, or nil if this is not a tree
func (te *TreeEntry) Tree() *Tree {
t, err := te.ptree.repo.getTree(te.ID)
Expand Down
106 changes: 106 additions & 0 deletions modules/git/tree_entry_common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package git

import (
"testing"

"github.com/stretchr/testify/assert"
)

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.Equal(t, "foo/nar/hello", target.FullPath())
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.ErrorAs(t, err, ErrBadLink{})

// 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.ErrorAs(t, err, ErrBadLink{})

// 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.ErrorAs(t, err, ErrBadLink{})

// testing fix for short link bug
target, err = commit.Tree.GetTreeEntryByPath("foo/link_short")
assert.NoError(t, err)
_, err = target.FollowLink()
assert.ErrorAs(t, err, ErrBadLink{})
}

func TestTryFollowingLinks(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
list, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello")
assert.NoError(t, err)
assert.True(t, list.IsLink())

// should be able to dereference to target
target := list.TryFollowingLinks()
assert.NotEqual(t, target, list)
assert.Equal(t, "hello", target.Name())
assert.Equal(t, "foo/nar/hello", target.FullPath())
assert.False(t, target.IsLink())
assert.Equal(t, "b14df6442ea5a1b382985a6549b85d435376c351", target.ID.String())

// should default to original when called on normal file
link, err := commit.Tree.GetTreeEntryByPath("file1.txt")
assert.NoError(t, err)
target = link.TryFollowingLinks()
assert.Same(t, link, target)

// should default to original for broken links
link, err = commit.Tree.GetTreeEntryByPath("foo/broken_link")
assert.NoError(t, err)
assert.True(t, link.IsLink())
target = link.TryFollowingLinks()
assert.Same(t, link, target)

// should default to original for external links
link, err = commit.Tree.GetTreeEntryByPath("foo/outside_repo")
assert.NoError(t, err)
assert.True(t, link.IsLink())
target = link.TryFollowingLinks()
assert.Same(t, link, target)

// testing fix for short link bug
link, err = commit.Tree.GetTreeEntryByPath("foo/link_short")
assert.NoError(t, err)
target = link.TryFollowingLinks()
assert.Same(t, link, target)
}
4 changes: 4 additions & 0 deletions modules/git/tree_entry_gogit.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func (te *TreeEntry) Size() int64 {
return te.size
}

func (te *TreeEntry) FullPath() string {
return te.Name()
}

// IsSubModule if the entry is a sub module
func (te *TreeEntry) IsSubModule() bool {
return te.gogitTreeEntry.Mode == filemode.Submodule
Expand Down
10 changes: 7 additions & 3 deletions modules/git/tree_entry_nogogit.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ type TreeEntry struct {

// Name returns the name of the entry
func (te *TreeEntry) Name() string {
if te.fullName != "" {
return te.fullName
}
return te.name
}

Expand Down Expand Up @@ -59,6 +56,13 @@ func (te *TreeEntry) Size() int64 {
return te.size
}

func (te *TreeEntry) FullPath() string {
if te.fullName == "" {
return te.Name()
}
return te.fullName
}

// IsSubModule if the entry is a sub module
func (te *TreeEntry) IsSubModule() bool {
return te.entryMode == EntryModeCommit
Expand Down
47 changes: 0 additions & 47 deletions modules/git/tree_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,50 +53,3 @@ func TestEntriesCustomSort(t *testing.T) {
assert.Equal(t, "bcd", entries[6].Name())
assert.Equal(t, "abc", entries[7].Name())
}

func TestFollowLink(t *testing.T) {
r, err := openRepositoryWithDefaultContext("tests/repos/repo1_bare")
assert.NoError(t, err)
defer r.Close()

commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123")
assert.NoError(t, err)

// get the symlink
lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello")
assert.NoError(t, err)
assert.True(t, lnk.IsLink())

// should be able to dereference to target
target, err := lnk.FollowLink()
assert.NoError(t, err)
assert.Equal(t, "hello", target.Name())
assert.False(t, target.IsLink())
assert.Equal(t, "b14df6442ea5a1b382985a6549b85d435376c351", target.ID.String())

// should error when called on normal file
target, err = commit.Tree.GetTreeEntryByPath("file1.txt")
assert.NoError(t, err)
_, err = target.FollowLink()
assert.EqualError(t, err, "file1.txt: not a symlink")

// should error for broken links
target, err = commit.Tree.GetTreeEntryByPath("foo/broken_link")
assert.NoError(t, err)
assert.True(t, target.IsLink())
_, err = target.FollowLink()
assert.EqualError(t, err, "broken_link: broken link")

// should error for external links
target, err = commit.Tree.GetTreeEntryByPath("foo/outside_repo")
assert.NoError(t, err)
assert.True(t, target.IsLink())
_, err = target.FollowLink()
assert.EqualError(t, err, "outside_repo: points outside of repo")

// testing fix for short link bug
target, err = commit.Tree.GetTreeEntryByPath("foo/link_short")
assert.NoError(t, err)
_, err = target.FollowLink()
assert.EqualError(t, err, "link_short: broken link")
}
6 changes: 4 additions & 2 deletions templates/repo/view_list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
{{if $entry.IsDir}}
{{$subJumpablePathName := $entry.GetSubJumpablePathName}}
{{svg "octicon-file-directory-fill"}}
<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}">
<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" data-tooltip-content title="{{$subJumpablePathName}}">
{{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}}
{{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}}
{{if eq $subJumpablePathFieldLast 0}}
Expand All @@ -44,7 +44,9 @@
</a>
{{else}}
{{svg (printf "octicon-%s" (EntryIcon $entry))}}
<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a>
{{$symLinkEntry := $entry}}
{{if $entry.IsLink}}{{$symLinkEntry = $entry.TryFollowingLinks}}{{end}}
<a class="muted" href="{{$.TreeLink}}/{{$symLinkEntry.FullPath}}" data-tooltip-content title="{{$symLinkEntry.FullPath}}">{{$entry.Name}}</a>
{{end}}
{{end}}
</span>
Expand Down