diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index ad79087513..2bdaeefb88 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -46,6 +46,11 @@ func RefBlame(ctx *context.Context) { return } + // ctx.Data["RepoPreferences"] = ctx.Session.Get("repoPreferences") + ctx.Data["RepoPreferences"] = &preferencesForm{ + ShowFileViewTreeSidebar: true, + } + branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() treeLink := branchLink rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() diff --git a/routers/web/repo/file.go b/routers/web/repo/file.go new file mode 100644 index 0000000000..60e7cb24b7 --- /dev/null +++ b/routers/web/repo/file.go @@ -0,0 +1,45 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" +) + +// canReadFiles returns true if repository is readable and user has proper access level. +func canReadFiles(r *context.Repository) bool { + return r.Permission.CanRead(unit.TypeCode) +} + +// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir +func GetContents(ctx *context.Context) { + if !canReadFiles(ctx.Repo) { + ctx.NotFound("Invalid FilePath", nil) + return + } + + treePath := ctx.PathParam("*") + ref := ctx.FormTrim("ref") + + if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref); err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound("GetContentsOrList", err) + return + } + ctx.ServerError("Repo.GitRepo.GetCommit", err) + } else { + ctx.JSON(http.StatusOK, fileList) + } +} + +// GetContentsList Get the metadata of all the entries of the root dir +func GetContentsList(ctx *context.Context) { + GetContents(ctx) +} diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 85a745acbd..4fa3179829 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -20,6 +20,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" @@ -757,3 +758,20 @@ 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 := ctx.Session.Set("repoPreferences", form); err != nil { + // ctx.ServerError("Session.Set", err) + // return + // } + ctx.JSONOK() +} diff --git a/routers/web/repo/treelist.go b/routers/web/repo/tree.go similarity index 60% rename from routers/web/repo/treelist.go rename to routers/web/repo/tree.go index d11af4669f..92dd2bfdf2 100644 --- a/routers/web/repo/treelist.go +++ b/routers/web/repo/tree.go @@ -8,7 +8,9 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" "github.com/go-enry/go-enry/v2" ) @@ -52,3 +54,26 @@ func isExcludedEntry(entry *git.TreeEntry) bool { return false } + +func Tree(ctx *context.Context) { + dir := ctx.PathParam("*") + ref := ctx.FormTrim("ref") + recursive := ctx.FormBool("recursive") + + gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, ctx.Repo.Repository) + if err != nil { + ctx.ServerError("RepositoryFromContextOrOpen", err) + return + } + defer closer.Close() + + refName := gitRepo.UnstableGuessRefByShortName(ref) + + results, err := files_service.GetTreeList(ctx, ctx.Repo.Repository, dir, refName, recursive) + if err != nil { + ctx.ServerError("GetTreeList", err) + return + } + + ctx.JSON(http.StatusOK, results) +} diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 70ba07f9a8..62d9fdae79 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -305,6 +305,11 @@ func Home(ctx *context.Context) { return } + // ctx.Data["RepoPreferences"] = ctx.Session.Get("repoPreferences") + ctx.Data["RepoPreferences"] = &preferencesForm{ + ShowFileViewTreeSidebar: true, + } + title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name if len(ctx.Repo.Repository.Description) > 0 { title += ": " + ctx.Repo.Repository.Description diff --git a/routers/web/web.go b/routers/web/web.go index 5e0995545e..3bcb7eaf8f 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -986,6 +986,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 @@ -1156,11 +1157,15 @@ func registerRoutes(m *web.Router) { m.Group("/{username}/{reponame}", func() { m.Get("/find/*", repo.FindFiles) - m.Group("/tree-list", func() { + m.Group("/tree-list", func() { // for find files m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.TreeList) m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.TreeList) m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.TreeList) }) + m.Group("/tree", func() { + m.Get("", repo.Tree) + m.Get("/*", 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). diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index 6775186afd..b292b5e549 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -7,9 +7,12 @@ import ( "context" "fmt" "net/url" + "path" + "strings" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -118,3 +121,92 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git } return tree, nil } + +type TreeEntry struct { + Name string `json:"name"` + IsFile bool `json:"isFile"` + Path string `json:"path"` + Children []*TreeEntry `json:"children"` +} + +func GetTreeList(ctx context.Context, repo *repo_model.Repository, treePath string, ref git.RefName, recursive bool) ([]*TreeEntry, 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 + + gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) + if err != nil { + return nil, err + } + defer closer.Close() + + // 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 treeList []*TreeEntry + mapTree := make(map[string][]*TreeEntry) + 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())], &TreeEntry{ + Name: path.Base(e.Name()), + IsFile: e.Mode() != git.EntryModeTree, + Path: subTreePath, + }) + } else { + treeList = append(treeList, &TreeEntry{ + Name: e.Name(), + IsFile: e.Mode() != git.EntryModeTree, + Path: subTreePath, + }) + } + } + + for _, tree := range treeList { + if !tree.IsFile { + tree.Children = mapTree[tree.Path] + } + } + + return treeList, nil +} diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go index 0c60fddf7b..49188b72e7 100644 --- a/services/repository/files/tree_test.go +++ b/services/repository/files/tree_test.go @@ -7,6 +7,7 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/contexttest" @@ -50,3 +51,50 @@ 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, "", 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.True(t, treeList[0].IsFile) + 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, "", refName, true) + assert.NoError(t, err) + assert.Len(t, treeList, 2) + assert.EqualValues(t, "README.md", treeList[0].Name) + assert.EqualValues(t, "README.md", treeList[0].Path) + assert.True(t, treeList[0].IsFile) + assert.Empty(t, treeList[0].Children) + + assert.EqualValues(t, "doc", treeList[1].Name) + assert.EqualValues(t, "doc", treeList[1].Path) + assert.False(t, treeList[1].IsFile) + assert.Len(t, treeList[1].Children, 1) + + assert.EqualValues(t, "doc.md", treeList[1].Children[0].Name) + assert.EqualValues(t, "doc/doc.md", treeList[1].Children[0].Path) + assert.True(t, treeList[1].Children[0].IsFile) + assert.Empty(t, treeList[1].Children[0].Children) +} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index d73b7470bc..0295464979 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -1,4 +1,11 @@ {{template "base/head" .}} +{{$treeNamesLen := len .TreeNames}} +{{$isTreePathRoot := eq $treeNamesLen 0}} +{{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}} +{{$hasTreeSidebar := not $isTreePathRoot}} +{{$showTreeSidebar := .RepoPreferences.ShowFileViewTreeSidebar}} +{{$hideTreeSidebar := not $showTreeSidebar}} +{{$hasAndShowTreeSidebar := and $hasTreeSidebar $showTreeSidebar}}