aboutsummaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
authorKerwin Bryant <kerwin612@qq.com>2025-03-15 16:26:49 +0800
committerGitHub <noreply@github.com>2025-03-15 16:26:49 +0800
commit92f997ce6b2535c0c71a33ade290378a744c7224 (patch)
tree68295c5ebc8cb1412e8d88757fd529538da4e2c1 /services
parent926f0a19bec2fa075ee547dd8b405489caa9923e (diff)
downloadgitea-92f997ce6b2535c0c71a33ade290378a744c7224.tar.gz
gitea-92f997ce6b2535c0c71a33ade290378a744c7224.zip
Add file tree to file view page (#32721)
Resolve #29328 This pull request introduces a file tree on the left side when reviewing files of a repository. --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Diffstat (limited to 'services')
-rw-r--r--services/contexttest/context_tests.go26
-rw-r--r--services/repository/files/tree.go99
-rw-r--r--services/repository/files/tree_test.go49
3 files changed, 163 insertions, 11 deletions
diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go
index 98b8bdd63e..c895de3569 100644
--- a/services/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
+ git_module "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/session"
@@ -30,6 +31,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func mockRequest(t *testing.T, reqPath string) *http.Request {
@@ -85,7 +87,7 @@ func MockAPIContext(t *testing.T, reqPath string) (*context.APIContext, *httptes
base := context.NewBaseContext(resp, req)
base.Data = middleware.GetContextData(req.Context())
base.Locale = &translation.MockLocale{}
- ctx := &context.APIContext{Base: base}
+ ctx := &context.APIContext{Base: base, Repo: &context.Repository{}}
chiCtx := chi.NewRouteContext()
ctx.SetContextValue(chi.RouteCtxKey, chiCtx)
return ctx, resp
@@ -106,13 +108,13 @@ func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext,
// LoadRepo load a repo into a test context.
func LoadRepo(t *testing.T, ctx gocontext.Context, repoID int64) {
var doer *user_model.User
- repo := &context.Repository{}
+ var repo *context.Repository
switch ctx := ctx.(type) {
case *context.Context:
- ctx.Repo = repo
+ repo = ctx.Repo
doer = ctx.Doer
case *context.APIContext:
- ctx.Repo = repo
+ repo = ctx.Repo
doer = ctx.Doer
default:
assert.FailNow(t, "context is not *context.Context or *context.APIContext")
@@ -140,15 +142,17 @@ func LoadRepoCommit(t *testing.T, ctx gocontext.Context) {
}
gitRepo, err := gitrepo.OpenRepository(ctx, repo.Repository)
- assert.NoError(t, err)
+ require.NoError(t, err)
defer gitRepo.Close()
- branch, err := gitRepo.GetHEADBranch()
- assert.NoError(t, err)
- assert.NotNil(t, branch)
- if branch != nil {
- repo.Commit, err = gitRepo.GetBranchCommit(branch.Name)
- assert.NoError(t, err)
+
+ if repo.RefFullName == "" {
+ repo.RefFullName = git_module.RefNameFromBranch(repo.Repository.DefaultBranch)
+ }
+ if repo.RefFullName.IsPull() {
+ repo.BranchName = repo.RefFullName.ShortName()
}
+ repo.Commit, err = gitRepo.GetCommit(repo.RefFullName.String())
+ require.NoError(t, err)
}
// LoadUser load a user into a test context
diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go
index 6775186afd..9142416347 100644
--- a/services/repository/files/tree.go
+++ b/services/repository/files/tree.go
@@ -7,9 +7,13 @@ import (
"context"
"fmt"
"net/url"
+ "path"
+ "sort"
+ "strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
@@ -118,3 +122,98 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
}
return tree, nil
}
+
+func entryModeString(entryMode git.EntryMode) string {
+ switch entryMode {
+ case git.EntryModeBlob:
+ return "blob"
+ case git.EntryModeExec:
+ return "exec"
+ case git.EntryModeSymlink:
+ return "symlink"
+ case git.EntryModeCommit:
+ return "commit" // submodule
+ case git.EntryModeTree:
+ return "tree"
+ }
+ return "unknown"
+}
+
+type TreeViewNode struct {
+ EntryName string `json:"entryName"`
+ EntryMode string `json:"entryMode"`
+ FullPath string `json:"fullPath"`
+ SubmoduleURL string `json:"submoduleUrl,omitempty"`
+ Children []*TreeViewNode `json:"children,omitempty"`
+}
+
+func (node *TreeViewNode) sortLevel() int {
+ return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1)
+}
+
+func newTreeViewNodeFromEntry(ctx context.Context, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode {
+ node := &TreeViewNode{
+ EntryName: entry.Name(),
+ EntryMode: entryModeString(entry.Mode()),
+ FullPath: path.Join(parentDir, entry.Name()),
+ }
+
+ if node.EntryMode == "commit" {
+ if subModule, err := commit.GetSubModule(node.FullPath); err != nil {
+ log.Error("GetSubModule: %v", err)
+ } else if subModule != nil {
+ submoduleFile := git.NewCommitSubmoduleFile(subModule.URL, entry.ID.String())
+ webLink := submoduleFile.SubmoduleWebLink(ctx)
+ node.SubmoduleURL = webLink.CommitWebLink
+ }
+ }
+
+ return node
+}
+
+// sortTreeViewNodes list directory first and with alpha sequence
+func sortTreeViewNodes(nodes []*TreeViewNode) {
+ sort.Slice(nodes, func(i, j int) bool {
+ a, b := nodes[i].sortLevel(), nodes[j].sortLevel()
+ if a != b {
+ return a < b
+ }
+ return nodes[i].EntryName < nodes[j].EntryName
+ })
+}
+
+func listTreeNodes(ctx context.Context, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
+ entries, err := tree.ListEntries()
+ if err != nil {
+ return nil, err
+ }
+
+ subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/")
+ nodes := make([]*TreeViewNode, 0, len(entries))
+ for _, entry := range entries {
+ node := newTreeViewNodeFromEntry(ctx, commit, treePath, entry)
+ nodes = append(nodes, node)
+ if entry.IsDir() && subPathDirName == entry.Name() {
+ subTreePath := treePath + "/" + node.EntryName
+ if subTreePath[0] == '/' {
+ subTreePath = subTreePath[1:]
+ }
+ subNodes, err := listTreeNodes(ctx, commit, entry.Tree(), subTreePath, subPathRemaining)
+ if err != nil {
+ log.Error("listTreeNodes: %v", err)
+ } else {
+ node.Children = subNodes
+ }
+ }
+ }
+ sortTreeViewNodes(nodes)
+ return nodes, nil
+}
+
+func GetTreeViewNodes(ctx context.Context, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
+ entry, err := commit.GetTreeEntryByPath(treePath)
+ if err != nil {
+ return nil, err
+ }
+ return listTreeNodes(ctx, commit, entry.Tree(), treePath, subPath)
+}
diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go
index 0c60fddf7b..8ea54969ce 100644
--- a/services/repository/files/tree_test.go
+++ b/services/repository/files/tree_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/contexttest"
@@ -50,3 +51,51 @@ func TestGetTreeBySHA(t *testing.T) {
assert.EqualValues(t, expectedTree, tree)
}
+
+func TestGetTreeViewNodes(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+ ctx, _ := contexttest.MockContext(t, "user2/repo1")
+ ctx.Repo.RefFullName = git.RefNameFromBranch("sub-home-md-img-check")
+ contexttest.LoadRepo(t, ctx, 1)
+ contexttest.LoadRepoCommit(t, ctx)
+ contexttest.LoadUser(t, ctx, 2)
+ contexttest.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+
+ treeNodes, err := GetTreeViewNodes(ctx, ctx.Repo.Commit, "", "")
+ assert.NoError(t, err)
+ assert.Equal(t, []*TreeViewNode{
+ {
+ EntryName: "docs",
+ EntryMode: "tree",
+ FullPath: "docs",
+ },
+ }, treeNodes)
+
+ treeNodes, err = GetTreeViewNodes(ctx, ctx.Repo.Commit, "", "docs/README.md")
+ assert.NoError(t, err)
+ assert.Equal(t, []*TreeViewNode{
+ {
+ EntryName: "docs",
+ EntryMode: "tree",
+ FullPath: "docs",
+ Children: []*TreeViewNode{
+ {
+ EntryName: "README.md",
+ EntryMode: "blob",
+ FullPath: "docs/README.md",
+ },
+ },
+ },
+ }, treeNodes)
+
+ treeNodes, err = GetTreeViewNodes(ctx, ctx.Repo.Commit, "docs", "README.md")
+ assert.NoError(t, err)
+ assert.Equal(t, []*TreeViewNode{
+ {
+ EntryName: "README.md",
+ EntryMode: "blob",
+ FullPath: "docs/README.md",
+ },
+ }, treeNodes)
+}