fix: "Follow symlink" to work with arbitrary links (#12246)

This change introduces a Path method on the TreeEntry struct, that
collects the path by moving upwards in the tree.

The existing FollowSymlink(s) methods interface has been changed, the
previously returned string has been removed, as after the fix it wasn't
used anywhere.

Fixes: #9931

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12246
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
Gabor Pihaj 2026-04-27 23:54:21 +02:00 committed by Gusted
parent 93296305f9
commit 9977df96d5
5 changed files with 105 additions and 21 deletions

View file

@ -5,6 +5,7 @@
package git
import (
"fmt"
"io"
"sort"
"strings"
@ -136,11 +137,11 @@ func (te *TreeEntry) LinkTarget() (string, error) {
}
// FollowLink returns the entry pointed to by a symlink
func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) {
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
// read the link
lnk, err := te.LinkTarget()
if err != nil {
return nil, "", err
return nil, err
}
t := te.ptree
@ -151,35 +152,33 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) {
}
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)
if err != nil {
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, lnk, nil
return target, nil
}
// FollowLinks returns the entry ultimately pointed to by a symlink
func (te *TreeEntry) FollowLinks() (*TreeEntry, string, error) {
func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
if !te.IsLink() {
return nil, "", ErrBadLink{te.Name(), "not a symlink"}
return nil, ErrBadLink{te.Name(), "not a symlink"}
}
entry := te
entryLink := ""
for range 999 {
if entry.IsLink() {
next, link, err := entry.FollowLink()
entryLink = link
next, err := entry.FollowLink()
if err != nil {
return nil, "", err
return nil, err
}
if next.ID == entry.ID {
return nil, "", ErrBadLink{
return nil, ErrBadLink{
entry.Name(),
"recursive link",
}
@ -190,12 +189,12 @@ func (te *TreeEntry) FollowLinks() (*TreeEntry, string, error) {
}
}
if entry.IsLink() {
return nil, "", ErrBadLink{
return nil, ErrBadLink{
te.Name(),
"too many levels of symbolic links",
}
}
return entry, entryLink, nil
return entry, nil
}
// returns the Tree pointed to by this TreeEntry, or nil if this is not a tree
@ -208,6 +207,42 @@ func (te *TreeEntry) Tree() *Tree {
return t
}
// returns the calulcated path within the tree of this TreeEntry, or an error if it can be determined
func (te *TreeEntry) Path() (string, error) {
targetPath := te.Name()
parentTree := te.ptree
if parentTree == nil {
return "", fmt.Errorf("couldn't find the parent tree of the entry")
}
prevID := parentTree.ID
parentTree = parentTree.ptree
for parentTree != nil {
entries, err := parentTree.ListEntries()
if err != nil {
return "", fmt.Errorf("couldn't list entries: %v", err)
}
var matchingEntry *TreeEntry
for _, entry := range entries {
if entry.ID == prevID {
matchingEntry = entry
break
}
}
if matchingEntry == nil {
return "", fmt.Errorf("this shouldn't happen: couldn't find entry (ID: %s) in tree (ID: %s)", prevID, parentTree.ID)
}
targetPath = matchingEntry.name + "/" + targetPath
prevID = parentTree.ID
parentTree = parentTree.ptree
}
return targetPath, nil
}
// GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
func (te *TreeEntry) GetSubJumpablePathName() string {
if te.IsSubmodule() || !te.IsDir() {

View file

@ -0,0 +1,46 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git_test
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTreeEntry_Path(t *testing.T) {
repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "templates_repo"))
require.NoError(t, err)
defer repo.Close()
tests := []struct {
name string // description of this test case
path string
}{
{
name: "Top level dir",
path: ".forgejo",
},
{
name: "File in subdir",
path: ".forgejo/default_merge_message/MERGE_TEMPLATE.md",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tree, err := repo.GetTree("HEAD^{tree}")
require.NoError(t, err)
te, err := tree.GetTreeEntryByPath(tt.path)
require.NoError(t, err)
got, gotErr := te.Path()
require.NoError(t, gotErr, "Path() failed: %v", gotErr)
assert.Equal(t, tt.path, got, "Path() = %v, want %v", got, tt.path)
})
}
}