mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-20 11:43:57 +08:00
Compare commits
94 Commits
c458787db5
...
7426633c45
Author | SHA1 | Date | |
---|---|---|---|
|
7426633c45 | ||
|
e632ba8824 | ||
|
40f8de2e13 | ||
|
7179916402 | ||
|
0aa1b867dd | ||
|
bb82979191 | ||
|
2d644fb4ce | ||
|
20198ea5a2 | ||
|
c06f30d77a | ||
|
76c82b21a5 | ||
|
ca964c1ce9 | ||
|
61be52be5d | ||
|
b1193e0291 | ||
|
9dfdb4fc08 | ||
|
abf95a76db | ||
|
9400d52ee9 | ||
|
459edfebcb | ||
|
fe656586f5 | ||
|
bdd78dfdcb | ||
|
4f90719901 | ||
|
4890434b1e | ||
|
0b441325a9 | ||
|
a55548feb2 | ||
|
fe212908b1 | ||
|
dc63b48493 | ||
|
cac5f1cbf3 | ||
|
ab98bfba9d | ||
|
c0e2fd2e7d | ||
|
67a749f52c | ||
|
3c863223c5 | ||
|
616fe58df0 | ||
|
eff3568d31 | ||
|
d4a99e53c8 | ||
|
62ecf34b99 | ||
|
c2670dbb81 | ||
|
0cafe2a39d | ||
|
9b95582f5d | ||
|
818ad6a3ed | ||
|
f769c8bf43 | ||
|
6441fe5a73 | ||
|
67342378d3 | ||
|
3e679d67b3 | ||
|
be7a6b6057 | ||
|
34dc5b1c1d | ||
|
335375fd76 | ||
|
2000a679fa | ||
|
d675289302 | ||
|
87ed579357 | ||
|
a125a58e81 | ||
|
6b86304589 | ||
|
1fd53fd6e8 | ||
|
96e050ec63 | ||
|
07e48964b5 | ||
|
91f972ce82 | ||
|
647cd304a0 | ||
|
cdf05fb726 | ||
|
cd3a9afe8f | ||
|
64c7e08df2 | ||
|
c949f42d4d | ||
|
ad9b126ad6 | ||
|
2a8f9c8531 | ||
|
a1cee9e4e2 | ||
|
09000c4ce9 | ||
|
087f0d41bd | ||
|
fe63c5fbc1 | ||
|
b1b24282ae | ||
|
91ffb2d720 | ||
|
bf15e1878c | ||
|
1a1fe32256 | ||
|
e860a363e7 | ||
|
a19a972159 | ||
|
9ae1c85ee8 | ||
|
2178347348 | ||
|
15f4b920b7 | ||
|
634fbe0c2d | ||
|
143249f4f1 | ||
|
158b79e40a | ||
|
76a56149d5 | ||
|
a1830dc1e8 | ||
|
f2b1da5392 | ||
|
3b3eaec840 | ||
|
6cb22e5046 | ||
|
023a26037e | ||
|
e46f7e40a1 | ||
|
a86c9e43a9 | ||
|
38de8aa341 | ||
|
a28b65d69b | ||
|
07fb84f830 | ||
|
b60ee86a24 | ||
|
64b4cf4f54 | ||
|
68ef9ef6b2 | ||
|
c4e7f0c119 | ||
|
59e46d46e0 | ||
|
181645f4ed |
@ -17,5 +17,6 @@ const (
|
|||||||
// SignupIP is the IP address that the user signed up with
|
// SignupIP is the IP address that the user signed up with
|
||||||
SignupIP = "signup.ip"
|
SignupIP = "signup.ip"
|
||||||
// SignupUserAgent is the user agent that the user signed up with
|
// 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"
|
||||||
)
|
)
|
||||||
|
@ -46,6 +46,8 @@ func RefBlame(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareHomeTreeSideBarSwitch(ctx)
|
||||||
|
|
||||||
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
||||||
treeLink := branchLink
|
treeLink := branchLink
|
||||||
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL()
|
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL()
|
||||||
@ -91,9 +93,16 @@ func RefBlame(ctx *context.Context) {
|
|||||||
ctx.Data["FileSize"] = fileSize
|
ctx.Data["FileSize"] = fileSize
|
||||||
ctx.Data["FileName"] = blob.Name()
|
ctx.Data["FileName"] = blob.Name()
|
||||||
|
|
||||||
|
var tplName templates.TplName
|
||||||
|
if ctx.FormBool("only_content") {
|
||||||
|
tplName = tplRepoHomeContent
|
||||||
|
} else {
|
||||||
|
tplName = tplRepoHome
|
||||||
|
}
|
||||||
|
|
||||||
if fileSize >= setting.UI.MaxDisplayFileSize {
|
if fileSize >= setting.UI.MaxDisplayFileSize {
|
||||||
ctx.Data["IsFileTooLarge"] = true
|
ctx.Data["IsFileTooLarge"] = true
|
||||||
ctx.HTML(http.StatusOK, tplRepoHome)
|
ctx.HTML(http.StatusOK, tplName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +130,7 @@ func RefBlame(ctx *context.Context) {
|
|||||||
|
|
||||||
renderBlame(ctx, result.Parts, commitNames)
|
renderBlame(ctx, result.Parts, commitNames)
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplRepoHome)
|
ctx.HTML(http.StatusOK, tplName)
|
||||||
}
|
}
|
||||||
|
|
||||||
type blameResult struct {
|
type blameResult struct {
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
@ -20,6 +21,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/cache"
|
"code.gitea.io/gitea/modules/cache"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
@ -648,3 +650,21 @@ func PrepareBranchList(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
ctx.Data["Branches"] = brs
|
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()
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
|
|
||||||
"github.com/go-enry/go-enry/v2"
|
"github.com/go-enry/go-enry/v2"
|
||||||
)
|
)
|
||||||
@ -52,3 +53,25 @@ func isExcludedEntry(entry *git.TreeEntry) bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Tree(ctx *context.Context) {
|
||||||
|
recursive := ctx.FormBool("recursive")
|
||||||
|
|
||||||
|
if ctx.Repo.RefFullName == "" {
|
||||||
|
ctx.Error(http.StatusBadRequest, "RefFullName", "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)
|
||||||
|
}
|
@ -47,12 +47,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tplRepoEMPTY templates.TplName = "repo/empty"
|
tplRepoEMPTY templates.TplName = "repo/empty"
|
||||||
tplRepoHome templates.TplName = "repo/home"
|
tplRepoHome templates.TplName = "repo/home"
|
||||||
tplRepoViewList templates.TplName = "repo/view_list"
|
tplRepoHomeContent templates.TplName = "repo/home_content"
|
||||||
tplWatchers templates.TplName = "repo/watchers"
|
tplRepoViewList templates.TplName = "repo/view_list"
|
||||||
tplForks templates.TplName = "repo/forks"
|
tplWatchers templates.TplName = "repo/watchers"
|
||||||
tplMigrating templates.TplName = "repo/migrate/migrating"
|
tplForks templates.TplName = "repo/forks"
|
||||||
|
tplMigrating templates.TplName = "repo/migrate/migrating"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fileInfo struct {
|
type fileInfo struct {
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ import (
|
|||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
unit_model "code.gitea.io/gitea/models/unit"
|
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/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
@ -328,6 +330,21 @@ func handleRepoHomeFeed(ctx *context.Context) bool {
|
|||||||
return true
|
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
|
// Home render repository home page
|
||||||
func Home(ctx *context.Context) {
|
func Home(ctx *context.Context) {
|
||||||
if handleRepoHomeFeed(ctx) {
|
if handleRepoHomeFeed(ctx) {
|
||||||
@ -341,6 +358,8 @@ func Home(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareHomeTreeSideBarSwitch(ctx)
|
||||||
|
|
||||||
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
|
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
|
||||||
if len(ctx.Repo.Repository.Description) > 0 {
|
if len(ctx.Repo.Repository.Description) > 0 {
|
||||||
title += ": " + ctx.Repo.Repository.Description
|
title += ": " + ctx.Repo.Repository.Description
|
||||||
@ -410,7 +429,11 @@ func Home(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplRepoHome)
|
if ctx.FormBool("only_content") {
|
||||||
|
ctx.HTML(http.StatusOK, tplRepoHomeContent)
|
||||||
|
} else {
|
||||||
|
ctx.HTML(http.StatusOK, tplRepoHome)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RedirectRepoTreeToSrc(ctx *context.Context) {
|
func RedirectRepoTreeToSrc(ctx *context.Context) {
|
||||||
|
@ -1001,6 +1001,7 @@ func registerRoutes(m *web.Router) {
|
|||||||
m.Get("/migrate", repo.Migrate)
|
m.Get("/migrate", repo.Migrate)
|
||||||
m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
|
m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
|
||||||
m.Get("/search", repo.SearchRepo)
|
m.Get("/search", repo.SearchRepo)
|
||||||
|
m.Put("/preferences", repo.UpdatePreferences)
|
||||||
}, reqSignIn)
|
}, reqSignIn)
|
||||||
// end "/repo": create, migrate, search
|
// 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("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList)
|
||||||
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), 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.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff)
|
||||||
m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists).
|
m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists).
|
||||||
Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
|
Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
|
||||||
|
@ -7,9 +7,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
@ -118,3 +122,389 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
|
|||||||
}
|
}
|
||||||
return tree, nil
|
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",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d1"
|
||||||
|
},{
|
||||||
|
"name": "d2",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d2"
|
||||||
|
},{
|
||||||
|
"name": "d3",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d3"
|
||||||
|
},{
|
||||||
|
"name": "f1",
|
||||||
|
"isFile": true,
|
||||||
|
"path": "f1"
|
||||||
|
},]
|
||||||
|
|
||||||
|
Example 2: (path: d3)
|
||||||
|
|
||||||
|
GET /repo/name/tree/d3
|
||||||
|
resp:
|
||||||
|
[{
|
||||||
|
"name": "d3d1",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d3/d3d1"
|
||||||
|
}]
|
||||||
|
|
||||||
|
Example 3: (path: d3/d3d1)
|
||||||
|
|
||||||
|
GET /repo/name/tree/d3/d3d1
|
||||||
|
resp:
|
||||||
|
[{
|
||||||
|
"name": "d3d1f1",
|
||||||
|
"isFile": true,
|
||||||
|
"path": "d3/d3d1/d3d1f1"
|
||||||
|
},{
|
||||||
|
"name": "d3d1f1",
|
||||||
|
"isFile": true,
|
||||||
|
"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, we return a FileContentResponse object
|
||||||
|
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(), "/") {
|
||||||
|
mapTree[path.Dir(e.Name())] = append(mapTree[path.Dir(e.Name())], &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",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d1"
|
||||||
|
},{
|
||||||
|
"name": "d2",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d2"
|
||||||
|
},{
|
||||||
|
"name": "d3",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d3"
|
||||||
|
},{
|
||||||
|
"name": "f1",
|
||||||
|
"isFile": true,
|
||||||
|
"path": "f1"
|
||||||
|
},]
|
||||||
|
|
||||||
|
Example 2: (path: d3)
|
||||||
|
GET /repo/name/tree/d3?recursive=true
|
||||||
|
resp:
|
||||||
|
[{
|
||||||
|
"name": "d1",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d1"
|
||||||
|
},{
|
||||||
|
"name": "d2",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d2"
|
||||||
|
},{
|
||||||
|
"name": "d3",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d3",
|
||||||
|
"children": [{
|
||||||
|
"name": "d3d1",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d3/d3d1"
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"name": "f1",
|
||||||
|
"isFile": true,
|
||||||
|
"path": "f1"
|
||||||
|
},]
|
||||||
|
|
||||||
|
Example 3: (path: d3/d3d1)
|
||||||
|
GET /repo/name/tree/d3/d3d1?recursive=true
|
||||||
|
resp:
|
||||||
|
[{
|
||||||
|
"name": "d1",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d1"
|
||||||
|
},{
|
||||||
|
"name": "d2",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d2"
|
||||||
|
},{
|
||||||
|
"name": "d3",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d3",
|
||||||
|
"children": [{
|
||||||
|
"name": "d3d1",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d3/d3d1",
|
||||||
|
"children": [{
|
||||||
|
"name": "d3d1f1",
|
||||||
|
"isFile": true,
|
||||||
|
"path": "d3/d3d1/d3d1f1"
|
||||||
|
},{
|
||||||
|
"name": "d3d1f1",
|
||||||
|
"isFile": true,
|
||||||
|
"path": "d3/d3d1/d3d1f2"
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"name": "f1",
|
||||||
|
"isFile": true,
|
||||||
|
"path": "f1"
|
||||||
|
},]
|
||||||
|
|
||||||
|
Example 4: (path: d2/d2f1)
|
||||||
|
GET /repo/name/tree/d2/d2f1?recursive=true
|
||||||
|
resp:
|
||||||
|
[{
|
||||||
|
"name": "d1",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d1"
|
||||||
|
},{
|
||||||
|
"name": "d2",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d2",
|
||||||
|
"children": [{
|
||||||
|
"name": "d2f1",
|
||||||
|
"isFile": true,
|
||||||
|
"path": "d2/d2f1"
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"name": "d3",
|
||||||
|
"isFile": false,
|
||||||
|
"path": "d3"
|
||||||
|
},{
|
||||||
|
"name": "f1",
|
||||||
|
"isFile": true,
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/services/contexttest"
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
@ -50,3 +51,139 @@ func TestGetTreeBySHA(t *testing.T) {
|
|||||||
|
|
||||||
assert.EqualValues(t, expectedTree, tree)
|
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)
|
||||||
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
{{template "base/head" .}}
|
{{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}}">
|
<div role="main" aria-label="{{.Title}}" class="page-content repository file list {{if .IsBlame}}blame{{end}}">
|
||||||
{{template "repo/header" .}}
|
{{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" .}}
|
{{template "base/alert" .}}
|
||||||
|
|
||||||
{{if .Repository.IsArchived}}
|
{{if .Repository.IsArchived}}
|
||||||
@ -16,112 +17,13 @@
|
|||||||
|
|
||||||
{{template "repo/code/recently_pushed_new_branches" .}}
|
{{template "repo/code/recently_pushed_new_branches" .}}
|
||||||
|
|
||||||
{{$treeNamesLen := len .TreeNames}}
|
<div class="{{Iif $showSidebar "repo-grid-filelist-sidebar" (Iif .RepoPreferences.ShowFileViewTreeSidebar "repo-grid-tree-sidebar" "repo-grid-filelist-only")}}">
|
||||||
{{$isTreePathRoot := eq $treeNamesLen 0}}
|
{{if .TreeNames}}
|
||||||
{{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}}
|
<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="{{Iif $showSidebar "repo-grid-filelist-sidebar" "repo-grid-filelist-only"}}">
|
{{end}}
|
||||||
|
|
||||||
<div class="repo-home-filelist">
|
<div class="repo-home-filelist">
|
||||||
{{template "repo/sub_menu" .}}
|
{{template "repo/home_content" .}}
|
||||||
<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}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if $showSidebar}}
|
{{if $showSidebar}}
|
||||||
|
12
templates/repo/home_branch_dropdown.tmpl
Normal file
12
templates/repo/home_branch_dropdown.tmpl
Normal 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
|
||||||
|
}}
|
98
templates/repo/home_content.tmpl
Normal file
98
templates/repo/home_content.tmpl
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
{{$isTreePathRoot := not .TreeNames}}
|
||||||
|
{{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}}
|
||||||
|
|
||||||
|
{{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" 20 "icon"}}
|
||||||
|
</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}}
|
18
templates/repo/view_file_tree_sidebar.tmpl
Normal file
18
templates/repo/view_file_tree_sidebar.tmpl
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div class="view-file-tree-sidebar-top">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<button class="hide-tree-sidebar-button ui compact basic button icon" title="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
|
||||||
|
{{svg "octicon-sidebar-expand" 20 "icon"}}
|
||||||
|
</button>
|
||||||
|
<b> Files</b>
|
||||||
|
</div>
|
||||||
|
<!--// TODO: Dynamically move components such as refSelector and createPR here-->
|
||||||
|
</div>
|
||||||
|
<div class="view-file-tree-sidebar-bottom">
|
||||||
|
<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>
|
@ -49,6 +49,68 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-grid-tree-sidebar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px auto;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-grid-tree-sidebar .repo-home-filelist {
|
||||||
|
min-width: 0;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / 4;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#view-file-tree.is-loading {
|
||||||
|
aspect-ratio: 5.415; /* the size is about 790 x 145 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-grid-tree-sidebar .repo-view-file-tree-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
position: sticky;
|
||||||
|
top: 14px;
|
||||||
|
z-index: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-grid-tree-sidebar .view-file-tree-sidebar-top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-grid-tree-sidebar .view-file-tree-sidebar-top .button {
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
height: 30px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-grid-tree-sidebar .view-file-tree-sidebar-top .sidebar-ref {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-grid-tree-sidebar .view-file-tree-sidebar-bottom {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-grid-tree-sidebar .repo-button-row {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.repo-grid-tree-sidebar {
|
||||||
|
grid-template-columns: auto;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.language-stats {
|
.language-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
26
web_src/js/components/ViewFileTree.vue
Normal file
26
web_src/js/components/ViewFileTree.vue
Normal 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>
|
160
web_src/js/components/ViewFileTreeItem.vue
Normal file
160
web_src/js/components/ViewFileTreeItem.vue
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
<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;
|
||||||
|
const _children = await props.loadChildren(props.item);
|
||||||
|
children.value = _children;
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doLoadDirContent = () => {
|
||||||
|
doLoadChildren();
|
||||||
|
props.loadContent(props.item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const doLoadFileContent = () => {
|
||||||
|
props.loadContent(props.item);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
@ -160,7 +160,11 @@ export function initGlobalButtons(): void {
|
|||||||
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
|
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
|
||||||
addDelegatedEventListener(document, 'click', 'form button.ui.cancel.button', (_ /* el */, e) => e.preventDefault());
|
addDelegatedEventListener(document, 'click', 'form button.ui.cancel.button', (_ /* el */, e) => e.preventDefault());
|
||||||
|
|
||||||
queryElems(document, '.show-panel', (el) => el.addEventListener('click', onShowPanelClick));
|
initTargetButtons(document);
|
||||||
queryElems(document, '.hide-panel', (el) => el.addEventListener('click', onHidePanelClick));
|
}
|
||||||
queryElems(document, '.show-modal', (el) => el.addEventListener('click', onShowModalClick));
|
|
||||||
|
export function initTargetButtons(target: ParentNode): void {
|
||||||
|
queryElems(target, '.show-panel', (el) => el.addEventListener('click', onShowPanelClick));
|
||||||
|
queryElems(target, '.hide-panel', (el) => el.addEventListener('click', onHidePanelClick));
|
||||||
|
queryElems(target, '.show-modal', (el) => el.addEventListener('click', onShowModalClick));
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,13 @@ export function initFootLanguageMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initGlobalDropdown() {
|
export function initGlobalDropdown() {
|
||||||
|
initTargetDropdown(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initTargetDropdown(target: Element) {
|
||||||
// Semantic UI modules.
|
// Semantic UI modules.
|
||||||
const $uiDropdowns = fomanticQuery('.ui.dropdown');
|
const $target = fomanticQuery(target);
|
||||||
|
const $uiDropdowns = $target.find('.ui.dropdown');
|
||||||
|
|
||||||
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
|
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
|
||||||
$uiDropdowns.filter(':not(.custom)').dropdown({hideDividers: 'empty'});
|
$uiDropdowns.filter(':not(.custom)').dropdown({hideDividers: 'empty'});
|
||||||
|
@ -6,7 +6,11 @@ import {GET} from '../modules/fetch.ts';
|
|||||||
const {i18n} = window.config;
|
const {i18n} = window.config;
|
||||||
|
|
||||||
export function initCopyContent() {
|
export function initCopyContent() {
|
||||||
const btn = document.querySelector('#copy-content');
|
initTargetCopyContent(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initTargetCopyContent(target: ParentNode) {
|
||||||
|
const btn = target.querySelector('#copy-content');
|
||||||
if (!btn || btn.classList.contains('disabled')) return;
|
if (!btn || btn.classList.contains('disabled')) return;
|
||||||
|
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
|
@ -2,7 +2,11 @@ import {createTippy} from '../modules/tippy.ts';
|
|||||||
import {toggleElem} from '../utils/dom.ts';
|
import {toggleElem} from '../utils/dom.ts';
|
||||||
|
|
||||||
export function initRepoEllipsisButton() {
|
export function initRepoEllipsisButton() {
|
||||||
for (const button of document.querySelectorAll<HTMLButtonElement>('.js-toggle-commit-body')) {
|
initTargetRepoEllipsisButton(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initTargetRepoEllipsisButton(target: ParentNode) {
|
||||||
|
for (const button of target.querySelectorAll<HTMLButtonElement>('.js-toggle-commit-body')) {
|
||||||
button.addEventListener('click', function (e) {
|
button.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const expanded = this.getAttribute('aria-expanded') === 'true';
|
const expanded = this.getAttribute('aria-expanded') === 'true';
|
||||||
|
@ -21,12 +21,6 @@ import {initRepoNew} from './repo-new.ts';
|
|||||||
import {createApp} from 'vue';
|
import {createApp} from 'vue';
|
||||||
import RepoBranchTagSelector from '../components/RepoBranchTagSelector.vue';
|
import RepoBranchTagSelector from '../components/RepoBranchTagSelector.vue';
|
||||||
|
|
||||||
function initRepoBranchTagSelector(selector: string) {
|
|
||||||
for (const elRoot of document.querySelectorAll(selector)) {
|
|
||||||
createApp(RepoBranchTagSelector, {elRoot}).mount(elRoot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initBranchSelectorTabs() {
|
export function initBranchSelectorTabs() {
|
||||||
const elSelectBranches = document.querySelectorAll('.ui.dropdown.select-branch');
|
const elSelectBranches = document.querySelectorAll('.ui.dropdown.select-branch');
|
||||||
for (const elSelectBranch of elSelectBranches) {
|
for (const elSelectBranch of elSelectBranches) {
|
||||||
@ -39,11 +33,17 @@ export function initBranchSelectorTabs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function initTargetRepoBranchTagSelector(target: ParentNode, selector: string = '.js-branch-tag-selector') {
|
||||||
|
for (const elRoot of target.querySelectorAll(selector)) {
|
||||||
|
createApp(RepoBranchTagSelector, {elRoot}).mount(elRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function initRepository() {
|
export function initRepository() {
|
||||||
const pageContent = document.querySelector('.page-content.repository');
|
const pageContent = document.querySelector('.page-content.repository');
|
||||||
if (!pageContent) return;
|
if (!pageContent) return;
|
||||||
|
|
||||||
initRepoBranchTagSelector('.js-branch-tag-selector');
|
initTargetRepoBranchTagSelector(document);
|
||||||
initRepoCommentFormAndSidebar();
|
initRepoCommentFormAndSidebar();
|
||||||
|
|
||||||
// Labels
|
// Labels
|
||||||
|
127
web_src/js/features/repo-view-file-tree-sidebar.ts
Normal file
127
web_src/js/features/repo-view-file-tree-sidebar.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import {createApp, ref} from 'vue';
|
||||||
|
import {toggleElem} from '../utils/dom.ts';
|
||||||
|
import {GET, PUT} from '../modules/fetch.ts';
|
||||||
|
import ViewFileTree from '../components/ViewFileTree.vue';
|
||||||
|
import {initMarkupContent} from '../markup/content.ts';
|
||||||
|
import {initTargetRepoBranchTagSelector} from './repo-legacy.ts';
|
||||||
|
import {initTargetDropdown} from './common-page.ts';
|
||||||
|
import {initTargetRepoEllipsisButton} from './repo-commit.ts';
|
||||||
|
import {initTargetPdfViewer} from '../render/pdf.ts';
|
||||||
|
import {initTargetButtons} from './common-button.ts';
|
||||||
|
import {initTargetCopyContent} from './copycontent.ts';
|
||||||
|
|
||||||
|
async function toggleSidebar(visibility, isSigned) {
|
||||||
|
const sidebarEl = document.querySelector('.repo-view-file-tree-sidebar');
|
||||||
|
const showBtnEl = document.querySelector('.show-tree-sidebar-button');
|
||||||
|
const containerClassList = sidebarEl.parentElement.classList;
|
||||||
|
containerClassList.toggle('repo-grid-tree-sidebar', visibility);
|
||||||
|
containerClassList.toggle('repo-grid-filelist-only', !visibility);
|
||||||
|
toggleElem(sidebarEl, visibility);
|
||||||
|
toggleElem(showBtnEl, !visibility);
|
||||||
|
|
||||||
|
if (!isSigned) return;
|
||||||
|
|
||||||
|
// save to session
|
||||||
|
await PUT('/repo/preferences', {
|
||||||
|
data: {
|
||||||
|
show_file_view_tree_sidebar: visibility,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadChildren(item, recursive?: boolean) {
|
||||||
|
const fileTree = document.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}/${item ? item.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() {
|
||||||
|
// load content by path (content based on home_content.tmpl)
|
||||||
|
const response = await GET(`${window.location.href}?only_content=true`);
|
||||||
|
const contentEl = document.querySelector('.repo-home-filelist');
|
||||||
|
contentEl.innerHTML = await response.text();
|
||||||
|
reloadContentScript(contentEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadContentScript(contentEl: Element) {
|
||||||
|
contentEl.querySelector('.show-tree-sidebar-button').addEventListener('click', () => {
|
||||||
|
toggleSidebar(true, document.querySelector('.repo-view-file-tree-sidebar').hasAttribute('data-is-signed'));
|
||||||
|
});
|
||||||
|
initMarkupContent();
|
||||||
|
initTargetButtons(contentEl);
|
||||||
|
initTargetDropdown(contentEl);
|
||||||
|
initTargetPdfViewer(contentEl);
|
||||||
|
initTargetRepoBranchTagSelector(contentEl);
|
||||||
|
initTargetRepoEllipsisButton(contentEl);
|
||||||
|
initTargetCopyContent(contentEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initViewFileTreeSidebar() {
|
||||||
|
const sidebarElement = document.querySelector('.repo-view-file-tree-sidebar');
|
||||||
|
if (!sidebarElement) return;
|
||||||
|
|
||||||
|
const isSigned = sidebarElement.hasAttribute('data-is-signed');
|
||||||
|
|
||||||
|
document.querySelector('.hide-tree-sidebar-button').addEventListener('click', () => {
|
||||||
|
toggleSidebar(false, isSigned);
|
||||||
|
});
|
||||||
|
document.querySelector('.repo-home-filelist .show-tree-sidebar-button').addEventListener('click', () => {
|
||||||
|
toggleSidebar(true, isSigned);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileTree = document.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(treePath);
|
||||||
|
|
||||||
|
const files = await loadChildren({path: treePath}, true);
|
||||||
|
|
||||||
|
fileTree.classList.remove('is-loading');
|
||||||
|
const fileTreeView = createApp(ViewFileTree, {files, selectedItem, loadChildren, loadContent: (item) => {
|
||||||
|
window.history.pushState(null, null, `${baseUrl}/src${refString}/${item.path}`);
|
||||||
|
selectedItem.value = item.path;
|
||||||
|
loadContent();
|
||||||
|
}});
|
||||||
|
fileTreeView.mount(fileTree);
|
||||||
|
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
selectedItem.value = extractPath(window.location.href);
|
||||||
|
loadContent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPath(url) {
|
||||||
|
// Create a URL object
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
|
||||||
|
// Get the pathname part
|
||||||
|
const path = urlObj.pathname;
|
||||||
|
|
||||||
|
// Define a regular expression to match "/{param1}/{param2}/src/{branch}/{main}/"
|
||||||
|
const regex = /^\/[^/]+\/[^/]+\/src\/[^/]+\/[^/]+\//;
|
||||||
|
|
||||||
|
// Use RegExp#exec() method to match the path
|
||||||
|
const match = regex.exec(path);
|
||||||
|
if (match) {
|
||||||
|
return path.substring(match[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the path does not match, return the original path
|
||||||
|
return path;
|
||||||
|
}
|
@ -32,6 +32,7 @@ import {
|
|||||||
} from './features/repo-issue.ts';
|
} from './features/repo-issue.ts';
|
||||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
||||||
import {initRepoTopicBar} from './features/repo-home.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 {initAdminCommon} from './features/admin/common.ts';
|
||||||
import {initRepoCodeView} from './features/repo-code.ts';
|
import {initRepoCodeView} from './features/repo-code.ts';
|
||||||
import {initSshKeyFormParser} from './features/sshkey-helper.ts';
|
import {initSshKeyFormParser} from './features/sshkey-helper.ts';
|
||||||
@ -191,6 +192,7 @@ onDomReady(() => {
|
|||||||
initRepoRelease,
|
initRepoRelease,
|
||||||
initRepoReleaseNew,
|
initRepoReleaseNew,
|
||||||
initRepoTopicBar,
|
initRepoTopicBar,
|
||||||
|
initViewFileTreeSidebar,
|
||||||
initRepoWikiForm,
|
initRepoWikiForm,
|
||||||
initRepository,
|
initRepository,
|
||||||
initRepositoryActionView,
|
initRepositoryActionView,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import {htmlEscape} from 'escape-goat';
|
import {htmlEscape} from 'escape-goat';
|
||||||
|
|
||||||
export async function initPdfViewer() {
|
export async function initPdfViewer() {
|
||||||
const els = document.querySelectorAll('.pdf-content');
|
initTargetPdfViewer(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initTargetPdfViewer(target: ParentNode) {
|
||||||
|
const els = target.querySelectorAll('.pdf-content');
|
||||||
if (!els.length) return;
|
if (!els.length) return;
|
||||||
|
|
||||||
const pdfobject = await import(/* webpackChunkName: "pdfobject" */'pdfobject');
|
const pdfobject = await import(/* webpackChunkName: "pdfobject" */'pdfobject');
|
||||||
|
@ -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 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 octiconFileDirectoryOpenFill from '../../public/assets/img/svg/octicon-file-directory-open-fill.svg';
|
||||||
import octiconFileSubmodule from '../../public/assets/img/svg/octicon-file-submodule.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 octiconFilter from '../../public/assets/img/svg/octicon-filter.svg';
|
||||||
import octiconGear from '../../public/assets/img/svg/octicon-gear.svg';
|
import octiconGear from '../../public/assets/img/svg/octicon-gear.svg';
|
||||||
import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.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-fill': octiconFileDirectoryFill,
|
||||||
'octicon-file-directory-open-fill': octiconFileDirectoryOpenFill,
|
'octicon-file-directory-open-fill': octiconFileDirectoryOpenFill,
|
||||||
'octicon-file-submodule': octiconFileSubmodule,
|
'octicon-file-submodule': octiconFileSubmodule,
|
||||||
|
'octicon-file-symlink-file': octiconFileSymlinkFile,
|
||||||
'octicon-filter': octiconFilter,
|
'octicon-filter': octiconFilter,
|
||||||
'octicon-gear': octiconGear,
|
'octicon-gear': octiconGear,
|
||||||
'octicon-git-branch': octiconGitBranch,
|
'octicon-git-branch': octiconGitBranch,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user