diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..eda1abeeeb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +# Instructions for agents + +- Use `make help` to find available development targets +- Before committing go code changes, run `make fmt` +- Before committing `go.mod` changes, run `make tidy` +- Before committing new `.go` files, add the current year into the copyright header +- Before committing files, removed any trailing whitespace diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 3eaffde970..084c66aab0 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1329,9 +1329,12 @@ LEVEL = Info ;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css" ;THEMES = ;; -;; The icons for file list (basic/material), this is a temporary option which will be replaced by a user setting in the future. +;; The icon theme for files (basic/material) ;FILE_ICON_THEME = material ;; +;; The icon theme for folders (basic/material) +;FOLDER_ICON_THEME = basic +;; ;; All available reactions users can choose on issues/prs and comments. ;; Values can be emoji alias (:smile:) or a unicode emoji. ;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png diff --git a/modules/fileicon/render.go b/modules/fileicon/render.go index 6b2fcfa81e..5bf2a3a02e 100644 --- a/modules/fileicon/render.go +++ b/modules/fileicon/render.go @@ -34,7 +34,13 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML { } func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML { - if setting.UI.FileIconTheme == "material" { + // Use folder theme for directories and symlinks to directories + theme := setting.UI.FileIconTheme + if entry.EntryMode.IsDir() || (entry.EntryMode.IsLink() && entry.SymlinkToMode.IsDir()) { + theme = setting.UI.FolderIconTheme + } + + if theme == "material" { return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry) } return BasicEntryIconHTML(entry) diff --git a/modules/fileicon/render_test.go b/modules/fileicon/render_test.go new file mode 100644 index 0000000000..d9998f3f4c --- /dev/null +++ b/modules/fileicon/render_test.go @@ -0,0 +1,75 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package fileicon_test + +import ( + "testing" + + "code.gitea.io/gitea/modules/fileicon" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestRenderEntryIconHTML_WithDifferentThemes(t *testing.T) { + // Test that folder icons use the folder theme + t.Run("FolderUsesBasicTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")() + + folderEntry := &fileicon.EntryInfo{ + BaseName: "testfolder", + EntryMode: git.EntryModeTree, + } + + html := fileicon.RenderEntryIconHTML(nil, folderEntry) + // Basic theme renders octicon classes + assert.Contains(t, string(html), "octicon-file-directory-fill") + }) + + t.Run("FileUsesMaterialTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")() + + fileEntry := &fileicon.EntryInfo{ + BaseName: "test.js", + EntryMode: git.EntryModeBlob, + } + + html := fileicon.RenderEntryIconHTML(nil, fileEntry) + // Material theme for files renders material icons + assert.Contains(t, string(html), "svg-mfi-") + }) + + t.Run("SymlinkToFolderUsesBasicTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")() + + symlinkEntry := &fileicon.EntryInfo{ + BaseName: "link", + EntryMode: git.EntryModeSymlink, + SymlinkToMode: git.EntryModeTree, + } + + html := fileicon.RenderEntryIconHTML(nil, symlinkEntry) + // Symlinks to folders should use folder theme + assert.Contains(t, string(html), "octicon-file-directory-symlink") + }) + + t.Run("BothMaterialTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "material")() + + folderEntry := &fileicon.EntryInfo{ + BaseName: "testfolder", + EntryMode: git.EntryModeTree, + } + + html := fileicon.RenderEntryIconHTML(nil, folderEntry) + // Material theme for folders renders material folder icons + assert.Contains(t, string(html), "svg-mfi-") + }) +} diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 13cb0f5c66..77a5b45d0a 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -29,6 +29,7 @@ var UI = struct { DefaultTheme string Themes []string FileIconTheme string + FolderIconTheme string Reactions []string ReactionsLookup container.Set[string] `ini:"-"` CustomEmojis []string @@ -88,6 +89,7 @@ var UI = struct { MaxDisplayFileSize: 8388608, DefaultTheme: `gitea-auto`, FileIconTheme: `material`, + FolderIconTheme: `basic`, Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go index e7511b3eed..b85f65f431 100644 --- a/services/repository/files/tree_test.go +++ b/services/repository/files/tree_test.go @@ -56,6 +56,7 @@ func TestGetTreeBySHA(t *testing.T) { func TestGetTreeViewNodes(t *testing.T) { unittest.PrepareTestEnv(t) + ctx, _ := contexttest.MockContext(t, "user2/repo1") ctx.Repo.RefFullName = git.RefNameFromBranch("sub-home-md-img-check") contexttest.LoadRepo(t, ctx, 1) @@ -69,11 +70,13 @@ func TestGetTreeViewNodes(t *testing.T) { mockIconForFile := func(id string) template.HTML { return template.HTML(``) } - mockIconForFolder := func(id string) template.HTML { - return template.HTML(``) + mockIconForFolder := func() template.HTML { + // With basic theme (default for folders), we get octicon icons without IDs + return template.HTML(`octicon-file-directory-fill(16/)`) } - mockOpenIconForFolder := func(id string) template.HTML { - return template.HTML(``) + mockOpenIconForFolder := func() template.HTML { + // With basic theme (default for folders), we get octicon icons without IDs + return template.HTML(`octicon-file-directory-open-fill(16/)`) } treeNodes, err := GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "") assert.NoError(t, err) @@ -82,8 +85,8 @@ func TestGetTreeViewNodes(t *testing.T) { EntryName: "docs", EntryMode: "tree", FullPath: "docs", - EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), - EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`), + EntryIcon: mockIconForFolder(), + EntryIconOpen: mockOpenIconForFolder(), }, }, treeNodes) @@ -94,8 +97,8 @@ func TestGetTreeViewNodes(t *testing.T) { EntryName: "docs", EntryMode: "tree", FullPath: "docs", - EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), - EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`), + EntryIcon: mockIconForFolder(), + EntryIconOpen: mockOpenIconForFolder(), Children: []*TreeViewNode{ { EntryName: "README.md",