aboutsummaryrefslogtreecommitdiffstats
path: root/services/repository
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/repository
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/repository')
-rw-r--r--services/repository/files/tree.go99
-rw-r--r--services/repository/files/tree_test.go49
2 files changed, 148 insertions, 0 deletions
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)
+}