Skip to content

Commit 15f4b92

Browse files
committed
Merge branch 'add-file-tree-to-file-view-page' of github.com:kerwin612/gitea into kerwin612-add-file-tree-to-file-view-page
2 parents 550abdb + 634fbe0 commit 15f4b92

File tree

15 files changed

+598
-7
lines changed

15 files changed

+598
-7
lines changed

routers/web/repo/blame.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ func RefBlame(ctx *context.Context) {
4646
return
4747
}
4848

49+
// ctx.Data["RepoPreferences"] = ctx.Session.Get("repoPreferences")
50+
ctx.Data["RepoPreferences"] = &preferencesForm{
51+
ShowFileViewTreeSidebar: true,
52+
}
53+
4954
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
5055
treeLink := branchLink
5156
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()

routers/web/repo/file.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2014 The Gogs Authors. All rights reserved.
2+
// Copyright 2018 The Gitea Authors. All rights reserved.
3+
// SPDX-License-Identifier: MIT
4+
5+
package repo
6+
7+
import (
8+
"net/http"
9+
10+
"code.gitea.io/gitea/models/unit"
11+
"code.gitea.io/gitea/modules/git"
12+
"code.gitea.io/gitea/services/context"
13+
files_service "code.gitea.io/gitea/services/repository/files"
14+
)
15+
16+
// canReadFiles returns true if repository is readable and user has proper access level.
17+
func canReadFiles(r *context.Repository) bool {
18+
return r.Permission.CanRead(unit.TypeCode)
19+
}
20+
21+
// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
22+
func GetContents(ctx *context.Context) {
23+
if !canReadFiles(ctx.Repo) {
24+
ctx.NotFound("Invalid FilePath", nil)
25+
return
26+
}
27+
28+
treePath := ctx.PathParam("*")
29+
ref := ctx.FormTrim("ref")
30+
31+
if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref); err != nil {
32+
if git.IsErrNotExist(err) {
33+
ctx.NotFound("GetContentsOrList", err)
34+
return
35+
}
36+
ctx.ServerError("Repo.GitRepo.GetCommit", err)
37+
} else {
38+
ctx.JSON(http.StatusOK, fileList)
39+
}
40+
}
41+
42+
// GetContentsList Get the metadata of all the entries of the root dir
43+
func GetContentsList(ctx *context.Context) {
44+
GetContents(ctx)
45+
}

routers/web/repo/repo.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
user_model "code.gitea.io/gitea/models/user"
2121
"code.gitea.io/gitea/modules/cache"
2222
"code.gitea.io/gitea/modules/git"
23+
"code.gitea.io/gitea/modules/json"
2324
"code.gitea.io/gitea/modules/log"
2425
"code.gitea.io/gitea/modules/optional"
2526
repo_module "code.gitea.io/gitea/modules/repository"
@@ -757,3 +758,20 @@ func PrepareBranchList(ctx *context.Context) {
757758
}
758759
ctx.Data["Branches"] = brs
759760
}
761+
762+
type preferencesForm struct {
763+
ShowFileViewTreeSidebar bool `json:"show_file_view_tree_sidebar"`
764+
}
765+
766+
func UpdatePreferences(ctx *context.Context) {
767+
form := &preferencesForm{}
768+
if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
769+
ctx.ServerError("DecodePreferencesForm", err)
770+
return
771+
}
772+
// if err := ctx.Session.Set("repoPreferences", form); err != nil {
773+
// ctx.ServerError("Session.Set", err)
774+
// return
775+
// }
776+
ctx.JSONOK()
777+
}

routers/web/repo/treelist.go renamed to routers/web/repo/tree.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88

99
"code.gitea.io/gitea/modules/base"
1010
"code.gitea.io/gitea/modules/git"
11+
"code.gitea.io/gitea/modules/gitrepo"
1112
"code.gitea.io/gitea/services/context"
13+
files_service "code.gitea.io/gitea/services/repository/files"
1214

1315
"github.com/go-enry/go-enry/v2"
1416
)
@@ -52,3 +54,26 @@ func isExcludedEntry(entry *git.TreeEntry) bool {
5254

5355
return false
5456
}
57+
58+
func Tree(ctx *context.Context) {
59+
dir := ctx.PathParam("*")
60+
ref := ctx.FormTrim("ref")
61+
recursive := ctx.FormBool("recursive")
62+
63+
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, ctx.Repo.Repository)
64+
if err != nil {
65+
ctx.ServerError("RepositoryFromContextOrOpen", err)
66+
return
67+
}
68+
defer closer.Close()
69+
70+
refName := gitRepo.UnstableGuessRefByShortName(ref)
71+
72+
results, err := files_service.GetTreeList(ctx, ctx.Repo.Repository, dir, refName, recursive)
73+
if err != nil {
74+
ctx.ServerError("GetTreeList", err)
75+
return
76+
}
77+
78+
ctx.JSON(http.StatusOK, results)
79+
}

routers/web/repo/view_home.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,11 @@ func Home(ctx *context.Context) {
305305
return
306306
}
307307

308+
// ctx.Data["RepoPreferences"] = ctx.Session.Get("repoPreferences")
309+
ctx.Data["RepoPreferences"] = &preferencesForm{
310+
ShowFileViewTreeSidebar: true,
311+
}
312+
308313
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
309314
if len(ctx.Repo.Repository.Description) > 0 {
310315
title += ": " + ctx.Repo.Repository.Description

routers/web/web.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,7 @@ func registerRoutes(m *web.Router) {
986986
m.Get("/migrate", repo.Migrate)
987987
m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
988988
m.Get("/search", repo.SearchRepo)
989+
m.Put("/preferences", repo.UpdatePreferences)
989990
}, reqSignIn)
990991
// end "/repo": create, migrate, search
991992

@@ -1156,11 +1157,15 @@ func registerRoutes(m *web.Router) {
11561157

11571158
m.Group("/{username}/{reponame}", func() {
11581159
m.Get("/find/*", repo.FindFiles)
1159-
m.Group("/tree-list", func() {
1160+
m.Group("/tree-list", func() { // for find files
11601161
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.TreeList)
11611162
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.TreeList)
11621163
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.TreeList)
11631164
})
1165+
m.Group("/tree", func() {
1166+
m.Get("", repo.Tree)
1167+
m.Get("/*", repo.Tree)
1168+
})
11641169
m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff)
11651170
m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists).
11661171
Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).

services/repository/files/tree.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import (
77
"context"
88
"fmt"
99
"net/url"
10+
"path"
11+
"strings"
1012

1113
repo_model "code.gitea.io/gitea/models/repo"
1214
"code.gitea.io/gitea/modules/git"
15+
"code.gitea.io/gitea/modules/gitrepo"
1316
"code.gitea.io/gitea/modules/setting"
1417
api "code.gitea.io/gitea/modules/structs"
1518
"code.gitea.io/gitea/modules/util"
@@ -118,3 +121,92 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
118121
}
119122
return tree, nil
120123
}
124+
125+
type TreeEntry struct {
126+
Name string `json:"name"`
127+
IsFile bool `json:"isFile"`
128+
Path string `json:"path"`
129+
Children []*TreeEntry `json:"children"`
130+
}
131+
132+
func GetTreeList(ctx context.Context, repo *repo_model.Repository, treePath string, ref git.RefName, recursive bool) ([]*TreeEntry, error) {
133+
if repo.IsEmpty {
134+
return nil, nil
135+
}
136+
if ref == "" {
137+
ref = git.RefNameFromBranch(repo.DefaultBranch)
138+
}
139+
140+
// Check that the path given in opts.treePath is valid (not a git path)
141+
cleanTreePath := CleanUploadFileName(treePath)
142+
if cleanTreePath == "" && treePath != "" {
143+
return nil, ErrFilenameInvalid{
144+
Path: treePath,
145+
}
146+
}
147+
treePath = cleanTreePath
148+
149+
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
150+
if err != nil {
151+
return nil, err
152+
}
153+
defer closer.Close()
154+
155+
// Get the commit object for the ref
156+
commit, err := gitRepo.GetCommit(ref.String())
157+
if err != nil {
158+
return nil, err
159+
}
160+
161+
entry, err := commit.GetTreeEntryByPath(treePath)
162+
if err != nil {
163+
return nil, err
164+
}
165+
166+
// If the entry is a file, we return a FileContentResponse object
167+
if entry.Type() != "tree" {
168+
return nil, fmt.Errorf("%s is not a tree", treePath)
169+
}
170+
171+
gitTree, err := commit.SubTree(treePath)
172+
if err != nil {
173+
return nil, err
174+
}
175+
var entries git.Entries
176+
if recursive {
177+
entries, err = gitTree.ListEntriesRecursiveFast()
178+
} else {
179+
entries, err = gitTree.ListEntries()
180+
}
181+
if err != nil {
182+
return nil, err
183+
}
184+
185+
var treeList []*TreeEntry
186+
mapTree := make(map[string][]*TreeEntry)
187+
for _, e := range entries {
188+
subTreePath := path.Join(treePath, e.Name())
189+
190+
if strings.Contains(e.Name(), "/") {
191+
mapTree[path.Dir(e.Name())] = append(mapTree[path.Dir(e.Name())], &TreeEntry{
192+
Name: path.Base(e.Name()),
193+
IsFile: e.Mode() != git.EntryModeTree,
194+
Path: subTreePath,
195+
})
196+
} else {
197+
treeList = append(treeList, &TreeEntry{
198+
Name: e.Name(),
199+
IsFile: e.Mode() != git.EntryModeTree,
200+
Path: subTreePath,
201+
})
202+
}
203+
}
204+
205+
for _, tree := range treeList {
206+
if !tree.IsFile {
207+
tree.Children = mapTree[tree.Path]
208+
}
209+
}
210+
211+
return treeList, nil
212+
}

services/repository/files/tree_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"code.gitea.io/gitea/models/unittest"
10+
"code.gitea.io/gitea/modules/git"
1011
api "code.gitea.io/gitea/modules/structs"
1112
"code.gitea.io/gitea/services/contexttest"
1213

@@ -50,3 +51,50 @@ func TestGetTreeBySHA(t *testing.T) {
5051

5152
assert.EqualValues(t, expectedTree, tree)
5253
}
54+
55+
func Test_GetTreeList(t *testing.T) {
56+
unittest.PrepareTestEnv(t)
57+
ctx1, _ := contexttest.MockContext(t, "user2/repo1")
58+
contexttest.LoadRepo(t, ctx1, 1)
59+
contexttest.LoadRepoCommit(t, ctx1)
60+
contexttest.LoadUser(t, ctx1, 2)
61+
contexttest.LoadGitRepo(t, ctx1)
62+
defer ctx1.Repo.GitRepo.Close()
63+
64+
refName := git.RefNameFromBranch(ctx1.Repo.Repository.DefaultBranch)
65+
66+
treeList, err := GetTreeList(ctx1, ctx1.Repo.Repository, "", refName, true)
67+
assert.NoError(t, err)
68+
assert.Len(t, treeList, 1)
69+
assert.EqualValues(t, "README.md", treeList[0].Name)
70+
assert.EqualValues(t, "README.md", treeList[0].Path)
71+
assert.True(t, treeList[0].IsFile)
72+
assert.Empty(t, treeList[0].Children)
73+
74+
ctx2, _ := contexttest.MockContext(t, "org3/repo3")
75+
contexttest.LoadRepo(t, ctx2, 3)
76+
contexttest.LoadRepoCommit(t, ctx2)
77+
contexttest.LoadUser(t, ctx2, 2)
78+
contexttest.LoadGitRepo(t, ctx2)
79+
defer ctx2.Repo.GitRepo.Close()
80+
81+
refName = git.RefNameFromBranch(ctx2.Repo.Repository.DefaultBranch)
82+
83+
treeList, err = GetTreeList(ctx2, ctx2.Repo.Repository, "", refName, true)
84+
assert.NoError(t, err)
85+
assert.Len(t, treeList, 2)
86+
assert.EqualValues(t, "README.md", treeList[0].Name)
87+
assert.EqualValues(t, "README.md", treeList[0].Path)
88+
assert.True(t, treeList[0].IsFile)
89+
assert.Empty(t, treeList[0].Children)
90+
91+
assert.EqualValues(t, "doc", treeList[1].Name)
92+
assert.EqualValues(t, "doc", treeList[1].Path)
93+
assert.False(t, treeList[1].IsFile)
94+
assert.Len(t, treeList[1].Children, 1)
95+
96+
assert.EqualValues(t, "doc.md", treeList[1].Children[0].Name)
97+
assert.EqualValues(t, "doc/doc.md", treeList[1].Children[0].Path)
98+
assert.True(t, treeList[1].Children[0].IsFile)
99+
assert.Empty(t, treeList[1].Children[0].Children)
100+
}

templates/repo/home.tmpl

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
{{template "base/head" .}}
2+
{{$treeNamesLen := len .TreeNames}}
3+
{{$isTreePathRoot := eq $treeNamesLen 0}}
4+
{{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}}
5+
{{$hasTreeSidebar := not $isTreePathRoot}}
6+
{{$showTreeSidebar := .RepoPreferences.ShowFileViewTreeSidebar}}
7+
{{$hideTreeSidebar := not $showTreeSidebar}}
8+
{{$hasAndShowTreeSidebar := and $hasTreeSidebar $showTreeSidebar}}
29
<div role="main" aria-label="{{.Title}}" class="page-content repository file list {{if .IsBlame}}blame{{end}}">
310
{{template "repo/header" .}}
411
<div class="ui container {{if .IsBlame}}fluid padded{{end}}">
@@ -16,14 +23,20 @@
1623

1724
{{template "repo/code/recently_pushed_new_branches" .}}
1825

19-
{{$treeNamesLen := len .TreeNames}}
20-
{{$isTreePathRoot := eq $treeNamesLen 0}}
21-
{{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}}
22-
<div class="{{Iif $showSidebar "repo-grid-filelist-sidebar" "repo-grid-filelist-only"}}">
26+
<div class="{{Iif $showSidebar "repo-grid-filelist-sidebar" (Iif $showTreeSidebar "repo-grid-tree-sidebar" "repo-grid-filelist-only")}}">
27+
{{if $hasTreeSidebar}}
28+
<div class="repo-view-file-tree-sidebar not-mobile {{if $hideTreeSidebar}}tw-hidden{{end}}">{{template "repo/view_file_tree_sidebar" .}}</div>
29+
{{end}}
30+
2331
<div class="repo-home-filelist">
2432
{{template "repo/sub_menu" .}}
2533
<div class="repo-button-row">
2634
<div class="repo-button-row-left">
35+
{{if $hasTreeSidebar}}
36+
<button class="show-tree-sidebar-button ui compact basic button icon not-mobile {{if $showTreeSidebar}}tw-hidden{{end}}" title="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
37+
{{svg "octicon-sidebar-collapse" 20 "icon"}}
38+
</button>
39+
{{end}}
2740
{{$branchDropdownCurrentRefType := "branch"}}
2841
{{$branchDropdownCurrentRefShortName := .BranchName}}
2942
{{if .IsViewTag}}
@@ -40,6 +53,7 @@
4053
"RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}"
4154
"AllowCreateNewRef" .CanCreateBranch
4255
"ShowViewAllRefsEntry" true
56+
"ContainerClasses" (Iif $hasAndShowTreeSidebar "tw-hidden" "")
4357
}}
4458
{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
4559
{{$cmpBranch := ""}}
@@ -48,7 +62,7 @@
4862
{{end}}
4963
{{$cmpBranch = print $cmpBranch (.BranchName|PathEscapeSegments)}}
5064
{{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}}
51-
<a id="new-pull-request" role="button" class="ui compact basic button" href="{{$compareLink}}"
65+
<a id="new-pull-request" role="button" class="ui compact basic button {{if $hasAndShowTreeSidebar}}tw-hidden{{end}}" href="{{$compareLink}}"
5266
data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}">
5367
{{svg "octicon-git-pull-request"}}
5468
</a>
@@ -60,7 +74,7 @@
6074
{{end}}
6175

6276
{{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
63-
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
77+
<button class="add-file-dropdown ui dropdown basic compact jump button {{if $hasAndShowTreeSidebar}}tw-hidden{{end}}"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
6478
{{ctx.Locale.Tr "repo.editor.add_file"}}
6579
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
6680
<div class="menu">

0 commit comments

Comments
 (0)