Skip to content

Keep file tree view icons consistent with icon theme #33921

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Apr 6, 2025
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions modules/fileicon/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,34 @@ import (
"code.gitea.io/gitea/modules/svg"
)

func BasicThemeIcon(entry *git.TreeEntry) template.HTML {
type FileIcon struct {
Name string
Entry git.TreeEntry
EntryMode git.EntryMode
}

func BasicThemeFolderIconName(isOpen bool) string {
if isOpen {
return "octicon-file-directory-open-fill"
}
return "octicon-file-directory-fill"
}

func BasicThemeFolderIcon(isOpen bool) template.HTML {
return svg.RenderHTML(BasicThemeFolderIconName(isOpen))
}

func BasicThemeIcon(file *FileIcon) template.HTML {
svgName := "octicon-file"
switch {
case entry.IsLink():
case file.EntryMode.IsLink():
svgName = "octicon-file-symlink-file"
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
if te, err := file.Entry.FollowLink(); err == nil && te.IsDir() {
svgName = "octicon-file-directory-symlink"
}
case entry.IsDir():
svgName = "octicon-file-directory-fill"
case entry.IsSubModule():
case file.EntryMode.IsDir():
svgName = BasicThemeFolderIconName(false)
case file.EntryMode.IsSubModule():
svgName = "octicon-file-submodule"
}
return svg.RenderHTML(svgName)
Expand Down
49 changes: 29 additions & 20 deletions modules/fileicon/material.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"strings"
"sync"

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options"
Expand Down Expand Up @@ -85,34 +84,44 @@ func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name
return template.HTML(svg)
}

func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.TreeEntry) template.HTML {
func (m *MaterialIconProvider) FolderIcon(ctx reqctx.RequestContext, isOpen bool) template.HTML {
// return svg.RenderHTML("material-folder-generic", 16, BasicThemeFolderIconName(isOpen))
iconName := "folder"
if isOpen {
iconName = "folder-open"
}
return m.renderIconByName(ctx, iconName, BasicThemeFolderIconName(isOpen))
}

func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, file *FileIcon) template.HTML {
if m.rules == nil {
return BasicThemeIcon(entry)
return BasicThemeIcon(file)
}

if entry.IsLink() {
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
if file.EntryMode.IsLink() {
if te, err := file.Entry.FollowLink(); err == nil && te.IsDir() {
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink")
}
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
}

name := m.findIconNameByGit(entry)
if name == "folder" {
// the material icon pack's "folder" icon doesn't look good, so use our built-in one
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
return svg.RenderHTML("material-folder-generic", 16, "octicon-file-directory-fill")
name := m.findIconNameByGit(file)

extraClass := "octicon-file"
switch {
case file.EntryMode.IsDir():
extraClass = BasicThemeFolderIconName(false)
case file.EntryMode.IsSubModule():
extraClass = "octicon-file-submodule"
}

return m.renderIconByName(ctx, name, extraClass)
}

func (m *MaterialIconProvider) renderIconByName(ctx reqctx.RequestContext, name, extraClass string) template.HTML {
if iconSVG, ok := m.svgs[name]; ok && iconSVG != "" {
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
extraClass := "octicon-file"
switch {
case entry.IsDir():
extraClass = "octicon-file-directory-fill"
case entry.IsSubModule():
extraClass = "octicon-file-submodule"
}
return m.renderFileIconSVG(ctx, name, iconSVG, extraClass)
}
return svg.RenderHTML("octicon-file")
Expand Down Expand Up @@ -159,9 +168,9 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
return "file"
}

func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string {
if entry.IsSubModule() {
func (m *MaterialIconProvider) findIconNameByGit(file *FileIcon) string {
if file.EntryMode.IsSubModule() {
return "folder-git"
}
return m.FindIconName(entry.Name(), entry.IsDir())
return m.FindIconName(file.Name, file.EntryMode.IsDir())
}
20 changes: 20 additions & 0 deletions modules/git/tree_entry_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ func (e EntryMode) String() string {
return strconv.FormatInt(int64(e), 8)
}

func (e EntryMode) IsSubModule() bool {
return e == EntryModeCommit
}

func (e EntryMode) IsDir() bool {
return e == EntryModeTree
}

func (e EntryMode) IsLink() bool {
return e == EntryModeSymlink
}

func (e EntryMode) IsRegular() bool {
return e == EntryModeBlob
}

func (e EntryMode) IsExecutable() bool {
return e == EntryModeExec
}

// ToEntryMode converts a string to an EntryMode
func ToEntryMode(value string) EntryMode {
v, _ := strconv.ParseInt(value, 8, 32)
Expand Down
10 changes: 5 additions & 5 deletions modules/git/tree_entry_nogogit.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 {

// IsSubModule if the entry is a sub module
func (te *TreeEntry) IsSubModule() bool {
return te.entryMode == EntryModeCommit
return te.entryMode.IsSubModule()
}

// IsDir if the entry is a sub dir
func (te *TreeEntry) IsDir() bool {
return te.entryMode == EntryModeTree
return te.entryMode.IsDir()
}

// IsLink if the entry is a symlink
func (te *TreeEntry) IsLink() bool {
return te.entryMode == EntryModeSymlink
return te.entryMode.IsLink()
}

// IsRegular if the entry is a regular file
func (te *TreeEntry) IsRegular() bool {
return te.entryMode == EntryModeBlob
return te.entryMode.IsRegular()
}

// IsExecutable if the entry is an executable file (not necessarily binary)
func (te *TreeEntry) IsExecutable() bool {
return te.entryMode == EntryModeExec
return te.entryMode.IsExecutable()
}

// Blob returns the blob object the entry
Expand Down
29 changes: 26 additions & 3 deletions modules/templates/util_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,34 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
textColor, itemColor, itemHTML)
}

func (ut *RenderUtils) RenderFileIcon(entry *git.TreeEntry) template.HTML {
func (ut *RenderUtils) RenderExpandedFolderIcon() template.HTML {
return ut.RenderFolderIconByExpansionState(true)
}

func (ut *RenderUtils) RenderCollapsedFolderIcon() template.HTML {
return ut.RenderFolderIconByExpansionState(false)
}

func (ut *RenderUtils) RenderFolderIconByExpansionState(isOpen bool) template.HTML {
if setting.UI.FileIconTheme == "material" {
return fileicon.DefaultMaterialIconProvider().FolderIcon(ut.ctx, isOpen)
}
return fileicon.BasicThemeFolderIcon(isOpen)
}

func (ut *RenderUtils) RenderFileIconByGitTreeEntry(entry *git.TreeEntry) template.HTML {
return ut.RenderFileIcon(&fileicon.FileIcon{
Name: entry.Name(),
Entry: *entry,
EntryMode: entry.Mode(),
})
}

func (ut *RenderUtils) RenderFileIcon(file *fileicon.FileIcon) template.HTML {
if setting.UI.FileIconTheme == "material" {
return fileicon.DefaultMaterialIconProvider().FileIcon(ut.ctx, entry)
return fileicon.DefaultMaterialIconProvider().FileIcon(ut.ctx, file)
}
return fileicon.BasicThemeIcon(entry)
return fileicon.BasicThemeIcon(file)
}

// RenderEmoji renders html text with emoji post processors
Expand Down
2 changes: 1 addition & 1 deletion routers/web/repo/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ func Diff(ctx *context.Context) {
return
}

ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, nil)
ctx.PageData["DiffFiles"] = transformDiffTreeForUI(ctx, diffTree, nil)
}

statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
Expand Down
20 changes: 10 additions & 10 deletions routers/web/repo/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,16 +638,6 @@ func PrepareCompareDiff(
ctx.Data["Diff"] = diff
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0

if !fileOnly {
diffTree, err := gitdiff.GetDiffTree(ctx, ci.HeadGitRepo, false, beforeCommitID, headCommitID)
if err != nil {
ctx.ServerError("GetDiffTree", err)
return false
}

ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, nil)
}

headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
Expand All @@ -662,6 +652,16 @@ func PrepareCompareDiff(
return false
}

if !fileOnly {
diffTree, err := gitdiff.GetDiffTree(ctx, ci.HeadGitRepo, false, beforeCommitID, headCommitID)
if err != nil {
ctx.ServerError("GetDiffTree", err)
return false
}

ctx.PageData["DiffFiles"] = transformDiffTreeForUI(ctx, diffTree, nil)
}

commits, err := processGitCommits(ctx, ci.CompareInfo.Commits)
if err != nil {
ctx.ServerError("processGitCommits", err)
Expand Down
25 changes: 13 additions & 12 deletions routers/web/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,18 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
}

commit, err := gitRepo.GetCommit(endCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}

baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}

if !fileOnly {
// note: use mergeBase is set to false because we already have the merge base from the pull request info
diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, startCommitID, endCommitID)
Expand All @@ -834,23 +846,12 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
}

ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, filesViewedState)
ctx.PageData["DiffFiles"] = transformDiffTreeForUI(ctx, diffTree, filesViewedState)
}

ctx.Data["Diff"] = diff
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0

baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
commit, err := gitRepo.GetCommit(endCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}

if ctx.IsSigned && ctx.Doer != nil {
if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil {
ctx.ServerError("CanMarkConversation", err)
Expand Down
17 changes: 13 additions & 4 deletions routers/web/repo/treelist.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
package repo

import (
"html/template"
"net/http"

pull_model "code.gitea.io/gitea/models/pull"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/gitdiff"
files_service "code.gitea.io/gitea/services/repository/files"
Expand Down Expand Up @@ -59,24 +63,29 @@ func isExcludedEntry(entry *git.TreeEntry) bool {
type FileDiffFile struct {
Name string
NameHash string
FileIcon template.HTML
IsSubmodule bool
IsViewed bool
Status string
}

// transformDiffTreeForUI transforms a DiffTree into a slice of FileDiffFile for UI rendering
// it also takes a map of file names to their viewed state, which is used to mark files as viewed
func transformDiffTreeForUI(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) []FileDiffFile {
func transformDiffTreeForUI(ctx *context.Context, diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) []FileDiffFile {
files := make([]FileDiffFile, 0, len(diffTree.Files))

renderUtils := templates.NewRenderUtils(reqctx.FromContext(ctx))
for _, file := range diffTree.Files {
nameHash := git.HashFilePathForWebUI(file.HeadPath)
isSubmodule := file.HeadMode == git.EntryModeCommit
isViewed := filesViewedState[file.HeadPath] == pull_model.Viewed

files = append(files, FileDiffFile{
Name: file.HeadPath,
NameHash: nameHash,
Name: file.HeadPath,
NameHash: nameHash,
FileIcon: renderUtils.RenderFileIcon(&fileicon.FileIcon{
Name: file.HeadPath,
EntryMode: file.HeadMode,
}),
IsSubmodule: isSubmodule,
IsViewed: isViewed,
Status: file.Status,
Expand Down
Loading