mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-06 23:38:48 +02:00
Merge branch 'main' into dropdowncss
This commit is contained in:
commit
2921992b79
11484
assets/emoji.json
generated
11484
assets/emoji.json
generated
File diff suppressed because one or more lines are too long
@ -24,8 +24,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
gemojiURL = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
|
||||
maxUnicodeVersion = 15
|
||||
gemojiURL = "https://raw.githubusercontent.com/rhysd/gemoji/537ff2d7e0496e9964824f7f73ec7ece88c9765a/db/emoji.json"
|
||||
maxUnicodeVersion = 16
|
||||
)
|
||||
|
||||
var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out")
|
||||
@ -149,8 +149,8 @@ func generate() ([]byte, error) {
|
||||
}
|
||||
|
||||
// write a JSON file to use with tribute (write before adding skin tones since we can't support them there yet)
|
||||
file, _ := json.Marshal(data)
|
||||
_ = os.WriteFile("assets/emoji.json", file, 0o644)
|
||||
file, _ := json.MarshalIndent(data, "", " ")
|
||||
_ = os.WriteFile("assets/emoji.json", append(file, '\n'), 0o644)
|
||||
|
||||
// Add skin tones to emoji that support it
|
||||
var (
|
||||
|
||||
@ -312,15 +312,12 @@ func IterateRepositoryIDsWithLFSMetaObjects(ctx context.Context, f func(ctx cont
|
||||
|
||||
// IterateLFSMetaObjectsForRepoOptions provides options for IterateLFSMetaObjectsForRepo
|
||||
type IterateLFSMetaObjectsForRepoOptions struct {
|
||||
OlderThan timeutil.TimeStamp
|
||||
UpdatedLessRecentlyThan timeutil.TimeStamp
|
||||
OrderByUpdated bool
|
||||
LoopFunctionAlwaysUpdates bool
|
||||
OlderThan timeutil.TimeStamp
|
||||
UpdatedLessRecentlyThan timeutil.TimeStamp
|
||||
}
|
||||
|
||||
// IterateLFSMetaObjectsForRepo provides a iterator for LFSMetaObjects per Repo
|
||||
func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(context.Context, *LFSMetaObject, int64) error, opts *IterateLFSMetaObjectsForRepoOptions) error {
|
||||
var start int
|
||||
batchSize := setting.Database.IterateBufferSize
|
||||
engine := db.GetEngine(ctx)
|
||||
type CountLFSMetaObject struct {
|
||||
@ -328,7 +325,7 @@ func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(cont
|
||||
LFSMetaObject `xorm:"extends"`
|
||||
}
|
||||
|
||||
id := int64(0)
|
||||
lastID := int64(0)
|
||||
|
||||
for {
|
||||
beans := make([]*CountLFSMetaObject, 0, batchSize)
|
||||
@ -341,29 +338,23 @@ func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(cont
|
||||
if !opts.UpdatedLessRecentlyThan.IsZero() {
|
||||
sess.And("`lfs_meta_object`.updated_unix < ?", opts.UpdatedLessRecentlyThan)
|
||||
}
|
||||
sess.GroupBy("`lfs_meta_object`.id")
|
||||
if opts.OrderByUpdated {
|
||||
sess.OrderBy("`lfs_meta_object`.updated_unix ASC")
|
||||
} else {
|
||||
sess.And("`lfs_meta_object`.id > ?", id)
|
||||
sess.OrderBy("`lfs_meta_object`.id ASC")
|
||||
}
|
||||
if err := sess.Limit(batchSize, start).Find(&beans); err != nil {
|
||||
sess.GroupBy("`lfs_meta_object`.id").
|
||||
And("`lfs_meta_object`.id > ?", lastID).
|
||||
OrderBy("`lfs_meta_object`.id ASC")
|
||||
|
||||
if err := sess.Limit(batchSize).Find(&beans); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(beans) == 0 {
|
||||
return nil
|
||||
}
|
||||
if !opts.LoopFunctionAlwaysUpdates {
|
||||
start += len(beans)
|
||||
}
|
||||
|
||||
for _, bean := range beans {
|
||||
if err := f(ctx, &bean.LFSMetaObject, bean.Count); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
id = beans[len(beans)-1].ID
|
||||
lastID = beans[len(beans)-1].ID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
61
models/git/lfs_test.go
Normal file
61
models/git/lfs_test.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIterateLFSMetaObjectsForRepoUpdatesDoNotSkip(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
ctx := t.Context()
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, "user2", "repo1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer test.MockVariableValue(&setting.Database.IterateBufferSize, 1)()
|
||||
|
||||
created := make([]*git_model.LFSMetaObject, 0, 3)
|
||||
for i := range 3 {
|
||||
content := []byte("gitea-lfs-" + strconv.Itoa(i))
|
||||
pointer, err := lfs.GeneratePointer(bytes.NewReader(content))
|
||||
assert.NoError(t, err)
|
||||
|
||||
meta, err := git_model.NewLFSMetaObject(ctx, repo.ID, pointer)
|
||||
assert.NoError(t, err)
|
||||
created = append(created, meta)
|
||||
}
|
||||
|
||||
iterated := make([]int64, 0, len(created))
|
||||
cutoff := time.Now().Add(24 * time.Hour)
|
||||
iterErr := git_model.IterateLFSMetaObjectsForRepo(ctx, repo.ID, func(ctx context.Context, meta *git_model.LFSMetaObject, count int64) error {
|
||||
iterated = append(iterated, meta.ID)
|
||||
_, err := db.GetEngine(ctx).ID(meta.ID).Cols("updated_unix").Update(&git_model.LFSMetaObject{
|
||||
UpdatedUnix: timeutil.TimeStamp(time.Now().Unix()),
|
||||
})
|
||||
return err
|
||||
}, &git_model.IterateLFSMetaObjectsForRepoOptions{
|
||||
OlderThan: timeutil.TimeStamp(cutoff.Unix()),
|
||||
UpdatedLessRecentlyThan: timeutil.TimeStamp(cutoff.Unix()),
|
||||
})
|
||||
assert.NoError(t, iterErr)
|
||||
|
||||
expected := []int64{created[0].ID, created[1].ID, created[2].ID}
|
||||
assert.Equal(t, expected, iterated)
|
||||
}
|
||||
@ -24,6 +24,18 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
|
||||
return ids
|
||||
}
|
||||
|
||||
// SplitByOpenClosed splits the milestone list into open and closed milestones
|
||||
func (milestones MilestoneList) SplitByOpenClosed() (open, closed MilestoneList) {
|
||||
for _, m := range milestones {
|
||||
if m.IsClosed {
|
||||
closed = append(closed, m)
|
||||
} else {
|
||||
open = append(open, m)
|
||||
}
|
||||
}
|
||||
return open, closed
|
||||
}
|
||||
|
||||
// FindMilestoneOptions contain options to get milestones
|
||||
type FindMilestoneOptions struct {
|
||||
db.ListOptions
|
||||
|
||||
@ -276,17 +276,22 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
|
||||
return UpdateUserCols(ctx, user, "rands")
|
||||
}
|
||||
|
||||
func MakeActiveEmailPrimary(ctx context.Context, emailID int64) error {
|
||||
return makeEmailPrimaryInternal(ctx, emailID, true)
|
||||
func MakeActiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error {
|
||||
return makeEmailPrimaryInternal(ctx, ownerID, emailID, true)
|
||||
}
|
||||
|
||||
func MakeInactiveEmailPrimary(ctx context.Context, emailID int64) error {
|
||||
return makeEmailPrimaryInternal(ctx, emailID, false)
|
||||
func MakeInactiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error {
|
||||
return makeEmailPrimaryInternal(ctx, ownerID, emailID, false)
|
||||
}
|
||||
|
||||
func makeEmailPrimaryInternal(ctx context.Context, emailID int64, isActive bool) error {
|
||||
func makeEmailPrimaryInternal(ctx context.Context, ownerID, emailID int64, isActive bool) error {
|
||||
email := &EmailAddress{}
|
||||
if has, err := db.GetEngine(ctx).ID(emailID).Where(builder.Eq{"is_activated": isActive}).Get(email); err != nil {
|
||||
if has, err := db.GetEngine(ctx).ID(emailID).
|
||||
Where(builder.Eq{
|
||||
"uid": ownerID,
|
||||
"is_activated": isActive,
|
||||
}).
|
||||
Get(email); err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return ErrEmailAddressNotExist{}
|
||||
@ -336,7 +341,7 @@ func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, ne
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MakeInactiveEmailPrimary(ctx, newEmail.ID)
|
||||
return MakeInactiveEmailPrimary(ctx, uid, newEmail.ID)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -46,22 +46,22 @@ func TestIsEmailUsed(t *testing.T) {
|
||||
func TestMakeEmailPrimary(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
err := user_model.MakeActiveEmailPrimary(t.Context(), 9999999)
|
||||
err := user_model.MakeActiveEmailPrimary(t.Context(), 1, 9999999)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{})
|
||||
|
||||
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user11@example.com"})
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) // inactive email is considered as not exist for "MakeActiveEmailPrimary"
|
||||
|
||||
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user9999999@example.com"})
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, user_model.IsErrUserNotExist(err))
|
||||
|
||||
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user101@example.com"})
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, _ := user_model.GetUserByID(t.Context(), int64(10))
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
package emoji
|
||||
|
||||
// Code generated by build/generate-emoji.go. DO NOT EDIT.
|
||||
// Sourced from https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json
|
||||
// Sourced from https://raw.githubusercontent.com/rhysd/gemoji/537ff2d7e0496e9964824f7f73ec7ece88c9765a/db/emoji.json
|
||||
var GemojiData = Gemoji{
|
||||
{"\U0001f44d", "thumbs up", []string{"+1", "thumbsup"}, "6.0", true},
|
||||
{"\U0001f44d\U0001f3ff", "thumbs up: Dark Skin Tone", []string{"+1_Dark_Skin_Tone"}, "12.0", false},
|
||||
@ -345,10 +345,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f1ee\U0001f1f4", "flag: British Indian Ocean Territory", []string{"british_indian_ocean_territory"}, "6.0", false},
|
||||
{"\U0001f1fb\U0001f1ec", "flag: British Virgin Islands", []string{"british_virgin_islands"}, "6.0", false},
|
||||
{"\U0001f966", "broccoli", []string{"broccoli"}, "11.0", false},
|
||||
{"\u26d3\ufe0f\u200d\U0001f4a5", "broken chain", []string{"broken_chain"}, "15.1", false},
|
||||
{"\U0001f494", "broken heart", []string{"broken_heart"}, "6.0", false},
|
||||
{"\U0001f9f9", "broom", []string{"broom"}, "11.0", false},
|
||||
{"\U0001f7e4", "brown circle", []string{"brown_circle"}, "12.0", false},
|
||||
{"\U0001f90e", "brown heart", []string{"brown_heart"}, "12.0", false},
|
||||
{"\U0001f344\u200d\U0001f7eb", "brown mushroom", []string{"brown_mushroom"}, "15.1", false},
|
||||
{"\U0001f7eb", "brown square", []string{"brown_square"}, "12.0", false},
|
||||
{"\U0001f1e7\U0001f1f3", "flag: Brunei", []string{"brunei"}, "6.0", false},
|
||||
{"\U0001f9cb", "bubble tea", []string{"bubble_tea"}, "13.0", false},
|
||||
@ -838,6 +840,7 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f62e\u200d\U0001f4a8", "face exhaling", []string{"face_exhaling"}, "13.1", false},
|
||||
{"\U0001f979", "face holding back tears", []string{"face_holding_back_tears"}, "14.0", false},
|
||||
{"\U0001f636\u200d\U0001f32b\ufe0f", "face in clouds", []string{"face_in_clouds"}, "13.1", false},
|
||||
{"\U0001fae9", "face with bags under eyes", []string{"face_with_bags_under_eyes"}, "16.0", false},
|
||||
{"\U0001fae4", "face with diagonal mouth", []string{"face_with_diagonal_mouth"}, "14.0", false},
|
||||
{"\U0001f915", "face with head-bandage", []string{"face_with_head_bandage"}, "8.0", false},
|
||||
{"\U0001fae2", "face with open eyes and hand over mouth", []string{"face_with_open_eyes_and_hand_over_mouth"}, "14.0", false},
|
||||
@ -879,6 +882,10 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f1eb\U0001f1f0", "flag: Falkland Islands", []string{"falkland_islands"}, "6.0", false},
|
||||
{"\U0001f342", "fallen leaf", []string{"fallen_leaf"}, "6.0", false},
|
||||
{"\U0001f46a", "family", []string{"family"}, "6.0", false},
|
||||
{"\U0001f9d1\u200d\U0001f9d1\u200d\U0001f9d2", "family: adult, adult, child", []string{"family_adult_adult_child"}, "15.1", false},
|
||||
{"\U0001f9d1\u200d\U0001f9d1\u200d\U0001f9d2\u200d\U0001f9d2", "family: adult, adult, child, child", []string{"family_adult_adult_child_child"}, "15.1", false},
|
||||
{"\U0001f9d1\u200d\U0001f9d2", "family: adult, child", []string{"family_adult_child"}, "15.1", false},
|
||||
{"\U0001f9d1\u200d\U0001f9d2\u200d\U0001f9d2", "family: adult, child, child", []string{"family_adult_child_child"}, "15.1", false},
|
||||
{"\U0001f468\u200d\U0001f466", "family: man, boy", []string{"family_man_boy"}, "6.0", false},
|
||||
{"\U0001f468\u200d\U0001f466\u200d\U0001f466", "family: man, boy, boy", []string{"family_man_boy_boy"}, "6.0", false},
|
||||
{"\U0001f468\u200d\U0001f467", "family: man, girl", []string{"family_man_girl"}, "6.0", false},
|
||||
@ -931,6 +938,7 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f4c1", "file folder", []string{"file_folder"}, "6.0", false},
|
||||
{"\U0001f4fd\ufe0f", "film projector", []string{"film_projector"}, "7.0", false},
|
||||
{"\U0001f39e\ufe0f", "film frames", []string{"film_strip"}, "7.0", false},
|
||||
{"\U0001fac6", "fingerprint", []string{"fingerprint"}, "16.0", false},
|
||||
{"\U0001f1eb\U0001f1ee", "flag: Finland", []string{"finland"}, "6.0", false},
|
||||
{"\U0001f525", "fire", []string{"fire"}, "6.0", false},
|
||||
{"\U0001f692", "fire engine", []string{"fire_engine"}, "6.0", false},
|
||||
@ -973,6 +981,7 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f91c\U0001f3fc", "right-facing fist: Medium-Light Skin Tone", []string{"fist_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f91c\U0001f3fd", "right-facing fist: Medium Skin Tone", []string{"fist_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"5\ufe0f\u20e3", "keycap: 5", []string{"five"}, "", false},
|
||||
{"\U0001f1e8\U0001f1f6", "flag: Sark", []string{"flag_sark"}, "16.0", false},
|
||||
{"\U0001f38f", "carp streamer", []string{"flags"}, "6.0", false},
|
||||
{"\U0001f9a9", "flamingo", []string{"flamingo"}, "12.0", false},
|
||||
{"\U0001f526", "flashlight", []string{"flashlight"}, "6.0", false},
|
||||
@ -1189,9 +1198,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f91d\U0001f3fc", "handshake: Medium-Light Skin Tone", []string{"handshake_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f91d\U0001f3fd", "handshake: Medium Skin Tone", []string{"handshake_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f4a9", "pile of poo", []string{"hankey", "poop", "shit"}, "6.0", false},
|
||||
{"\U0001fa89", "harp", []string{"harp"}, "16.0", false},
|
||||
{"#\ufe0f\u20e3", "keycap: #", []string{"hash"}, "", false},
|
||||
{"\U0001f425", "front-facing baby chick", []string{"hatched_chick"}, "6.0", false},
|
||||
{"\U0001f423", "hatching chick", []string{"hatching_chick"}, "6.0", false},
|
||||
{"\U0001f642\u200d\u2194\ufe0f", "head shaking horizontally", []string{"head_shaking_horizontally"}, "15.1", false},
|
||||
{"\U0001f642\u200d\u2195\ufe0f", "head shaking vertically", []string{"head_shaking_vertically"}, "15.1", false},
|
||||
{"\U0001f3a7", "headphone", []string{"headphones"}, "6.0", false},
|
||||
{"\U0001faa6", "headstone", []string{"headstone"}, "13.0", false},
|
||||
{"\U0001f9d1\u200d\u2695\ufe0f", "health worker", []string{"health_worker"}, "12.1", true},
|
||||
@ -1380,6 +1392,7 @@ var GemojiData = Gemoji{
|
||||
{"\u271d\ufe0f", "latin cross", []string{"latin_cross"}, "", false},
|
||||
{"\U0001f1f1\U0001f1fb", "flag: Latvia", []string{"latvia"}, "6.0", false},
|
||||
{"\U0001f606", "grinning squinting face", []string{"laughing", "satisfied", "laugh"}, "6.0", false},
|
||||
{"\U0001fabe", "leafless tree", []string{"leafless_tree"}, "16.0", false},
|
||||
{"\U0001f96c", "leafy green", []string{"leafy_green"}, "11.0", false},
|
||||
{"\U0001f343", "leaf fluttering in wind", []string{"leaves"}, "6.0", false},
|
||||
{"\U0001f1f1\U0001f1e7", "flag: Lebanon", []string{"lebanon"}, "6.0", false},
|
||||
@ -1417,6 +1430,7 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f1f1\U0001f1ee", "flag: Liechtenstein", []string{"liechtenstein"}, "6.0", false},
|
||||
{"\U0001fa75", "light blue heart", []string{"light_blue_heart"}, "15.0", false},
|
||||
{"\U0001f688", "light rail", []string{"light_rail"}, "6.0", false},
|
||||
{"\U0001f34b\u200d\U0001f7e9", "lime", []string{"lime"}, "15.1", false},
|
||||
{"\U0001f517", "link", []string{"link"}, "6.0", false},
|
||||
{"\U0001f981", "lion", []string{"lion"}, "8.0", false},
|
||||
{"\U0001f444", "mouth", []string{"lips"}, "6.0", false},
|
||||
@ -1594,12 +1608,24 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f468\U0001f3fe\u200d\U0001f9bd", "man in manual wheelchair: Medium-Dark Skin Tone", []string{"man_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fc\u200d\U0001f9bd", "man in manual wheelchair: Medium-Light Skin Tone", []string{"man_in_manual_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fd\u200d\U0001f9bd", "man in manual wheelchair: Medium Skin Tone", []string{"man_in_manual_wheelchair_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right", []string{"man_in_manual_wheelchair_facing_right"}, "15.1", true},
|
||||
{"\U0001f468\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right: Dark Skin Tone", []string{"man_in_manual_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right: Light Skin Tone", []string{"man_in_manual_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right: Medium-Dark Skin Tone", []string{"man_in_manual_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right: Medium-Light Skin Tone", []string{"man_in_manual_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right: Medium Skin Tone", []string{"man_in_manual_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\u200d\U0001f9bc", "man in motorized wheelchair", []string{"man_in_motorized_wheelchair"}, "12.0", true},
|
||||
{"\U0001f468\U0001f3ff\u200d\U0001f9bc", "man in motorized wheelchair: Dark Skin Tone", []string{"man_in_motorized_wheelchair_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fb\u200d\U0001f9bc", "man in motorized wheelchair: Light Skin Tone", []string{"man_in_motorized_wheelchair_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fe\u200d\U0001f9bc", "man in motorized wheelchair: Medium-Dark Skin Tone", []string{"man_in_motorized_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fc\u200d\U0001f9bc", "man in motorized wheelchair: Medium-Light Skin Tone", []string{"man_in_motorized_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fd\u200d\U0001f9bc", "man in motorized wheelchair: Medium Skin Tone", []string{"man_in_motorized_wheelchair_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right", []string{"man_in_motorized_wheelchair_facing_right"}, "15.1", true},
|
||||
{"\U0001f468\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right: Dark Skin Tone", []string{"man_in_motorized_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right: Light Skin Tone", []string{"man_in_motorized_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right: Medium-Dark Skin Tone", []string{"man_in_motorized_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right: Medium-Light Skin Tone", []string{"man_in_motorized_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right: Medium Skin Tone", []string{"man_in_motorized_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f935\u200d\u2642\ufe0f", "man in tuxedo", []string{"man_in_tuxedo"}, "13.0", true},
|
||||
{"\U0001f935\U0001f3ff\u200d\u2642\ufe0f", "man in tuxedo: Dark Skin Tone", []string{"man_in_tuxedo_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f935\U0001f3fb\u200d\u2642\ufe0f", "man in tuxedo: Light Skin Tone", []string{"man_in_tuxedo_Light_Skin_Tone"}, "12.0", false},
|
||||
@ -1618,6 +1644,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f939\U0001f3fe\u200d\u2642\ufe0f", "man juggling: Medium-Dark Skin Tone", []string{"man_juggling_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f939\U0001f3fc\u200d\u2642\ufe0f", "man juggling: Medium-Light Skin Tone", []string{"man_juggling_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f939\U0001f3fd\u200d\u2642\ufe0f", "man juggling: Medium Skin Tone", []string{"man_juggling_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right", []string{"man_kneeling_facing_right"}, "15.1", true},
|
||||
{"\U0001f9ce\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right: Dark Skin Tone", []string{"man_kneeling_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right: Light Skin Tone", []string{"man_kneeling_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right: Medium-Dark Skin Tone", []string{"man_kneeling_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right: Medium-Light Skin Tone", []string{"man_kneeling_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right: Medium Skin Tone", []string{"man_kneeling_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\u200d\U0001f527", "man mechanic", []string{"man_mechanic"}, "", true},
|
||||
{"\U0001f468\U0001f3ff\u200d\U0001f527", "man mechanic: Dark Skin Tone", []string{"man_mechanic_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fb\u200d\U0001f527", "man mechanic: Light Skin Tone", []string{"man_mechanic_Light_Skin_Tone"}, "12.0", false},
|
||||
@ -1648,6 +1680,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f93d\U0001f3fe\u200d\u2642\ufe0f", "man playing water polo: Medium-Dark Skin Tone", []string{"man_playing_water_polo_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f93d\U0001f3fc\u200d\u2642\ufe0f", "man playing water polo: Medium-Light Skin Tone", []string{"man_playing_water_polo_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f93d\U0001f3fd\u200d\u2642\ufe0f", "man playing water polo: Medium Skin Tone", []string{"man_playing_water_polo_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right", []string{"man_running_facing_right"}, "15.1", true},
|
||||
{"\U0001f3c3\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right: Dark Skin Tone", []string{"man_running_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right: Light Skin Tone", []string{"man_running_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right: Medium-Dark Skin Tone", []string{"man_running_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right: Medium-Light Skin Tone", []string{"man_running_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right: Medium Skin Tone", []string{"man_running_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\u200d\U0001f52c", "man scientist", []string{"man_scientist"}, "", true},
|
||||
{"\U0001f468\U0001f3ff\u200d\U0001f52c", "man scientist: Dark Skin Tone", []string{"man_scientist_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fb\u200d\U0001f52c", "man scientist: Light Skin Tone", []string{"man_scientist_Light_Skin_Tone"}, "12.0", false},
|
||||
@ -1684,6 +1722,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f468\U0001f3fe\u200d\U0001f4bb", "man technologist: Medium-Dark Skin Tone", []string{"man_technologist_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fc\u200d\U0001f4bb", "man technologist: Medium-Light Skin Tone", []string{"man_technologist_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fd\u200d\U0001f4bb", "man technologist: Medium Skin Tone", []string{"man_technologist_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right", []string{"man_walking_facing_right"}, "15.1", true},
|
||||
{"\U0001f6b6\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right: Dark Skin Tone", []string{"man_walking_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right: Light Skin Tone", []string{"man_walking_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right: Medium-Dark Skin Tone", []string{"man_walking_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right: Medium-Light Skin Tone", []string{"man_walking_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right: Medium Skin Tone", []string{"man_walking_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f472", "person with skullcap", []string{"man_with_gua_pi_mao"}, "6.0", true},
|
||||
{"\U0001f472\U0001f3ff", "person with skullcap: Dark Skin Tone", []string{"man_with_gua_pi_mao_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f472\U0001f3fb", "person with skullcap: Light Skin Tone", []string{"man_with_gua_pi_mao_Light_Skin_Tone"}, "12.0", false},
|
||||
@ -1708,6 +1752,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f470\U0001f3fe\u200d\u2642\ufe0f", "man with veil: Medium-Dark Skin Tone", []string{"man_with_veil_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f470\U0001f3fc\u200d\u2642\ufe0f", "man with veil: Medium-Light Skin Tone", []string{"man_with_veil_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f470\U0001f3fd\u200d\u2642\ufe0f", "man with veil: Medium Skin Tone", []string{"man_with_veil_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right", []string{"man_with_white_cane_facing_right"}, "15.1", true},
|
||||
{"\U0001f468\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right: Dark Skin Tone", []string{"man_with_white_cane_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right: Light Skin Tone", []string{"man_with_white_cane_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right: Medium-Dark Skin Tone", []string{"man_with_white_cane_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right: Medium-Light Skin Tone", []string{"man_with_white_cane_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f468\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right: Medium Skin Tone", []string{"man_with_white_cane_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f96d", "mango", []string{"mango"}, "11.0", false},
|
||||
{"\U0001f45e", "man’s shoe", []string{"mans_shoe", "shoe"}, "6.0", false},
|
||||
{"\U0001f570\ufe0f", "mantelpiece clock", []string{"mantelpiece_clock"}, "7.0", false},
|
||||
@ -1874,12 +1924,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f3b5", "musical note", []string{"musical_note"}, "6.0", false},
|
||||
{"\U0001f3bc", "musical score", []string{"musical_score"}, "6.0", false},
|
||||
{"\U0001f507", "muted speaker", []string{"mute"}, "6.0", false},
|
||||
{"\U0001f9d1\u200d\U0001f384", "mx claus", []string{"mx_claus"}, "13.0", true},
|
||||
{"\U0001f9d1\U0001f3ff\u200d\U0001f384", "mx claus: Dark Skin Tone", []string{"mx_claus_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fb\u200d\U0001f384", "mx claus: Light Skin Tone", []string{"mx_claus_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fe\u200d\U0001f384", "mx claus: Medium-Dark Skin Tone", []string{"mx_claus_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fc\u200d\U0001f384", "mx claus: Medium-Light Skin Tone", []string{"mx_claus_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fd\u200d\U0001f384", "mx claus: Medium Skin Tone", []string{"mx_claus_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\u200d\U0001f384", "Mx Claus", []string{"mx_claus"}, "13.0", true},
|
||||
{"\U0001f9d1\U0001f3ff\u200d\U0001f384", "Mx Claus: Dark Skin Tone", []string{"mx_claus_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fb\u200d\U0001f384", "Mx Claus: Light Skin Tone", []string{"mx_claus_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fe\u200d\U0001f384", "Mx Claus: Medium-Dark Skin Tone", []string{"mx_claus_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fc\u200d\U0001f384", "Mx Claus: Medium-Light Skin Tone", []string{"mx_claus_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fd\u200d\U0001f384", "Mx Claus: Medium Skin Tone", []string{"mx_claus_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f1f2\U0001f1f2", "flag: Myanmar (Burma)", []string{"myanmar"}, "6.0", false},
|
||||
{"\U0001f485", "nail polish", []string{"nail_care"}, "6.0", true},
|
||||
{"\U0001f485\U0001f3ff", "nail polish: Dark Skin Tone", []string{"nail_care_Dark_Skin_Tone"}, "12.0", false},
|
||||
@ -2140,24 +2190,54 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f9d1\U0001f3fe\u200d\U0001f9bd", "person in manual wheelchair: Medium-Dark Skin Tone", []string{"person_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fc\u200d\U0001f9bd", "person in manual wheelchair: Medium-Light Skin Tone", []string{"person_in_manual_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fd\u200d\U0001f9bd", "person in manual wheelchair: Medium Skin Tone", []string{"person_in_manual_wheelchair_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right", []string{"person_in_manual_wheelchair_facing_right"}, "15.1", true},
|
||||
{"\U0001f9d1\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right: Dark Skin Tone", []string{"person_in_manual_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right: Light Skin Tone", []string{"person_in_manual_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right: Medium-Dark Skin Tone", []string{"person_in_manual_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right: Medium-Light Skin Tone", []string{"person_in_manual_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right: Medium Skin Tone", []string{"person_in_manual_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\u200d\U0001f9bc", "person in motorized wheelchair", []string{"person_in_motorized_wheelchair"}, "12.1", true},
|
||||
{"\U0001f9d1\U0001f3ff\u200d\U0001f9bc", "person in motorized wheelchair: Dark Skin Tone", []string{"person_in_motorized_wheelchair_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fb\u200d\U0001f9bc", "person in motorized wheelchair: Light Skin Tone", []string{"person_in_motorized_wheelchair_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fe\u200d\U0001f9bc", "person in motorized wheelchair: Medium-Dark Skin Tone", []string{"person_in_motorized_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fc\u200d\U0001f9bc", "person in motorized wheelchair: Medium-Light Skin Tone", []string{"person_in_motorized_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fd\u200d\U0001f9bc", "person in motorized wheelchair: Medium Skin Tone", []string{"person_in_motorized_wheelchair_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right", []string{"person_in_motorized_wheelchair_facing_right"}, "15.1", true},
|
||||
{"\U0001f9d1\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right: Dark Skin Tone", []string{"person_in_motorized_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right: Light Skin Tone", []string{"person_in_motorized_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right: Medium-Dark Skin Tone", []string{"person_in_motorized_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right: Medium-Light Skin Tone", []string{"person_in_motorized_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right: Medium Skin Tone", []string{"person_in_motorized_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f935", "person in tuxedo", []string{"person_in_tuxedo"}, "9.0", true},
|
||||
{"\U0001f935\U0001f3ff", "person in tuxedo: Dark Skin Tone", []string{"person_in_tuxedo_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f935\U0001f3fb", "person in tuxedo: Light Skin Tone", []string{"person_in_tuxedo_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f935\U0001f3fe", "person in tuxedo: Medium-Dark Skin Tone", []string{"person_in_tuxedo_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f935\U0001f3fc", "person in tuxedo: Medium-Light Skin Tone", []string{"person_in_tuxedo_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f935\U0001f3fd", "person in tuxedo: Medium Skin Tone", []string{"person_in_tuxedo_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\u200d\u27a1\ufe0f", "person kneeling facing right", []string{"person_kneeling_facing_right"}, "15.1", true},
|
||||
{"\U0001f9ce\U0001f3ff\u200d\u27a1\ufe0f", "person kneeling facing right: Dark Skin Tone", []string{"person_kneeling_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fb\u200d\u27a1\ufe0f", "person kneeling facing right: Light Skin Tone", []string{"person_kneeling_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fe\u200d\u27a1\ufe0f", "person kneeling facing right: Medium-Dark Skin Tone", []string{"person_kneeling_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fc\u200d\u27a1\ufe0f", "person kneeling facing right: Medium-Light Skin Tone", []string{"person_kneeling_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fd\u200d\u27a1\ufe0f", "person kneeling facing right: Medium Skin Tone", []string{"person_kneeling_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\u200d\U0001f9b0", "person: red hair", []string{"person_red_hair"}, "12.1", true},
|
||||
{"\U0001f9d1\U0001f3ff\u200d\U0001f9b0", "person: red hair: Dark Skin Tone", []string{"person_red_hair_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fb\u200d\U0001f9b0", "person: red hair: Light Skin Tone", []string{"person_red_hair_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fe\u200d\U0001f9b0", "person: red hair: Medium-Dark Skin Tone", []string{"person_red_hair_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fc\u200d\U0001f9b0", "person: red hair: Medium-Light Skin Tone", []string{"person_red_hair_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fd\u200d\U0001f9b0", "person: red hair: Medium Skin Tone", []string{"person_red_hair_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\u200d\u27a1\ufe0f", "person running facing right", []string{"person_running_facing_right"}, "15.1", true},
|
||||
{"\U0001f3c3\U0001f3ff\u200d\u27a1\ufe0f", "person running facing right: Dark Skin Tone", []string{"person_running_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fb\u200d\u27a1\ufe0f", "person running facing right: Light Skin Tone", []string{"person_running_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fe\u200d\u27a1\ufe0f", "person running facing right: Medium-Dark Skin Tone", []string{"person_running_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fc\u200d\u27a1\ufe0f", "person running facing right: Medium-Light Skin Tone", []string{"person_running_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fd\u200d\u27a1\ufe0f", "person running facing right: Medium Skin Tone", []string{"person_running_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\u200d\u27a1\ufe0f", "person walking facing right", []string{"person_walking_facing_right"}, "15.1", true},
|
||||
{"\U0001f6b6\U0001f3ff\u200d\u27a1\ufe0f", "person walking facing right: Dark Skin Tone", []string{"person_walking_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fb\u200d\u27a1\ufe0f", "person walking facing right: Light Skin Tone", []string{"person_walking_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fe\u200d\u27a1\ufe0f", "person walking facing right: Medium-Dark Skin Tone", []string{"person_walking_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fc\u200d\u27a1\ufe0f", "person walking facing right: Medium-Light Skin Tone", []string{"person_walking_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fd\u200d\u27a1\ufe0f", "person walking facing right: Medium Skin Tone", []string{"person_walking_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\u200d\U0001f9b3", "person: white hair", []string{"person_white_hair"}, "12.1", true},
|
||||
{"\U0001f9d1\U0001f3ff\u200d\U0001f9b3", "person: white hair: Dark Skin Tone", []string{"person_white_hair_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fb\u200d\U0001f9b3", "person: white hair: Light Skin Tone", []string{"person_white_hair_Light_Skin_Tone"}, "12.0", false},
|
||||
@ -2188,9 +2268,16 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f470\U0001f3fe", "person with veil: Medium-Dark Skin Tone", []string{"person_with_veil_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f470\U0001f3fc", "person with veil: Medium-Light Skin Tone", []string{"person_with_veil_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f470\U0001f3fd", "person with veil: Medium Skin Tone", []string{"person_with_veil_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right", []string{"person_with_white_cane_facing_right"}, "15.1", true},
|
||||
{"\U0001f9d1\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right: Dark Skin Tone", []string{"person_with_white_cane_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right: Light Skin Tone", []string{"person_with_white_cane_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right: Medium-Dark Skin Tone", []string{"person_with_white_cane_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right: Medium-Light Skin Tone", []string{"person_with_white_cane_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d1\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right: Medium Skin Tone", []string{"person_with_white_cane_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f1f5\U0001f1ea", "flag: Peru", []string{"peru"}, "6.0", false},
|
||||
{"\U0001f9eb", "petri dish", []string{"petri_dish"}, "11.0", false},
|
||||
{"\U0001f1f5\U0001f1ed", "flag: Philippines", []string{"philippines"}, "6.0", false},
|
||||
{"\U0001f426\u200d\U0001f525", "phoenix", []string{"phoenix"}, "15.1", false},
|
||||
{"\u260e\ufe0f", "telephone", []string{"phone", "telephone"}, "", false},
|
||||
{"\u26cf\ufe0f", "pick", []string{"pick"}, "5.2", false},
|
||||
{"\U0001f6fb", "pickup truck", []string{"pickup_truck"}, "13.0", false},
|
||||
@ -2480,6 +2567,7 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f6fc", "roller skate", []string{"roller_skate"}, "13.0", false},
|
||||
{"\U0001f1f7\U0001f1f4", "flag: Romania", []string{"romania"}, "6.0", false},
|
||||
{"\U0001f413", "rooster", []string{"rooster"}, "6.0", false},
|
||||
{"\U0001fadc", "root vegetable", []string{"root_vegetable"}, "16.0", false},
|
||||
{"\U0001f339", "rose", []string{"rose"}, "6.0", false},
|
||||
{"\U0001f3f5\ufe0f", "rosette", []string{"rosette"}, "7.0", false},
|
||||
{"\U0001f6a8", "police car light", []string{"rotating_light"}, "6.0", false},
|
||||
@ -2613,6 +2701,7 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f6cd\ufe0f", "shopping bags", []string{"shopping"}, "7.0", false},
|
||||
{"\U0001f6d2", "shopping cart", []string{"shopping_cart"}, "9.0", false},
|
||||
{"\U0001fa73", "shorts", []string{"shorts"}, "12.0", false},
|
||||
{"\U0001fa8f", "shovel", []string{"shovel"}, "16.0", false},
|
||||
{"\U0001f6bf", "shower", []string{"shower"}, "6.0", false},
|
||||
{"\U0001f990", "shrimp", []string{"shrimp"}, "9.0", false},
|
||||
{"\U0001f937", "person shrugging", []string{"shrug"}, "11.0", true},
|
||||
@ -2711,6 +2800,7 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f578\ufe0f", "spider web", []string{"spider_web"}, "7.0", false},
|
||||
{"\U0001f5d3\ufe0f", "spiral calendar", []string{"spiral_calendar"}, "7.0", false},
|
||||
{"\U0001f5d2\ufe0f", "spiral notepad", []string{"spiral_notepad"}, "7.0", false},
|
||||
{"\U0001fadf", "splatter", []string{"splatter"}, "16.0", false},
|
||||
{"\U0001f9fd", "sponge", []string{"sponge"}, "11.0", false},
|
||||
{"\U0001f944", "spoon", []string{"spoon"}, "9.0", false},
|
||||
{"\U0001f991", "squid", []string{"squid"}, "9.0", false},
|
||||
@ -2945,7 +3035,7 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f51d", "TOP arrow", []string{"top"}, "6.0", false},
|
||||
{"\U0001f3a9", "top hat", []string{"tophat"}, "6.0", false},
|
||||
{"\U0001f32a\ufe0f", "tornado", []string{"tornado"}, "7.0", false},
|
||||
{"\U0001f1f9\U0001f1f7", "flag: Turkey", []string{"tr"}, "8.0", false},
|
||||
{"\U0001f1f9\U0001f1f7", "flag: Türkiye", []string{"tr"}, "8.0", false},
|
||||
{"\U0001f5b2\ufe0f", "trackball", []string{"trackball"}, "7.0", false},
|
||||
{"\U0001f69c", "tractor", []string{"tractor"}, "6.0", false},
|
||||
{"\U0001f6a5", "horizontal traffic light", []string{"traffic_light"}, "6.0", false},
|
||||
@ -3247,12 +3337,24 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f469\U0001f3fe\u200d\U0001f9bd", "woman in manual wheelchair: Medium-Dark Skin Tone", []string{"woman_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fc\u200d\U0001f9bd", "woman in manual wheelchair: Medium-Light Skin Tone", []string{"woman_in_manual_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fd\u200d\U0001f9bd", "woman in manual wheelchair: Medium Skin Tone", []string{"woman_in_manual_wheelchair_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right", []string{"woman_in_manual_wheelchair_facing_right"}, "15.1", true},
|
||||
{"\U0001f469\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right: Dark Skin Tone", []string{"woman_in_manual_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right: Light Skin Tone", []string{"woman_in_manual_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right: Medium-Dark Skin Tone", []string{"woman_in_manual_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right: Medium-Light Skin Tone", []string{"woman_in_manual_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right: Medium Skin Tone", []string{"woman_in_manual_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\u200d\U0001f9bc", "woman in motorized wheelchair", []string{"woman_in_motorized_wheelchair"}, "12.0", true},
|
||||
{"\U0001f469\U0001f3ff\u200d\U0001f9bc", "woman in motorized wheelchair: Dark Skin Tone", []string{"woman_in_motorized_wheelchair_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fb\u200d\U0001f9bc", "woman in motorized wheelchair: Light Skin Tone", []string{"woman_in_motorized_wheelchair_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fe\u200d\U0001f9bc", "woman in motorized wheelchair: Medium-Dark Skin Tone", []string{"woman_in_motorized_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fc\u200d\U0001f9bc", "woman in motorized wheelchair: Medium-Light Skin Tone", []string{"woman_in_motorized_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fd\u200d\U0001f9bc", "woman in motorized wheelchair: Medium Skin Tone", []string{"woman_in_motorized_wheelchair_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right", []string{"woman_in_motorized_wheelchair_facing_right"}, "15.1", true},
|
||||
{"\U0001f469\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right: Dark Skin Tone", []string{"woman_in_motorized_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right: Light Skin Tone", []string{"woman_in_motorized_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right: Medium-Dark Skin Tone", []string{"woman_in_motorized_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right: Medium-Light Skin Tone", []string{"woman_in_motorized_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right: Medium Skin Tone", []string{"woman_in_motorized_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f935\u200d\u2640\ufe0f", "woman in tuxedo", []string{"woman_in_tuxedo"}, "13.0", true},
|
||||
{"\U0001f935\U0001f3ff\u200d\u2640\ufe0f", "woman in tuxedo: Dark Skin Tone", []string{"woman_in_tuxedo_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f935\U0001f3fb\u200d\u2640\ufe0f", "woman in tuxedo: Light Skin Tone", []string{"woman_in_tuxedo_Light_Skin_Tone"}, "12.0", false},
|
||||
@ -3271,6 +3373,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f939\U0001f3fe\u200d\u2640\ufe0f", "woman juggling: Medium-Dark Skin Tone", []string{"woman_juggling_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f939\U0001f3fc\u200d\u2640\ufe0f", "woman juggling: Medium-Light Skin Tone", []string{"woman_juggling_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f939\U0001f3fd\u200d\u2640\ufe0f", "woman juggling: Medium Skin Tone", []string{"woman_juggling_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right", []string{"woman_kneeling_facing_right"}, "15.1", true},
|
||||
{"\U0001f9ce\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right: Dark Skin Tone", []string{"woman_kneeling_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right: Light Skin Tone", []string{"woman_kneeling_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right: Medium-Dark Skin Tone", []string{"woman_kneeling_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right: Medium-Light Skin Tone", []string{"woman_kneeling_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9ce\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right: Medium Skin Tone", []string{"woman_kneeling_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\u200d\U0001f527", "woman mechanic", []string{"woman_mechanic"}, "", true},
|
||||
{"\U0001f469\U0001f3ff\u200d\U0001f527", "woman mechanic: Dark Skin Tone", []string{"woman_mechanic_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fb\u200d\U0001f527", "woman mechanic: Light Skin Tone", []string{"woman_mechanic_Light_Skin_Tone"}, "12.0", false},
|
||||
@ -3301,6 +3409,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f93d\U0001f3fe\u200d\u2640\ufe0f", "woman playing water polo: Medium-Dark Skin Tone", []string{"woman_playing_water_polo_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f93d\U0001f3fc\u200d\u2640\ufe0f", "woman playing water polo: Medium-Light Skin Tone", []string{"woman_playing_water_polo_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f93d\U0001f3fd\u200d\u2640\ufe0f", "woman playing water polo: Medium Skin Tone", []string{"woman_playing_water_polo_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right", []string{"woman_running_facing_right"}, "15.1", true},
|
||||
{"\U0001f3c3\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right: Dark Skin Tone", []string{"woman_running_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right: Light Skin Tone", []string{"woman_running_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right: Medium-Dark Skin Tone", []string{"woman_running_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right: Medium-Light Skin Tone", []string{"woman_running_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f3c3\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right: Medium Skin Tone", []string{"woman_running_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\u200d\U0001f52c", "woman scientist", []string{"woman_scientist"}, "", true},
|
||||
{"\U0001f469\U0001f3ff\u200d\U0001f52c", "woman scientist: Dark Skin Tone", []string{"woman_scientist_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fb\u200d\U0001f52c", "woman scientist: Light Skin Tone", []string{"woman_scientist_Light_Skin_Tone"}, "12.0", false},
|
||||
@ -3337,6 +3451,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f469\U0001f3fe\u200d\U0001f4bb", "woman technologist: Medium-Dark Skin Tone", []string{"woman_technologist_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fc\u200d\U0001f4bb", "woman technologist: Medium-Light Skin Tone", []string{"woman_technologist_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fd\u200d\U0001f4bb", "woman technologist: Medium Skin Tone", []string{"woman_technologist_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right", []string{"woman_walking_facing_right"}, "15.1", true},
|
||||
{"\U0001f6b6\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right: Dark Skin Tone", []string{"woman_walking_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right: Light Skin Tone", []string{"woman_walking_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right: Medium-Dark Skin Tone", []string{"woman_walking_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right: Medium-Light Skin Tone", []string{"woman_walking_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f6b6\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right: Medium Skin Tone", []string{"woman_walking_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d5", "woman with headscarf", []string{"woman_with_headscarf"}, "11.0", true},
|
||||
{"\U0001f9d5\U0001f3ff", "woman with headscarf: Dark Skin Tone", []string{"woman_with_headscarf_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f9d5\U0001f3fb", "woman with headscarf: Light Skin Tone", []string{"woman_with_headscarf_Light_Skin_Tone"}, "12.0", false},
|
||||
@ -3361,6 +3481,12 @@ var GemojiData = Gemoji{
|
||||
{"\U0001f470\U0001f3fe\u200d\u2640\ufe0f", "woman with veil: Medium-Dark Skin Tone", []string{"woman_with_veil_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f470\U0001f3fc\u200d\u2640\ufe0f", "woman with veil: Medium-Light Skin Tone", []string{"woman_with_veil_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f470\U0001f3fd\u200d\u2640\ufe0f", "woman with veil: Medium Skin Tone", []string{"woman_with_veil_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right", []string{"woman_with_white_cane_facing_right"}, "15.1", true},
|
||||
{"\U0001f469\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right: Dark Skin Tone", []string{"woman_with_white_cane_facing_right_Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right: Light Skin Tone", []string{"woman_with_white_cane_facing_right_Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right: Medium-Dark Skin Tone", []string{"woman_with_white_cane_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right: Medium-Light Skin Tone", []string{"woman_with_white_cane_facing_right_Medium-Light_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f469\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right: Medium Skin Tone", []string{"woman_with_white_cane_facing_right_Medium_Skin_Tone"}, "12.0", false},
|
||||
{"\U0001f45a", "woman’s clothes", []string{"womans_clothes"}, "6.0", false},
|
||||
{"\U0001f452", "woman’s hat", []string{"womans_hat"}, "6.0", false},
|
||||
{"\U0001f93c\u200d\u2640\ufe0f", "women wrestling", []string{"women_wrestling"}, "9.0", false},
|
||||
|
||||
@ -28,44 +28,37 @@ const (
|
||||
|
||||
// GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
|
||||
func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer io.Writer) (retErr error) {
|
||||
diffOutput, diffFinish, err := getRepoRawDiffForFile(repo.Ctx, repo, "", commitID, diffType, "")
|
||||
cmd, err := getRepoRawDiffForFileCmd(repo.Ctx, repo, "", commitID, diffType, "")
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getRepoRawDiffForFileCmd: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err := diffFinish()
|
||||
if retErr == nil {
|
||||
retErr = err // only return command's error if no previous error
|
||||
}
|
||||
}()
|
||||
_, err = io.Copy(writer, diffOutput)
|
||||
return err
|
||||
return cmd.WithStdoutCopy(writer).RunWithStderr(repo.Ctx)
|
||||
}
|
||||
|
||||
// GetFileDiffCutAroundLine cuts the old or new part of the diff of a file around a specific line number
|
||||
func GetFileDiffCutAroundLine(
|
||||
repo *Repository, startCommit, endCommit, treePath string,
|
||||
line int64, old bool, numbersOfLine int,
|
||||
) (_ string, retErr error) {
|
||||
diffOutput, diffFinish, err := getRepoRawDiffForFile(repo.Ctx, repo, startCommit, endCommit, RawDiffNormal, treePath)
|
||||
) (ret string, retErr error) {
|
||||
cmd, err := getRepoRawDiffForFileCmd(repo.Ctx, repo, startCommit, endCommit, RawDiffNormal, treePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("getRepoRawDiffForFileCmd: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err := diffFinish()
|
||||
if retErr == nil {
|
||||
retErr = err // only return command's error if no previous error
|
||||
}
|
||||
}()
|
||||
return CutDiffAroundLine(diffOutput, line, old, numbersOfLine)
|
||||
stdoutReader, stdoutClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutClose()
|
||||
cmd.WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||
ret, err = CutDiffAroundLine(stdoutReader, line, old, numbersOfLine)
|
||||
return err
|
||||
})
|
||||
return ret, cmd.RunWithStderr(repo.Ctx)
|
||||
}
|
||||
|
||||
// getRepoRawDiffForFile returns an io.Reader for the diff results of file in given commit ID
|
||||
// and a "finish" function to wait for the git command and clean up resources after reading is done.
|
||||
func getRepoRawDiffForFile(ctx context.Context, repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string) (io.Reader, func() gitcmd.RunStdError, error) {
|
||||
func getRepoRawDiffForFileCmd(_ context.Context, repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string) (*gitcmd.Command, error) {
|
||||
commit, err := repo.GetCommit(endCommit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
var files []string
|
||||
if len(file) > 0 {
|
||||
@ -84,7 +77,7 @@ func getRepoRawDiffForFile(ctx context.Context, repo *Repository, startCommit, e
|
||||
} else {
|
||||
c, err := commit.Parent(0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
cmd.AddArguments("diff").
|
||||
AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold).
|
||||
@ -99,25 +92,15 @@ func getRepoRawDiffForFile(ctx context.Context, repo *Repository, startCommit, e
|
||||
} else {
|
||||
c, err := commit.Parent(0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
|
||||
cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...)
|
||||
}
|
||||
default:
|
||||
return nil, nil, util.NewInvalidArgumentErrorf("invalid diff type: %s", diffType)
|
||||
return nil, util.NewInvalidArgumentErrorf("invalid diff type: %s", diffType)
|
||||
}
|
||||
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
err = cmd.StartWithStderr(ctx)
|
||||
if err != nil {
|
||||
stdoutReaderClose()
|
||||
return nil, nil, err
|
||||
}
|
||||
return stdoutReader, func() gitcmd.RunStdError {
|
||||
stdoutReaderClose()
|
||||
return cmd.WaitWithStderr()
|
||||
}, nil
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// ParseDiffHunkString parse the diff hunk content and return
|
||||
@ -254,7 +237,7 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("CutDiffAroundLine: scan: %w", err)
|
||||
}
|
||||
|
||||
// No hunk found
|
||||
|
||||
@ -306,6 +306,10 @@ func (c *Command) MakeStdinPipe() (writer PipeWriter, closer func()) {
|
||||
// MakeStdoutPipe creates a reader for the command's stdout.
|
||||
// The returned closer function must be called by the caller to close the pipe.
|
||||
// After the pipe reader is closed, the unread data will be discarded.
|
||||
//
|
||||
// If the process (git command) still tries to write after the pipe is closed, the Wait error will be "signal: broken pipe".
|
||||
// WithPipelineFunc + Run won't return "broken pipe" error in this case if the callback returns no error.
|
||||
// But if you are calling Start / Wait family functions, you should either drain the pipe before close it, or handle the Wait error correctly.
|
||||
func (c *Command) MakeStdoutPipe() (reader PipeReader, closer func()) {
|
||||
return c.makeStdoutStderr(&c.cmdStdout)
|
||||
}
|
||||
|
||||
@ -11,20 +11,16 @@ import (
|
||||
gohtml "html"
|
||||
"html/template"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/analyze"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/go-enry/go-enry/v2"
|
||||
)
|
||||
|
||||
// don't index files larger than this many bytes for performance purposes
|
||||
@ -84,85 +80,21 @@ func UnsafeSplitHighlightedLines(code template.HTML) (ret [][]byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func getChromaLexerByLanguage(fileName, lang string) chroma.Lexer {
|
||||
lang, _, _ = strings.Cut(lang, "?") // maybe, the value from gitattributes might contain `?` parameters?
|
||||
ext := path.Ext(fileName)
|
||||
// the "lang" might come from enry, it has different naming for some languages
|
||||
switch lang {
|
||||
case "F#":
|
||||
lang = "FSharp"
|
||||
case "Pascal":
|
||||
lang = "ObjectPascal"
|
||||
case "C":
|
||||
if ext == ".C" || ext == ".H" {
|
||||
lang = "C++"
|
||||
}
|
||||
}
|
||||
if lang == "" && util.AsciiEqualFold(ext, ".sql") {
|
||||
// there is a bug when using MySQL lexer: "--\nSELECT", the second line will be rendered as comment incorrectly
|
||||
lang = "SQL"
|
||||
}
|
||||
// lexers.Get is slow if the language name can't be matched directly: it does extra "Match" call to iterate all lexers
|
||||
return lexers.Get(lang)
|
||||
}
|
||||
|
||||
// GetChromaLexerWithFallback returns a chroma lexer by given file name, language and code content. All parameters can be optional.
|
||||
// When code content is provided, it will be slow if no lexer is found by file name or language.
|
||||
// If no lexer is found, it will return the fallback lexer.
|
||||
func GetChromaLexerWithFallback(fileName, lang string, code []byte) (lexer chroma.Lexer) {
|
||||
if lang != "" {
|
||||
lexer = getChromaLexerByLanguage(fileName, lang)
|
||||
}
|
||||
|
||||
if lexer == nil {
|
||||
fileExt := path.Ext(fileName)
|
||||
if val, ok := globalVars().highlightMapping[fileExt]; ok {
|
||||
lexer = getChromaLexerByLanguage(fileName, val) // use mapped value to find lexer
|
||||
}
|
||||
}
|
||||
|
||||
if lexer == nil {
|
||||
// when using "code" to detect, analyze.GetCodeLanguage is slower, it iterates many rules to detect language from content
|
||||
// this is the old logic: use enry to detect language, and use chroma to render, but their naming is different for some languages
|
||||
enryLanguage := analyze.GetCodeLanguage(fileName, code)
|
||||
lexer = getChromaLexerByLanguage(fileName, enryLanguage)
|
||||
if lexer == nil {
|
||||
if enryLanguage != enry.OtherLanguage {
|
||||
log.Warn("No chroma lexer found for enry detected language: %s (file: %s), need to fix the language mapping between enry and chroma.", enryLanguage, fileName)
|
||||
}
|
||||
lexer = lexers.Match(fileName) // lexers.Match will search by its basename and extname
|
||||
}
|
||||
}
|
||||
|
||||
return util.IfZero(lexer, lexers.Fallback)
|
||||
}
|
||||
|
||||
func renderCode(fileName, language, code string, slowGuess bool) (output template.HTML, lexerName string) {
|
||||
// RenderCodeSlowGuess tries to get a lexer by file name and language first,
|
||||
// if not found, it will try to guess the lexer by code content, which is slow (more than several hundreds of milliseconds).
|
||||
func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML, lexer chroma.Lexer, lexerDisplayName string) {
|
||||
// diff view newline will be passed as empty, change to literal '\n' so it can be copied
|
||||
// preserve literal newline in blame view
|
||||
if code == "" || code == "\n" {
|
||||
return "\n", ""
|
||||
return "\n", nil, ""
|
||||
}
|
||||
|
||||
if len(code) > sizeLimit {
|
||||
return template.HTML(template.HTMLEscapeString(code)), ""
|
||||
return template.HTML(template.HTMLEscapeString(code)), nil, ""
|
||||
}
|
||||
|
||||
var codeForGuessLexer []byte
|
||||
if slowGuess {
|
||||
// it is slower to guess lexer by code content, so only do it when necessary
|
||||
codeForGuessLexer = util.UnsafeStringToBytes(code)
|
||||
}
|
||||
lexer := GetChromaLexerWithFallback(fileName, language, codeForGuessLexer)
|
||||
return RenderCodeByLexer(lexer, code), formatLexerName(lexer.Config().Name)
|
||||
}
|
||||
|
||||
func RenderCodeFast(fileName, language, code string) (output template.HTML, lexerName string) {
|
||||
return renderCode(fileName, language, code, false)
|
||||
}
|
||||
|
||||
func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML, lexerName string) {
|
||||
return renderCode(fileName, language, code, true)
|
||||
lexer = detectChromaLexerWithAnalyze(fileName, language, util.UnsafeStringToBytes(code)) // it is also slow
|
||||
return RenderCodeByLexer(lexer, code), lexer, formatLexerName(lexer.Config().Name)
|
||||
}
|
||||
|
||||
// RenderCodeByLexer returns a HTML version of code string with chroma syntax highlighting classes
|
||||
@ -204,7 +136,7 @@ func RenderFullFile(fileName, language string, code []byte) ([]template.HTML, st
|
||||
html.PreventSurroundingPre(true),
|
||||
)
|
||||
|
||||
lexer := GetChromaLexerWithFallback(fileName, language, code)
|
||||
lexer := detectChromaLexerWithAnalyze(fileName, language, code)
|
||||
lexerName := formatLexerName(lexer.Config().Name)
|
||||
|
||||
iterator, err := lexer.Tokenise(nil, string(code))
|
||||
|
||||
@ -205,36 +205,3 @@ func TestUnsafeSplitHighlightedLines(t *testing.T) {
|
||||
assert.Equal(t, "<span>a</span>\n", string(ret[0]))
|
||||
assert.Equal(t, "<span>b\n</span>", string(ret[1]))
|
||||
}
|
||||
|
||||
func TestGetChromaLexer(t *testing.T) {
|
||||
globalVars().highlightMapping[".my-html"] = "HTML"
|
||||
t.Cleanup(func() { delete(globalVars().highlightMapping, ".my-html") })
|
||||
|
||||
cases := []struct {
|
||||
fileName string
|
||||
language string
|
||||
content string
|
||||
expected string
|
||||
}{
|
||||
{"test.py", "", "", "Python"},
|
||||
|
||||
{"any-file", "javascript", "", "JavaScript"},
|
||||
{"any-file", "", "/* vim: set filetype=python */", "Python"},
|
||||
{"any-file", "", "", "fallback"},
|
||||
|
||||
{"test.fs", "", "", "Forth"},
|
||||
{"test.fs", "F#", "", "FSharp"},
|
||||
{"test.fs", "", "let x = 1", "FSharp"},
|
||||
|
||||
{"test.c", "", "", "C"},
|
||||
{"test.C", "", "", "C++"},
|
||||
{"OLD-CODE.PAS", "", "", "ObjectPascal"},
|
||||
{"test.my-html", "", "", "HTML"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
lexer := GetChromaLexerWithFallback(c.fileName, c.language, []byte(c.content))
|
||||
if assert.NotNil(t, lexer, "case: %+v", c) {
|
||||
assert.Equal(t, c.expected, lexer.Config().Name, "case: %+v", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
279
modules/highlight/lexerdetect.go
Normal file
279
modules/highlight/lexerdetect.go
Normal file
@ -0,0 +1,279 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package highlight
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/analyze"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/go-enry/go-enry/v2"
|
||||
)
|
||||
|
||||
const mapKeyLowerPrefix = "lower/"
|
||||
|
||||
// chromaLexers is fully managed by us to do fast lookup for chroma lexers by file name or language name
|
||||
// Don't use lexers.Get because it is very slow in many cases (iterate all rules, filepath glob match, etc.)
|
||||
var chromaLexers = sync.OnceValue(func() (ret struct {
|
||||
conflictingExtLangMap map[string]string
|
||||
|
||||
lowerNameMap map[string]chroma.Lexer // lexer name (lang name) in lower-case
|
||||
fileBaseMap map[string]chroma.Lexer
|
||||
fileExtMap map[string]chroma.Lexer
|
||||
fileParts []struct {
|
||||
part string
|
||||
lexer chroma.Lexer
|
||||
}
|
||||
},
|
||||
) {
|
||||
ret.lowerNameMap = make(map[string]chroma.Lexer)
|
||||
ret.fileBaseMap = make(map[string]chroma.Lexer)
|
||||
ret.fileExtMap = make(map[string]chroma.Lexer)
|
||||
|
||||
// Chroma has overlaps in file extension for different languages,
|
||||
// When we need to do fast render, there is no way to detect the language by content,
|
||||
// So we can only choose some default languages for the overlapped file extensions.
|
||||
ret.conflictingExtLangMap = map[string]string{
|
||||
".as": "ActionScript 3", // ActionScript
|
||||
".asm": "NASM", // TASM, NASM, RGBDS Assembly, Z80 Assembly
|
||||
".ASM": "NASM",
|
||||
".bas": "VB.net", // QBasic
|
||||
".bf": "Beef", // Brainfuck
|
||||
".fs": "FSharp", // Forth
|
||||
".gd": "GDScript", // GDScript3
|
||||
".h": "C", // Objective-C
|
||||
".hcl": "Terraform", // HCL
|
||||
".hh": "C++", // HolyC
|
||||
".inc": "PHP", // ObjectPascal, POVRay, SourcePawn, PHTML
|
||||
".m": "Objective-C", // Matlab, Mathematica, Mason
|
||||
".mc": "Mason", // MonkeyC
|
||||
".network": "SYSTEMD", // INI
|
||||
".php": "PHP", // PHTML
|
||||
".php3": "PHP", // PHTML
|
||||
".php4": "PHP", // PHTML
|
||||
".php5": "PHP", // PHTML
|
||||
".pl": "Perl", // Prolog, Raku
|
||||
".pm": "Perl", // Promela, Raku
|
||||
".pp": "ObjectPascal", // Puppet
|
||||
".s": "ArmAsm", // GAS
|
||||
".S": "ArmAsm", // R, GAS
|
||||
".service": "SYSTEMD", // INI
|
||||
".socket": "SYSTEMD", // INI
|
||||
".sql": "SQL", // MySQL
|
||||
".t": "Perl", // Raku
|
||||
".ts": "TypeScript", // TypoScript
|
||||
".v": "V", // verilog
|
||||
".xslt": "HTML", // XML
|
||||
}
|
||||
|
||||
isPlainPattern := func(key string) bool {
|
||||
return !strings.ContainsAny(key, "*?[]") // only support simple patterns
|
||||
}
|
||||
|
||||
setMapWithLowerKey := func(m map[string]chroma.Lexer, key string, lexer chroma.Lexer) {
|
||||
if _, conflict := m[key]; conflict {
|
||||
panic("duplicate key in lexer map: " + key + ", need to add it to conflictingExtLangMap")
|
||||
}
|
||||
m[key] = lexer
|
||||
m[mapKeyLowerPrefix+strings.ToLower(key)] = lexer
|
||||
}
|
||||
|
||||
processFileName := func(fileName string, lexer chroma.Lexer) bool {
|
||||
if isPlainPattern(fileName) {
|
||||
// full base name match
|
||||
setMapWithLowerKey(ret.fileBaseMap, fileName, lexer)
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(fileName, "*") {
|
||||
// ext name match: "*.js"
|
||||
fileExt := strings.Trim(fileName, "*")
|
||||
if isPlainPattern(fileExt) {
|
||||
presetName := ret.conflictingExtLangMap[fileExt]
|
||||
if presetName == "" || lexer.Config().Name == presetName {
|
||||
setMapWithLowerKey(ret.fileExtMap, fileExt, lexer)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(fileName, "*") {
|
||||
// part match: "*.env.*"
|
||||
filePart := strings.Trim(fileName, "*")
|
||||
if isPlainPattern(filePart) {
|
||||
ret.fileParts = append(ret.fileParts, struct {
|
||||
part string
|
||||
lexer chroma.Lexer
|
||||
}{
|
||||
part: filePart,
|
||||
lexer: lexer,
|
||||
})
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
expandGlobPatterns := func(patterns []string) []string {
|
||||
// expand patterns like "file.[ch]" to "file.c" and "file.h", only one pair of "[]" is supported, enough for current Chroma lexers
|
||||
for idx, s := range patterns {
|
||||
idx1 := strings.IndexByte(s, '[')
|
||||
idx2 := strings.IndexByte(s, ']')
|
||||
if idx1 != -1 && idx2 != -1 && idx2 > idx1+1 {
|
||||
left, mid, right := s[:idx1], s[idx1+1:idx2], s[idx2+1:]
|
||||
patterns[idx] = left + mid[0:1] + right
|
||||
for i := 1; i < len(mid); i++ {
|
||||
patterns = append(patterns, left+mid[i:i+1]+right)
|
||||
}
|
||||
}
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
// add lexers to our map, for fast lookup
|
||||
for _, lexer := range lexers.GlobalLexerRegistry.Lexers {
|
||||
cfg := lexer.Config()
|
||||
ret.lowerNameMap[strings.ToLower(lexer.Config().Name)] = lexer
|
||||
for _, alias := range cfg.Aliases {
|
||||
ret.lowerNameMap[strings.ToLower(alias)] = lexer
|
||||
}
|
||||
for _, s := range expandGlobPatterns(cfg.Filenames) {
|
||||
if !processFileName(s, lexer) {
|
||||
panic("unsupported file name pattern in lexer: " + s)
|
||||
}
|
||||
}
|
||||
for _, s := range expandGlobPatterns(cfg.AliasFilenames) {
|
||||
if !processFileName(s, lexer) {
|
||||
panic("unsupported alias file name pattern in lexer: " + s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// final check: make sure the default ext-lang mapping is correct, nothing is missing
|
||||
for ext, lexerName := range ret.conflictingExtLangMap {
|
||||
if lexer, ok := ret.fileExtMap[ext]; !ok || lexer.Config().Name != lexerName {
|
||||
panic("missing default ext-lang mapping for: " + ext)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
})
|
||||
|
||||
func normalizeFileNameLang(fileName, fileLang string) (string, string) {
|
||||
fileName = path.Base(fileName)
|
||||
fileLang, _, _ = strings.Cut(fileLang, "?") // maybe, the value from gitattributes might contain `?` parameters?
|
||||
ext := path.Ext(fileName)
|
||||
// the "lang" might come from enry or gitattributes, it has different naming for some languages
|
||||
switch fileLang {
|
||||
case "F#":
|
||||
fileLang = "FSharp"
|
||||
case "Pascal":
|
||||
fileLang = "ObjectPascal"
|
||||
case "C":
|
||||
if ext == ".C" || ext == ".H" {
|
||||
fileLang = "C++"
|
||||
}
|
||||
}
|
||||
return fileName, fileLang
|
||||
}
|
||||
|
||||
func DetectChromaLexerByFileName(fileName, fileLang string) chroma.Lexer {
|
||||
lexer, _ := detectChromaLexerByFileName(fileName, fileLang)
|
||||
return lexer
|
||||
}
|
||||
|
||||
func detectChromaLexerByFileName(fileName, fileLang string) (_ chroma.Lexer, byLang bool) {
|
||||
fileName, fileLang = normalizeFileNameLang(fileName, fileLang)
|
||||
fileExt := path.Ext(fileName)
|
||||
|
||||
// apply custom mapping for file extension, highest priority, for example:
|
||||
// * ".my-js" -> ".js"
|
||||
// * ".my-html" -> "HTML"
|
||||
if fileExt != "" {
|
||||
if val, ok := globalVars().highlightMapping[fileExt]; ok {
|
||||
if strings.HasPrefix(val, ".") {
|
||||
fileName = "dummy" + val
|
||||
fileLang = ""
|
||||
} else {
|
||||
fileLang = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try to use language for lexer name
|
||||
if fileLang != "" {
|
||||
lexer := chromaLexers().lowerNameMap[strings.ToLower(fileLang)]
|
||||
if lexer != nil {
|
||||
return lexer, true
|
||||
}
|
||||
}
|
||||
|
||||
if fileName == "" {
|
||||
return lexers.Fallback, false
|
||||
}
|
||||
|
||||
// try base name
|
||||
{
|
||||
baseName := path.Base(fileName)
|
||||
if lexer, ok := chromaLexers().fileBaseMap[baseName]; ok {
|
||||
return lexer, false
|
||||
} else if lexer, ok = chromaLexers().fileBaseMap[mapKeyLowerPrefix+strings.ToLower(baseName)]; ok {
|
||||
return lexer, false
|
||||
}
|
||||
}
|
||||
|
||||
if fileExt == "" {
|
||||
return lexers.Fallback, false
|
||||
}
|
||||
|
||||
// try ext name
|
||||
{
|
||||
if lexer, ok := chromaLexers().fileExtMap[fileExt]; ok {
|
||||
return lexer, false
|
||||
} else if lexer, ok = chromaLexers().fileExtMap[mapKeyLowerPrefix+strings.ToLower(fileExt)]; ok {
|
||||
return lexer, false
|
||||
}
|
||||
}
|
||||
|
||||
// try file part match, for example: ".env.local" for "*.env.*"
|
||||
// it assumes that there must be a dot in filename (fileExt isn't empty)
|
||||
for _, item := range chromaLexers().fileParts {
|
||||
if strings.Contains(fileName, item.part) {
|
||||
return item.lexer, false
|
||||
}
|
||||
}
|
||||
return lexers.Fallback, false
|
||||
}
|
||||
|
||||
// detectChromaLexerWithAnalyze returns a chroma lexer by given file name, language and code content. All parameters can be optional.
|
||||
// When code content is provided, it will be slow if no lexer is found by file name or language.
|
||||
// If no lexer is found, it will return the fallback lexer.
|
||||
func detectChromaLexerWithAnalyze(fileName, lang string, code []byte) chroma.Lexer {
|
||||
lexer, byLang := detectChromaLexerByFileName(fileName, lang)
|
||||
|
||||
// if lang is provided, and it matches a lexer, use it directly
|
||||
if byLang {
|
||||
return lexer
|
||||
}
|
||||
|
||||
// if a lexer is detected and there is no conflict for the file extension, use it directly
|
||||
fileExt := path.Ext(fileName)
|
||||
_, hasConflicts := chromaLexers().conflictingExtLangMap[fileExt]
|
||||
if !hasConflicts && lexer != lexers.Fallback {
|
||||
return lexer
|
||||
}
|
||||
|
||||
// try to detect language by content, for best guessing for the language
|
||||
// when using "code" to detect, analyze.GetCodeLanguage is slow, it iterates many rules to detect language from content
|
||||
analyzedLanguage := analyze.GetCodeLanguage(fileName, code)
|
||||
lexer = DetectChromaLexerByFileName(fileName, analyzedLanguage)
|
||||
if lexer == lexers.Fallback {
|
||||
if analyzedLanguage != enry.OtherLanguage {
|
||||
log.Warn("No chroma lexer found for enry detected language: %s (file: %s), need to fix the language mapping between enry and chroma.", analyzedLanguage, fileName)
|
||||
}
|
||||
}
|
||||
return lexer
|
||||
}
|
||||
90
modules/highlight/lexerdetect_test.go
Normal file
90
modules/highlight/lexerdetect_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package highlight
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkDetectChromaLexerByFileName(b *testing.B) {
|
||||
for b.Loop() {
|
||||
// BenchmarkDetectChromaLexerByFileName-12 18214717 61.35 ns/op
|
||||
DetectChromaLexerByFileName("a.sql", "")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDetectChromaLexerWithAnalyze(b *testing.B) {
|
||||
b.StopTimer()
|
||||
code := []byte(strings.Repeat("SELECT * FROM table;\n", 1000))
|
||||
b.StartTimer()
|
||||
for b.Loop() {
|
||||
// BenchmarkRenderCodeSlowGuess-12 87946 13310 ns/op
|
||||
detectChromaLexerWithAnalyze("a", "", code)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChromaAnalyze(b *testing.B) {
|
||||
b.StopTimer()
|
||||
code := strings.Repeat("SELECT * FROM table;\n", 1000)
|
||||
b.StartTimer()
|
||||
for b.Loop() {
|
||||
// comparing to detectChromaLexerWithAnalyze (go-enry), "chroma/lexers.Analyse" is very slow
|
||||
// BenchmarkChromaAnalyze-12 519 2247104 ns/op
|
||||
lexers.Analyse(code)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRenderCodeByLexer(b *testing.B) {
|
||||
b.StopTimer()
|
||||
code := strings.Repeat("SELECT * FROM table;\n", 1000)
|
||||
lexer := DetectChromaLexerByFileName("a.sql", "")
|
||||
b.StartTimer()
|
||||
for b.Loop() {
|
||||
// Really slow .......
|
||||
// BenchmarkRenderCodeByLexer-12 22 47159038 ns/op
|
||||
RenderCodeByLexer(lexer, code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectChromaLexer(t *testing.T) {
|
||||
globalVars().highlightMapping[".my-html"] = "HTML"
|
||||
t.Cleanup(func() { delete(globalVars().highlightMapping, ".my-html") })
|
||||
|
||||
cases := []struct {
|
||||
fileName string
|
||||
language string
|
||||
content string
|
||||
expected string
|
||||
}{
|
||||
{"test.py", "", "", "Python"},
|
||||
|
||||
{"any-file", "javascript", "", "JavaScript"},
|
||||
{"any-file", "", "/* vim: set filetype=python */", "Python"},
|
||||
{"any-file", "", "", "fallback"},
|
||||
|
||||
{"test.fs", "", "", "FSharp"},
|
||||
{"test.fs", "F#", "", "FSharp"},
|
||||
{"test.fs", "", "let x = 1", "FSharp"},
|
||||
|
||||
{"test.c", "", "", "C"},
|
||||
{"test.C", "", "", "C++"},
|
||||
{"OLD-CODE.PAS", "", "", "ObjectPascal"},
|
||||
{"test.my-html", "", "", "HTML"},
|
||||
|
||||
{"a.php", "", "", "PHP"},
|
||||
{"a.sql", "", "", "SQL"},
|
||||
{"dhcpd.conf", "", "", "ISCdhcpd"},
|
||||
{".env.my-production", "", "", "Bash"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
lexer := detectChromaLexerWithAnalyze(c.fileName, c.language, []byte(c.content))
|
||||
if assert.NotNil(t, lexer, "case: %+v", c) {
|
||||
assert.Equal(t, c.expected, lexer.Config().Name, "case: %+v", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -72,7 +72,8 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error {
|
||||
|
||||
func HighlightSearchResultCode(filename, language string, lineNums []int, code string) []*ResultLine {
|
||||
// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
|
||||
hl, _ := highlight.RenderCodeFast(filename, language, code)
|
||||
lexer := highlight.DetectChromaLexerByFileName(filename, language)
|
||||
hl := highlight.RenderCodeByLexer(lexer, code)
|
||||
highlightedLines := strings.Split(string(hl), "\n")
|
||||
|
||||
// The lineNums outputted by render might not match the original lineNums, because "highlight" removes the last `\n`
|
||||
|
||||
@ -56,7 +56,7 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
}
|
||||
}()
|
||||
|
||||
lexer := highlight.GetChromaLexerWithFallback("", lang, nil) // don't use content to detect, it is too slow
|
||||
lexer := highlight.DetectChromaLexerByFileName("", lang) // don't use content to detect, it is too slow
|
||||
lexer = chroma.Coalesce(lexer)
|
||||
|
||||
sb := &strings.Builder{}
|
||||
|
||||
@ -758,6 +758,7 @@
|
||||
"settings.add_email": "Add Email Address",
|
||||
"settings.add_openid": "Add OpenID URI",
|
||||
"settings.add_email_confirmation_sent": "A confirmation email has been sent to \"%s\". Please check your inbox within the next %s to confirm your email address.",
|
||||
"settings.email_primary_not_found": "The selected email address could not be found.",
|
||||
"settings.add_email_success": "The new email address has been added.",
|
||||
"settings.email_preference_set_success": "Email preference has been set successfully.",
|
||||
"settings.add_openid_success": "The new OpenID address has been added.",
|
||||
|
||||
@ -13,7 +13,7 @@ import (
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
attachment_model "code.gitea.io/gitea/models/repo"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
@ -25,6 +25,8 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
project_service "code.gitea.io/gitea/services/projects"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -332,12 +334,26 @@ func ViewProject(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
assigneeID := ctx.FormString("assignee")
|
||||
milestoneID := ctx.FormInt64("milestone")
|
||||
|
||||
// Prepare milestone IDs for filtering
|
||||
var milestoneIDs []int64
|
||||
if milestoneID > 0 {
|
||||
milestoneIDs = []int64{milestoneID}
|
||||
} else if milestoneID == db.NoConditionID {
|
||||
milestoneIDs = []int64{db.NoConditionID}
|
||||
}
|
||||
|
||||
opts := issues_model.IssuesOptions{
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
Owner: project.Owner,
|
||||
Doer: ctx.Doer,
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
MilestoneIDs: milestoneIDs,
|
||||
Owner: project.Owner,
|
||||
}
|
||||
if ctx.Doer != nil {
|
||||
opts.Doer = ctx.Doer
|
||||
} else {
|
||||
opts.AllPublic = true
|
||||
}
|
||||
|
||||
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts)
|
||||
@ -350,10 +366,10 @@ func ViewProject(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if project.CardType != project_model.CardTypeTextOnly {
|
||||
issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
|
||||
issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
|
||||
for _, issuesList := range issuesMap {
|
||||
for _, issue := range issuesList {
|
||||
if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
|
||||
if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
|
||||
issuesAttachmentMap[issue.ID] = issueAttachment
|
||||
}
|
||||
}
|
||||
@ -411,6 +427,42 @@ func ViewProject(ctx *context.Context) {
|
||||
ctx.Data["Labels"] = labels
|
||||
ctx.Data["NumLabels"] = len(labels)
|
||||
|
||||
// Get milestones for filtering
|
||||
// For organization projects, we need to get milestones from all repos the user has access to
|
||||
var milestones issues_model.MilestoneList
|
||||
if project.RepoID > 0 {
|
||||
// Repo-specific project
|
||||
milestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
RepoID: project.RepoID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoMilestones", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Organization-wide project - get milestones from all organization repos
|
||||
// but only from repositories the current user can access.
|
||||
// Use RepoCond with a subquery to avoid materializing all repo IDs in memory
|
||||
// which can hit SQL parameter limits for orgs with many repos.
|
||||
accessCond := repo_model.AccessibleRepositoryCondition(ctx.Doer, unit.TypeIssues)
|
||||
repoCond := builder.And(
|
||||
builder.Eq{"owner_id": project.OwnerID},
|
||||
accessCond,
|
||||
)
|
||||
milestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
RepoCond: repoCond,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOrgMilestones", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
openMilestones, closedMilestones := milestones.SplitByOpenClosed()
|
||||
ctx.Data["OpenMilestones"] = openMilestones
|
||||
ctx.Data["ClosedMilestones"] = closedMilestones
|
||||
ctx.Data["MilestoneID"] = milestoneID
|
||||
|
||||
// Get assignees.
|
||||
assigneeUsers, err := org_model.GetOrgAssignees(ctx, project.OwnerID)
|
||||
if err != nil {
|
||||
|
||||
@ -267,7 +267,7 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa
|
||||
|
||||
bufContent := buf.Bytes()
|
||||
bufContent = charset.ToUTF8(bufContent, charset.ConvertOpts{})
|
||||
highlighted, lexerName := highlight.RenderCodeSlowGuess(path.Base(ctx.Repo.TreePath), language, util.UnsafeBytesToString(bufContent))
|
||||
highlighted, _, lexerDisplayName := highlight.RenderCodeSlowGuess(path.Base(ctx.Repo.TreePath), language, util.UnsafeBytesToString(bufContent))
|
||||
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlighted)
|
||||
for i, br := range rows {
|
||||
var line template.HTML
|
||||
@ -280,5 +280,5 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa
|
||||
|
||||
ctx.Data["EscapeStatus"] = escapeStatus
|
||||
ctx.Data["BlameRows"] = rows
|
||||
ctx.Data["LexerName"] = lexerName
|
||||
ctx.Data["LexerName"] = lexerDisplayName
|
||||
}
|
||||
|
||||
@ -462,14 +462,7 @@ func renderMilestones(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{}
|
||||
for _, milestone := range milestones {
|
||||
if milestone.IsClosed {
|
||||
closedMilestones = append(closedMilestones, milestone)
|
||||
} else {
|
||||
openMilestones = append(openMilestones, milestone)
|
||||
}
|
||||
}
|
||||
openMilestones, closedMilestones := issues_model.MilestoneList(milestones).SplitByOpenClosed()
|
||||
ctx.Data["OpenMilestones"] = openMilestones
|
||||
ctx.Data["ClosedMilestones"] = closedMilestones
|
||||
}
|
||||
|
||||
@ -311,13 +311,25 @@ func ViewProject(ctx *context.Context) {
|
||||
}
|
||||
|
||||
preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
assigneeID := ctx.FormString("assignee")
|
||||
milestoneID := ctx.FormInt64("milestone")
|
||||
|
||||
var milestoneIDs []int64
|
||||
if milestoneID > 0 {
|
||||
milestoneIDs = []int64{milestoneID}
|
||||
} else if milestoneID == db.NoConditionID {
|
||||
milestoneIDs = []int64{db.NoConditionID}
|
||||
}
|
||||
|
||||
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
MilestoneIDs: milestoneIDs,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||
@ -408,6 +420,12 @@ func ViewProject(ctx *context.Context) {
|
||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||
ctx.Data["AssigneeID"] = assigneeID
|
||||
|
||||
renderMilestones(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.Data["MilestoneID"] = milestoneID
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
|
||||
project.RenderedContent, err = markdown.RenderString(rctx, project.Description)
|
||||
if err != nil {
|
||||
|
||||
@ -113,7 +113,12 @@ func EmailPost(ctx *context.Context) {
|
||||
|
||||
// Make email address primary.
|
||||
if ctx.FormString("_method") == "PRIMARY" {
|
||||
if err := user_model.MakeActiveEmailPrimary(ctx, ctx.FormInt64("id")); err != nil {
|
||||
if err := user_model.MakeActiveEmailPrimary(ctx, ctx.Doer.ID, ctx.FormInt64("id")); err != nil {
|
||||
if user_model.IsErrEmailAddressNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("settings.email_primary_not_found"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
|
||||
return
|
||||
}
|
||||
ctx.ServerError("MakeEmailPrimary", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
stdcharset "golang.org/x/net/html/charset"
|
||||
"golang.org/x/text/encoding"
|
||||
@ -306,6 +307,7 @@ type DiffSection struct {
|
||||
language *diffVarMutable[string]
|
||||
highlightedLeftLines *diffVarMutable[map[int]template.HTML]
|
||||
highlightedRightLines *diffVarMutable[map[int]template.HTML]
|
||||
highlightLexer *diffVarMutable[chroma.Lexer]
|
||||
|
||||
FileName string
|
||||
Lines []*DiffLine
|
||||
@ -347,8 +349,10 @@ func (diffSection *DiffSection) getLineContentForRender(lineIdx int, diffLine *D
|
||||
if setting.Git.DisableDiffHighlight {
|
||||
return template.HTML(html.EscapeString(diffLine.Content[1:]))
|
||||
}
|
||||
h, _ = highlight.RenderCodeFast(diffSection.FileName, fileLanguage, diffLine.Content[1:])
|
||||
return h
|
||||
if diffSection.highlightLexer.value == nil {
|
||||
diffSection.highlightLexer.value = highlight.DetectChromaLexerByFileName(diffSection.FileName, fileLanguage)
|
||||
}
|
||||
return highlight.RenderCodeByLexer(diffSection.highlightLexer.value, diffLine.Content[1:])
|
||||
}
|
||||
|
||||
func (diffSection *DiffSection) getDiffLineForRender(diffLineType DiffLineType, leftLine, rightLine *DiffLine, locale translation.Locale) DiffInline {
|
||||
@ -391,6 +395,12 @@ func (diffSection *DiffSection) getDiffLineForRender(diffLineType DiffLineType,
|
||||
|
||||
// GetComputedInlineDiffFor computes inline diff for the given line.
|
||||
func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, locale translation.Locale) DiffInline {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// the logic is too complex in this function, help to catch any panic because Golang template doesn't print the stack
|
||||
log.Error("panic in GetComputedInlineDiffFor: %v\nStack: %s", err, log.Stack(2))
|
||||
}
|
||||
}()
|
||||
// try to find equivalent diff line. ignore, otherwise
|
||||
switch diffLine.Type {
|
||||
case DiffLineSection:
|
||||
@ -452,6 +462,7 @@ type DiffFile struct {
|
||||
|
||||
// for render purpose only, will be filled by the extra loop in GitDiffForRender, the maps of lines are 0-based
|
||||
language diffVarMutable[string]
|
||||
highlightRender diffVarMutable[chroma.Lexer] // cache render (atm: lexer) for current file, only detect once for line-by-line mode
|
||||
highlightedLeftLines diffVarMutable[map[int]template.HTML]
|
||||
highlightedRightLines diffVarMutable[map[int]template.HTML]
|
||||
}
|
||||
@ -932,6 +943,7 @@ func skipToNextDiffHead(input *bufio.Reader) (line string, err error) {
|
||||
func newDiffSectionForDiffFile(curFile *DiffFile) *DiffSection {
|
||||
return &DiffSection{
|
||||
language: &curFile.language,
|
||||
highlightLexer: &curFile.highlightRender,
|
||||
highlightedLeftLines: &curFile.highlightedLeftLines,
|
||||
highlightedRightLines: &curFile.highlightedRightLines,
|
||||
}
|
||||
@ -1395,7 +1407,8 @@ func highlightCodeLines(name, lang string, sections []*DiffSection, isLeft bool,
|
||||
}
|
||||
|
||||
content := util.UnsafeBytesToString(charset.ToUTF8(rawContent, charset.ConvertOpts{}))
|
||||
highlightedNewContent, _ := highlight.RenderCodeFast(name, lang, content)
|
||||
lexer := highlight.DetectChromaLexerByFileName(name, lang)
|
||||
highlightedNewContent := highlight.RenderCodeByLexer(lexer, content)
|
||||
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlightedNewContent)
|
||||
lines := make(map[int]template.HTML, len(unsafeLines))
|
||||
// only save the highlighted lines we need, but not the whole file, to save memory
|
||||
|
||||
@ -11,6 +11,8 @@ import (
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
)
|
||||
|
||||
type BlobExcerptOptions struct {
|
||||
@ -65,6 +67,7 @@ func BuildBlobExcerptDiffSection(filePath string, reader io.Reader, opts BlobExc
|
||||
chunkSize := BlobExcerptChunkSize
|
||||
section := &DiffSection{
|
||||
language: &diffVarMutable[string]{value: language},
|
||||
highlightLexer: &diffVarMutable[chroma.Lexer]{},
|
||||
highlightedLeftLines: &diffVarMutable[map[int]template.HTML]{},
|
||||
highlightedRightLines: &diffVarMutable[map[int]template.HTML]{},
|
||||
FileName: filePath,
|
||||
|
||||
@ -76,8 +76,8 @@ func TestDiffWithHighlight(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ComplexDiff1", func(t *testing.T) {
|
||||
oldCode, _ := highlight.RenderCodeFast("a.go", "Go", `xxx || yyy`)
|
||||
newCode, _ := highlight.RenderCodeFast("a.go", "Go", `bot&xxx || bot&yyy`)
|
||||
oldCode, _, _ := highlight.RenderCodeSlowGuess("a.go", "Go", `xxx || yyy`)
|
||||
newCode, _, _ := highlight.RenderCodeSlowGuess("a.go", "Go", `bot&xxx || bot&yyy`)
|
||||
hcd := newHighlightCodeDiff()
|
||||
out := hcd.diffLineWithHighlight(DiffLineAdd, oldCode, newCode)
|
||||
assert.Equal(t, strings.ReplaceAll(`
|
||||
|
||||
@ -123,10 +123,8 @@ func GarbageCollectLFSMetaObjectsForRepo(ctx context.Context, repo *repo_model.R
|
||||
//
|
||||
// It is likely that a week is potentially excessive but it should definitely be enough that any
|
||||
// unassociated LFS object is genuinely unassociated.
|
||||
OlderThan: timeutil.TimeStamp(opts.OlderThan.Unix()),
|
||||
UpdatedLessRecentlyThan: timeutil.TimeStamp(opts.UpdatedLessRecentlyThan.Unix()),
|
||||
OrderByUpdated: true,
|
||||
LoopFunctionAlwaysUpdates: true,
|
||||
OlderThan: timeutil.TimeStamp(opts.OlderThan.Unix()),
|
||||
UpdatedLessRecentlyThan: timeutil.TimeStamp(opts.UpdatedLessRecentlyThan.Unix()),
|
||||
})
|
||||
|
||||
if err == errStop {
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -22,7 +23,8 @@ import (
|
||||
func TestGarbageCollectLFSMetaObjects(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
setting.LFS.StartServer = true
|
||||
defer test.MockVariableValue(&setting.LFS.StartServer, true)()
|
||||
|
||||
err := storage.Init()
|
||||
assert.NoError(t, err)
|
||||
|
||||
@ -46,6 +48,32 @@ func TestGarbageCollectLFSMetaObjects(t *testing.T) {
|
||||
assert.ErrorIs(t, err, git_model.ErrLFSObjectNotExist)
|
||||
}
|
||||
|
||||
func TestGarbageCollectLFSMetaObjectsForRepoAutoFix(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
defer test.MockVariableValue(&setting.LFS.StartServer, true)()
|
||||
|
||||
err := storage.Init()
|
||||
assert.NoError(t, err)
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
// add lfs object
|
||||
lfsContent := []byte("gitea2")
|
||||
lfsOid := storeObjectInRepo(t, repo.ID, &lfsContent)
|
||||
|
||||
err = repo_service.GarbageCollectLFSMetaObjectsForRepo(t.Context(), repo, repo_service.GarbageCollectLFSMetaObjectsOptions{
|
||||
LogDetail: func(string, ...any) {},
|
||||
AutoFix: true,
|
||||
OlderThan: time.Now().Add(24 * time.Hour * 7),
|
||||
UpdatedLessRecentlyThan: time.Now().Add(24 * time.Hour * 3),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = git_model.GetLFSMetaObjectByOid(t.Context(), repo.ID, lfsOid)
|
||||
assert.ErrorIs(t, err, git_model.ErrLFSObjectNotExist)
|
||||
}
|
||||
|
||||
func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string {
|
||||
pointer, err := lfs.GeneratePointer(bytes.NewReader(*content))
|
||||
assert.NoError(t, err)
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<h2>{{.Project.Title}}</h2>
|
||||
<div class="tw-flex-1"></div>
|
||||
<div class="ui secondary menu tw-m-0">
|
||||
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
|
||||
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "milestone" $.MilestoneID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
|
||||
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
|
||||
{{template "repo/issue/filter_item_user_assign" dict
|
||||
"QueryParamKey" "assignee"
|
||||
@ -16,6 +16,12 @@
|
||||
"TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assignee_no_assignee")
|
||||
"TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
|
||||
}}
|
||||
{{template "repo/issue/filter_item_milestone" dict
|
||||
"QueryLink" $queryLink
|
||||
"MilestoneID" $.MilestoneID
|
||||
"OpenMilestones" .OpenMilestones
|
||||
"ClosedMilestones" .ClosedMilestones
|
||||
}}
|
||||
</div>
|
||||
{{if $canWriteProject}}
|
||||
<div class="ui compact mini menu">
|
||||
|
||||
42
templates/repo/issue/filter_item_milestone.tmpl
Normal file
42
templates/repo/issue/filter_item_milestone.tmpl
Normal file
@ -0,0 +1,42 @@
|
||||
{{/* Milestone filter dropdown partial
|
||||
* QueryLink: the base query link for building filter URLs
|
||||
* MilestoneID: the currently selected milestone ID (0=all, -1=none, >0=specific)
|
||||
* OpenMilestones: list of open milestones
|
||||
* ClosedMilestones: list of closed milestones
|
||||
*/}}
|
||||
{{$queryLink := .QueryLink}}
|
||||
<div class="item ui dropdown jump {{if not (or .OpenMilestones .ClosedMilestones)}}disabled{{end}}">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_milestone"}}
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}">
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<a class="{{if not .MilestoneID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
|
||||
<a class="{{if .MilestoneID}}{{if eq .MilestoneID -1}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" -1}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a>
|
||||
{{if .OpenMilestones}}
|
||||
<div class="divider"></div>
|
||||
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div>
|
||||
{{range .OpenMilestones}}
|
||||
<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" .ID}}">
|
||||
{{svg "octicon-milestone" 16 "mr-2"}}
|
||||
{{.Name}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .ClosedMilestones}}
|
||||
<div class="divider"></div>
|
||||
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div>
|
||||
{{range .ClosedMilestones}}
|
||||
<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" .ID}}">
|
||||
{{svg "octicon-milestone" 16 "mr-2"}}
|
||||
{{.Name}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
@ -3,42 +3,12 @@
|
||||
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
|
||||
|
||||
{{if not .Milestone}}
|
||||
<!-- Milestone -->
|
||||
<div class="item ui dropdown jump {{if not (or .OpenMilestones .ClosedMilestones)}}disabled{{end}}">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_milestone"}}
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}">
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
|
||||
<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" -1}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a>
|
||||
{{if .OpenMilestones}}
|
||||
<div class="divider"></div>
|
||||
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div>
|
||||
{{range .OpenMilestones}}
|
||||
<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" .ID}}">
|
||||
{{svg "octicon-milestone" 16 "mr-2"}}
|
||||
{{.Name}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .ClosedMilestones}}
|
||||
<div class="divider"></div>
|
||||
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div>
|
||||
{{range .ClosedMilestones}}
|
||||
<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" .ID}}">
|
||||
{{svg "octicon-milestone" 16 "mr-2"}}
|
||||
{{.Name}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "repo/issue/filter_item_milestone" dict
|
||||
"QueryLink" $queryLink
|
||||
"MilestoneID" $.MilestoneID
|
||||
"OpenMilestones" .OpenMilestones
|
||||
"ClosedMilestones" .ClosedMilestones
|
||||
}}
|
||||
{{end}}
|
||||
|
||||
<!-- Project -->
|
||||
|
||||
@ -6,15 +6,20 @@ package integration
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPrivateRepoProject(t *testing.T) {
|
||||
@ -83,3 +88,163 @@ func TestMoveRepoProjectColumns(t *testing.T) {
|
||||
|
||||
assert.NoError(t, project_model.DeleteProjectByID(t.Context(), project1.ID))
|
||||
}
|
||||
|
||||
// getProjectIssueIDs returns the set of issue IDs rendered as cards on the project board page.
|
||||
func getProjectIssueIDs(t *testing.T, htmlDoc *HTMLDoc) map[int64]struct{} {
|
||||
t.Helper()
|
||||
ids := make(map[int64]struct{})
|
||||
htmlDoc.Find(".issue-card[data-issue]").Each(func(_ int, s *goquery.Selection) {
|
||||
idStr, exists := s.Attr("data-issue")
|
||||
require.True(t, exists)
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
require.NoError(t, err)
|
||||
ids[id] = struct{}{}
|
||||
})
|
||||
return ids
|
||||
}
|
||||
|
||||
func TestRepoProjectFilterByMilestone(t *testing.T) {
|
||||
// Project 1 is on repo 1 (user2/repo1) and has issues:
|
||||
// issue 1 (milestone_id=0), issue 2 (milestone_id=1), issue 3 (milestone_id=3), issue 5 (milestone_id=0)
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
sess := loginUser(t, "user2")
|
||||
|
||||
t.Run("NoFilter", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/repo1/projects/1")
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
issueIDs := getProjectIssueIDs(t, htmlDoc)
|
||||
// All issues should be visible
|
||||
assert.Contains(t, issueIDs, int64(1))
|
||||
assert.Contains(t, issueIDs, int64(2))
|
||||
assert.Contains(t, issueIDs, int64(3))
|
||||
assert.Contains(t, issueIDs, int64(5))
|
||||
})
|
||||
|
||||
t.Run("FilterByMilestone", func(t *testing.T) {
|
||||
// milestone_id=1 is "milestone1" (open), only issue 2 has it
|
||||
req := NewRequest(t, "GET", "/user2/repo1/projects/1?milestone=1")
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
issueIDs := getProjectIssueIDs(t, htmlDoc)
|
||||
assert.Contains(t, issueIDs, int64(2))
|
||||
assert.NotContains(t, issueIDs, int64(1))
|
||||
assert.NotContains(t, issueIDs, int64(3))
|
||||
assert.NotContains(t, issueIDs, int64(5))
|
||||
})
|
||||
|
||||
t.Run("FilterByNoMilestone", func(t *testing.T) {
|
||||
// milestone=-1 means "no milestone", issues 1 and 5 have no milestone
|
||||
req := NewRequest(t, "GET", "/user2/repo1/projects/1?milestone=-1")
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
issueIDs := getProjectIssueIDs(t, htmlDoc)
|
||||
assert.Contains(t, issueIDs, int64(1))
|
||||
assert.Contains(t, issueIDs, int64(5))
|
||||
assert.NotContains(t, issueIDs, int64(2))
|
||||
assert.NotContains(t, issueIDs, int64(3))
|
||||
})
|
||||
|
||||
t.Run("FilterByClosedMilestone", func(t *testing.T) {
|
||||
// milestone_id=3 is "milestone3" (closed), only issue 3 has it
|
||||
req := NewRequest(t, "GET", "/user2/repo1/projects/1?milestone=3")
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
issueIDs := getProjectIssueIDs(t, htmlDoc)
|
||||
assert.Contains(t, issueIDs, int64(3))
|
||||
assert.NotContains(t, issueIDs, int64(1))
|
||||
assert.NotContains(t, issueIDs, int64(2))
|
||||
assert.NotContains(t, issueIDs, int64(5))
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrgProjectFilterByMilestone(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// org3 owns repo32 (public) which has issues 16 and 17
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32})
|
||||
issue16 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 16})
|
||||
issue17 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 17})
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
// Create a milestone on repo32 and assign it to issue16
|
||||
milestone := &issues_model.Milestone{
|
||||
RepoID: repo.ID,
|
||||
Name: "org-test-milestone",
|
||||
}
|
||||
require.NoError(t, issues_model.NewMilestone(t.Context(), milestone))
|
||||
|
||||
issue16.MilestoneID = milestone.ID
|
||||
require.NoError(t, issues_model.UpdateIssueCols(t.Context(), issue16, "milestone_id"))
|
||||
|
||||
// Create an org-level project
|
||||
project := project_model.Project{
|
||||
Title: "org milestone filter test",
|
||||
OwnerID: org.ID,
|
||||
Type: project_model.TypeOrganization,
|
||||
TemplateType: project_model.TemplateTypeBasicKanban,
|
||||
}
|
||||
require.NoError(t, project_model.NewProject(t.Context(), &project))
|
||||
|
||||
// Get the default column
|
||||
columns, err := project.GetColumns(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, columns)
|
||||
defaultColumnID := columns[0].ID
|
||||
|
||||
// Add issues to the project
|
||||
require.NoError(t, issues_model.IssueAssignOrRemoveProject(t.Context(), issue16, user1, project.ID, defaultColumnID))
|
||||
require.NoError(t, issues_model.IssueAssignOrRemoveProject(t.Context(), issue17, user1, project.ID, defaultColumnID))
|
||||
|
||||
sess := loginUser(t, "user1")
|
||||
projectURL := fmt.Sprintf("/org3/-/projects/%d", project.ID)
|
||||
|
||||
t.Run("NoFilter", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", projectURL)
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
issueIDs := getProjectIssueIDs(t, htmlDoc)
|
||||
assert.Contains(t, issueIDs, issue16.ID)
|
||||
assert.Contains(t, issueIDs, issue17.ID)
|
||||
})
|
||||
|
||||
t.Run("FilterByMilestone", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s?milestone=%d", projectURL, milestone.ID))
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
issueIDs := getProjectIssueIDs(t, htmlDoc)
|
||||
assert.Contains(t, issueIDs, issue16.ID)
|
||||
assert.NotContains(t, issueIDs, issue17.ID)
|
||||
})
|
||||
|
||||
t.Run("FilterByNoMilestone", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", projectURL+"?milestone=-1")
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
issueIDs := getProjectIssueIDs(t, htmlDoc)
|
||||
assert.Contains(t, issueIDs, issue17.ID)
|
||||
assert.NotContains(t, issueIDs, issue16.ID)
|
||||
})
|
||||
|
||||
t.Run("AnonymousAccess", func(t *testing.T) {
|
||||
// Anonymous users should be able to view org project boards for public orgs
|
||||
// and the milestone filter should work without exposing private repo data
|
||||
req := NewRequest(t, "GET", projectURL)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
issueIDs := getProjectIssueIDs(t, htmlDoc)
|
||||
// repo32 is public, so anonymous users should see its issues
|
||||
assert.Contains(t, issueIDs, issue16.ID)
|
||||
assert.Contains(t, issueIDs, issue17.ID)
|
||||
|
||||
// Milestone filtering should also work for anonymous users
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s?milestone=%d", projectURL, milestone.ID))
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
issueIDs = getProjectIssueIDs(t, htmlDoc)
|
||||
assert.Contains(t, issueIDs, issue16.ID)
|
||||
assert.NotContains(t, issueIDs, issue17.ID)
|
||||
})
|
||||
}
|
||||
|
||||
@ -158,6 +158,34 @@ func TestUserSettingsUpdateEmail(t *testing.T) {
|
||||
req := NewRequest(t, "POST", "/user/settings/account/email")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("primary email not found", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings/account/email", map[string]string{
|
||||
"_method": "PRIMARY",
|
||||
"id": "9999",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
assert.Equal(t, "/user/settings/account", resp.Header().Get("Location"))
|
||||
flashMsg := session.GetCookieFlashMessage()
|
||||
assert.Equal(t, "The selected email address could not be found.", flashMsg.ErrorMsg)
|
||||
})
|
||||
|
||||
t.Run("primary email not owned by user", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings/account/email", map[string]string{
|
||||
"_method": "PRIMARY",
|
||||
"id": "6",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
assert.Equal(t, "/user/settings/account", resp.Header().Get("Location"))
|
||||
flashMsg := session.GetCookieFlashMessage()
|
||||
assert.Equal(t, "The selected email address could not be found.", flashMsg.ErrorMsg)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsDeleteEmail(t *testing.T) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {showGlobalErrorMessage} from './bootstrap.ts';
|
||||
import {showGlobalErrorMessage, shouldIgnoreError} from './bootstrap.ts';
|
||||
|
||||
test('showGlobalErrorMessage', () => {
|
||||
document.body.innerHTML = '<div class="page-content"></div>';
|
||||
@ -10,3 +10,19 @@ test('showGlobalErrorMessage', () => {
|
||||
expect(document.body.innerHTML).toContain('>test msg 2<');
|
||||
expect(document.querySelectorAll('.js-global-error').length).toEqual(2);
|
||||
});
|
||||
|
||||
test('shouldIgnoreError', () => {
|
||||
for (const url of [
|
||||
'https://gitea.test/assets/js/monaco.b359ef7e.js',
|
||||
'https://gitea.test/assets/js/monaco-editor.4a969118.worker.js',
|
||||
'https://gitea.test/assets/js/vendors-node_modules_pnpm_monaco-editor_0_55_1_node_modules_monaco-editor_esm_vs_base_common_-e11c7c.966a028d.js',
|
||||
]) {
|
||||
const err = new Error('test');
|
||||
err.stack = `Error: test\n at ${url}:1:1`;
|
||||
expect(shouldIgnoreError(err)).toEqual(true);
|
||||
}
|
||||
|
||||
const otherError = new Error('test');
|
||||
otherError.stack = 'Error: test\n at https://gitea.test/assets/js/index.js:1:1';
|
||||
expect(shouldIgnoreError(otherError)).toEqual(false);
|
||||
});
|
||||
|
||||
@ -8,12 +8,15 @@ import {html} from './utils/html.ts';
|
||||
// This file must be imported before any lazy-loading is being attempted.
|
||||
window.__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
|
||||
|
||||
function shouldIgnoreError(err: Error) {
|
||||
const ignorePatterns = [
|
||||
'/assets/js/monaco.', // https://github.com/go-gitea/gitea/issues/30861 , https://github.com/microsoft/monaco-editor/issues/4496
|
||||
export function shouldIgnoreError(err: Error) {
|
||||
const ignorePatterns: Array<RegExp> = [
|
||||
// https://github.com/go-gitea/gitea/issues/30861
|
||||
// https://github.com/microsoft/monaco-editor/issues/4496
|
||||
// https://github.com/microsoft/monaco-editor/issues/4679
|
||||
/\/assets\/js\/.*monaco/,
|
||||
];
|
||||
for (const pattern of ignorePatterns) {
|
||||
if (err.stack?.includes(pattern)) return true;
|
||||
if (pattern.test(err.stack ?? '')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -39,6 +39,7 @@ const baseOptions: MonacoOpts = {
|
||||
wordWrapBreakAfterCharacters: '',
|
||||
wordWrapBreakBeforeCharacters: '',
|
||||
matchBrackets: 'never',
|
||||
editContext: false, // https://github.com/microsoft/monaco-editor/issues/5081
|
||||
};
|
||||
|
||||
function getCodeEditorConfig(input: HTMLInputElement): CodeEditorConfig | null {
|
||||
|
||||
@ -1,47 +1,59 @@
|
||||
import {matchEmoji, matchMention} from './match.ts';
|
||||
|
||||
test('matchEmoji', () => {
|
||||
expect(matchEmoji('')).toEqual([
|
||||
'+1',
|
||||
'-1',
|
||||
'100',
|
||||
'1234',
|
||||
'1st_place_medal',
|
||||
'2nd_place_medal',
|
||||
]);
|
||||
expect(matchEmoji('')).toMatchInlineSnapshot(`
|
||||
[
|
||||
"+1",
|
||||
"-1",
|
||||
"100",
|
||||
"1234",
|
||||
"1st_place_medal",
|
||||
"2nd_place_medal",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(matchEmoji('hea')).toEqual([
|
||||
'headphones',
|
||||
'headstone',
|
||||
'health_worker',
|
||||
'hear_no_evil',
|
||||
'heard_mcdonald_islands',
|
||||
'heart',
|
||||
]);
|
||||
expect(matchEmoji('hea')).toMatchInlineSnapshot(`
|
||||
[
|
||||
"head_shaking_horizontally",
|
||||
"head_shaking_vertically",
|
||||
"headphones",
|
||||
"headstone",
|
||||
"health_worker",
|
||||
"hear_no_evil",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(matchEmoji('hear')).toEqual([
|
||||
'hear_no_evil',
|
||||
'heard_mcdonald_islands',
|
||||
'heart',
|
||||
'heart_decoration',
|
||||
'heart_eyes',
|
||||
'heart_eyes_cat',
|
||||
]);
|
||||
expect(matchEmoji('hear')).toMatchInlineSnapshot(`
|
||||
[
|
||||
"hear_no_evil",
|
||||
"heard_mcdonald_islands",
|
||||
"heart",
|
||||
"heart_decoration",
|
||||
"heart_eyes",
|
||||
"heart_eyes_cat",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(matchEmoji('poo')).toEqual([
|
||||
'poodle',
|
||||
'hankey',
|
||||
'spoon',
|
||||
'bowl_with_spoon',
|
||||
]);
|
||||
expect(matchEmoji('poo')).toMatchInlineSnapshot(`
|
||||
[
|
||||
"poodle",
|
||||
"hankey",
|
||||
"spoon",
|
||||
"bowl_with_spoon",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(matchEmoji('1st_')).toEqual([
|
||||
'1st_place_medal',
|
||||
]);
|
||||
expect(matchEmoji('1st_')).toMatchInlineSnapshot(`
|
||||
[
|
||||
"1st_place_medal",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(matchEmoji('jellyfis')).toEqual([
|
||||
'jellyfish',
|
||||
]);
|
||||
expect(matchEmoji('jellyfis')).toMatchInlineSnapshot(`
|
||||
[
|
||||
"jellyfish",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('matchMention', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user