0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-06 08:29:54 +02:00

Merge branch 'main' into lunny/some_refactors

This commit is contained in:
Lunny Xiao 2026-01-14 09:25:47 -08:00 committed by GitHub
commit 733c5b608a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1348 additions and 903 deletions

View File

@ -80,7 +80,7 @@ The more detailed and specific you are, the faster we can fix the issue. \
It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. <https://demo.gitea.com>, as perhaps your problem has already been fixed on a current version. \
Please follow the guidelines described in [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) for your report.
Please be kind, remember that Gitea comes at no cost to you, and you're getting free help.
Please be kindremember that Gitea comes at no cost to you, and you're getting free help.
### Types of issues

View File

@ -233,7 +233,7 @@ export default defineConfig([
'@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'all', caughtErrors: 'all', ignoreRestSiblings: false, argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'}],
'@typescript-eslint/no-use-before-define': [2, {functions: false, classes: true, variables: true, allowNamedExports: true, typedefs: false, enums: false, ignoreTypeReferences: true}],
'@typescript-eslint/no-useless-constructor': [0],
'@typescript-eslint/no-useless-default-assignment': [0], // https://github.com/typescript-eslint/typescript-eslint/issues/11847
'@typescript-eslint/no-useless-default-assignment': [2],
'@typescript-eslint/no-useless-empty-export': [0],
'@typescript-eslint/no-wrapper-object-types': [2],
'@typescript-eslint/non-nullable-type-assertion-style': [0],
@ -264,6 +264,7 @@ export default defineConfig([
'@typescript-eslint/restrict-template-expressions': [0],
'@typescript-eslint/return-await': [0],
'@typescript-eslint/strict-boolean-expressions': [0],
'@typescript-eslint/strict-void-return': [0],
'@typescript-eslint/switch-exhaustiveness-check': [0],
'@typescript-eslint/triple-slash-reference': [2],
'@typescript-eslint/typedef': [0],

View File

@ -12,6 +12,8 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
// Stopwatch represents a stopwatch for time tracking.
@ -232,3 +234,14 @@ func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (
})
return ok, err
}
// RemoveStopwatchesByRepoID removes all stopwatches for a user in a specific repository
// this function should be called before removing all the issues of the repository
func RemoveStopwatchesByRepoID(ctx context.Context, userID, repoID int64) error {
_, err := db.GetEngine(ctx).
Where("`stopwatch`.user_id = ?", userID).
And(builder.In("`stopwatch`.issue_id",
builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repoID}))).
Delete(new(Stopwatch))
return err
}

View File

@ -93,15 +93,21 @@ func init() {
db.RegisterModel(new(Release))
}
// LoadAttributes load repo and publisher attributes for a release
func (r *Release) LoadAttributes(ctx context.Context) error {
var err error
if r.Repo == nil {
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
if err != nil {
return err
}
func (r *Release) LoadRepo(ctx context.Context) (err error) {
if r.Repo != nil {
return nil
}
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
return err
}
// LoadAttributes load repo and publisher attributes for a release
func (r *Release) LoadAttributes(ctx context.Context) (err error) {
if err := r.LoadRepo(ctx); err != nil {
return err
}
if r.Publisher == nil {
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
if err != nil {

View File

@ -176,3 +176,13 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error
}
return watchRepoMode(ctx, watch, WatchModeAuto)
}
// ClearRepoWatches clears all watches for a repository and from the user that watched it.
// Used when a repository is set to private.
func ClearRepoWatches(ctx context.Context, repoID int64) error {
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_watches = 0 WHERE id = ?", repoID); err != nil {
return err
}
return db.DeleteBeans(ctx, Watch{RepoID: repoID})
}

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIsWatching(t *testing.T) {
@ -119,3 +120,21 @@ func TestWatchIfAuto(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
}
func TestClearRepoWatches(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const repoID int64 = 1
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repoID)
require.NoError(t, err)
require.NotEmpty(t, watchers)
assert.NoError(t, repo_model.ClearRepoWatches(t.Context(), repoID))
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repoID)
assert.NoError(t, err)
assert.Empty(t, watchers)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
assert.Zero(t, repo.NumWatches)
}

View File

@ -102,7 +102,13 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
}
// ToggleUserOpenIDVisibility toggles visibility of an openid address of given user.
func ToggleUserOpenIDVisibility(ctx context.Context, id int64) (err error) {
_, err = db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ?", id)
return err
func ToggleUserOpenIDVisibility(ctx context.Context, id int64, user *User) error {
affected, err := db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ? AND uid = ?", id, user.ID)
if err != nil {
return err
}
if n, _ := affected.RowsAffected(); n != 1 {
return util.NewNotExistErrorf("OpenID is unknown")
}
return nil
}

View File

@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -33,12 +34,14 @@ func TestGetUserOpenIDs(t *testing.T) {
func TestToggleUserOpenIDVisibility(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user, err := user_model.GetUserByID(t.Context(), int64(2))
require.NoError(t, err)
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2))
require.NoError(t, err)
require.Len(t, oids, 1)
assert.True(t, oids[0].Show)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
require.NoError(t, err)
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
@ -46,7 +49,7 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
require.Len(t, oids, 1)
assert.False(t, oids[0].Show)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
require.NoError(t, err)
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
@ -55,3 +58,13 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
assert.True(t, oids[0].Show)
}
}
func TestToggleUserOpenIDVisibilityRequiresOwnership(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
unauthorizedUser, err := user_model.GetUserByID(t.Context(), int64(2))
require.NoError(t, err)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), int64(1), unauthorizedUser)
require.Error(t, err)
assert.ErrorIs(t, err, util.ErrNotExist)
}

View File

@ -9,6 +9,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
@ -91,7 +92,13 @@ loop:
}
for _, userStopwatches := range usersStopwatches {
apiSWs, err := convert.ToStopWatches(ctx, userStopwatches.StopWatches)
u, err := user_model.GetUserByID(ctx, userStopwatches.UserID)
if err != nil {
log.Error("Unable to get user %d: %v", userStopwatches.UserID, err)
continue
}
apiSWs, err := convert.ToStopWatches(ctx, u, userStopwatches.StopWatches)
if err != nil {
if !issues_model.IsErrIssueNotExist(err) {
log.Error("Unable to APIFormat stopwatches: %v", err)

View File

@ -107,6 +107,17 @@ func detectFileTypeBox(data []byte) (brands []string, found bool) {
return brands, true
}
func isEmbeddedOpenType(data []byte) bool {
// https://www.w3.org/submissions/EOT
if len(data) < 80 {
return false
}
version := binary.LittleEndian.Uint32(data[8:]) // Actually this standard is abandoned (for IE6-IE11 only), there are only 3 versions defined
magic := binary.LittleEndian.Uint16(data[34:36]) // MagicNumber: 0x504C ("LP")
reserved := data[64:80] // Reserved 1-4 (each: unsigned long)
return (version == 0x00010000 || version == 0x00020001 || version == 0x00020002) && magic == 0x504C && bytes.Count(reserved, []byte{0}) == len(reserved)
}
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/plain if input is empty.
func DetectContentType(data []byte) SniffedType {
if len(data) == 0 {
@ -119,6 +130,18 @@ func DetectContentType(data []byte) SniffedType {
data = data[:SniffContentSize]
}
const typeMsFontObject = "application/vnd.ms-fontobject"
if ct == typeMsFontObject {
// Stupid Golang blindly detects any content with 34th-35th bytes being "LP" as font.
// If it is not really for ".eot" content, we try to detect it again by hiding the "LP", see the test for more details.
if isEmbeddedOpenType(data) {
return SniffedType{typeMsFontObject}
}
data = slices.Clone(data)
data[34] = 'l'
ct = http.DetectContentType(data)
}
vars := globalVars()
// SVG is unsupported by http.DetectContentType, https://github.com/golang/go/issues/15888
detectByHTML := strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html")

View File

@ -6,6 +6,7 @@ package typesniffer
import (
"encoding/base64"
"encoding/hex"
"net/http"
"strings"
"testing"
@ -154,3 +155,25 @@ func TestDetectContentTypeAvif(t *testing.T) {
st := DetectContentType(buf)
assert.Equal(t, MimeTypeImageAvif, st.contentType)
}
func TestDetectContentTypeIncorrectFont(t *testing.T) {
s := "Stupid Golang keep detecting 34th LP as font"
// They don't want to have any improvement to it: https://github.com/golang/go/issues/77172
golangDetected := http.DetectContentType([]byte(s))
assert.Equal(t, "application/vnd.ms-fontobject", golangDetected)
// We have to make our patch to make it work correctly
ourDetected := DetectContentType([]byte(s))
assert.Equal(t, "text/plain; charset=utf-8", ourDetected.contentType)
// For binary content, ensure it still detects as font. The content is from "opensans-regular.eot"
b := []byte{
0x3d, 0x30, 0x00, 0x00, 0x6b, 0x2f, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00,
0x02, 0x0b, 0x06, 0x06, 0x03, 0x05, 0x04, 0x02, 0x02, 0x04, 0x01, 0x00, 0x90, 0x01, 0x00, 0x00,
0x04, 0x00, 0x4c, 0x50, 0xef, 0x02, 0x00, 0xe0, 0x5b, 0x20, 0x00, 0x40, 0x28, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x63, 0xf4, 0x17, 0x14,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x12, 0x00, 0x4f, 0x00, 0x70, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x20, 0x00, 0x53, 0x00,
}
assert.Equal(t, "application/vnd.ms-fontobject", http.DetectContentType(b))
assert.Equal(t, "application/vnd.ms-fontobject", DetectContentType(b).contentType)
}

View File

@ -460,6 +460,22 @@
".blog": "folder-docs",
"_blog": "folder-docs",
"__blog__": "folder-docs",
"knowledge": "folder-docs",
".knowledge": "folder-docs",
"_knowledge": "folder-docs",
"__knowledge__": "folder-docs",
"diary": "folder-docs",
".diary": "folder-docs",
"_diary": "folder-docs",
"__diary__": "folder-docs",
"note": "folder-docs",
".note": "folder-docs",
"_note": "folder-docs",
"__note__": "folder-docs",
"notes": "folder-docs",
".notes": "folder-docs",
"_notes": "folder-docs",
"__notes__": "folder-docs",
"github/workflows": "folder-gh-workflows",
".github/workflows": "folder-gh-workflows",
"_github/workflows": "folder-gh-workflows",
@ -916,6 +932,14 @@
".sql": "folder-database",
"_sql": "folder-database",
"__sql__": "folder-database",
"migrations": "folder-migrations",
".migrations": "folder-migrations",
"_migrations": "folder-migrations",
"__migrations__": "folder-migrations",
"migration": "folder-migrations",
".migration": "folder-migrations",
"_migration": "folder-migrations",
"__migration__": "folder-migrations",
"log": "folder-log",
".log": "folder-log",
"_log": "folder-log",
@ -1008,6 +1032,14 @@
".recordings": "folder-audio",
"_recordings": "folder-audio",
"__recordings__": "folder-audio",
"playlist": "folder-audio",
".playlist": "folder-audio",
"_playlist": "folder-audio",
"__playlist__": "folder-audio",
"playlists": "folder-audio",
".playlists": "folder-audio",
"_playlists": "folder-audio",
"__playlists__": "folder-audio",
"vid": "folder-video",
".vid": "folder-video",
"_vid": "folder-video",
@ -1544,6 +1576,22 @@
".backends": "folder-server",
"_backends": "folder-server",
"__backends__": "folder-server",
"inventory": "folder-server",
".inventory": "folder-server",
"_inventory": "folder-server",
"__inventory__": "folder-server",
"inventories": "folder-server",
".inventories": "folder-server",
"_inventories": "folder-server",
"__inventories__": "folder-server",
"infrastructure": "folder-server",
".infrastructure": "folder-server",
"_infrastructure": "folder-server",
"__infrastructure__": "folder-server",
"infra": "folder-server",
".infra": "folder-server",
"_infra": "folder-server",
"__infra__": "folder-server",
"client": "folder-client",
".client": "folder-client",
"_client": "folder-client",
@ -1992,6 +2040,14 @@
".calculations": "folder-functions",
"_calculations": "folder-functions",
"__calculations__": "folder-functions",
"composable": "folder-functions",
".composable": "folder-functions",
"_composable": "folder-functions",
"__composable__": "folder-functions",
"composables": "folder-functions",
".composables": "folder-functions",
"_composables": "folder-functions",
"__composables__": "folder-functions",
"generator": "folder-generator",
".generator": "folder-generator",
"_generator": "folder-generator",
@ -2936,6 +2992,14 @@
".projects": "folder-project",
"_projects": "folder-project",
"__projects__": "folder-project",
"proj": "folder-project",
".proj": "folder-project",
"_proj": "folder-project",
"__proj__": "folder-project",
"projs": "folder-project",
".projs": "folder-project",
"_projs": "folder-project",
"__projs__": "folder-project",
"prompt": "folder-prompts",
".prompt": "folder-prompts",
"_prompt": "folder-prompts",
@ -3447,6 +3511,14 @@
".in": "folder-input",
"_in": "folder-input",
"__in__": "folder-input",
"salt": "folder-salt",
".salt": "folder-salt",
"_salt": "folder-salt",
"__salt__": "folder-salt",
"saltstack": "folder-salt",
".saltstack": "folder-salt",
"_saltstack": "folder-salt",
"__saltstack__": "folder-salt",
"simulations": "folder-simulations",
".simulations": "folder-simulations",
"_simulations": "folder-simulations",
@ -3961,6 +4033,22 @@
".blog": "folder-docs-open",
"_blog": "folder-docs-open",
"__blog__": "folder-docs-open",
"knowledge": "folder-docs-open",
".knowledge": "folder-docs-open",
"_knowledge": "folder-docs-open",
"__knowledge__": "folder-docs-open",
"diary": "folder-docs-open",
".diary": "folder-docs-open",
"_diary": "folder-docs-open",
"__diary__": "folder-docs-open",
"note": "folder-docs-open",
".note": "folder-docs-open",
"_note": "folder-docs-open",
"__note__": "folder-docs-open",
"notes": "folder-docs-open",
".notes": "folder-docs-open",
"_notes": "folder-docs-open",
"__notes__": "folder-docs-open",
"github/workflows": "folder-gh-workflows-open",
".github/workflows": "folder-gh-workflows-open",
"_github/workflows": "folder-gh-workflows-open",
@ -4417,6 +4505,14 @@
".sql": "folder-database-open",
"_sql": "folder-database-open",
"__sql__": "folder-database-open",
"migrations": "folder-migrations-open",
".migrations": "folder-migrations-open",
"_migrations": "folder-migrations-open",
"__migrations__": "folder-migrations-open",
"migration": "folder-migrations-open",
".migration": "folder-migrations-open",
"_migration": "folder-migrations-open",
"__migration__": "folder-migrations-open",
"log": "folder-log-open",
".log": "folder-log-open",
"_log": "folder-log-open",
@ -4509,6 +4605,14 @@
".recordings": "folder-audio-open",
"_recordings": "folder-audio-open",
"__recordings__": "folder-audio-open",
"playlist": "folder-audio-open",
".playlist": "folder-audio-open",
"_playlist": "folder-audio-open",
"__playlist__": "folder-audio-open",
"playlists": "folder-audio-open",
".playlists": "folder-audio-open",
"_playlists": "folder-audio-open",
"__playlists__": "folder-audio-open",
"vid": "folder-video-open",
".vid": "folder-video-open",
"_vid": "folder-video-open",
@ -5045,6 +5149,22 @@
".backends": "folder-server-open",
"_backends": "folder-server-open",
"__backends__": "folder-server-open",
"inventory": "folder-server-open",
".inventory": "folder-server-open",
"_inventory": "folder-server-open",
"__inventory__": "folder-server-open",
"inventories": "folder-server-open",
".inventories": "folder-server-open",
"_inventories": "folder-server-open",
"__inventories__": "folder-server-open",
"infrastructure": "folder-server-open",
".infrastructure": "folder-server-open",
"_infrastructure": "folder-server-open",
"__infrastructure__": "folder-server-open",
"infra": "folder-server-open",
".infra": "folder-server-open",
"_infra": "folder-server-open",
"__infra__": "folder-server-open",
"client": "folder-client-open",
".client": "folder-client-open",
"_client": "folder-client-open",
@ -5493,6 +5613,14 @@
".calculations": "folder-functions-open",
"_calculations": "folder-functions-open",
"__calculations__": "folder-functions-open",
"composable": "folder-functions-open",
".composable": "folder-functions-open",
"_composable": "folder-functions-open",
"__composable__": "folder-functions-open",
"composables": "folder-functions-open",
".composables": "folder-functions-open",
"_composables": "folder-functions-open",
"__composables__": "folder-functions-open",
"generator": "folder-generator-open",
".generator": "folder-generator-open",
"_generator": "folder-generator-open",
@ -6437,6 +6565,14 @@
".projects": "folder-project-open",
"_projects": "folder-project-open",
"__projects__": "folder-project-open",
"proj": "folder-project-open",
".proj": "folder-project-open",
"_proj": "folder-project-open",
"__proj__": "folder-project-open",
"projs": "folder-project-open",
".projs": "folder-project-open",
"_projs": "folder-project-open",
"__projs__": "folder-project-open",
"prompt": "folder-prompts-open",
".prompt": "folder-prompts-open",
"_prompt": "folder-prompts-open",
@ -6948,6 +7084,14 @@
".in": "folder-input-open",
"_in": "folder-input-open",
"__in__": "folder-input-open",
"salt": "folder-salt-open",
".salt": "folder-salt-open",
"_salt": "folder-salt-open",
"__salt__": "folder-salt-open",
"saltstack": "folder-salt-open",
".saltstack": "folder-salt-open",
"_saltstack": "folder-salt-open",
"__saltstack__": "folder-salt-open",
"simulations": "folder-simulations-open",
".simulations": "folder-simulations-open",
"_simulations": "folder-simulations-open",
@ -7213,6 +7357,7 @@
"csproj": "visualstudio",
"ruleset": "visualstudio",
"sln": "visualstudio",
"slnf": "visualstudio",
"slnx": "visualstudio",
"suo": "visualstudio",
"vb": "visualstudio",
@ -8162,6 +8307,7 @@
"toc": "toc",
"cue": "cue",
"lean": "lean",
"sls": "salt",
"cljx": "clojure",
"clojure": "clojure",
"edn": "clojure",
@ -10150,6 +10296,30 @@
"esbuild.config.ts": "esbuild",
"esbuild.config.mts": "esbuild",
"esbuild.config.cts": "esbuild",
"esbuild.dev.js": "esbuild",
"esbuild.dev.mjs": "esbuild",
"esbuild.dev.cjs": "esbuild",
"esbuild.dev.ts": "esbuild",
"esbuild.dev.mts": "esbuild",
"esbuild.dev.cts": "esbuild",
"esbuild.stage.js": "esbuild",
"esbuild.stage.mjs": "esbuild",
"esbuild.stage.cjs": "esbuild",
"esbuild.stage.ts": "esbuild",
"esbuild.stage.mts": "esbuild",
"esbuild.stage.cts": "esbuild",
"esbuild.prod.js": "esbuild",
"esbuild.prod.mjs": "esbuild",
"esbuild.prod.cjs": "esbuild",
"esbuild.prod.ts": "esbuild",
"esbuild.prod.mts": "esbuild",
"esbuild.prod.cts": "esbuild",
"esbuild.test.js": "esbuild",
"esbuild.test.mjs": "esbuild",
"esbuild.test.cjs": "esbuild",
"esbuild.test.ts": "esbuild",
"esbuild.test.mts": "esbuild",
"esbuild.test.cts": "esbuild",
"drizzle.config.ts": "drizzle",
"drizzle.config.dev.ts": "drizzle",
"drizzle.config.prod.ts": "drizzle",

View File

@ -151,8 +151,8 @@
"database": "<svg viewBox='0 0 32 32'><path fill='#ffca28' d='M16 24c-5.525 0-10-.9-10-2v4c0 1.1 4.475 2 10 2s10-.9 10-2v-4c0 1.1-4.475 2-10 2m0-8c-5.525 0-10-.9-10-2v4c0 1.1 4.475 2 10 2s10-.9 10-2v-4c0 1.1-4.475 2-10 2m0-12C10.477 4 6 4.895 6 6v4c0 1.1 4.475 2 10 2s10-.9 10-2V6c0-1.105-4.477-2-10-2'/></svg>",
"deepsource": "<svg viewBox='0 0 16 16'><path fill='#1de9b6' d='M2 2h9a1 1 0 0 1 1 .992A1 1 0 0 1 11 4H2z'/><path fill='#f44336' d='M2 12h11a1 1 0 0 1 1 1 1 1 0 0 1-1 1H2z'/><path fill='#ffb300' d='M2 9h7a1 1 0 0 0 1-1 1 1 0 0 0-1-1H2z'/></svg>",
"denizenscript": "<svg viewBox='0 0 32 32'><path fill='#ffd54f' d='M6 4h8.804a17 17 0 0 1 4.54.459 8 8 0 0 1 3.597 2.21 10.5 10.5 0 0 1 2.278 3.887A17.8 17.8 0 0 1 26 16.23a15.8 15.8 0 0 1-.733 5.108 10.6 10.6 0 0 1-2.554 4.24 8.45 8.45 0 0 1-3.385 1.915 14.5 14.5 0 0 1-4.264.508H6Zm4 4.06v15.896h4.413a13 13 0 0 0 2.913-.228 4.45 4.45 0 0 0 1.945-1 5.1 5.1 0 0 0 1.261-2.316 15.8 15.8 0 0 0 .488-4.395 14.5 14.5 0 0 0-.488-4.274 5.5 5.5 0 0 0-1.367-2.324 4.57 4.57 0 0 0-2.23-1.13 21.7 21.7 0 0 0-3.954-.229Z' style='isolation:isolate'/></svg>",
"deno": "<svg viewBox='0 0 32 32'><path fill='#cfd8dc' d='M3.069 10.688C3.069 5.873 7.859 2 14 2a11.9 11.9 0 0 1 7.49 2.378 10.64 10.64 0 0 1 3.593 5.236l.015.049.017.057.034.108.048.198.134.463.14.529.238.875.38 1.386.613 2.28.692 2.593 1.116 4.168.42 1.571-.09.1A18.98 18.98 0 0 1 17.337 30l-.04-.273-.074-.545-.066-.395-.076-.52-.097-.634-.042-.25-.091-.602-.057-.356-.074-.462-.076-.444-.074-.432-.074-.422-.066-.413-.074-.395-.068-.38-.048-.281-.057-.271-.034-.173-.066-.35-.05-.246-.057-.305-.049-.215-.042-.205-.043-.2-.023-.132-.059-.248-.042-.181-.04-.182-.032-.114-.042-.167-.032-.157-.042-.156-.043-.148-.023-.091-.042-.142-.032-.13-.025-.092-.034-.084-.023-.072-.034-.116-.025-.085-.017-.049q-.067-.196-.148-.386l-.026-.051.19-.495-.75.026-.207.008c-6.82.14-11.222-2.759-11.222-7.301Zm14.345-4.101a2 2 0 1 0 0 2.827 2 2 0 0 0 0-2.827'/><path fill='#cfd8dc' d='M3.069 10.688c.95-12.027 21.388-11.423 22.64 1.205 1.027 3.74 2.21 8.244 3.222 11.998A18.98 18.98 0 0 1 17.337 30c-.407-2.79-.84-5.602-1.41-8.364a27 27 0 0 0-.505-2.123c-.104-.536-.523-1.043-.173-1.56-6.665.529-12.374-2.428-12.18-7.267Zm14.345-4.101c-1.807-1.861-4.689 1.02-2.827 2.828 1.807 1.86 4.689-1.021 2.827-2.828'/></svg>",
"deno_light": "<svg viewBox='0 0 32 32'><path fill='#455a64' d='M3.07 10.688C4.02-1.34 24.46-.735 25.713 11.893c1.027 3.74 2.21 8.244 3.222 11.998A18.98 18.98 0 0 1 17.339 30c-.407-2.79-.839-5.602-1.41-8.364a27 27 0 0 0-.505-2.123c-.103-.536-.522-1.043-.173-1.56-6.665.529-12.374-2.428-12.18-7.267Zm14.347-4.101c-1.808-1.861-4.69 1.02-2.828 2.828 1.808 1.86 4.69-1.021 2.828-2.828'/></svg>",
"deno": "<svg viewBox='0 0 32 32'><path fill='#cfd8dc' d='M3.288 23.102A13.937 14.098 0 0 1 2 17.16c0-.553.028-1.09.091-1.622a14.049 14.211 0 0 1 .273-1.586 14 14.162 0 0 1 4.333-7.37 14.007 14.169 0 0 1 6.37-3.272 14.028 14.19 0 0 1 3.997-.27 13.958 14.12 0 0 1 4.76 1.24 14.021 14.183 0 0 1 3.241 2.089 14.028 14.19 0 0 1 4.732 8.355A14.063 14.225 0 0 1 30 17.161a14.175 14.339 0 0 1-.042 1.076 13.986 14.147 0 0 1-.847 3.902 14.007 14.169 0 0 1-2.779 4.574A7.245 7.329 0 0 1 21.166 29a5.054 5.112 0 0 1-4.816-6.444c.119-.453.434-1.317.889-1.699a5.334 5.396 0 0 1-1.435-.977c-.049-.057-.042-.156.007-.22a.182.184 0 0 1 .203-.056 10.206 10.324 0 0 0 1.596.41c.777.128 1.736.298 2.709.34 2.366.12 4.844-.956 5.614-3.094.763-2.146.469-4.263-2.289-5.53-2.758-1.275-4.032-2.783-6.258-3.696-1.456-.595-3.073-.241-4.732.694-4.48 2.5-8.491 10.408-6.643 17.737a.224.227 0 0 1-.364.22 14.077 14.24 0 0 1-1.463-1.927 14.021 14.183 0 0 1-.896-1.656M15.503 9.018c.749-.057 1.414.595 1.526 1.459.147 1.154-.273 2.35-1.645 2.379-1.183.02-1.54-1.176-1.456-1.905.077-.73.665-1.862 1.568-1.933z'/></svg>",
"deno_light": "<svg viewBox='0 0 32 32'><path fill='#455a64' d='M3.288 23.102A13.937 14.098 0 0 1 2 17.16c0-.553.028-1.09.091-1.622a14.049 14.211 0 0 1 .273-1.586 14 14.162 0 0 1 4.333-7.37 14.007 14.169 0 0 1 6.37-3.272 14.028 14.19 0 0 1 3.997-.27 13.958 14.12 0 0 1 4.76 1.24 14.021 14.183 0 0 1 3.241 2.089 14.028 14.19 0 0 1 4.732 8.355A14.063 14.225 0 0 1 30 17.161a14.175 14.339 0 0 1-.042 1.076 13.986 14.147 0 0 1-.847 3.902 14.007 14.169 0 0 1-2.779 4.574A7.245 7.329 0 0 1 21.166 29a5.054 5.112 0 0 1-4.816-6.444c.119-.453.434-1.317.889-1.699a5.334 5.396 0 0 1-1.435-.977c-.049-.057-.042-.156.007-.22a.182.184 0 0 1 .203-.056 10.206 10.324 0 0 0 1.596.41c.777.128 1.736.298 2.709.34 2.366.12 4.844-.956 5.614-3.094.763-2.146.469-4.263-2.289-5.53-2.758-1.275-4.032-2.783-6.258-3.696-1.456-.595-3.073-.241-4.732.694-4.48 2.5-8.491 10.408-6.643 17.737a.224.227 0 0 1-.364.22 14.077 14.24 0 0 1-1.463-1.927 14.021 14.183 0 0 1-.896-1.656M15.503 9.018c.749-.057 1.414.595 1.526 1.459.147 1.154-.273 2.35-1.645 2.379-1.183.02-1.54-1.176-1.456-1.905.077-.73.665-1.862 1.568-1.933z'/></svg>",
"dependabot": "<svg viewBox='0 0 32 32'><path fill='#448aff' d='M29.5 16H28v-4a2 2 0 0 0-2-2h-6V2.5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5H18v4H6a2 2 0 0 0-2 2v4H2.5a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .5.5H4v2a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-2h1.5a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.5-.5m-15.533 2.647-3.106 3.106a.6.6 0 0 1-.84 0l-1.867-1.866a.6.6 0 0 1 0-.84l.627-.64a.6.6 0 0 1 .848-.005l.005.005.8.8 2.053-2.04a.6.6 0 0 1 .84 0l.64.64a.58.58 0 0 1 0 .84m9.88 0-3.106 3.106a.6.6 0 0 1-.84 0l-1.867-1.866a.6.6 0 0 1 0-.84l.627-.64a.6.6 0 0 1 .84 0l.813.8 2.053-2.04a.6.6 0 0 1 .84 0l.64.64a.604.604 0 0 1 0 .84'/></svg>",
"dependencies-update": "<svg fill='none' viewBox='0 0 16 16'><path fill='#8bc34a' d='m10.484 3.635-2.5 2.546-.875-.891 1-1.018H8q-1.563 0-2.656 1.121Q4.249 6.515 4.25 8.122a3.5 3.5 0 0 0 .375 1.59l-.937.955a5.156 5.25 0 0 1-.516-1.24A4.81 4.897 0 0 1 3 8.121Q3 5.99 4.453 4.494T8 2.999h.11l-1-1.018.874-.891zm-4.968 8.747 2.5-2.546.875.891-1 1.018H8q1.563 0 2.656-1.12 1.095-1.123 1.094-2.73a3.5 3.5 0 0 0-.375-1.59l.938-.955q.343.604.515 1.24.172.638.172 1.305 0 2.131-1.453 3.628Q10.094 13.018 8 13.018h-.11l1 1.018-.874.891z'/></svg>",
"dhall": "<svg viewBox='0 0 24 24'><path fill='#78909c' d='M15.81 2.853 21.148 8.2l-1.54 1.518-5.326-5.327 1.528-1.54M2.853 20.373l6.995-6.962a1.04 1.04 0 0 1 .247-1.033c.42-.42 1.109-.42 1.528 0 .42.43.42 1.108 0 1.528-.28.28-.7.355-1.033.248l-6.962 6.995 11.418-3.82 3.799-6.845-5.317-5.327-6.855 3.799z'/></svg>",
@ -495,6 +495,8 @@
"folder-metro": "<svg viewBox='0 0 32 32'><path fill='#ef5350' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#ffcdd2' d='M24 10a2.917 2.92 0 0 0-1.305.308l-5.39 2.7A2.36 2.363 0 0 0 16 15.12v7.759a2.36 2.363 0 0 0 1.304 2.111l5.39 2.701a2.917 2.92 0 0 0 2.61 0l5.39-2.7A2.36 2.363 0 0 0 32 22.878V15.12a2.36 2.363 0 0 0-1.304-2.112l-5.391-2.7A2.917 2.92 0 0 0 23.999 10m0 1.33a2.875 2.878 0 0 1 1.286.305l4.047 2.025-1.667.834-3.667-1.835-3.666 1.835-1.667-.834 4.047-2.025A2.875 2.878 0 0 1 24 11.329zm-5.735 3.545a.882.883 0 0 1 .344.091l5.39 2.699 5.39-2.699a.882.883 0 0 1 .313-.088.882.883 0 0 1 .964.878v6.153a2.308 2.31 0 0 1-1.276 2.067l-4.114 2.06a2.853 2.856 0 0 1-2.553 0l-4.114-2.06a2.308 2.31 0 0 1-1.276-2.067v-6.153a.882.883 0 0 1 .932-.881m.401 1.788v5.673L24 25.006l5.334-2.67v-5.673l-4.133 2.07a2.683 2.686 0 0 1-2.401 0z'/></svg>",
"folder-middleware-open": "<svg viewBox='0 0 32 32'><path fill='#5c6bc0' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><path fill='#c5cae9' d='M30.107 20H32v-4a2 2 0 0 0-2-2h-4v-2a2 2 0 0 0-4 0v2h-4a2 2 0 0 0-2 2v4h-2a2 2 0 0 0 0 4h2v4a2 2 0 0 0 2 2h4v-1.893a2.074 2.074 0 0 1 1.664-2.08A2 2 0 0 1 26 28v2h4a2 2 0 0 0 2-2v-4h-2a2 2 0 0 1-1.973-2.336A2.074 2.074 0 0 1 30.107 20'/></svg>",
"folder-middleware": "<svg viewBox='0 0 32 32'><path fill='#5c6bc0' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#c5cae9' d='M30.107 20H32v-4a2 2 0 0 0-2-2h-4v-2a2 2 0 0 0-4 0v2h-4a2 2 0 0 0-2 2v4h-2a2 2 0 0 0 0 4h2v4a2 2 0 0 0 2 2h4v-1.893a2.074 2.074 0 0 1 1.664-2.08A2 2 0 0 1 26 28v2h4a2 2 0 0 0 2-2v-4h-2a2 2 0 0 1-1.973-2.336A2.074 2.074 0 0 1 30.107 20'/></svg>",
"folder-migrations-open": "<svg viewBox='0 0 32 32'><path fill='#ec407a' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><path fill='#f8bbd0' d='M24 10c-3.41 0-6.31 1.07-7.46 2.57l5.25 5.25c.71.11 1.43.18 2.21.18 4.42 0 8-1.79 8-4s-3.58-4-8-4m-8 4v4h-4v2h4v4l4.84-5M32 16c0 2.21-3.58 4-8 4-.66 0-1.3-.05-1.91-.13l-2.47 2.47c1.26.41 2.76.66 4.38.66 4.42 0 8-1.79 8-4m0 2c0 2.21-3.58 4-8 4-2.28 0-4.33-.5-5.79-1.25l-1.68 1.68C17.68 26.93 20.59 28 24 28c4.42 0 8-1.79 8-4'/></svg>",
"folder-migrations": "<svg viewBox='0 0 32 32'><path fill='#ec407a' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#f8bbd0' d='M24 10c-3.41 0-6.31 1.07-7.46 2.57l5.25 5.25c.71.11 1.43.18 2.21.18 4.42 0 8-1.79 8-4s-3.58-4-8-4m-8 4v4h-4v2h4v4l4.84-5M32 16c0 2.21-3.58 4-8 4-.66 0-1.3-.05-1.91-.13l-2.47 2.47c1.26.41 2.76.66 4.38.66 4.42 0 8-1.79 8-4m0 2c0 2.21-3.58 4-8 4-2.28 0-4.33-.5-5.79-1.25l-1.68 1.68C17.68 26.93 20.59 28 24 28c4.42 0 8-1.79 8-4'/></svg>",
"folder-mjml-open": "<svg viewBox='0 0 32 32'><path fill='#ff5722' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><rect width='12' height='4' x='14' y='24' fill='#ffccbc' rx='2'/><rect width='12' height='4' x='20' y='18' fill='#ffccbc' rx='2'/><rect width='12' height='4' x='14' y='12' fill='#ffccbc' rx='2'/><circle cx='30' cy='26' r='2' fill='#ffccbc'/><circle cx='16' cy='20' r='2' fill='#ffccbc'/><circle cx='30' cy='14' r='2' fill='#ffccbc'/></svg>",
"folder-mjml": "<svg viewBox='0 0 32 32'><path fill='#ff5722' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><rect width='12' height='4' x='14' y='24' fill='#ffccbc' rx='2'/><rect width='12' height='4' x='20' y='18' fill='#ffccbc' rx='2'/><rect width='12' height='4' x='14' y='12' fill='#ffccbc' rx='2'/><circle cx='30' cy='26' r='2' fill='#ffccbc'/><circle cx='16' cy='20' r='2' fill='#ffccbc'/><circle cx='30' cy='14' r='2' fill='#ffccbc'/></svg>",
"folder-mobile-open": "<svg viewBox='0 0 32 32'><path fill='#ff5252' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><path fill='#ffcdd2' d='M12 14v10a2 2 0 0 0 2 2h8v-2h-6V14h12v2h4v-2a2 2 0 0 0-2-2H14a2 2 0 0 0-2 2'/><path fill='#ffcdd2' d='M24 19v8a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1m6 7h-4v-6h4Z'/></svg>",
@ -604,6 +606,8 @@
"folder-rules": "<svg viewBox='0 0 32 32'><path fill='#ef5350' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#ffcdd2' d='M30 14h-2a3 3 0 0 0-6 0h-2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V16a2 2 0 0 0-2-2m-5-1a1 1 0 1 1-1 1 1.003 1.003 0 0 1 1-1m-1.547 11.597-3.093-3.093 1.09-1.09 2.003 1.995 5.096-5.096 1.091 1.099Z'/></svg>",
"folder-rust-open": "<svg viewBox='0 0 32 32'><path fill='#ff7043' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><path fill='#ffccbc' d='M30 24v1a1 1 0 0 1-2 0v-1a2 2 0 0 0-2-2 3 3 0 0 0 2.996-3.16A3.115 3.114 0 0 0 25.83 16H14v2h2v8h-2v2h8v-2h-2v-2h4c.82.819.297 2.308 1.179 3.37a1.89 1.89 0 0 0 1.46.63H32v-4zm-6-4h-4v-2h4a1 1 0 0 1 0 2'/></svg>",
"folder-rust": "<svg viewBox='0 0 32 32'><path fill='#ff7043' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#ffccbc' d='M30 24v1a1 1 0 0 1-2 0v-1a2 2 0 0 0-2-2 3 3 0 0 0 2.996-3.16A3.115 3.114 0 0 0 25.83 16H14v2h2v8h-2v2h8v-2h-2v-2h4c.82.819.297 2.308 1.179 3.37a1.89 1.89 0 0 0 1.46.63H32v-4zm-6-4h-4v-2h4a1 1 0 0 1 0 2'/></svg>",
"folder-salt-open": "<svg viewBox='0 0 32 32'><path fill='#03a9f4' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><path fill='#b3e5fc' d='M12 20v8h10l4-8h6v-8H22l-4 8z'/></svg>",
"folder-salt": "<svg viewBox='0 0 32 32'><path fill='#03a9f4' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#b3e5fc' d='M12 20v8h10l4-8h6v-8H22l-4 8z'/></svg>",
"folder-sandbox-open": "<svg viewBox='0 0 32 32'><path fill='#1e88e5' d='M28.965 12.001H9.44a2 2 0 0 0-1.898 1.368L3.998 24.001v-14h24a2 2 0 0 0-2-2H15.122a2 2 0 0 1-1.28-.464l-1.288-1.072a2 2 0 0 0-1.28-.464H3.998a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212a2 2 0 0 0-1.838-2.788'/><path fill='#bbdefb' d='M21 20h-6a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1m7-8a4 4 0 1 0 4 4 4 4 0 0 0-4-4m.707 17.707 3-3a1 1 0 0 0 0-1.414l-3-3a1 1 0 0 0-1.414 0l-3 3a1 1 0 0 0 0 1.414l3 3a1 1 0 0 0 1.414 0m-11.581-7.193-2.999 6A1 1 0 0 0 15.001 30H21a1 1 0 0 0 .874-1.486l-2.999-6a1 1 0 0 0-1.748 0'/></svg>",
"folder-sandbox": "<svg viewBox='0 0 32 32'><path fill='#1e88e5' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#bbdefb' d='M21 20h-6a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1m7-8a4 4 0 1 0 4 4 4 4 0 0 0-4-4m.707 17.707 3-3a1 1 0 0 0 0-1.414l-3-3a1 1 0 0 0-1.414 0l-3 3a1 1 0 0 0 0 1.414l3 3a1 1 0 0 0 1.414 0m-11.581-7.193-2.999 6A1 1 0 0 0 15.001 30H21a1 1 0 0 0 .874-1.486l-2.999-6a1 1 0 0 0-1.748 0'/></svg>",
"folder-sass-open": "<svg viewBox='0 0 32 32'><path fill='#f06292' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><path fill='#fce4ec' d='M31.897 12.592a3 3 0 0 0-1.53-1.912 7.95 7.95 0 0 0-6.348-.05 17.4 17.4 0 0 0-5.864 3.557c-1.83 1.81-2.288 3.496-2.124 4.39.346 1.89 2.181 3.227 3.658 4.3.314.23.618.45.876.657-.92.513-2.916 1.749-3.483 3.074a2.9 2.9 0 0 0-.074 2.347 1.57 1.57 0 0 0 .874.903 3.5 3.5 0 0 0 .986.142 4.14 4.14 0 0 0 3.438-2.025 5.03 5.03 0 0 0 .55-3.886 4.5 4.5 0 0 1 1.46-.034 2.64 2.64 0 0 1 1.927.96 1.44 1.44 0 0 1 .304.968 1.2 1.2 0 0 1-.55.805c-.159.104-.356.233-.31.504.028.151.13.393.532.313a1.99 1.99 0 0 0 1.392-1.841 2.9 2.9 0 0 0-.801-2.051 3.9 3.9 0 0 0-2.897-1.135 6.5 6.5 0 0 0-1.813.226 13 13 0 0 0-1.498-1.346c-1.165-.947-2.265-1.842-2.2-3.125.085-1.654 1.672-3.306 4.716-4.909 2.7-1.422 4.894-1.47 6.04-1.041a1.44 1.44 0 0 1 .858.674 2.23 2.23 0 0 1-.257 1.866 6.57 6.57 0 0 1-5.023 3.105 2.56 2.56 0 0 1-2.225-.565c-.189-.219-.37-.423-.65-.263-.332.196-.175.625-.123.768a2.6 2.6 0 0 0 1.578 1.342 7.32 7.32 0 0 0 4.752-.482c2.631-1.078 4.384-3.933 3.83-6.236ZM21.301 26.118a3 3 0 0 1-.13.345 3.4 3.4 0 0 1-.517.795c-.648.743-1.499.978-1.776.808a.27.27 0 0 1-.088-.187 2.5 2.5 0 0 1 .742-1.704 7.8 7.8 0 0 1 1.865-1.445 3.05 3.05 0 0 1-.096 1.388'/></svg>",
@ -1024,6 +1028,7 @@
"ruff": "<svg viewBox='0 0 32 32'><path fill='#43a047' d='M26 16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4v24h10v-8h2v8h12V18h-6v-2Zm-8-2h-6v-2h6Z'/></svg>",
"rust": "<svg viewBox='0 0 32 32'><path fill='#ff7043' d='m30 12-4-2V6h-4l-2-4-4 2-4-2-2 4H6v4l-4 2 2 4-2 4 4 2v4h4l2 4 4-2 4 2 2-4h4v-4l4-2-2-4ZM6 16a9.9 9.9 0 0 1 .842-4H10v8H6.842A9.9 9.9 0 0 1 6 16m10 10a9.98 9.98 0 0 1-7.978-4H16v-2h-2v-2h4c.819.819.297 2.308 1.179 3.37a1.89 1.89 0 0 0 1.46.63h3.34A9.98 9.98 0 0 1 16 26m-2-12v-2h4a1 1 0 0 1 0 2Zm11.158 6H24a2.006 2.006 0 0 1-2-2 2 2 0 0 0-2-2 3 3 0 0 0 3-3q0-.08-.004-.161A3.115 3.115 0 0 0 19.83 10H8.022a9.986 9.986 0 0 1 17.136 10'/></svg>",
"salesforce": "<svg viewBox='0 0 24 24'><path fill='#039be5' d='M18.206 6.522c-.681 0-1.275.204-1.858.399-.681-1.177-1.956-1.956-3.328-1.956-1.07 0-2.043.487-2.734 1.168-.779-.973-1.956-1.664-3.318-1.664-2.267 0-4.213 1.858-4.213 4.126 0 .574.204 1.157.399 1.741a3.58 3.58 0 0 0-1.859 3.124c0 1.946 1.567 3.62 3.523 3.62.292 0 .583 0 .778-.098.39 1.469 1.858 2.55 3.62 2.55 1.654 0 3.026-.984 3.512-2.356.496.205.983.4 1.47.4 1.274 0 2.442-.71 3.025-1.762.302.078.613.078.886.078 2.54 0 4.592-2.034 4.592-4.67.098-2.627-1.946-4.7-4.495-4.7'/></svg>",
"salt": "<svg viewBox='0 0 16 16'><path fill='#03a9f4' d='M1 8v6h7l3-6h4V2H8L5 8z'/></svg>",
"san": "<svg viewBox='0 0 32 32'><path fill='#01579b' d='M28 17.898 4 23.316V30l24-5.418Zm0-10.623L4 12.694v6.683l24-5.418Z'/><path fill='#b3e5fc' d='M28 13.926 4 8.684V2l24 5.242Zm0 10.623L4 19.307v-6.684l24 5.242Z'/></svg>",
"sas": "<svg viewBox='0 0 6.35 6.35'><path fill='#039be5' d='M3.16.546a1.9 1.9 0 0 0-.667.13c-.378.157-.678.4-.846.74-.255.452-.296 1.032.04 1.536.31.474.72.908 1.148 1.407.382.139.572-.272.515-.42-.276-.35-.57-.695-.804-1.06a1.1 1.1 0 0 1-.07-.906c.075-.198.503-.84 1.372-.836.345.04.658.059 1.086.53a1.6 1.6 0 0 0-.536-.753A2.05 2.05 0 0 0 3.159.546zm.245 1.4c-.277.002-.406.227-.348.437.253.365.578.748.869 1.122.316.609.092 1.077-.512 1.472-.82.458-1.576.116-2.026-.298.234.489.423.636.471.679.31.27.81.527 1.62.423.456-.102 1.06-.256 1.396-1.097.093-.265.125-.54.03-.843-.09-.338-.274-.58-.466-.829-.3-.351-.77-.979-.906-1.053a.6.6 0 0 0-.128-.013'/></svg>",
"sass": "<svg viewBox='0 0 32 32'><path fill='#ec407a' d='M27.837 5.673a4.33 4.33 0 0 0-2.293-2.701c-2.362-1.261-6.11-1.298-9.548-.092a26.3 26.3 0 0 0-8.76 4.966c-2.752 2.542-3.438 4.925-3.189 6.194.523 2.668 3.274 4.539 5.485 6.042.418.284.822.559 1.175.816-1.429.76-4.261 2.444-5.088 4.248a3.88 3.88 0 0 0-.118 3.332A2.37 2.37 0 0 0 6.869 29.8a5.6 5.6 0 0 0 1.49.2 6.35 6.35 0 0 0 5.19-2.856 6.74 6.74 0 0 0 .864-5.382 7.3 7.3 0 0 1 2.044-.03 3.92 3.92 0 0 1 2.816 1.311 1.82 1.82 0 0 1 .423 1.262 1.55 1.55 0 0 1-.772 1.05c-.234.14-.586.355-.504.803.036.194.198.633.894.512a2.93 2.93 0 0 0 2.145-2.651 4 4 0 0 0-1.197-2.904 5.94 5.94 0 0 0-4.396-1.626 10.6 10.6 0 0 0-2.672.304 20 20 0 0 0-2.203-1.846c-1.712-1.3-3.33-2.529-3.235-4.26.125-2.263 2.468-4.532 6.964-6.744 4.016-1.976 7.254-2.037 8.944-1.438a2 2 0 0 1 1.204.883 2.77 2.77 0 0 1-.36 2.47 9.71 9.71 0 0 1-7.425 4.304 3.86 3.86 0 0 1-3.238-.757c-.278-.302-.593-.645-1.074-.383q-.565.31-.225 1.189a3.9 3.9 0 0 0 2.407 1.92 11.7 11.7 0 0 0 7.128-.671c3.527-1.35 6.681-5.202 5.756-8.787M11.895 24.475a4 4 0 0 1-.192.468 4.5 4.5 0 0 1-.753 1.081 2.83 2.83 0 0 1-2.533 1.107c-.056-.032-.078-.146-.085-.193a3.28 3.28 0 0 1 1.076-2.284 11.3 11.3 0 0 1 2.644-1.933 3.85 3.85 0 0 1-.157 1.754'/></svg>",

View File

@ -1,6 +1,6 @@
{
"type": "module",
"packageManager": "pnpm@10.26.0",
"packageManager": "pnpm@10.28.0",
"engines": {
"node": ">= 22.6.0",
"pnpm": ">= 10.0.0"
@ -8,7 +8,7 @@
"dependencies": {
"@citation-js/core": "0.7.21",
"@citation-js/plugin-bibtex": "0.7.21",
"@citation-js/plugin-csl": "0.7.21",
"@citation-js/plugin-csl": "0.7.22",
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/paste-markdown": "1.5.3",
@ -21,7 +21,7 @@
"@techknowlogick/license-checker-webpack-plugin": "0.3.0",
"add-asset-webpack-plugin": "3.1.1",
"ansi_up": "6.0.6",
"asciinema-player": "3.13.5",
"asciinema-player": "3.14.0",
"chart.js": "4.5.1",
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.2.0",
@ -32,7 +32,7 @@
"dayjs": "1.11.19",
"dropzone": "6.0.0-beta.2",
"easymde": "2.20.0",
"esbuild-loader": "4.4.0",
"esbuild-loader": "4.4.2",
"htmx.org": "2.0.8",
"idiomorph": "0.7.4",
"jquery": "3.7.1",
@ -41,7 +41,7 @@
"mini-css-extract-plugin": "2.9.4",
"monaco-editor": "0.55.1",
"monaco-editor-webpack-plugin": "7.1.1",
"online-3d-viewer": "0.17.0",
"online-3d-viewer": "0.18.0",
"pdfobject": "2.3.1",
"perfect-debounce": "2.0.0",
"postcss": "8.5.6",
@ -56,11 +56,11 @@
"tributejs": "5.1.3",
"uint8-to-base64": "0.2.1",
"vanilla-colorful": "0.7.2",
"vue": "3.5.25",
"vue": "3.5.26",
"vue-bar-graph": "2.2.0",
"vue-chartjs": "5.3.3",
"vue-loader": "17.4.2",
"webpack": "5.104.0",
"webpack": "5.104.1",
"webpack-cli": "6.0.1",
"wrap-ansi": "9.0.2"
},
@ -68,38 +68,38 @@
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
"@eslint/json": "0.14.0",
"@playwright/test": "1.57.0",
"@stylistic/eslint-plugin": "5.6.1",
"@stylistic/eslint-plugin": "5.7.0",
"@stylistic/stylelint-plugin": "4.0.0",
"@types/codemirror": "5.60.17",
"@types/dropzone": "5.7.9",
"@types/jquery": "3.5.33",
"@types/katex": "0.16.7",
"@types/katex": "0.16.8",
"@types/pdfobject": "2.2.5",
"@types/sortablejs": "1.15.9",
"@types/swagger-ui-dist": "3.30.6",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.4",
"@typescript-eslint/parser": "8.50.0",
"@typescript-eslint/parser": "8.53.0",
"@vitejs/plugin-vue": "6.0.3",
"@vitest/eslint-plugin": "1.5.2",
"@vitest/eslint-plugin": "1.6.6",
"eslint": "9.39.2",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-array-func": "5.1.0",
"eslint-plugin-github": "6.0.0",
"eslint-plugin-import-x": "4.16.1",
"eslint-plugin-playwright": "2.4.0",
"eslint-plugin-playwright": "2.5.0",
"eslint-plugin-regexp": "2.10.0",
"eslint-plugin-sonarjs": "3.0.5",
"eslint-plugin-unicorn": "62.0.0",
"eslint-plugin-vue": "10.6.2",
"eslint-plugin-vue-scoped-css": "2.12.0",
"eslint-plugin-wc": "3.0.2",
"globals": "16.5.0",
"happy-dom": "20.0.11",
"globals": "17.0.0",
"happy-dom": "20.1.0",
"jiti": "2.6.1",
"markdownlint-cli": "0.47.0",
"material-icon-theme": "5.29.0",
"material-icon-theme": "5.30.0",
"nolyfill": "1.0.44",
"postcss-html": "1.8.0",
"spectral-cli-bundle": "1.0.3",
@ -107,14 +107,14 @@
"stylelint-config-recommended": "17.0.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.11",
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"stylelint-value-no-unknown-custom-properties": "6.1.0",
"svgo": "4.0.0",
"typescript": "5.9.3",
"typescript-eslint": "8.50.0",
"typescript-eslint": "8.53.0",
"updates": "17.0.7",
"vite-string-plugin": "1.4.9",
"vitest": "4.0.16",
"vue-tsc": "3.1.8"
"vitest": "4.0.17",
"vue-tsc": "3.2.2"
},
"browserslist": [
"defaults"

1489
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -224,7 +224,7 @@ func GetStopwatches(ctx *context.APIContext) {
return
}
apiSWs, err := convert.ToStopWatches(ctx, sws)
apiSWs, err := convert.ToStopWatches(ctx, ctx.Doer, sws)
if err != nil {
ctx.APIErrorInternal(err)
return

View File

@ -15,6 +15,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
@ -128,7 +129,9 @@ func prepareUserNotificationsData(ctx *context.Context) {
ctx.Data["Notifications"] = notifications
ctx.Data["Link"] = setting.AppSubURL + "/notifications"
ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number")
pager.AddParamFromRequest(ctx.Req)
pager.RemoveParam(container.SetOf("div-only", "sequence-number"))
ctx.Data["Page"] = pager
}

View File

@ -0,0 +1,14 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"testing"
"code.gitea.io/gitea/models/unittest"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}

View File

@ -4,12 +4,14 @@
package security
import (
"errors"
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/openid"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@ -116,7 +118,11 @@ func DeleteOpenID(ctx *context.Context) {
}
if err := user_model.DeleteUserOpenID(ctx, &user_model.UserOpenID{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil {
ctx.ServerError("DeleteUserOpenID", err)
if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound)
} else {
ctx.ServerError("DeleteUserOpenID", err)
}
return
}
log.Trace("OpenID address deleted: %s", ctx.Doer.Name)
@ -132,8 +138,12 @@ func ToggleOpenIDVisibility(ctx *context.Context) {
return
}
if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id")); err != nil {
ctx.ServerError("ToggleUserOpenIDVisibility", err)
if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id"), ctx.Doer); err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound)
} else {
ctx.ServerError("ToggleUserOpenIDVisibility", err)
}
return
}

View File

@ -0,0 +1,36 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"net/http"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
func TestDeleteOpenIDReturnsNotFoundForOtherUsersAddress(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security")
contexttest.LoadUser(t, ctx, 2)
ctx.SetFormString("id", "1")
DeleteOpenID(ctx)
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
}
func TestToggleOpenIDVisibilityReturnsNotFoundForOtherUsersAddress(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security")
contexttest.LoadUser(t, ctx, 2)
ctx.SetFormString("id", "1")
ToggleOpenIDVisibility(ctx)
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
}

View File

@ -29,7 +29,7 @@ func GetStopwatches(ctx *context.Context) {
return
}
apiSWs, err := convert.ToStopWatches(ctx, sws)
apiSWs, err := convert.ToStopWatches(ctx, ctx.Doer, sws)
if err != nil {
ctx.HTTPError(http.StatusInternalServerError, err.Error())
return

View File

@ -8,8 +8,10 @@ import (
"html/template"
"net/http"
"net/url"
"slices"
"strings"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/paginator"
)
@ -49,6 +51,14 @@ func (p *Pagination) AddParamFromRequest(req *http.Request) {
p.AddParamFromQuery(req.URL.Query())
}
func (p *Pagination) RemoveParam(keys container.Set[string]) {
p.urlParams = slices.DeleteFunc(p.urlParams, func(s string) bool {
k, _, _ := strings.Cut(s, "=")
k, _ = url.QueryUnescape(k)
return keys.Contains(k)
})
}
// GetParams returns the configured URL params
func (p *Pagination) GetParams() template.URL {
return template.URL(strings.Join(p.urlParams, "&"))

View File

@ -0,0 +1,35 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"net/url"
"testing"
"code.gitea.io/gitea/modules/container"
"github.com/stretchr/testify/assert"
)
func TestPagination(t *testing.T) {
p := NewPagination(1, 1, 1, 1)
params := url.Values{}
params.Add("k1", "11")
params.Add("k1", "12")
params.Add("k", "a")
params.Add("k", "b")
params.Add("k2", "21")
params.Add("k2", "22")
params.Add("foo", "bar")
p.AddParamFromQuery(params)
v, _ := url.ParseQuery(string(p.GetParams()))
assert.Equal(t, params, v)
p.RemoveParam(container.SetOf("k", "foo"))
params.Del("k")
params.Del("foo")
v, _ = url.ParseQuery(string(p.GetParams()))
assert.Equal(t, params, v)
}

View File

@ -10,6 +10,7 @@ import (
"strings"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/label"
@ -163,11 +164,12 @@ func ToTrackedTime(ctx context.Context, doer *user_model.User, t *issues_model.T
}
// ToStopWatches convert Stopwatch list to api.StopWatches
func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.StopWatches, error) {
func ToStopWatches(ctx context.Context, doer *user_model.User, sws []*issues_model.Stopwatch) (api.StopWatches, error) {
result := api.StopWatches(make([]api.StopWatch, 0, len(sws)))
issueCache := make(map[int64]*issues_model.Issue)
repoCache := make(map[int64]*repo_model.Repository)
permCache := make(map[int64]access_model.Permission)
var (
issue *issues_model.Issue
repo *repo_model.Repository
@ -182,13 +184,30 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop
if err != nil {
return nil, err
}
issueCache[sw.IssueID] = issue
}
repo, ok = repoCache[issue.RepoID]
if !ok {
repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
if err != nil {
return nil, err
log.Error("GetRepositoryByID(%d): %v", issue.RepoID, err)
continue
}
repoCache[issue.RepoID] = repo
}
// ADD: Check user permissions
perm, ok := permCache[repo.ID]
if !ok {
perm, err = access_model.GetUserRepoPermission(ctx, repo, doer)
if err != nil {
continue
}
permCache[repo.ID] = perm
}
if !perm.CanReadIssuesOrPulls(issue.IsPull) {
continue
}
result = append(result, api.StopWatch{

View File

@ -8,9 +8,11 @@ import (
"testing"
"time"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
@ -55,3 +57,29 @@ func TestMilestone_APIFormat(t *testing.T) {
Deadline: milestone.DeadlineUnix.AsTimePtr(),
}, *ToAPIMilestone(milestone))
}
func TestToStopWatchesRespectsPermissions(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
publicSW := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{ID: 1})
privateIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 3})
privateSW := &issues_model.Stopwatch{IssueID: privateIssue.ID, UserID: 5}
assert.NoError(t, db.Insert(ctx, privateSW))
assert.NotZero(t, privateSW.ID)
regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
sws := []*issues_model.Stopwatch{publicSW, privateSW}
visible, err := ToStopWatches(ctx, regularUser, sws)
assert.NoError(t, err)
assert.Len(t, visible, 1)
assert.Equal(t, "repo1", visible[0].RepoName)
visibleAdmin, err := ToStopWatches(ctx, adminUser, sws)
assert.NoError(t, err)
assert.Len(t, visibleAdmin, 2)
assert.ElementsMatch(t, []string{"repo1", "repo3"}, []string{visibleAdmin[0].RepoName, visibleAdmin[1].RepoName})
}

View File

@ -127,20 +127,10 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
projectsMode = config.ProjectsMode
}
hasReleases := false
if _, err := repo.GetUnit(ctx, unit_model.TypeReleases); err == nil {
hasReleases = true
}
hasPackages := false
if _, err := repo.GetUnit(ctx, unit_model.TypePackages); err == nil {
hasPackages = true
}
hasActions := false
if _, err := repo.GetUnit(ctx, unit_model.TypeActions); err == nil {
hasActions = true
}
hasCode := repo.UnitEnabled(ctx, unit_model.TypeCode)
hasReleases := repo.UnitEnabled(ctx, unit_model.TypeReleases)
hasPackages := repo.UnitEnabled(ctx, unit_model.TypePackages)
hasActions := repo.UnitEnabled(ctx, unit_model.TypeActions)
if err := repo.LoadOwner(ctx); err != nil {
return nil
@ -221,6 +211,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
Updated: repo.UpdatedUnix.AsTime(),
ArchivedAt: repo.ArchivedUnix.AsTime(),
Permissions: permission,
HasCode: hasCode,
HasIssues: hasIssues,
ExternalTracker: externalTracker,
InternalTracker: internalTracker,

View File

@ -7,9 +7,12 @@ import (
"bytes"
"context"
"fmt"
"slices"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
@ -44,6 +47,16 @@ func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
return
}
if err := rel.LoadRepo(ctx); err != nil {
log.Error("rel.LoadRepo: %v", err)
return
}
// delete publisher or any users with no permission
recipients = slices.DeleteFunc(recipients, func(u *user_model.User) bool {
return u.ID == rel.PublisherID || !access_model.CheckRepoUnitUser(ctx, rel.Repo, u, unit.TypeReleases)
})
langMap := make(map[string][]*user_model.User)
for _, user := range recipients {
if user.ID != rel.PublisherID {

View File

@ -0,0 +1,71 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package mailer
import (
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
sender_service "code.gitea.io/gitea/services/mailer/sender"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMailNewReleaseFiltersUnauthorizedWatchers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
origMailService := setting.MailService
origDomain := setting.Domain
origAppName := setting.AppName
origAppURL := setting.AppURL
origTemplates := LoadedTemplates()
defer func() {
setting.MailService = origMailService
setting.Domain = origDomain
setting.AppName = origAppName
setting.AppURL = origAppURL
loadedTemplates.Store(origTemplates)
}()
setting.MailService = &setting.Mailer{
From: "Gitea",
FromEmail: "noreply@example.com",
}
setting.Domain = "example.com"
setting.AppName = "Gitea"
setting.AppURL = "https://example.com/"
prepareMailTemplates(string(tplNewReleaseMail), "{{.Subject}}", "<p>{{.Release.TagName}}</p>")
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
require.True(t, repo.IsPrivate)
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
unauthorized := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
assert.NoError(t, repo_model.WatchRepo(t.Context(), admin, repo, true))
assert.NoError(t, repo_model.WatchRepo(t.Context(), unauthorized, repo, true))
rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 11})
rel.Repo = nil
rel.Publisher = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: rel.PublisherID})
var sent []*sender_service.Message
origSend := SendAsync
SendAsync = func(msgs ...*sender_service.Message) {
sent = append(sent, msgs...)
}
defer func() {
SendAsync = origSend
}()
MailNewRelease(t.Context(), rel)
require.Len(t, sent, 1)
assert.Equal(t, admin.EmailTo(), sent[0].To)
assert.NotEqual(t, unauthorized.EmailTo(), sent[0].To)
}

View File

@ -8,6 +8,7 @@ import (
"strings"
"testing"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
@ -62,6 +63,36 @@ func TestTeam_RemoveMember(t *testing.T) {
assert.True(t, organization.IsErrLastOrgOwner(err))
}
func TestRemoveTeamMemberRemovesSubscriptionsAndStopwatches(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
assert.NoError(t, repo_model.WatchRepo(ctx, user, repo, true))
assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(ctx, user.ID, issue.ID, true))
ok, err := issues_model.CreateIssueStopwatch(ctx, user, issue)
assert.NoError(t, err)
assert.True(t, ok)
assert.NoError(t, RemoveTeamMember(ctx, team, user))
watch, err := repo_model.GetWatch(ctx, user.ID, repo.ID)
assert.NoError(t, err)
assert.False(t, repo_model.IsWatchMode(watch.Mode))
_, exists, err := issues_model.GetIssueWatch(ctx, user.ID, issue.ID)
assert.NoError(t, err)
assert.False(t, exists)
hasStopwatch, _, _, err := issues_model.HasUserStopwatch(ctx, user.ID)
assert.NoError(t, err)
assert.False(t, hasStopwatch)
}
func TestNewTeam(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

View File

@ -120,6 +120,11 @@ func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *u
return err
}
// Remove all stopwatches a user has running in the repository
if err := issues_model.RemoveStopwatchesByRepoID(ctx, user.ID, repo.ID); err != nil {
return err
}
// Remove all IssueWatches a user has subscribed to in the repository
return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID)
}

View File

@ -6,7 +6,10 @@ package repository
import (
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@ -32,8 +35,8 @@ func TestRepository_AddCollaborator(t *testing.T) {
func TestRepository_DeleteCollaboration(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
assert.NoError(t, repo.LoadOwner(t.Context()))
assert.NoError(t, DeleteCollaboration(t.Context(), repo, user))
@ -44,3 +47,50 @@ func TestRepository_DeleteCollaboration(t *testing.T) {
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
func TestRepository_DeleteCollaborationRemovesSubscriptionsAndStopwatches(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
assert.NoError(t, repo.LoadOwner(ctx))
assert.NoError(t, repo_model.WatchRepo(ctx, user, repo, true))
hasAccess, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo)
assert.NoError(t, err)
assert.True(t, hasAccess)
issueCount, err := db.GetEngine(ctx).Where("repo_id=?", repo.ID).Count(new(issues_model.Issue))
assert.NoError(t, err)
tempIssue := &issues_model.Issue{
RepoID: repo.ID,
Index: issueCount + 1,
PosterID: repo.OwnerID,
Title: "temp issue",
Content: "temp",
}
assert.NoError(t, db.Insert(ctx, tempIssue))
assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(ctx, user.ID, tempIssue.ID, true))
ok, err := issues_model.CreateIssueStopwatch(ctx, user, tempIssue)
assert.NoError(t, err)
assert.True(t, ok)
assert.NoError(t, DeleteCollaboration(ctx, repo, user))
hasAccess, err = access_model.HasAnyUnitAccess(ctx, user.ID, repo)
assert.NoError(t, err)
assert.False(t, hasAccess)
watch, err := repo_model.GetWatch(ctx, user.ID, repo.ID)
assert.NoError(t, err)
assert.False(t, repo_model.IsWatchMode(watch.Mode))
_, exists, err := issues_model.GetIssueWatch(ctx, user.ID, tempIssue.ID)
assert.NoError(t, err)
assert.False(t, exists)
hasStopwatch, _, _, err := issues_model.HasUserStopwatch(ctx, user.ID)
assert.NoError(t, err)
assert.False(t, hasStopwatch)
}

View File

@ -194,6 +194,10 @@ func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository) (err erro
return err
}
if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil {
return err
}
// Create/Remove git-daemon-export-ok for git-daemon...
if err := CheckDaemonExportOK(ctx, repo); err != nil {
return err

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLinkedRepository(t *testing.T) {
@ -70,3 +71,24 @@ func TestRepository_HasWiki(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
assert.False(t, HasWiki(t.Context(), repo2))
}
func TestMakeRepoPrivateClearsWatches(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repo.IsPrivate = false
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)
require.NoError(t, err)
require.NotEmpty(t, watchers)
assert.NoError(t, MakeRepoPrivate(t.Context(), repo))
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)
assert.NoError(t, err)
assert.Empty(t, watchers)
updatedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
assert.True(t, updatedRepo.IsPrivate)
assert.Zero(t, updatedRepo.NumWatches)
}

View File

@ -122,7 +122,7 @@ function initAdminAuthentication() {
document.querySelector<HTMLInputElement>(`#oauth2_${custom}`)!.value = document.querySelector<HTMLInputElement>(`#${provider}_${custom}`)!.value;
}
const customInput = document.querySelector(`#${provider}_${custom}`);
if (customInput && customInput.getAttribute('data-available') === 'true') {
if (customInput?.getAttribute('data-available') === 'true') {
for (const input of document.querySelectorAll(`.oauth2_${custom} input`)) {
input.setAttribute('required', 'required');
}

View File

@ -369,7 +369,7 @@ export class ComboMarkdownEditor {
hideElem(this.textareaMarkdownToolbar);
}
value(v: any = undefined) {
value(v?: any) {
if (v === undefined) {
if (this.easyMDE) {
return this.easyMDE.value();