This commit is contained in:
Kerwin Bryant 2025-03-11 11:39:52 -07:00 committed by GitHub
commit 802c208a67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1125 additions and 115 deletions

View File

@ -17,5 +17,6 @@ const (
// SignupIP is the IP address that the user signed up with
SignupIP = "signup.ip"
// SignupUserAgent is the user agent that the user signed up with
SignupUserAgent = "signup.user_agent"
SignupUserAgent = "signup.user_agent"
SettingsKeyShowFileViewTreeSidebar = "tree.show_file_view_tree_sidebar"
)

View File

@ -46,6 +46,8 @@ func RefBlame(ctx *context.Context) {
return
}
prepareHomeTreeSideBarSwitch(ctx)
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
treeLink := branchLink
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL()
@ -91,9 +93,16 @@ func RefBlame(ctx *context.Context) {
ctx.Data["FileSize"] = fileSize
ctx.Data["FileName"] = blob.Name()
var tplName templates.TplName
if ctx.FormBool("only_content") {
tplName = tplRepoViewContent
} else {
tplName = tplRepoView
}
if fileSize >= setting.UI.MaxDisplayFileSize {
ctx.Data["IsFileTooLarge"] = true
ctx.HTML(http.StatusOK, tplRepoHome)
ctx.HTML(http.StatusOK, tplName)
return
}
@ -121,7 +130,7 @@ func RefBlame(ctx *context.Context) {
renderBlame(ctx, result.Parts, commitNames)
ctx.HTML(http.StatusOK, tplRepoHome)
ctx.HTML(http.StatusOK, tplName)
}
type blameResult struct {

View File

@ -9,6 +9,7 @@ import (
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
@ -20,6 +21,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
@ -648,3 +650,21 @@ func PrepareBranchList(ctx *context.Context) {
}
ctx.Data["Branches"] = brs
}
type preferencesForm struct {
ShowFileViewTreeSidebar bool `json:"show_file_view_tree_sidebar"`
}
func UpdatePreferences(ctx *context.Context) {
form := &preferencesForm{}
if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
ctx.ServerError("DecodePreferencesForm", err)
return
}
if err := user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowFileViewTreeSidebar,
strconv.FormatBool(form.ShowFileViewTreeSidebar)); err != nil {
log.Error("SetUserSetting: %v", err)
}
ctx.JSONOK()
}

View File

@ -4,6 +4,7 @@
package repo
import (
"errors"
"net/http"
pull_model "code.gitea.io/gitea/models/pull"
@ -11,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/gitdiff"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/go-enry/go-enry/v2"
)
@ -84,3 +86,25 @@ func transformDiffTreeForUI(diffTree *gitdiff.DiffTree, filesViewedState map[str
return files
}
func Tree(ctx *context.Context) {
recursive := ctx.FormBool("recursive")
if ctx.Repo.RefFullName == "" {
ctx.ServerError("RefFullName", errors.New("ref_name is invalid"))
return
}
var results []*files_service.TreeViewNode
var err error
if !recursive {
results, err = files_service.GetTreeList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.RefFullName, false)
} else {
results, err = files_service.GetTreeInformation(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.RefFullName)
}
if err != nil {
ctx.ServerError("GetTreeInformation", err)
return
}
ctx.JSON(http.StatusOK, results)
}

View File

@ -47,12 +47,14 @@ import (
)
const (
tplRepoEMPTY templates.TplName = "repo/empty"
tplRepoHome templates.TplName = "repo/home"
tplRepoViewList templates.TplName = "repo/view_list"
tplWatchers templates.TplName = "repo/watchers"
tplForks templates.TplName = "repo/forks"
tplMigrating templates.TplName = "repo/migrate/migrating"
tplRepoEMPTY templates.TplName = "repo/empty"
tplRepoHome templates.TplName = "repo/home"
tplRepoView templates.TplName = "repo/view"
tplRepoViewContent templates.TplName = "repo/view_content"
tplRepoViewList templates.TplName = "repo/view_list"
tplWatchers templates.TplName = "repo/watchers"
tplForks templates.TplName = "repo/forks"
tplMigrating templates.TplName = "repo/migrate/migrating"
)
type fileInfo struct {

View File

@ -9,6 +9,7 @@ import (
"html/template"
"net/http"
"path"
"strconv"
"strings"
"time"
@ -17,6 +18,7 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
@ -328,6 +330,21 @@ func handleRepoHomeFeed(ctx *context.Context) bool {
return true
}
func prepareHomeTreeSideBarSwitch(ctx *context.Context) {
showFileViewTreeSidebar := true
if ctx.Doer != nil {
v, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowFileViewTreeSidebar, "true")
if err != nil {
log.Error("GetUserSetting: %v", err)
} else {
showFileViewTreeSidebar, _ = strconv.ParseBool(v)
}
}
ctx.Data["RepoPreferences"] = &preferencesForm{
ShowFileViewTreeSidebar: showFileViewTreeSidebar,
}
}
// Home render repository home page
func Home(ctx *context.Context) {
if handleRepoHomeFeed(ctx) {
@ -341,6 +358,8 @@ func Home(ctx *context.Context) {
return
}
prepareHomeTreeSideBarSwitch(ctx)
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
if len(ctx.Repo.Repository.Description) > 0 {
title += ": " + ctx.Repo.Repository.Description
@ -410,7 +429,13 @@ func Home(ctx *context.Context) {
}
}
ctx.HTML(http.StatusOK, tplRepoHome)
if ctx.FormBool("only_content") {
ctx.HTML(http.StatusOK, tplRepoViewContent)
} else if len(treeNames) != 0 {
ctx.HTML(http.StatusOK, tplRepoView)
} else {
ctx.HTML(http.StatusOK, tplRepoHome)
}
}
func RedirectRepoTreeToSrc(ctx *context.Context) {

View File

@ -1001,6 +1001,7 @@ func registerRoutes(m *web.Router) {
m.Get("/migrate", repo.Migrate)
m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
m.Get("/search", repo.SearchRepo)
m.Put("/preferences", repo.UpdatePreferences)
}, reqSignIn)
// end "/repo": create, migrate, search
@ -1175,6 +1176,11 @@ func registerRoutes(m *web.Router) {
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList)
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeList)
})
m.Group("/tree", func() {
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.Tree)
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.Tree)
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.Tree)
})
m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff)
m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists).
Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).

View File

@ -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,396 @@ 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 {
Name string `json:"name"`
Type string `json:"type"`
Path string `json:"path"`
SubModuleURL string `json:"sub_module_url,omitempty"`
Children []*TreeViewNode `json:"children,omitempty"`
}
func (node *TreeViewNode) sortLevel() int {
switch node.Type {
case "tree", "commit":
return 0
default:
return 1
}
}
func newTreeViewNodeFromEntry(ctx context.Context, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode {
node := &TreeViewNode{
Name: entry.Name(),
Type: entryModeString(entry.Mode()),
Path: path.Join(parentDir, entry.Name()),
}
if node.Type == "commit" {
if subModule, err := commit.GetSubModule(node.Path); 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 {
if nodes[i].sortLevel() != nodes[j].sortLevel() {
return nodes[i].sortLevel() < nodes[j].sortLevel()
}
return nodes[i].Name < nodes[j].Name
})
}
/*
Example 1: (path: /)
GET /repo/name/tree/
resp:
[{
"name": "d1",
"type": "commit",
"path": "d1",
"sub_module_url": "https://gitea.com/gitea/awesome-gitea/tree/887fe27678dced0bd682923b30b2d979575d35d6"
},{
"name": "d2",
"type": "symlink",
"path": "d2"
},{
"name": "d3",
"type": "tree",
"path": "d3"
},{
"name": "f1",
"type": "blob",
"path": "f1"
},]
Example 2: (path: d3)
GET /repo/name/tree/d3
resp:
[{
"name": "d3d1",
"type": "tree",
"path": "d3/d3d1"
}]
Example 3: (path: d3/d3d1)
GET /repo/name/tree/d3/d3d1
resp:
[{
"name": "d3d1f1",
"type": "blob",
"path": "d3/d3d1/d3d1f1"
},{
"name": "d3d1f2",
"type": "blob",
"path": "d3/d3d1/d3d1f2"
}]
*/
func GetTreeList(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, treePath string, ref git.RefName, recursive bool) ([]*TreeViewNode, error) {
if repo.IsEmpty {
return nil, nil
}
if ref == "" {
ref = git.RefNameFromBranch(repo.DefaultBranch)
}
// Check that the path given in opts.treePath is valid (not a git path)
cleanTreePath := CleanUploadFileName(treePath)
if cleanTreePath == "" && treePath != "" {
return nil, ErrFilenameInvalid{
Path: treePath,
}
}
treePath = cleanTreePath
// Get the commit object for the ref
commit, err := gitRepo.GetCommit(ref.String())
if err != nil {
return nil, err
}
entry, err := commit.GetTreeEntryByPath(treePath)
if err != nil {
return nil, err
}
// If the entry is a file, an exception will be thrown.
// This is because this interface is specifically designed for expanding folders and only supports the retrieval and return of the file list within a folder.
if entry.Type() != "tree" {
return nil, fmt.Errorf("%s is not a tree", treePath)
}
gitTree, err := commit.SubTree(treePath)
if err != nil {
return nil, err
}
var entries git.Entries
if recursive {
entries, err = gitTree.ListEntriesRecursiveFast()
} else {
entries, err = gitTree.ListEntries()
}
if err != nil {
return nil, err
}
var treeViewNodes []*TreeViewNode
mapTree := make(map[string][]*TreeViewNode)
for _, e := range entries {
subTreePath := path.Join(treePath, e.Name())
if strings.Contains(e.Name(), "/") {
dirName := path.Dir(e.Name())
mapTree[dirName] = append(mapTree[dirName], &TreeViewNode{
Name: path.Base(e.Name()),
Type: entryModeString(e.Mode()),
Path: subTreePath,
})
} else {
treeViewNodes = append(treeViewNodes, &TreeViewNode{
Name: e.Name(),
Type: entryModeString(e.Mode()),
Path: subTreePath,
})
}
}
for _, node := range treeViewNodes {
if node.Type == "tree" {
node.Children = mapTree[node.Path]
sortTreeViewNodes(node.Children)
}
}
sortTreeViewNodes(treeViewNodes)
return treeViewNodes, nil
}
// GetTreeInformation returns the first level directories and files and all the trees of the path to treePath.
// If treePath is a directory, list all subdirectories and files of treePath.
/*
Example 1: (path: /)
GET /repo/name/tree/?recursive=true
resp:
[{
"name": "d1",
"type": "commit",
"path": "d1",
"sub_module_url": "https://gitea.com/gitea/awesome-gitea/tree/887fe27678dced0bd682923b30b2d979575d35d6"
},{
"name": "d2",
"type": "symlink",
"path": "d2"
},{
"name": "d3",
"type": "tree",
"path": "d3"
},{
"name": "f1",
"type": "blob",
"path": "f1"
},]
Example 2: (path: d3)
GET /repo/name/tree/d3?recursive=true
resp:
[{
"name": "d1",
"type": "commit",
"path": "d1",
"sub_module_url": "https://gitea.com/gitea/awesome-gitea/tree/887fe27678dced0bd682923b30b2d979575d35d6"
},{
"name": "d2",
"type": "symlink",
"path": "d2"
},{
"name": "d3",
"type": "tree",
"path": "d3",
"children": [{
"name": "d3d1",
"type": "tree",
"path": "d3/d3d1"
}]
},{
"name": "f1",
"type": "blob",
"path": "f1"
},]
Example 3: (path: d3/d3d1)
GET /repo/name/tree/d3/d3d1?recursive=true
resp:
[{
"name": "d1",
"type": "commit",
"path": "d1",
"sub_module_url": "https://gitea.com/gitea/awesome-gitea/tree/887fe27678dced0bd682923b30b2d979575d35d6"
},{
"name": "d2",
"type": "symlink",
"path": "d2"
},{
"name": "d3",
"type": "tree",
"path": "d3",
"children": [{
"name": "d3d1",
"type": "tree",
"path": "d3/d3d1",
"children": [{
"name": "d3d1f1",
"type": "blob",
"path": "d3/d3d1/d3d1f1"
},{
"name": "d3d1f2",
"type": "blob",
"path": "d3/d3d1/d3d1f2"
}]
}]
},{
"name": "f1",
"type": "blob",
"path": "f1"
},]
Example 4: (path: d2/d2f1)
GET /repo/name/tree/d2/d2f1?recursive=true
resp:
[{
"name": "d1",
"type": "commit",
"path": "d1",
"sub_module_url": "https://gitea.com/gitea/awesome-gitea/tree/887fe27678dced0bd682923b30b2d979575d35d6"
},{
"name": "d2",
"type": "tree",
"path": "d2",
"children": [{
"name": "d2f1",
"type": "blob",
"path": "d2/d2f1"
}]
},{
"name": "d3",
"type": "tree",
"path": "d3"
},{
"name": "f1",
"type": "blob",
"path": "f1"
},]
*/
func GetTreeInformation(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, treePath string, ref git.RefName) ([]*TreeViewNode, error) {
if repo.IsEmpty {
return nil, nil
}
if ref == "" {
ref = git.RefNameFromBranch(repo.DefaultBranch)
}
// Check that the path given in opts.treePath is valid (not a git path)
cleanTreePath := CleanUploadFileName(treePath)
if cleanTreePath == "" && treePath != "" {
return nil, ErrFilenameInvalid{
Path: treePath,
}
}
treePath = cleanTreePath
// Get the commit object for the ref
commit, err := gitRepo.GetCommit(ref.String())
if err != nil {
return nil, err
}
// get root entries
rootEntries, err := commit.ListEntries()
if err != nil {
return nil, err
}
dir := treePath
if dir != "" {
lastDirEntry, err := commit.GetTreeEntryByPath(treePath)
if err != nil {
return nil, err
}
if lastDirEntry.IsRegular() {
// path.Dir cannot correctly handle .xxx file
dir, _ = path.Split(treePath)
dir = strings.TrimRight(dir, "/")
}
}
treeViewNodes := make([]*TreeViewNode, 0, len(rootEntries))
fields := strings.Split(dir, "/")
var parentNode *TreeViewNode
for _, entry := range rootEntries {
node := newTreeViewNodeFromEntry(ctx, commit, "", entry)
treeViewNodes = append(treeViewNodes, node)
if dir != "" && fields[0] == entry.Name() {
parentNode = node
}
}
sortTreeViewNodes(treeViewNodes)
if dir == "" || parentNode == nil {
return treeViewNodes, nil
}
for i := 1; i < len(fields); i++ {
parentNode.Children = []*TreeViewNode{
{
Name: fields[i],
Type: "tree",
Path: path.Join(fields[:i+1]...),
},
}
parentNode = parentNode.Children[0]
}
tree, err := commit.Tree.SubTree(dir)
if err != nil {
return nil, err
}
entries, err := tree.ListEntries()
if err != nil {
return nil, err
}
for _, entry := range entries {
parentNode.Children = append(parentNode.Children, newTreeViewNodeFromEntry(ctx, commit, dir, entry))
}
sortTreeViewNodes(parentNode.Children)
return treeViewNodes, nil
}

View File

@ -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,139 @@ func TestGetTreeBySHA(t *testing.T) {
assert.EqualValues(t, expectedTree, tree)
}
func Test_GetTreeList(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx1, _ := contexttest.MockContext(t, "user2/repo1")
contexttest.LoadRepo(t, ctx1, 1)
contexttest.LoadRepoCommit(t, ctx1)
contexttest.LoadUser(t, ctx1, 2)
contexttest.LoadGitRepo(t, ctx1)
defer ctx1.Repo.GitRepo.Close()
refName := git.RefNameFromBranch(ctx1.Repo.Repository.DefaultBranch)
treeList, err := GetTreeList(ctx1, ctx1.Repo.Repository, ctx1.Repo.GitRepo, "", refName, true)
assert.NoError(t, err)
assert.Len(t, treeList, 1)
assert.EqualValues(t, "README.md", treeList[0].Name)
assert.EqualValues(t, "README.md", treeList[0].Path)
assert.EqualValues(t, "blob", treeList[0].Type)
assert.Empty(t, treeList[0].Children)
ctx2, _ := contexttest.MockContext(t, "org3/repo3")
contexttest.LoadRepo(t, ctx2, 3)
contexttest.LoadRepoCommit(t, ctx2)
contexttest.LoadUser(t, ctx2, 2)
contexttest.LoadGitRepo(t, ctx2)
defer ctx2.Repo.GitRepo.Close()
refName = git.RefNameFromBranch(ctx2.Repo.Repository.DefaultBranch)
treeList, err = GetTreeList(ctx2, ctx2.Repo.Repository, ctx2.Repo.GitRepo, "", refName, true)
assert.NoError(t, err)
assert.Len(t, treeList, 2)
assert.EqualValues(t, "doc", treeList[0].Name)
assert.EqualValues(t, "doc", treeList[0].Path)
assert.EqualValues(t, "tree", treeList[0].Type)
assert.Len(t, treeList[0].Children, 1)
assert.EqualValues(t, "doc.md", treeList[0].Children[0].Name)
assert.EqualValues(t, "doc/doc.md", treeList[0].Children[0].Path)
assert.EqualValues(t, "blob", treeList[0].Children[0].Type)
assert.Empty(t, treeList[0].Children[0].Children)
assert.EqualValues(t, "README.md", treeList[1].Name)
assert.EqualValues(t, "README.md", treeList[1].Path)
assert.EqualValues(t, "blob", treeList[1].Type)
assert.Empty(t, treeList[1].Children)
}
func Test_GetTreeInformation(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx1, _ := contexttest.MockContext(t, "user2/repo1")
contexttest.LoadRepo(t, ctx1, 1)
contexttest.LoadRepoCommit(t, ctx1)
contexttest.LoadUser(t, ctx1, 2)
contexttest.LoadGitRepo(t, ctx1)
defer ctx1.Repo.GitRepo.Close()
refName := git.RefNameFromBranch(ctx1.Repo.Repository.DefaultBranch)
treeList, err := GetTreeInformation(ctx1, ctx1.Repo.Repository, ctx1.Repo.GitRepo, "", refName)
assert.NoError(t, err)
assert.Len(t, treeList, 1)
assert.EqualValues(t, "README.md", treeList[0].Name)
assert.EqualValues(t, "README.md", treeList[0].Path)
assert.EqualValues(t, "blob", treeList[0].Type)
assert.Empty(t, treeList[0].Children)
treeList, err = GetTreeInformation(ctx1, ctx1.Repo.Repository, ctx1.Repo.GitRepo, "README.md", refName)
assert.NoError(t, err)
assert.Len(t, treeList, 1)
assert.EqualValues(t, "README.md", treeList[0].Name)
assert.EqualValues(t, "README.md", treeList[0].Path)
assert.EqualValues(t, "blob", treeList[0].Type)
assert.Empty(t, treeList[0].Children)
ctx2, _ := contexttest.MockContext(t, "org3/repo3")
contexttest.LoadRepo(t, ctx2, 3)
contexttest.LoadRepoCommit(t, ctx2)
contexttest.LoadUser(t, ctx2, 2)
contexttest.LoadGitRepo(t, ctx2)
defer ctx2.Repo.GitRepo.Close()
refName = git.RefNameFromBranch(ctx2.Repo.Repository.DefaultBranch)
treeList, err = GetTreeInformation(ctx2, ctx2.Repo.Repository, ctx2.Repo.GitRepo, "", refName)
assert.NoError(t, err)
assert.Len(t, treeList, 2)
assert.EqualValues(t, "doc", treeList[0].Name)
assert.EqualValues(t, "doc", treeList[0].Path)
assert.EqualValues(t, "tree", treeList[0].Type)
assert.Empty(t, treeList[0].Children)
assert.EqualValues(t, "README.md", treeList[1].Name)
assert.EqualValues(t, "README.md", treeList[1].Path)
assert.EqualValues(t, "blob", treeList[1].Type)
assert.Empty(t, treeList[1].Children)
treeList, err = GetTreeInformation(ctx2, ctx2.Repo.Repository, ctx2.Repo.GitRepo, "doc", refName)
assert.NoError(t, err)
assert.Len(t, treeList, 2)
assert.EqualValues(t, "doc", treeList[0].Name)
assert.EqualValues(t, "doc", treeList[0].Path)
assert.EqualValues(t, "tree", treeList[0].Type)
assert.Len(t, treeList[0].Children, 1)
assert.EqualValues(t, "doc.md", treeList[0].Children[0].Name)
assert.EqualValues(t, "doc/doc.md", treeList[0].Children[0].Path)
assert.EqualValues(t, "blob", treeList[0].Children[0].Type)
assert.Empty(t, treeList[0].Children[0].Children)
assert.EqualValues(t, "README.md", treeList[1].Name)
assert.EqualValues(t, "README.md", treeList[1].Path)
assert.EqualValues(t, "blob", treeList[1].Type)
assert.Empty(t, treeList[1].Children)
treeList, err = GetTreeInformation(ctx2, ctx2.Repo.Repository, ctx2.Repo.GitRepo, "doc/doc.md", refName)
assert.NoError(t, err)
assert.Len(t, treeList, 2)
assert.EqualValues(t, "doc", treeList[0].Name)
assert.EqualValues(t, "doc", treeList[0].Path)
assert.EqualValues(t, "tree", treeList[0].Type)
assert.Len(t, treeList[0].Children, 1)
assert.EqualValues(t, "doc.md", treeList[0].Children[0].Name)
assert.EqualValues(t, "doc/doc.md", treeList[0].Children[0].Path)
assert.EqualValues(t, "blob", treeList[0].Children[0].Type)
assert.Empty(t, treeList[0].Children[0].Children)
assert.EqualValues(t, "README.md", treeList[1].Name)
assert.EqualValues(t, "README.md", treeList[1].Path)
assert.EqualValues(t, "blob", treeList[1].Type)
assert.Empty(t, treeList[1].Children)
}

View File

@ -1,7 +1,8 @@
{{template "base/head" .}}
{{$showSidebar := and (not .TreeNames) (not .HideRepoInfo) (not .IsBlame)}}
<div role="main" aria-label="{{.Title}}" class="page-content repository file list {{if .IsBlame}}blame{{end}}">
{{template "repo/header" .}}
<div class="ui container {{if .IsBlame}}fluid padded{{end}}">
<div class="ui container {{if or .TreeNames .IsBlame}}fluid padded{{end}}">
{{template "base/alert" .}}
{{if .Repository.IsArchived}}
@ -16,112 +17,9 @@
{{template "repo/code/recently_pushed_new_branches" .}}
{{$treeNamesLen := len .TreeNames}}
{{$isTreePathRoot := eq $treeNamesLen 0}}
{{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}}
<div class="{{Iif $showSidebar "repo-grid-filelist-sidebar" "repo-grid-filelist-only"}}">
<div class="repo-home-filelist">
{{template "repo/sub_menu" .}}
<div class="repo-button-row">
<div class="repo-button-row-left">
{{- /* for repo home (default branch) and /owner/repo/src/{RefType}/{RefShortName} */ -}}
{{- template "repo/branch_dropdown" dict
"Repository" .Repository
"ShowTabBranches" true
"ShowTabTags" true
"CurrentRefType" .RefFullName.RefType
"CurrentRefShortName" .RefFullName.ShortName
"CurrentTreePath" .TreePath
"RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}"
"AllowCreateNewRef" .CanCreateBranch
"ShowViewAllRefsEntry" true
-}}
{{if and .CanCompareOrPull .RefFullName.IsBranch (not .Repository.IsArchived)}}
{{$cmpBranch := ""}}
{{if ne .Repository.ID .BaseRepo.ID}}
{{$cmpBranch = printf "%s/%s:" (.Repository.OwnerName|PathEscape) (.Repository.Name|PathEscape)}}
{{end}}
{{$cmpBranch = print $cmpBranch (.BranchName|PathEscapeSegments)}}
{{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}}
<a id="new-pull-request" role="button" class="ui compact basic button" href="{{$compareLink}}"
data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}">
{{svg "octicon-git-pull-request"}}
</a>
{{end}}
<!-- Show go to file if on home page -->
{{if $isTreePathRoot}}
<a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
{{end}}
{{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
{{ctx.Locale.Tr "repo.editor.add_file"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<a class="item" href="{{.RepoLink}}/_new/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
{{ctx.Locale.Tr "repo.editor.new_file"}}
</a>
{{if .RepositoryUploadEnabled}}
<a class="item" href="{{.RepoLink}}/_upload/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
{{ctx.Locale.Tr "repo.editor.upload_file"}}
</a>
{{end}}
<a class="item" href="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
{{ctx.Locale.Tr "repo.editor.patch"}}
</a>
</div>
</button>
{{end}}
{{if and $isTreePathRoot .Repository.IsTemplate}}
<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}">
{{ctx.Locale.Tr "repo.use_template"}}
</a>
{{end}}
{{if not $isTreePathRoot}}
{{$treeNameIdxLast := Eval $treeNamesLen "-" 1}}
<span class="breadcrumb repo-path tw-ml-1">
<a class="section" href="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
{{- range $i, $v := .TreeNames -}}
<span class="breadcrumb-divider">/</span>
{{- if eq $i $treeNameIdxLast -}}
<span class="active section" title="{{$v}}">{{$v}}</span>
<button class="btn interact-fg tw-mx-1" data-clipboard-text="{{$.TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "copy_path"}}">{{svg "octicon-copy" 14}}</button>
{{- else -}}
{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{$v}}</a></span>
{{- end -}}
{{- end -}}
</span>
{{end}}
</div>
<div class="repo-button-row-right">
<!-- Only show clone panel in repository home page -->
{{if $isTreePathRoot}}
{{template "repo/clone_panel" .}}
{{end}}
{{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
<a class="ui button" href="{{.RepoLink}}/commits/{{.RefTypeNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
</a>
{{end}}
</div>
</div>
{{if .IsViewFile}}
{{template "repo/view_file" .}}
{{else if .IsBlame}}
{{template "repo/blame" .}}
{{else}}{{/* IsViewDirectory */}}
{{if $isTreePathRoot}}
{{template "repo/code/upstream_diverging_info" .}}
{{end}}
{{template "repo/view_list" .}}
{{if and .ReadmeExist (or .IsMarkup .IsPlainText)}}
{{template "repo/view_file" .}}
{{end}}
{{end}}
{{template "repo/view_content" .}}
</div>
{{if $showSidebar}}

View File

@ -0,0 +1,12 @@
{{template "repo/branch_dropdown" dict
"Repository" .ctxData.Repository
"ShowTabBranches" true
"ShowTabTags" true
"CurrentRefType" .ctxData.RefFullName.RefType
"CurrentRefShortName" .ctxData.RefFullName.ShortName
"CurrentTreePath" .ctxData.TreePath
"RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}"
"AllowCreateNewRef" .ctxData.CanCreateBranch
"ShowViewAllRefsEntry" true
"ContainerClasses" .containerClasses
}}

27
templates/repo/view.tmpl Normal file
View File

@ -0,0 +1,27 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository file list {{if .IsBlame}}blame{{end}}">
{{template "repo/header" .}}
<div class="ui container {{if or .TreeNames .IsBlame}}fluid padded{{end}}">
{{template "base/alert" .}}
{{if .Repository.IsArchived}}
<div class="ui warning message tw-text-center">
{{if .Repository.ArchivedUnix.IsZero}}
{{ctx.Locale.Tr "repo.archive.title"}}
{{else}}
{{ctx.Locale.Tr "repo.archive.title_date" (DateUtils.AbsoluteLong .Repository.ArchivedUnix)}}
{{end}}
</div>
{{end}}
{{template "repo/code/recently_pushed_new_branches" .}}
<div class="repo-view-container {{Iif .RepoPreferences.ShowFileViewTreeSidebar "repo-view-with-sidebar" "repo-view-content-only"}}">
<div class="repo-view-file-tree-sidebar not-mobile {{if not .RepoPreferences.ShowFileViewTreeSidebar}}tw-hidden{{end}}" {{if .IsSigned}} data-is-signed {{end}}>{{template "repo/view_file_tree_sidebar" .}}</div>
<div class="repo-view-content">
{{template "repo/view_content" .}}
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@ -0,0 +1,97 @@
{{$isTreePathRoot := not .TreeNames}}
{{template "repo/sub_menu" .}}
<div class="repo-button-row">
<div class="repo-button-row-left">
{{if not $isTreePathRoot}}
<button class="show-tree-sidebar-button ui compact basic button icon not-mobile {{if .RepoPreferences.ShowFileViewTreeSidebar}}tw-hidden{{end}}" title="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
{{svg "octicon-sidebar-collapse"}}
</button>
{{end}}
{{template "repo/home_branch_dropdown" (dict "ctxData" .)}}
{{if and .CanCompareOrPull .RefFullName.IsBranch (not .Repository.IsArchived)}}
{{$cmpBranch := ""}}
{{if ne .Repository.ID .BaseRepo.ID}}
{{$cmpBranch = printf "%s/%s:" (.Repository.OwnerName|PathEscape) (.Repository.Name|PathEscape)}}
{{end}}
{{$cmpBranch = print $cmpBranch (.BranchName|PathEscapeSegments)}}
{{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}}
<a id="new-pull-request" role="button" class="ui compact basic button" href="{{$compareLink}}"
data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}">
{{svg "octicon-git-pull-request"}}
</a>
{{end}}
<!-- Show go to file if on home page -->
{{if $isTreePathRoot}}
<a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
{{end}}
{{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
{{ctx.Locale.Tr "repo.editor.add_file"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<a class="item" href="{{.RepoLink}}/_new/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
{{ctx.Locale.Tr "repo.editor.new_file"}}
</a>
{{if .RepositoryUploadEnabled}}
<a class="item" href="{{.RepoLink}}/_upload/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
{{ctx.Locale.Tr "repo.editor.upload_file"}}
</a>
{{end}}
<a class="item" href="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
{{ctx.Locale.Tr "repo.editor.patch"}}
</a>
</div>
</button>
{{end}}
{{if and $isTreePathRoot .Repository.IsTemplate}}
<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}">
{{ctx.Locale.Tr "repo.use_template"}}
</a>
{{end}}
{{if not $isTreePathRoot}}
{{$treeNameIdxLast := Eval (len .TreeNames) "-" 1}}
<span class="breadcrumb repo-path tw-ml-1">
<a class="section" href="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
{{- range $i, $v := .TreeNames -}}
<span class="breadcrumb-divider">/</span>
{{- if eq $i $treeNameIdxLast -}}
<span class="active section" title="{{$v}}">{{$v}}</span>
<button class="btn interact-fg tw-mx-1" data-clipboard-text="{{$.TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "copy_path"}}">{{svg "octicon-copy" 14}}</button>
{{- else -}}
{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{$v}}</a></span>
{{- end -}}
{{- end -}}
</span>
{{end}}
</div>
<div class="repo-button-row-right">
<!-- Only show clone panel in repository home page -->
{{if $isTreePathRoot}}
{{template "repo/clone_panel" .}}
{{end}}
{{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
<a class="ui button" href="{{.RepoLink}}/commits/{{.RefTypeNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
</a>
{{end}}
</div>
</div>
{{if .IsViewFile}}
{{template "repo/view_file" .}}
{{else if .IsBlame}}
{{template "repo/blame" .}}
{{else}}{{/* IsViewDirectory */}}
{{if $isTreePathRoot}}
{{template "repo/code/upstream_diverging_info" .}}
{{end}}
{{template "repo/view_list" .}}
{{if and .ReadmeExist (or .IsMarkup .IsPlainText)}}
{{template "repo/view_file" .}}
{{end}}
{{end}}

View File

@ -0,0 +1,17 @@
<div class="flex-text-block">
<button class="hide-tree-sidebar-button ui compact tiny icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
{{svg "octicon-sidebar-expand"}}
</button>
<b>Files</b>
</div>
<!--// TODO: Dynamically move components such as refSelector and createPR here-->
<div class="view-file-tree-sidebar-bottom tw-overflow-auto">
<div id="view-file-tree" class="is-loading"
data-api-base-url="{{.RepoLink}}"
data-tree-path="{{$.TreePath}}"
data-current-ref-type="{{.RefFullName.RefType}}"
data-current-ref-short-name="{{.RefFullName.ShortName}}"
data-current-ref-type-name-sub-url="{{.RefTypeNameSubURL}}"
></div>
</div>

View File

@ -49,6 +49,45 @@
}
}
.repo-view-container {
display: flex;
flex-wrap: wrap;
gap: var(--page-spacing);
}
.repo-view-content {
flex: 1;
}
#view-file-tree.is-loading {
aspect-ratio: 5.415; /* the size is about 790 x 145 */
}
.repo-view-with-sidebar .repo-view-file-tree-sidebar {
flex: 0 1 15%;
display: flex;
flex-direction: column;
gap: 8px;
max-height: 100vh;
overflow: hidden;
position: sticky;
top: 14px;
z-index: 8;
}
.repo-view-container .repo-button-row {
margin-top: 0 !important;
}
@media (max-width: 767.98px) {
.repo-view-with-sidebar {
flex: 1 1 0;
}
.repo-view-content {
flex: 1 1 0;
}
}
.language-stats {
display: flex;
gap: 2px;

View File

@ -0,0 +1,26 @@
<script lang="ts" setup>
import ViewFileTreeItem from './ViewFileTreeItem.vue';
defineProps<{
files: any,
selectedItem: any,
loadChildren: any,
loadContent: any;
}>();
</script>
<template>
<div class="view-file-tree-items">
<!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
<ViewFileTreeItem v-for="item in files" :key="item.name" :item="item" :selected-item="selectedItem" :load-content="loadContent" :load-children="loadChildren"/>
</div>
</template>
<style scoped>
.view-file-tree-items {
display: flex;
flex-direction: column;
gap: 1px;
margin-right: .5rem;
}
</style>

View File

@ -0,0 +1,163 @@
<script lang="ts" setup>
import {SvgIcon} from '../svg.ts';
import {ref} from 'vue';
type Item = {
name: string;
path: string;
sub_module_url?: string;
type: string;
children?: Item[];
};
const props = defineProps<{
item: Item,
loadContent: any;
loadChildren: any;
selectedItem?: any;
}>();
const isLoading = ref(false);
const children = ref(props.item.children);
const collapsed = ref(!props.item.children);
const doLoadChildren = async () => {
collapsed.value = !collapsed.value;
if (!collapsed.value && props.loadChildren) {
isLoading.value = true;
try {
const _children = await props.loadChildren(props.item.path);
children.value = _children;
} finally {
isLoading.value = false;
}
}
};
const doLoadDirContent = () => {
doLoadChildren();
props.loadContent(props.item.path);
};
const doLoadFileContent = () => {
props.loadContent(props.item.path);
};
const doGotoSubModule = () => {
location.href = props.item.sub_module_url;
};
</script>
<template>
<!--title instead of tooltip above as the tooltip needs too much work with the current methods, i.e. not being loaded or staying open for "too long"-->
<div
v-if="item.type === 'commit'" class="item-submodule"
:title="item.name"
@click.stop="doGotoSubModule"
>
<!-- submodule -->
<div class="item-content">
<SvgIcon class="text primary" name="octicon-file-submodule"/>
<span class="gt-ellipsis tw-flex-1">{{ item.name }}</span>
</div>
</div>
<div
v-else-if="item.type === 'symlink'" class="item-symlink"
:class="{'selected': selectedItem.value === item.path}"
:title="item.name"
@click.stop="doLoadFileContent"
>
<!-- symlink -->
<div class="item-content">
<SvgIcon name="octicon-file-symlink-file"/>
<span class="gt-ellipsis tw-flex-1">{{ item.name }}</span>
</div>
</div>
<div
v-else-if="item.type !== 'tree'" class="item-file"
:class="{'selected': selectedItem.value === item.path}"
:title="item.name"
@click.stop="doLoadFileContent"
>
<!-- file -->
<div class="item-content">
<SvgIcon name="octicon-file"/>
<span class="gt-ellipsis tw-flex-1">{{ item.name }}</span>
</div>
</div>
<div
v-else class="item-directory"
:class="{'selected': selectedItem.value === item.path}"
:title="item.name"
@click.stop="doLoadDirContent"
>
<!-- directory -->
<div class="item-toggle">
<SvgIcon v-if="isLoading" name="octicon-sync" class="job-status-rotate"/>
<SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/>
</div>
<div class="item-content">
<SvgIcon class="text primary" :name="collapsed ? 'octicon-file-directory-fill' : 'octicon-file-directory-open-fill'"/>
<span class="gt-ellipsis">{{ item.name }}</span>
</div>
</div>
<div v-if="children?.length" v-show="!collapsed" class="sub-items">
<ViewFileTreeItem v-for="childItem in children" :key="childItem.name" :item="childItem" :selected-item="selectedItem" :load-content="loadContent" :load-children="loadChildren"/>
</div>
</template>
<style scoped>
.sub-items {
display: flex;
flex-direction: column;
gap: 1px;
margin-left: 14px;
border-left: 1px solid var(--color-secondary);
}
.item-directory.selected,
.item-symlink.selected,
.item-file.selected {
color: var(--color-text);
background: var(--color-active);
border-radius: 4px;
}
.item-directory {
user-select: none;
}
.item-file,
.item-symlink,
.item-submodule,
.item-directory {
display: grid;
grid-template-columns: 16px 1fr;
grid-template-areas: "toggle content";
gap: 0.25em;
padding: 6px;
}
.item-file:hover,
.item-symlink:hover,
.item-submodule:hover,
.item-directory:hover {
color: var(--color-text);
background: var(--color-hover);
border-radius: 4px;
cursor: pointer;
}
.item-toggle {
grid-area: toggle;
display: flex;
align-items: center;
}
.item-content {
grid-area: content;
display: flex;
align-items: center;
gap: 0.25em;
}
</style>

View File

@ -0,0 +1,102 @@
import {createApp, ref} from 'vue';
import {toggleElem} from '../utils/dom.ts';
import {pathEscapeSegments, pathUnescapeSegments} from '../utils/url.ts';
import {GET, PUT} from '../modules/fetch.ts';
import ViewFileTree from '../components/ViewFileTree.vue';
const {appSubUrl} = window.config;
async function toggleSidebar(sidebarEl: HTMLElement, shouldShow: boolean) {
const showBtnEl = sidebarEl.parentElement.querySelector('.show-tree-sidebar-button');
const containerClassList = sidebarEl.parentElement.classList;
containerClassList.toggle('repo-view-with-sidebar', shouldShow);
containerClassList.toggle('repo-view-content-only', !shouldShow);
toggleElem(sidebarEl, shouldShow);
toggleElem(showBtnEl, !shouldShow);
// FIXME: need to remove "full height" style from parent element
if (!sidebarEl.hasAttribute('data-is-signed')) return;
// save to session
await PUT(`${appSubUrl}/repo/preferences`, {
data: {
show_file_view_tree_sidebar: shouldShow,
},
});
}
function childrenLoader(sidebarEl: HTMLElement) {
return async (path: string, recursive?: boolean) => {
const fileTree = sidebarEl.querySelector('#view-file-tree');
const apiBaseUrl = fileTree.getAttribute('data-api-base-url');
const refTypeNameSubURL = fileTree.getAttribute('data-current-ref-type-name-sub-url');
const response = await GET(`${apiBaseUrl}/tree/${refTypeNameSubURL}/${pathEscapeSegments(path ?? '')}?recursive=${recursive ?? false}`);
const json = await response.json();
if (json instanceof Array) {
return json.map((i) => ({
name: i.name,
type: i.type,
path: i.path,
sub_module_url: i.sub_module_url,
children: i.children,
}));
}
return null;
};
}
async function loadContent(sidebarEl: HTMLElement) {
// load content by path (content based on home_content.tmpl)
const response = await GET(`${window.location.href}?only_content=true`);
const contentEl = sidebarEl.parentElement.querySelector('.repo-view-content');
contentEl.innerHTML = await response.text();
reloadContentScript(sidebarEl, contentEl);
}
function reloadContentScript(sidebarEl: HTMLElement, contentEl: Element) {
contentEl.querySelector('.show-tree-sidebar-button')?.addEventListener('click', () => {
toggleSidebar(sidebarEl, true);
});
}
export async function initViewFileTreeSidebar() {
const sidebarEl = document.querySelector('.repo-view-file-tree-sidebar');
if (!sidebarEl || !(sidebarEl instanceof HTMLElement)) return;
sidebarEl.querySelector('.hide-tree-sidebar-button').addEventListener('click', () => {
toggleSidebar(sidebarEl, false);
});
sidebarEl.parentElement.querySelector('.repo-view-content .show-tree-sidebar-button').addEventListener('click', () => {
toggleSidebar(sidebarEl, true);
});
const fileTree = sidebarEl.querySelector('#view-file-tree');
const baseUrl = fileTree.getAttribute('data-api-base-url');
const treePath = fileTree.getAttribute('data-tree-path');
const refType = fileTree.getAttribute('data-current-ref-type');
const refName = fileTree.getAttribute('data-current-ref-short-name');
const refString = (refType ? (`/${refType}`) : '') + (refName ? (`/${refName}`) : '');
const selectedItem = ref(getSelectedPath(refString));
const files = await childrenLoader(sidebarEl)(treePath, true);
fileTree.classList.remove('is-loading');
const fileTreeView = createApp(ViewFileTree, {files, selectedItem, loadChildren: childrenLoader(sidebarEl), loadContent: (path: string) => {
window.history.pushState(null, null, `${baseUrl}/src${refString}/${pathEscapeSegments(path)}`);
selectedItem.value = path;
loadContent(sidebarEl);
}});
fileTreeView.mount(fileTree);
window.addEventListener('popstate', () => {
selectedItem.value = getSelectedPath(refString);
loadContent(sidebarEl);
});
}
function getSelectedPath(ref: string) {
const path = pathUnescapeSegments(new URL(window.location.href).pathname);
return path.substring(path.indexOf(ref) + ref.length + 1);
}

View File

@ -24,6 +24,7 @@ import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts';
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
import {initRepoTopicBar} from './features/repo-home.ts';
import {initViewFileTreeSidebar} from './features/repo-view-file-tree-sidebar.ts';
import {initAdminCommon} from './features/admin/common.ts';
import {initRepoCodeView} from './features/repo-code.ts';
import {initSshKeyFormParser} from './features/sshkey-helper.ts';
@ -139,6 +140,7 @@ onDomReady(() => {
initRepoRelease,
initRepoReleaseNew,
initRepoTopicBar,
initViewFileTreeSidebar,
initRepoWikiForm,
initRepository,
initRepositoryActionView,

View File

@ -29,6 +29,7 @@ import octiconFile from '../../public/assets/img/svg/octicon-file.svg';
import octiconFileDirectoryFill from '../../public/assets/img/svg/octicon-file-directory-fill.svg';
import octiconFileDirectoryOpenFill from '../../public/assets/img/svg/octicon-file-directory-open-fill.svg';
import octiconFileSubmodule from '../../public/assets/img/svg/octicon-file-submodule.svg';
import octiconFileSymlinkFile from '../../public/assets/img/svg/octicon-file-symlink-file.svg';
import octiconFilter from '../../public/assets/img/svg/octicon-filter.svg';
import octiconGear from '../../public/assets/img/svg/octicon-gear.svg';
import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg';
@ -107,6 +108,7 @@ const svgs = {
'octicon-file-directory-fill': octiconFileDirectoryFill,
'octicon-file-directory-open-fill': octiconFileDirectoryOpenFill,
'octicon-file-submodule': octiconFileSubmodule,
'octicon-file-symlink-file': octiconFileSymlinkFile,
'octicon-filter': octiconFilter,
'octicon-gear': octiconGear,
'octicon-git-branch': octiconGitBranch,

View File

@ -2,6 +2,10 @@ export function pathEscapeSegments(s: string): string {
return s.split('/').map(encodeURIComponent).join('/');
}
export function pathUnescapeSegments(s: string): string {
return s.split('/').map(decodeURIComponent).join('/');
}
function stripSlash(url: string): string {
return url.endsWith('/') ? url.slice(0, -1) : url;
}