0
0
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:
silverwind 2026-02-13 06:36:57 +01:00 committed by GitHub
commit 2921992b79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 12586 additions and 299 deletions

11484
assets/emoji.json generated

File diff suppressed because one or more lines are too long

View File

@ -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 (

View File

@ -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
View 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)
}

View File

@ -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

View File

@ -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)
})
}

View File

@ -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))

View File

@ -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", "mans 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", "womans clothes", []string{"womans_clothes"}, "6.0", false},
{"\U0001f452", "womans hat", []string{"womans_hat"}, "6.0", false},
{"\U0001f93c\u200d\u2640\ufe0f", "women wrestling", []string{"women_wrestling"}, "9.0", false},

View File

@ -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

View File

@ -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)
}

View File

@ -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))

View File

@ -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)
}
}
}

View 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
}

View 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)
}
}
}

View File

@ -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`

View File

@ -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{}

View File

@ -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.",

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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,

View File

@ -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(`

View File

@ -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 {

View File

@ -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)

View File

@ -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">

View 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>

View File

@ -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 -->

View File

@ -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)
})
}

View File

@ -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) {

View File

@ -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);
});

View File

@ -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;
}

View File

@ -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 {

View File

@ -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', () => {