Skip to content

Commit 8c9d2bd

Browse files
Keep file tree view icons consistent with icon theme (#33921)
Fix #33914 before: ![3000-gogitea-gitea-y4ulxr46c4k ws-us118 gitpod io_test_test gitea_src_branch_main_ gitmodules](https://github.com/user-attachments/assets/ca50eeff-cc44-4041-b01f-c0c5bdd3b6aa) after: ![3000-gogitea-gitea-y4ulxr46c4k ws-us118 gitpod io_test_test gitea_src_branch_main_README md](https://github.com/user-attachments/assets/3b87fdbd-81d0-4831-8a74-4dbfcd5b6d91) --------- Co-authored-by: wxiaoguang <[email protected]>
1 parent bcc38eb commit 8c9d2bd

File tree

14 files changed

+170
-86
lines changed

14 files changed

+170
-86
lines changed

modules/fileicon/material.go

+6-16
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"code.gitea.io/gitea/modules/json"
1414
"code.gitea.io/gitea/modules/log"
1515
"code.gitea.io/gitea/modules/options"
16-
"code.gitea.io/gitea/modules/reqctx"
1716
"code.gitea.io/gitea/modules/svg"
1817
)
1918

@@ -62,30 +61,21 @@ func (m *MaterialIconProvider) loadData() {
6261
log.Debug("Loaded material icon rules and SVG images")
6362
}
6463

65-
func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name, svg, extraClass string) template.HTML {
66-
data := ctx.GetData()
67-
renderedSVGs, _ := data["_RenderedSVGs"].(map[string]bool)
68-
if renderedSVGs == nil {
69-
renderedSVGs = make(map[string]bool)
70-
data["_RenderedSVGs"] = renderedSVGs
71-
}
64+
func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg, extraClass string) template.HTML {
7265
// This part is a bit hacky, but it works really well. It should be safe to do so because all SVG icons are generated by us.
7366
// Will try to refactor this in the future.
7467
if !strings.HasPrefix(svg, "<svg") {
7568
panic("Invalid SVG icon")
7669
}
7770
svgID := "svg-mfi-" + name
7871
svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
79-
posOuterBefore := strings.IndexByte(svg, '>')
80-
if renderedSVGs[svgID] && posOuterBefore != -1 {
81-
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
72+
if p.IconSVGs[svgID] == "" {
73+
p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
8274
}
83-
svg = `<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]
84-
renderedSVGs[svgID] = true
85-
return template.HTML(svg)
75+
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
8676
}
8777

88-
func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.TreeEntry) template.HTML {
78+
func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML {
8979
if m.rules == nil {
9080
return BasicThemeIcon(entry)
9181
}
@@ -110,7 +100,7 @@ func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.Tr
110100
case entry.IsSubModule():
111101
extraClass = "octicon-file-submodule"
112102
}
113-
return m.renderFileIconSVG(ctx, name, iconSVG, extraClass)
103+
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
114104
}
115105
// TODO: use an interface or wrapper for git.Entry to make the code testable.
116106
return BasicThemeIcon(entry)

modules/fileicon/render.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package fileicon
5+
6+
import (
7+
"html/template"
8+
"strings"
9+
10+
"code.gitea.io/gitea/modules/git"
11+
"code.gitea.io/gitea/modules/setting"
12+
)
13+
14+
type RenderedIconPool struct {
15+
IconSVGs map[string]template.HTML
16+
}
17+
18+
func NewRenderedIconPool() *RenderedIconPool {
19+
return &RenderedIconPool{
20+
IconSVGs: make(map[string]template.HTML),
21+
}
22+
}
23+
24+
func (p *RenderedIconPool) RenderToHTML() template.HTML {
25+
if len(p.IconSVGs) == 0 {
26+
return ""
27+
}
28+
sb := &strings.Builder{}
29+
sb.WriteString(`<div class=tw-hidden>`)
30+
for _, icon := range p.IconSVGs {
31+
sb.WriteString(string(icon))
32+
}
33+
sb.WriteString(`</div>`)
34+
return template.HTML(sb.String())
35+
}
36+
37+
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module
38+
39+
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
40+
if setting.UI.FileIconTheme == "material" {
41+
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
42+
}
43+
return BasicThemeIcon(entry)
44+
}
45+
46+
func RenderEntryIconOpen(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
47+
// TODO: add "open icon" support
48+
if setting.UI.FileIconTheme == "material" {
49+
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
50+
}
51+
return BasicThemeIcon(entry)
52+
}

modules/git/error.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,19 @@ func (err ErrNotExist) Unwrap() error {
3232
return util.ErrNotExist
3333
}
3434

35-
// ErrBadLink entry.FollowLink error
36-
type ErrBadLink struct {
35+
// ErrSymlinkUnresolved entry.FollowLink error
36+
type ErrSymlinkUnresolved struct {
3737
Name string
3838
Message string
3939
}
4040

41-
func (err ErrBadLink) Error() string {
41+
func (err ErrSymlinkUnresolved) Error() string {
4242
return fmt.Sprintf("%s: %s", err.Name, err.Message)
4343
}
4444

45-
// IsErrBadLink if some error is ErrBadLink
46-
func IsErrBadLink(err error) bool {
47-
_, ok := err.(ErrBadLink)
45+
// IsErrSymlinkUnresolved if some error is ErrSymlinkUnresolved
46+
func IsErrSymlinkUnresolved(err error) bool {
47+
_, ok := err.(ErrSymlinkUnresolved)
4848
return ok
4949
}
5050

modules/git/tree_entry.go

+19-23
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"io"
99
"sort"
1010
"strings"
11+
12+
"code.gitea.io/gitea/modules/util"
1113
)
1214

1315
// Type returns the type of the entry (commit, tree, blob)
@@ -25,7 +27,7 @@ func (te *TreeEntry) Type() string {
2527
// FollowLink returns the entry pointed to by a symlink
2628
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
2729
if !te.IsLink() {
28-
return nil, ErrBadLink{te.Name(), "not a symlink"}
30+
return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"}
2931
}
3032

3133
// read the link
@@ -56,47 +58,41 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
5658
}
5759

5860
if t == nil {
59-
return nil, ErrBadLink{te.Name(), "points outside of repo"}
61+
return nil, ErrSymlinkUnresolved{te.Name(), "points outside of repo"}
6062
}
6163

6264
target, err := t.GetTreeEntryByPath(lnk)
6365
if err != nil {
6466
if IsErrNotExist(err) {
65-
return nil, ErrBadLink{te.Name(), "broken link"}
67+
return nil, ErrSymlinkUnresolved{te.Name(), "broken link"}
6668
}
6769
return nil, err
6870
}
6971
return target, nil
7072
}
7173

7274
// FollowLinks returns the entry ultimately pointed to by a symlink
73-
func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
75+
func (te *TreeEntry) FollowLinks(optLimit ...int) (*TreeEntry, error) {
7476
if !te.IsLink() {
75-
return nil, ErrBadLink{te.Name(), "not a symlink"}
77+
return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"}
7678
}
79+
limit := util.OptionalArg(optLimit, 10)
7780
entry := te
78-
for i := 0; i < 999; i++ {
79-
if entry.IsLink() {
80-
next, err := entry.FollowLink()
81-
if err != nil {
82-
return nil, err
83-
}
84-
if next.ID == entry.ID {
85-
return nil, ErrBadLink{
86-
entry.Name(),
87-
"recursive link",
88-
}
89-
}
90-
entry = next
91-
} else {
81+
for i := 0; i < limit; i++ {
82+
if !entry.IsLink() {
9283
break
9384
}
85+
next, err := entry.FollowLink()
86+
if err != nil {
87+
return nil, err
88+
}
89+
if next.ID == entry.ID {
90+
return nil, ErrSymlinkUnresolved{entry.Name(), "recursive link"}
91+
}
92+
entry = next
9493
}
9594
if entry.IsLink() {
96-
return nil, ErrBadLink{
97-
te.Name(),
98-
"too many levels of symbolic links",
99-
}
95+
return nil, ErrSymlinkUnresolved{te.Name(), "too many levels of symbolic links"}
10096
}
10197
return entry, nil
10298
}

modules/git/tree_entry_mode.go

+5-15
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,19 @@ const (
1717
// EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of
1818
// added the base commit will not have the file in its tree so a mode of 0o000000 is used.
1919
EntryModeNoEntry EntryMode = 0o000000
20-
// EntryModeBlob
21-
EntryModeBlob EntryMode = 0o100644
22-
// EntryModeExec
23-
EntryModeExec EntryMode = 0o100755
24-
// EntryModeSymlink
20+
21+
EntryModeBlob EntryMode = 0o100644
22+
EntryModeExec EntryMode = 0o100755
2523
EntryModeSymlink EntryMode = 0o120000
26-
// EntryModeCommit
27-
EntryModeCommit EntryMode = 0o160000
28-
// EntryModeTree
29-
EntryModeTree EntryMode = 0o040000
24+
EntryModeCommit EntryMode = 0o160000
25+
EntryModeTree EntryMode = 0o040000
3026
)
3127

3228
// String converts an EntryMode to a string
3329
func (e EntryMode) String() string {
3430
return strconv.FormatInt(int64(e), 8)
3531
}
3632

37-
// ToEntryMode converts a string to an EntryMode
38-
func ToEntryMode(value string) EntryMode {
39-
v, _ := strconv.ParseInt(value, 8, 32)
40-
return EntryMode(v)
41-
}
42-
4333
func ParseEntryMode(mode string) (EntryMode, error) {
4434
switch mode {
4535
case "000000":

modules/templates/util_render.go

-9
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import (
1515

1616
issues_model "code.gitea.io/gitea/models/issues"
1717
"code.gitea.io/gitea/modules/emoji"
18-
"code.gitea.io/gitea/modules/fileicon"
19-
"code.gitea.io/gitea/modules/git"
2018
"code.gitea.io/gitea/modules/htmlutil"
2119
"code.gitea.io/gitea/modules/log"
2220
"code.gitea.io/gitea/modules/markup"
@@ -181,13 +179,6 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
181179
textColor, itemColor, itemHTML)
182180
}
183181

184-
func (ut *RenderUtils) RenderFileIcon(entry *git.TreeEntry) template.HTML {
185-
if setting.UI.FileIconTheme == "material" {
186-
return fileicon.DefaultMaterialIconProvider().FileIcon(ut.ctx, entry)
187-
}
188-
return fileicon.BasicThemeIcon(entry)
189-
}
190-
191182
// RenderEmoji renders html text with emoji post processors
192183
func (ut *RenderUtils) RenderEmoji(text string) template.HTML {
193184
renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text))

routers/web/repo/treelist.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
pull_model "code.gitea.io/gitea/models/pull"
1010
"code.gitea.io/gitea/modules/base"
11+
"code.gitea.io/gitea/modules/fileicon"
1112
"code.gitea.io/gitea/modules/git"
1213
"code.gitea.io/gitea/services/context"
1314
"code.gitea.io/gitea/services/gitdiff"
@@ -87,10 +88,11 @@ func transformDiffTreeForUI(diffTree *gitdiff.DiffTree, filesViewedState map[str
8788
}
8889

8990
func TreeViewNodes(ctx *context.Context) {
90-
results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
91+
renderedIconPool := fileicon.NewRenderedIconPool()
92+
results, err := files_service.GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
9193
if err != nil {
9294
ctx.ServerError("GetTreeViewNodes", err)
9395
return
9496
}
95-
ctx.JSON(http.StatusOK, map[string]any{"fileTreeNodes": results})
97+
ctx.JSON(http.StatusOK, map[string]any{"fileTreeNodes": results, "renderedIconPool": renderedIconPool.IconSVGs})
9698
}

routers/web/repo/view.go

+12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
user_model "code.gitea.io/gitea/models/user"
3030
"code.gitea.io/gitea/modules/base"
3131
"code.gitea.io/gitea/modules/charset"
32+
"code.gitea.io/gitea/modules/fileicon"
3233
"code.gitea.io/gitea/modules/git"
3334
"code.gitea.io/gitea/modules/lfs"
3435
"code.gitea.io/gitea/modules/log"
@@ -252,6 +253,16 @@ func LastCommit(ctx *context.Context) {
252253
ctx.HTML(http.StatusOK, tplRepoViewList)
253254
}
254255

256+
func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
257+
renderedIconPool := fileicon.NewRenderedIconPool()
258+
fileIcons := map[string]template.HTML{}
259+
for _, f := range files {
260+
fileIcons[f.Entry.Name()] = fileicon.RenderEntryIcon(renderedIconPool, f.Entry)
261+
}
262+
ctx.Data["FileIcons"] = fileIcons
263+
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
264+
}
265+
255266
func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
256267
tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
257268
if err != nil {
@@ -293,6 +304,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
293304
return nil
294305
}
295306
ctx.Data["Files"] = files
307+
prepareDirectoryFileIcons(ctx, files)
296308
for _, f := range files {
297309
if f.Commit == nil {
298310
ctx.Data["HasFilesWithoutLatestCommit"] = true

routers/web/repo/view_readme.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
6969
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
7070
if entry.IsLink() {
7171
target, err := entry.FollowLinks()
72-
if err != nil && !git.IsErrBadLink(err) {
72+
if err != nil && !git.IsErrSymlinkUnresolved(err) {
7373
return "", nil, err
7474
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
7575
readmeFiles[i] = entry

0 commit comments

Comments
 (0)