Skip to content

Commit 5f78cc8

Browse files
committed
Merge branch 'main' into lunny/optimaze_feed_count
2 parents ec86404 + 7fa47de commit 5f78cc8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+11122
-130
lines changed

custom/conf/app.example.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,9 @@ LEVEL = Info
12941294
;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css"
12951295
;THEMES =
12961296
;;
1297+
;; The icons for file list (basic/material), this is a temporary option which will be replaced by a user setting in the future.
1298+
;FILE_ICON_THEME = material
1299+
;;
12971300
;; All available reactions users can choose on issues/prs and comments.
12981301
;; Values can be emoji alias (:smile:) or a unicode emoji.
12991302
;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png

modules/base/tool.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"strings"
1818
"time"
1919

20-
"code.gitea.io/gitea/modules/git"
2120
"code.gitea.io/gitea/modules/setting"
2221
"code.gitea.io/gitea/modules/util"
2322

@@ -139,24 +138,3 @@ func Int64sToStrings(ints []int64) []string {
139138
}
140139
return strs
141140
}
142-
143-
// EntryIcon returns the octicon name for displaying files/directories
144-
func EntryIcon(entry *git.TreeEntry) string {
145-
switch {
146-
case entry.IsLink():
147-
te, err := entry.FollowLink()
148-
if err != nil {
149-
return "file-symlink-file"
150-
}
151-
if te.IsDir() {
152-
return "file-directory-symlink"
153-
}
154-
return "file-symlink-file"
155-
case entry.IsDir():
156-
return "file-directory-fill"
157-
case entry.IsSubModule():
158-
return "file-submodule"
159-
}
160-
161-
return "file"
162-
}

modules/fileicon/basic.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
9+
"code.gitea.io/gitea/modules/git"
10+
"code.gitea.io/gitea/modules/svg"
11+
)
12+
13+
func BasicThemeIcon(entry *git.TreeEntry) template.HTML {
14+
svgName := "octicon-file"
15+
switch {
16+
case entry.IsLink():
17+
svgName = "octicon-file-symlink-file"
18+
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
19+
svgName = "octicon-file-directory-symlink"
20+
}
21+
case entry.IsDir():
22+
svgName = "octicon-file-directory-fill"
23+
case entry.IsSubModule():
24+
svgName = "octicon-file-submodule"
25+
}
26+
return svg.RenderHTML(svgName)
27+
}

modules/fileicon/material.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
"path"
9+
"strings"
10+
"sync"
11+
12+
"code.gitea.io/gitea/modules/git"
13+
"code.gitea.io/gitea/modules/json"
14+
"code.gitea.io/gitea/modules/log"
15+
"code.gitea.io/gitea/modules/options"
16+
"code.gitea.io/gitea/modules/reqctx"
17+
"code.gitea.io/gitea/modules/svg"
18+
)
19+
20+
type materialIconRulesData struct {
21+
FileNames map[string]string `json:"fileNames"`
22+
FolderNames map[string]string `json:"folderNames"`
23+
FileExtensions map[string]string `json:"fileExtensions"`
24+
}
25+
26+
type MaterialIconProvider struct {
27+
once sync.Once
28+
rules *materialIconRulesData
29+
svgs map[string]string
30+
}
31+
32+
var materialIconProvider MaterialIconProvider
33+
34+
func DefaultMaterialIconProvider() *MaterialIconProvider {
35+
materialIconProvider.once.Do(materialIconProvider.loadData)
36+
return &materialIconProvider
37+
}
38+
39+
func (m *MaterialIconProvider) loadData() {
40+
buf, err := options.AssetFS().ReadFile("fileicon/material-icon-rules.json")
41+
if err != nil {
42+
log.Error("Failed to read material icon rules: %v", err)
43+
return
44+
}
45+
err = json.Unmarshal(buf, &m.rules)
46+
if err != nil {
47+
log.Error("Failed to unmarshal material icon rules: %v", err)
48+
return
49+
}
50+
51+
buf, err = options.AssetFS().ReadFile("fileicon/material-icon-svgs.json")
52+
if err != nil {
53+
log.Error("Failed to read material icon rules: %v", err)
54+
return
55+
}
56+
err = json.Unmarshal(buf, &m.svgs)
57+
if err != nil {
58+
log.Error("Failed to unmarshal material icon rules: %v", err)
59+
return
60+
}
61+
log.Debug("Loaded material icon rules and SVG images")
62+
}
63+
64+
func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name, svg string) template.HTML {
65+
data := ctx.GetData()
66+
renderedSVGs, _ := data["_RenderedSVGs"].(map[string]bool)
67+
if renderedSVGs == nil {
68+
renderedSVGs = make(map[string]bool)
69+
data["_RenderedSVGs"] = renderedSVGs
70+
}
71+
// 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.
72+
// Will try to refactor this in the future.
73+
if !strings.HasPrefix(svg, "<svg") {
74+
panic("Invalid SVG icon")
75+
}
76+
svgID := "svg-mfi-" + name
77+
svgCommonAttrs := `class="svg fileicon" width="16" height="16" aria-hidden="true"`
78+
posOuterBefore := strings.IndexByte(svg, '>')
79+
if renderedSVGs[svgID] && posOuterBefore != -1 {
80+
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
81+
}
82+
svg = `<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]
83+
renderedSVGs[svgID] = true
84+
return template.HTML(svg)
85+
}
86+
87+
func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.TreeEntry) template.HTML {
88+
if m.rules == nil {
89+
return BasicThemeIcon(entry)
90+
}
91+
92+
if entry.IsLink() {
93+
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
94+
return svg.RenderHTML("material-folder-symlink")
95+
}
96+
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
97+
}
98+
99+
name := m.findIconNameByGit(entry)
100+
if name == "folder" {
101+
// the material icon pack's "folder" icon doesn't look good, so use our built-in one
102+
return svg.RenderHTML("material-folder-generic")
103+
}
104+
if iconSVG, ok := m.svgs[name]; ok && iconSVG != "" {
105+
return m.renderFileIconSVG(ctx, name, iconSVG)
106+
}
107+
return svg.RenderHTML("octicon-file")
108+
}
109+
110+
func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
111+
iconsData := m.rules
112+
fileNameLower := strings.ToLower(path.Base(name))
113+
if isDir {
114+
if s, ok := iconsData.FolderNames[fileNameLower]; ok {
115+
return s
116+
}
117+
return "folder"
118+
}
119+
120+
if s, ok := iconsData.FileNames[fileNameLower]; ok {
121+
return s
122+
}
123+
124+
for i := len(fileNameLower) - 1; i >= 0; i-- {
125+
if fileNameLower[i] == '.' {
126+
ext := fileNameLower[i+1:]
127+
if s, ok := iconsData.FileExtensions[ext]; ok {
128+
return s
129+
}
130+
}
131+
}
132+
133+
return "file"
134+
}
135+
136+
func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string {
137+
if entry.IsSubModule() {
138+
return "folder-git"
139+
}
140+
return m.FindIconName(entry.Name(), entry.IsDir())
141+
}

modules/fileicon/material_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package fileicon_test
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/unittest"
10+
"code.gitea.io/gitea/modules/fileicon"
11+
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestMain(m *testing.M) {
16+
unittest.MainTest(m, &unittest.TestOptions{FixtureFiles: []string{}})
17+
}
18+
19+
func TestFindIconName(t *testing.T) {
20+
unittest.PrepareTestEnv(t)
21+
p := fileicon.DefaultMaterialIconProvider()
22+
assert.Equal(t, "php", p.FindIconName("foo.php", false))
23+
assert.Equal(t, "php", p.FindIconName("foo.PHP", false))
24+
}

modules/git/commit_info_nogogit.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
6565
log.Debug("missing commit for %s", entry.Name())
6666
}
6767

68-
// If the entry if a submodule add a submodule file for this
68+
// If the entry is a submodule add a submodule file for this
6969
if entry.IsSubModule() {
7070
subModuleURL := ""
7171
var fullPath string
@@ -85,8 +85,8 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
8585
}
8686

8787
// Retrieve the commit for the treePath itself (see above). We basically
88-
// get it for free during the tree traversal and it's used for listing
89-
// pages to display information about newest commit for a given path.
88+
// get it for free during the tree traversal, and it's used for listing
89+
// pages to display information about the newest commit for a given path.
9090
var treeCommit *Commit
9191
var ok bool
9292
if treePath == "" {

modules/httplib/request.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"context"
1010
"crypto/tls"
11+
"errors"
1112
"fmt"
1213
"io"
1314
"net"
@@ -101,6 +102,9 @@ func (r *Request) Param(key, value string) *Request {
101102

102103
// Body adds request raw body. It supports string, []byte and io.Reader as body.
103104
func (r *Request) Body(data any) *Request {
105+
if r == nil {
106+
return nil
107+
}
104108
switch t := data.(type) {
105109
case nil: // do nothing
106110
case string:
@@ -193,6 +197,9 @@ func (r *Request) getResponse() (*http.Response, error) {
193197
// Response executes request client gets response manually.
194198
// Caller MUST close the response body if no error occurs
195199
func (r *Request) Response() (*http.Response, error) {
200+
if r == nil {
201+
return nil, errors.New("invalid request")
202+
}
196203
return r.getResponse()
197204
}
198205

modules/lfstransfer/backend/backend.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,13 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
7070
g.logger.Log("json marshal error", err)
7171
return nil, err
7272
}
73-
url := g.server.JoinPath("objects/batch").String()
7473
headers := map[string]string{
7574
headerAuthorization: g.authToken,
7675
headerGiteaInternalAuth: g.internalAuth,
7776
headerAccept: mimeGitLFS,
7877
headerContentType: mimeGitLFS,
7978
}
80-
req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
79+
req := newInternalRequestLFS(g.ctx, g.server.JoinPath("objects/batch").String(), http.MethodPost, headers, bodyBytes)
8180
resp, err := req.Response()
8281
if err != nil {
8382
g.logger.Log("http request error", err)
@@ -179,13 +178,12 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser,
179178
g.logger.Log("argument id incorrect")
180179
return nil, 0, transfer.ErrCorruptData
181180
}
182-
url := action.Href
183181
headers := map[string]string{
184182
headerAuthorization: g.authToken,
185183
headerGiteaInternalAuth: g.internalAuth,
186184
headerAccept: mimeOctetStream,
187185
}
188-
req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil)
186+
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodGet, headers, nil)
189187
resp, err := req.Response()
190188
if err != nil {
191189
return nil, 0, fmt.Errorf("failed to get response: %w", err)
@@ -225,15 +223,14 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer
225223
g.logger.Log("argument id incorrect")
226224
return transfer.ErrCorruptData
227225
}
228-
url := action.Href
229226
headers := map[string]string{
230227
headerAuthorization: g.authToken,
231228
headerGiteaInternalAuth: g.internalAuth,
232229
headerContentType: mimeOctetStream,
233230
headerContentLength: strconv.FormatInt(size, 10),
234231
}
235232

236-
req := newInternalRequestLFS(g.ctx, url, http.MethodPut, headers, nil)
233+
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPut, headers, nil)
237234
req.Body(r)
238235
resp, err := req.Response()
239236
if err != nil {
@@ -274,14 +271,13 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans
274271
// the server sent no verify action
275272
return transfer.SuccessStatus(), nil
276273
}
277-
url := action.Href
278274
headers := map[string]string{
279275
headerAuthorization: g.authToken,
280276
headerGiteaInternalAuth: g.internalAuth,
281277
headerAccept: mimeGitLFS,
282278
headerContentType: mimeGitLFS,
283279
}
284-
req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
280+
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPost, headers, bodyBytes)
285281
resp, err := req.Response()
286282
if err != nil {
287283
return transfer.NewStatus(transfer.StatusInternalServerError), err

0 commit comments

Comments
 (0)