From afcd11c77f91828700ea1761c338d744b6951b8b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 13 Feb 2026 09:16:43 +0100 Subject: [PATCH 01/23] BUG: Fix workflow run jobs API returning null steps (#36603) ## Problem `GET /api/v1/repos/{owner}/{repo}/actions/runs/{runId}/jobs` was always returning `steps: null` for each job. ## Cause In `convert.ToActionWorkflowJob`, when the job had a `TaskID` we loaded the task with `db.GetByID` but never loaded `task.Steps`. `ActionTask.Steps` is not stored in the task row (`xorm:"-"`); it comes from `action_task_step` and is only filled by `task.LoadAttributes()` / `GetTaskStepsByTaskID()`. So the conversion loop over `task.Steps` always saw nil and produced no steps in the API response. ## Solution After resolving the task (by ID when the caller passes `nil`), we now load its steps with `GetTaskStepsByTaskID(ctx, task.ID)` and set `task.Steps` before building the API steps slice. No other behavior is changed. ## Testing - New integration test `TestAPIListWorkflowRunJobsReturnsSteps`: calls the runs/{runId}/jobs endpoint, inserts a task step for a fixture job, and asserts that the response includes non-null, non-empty `steps` with the expected step data. - `make test-sqlite#TestAPIListWorkflowRunJobsReturnsSteps` passes with this fix. --------- Co-authored-by: Manav --- services/convert/convert.go | 39 ++++++++++------- tests/integration/api_actions_run_test.go | 53 +++++++++++++++++++---- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/services/convert/convert.go b/services/convert/convert.go index c081aec771..e1cd30705e 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -349,20 +349,29 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task } } - runnerID = task.RunnerID - if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok { - runnerName = runner.Name - } - for i, step := range task.Steps { - stepStatus, stepConclusion := ToActionsStatus(job.Status) - steps = append(steps, &api.ActionWorkflowStep{ - Name: step.Name, - Number: int64(i), - Status: stepStatus, - Conclusion: stepConclusion, - StartedAt: step.Started.AsTime().UTC(), - CompletedAt: step.Stopped.AsTime().UTC(), - }) + if task != nil { + if task.Steps == nil { + task.Steps, err = actions_model.GetTaskStepsByTaskID(ctx, task.ID) + if err != nil { + return nil, err + } + task.Steps = util.SliceNilAsEmpty(task.Steps) + } + runnerID = task.RunnerID + if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok { + runnerName = runner.Name + } + for i, step := range task.Steps { + stepStatus, stepConclusion := ToActionsStatus(job.Status) + steps = append(steps, &api.ActionWorkflowStep{ + Name: step.Name, + Number: int64(i), + Status: stepStatus, + Conclusion: stepConclusion, + StartedAt: step.Started.AsTime().UTC(), + CompletedAt: step.Stopped.AsTime().UTC(), + }) + } } } @@ -383,7 +392,7 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task Conclusion: conclusion, RunnerID: runnerID, RunnerName: runnerName, - Steps: steps, + Steps: util.SliceNilAsEmpty(steps), CreatedAt: job.Created.AsTime().UTC(), StartedAt: job.Started.AsTime().UTC(), CompletedAt: job.Stopped.AsTime().UTC(), diff --git a/tests/integration/api_actions_run_test.go b/tests/integration/api_actions_run_test.go index a0292f8f8b..4838409560 100644 --- a/tests/integration/api_actions_run_test.go +++ b/tests/integration/api_actions_run_test.go @@ -6,16 +6,21 @@ package integration import ( "fmt" "net/http" + "slices" "testing" + actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAPIActionsGetWorkflowRun(t *testing.T) { @@ -26,15 +31,45 @@ func TestAPIActionsGetWorkflowRun(t *testing.T) { session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802802", repo.FullName())). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusNotFound) - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802", repo.FullName())). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusNotFound) - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/803", repo.FullName())). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusOK) + t.Run("GetRun", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802802", repo.FullName())). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802", repo.FullName())). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/803", repo.FullName())). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + }) + + t.Run("GetJobSteps", func(t *testing.T) { + // Insert task steps for task_id 53 (job 198) so the API can return them once the backend loads them + _, err := db.GetEngine(t.Context()).Insert(&actions_model.ActionTaskStep{ + Name: "main", + TaskID: 53, + Index: 0, + RepoID: repo.ID, + Status: actions_model.StatusSuccess, + Started: timeutil.TimeStamp(1683636528), + Stopped: timeutil.TimeStamp(1683636626), + }) + require.NoError(t, err) + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs", repo.FullName())). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + var jobList api.ActionWorkflowJobsResponse + err = json.Unmarshal(resp.Body.Bytes(), &jobList) + require.NoError(t, err) + + job198Idx := slices.IndexFunc(jobList.Entries, func(job *api.ActionWorkflowJob) bool { return job.ID == 198 }) + require.NotEqual(t, -1, job198Idx, "expected to find job 198 in run 795 jobs list") + job198 := jobList.Entries[job198Idx] + require.NotEmpty(t, job198.Steps, "job must return at least one step when task has steps") + assert.Equal(t, "main", job198.Steps[0].Name, "first step name") + }) } func TestAPIActionsGetWorkflowJob(t *testing.T) { From ce61d6d99df2121c09650545e46a723dd3f90e27 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 14 Feb 2026 00:47:43 +0000 Subject: [PATCH 02/23] [skip ci] Updated translations via Crowdin --- options/locale/locale_tr-TR.json | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_tr-TR.json b/options/locale/locale_tr-TR.json index c7c85ad1dd..edd2db2e39 100644 --- a/options/locale/locale_tr-TR.json +++ b/options/locale/locale_tr-TR.json @@ -148,6 +148,13 @@ "filter.private": "Özel", "no_results_found": "Sonuç bulunamadı.", "internal_error_skipped": "Dahili bir hata oluştu ama atlandı: %s", + "characters_spaces": "Boşluklar", + "characters_tabs": "Sekmeler", + "text_indent_style": "Girinti biçimi", + "text_indent_size": "Girinti boyutu", + "text_line_wrap": "Metni kaydır", + "text_line_nowrap": "Metni kaydırma", + "text_line_wrap_mode": "Satır sarma kipi", "search.search": "Ara...", "search.type_tooltip": "Arama türü", "search.fuzzy": "Bulanık", @@ -751,6 +758,7 @@ "settings.add_email": "E-posta Adresi Ekle", "settings.add_openid": "Açık Kimlik URI 'si ekle", "settings.add_email_confirmation_sent": "\"%s\" adresine bir doğrulama e-postası gönderildi. E-postanızı doğrulamak için %s içinde gelen kutunuzu kontrol ediniz.", + "settings.email_primary_not_found": "Seçilen e-posta adresi bulunamıyor.", "settings.add_email_success": "Yeni e-posta adresi eklendi.", "settings.email_preference_set_success": "E-posta tercihi başarıyla ayarlandı.", "settings.add_openid_success": "Yeni OpenID adresi eklendi.", @@ -1778,6 +1786,8 @@ "repo.pulls.title_desc": "%[2]s içindeki %[1]d işlemeyi %[3]s ile birleştirmek istiyor", "repo.pulls.merged_title_desc": "%[4]s %[2]s içindeki %[1]d işlemeyi %[3]s ile birleştirdi", "repo.pulls.change_target_branch_at": "hedef dal %s adresinden %s%s adresine değiştirildi", + "repo.pulls.marked_as_work_in_progress_at": "değişiklik isteğini devam eden iş olarak işaretledi %s", + "repo.pulls.marked_as_ready_for_review_at": "değişiklik isteğini incelemeye hazır olarak işaretledi %s", "repo.pulls.tab_conversation": "Sohbet", "repo.pulls.tab_commits": "İşleme", "repo.pulls.tab_files": "Değiştirilen Dosyalar", @@ -2122,6 +2132,8 @@ "repo.settings.pulls.ignore_whitespace": "Çakışmalar için Boşlukları Gözardı Et", "repo.settings.pulls.enable_autodetect_manual_merge": "Kendiliğinden algılamalı elle birleştirmeyi etkinleştir (Not: Bazı özel durumlarda yanlış kararlar olabilir)", "repo.settings.pulls.allow_rebase_update": "Değişiklik isteği dalının yeniden yapılandırmayla güncellenmesine izin ver", + "repo.settings.pulls.default_target_branch": "Yeni değişiklik istekleri için varsayılan hedef dal", + "repo.settings.pulls.default_target_branch_default": "Varsayılan dal (%s)", "repo.settings.pulls.default_delete_branch_after_merge": "Varsayılan olarak birleştirmeden sonra değişiklik isteği dalını sil", "repo.settings.pulls.default_allow_edits_from_maintainers": "Bakımcıların düzenlemelerine izin ver", "repo.settings.releases_desc": "Depo Sürümlerini Etkinleştir", @@ -2434,9 +2446,10 @@ "repo.settings.block_outdated_branch_desc": "Baş dal taban dalın arkasındayken birleştirme mümkün olmayacaktır.", "repo.settings.block_admin_merge_override": "Yöneticiler dal koruma kurallarına uymalıdır", "repo.settings.block_admin_merge_override_desc": "Yöneticiler dal koruma kurallarına uymalıdır ve kurallardan kaçınamazlar.", - "repo.settings.default_branch_desc": "Değişiklik istekleri ve kod işlemeleri için varsayılan bir depo dalı seçin:", + "repo.settings.default_branch_desc": "Kod işlemeleri için varsayılan bir depo dalı seçin.", + "repo.settings.default_target_branch_desc": "Değişiklik istekleri, Depo Gelişmiş Ayarları'nın Değişiklik İstekleri bölümünde ayarlanmışsa farklı varsayılan hedef dal kullanabilir.", "repo.settings.merge_style_desc": "Biçimleri Birleştir", - "repo.settings.default_merge_style_desc": "Değişiklik istekleri için varsayılan birleştirme tarzı", + "repo.settings.default_merge_style_desc": "Varsayılan birleştirme tarzı", "repo.settings.choose_branch": "Bir dal seç…", "repo.settings.no_protected_branch": "Korumalı dal yok.", "repo.settings.edit_protected_branch": "Düzenle", @@ -2648,7 +2661,7 @@ "repo.branch.restore_success": "\"%s\" dalı geri yüklendi.", "repo.branch.restore_failed": "\"%s\" dalı geri yüklenemedi.", "repo.branch.protected_deletion_failed": "\"%s\" dalı korunuyor. Silinemez.", - "repo.branch.default_deletion_failed": "\"%s\" dalı varsayılan daldır. Silinemez.", + "repo.branch.default_deletion_failed": "\"%s\" dalı varsayılan veya değişiklik isteği hedef dalıdır. Silinemez.", "repo.branch.default_branch_not_exist": "Varsayılan dal \"%s\" mevcut değil.", "repo.branch.restore": "\"%s\" Dalını Geri Yükle", "repo.branch.download": "\"%s\" Dalını İndir", From 7a8fe9eb370c6f3f5ec6eae2e1ebba5ac77b1f25 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Sat, 14 Feb 2026 15:40:59 +0800 Subject: [PATCH 03/23] feat(db): Improve BuildCaseInsensitiveLike with lowercase (#36598) Improve BuildCaseInsensitiveLike with lowercase, users are more likely to input lowercase letters, so lowercase letters are used. --------- Signed-off-by: Tyrone Yeh Co-authored-by: silverwind Co-authored-by: wxiaoguang --- models/db/common.go | 30 +++++++++++++++--------------- models/repo/user_repo.go | 2 +- modules/util/util.go | 8 ++++---- modules/util/util_test.go | 28 ++++++++++++---------------- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/models/db/common.go b/models/db/common.go index ea628bf2a0..b3c43f8b62 100644 --- a/models/db/common.go +++ b/models/db/common.go @@ -12,30 +12,30 @@ import ( "xorm.io/builder" ) -// BuildCaseInsensitiveLike returns a condition to check if the given value is like the given key case-insensitively. -// Handles especially SQLite correctly as UPPER there only transforms ASCII letters. +// BuildCaseInsensitiveLike returns a case-insensitive LIKE condition for the given key and value. +// Cast the search value and the database column value to the same case for case-insensitive matching. +// * SQLite: only cast ASCII chars because it doesn't handle complete Unicode case folding +// * Other databases: use database's string function, assuming that they are able to handle complete Unicode case folding correctly func BuildCaseInsensitiveLike(key, value string) builder.Cond { + // ToLowerASCII is about 7% faster than ToUpperASCII (according to Golang's benchmark) if setting.Database.Type.IsSQLite3() { - return builder.Like{"UPPER(" + key + ")", util.ToUpperASCII(value)} + return builder.Like{"LOWER(" + key + ")", util.ToLowerASCII(value)} } - return builder.Like{"UPPER(" + key + ")", strings.ToUpper(value)} + return builder.Like{"LOWER(" + key + ")", strings.ToLower(value)} } // BuildCaseInsensitiveIn returns a condition to check if the given value is in the given values case-insensitively. -// Handles especially SQLite correctly as UPPER there only transforms ASCII letters. +// See BuildCaseInsensitiveLike for more details func BuildCaseInsensitiveIn(key string, values []string) builder.Cond { - uppers := make([]string, 0, len(values)) + incaseValues := make([]string, len(values)) + caseCast := strings.ToLower if setting.Database.Type.IsSQLite3() { - for _, value := range values { - uppers = append(uppers, util.ToUpperASCII(value)) - } - } else { - for _, value := range values { - uppers = append(uppers, strings.ToUpper(value)) - } + caseCast = util.ToLowerASCII } - - return builder.In("UPPER("+key+")", uppers) + for i, value := range values { + incaseValues[i] = caseCast(value) + } + return builder.In("LOWER("+key+")", incaseValues) } // BuilderDialect returns the xorm.Builder dialect of the engine diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index 232087d865..08cf964bc8 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -151,7 +151,7 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) { users := make([]*user_model.User, 0, 30) var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"} - if isShowFullName { + if search != "" && isShowFullName { prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%")) } diff --git a/modules/util/util.go b/modules/util/util.go index f197d4d6a4..d7702439d6 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -90,12 +90,12 @@ func CryptoRandomBytes(length int64) ([]byte, error) { return buf, err } -// ToUpperASCII returns s with all ASCII letters mapped to their upper case. -func ToUpperASCII(s string) string { +// ToLowerASCII returns s with all ASCII letters mapped to their lower case. +func ToLowerASCII(s string) string { b := []byte(s) for i, c := range b { - if 'a' <= c && c <= 'z' { - b[i] -= 'a' - 'A' + if 'A' <= c && c <= 'Z' { + b[i] += 'a' - 'A' } } return string(b) diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 38876276e3..fd677f5c11 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -178,30 +178,26 @@ type StringTest struct { in, out string } -var upperTests = []StringTest{ +var lowerTests = []StringTest{ {"", ""}, - {"ONLYUPPER", "ONLYUPPER"}, - {"abc", "ABC"}, - {"AbC123", "ABC123"}, - {"azAZ09_", "AZAZ09_"}, - {"longStrinGwitHmixofsmaLLandcAps", "LONGSTRINGWITHMIXOFSMALLANDCAPS"}, - {"long\u0250string\u0250with\u0250nonascii\u2C6Fchars", "LONG\u0250STRING\u0250WITH\u0250NONASCII\u2C6FCHARS"}, - {"\u0250\u0250\u0250\u0250\u0250", "\u0250\u0250\u0250\u0250\u0250"}, - {"a\u0080\U0010FFFF", "A\u0080\U0010FFFF"}, - {"lél", "LéL"}, + {"ABC", "abc"}, + {"AbC123_", "abc123_"}, + {"LONG\u0250string\u0250WITH\u0250non-ascii\u2C6FCHARS\u0080\uFFFF", "long\u0250string\u0250with\u0250non-ascii\u2C6Fchars\u0080\uFFFF"}, + {"lél", "lél"}, + {"LÉL", "lÉl"}, } -func TestToUpperASCII(t *testing.T) { - for _, tc := range upperTests { - assert.Equal(t, ToUpperASCII(tc.in), tc.out) +func TestToLowerASCII(t *testing.T) { + for _, tc := range lowerTests { + assert.Equal(t, ToLowerASCII(tc.in), tc.out) } } -func BenchmarkToUpper(b *testing.B) { - for _, tc := range upperTests { +func BenchmarkToLower(b *testing.B) { + for _, tc := range lowerTests { b.Run(tc.in, func(b *testing.B) { for b.Loop() { - ToUpperASCII(tc.in) + ToLowerASCII(tc.in) } }) } From 4805151f56d49a3f67e5c9f192c7322fe813a4ab Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Sat, 14 Feb 2026 17:51:03 +0100 Subject: [PATCH 04/23] use user id in noreply emails (#36550) This implements id based hidden emails in format of `user+id@NoReplyAddress` resolves: https://github.com/go-gitea/gitea/issues/33471 --- The change is not breaking however it is recommended for users to move to this newer type of no reply address --------- Co-authored-by: Lauris B --- models/user/user.go | 82 ++++++++++++++++------ models/user/user_test.go | 47 ++++++++++--- tests/integration/editor_test.go | 2 +- tests/integration/repofiles_change_test.go | 12 ++-- 4 files changed, 106 insertions(+), 37 deletions(-) diff --git a/models/user/user.go b/models/user/user.go index 1797d3eefc..59a0b4e5d1 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -13,6 +13,7 @@ import ( "net/url" "path/filepath" "regexp" + "strconv" "strings" "sync" "time" @@ -212,7 +213,7 @@ func (u *User) SetLastLogin() { // GetPlaceholderEmail returns an noreply email func (u *User) GetPlaceholderEmail() string { - return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress) + return fmt.Sprintf("%s+%d@%s", u.LowerName, u.ID, setting.Service.NoReplyAddress) } // GetEmail returns a noreply email, if the user has set to keep his @@ -1197,14 +1198,18 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro needCheckEmails := make(container.Set[string]) needCheckUserNames := make(container.Set[string]) + needCheckUserIDs := make(container.Set[int64]) noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress) for _, email := range emails { emailLower := strings.ToLower(email) - if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok { - needCheckUserNames.Add(noReplyUserNameLower) - needCheckEmails.Add(emailLower) - } else { - needCheckEmails.Add(emailLower) + needCheckEmails.Add(emailLower) + if localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok { + name, id := parseLocalPartToNameID(localPart) + if id != 0 { + needCheckUserIDs.Add(id) + } else if name != "" { + needCheckUserNames.Add(name) + } } } @@ -1234,16 +1239,57 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro } } - users := make(map[int64]*User, len(needCheckUserNames)) - if err := db.GetEngine(ctx).In("lower_name", needCheckUserNames.Values()).Find(&users); err != nil { - return nil, err + usersByIDs := make(map[int64]*User) + if len(needCheckUserIDs) > 0 || len(needCheckUserNames) > 0 { + cond := builder.NewCond() + if len(needCheckUserIDs) > 0 { + cond = cond.Or(builder.In("id", needCheckUserIDs.Values())) + } + if len(needCheckUserNames) > 0 { + cond = cond.Or(builder.In("lower_name", needCheckUserNames.Values())) + } + if err := db.GetEngine(ctx).Where(cond).Find(&usersByIDs); err != nil { + return nil, err + } } - for _, user := range users { - results[strings.ToLower(user.GetPlaceholderEmail())] = user + + usersByName := make(map[string]*User) + for _, user := range usersByIDs { + usersByName[user.LowerName] = user } + + for _, email := range emails { + emailLower := strings.ToLower(email) + if _, ok := results[emailLower]; ok { + continue + } + + localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix) + if !ok { + continue + } + name, id := parseLocalPartToNameID(localPart) + if user, ok := usersByIDs[id]; ok { + results[emailLower] = user + } else if user, ok := usersByName[name]; ok { + results[emailLower] = user + } + } + return &EmailUserMap{results}, nil } +// parseLocalPartToNameID attempts to unparse local-part of email that's in format user+id +// returns user and id if possible +func parseLocalPartToNameID(localPart string) (string, int64) { + var id int64 + name, idstr, hasPlus := strings.Cut(localPart, "+") + if hasPlus { + id, _ = strconv.ParseInt(idstr, 10, 64) + } + return name, id +} + // GetUserByEmail returns the user object by given e-mail if exists. func GetUserByEmail(ctx context.Context, email string) (*User, error) { if len(email) == 0 { @@ -1262,16 +1308,12 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { } // Finally, if email address is the protected email address: - if before, ok := strings.CutSuffix(email, "@"+setting.Service.NoReplyAddress); ok { - username := before - user := &User{} - has, err := db.GetEngine(ctx).Where("lower_name=?", username).Get(user) - if err != nil { - return nil, err - } - if has { - return user, nil + if localPart, ok := strings.CutSuffix(email, strings.ToLower("@"+setting.Service.NoReplyAddress)); ok { + name, id := parseLocalPartToNameID(localPart) + if id != 0 { + return GetUserByID(ctx, id) } + return GetUserByName(ctx, name) } return nil, ErrUserNotExist{Name: email} diff --git a/models/user/user_test.go b/models/user/user_test.go index 923f2cd40e..378acc4180 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -51,12 +51,27 @@ func TestOAuth2Application_LoadUser(t *testing.T) { func TestUserEmails(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")() t.Run("GetUserEmailsByNames", func(t *testing.T) { - // ignore none active user email + // ignore not active user email assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user9"})) assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user5"})) assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "org7"})) }) + + cases := []struct { + Email string + UID int64 + }{ + {"UseR1@example.com", 1}, + {"user1-2@example.COM", 1}, + {"USER2@" + setting.Service.NoReplyAddress, 2}, + {"user2+2@" + setting.Service.NoReplyAddress, 2}, + {"oldUser2UsernameWhichDoesNotMatterForQuery+2@" + setting.Service.NoReplyAddress, 2}, + {"badUser+99999@" + setting.Service.NoReplyAddress, 0}, + {"user4@example.com", 4}, + {"no-such", 0}, + } t.Run("GetUsersByEmails", func(t *testing.T) { defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")() testGetUserByEmail := func(t *testing.T, email string, uid int64) { @@ -70,15 +85,27 @@ func TestUserEmails(t *testing.T) { require.NotNil(t, user) assert.Equal(t, uid, user.ID) } - cases := []struct { - Email string - UID int64 - }{ - {"UseR1@example.com", 1}, - {"user1-2@example.COM", 1}, - {"USER2@" + setting.Service.NoReplyAddress, 2}, - {"user4@example.com", 4}, - {"no-such", 0}, + for _, c := range cases { + t.Run(c.Email, func(t *testing.T) { + testGetUserByEmail(t, c.Email, c.UID) + }) + } + + t.Run("NoReplyConflict", func(t *testing.T) { + setting.Service.NoReplyAddress = "example.com" + testGetUserByEmail(t, "user1-2@example.COM", 1) + }) + }) + t.Run("GetUserByEmail", func(t *testing.T) { + testGetUserByEmail := func(t *testing.T, email string, uid int64) { + user, err := user_model.GetUserByEmail(t.Context(), email) + if uid == 0 { + require.Error(t, err) + assert.Nil(t, user) + } else { + require.NotNil(t, user) + assert.Equal(t, uid, user.ID) + } } for _, c := range cases { t.Run(c.Email, func(t *testing.T) { diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index c70bb061c9..8368c91258 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -258,7 +258,7 @@ func testEditorWebGitCommitEmail(t *testing.T) { t.Run("DefaultEmailKeepPrivate", func(t *testing.T) { defer tests.PrintCurrentTest(t)() paramsForKeepPrivate["commit_email"] = "" - resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org") + resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2+2@noreply.example.org") }) t.Run("ChooseEmail", func(t *testing.T) { defer tests.PrintCurrentTest(t)() diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index 442959b8a5..390ec2ebeb 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -132,14 +132,14 @@ func getExpectedFileResponseForRepoFilesCreate(commitID string, lastCommit *git. Author: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2@noreply.example.org", + Email: "user2+2@noreply.example.org", }, Date: time.Now().UTC().Format(time.RFC3339), }, Committer: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2@noreply.example.org", + Email: "user2+2@noreply.example.org", }, Date: time.Now().UTC().Format(time.RFC3339), }, @@ -202,14 +202,14 @@ func getExpectedFileResponseForRepoFilesUpdate(commitID, filename, lastCommitSHA Author: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2@noreply.example.org", + Email: "user2+2@noreply.example.org", }, Date: time.Now().UTC().Format(time.RFC3339), }, Committer: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2@noreply.example.org", + Email: "user2+2@noreply.example.org", }, Date: time.Now().UTC().Format(time.RFC3339), }, @@ -312,13 +312,13 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str Author: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2@noreply.example.org", + Email: "user2+2@noreply.example.org", }, }, Committer: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2@noreply.example.org", + Email: "user2+2@noreply.example.org", }, }, Parents: []*api.CommitMeta{ From 1d4b8486f0fc64012418c560396938e0096cc110 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Feb 2026 18:11:13 +0100 Subject: [PATCH 05/23] Update AGENTS.md instructions (#36627) --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index d0912c6bde..402a9d6945 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,3 +6,5 @@ - Before committing `go.mod` changes, run `make tidy` - Before committing new `.go` files, add the current year into the copyright header - Before committing any files, remove all trailing whitespace from source code lines +- Never force-push to pull request branches +- Always start issue and pull request comments with an authorship attribution From 2cdf86e18489d940399c52c3052c2f91fb83c924 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sat, 14 Feb 2026 19:00:36 +0100 Subject: [PATCH 06/23] automate updating nix flakes (#35641) --- .github/workflows/cron-flake-updater.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/cron-flake-updater.yml diff --git a/.github/workflows/cron-flake-updater.yml b/.github/workflows/cron-flake-updater.yml new file mode 100644 index 0000000000..105802e558 --- /dev/null +++ b/.github/workflows/cron-flake-updater.yml @@ -0,0 +1,22 @@ +name: cron-flake-updater + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # runs weekly on Sunday at 00:00 + +jobs: + nix-flake-update: + permissions: + contents: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: DeterminateSystems/determinate-nix-action@v3 + - uses: DeterminateSystems/update-flake-lock@main + with: + pr-title: "Update Nix flake" + pr-labels: | + dependencies From a6282c98d72addd66d356a5ce2775089dd46cddb Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 15 Feb 2026 00:52:30 +0000 Subject: [PATCH 07/23] [skip ci] Updated translations via Crowdin --- options/locale/locale_zh-CN.json | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/options/locale/locale_zh-CN.json b/options/locale/locale_zh-CN.json index a5721012cc..861090cea7 100644 --- a/options/locale/locale_zh-CN.json +++ b/options/locale/locale_zh-CN.json @@ -148,6 +148,13 @@ "filter.private": "私有", "no_results_found": "未找到结果", "internal_error_skipped": "发生内部错误,但已跳过: %s", + "characters_spaces": "空格", + "characters_tabs": "制表符", + "text_indent_style": "缩进风格", + "text_indent_size": "缩进大小", + "text_line_wrap": "换行", + "text_line_nowrap": "无换行", + "text_line_wrap_mode": "换行模式", "search.search": "搜索…", "search.type_tooltip": "搜索类型", "search.fuzzy": "模糊", @@ -751,6 +758,7 @@ "settings.add_email": "新增邮箱地址", "settings.add_openid": "添加 OpenID URI", "settings.add_email_confirmation_sent": "一封确认邮件已经发送至「%s」,请检查您的收件箱并在 %s 内完成确认注册操作。", + "settings.email_primary_not_found": "找不到选定的电子邮件地址。", "settings.add_email_success": "新邮箱地址已添加。", "settings.email_preference_set_success": "邮件首选项已成功设置。", "settings.add_openid_success": "新的 OpenID 地址已添加。", @@ -1635,7 +1643,7 @@ "repo.issues.cancel_tracking": "取消", "repo.issues.cancel_tracking_history": "取消时间跟踪 %s", "repo.issues.del_time": "删除此时间跟踪日志", - "repo.issues.add_time_history": "于 %[2]s 添加计时 %[1]", + "repo.issues.add_time_history": "于 %[2]s 添加计时 %[1]s", "repo.issues.del_time_history": "已删除时间 %s", "repo.issues.add_time_manually": "手动添加时间", "repo.issues.add_time_hours": "小时", @@ -1778,6 +1786,8 @@ "repo.pulls.title_desc": "请求将 %[1]d 次代码提交从 %[2]s 合并至 %[3]s", "repo.pulls.merged_title_desc": "于 %[4]s 将 %[1]d 次代码提交从 %[2]s合并至 %[3]s", "repo.pulls.change_target_branch_at": "将目标分支从 %s 更改为 %s %s", + "repo.pulls.marked_as_work_in_progress_at": "已将合并请求标记为进行中 %s", + "repo.pulls.marked_as_ready_for_review_at": "已将合并请求标记为准备评审 %s", "repo.pulls.tab_conversation": "对话内容", "repo.pulls.tab_commits": "代码提交", "repo.pulls.tab_files": "文件变动", @@ -2122,6 +2132,8 @@ "repo.settings.pulls.ignore_whitespace": "忽略空白冲突", "repo.settings.pulls.enable_autodetect_manual_merge": "启用自动检查手动合并(注意:在某些特殊情况下可能会出现误判)", "repo.settings.pulls.allow_rebase_update": "允许通过变基更新合并请求分支", + "repo.settings.pulls.default_target_branch": "新合并请求的默认目标分支", + "repo.settings.pulls.default_target_branch_default": "默认分支(%s)", "repo.settings.pulls.default_delete_branch_after_merge": "默认合并后删除合并请求分支", "repo.settings.pulls.default_allow_edits_from_maintainers": "默认允许维护者编辑", "repo.settings.releases_desc": "启用仓库发布", @@ -2434,7 +2446,8 @@ "repo.settings.block_outdated_branch_desc": "当头部分支落后基础分支时,不能合并。", "repo.settings.block_admin_merge_override": "管理员须遵守分支保护规则", "repo.settings.block_admin_merge_override_desc": "管理员须遵守分支保护规则,不能规避该规则。", - "repo.settings.default_branch_desc": "请选择一个默认的分支用于合并请求和提交:", + "repo.settings.default_branch_desc": "选择一个默认分支用于提交代码。", + "repo.settings.default_target_branch_desc": "如果在仓库高级设置的合并请求部分中进行了设置,则合并请求可以使用不同的默认目标分支。", "repo.settings.merge_style_desc": "合并方式", "repo.settings.default_merge_style_desc": "默认合并风格", "repo.settings.choose_branch": "选择一个分支…", @@ -2548,7 +2561,7 @@ "repo.diff.show_more": "显示更多", "repo.diff.load": "加载差异", "repo.diff.generated": "自动生成", - "repo.diff.vendored": "vendored", + "repo.diff.vendored": "第三方依赖", "repo.diff.comment.add_line_comment": "添加行内评论", "repo.diff.comment.placeholder": "留下评论", "repo.diff.comment.add_single_comment": "添加单条评论", @@ -2648,7 +2661,7 @@ "repo.branch.restore_success": "分支「%s」已还原。", "repo.branch.restore_failed": "分支「%s」还原失败。", "repo.branch.protected_deletion_failed": "不能删除受保护的分支「%s」。", - "repo.branch.default_deletion_failed": "不能删除默认分支「%s」。", + "repo.branch.default_deletion_failed": "分支「%s」是默认分支或合并请求目标分支,无法删除。", "repo.branch.default_branch_not_exist": "默认分支「%s」不存在。", "repo.branch.restore": "还原分支「%s」", "repo.branch.download": "下载分支「%s」", @@ -2672,7 +2685,7 @@ "repo.tag.create_tag_operation": "创建 Git 标签", "repo.tag.confirm_create_tag": "创建 Git 标签", "repo.tag.create_tag_from": "基于「%s」创建新 Git 标签", - "repo.tag.create_success": "Git 标签「%s」已存在。", + "repo.tag.create_success": "Git 标签「%s」创建成功。", "repo.topic.manage_topics": "管理主题", "repo.topic.done": "保存", "repo.topic.count_prompt": "您最多选择25个主题", From 26bb175d69edba794cf7d70007025d21aa31110b Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Feb 2026 20:41:59 +0100 Subject: [PATCH 08/23] Persist actions log time display settings in `localStorage` (#36623) Persist the two boolean settings in the actions log into `localStorage` so that they are remembered across page reloads. --------- Co-authored-by: Claude Opus 4.6 --- web_src/js/components/RepoActionView.vue | 22 +++++++++++++++------- web_src/js/modules/user-settings.ts | 4 ++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index a5275c99e9..d099344d2b 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -107,6 +107,8 @@ function isLogElementInViewport(el: Element, {extraViewPortHeight}={extraViewPor type LocaleStorageOptions = { autoScroll: boolean; expandRunning: boolean; + actionsLogShowSeconds: boolean; + actionsLogShowTimestamps: boolean; }; export default defineComponent({ @@ -135,8 +137,8 @@ export default defineComponent({ }, data() { - const defaultViewOptions: LocaleStorageOptions = {autoScroll: true, expandRunning: false}; - const {autoScroll, expandRunning} = localUserSettings.getJsonObject('actions-view-options', defaultViewOptions); + const defaultViewOptions: LocaleStorageOptions = {autoScroll: true, expandRunning: false, actionsLogShowSeconds: false, actionsLogShowTimestamps: false}; + const {autoScroll, expandRunning, actionsLogShowSeconds, actionsLogShowTimestamps} = localUserSettings.getJsonObject('actions-view-options', defaultViewOptions); return { // internal state loadingAbortController: null as AbortController | null, @@ -146,11 +148,11 @@ export default defineComponent({ menuVisible: false, isFullScreen: false, timeVisible: { - 'log-time-stamp': false, - 'log-time-seconds': false, + 'log-time-stamp': actionsLogShowTimestamps, + 'log-time-seconds': actionsLogShowSeconds, }, - optionAlwaysAutoScroll: autoScroll ?? false, - optionAlwaysExpandRunning: expandRunning ?? false, + optionAlwaysAutoScroll: autoScroll, + optionAlwaysExpandRunning: expandRunning, // provided by backend run: { @@ -253,7 +255,12 @@ export default defineComponent({ methods: { saveLocaleStorageOptions() { - const opts: LocaleStorageOptions = {autoScroll: this.optionAlwaysAutoScroll, expandRunning: this.optionAlwaysExpandRunning}; + const opts: LocaleStorageOptions = { + autoScroll: this.optionAlwaysAutoScroll, + expandRunning: this.optionAlwaysExpandRunning, + actionsLogShowSeconds: this.timeVisible['log-time-seconds'], + actionsLogShowTimestamps: this.timeVisible['log-time-stamp'], + }; localUserSettings.setJsonObject('actions-view-options', opts); }, @@ -470,6 +477,7 @@ export default defineComponent({ for (const el of this.elStepsContainer().querySelectorAll(`.log-time-${type}`)) { toggleElem(el, this.timeVisible[`log-time-${type}`]); } + this.saveLocaleStorageOptions(); }, toggleFullScreen() { diff --git a/web_src/js/modules/user-settings.ts b/web_src/js/modules/user-settings.ts index 7c96ad8373..fe0113407c 100644 --- a/web_src/js/modules/user-settings.ts +++ b/web_src/js/modules/user-settings.ts @@ -58,8 +58,8 @@ export const localUserSettings = { getJsonObject: >(key: string, def: T): T => { const value = getLocalStorageUserSetting(key); try { - const decoded = value !== null ? JSON.parse(value) : def; - return decoded ?? def; + const decoded = value !== null ? JSON.parse(value) : null; + return {...def, ...decoded}; } catch (e) { console.error(`Unable to parse JSON value for local user settings ${key}=${value}`, e); } From 838bb1d379411d343884e2617e27674efea4eff1 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Feb 2026 21:33:04 +0100 Subject: [PATCH 09/23] Fix minor UI issues in runner edit page (#36590) Before: Screenshot 2026-02-11 at 16 39 46 Screenshot 2026-02-11 at 16 42 57 After: Screenshot 2026-02-11 at 16 39 32 Screenshot 2026-02-11 at 16 42 49 --------- Signed-off-by: silverwind --- templates/shared/actions/runner_edit.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl index 8652d161bc..dbf4104fe5 100644 --- a/templates/shared/actions/runner_edit.tmpl +++ b/templates/shared/actions/runner_edit.tmpl @@ -16,7 +16,7 @@
- + {{range .Runner.AgentLabels}} {{.}} {{end}} @@ -66,7 +66,7 @@ {{.Status.LocaleString ctx.Locale}} {{.GetRepoName}} - {{ShortSha .CommitSHA}} + {{ShortSha .CommitSHA}} {{if .IsStopped}} {{DateUtils.TimeSince .Stopped}} From 692ef9eca6a4b4ac231a1e1494eb36e2afc1948d Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Sun, 15 Feb 2026 19:17:05 -0300 Subject: [PATCH 10/23] Update the Unlicense copy to latest version (#36636) It appears that an older version of the Unlicensed was used (at the least, `http` url was referenced therein over `https` which is used in the original) Original formatting also has been preserved. Signed-off-by: Beda Schmid --- options/license/Unlicense | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/options/license/Unlicense b/options/license/Unlicense index cde4ac6981..efb9808816 100644 --- a/options/license/Unlicense +++ b/options/license/Unlicense @@ -1,10 +1,24 @@ This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. -In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. -For more information, please refer to +For more information, please refer to From 88752bc1593e766b7ee4e5d0cfcefcb40f4b8592 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 16 Feb 2026 00:47:02 +0100 Subject: [PATCH 11/23] Exclude cancelled runs from failure-only email notifications (#36569) The default configuration of `failure-only` added in https://github.com/go-gitea/gitea/pull/34982 included sending mails for cancelled runs which is not what one would expect from a option named like that because a cancelled run is not a failure. This change makes it omit mails for cancelled runs: | Run Status | `failure-only` before | `failure-only` after | |------------|-----------------------|----------------------| | Success | no | no | | Failure | mail | mail | | Cancelled | mail | no | The first commit in this PR is the fix, and there are a few more refactor commits afterwards. --------- Co-authored-by: Claude Opus 4.6 --- models/user/user.go | 6 ++--- services/mailer/mail_workflow_run.go | 35 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/models/user/user.go b/models/user/user.go index 59a0b4e5d1..38042631de 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -496,10 +496,10 @@ func (u *User) ShortName(length int) string { return util.EllipsisDisplayString(u.Name, length) } -// IsMailable checks if a user is eligible -// to receive emails. +// IsMailable checks if a user is eligible to receive emails. +// System users like Ghost and Gitea Actions are excluded. func (u *User) IsMailable() bool { - return u.IsActive + return u.IsActive && !u.IsGiteaActions() && !u.IsGhost() } // IsUserExist checks if given username exist, diff --git a/services/mailer/mail_workflow_run.go b/services/mailer/mail_workflow_run.go index 3789102812..9efaa4182b 100644 --- a/services/mailer/mail_workflow_run.go +++ b/services/mailer/mail_workflow_run.go @@ -149,30 +149,31 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo return nil } -func MailActionsTrigger(ctx context.Context, sender *user_model.User, repo *repo_model.Repository, run *actions_model.ActionRun) error { +func MailActionsTrigger(ctx context.Context, recipient *user_model.User, repo *repo_model.Repository, run *actions_model.ActionRun) error { if setting.MailService == nil { return nil } if !run.Status.IsDone() || run.Status.IsSkipped() { return nil } - - recipients := make([]*user_model.User, 0) - - if !sender.IsGiteaActions() && !sender.IsGhost() && sender.IsMailable() { - notifyPref, err := user_model.GetUserSetting(ctx, sender.ID, - user_model.SettingsKeyEmailNotificationGiteaActions, user_model.SettingEmailNotificationGiteaActionsFailureOnly) - if err != nil { - return err - } - if notifyPref == user_model.SettingEmailNotificationGiteaActionsAll || !run.Status.IsSuccess() && notifyPref != user_model.SettingEmailNotificationGiteaActionsDisabled { - recipients = append(recipients, sender) - } + if !recipient.IsMailable() { + return nil } - if len(recipients) > 0 { - log.Debug("MailActionsTrigger: Initiate email composition") - return composeAndSendActionsWorkflowRunStatusEmail(ctx, repo, run, sender, recipients) + notifyPref, err := user_model.GetUserSetting(ctx, recipient.ID, + user_model.SettingsKeyEmailNotificationGiteaActions, user_model.SettingEmailNotificationGiteaActionsFailureOnly) + if err != nil { + return err } - return nil + // "disabled" never sends + if notifyPref == user_model.SettingEmailNotificationGiteaActionsDisabled { + return nil + } + // "failure-only" skips non-failure runs + if notifyPref != user_model.SettingEmailNotificationGiteaActionsAll && !run.Status.IsFailure() { + return nil + } + + log.Debug("MailActionsTrigger: Initiate email composition") + return composeAndSendActionsWorkflowRunStatusEmail(ctx, repo, run, recipient, []*user_model.User{recipient}) } From 2896dac5369e6f2a8847c127e4bd9ecd4b82c098 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 16 Feb 2026 01:49:03 +0100 Subject: [PATCH 12/23] Fix state desync in ComboMarkdownEditor (#36625) Fixes https://github.com/go-gitea/gitea/issues/24253 When a tasklist checkbox is clicked, the tasklist code [updates `.raw-content` with latest server data](https://github.com/go-gitea/gitea/blob/7a8fe9eb370c6f3f5ec6eae2e1ebba5ac77b1f25/web_src/js/markup/tasklist.ts#L73) in the DOM after POSTing. Then when "Edit" is clicked the ComboMarkdownEditor is shown with a stale value from the previous edit session. The fix makes it always read from `.raw-content`, no server syncronization necessary because the value in `.raw-content` is the latest from the server. --------- Co-authored-by: wxiaoguang --- web_src/js/features/repo-issue-edit.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/web_src/js/features/repo-issue-edit.ts b/web_src/js/features/repo-issue-edit.ts index 3838c4f041..4a112368f4 100644 --- a/web_src/js/features/repo-issue-edit.ts +++ b/web_src/js/features/repo-issue-edit.ts @@ -97,11 +97,9 @@ async function tryOnEditContent(e: Event) { cancelButton.addEventListener('click', cancelAndReset); form.addEventListener('submit', saveAndRefresh); } - - // FIXME: ideally here should reload content and attachment list from backend for existing editor, to avoid losing data - if (!comboMarkdownEditor.value()) { - comboMarkdownEditor.value(rawContent.textContent); - } + // when the content has changed on server side, there is no sync, and this page doesn't have the latest content, + // the editor still shows the old content, server will reject end user's submit by "data-content-version" check + comboMarkdownEditor.value(rawContent.textContent); comboMarkdownEditor.switchTabToEditor(); comboMarkdownEditor.focus(); triggerUploadStateChanged(comboMarkdownEditor.container); From 4ca4217b3d6874e46ce51701eb1ad7977050a683 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Mon, 16 Feb 2026 00:50:05 +0000 Subject: [PATCH 13/23] [skip ci] Updated translations via Crowdin --- options/locale/locale_ja-JP.json | 105 ++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 7 deletions(-) diff --git a/options/locale/locale_ja-JP.json b/options/locale/locale_ja-JP.json index 4c62a758b6..ffbd8c0d9e 100644 --- a/options/locale/locale_ja-JP.json +++ b/options/locale/locale_ja-JP.json @@ -12,6 +12,7 @@ "link_account": "アカウント連携", "register": "登録", "version": "バージョン", + "powered_by": "Powered by %s", "page": "ページ", "template": "テンプレート", "language": "言語", @@ -31,6 +32,7 @@ "password": "パスワード", "access_token": "アクセストークン", "re_type": "パスワード確認", + "captcha": "CAPTCHA", "twofa": "2要素認証", "twofa_scratch": "2要素認証スクラッチコード", "passcode": "パスコード", @@ -74,6 +76,7 @@ "pull_requests": "プルリクエスト", "issues": "イシュー", "milestones": "マイルストーン", + "ok": "OK", "cancel": "キャンセル", "retry": "再試行", "rerun": "再実行", @@ -130,6 +133,7 @@ "confirm_delete_selected": "選択したすべてのアイテムを削除してよろしいですか?", "name": "名称", "value": "値", + "readme": "Readme", "filter_title": "フィルター", "filter.clear": "フィルターをクリア", "filter.is_archived": "アーカイブ", @@ -144,6 +148,13 @@ "filter.private": "プライベート", "no_results_found": "見つかりません。", "internal_error_skipped": "内部エラーが発生しましたがスキップされました: %s", + "characters_spaces": "スペース", + "characters_tabs": "タブ", + "text_indent_style": "インデントスタイル", + "text_indent_size": "インデントサイズ", + "text_line_wrap": "折り返す", + "text_line_nowrap": "折り返さない", + "text_line_wrap_mode": "行折り返しモード", "search.search": "検索…", "search.type_tooltip": "検索タイプ", "search.fuzzy": "あいまい", @@ -183,6 +194,7 @@ "editor.buttons.heading.tooltip": "見出し追加", "editor.buttons.bold.tooltip": "太字追加", "editor.buttons.italic.tooltip": "イタリック体追加", + "editor.buttons.strikethrough.tooltip": "取り消し線のテキストを追加", "editor.buttons.quote.tooltip": "引用", "editor.buttons.code.tooltip": "コード追加", "editor.buttons.link.tooltip": "リンク追加", @@ -198,6 +210,8 @@ "editor.buttons.switch_to_legacy.tooltip": "レガシーエディタを使用する", "editor.buttons.enable_monospace_font": "等幅フォントを有効にする", "editor.buttons.disable_monospace_font": "等幅フォントを無効にする", + "filter.string.asc": "A–Z", + "filter.string.desc": "Z–A", "error.occurred": "エラーが発生しました", "error.report_message": "Gitea のバグが疑われる場合は、GitHubでIssueを検索して、見つからなければ新しいIssueを作成してください。", "error.not_found": "ターゲットが見つかりませんでした。", @@ -224,6 +238,7 @@ "install.db_name": "データベース名", "install.db_schema": "スキーマ", "install.db_schema_helper": "空の場合はデータベースのデフォルト(\"public\")となります。", + "install.ssl_mode": "SSL", "install.path": "パス", "install.sqlite_helper": "SQLite3のデータベースファイルパス。
Giteaをサービスとして実行する場合は絶対パスを入力します。", "install.reinstall_error": "既存のGiteaデータベースへインストールしようとしています", @@ -401,6 +416,7 @@ "auth.twofa_scratch_token_incorrect": "スクラッチコードが正しくありません。", "auth.twofa_required": "リポジトリにアクセスするには2段階認証を設定するか、再度ログインしてください。", "auth.login_userpass": "サインイン", + "auth.login_openid": "OpenID", "auth.oauth_signup_tab": "新規アカウント登録", "auth.oauth_signup_title": "新規アカウントの仕上げ", "auth.oauth_signup_submit": "アカウント登録完了", @@ -505,6 +521,7 @@ "form.Password": "パスワード", "form.Retype": "パスワード確認", "form.SSHTitle": "SSHキー名", + "form.HttpsUrl": "HTTPS URL", "form.PayloadUrl": "ペイロードのURL", "form.TeamName": "チーム名", "form.AuthName": "承認名", @@ -648,6 +665,7 @@ "settings.twofa": "2要素認証 (TOTP)", "settings.account_link": "連携アカウント", "settings.organization": "組織", + "settings.uid": "UID", "settings.webauthn": "2要素認証 (セキュリティキー)", "settings.public_profile": "公開プロフィール", "settings.biography_placeholder": "自己紹介してください!(Markdownを使うことができます)", @@ -740,6 +758,7 @@ "settings.add_email": "メールアドレスを追加", "settings.add_openid": "OpenID URIを追加する", "settings.add_email_confirmation_sent": "\"%s\" に確認メールを送信しました。 %s以内に受信トレイを確認し、メールアドレス確認を行ってください。", + "settings.email_primary_not_found": "選択したメールアドレスが見つかりませんでした。", "settings.add_email_success": "新しいメールアドレスを追加しました。", "settings.email_preference_set_success": "メール設定を保存しました。", "settings.add_openid_success": "新しいOpenIDアドレスを追加しました。", @@ -966,6 +985,7 @@ "repo.fork.blocked_user": "リポジトリのオーナーがあなたをブロックしているため、リポジトリをフォークできません。", "repo.use_template": "このテンプレートを使用", "repo.open_with_editor": "%s で開く", + "repo.download_directory_as": "%sとしてディレクトリをダウンロード", "repo.download_zip": "ZIPファイルをダウンロード", "repo.download_tar": "TAR.GZファイルをダウンロード", "repo.download_bundle": "バンドルをダウンロード", @@ -985,6 +1005,7 @@ "repo.multiple_licenses": "複数のライセンス", "repo.object_format": "オブジェクトのフォーマット", "repo.object_format_helper": "リポジトリのオブジェクトフォーマット。後で変更することはできません。SHA1 は最も互換性があります。", + "repo.readme": "README", "repo.readme_helper": "READMEファイル テンプレートを選択してください。", "repo.readme_helper_desc": "プロジェクトについての説明をひととおり書く場所です。", "repo.auto_init": "リポジトリの初期設定 (.gitignore、ライセンスファイル、READMEファイルの追加)", @@ -997,6 +1018,7 @@ "repo.default_branch": "デフォルトブランチ", "repo.default_branch_label": "デフォルト", "repo.default_branch_helper": "デフォルトブランチはプルリクエストとコードコミットのベースブランチとなります。", + "repo.mirror_prune": "Prune", "repo.mirror_prune_desc": "不要になった古いリモートトラッキング参照を削除", "repo.mirror_interval": "ミラー間隔 (有効な時間の単位は'h'、'm'、's')。 定期的な同期を無効にする場合は0。(最小間隔: %s)", "repo.mirror_interval_invalid": "ミラー間隔が不正です。", @@ -1006,6 +1028,7 @@ "repo.mirror_address_desc": "必要な資格情報は「認証」セクションに設定してください。", "repo.mirror_address_url_invalid": "入力したURLは無効です。 URLの構成要素はすべて正しくエスケープしてください。", "repo.mirror_address_protocol_invalid": "入力したURLは無効です。 ミラーできるのは、http(s):// または git:// からだけです。", + "repo.mirror_lfs": "Large File Storage (LFS)", "repo.mirror_lfs_desc": "LFS データのミラーリングを有効にする。", "repo.mirror_lfs_endpoint": "LFS エンドポイント", "repo.mirror_lfs_endpoint_desc": "同期するときは、クローンURLをもとにLFSサーバーを決定しようとします。 リポジトリのLFSデータがほかの場所に保存されている場合は、独自のエンドポイントを指定することができます。", @@ -1047,6 +1070,7 @@ "repo.desc.template": "テンプレート", "repo.desc.internal": "内部", "repo.desc.archived": "アーカイブ", + "repo.desc.sha256": "SHA256", "repo.template.items": "テンプレート項目", "repo.template.git_content": "Gitコンテンツ (デフォルトブランチ)", "repo.template.git_hooks": "Gitフック", @@ -1075,6 +1099,7 @@ "repo.migrate_options_lfs_endpoint.description.local": "ローカルサーバーのパスもサポートされています。", "repo.migrate_options_lfs_endpoint.placeholder": "空にするとエンドポイントはクローン URL から決定されます。", "repo.migrate_items": "移行する項目", + "repo.migrate_items_wiki": "Wiki", "repo.migrate_items_milestones": "マイルストーン", "repo.migrate_items_labels": "ラベル", "repo.migrate_items_issues": "イシュー", @@ -1124,6 +1149,7 @@ "repo.migration_status": "移行状況", "repo.mirror_from": "ミラー元", "repo.forked_from": "フォーク元", + "repo.generated_from": "generated from", "repo.fork_from_self": "自分が所有しているリポジトリはフォークできません。", "repo.fork_guest_user": "リポジトリをフォークするにはサインインしてください。", "repo.watch_guest_user": "リポジトリをウォッチするにはサインインしてください。", @@ -1157,6 +1183,7 @@ "repo.pulls": "プルリクエスト", "repo.projects": "プロジェクト", "repo.packages": "パッケージ", + "repo.actions": "Actions", "repo.labels": "ラベル", "repo.org_labels_desc": "組織で定義されているラベル (組織のすべてのリポジトリで使用可能なもの)", "repo.org_labels_desc_manage": "編集", @@ -1167,8 +1194,11 @@ "repo.release": "リリース", "repo.releases": "リリース", "repo.tag": "タグ", + "repo.git_tag": "Gitタグ", "repo.released_this": "がこれをリリース", "repo.tagged_this": "がタグ付け", + "repo.file.title": "%s at %s", + "repo.file_raw": "Raw", "repo.file_history": "履歴", "repo.file_view_source": "ソースを表示", "repo.file_view_rendered": "レンダリング表示", @@ -1204,6 +1234,7 @@ "repo.commit.contained_in_default_branch": "このコミットはデフォルトブランチに含まれています", "repo.commit.load_referencing_branches_and_tags": "このコミットを参照しているブランチやタグを取得", "repo.commit.merged_in_pr": "このコミットはプルリクエスト %s でマージされました。", + "repo.blame": "Blame", "repo.download_file": "ファイルをダウンロード", "repo.normal_view": "通常表示", "repo.line": "行", @@ -1467,6 +1498,7 @@ "repo.issues.filter_sort.feweststars": "スターが少ない順", "repo.issues.filter_sort.mostforks": "フォークが多い順", "repo.issues.filter_sort.fewestforks": "フォークが少ない順", + "repo.issues.quick_goto": "イシューへ移動", "repo.issues.action_open": "オープン", "repo.issues.action_close": "クローズ", "repo.issues.action_label": "ラベル", @@ -1627,6 +1659,7 @@ "repo.issues.push_commits_n": "が %d コミット追加 %s", "repo.issues.force_push_codes": "が %[1]s を強制プッシュ ( %[2]s から %[4]s へ ) %[6]s", "repo.issues.force_push_compare": "比較", + "repo.issues.due_date_form": "yyyy-mm-dd", "repo.issues.due_date_form_add": "期日の追加", "repo.issues.due_date_form_edit": "変更", "repo.issues.due_date_form_remove": "削除", @@ -1678,6 +1711,7 @@ "repo.issues.review.content.empty": "修正を指示するコメントを残す必要があります。", "repo.issues.review.reject": "が変更を要請 %s", "repo.issues.review.wait": "にレビュー依頼 %s", + "repo.issues.review.codeowners_rules": "CODEOWNERSルール", "repo.issues.review.add_review_request": "が %s にレビューを依頼 %s", "repo.issues.review.remove_review_request": "が %s へのレビュー依頼を取り消し %s", "repo.issues.review.remove_review_request_self": "がレビューを辞退 %s", @@ -1713,17 +1747,20 @@ "repo.issues.reference_link": "リファレンス: %s", "repo.compare.compare_base": "基準", "repo.compare.compare_head": "比較", + "repo.compare.title": "変更の比較", + "repo.compare.description": "ふたつのブランチまたはタグを選び、変更された内容を確認、あるいは新しいプルリクエストを開始してください。", "repo.pulls.desc": "プルリクエストとコードレビューの有効化。", "repo.pulls.new": "新しいプルリクエスト", + "repo.pulls.new.description": "この比較における変更点について議論し、レビューします。", "repo.pulls.new.blocked_user": "リポジトリのオーナーがあなたをブロックしているため、プルリクエストを作成できません。", "repo.pulls.new.must_collaborator": "プルリクエストを作成するには、共同作業者である必要があります。", + "repo.pulls.new.already_existed": "これらのブランチのプルリクエストはすでに存在します", "repo.pulls.edit.already_changed": "プルリクエストの変更を保存できません。 他のユーザーによって内容がすでに変更されているようです。 変更を上書きしないようにするため、ページを更新してからもう一度編集してください。", "repo.pulls.view": "プルリクエストを表示", "repo.pulls.compare_changes": "新規プルリクエスト", "repo.pulls.allow_edits_from_maintainers": "メンテナーからの編集を許可する", "repo.pulls.allow_edits_from_maintainers_desc": "ベースブランチへの書き込みアクセス権を持つユーザーは、このブランチにプッシュすることもできます", "repo.pulls.allow_edits_from_maintainers_err": "更新に失敗しました", - "repo.pulls.compare_changes_desc": "マージ先ブランチとプル元ブランチを選択。", "repo.pulls.has_viewed_file": "閲覧済", "repo.pulls.has_changed_since_last_review": "前回のレビュー後に変更あり", "repo.pulls.viewed_files_label": "%[1]d / %[2]d ファイル閲覧済み", @@ -1749,6 +1786,8 @@ "repo.pulls.title_desc": "が %[2]s から %[3]s への %[1]d コミットのマージを希望しています", "repo.pulls.merged_title_desc": "が %[1]d 個のコミットを %[2]s から %[3]s へマージ %[4]s", "repo.pulls.change_target_branch_at": "がターゲットブランチを %s から %s に変更 %s", + "repo.pulls.marked_as_work_in_progress_at": "がこのプルリクエストを作業中(WIP)とマーク %s", + "repo.pulls.marked_as_ready_for_review_at": "がこのプルリクエストをレビュー可とマーク %s", "repo.pulls.tab_conversation": "会話", "repo.pulls.tab_commits": "コミット", "repo.pulls.tab_files": "変更されたファイル", @@ -1767,6 +1806,7 @@ "repo.pulls.remove_prefix": "先頭の %s を除去", "repo.pulls.data_broken": "このプルリクエストは、フォークの情報が見つからないため壊れています。", "repo.pulls.files_conflicted": "このプルリクエストは、ターゲットブランチと競合する変更を含んでいます。", + "repo.pulls.files_conflicted_no_listed_files": "(競合するファイルはありません)", "repo.pulls.is_checking": "マージのコンフリクトを確認中…", "repo.pulls.is_ancestor": "このブランチは既にターゲットブランチに含まれています。マージするものはありません。", "repo.pulls.is_empty": "このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。", @@ -1821,7 +1861,8 @@ "repo.pulls.status_checking": "いくつかのステータスチェックが待機中です", "repo.pulls.status_checks_success": "ステータスチェックはすべて成功しました", "repo.pulls.status_checks_warning": "ステータスチェックにより警告が出ています", - "repo.pulls.status_checks_failure": "失敗したステータスチェックがあります", + "repo.pulls.status_checks_failure_required": "必須チェックに失敗しています", + "repo.pulls.status_checks_failure_optional": "必須ではないチェックが失敗しています", "repo.pulls.status_checks_error": "ステータスチェックによりエラーが出ています", "repo.pulls.status_checks_requested": "必須", "repo.pulls.status_checks_details": "詳細", @@ -1911,6 +1952,7 @@ "repo.signing.wont_sign.not_signed_in": "サインインしていません。", "repo.ext_wiki": "外部Wikiへのアクセス", "repo.ext_wiki.desc": "外部Wikiへのリンク。", + "repo.wiki": "Wiki", "repo.wiki.welcome": "Wikiへようこそ。", "repo.wiki.welcome_desc": "Wikiを使って共同作業者とドキュメンテーションの作成と共有ができます。", "repo.wiki.desc": "共同作業者とのドキュメンテーションの作成と共有。", @@ -1937,6 +1979,7 @@ "repo.wiki.page_name_desc": "この Wiki ページの名前を入力してください。いくつかの特別な名前として 'Home', '_Sidebar' と '_Footer' があります。", "repo.wiki.original_git_entry_tooltip": "フレンドリーリンクを使用する代わりにオリジナルのGitファイルを表示します。", "repo.activity": "アクティビティ", + "repo.activity.navbar.pulse": "Pulse", "repo.activity.navbar.code_frequency": "コード更新頻度", "repo.activity.navbar.contributors": "貢献者", "repo.activity.navbar.recent_commits": "最近のコミット", @@ -2089,6 +2132,8 @@ "repo.settings.pulls.ignore_whitespace": "空白文字のコンフリクトを無視する", "repo.settings.pulls.enable_autodetect_manual_merge": "手動マージの自動検出を有効にする (注意: 特殊なケースでは判定ミスが発生する場合があります)", "repo.settings.pulls.allow_rebase_update": "リベースでプルリクエストのブランチの更新を可能にする", + "repo.settings.pulls.default_target_branch": "新しいプルリクエストのデフォルトのターゲットブランチ", + "repo.settings.pulls.default_target_branch_default": "デフォルトブランチ (%s)", "repo.settings.pulls.default_delete_branch_after_merge": "デフォルトでプルリクエストのブランチをマージ後に削除する", "repo.settings.pulls.default_allow_edits_from_maintainers": "デフォルトでメンテナからの編集を許可する", "repo.settings.releases_desc": "リリースを有効にする", @@ -2210,6 +2255,8 @@ "repo.settings.add_webhook_desc": "GiteaはターゲットURLに、指定したContent TypeでPOSTリクエストを送ります。 詳細はWebhookガイドへ。", "repo.settings.payload_url": "ターゲットURL", "repo.settings.http_method": "HTTPメソッド", + "repo.settings.content_type": "POST Content Type", + "repo.settings.secret": "シークレット", "repo.settings.webhook_secret_desc": "Webhookサーバーがsecretの使用をサポートしている場合は、webhookのマニュアルに従いここにsecretを入力できます。", "repo.settings.slack_username": "ユーザー名", "repo.settings.slack_icon_url": "アイコンのURL", @@ -2227,6 +2274,7 @@ "repo.settings.event_delete_desc": "ブランチやタグが削除されたとき。", "repo.settings.event_fork": "フォーク", "repo.settings.event_fork_desc": "リポジトリがフォークされたとき。", + "repo.settings.event_wiki": "Wiki", "repo.settings.event_wiki_desc": "Wikiページが作成・名前変更・編集・削除されたとき。", "repo.settings.event_statuses": "ステータス", "repo.settings.event_statuses_desc": "APIによってコミットのステータスが更新されたとき。", @@ -2292,6 +2340,19 @@ "repo.settings.slack_domain": "ドメイン", "repo.settings.slack_channel": "チャンネル", "repo.settings.add_web_hook_desc": "%s をリポジトリと組み合わせます。", + "repo.settings.web_hook_name_gitea": "Gitea", + "repo.settings.web_hook_name_gogs": "Gogs", + "repo.settings.web_hook_name_slack": "Slack", + "repo.settings.web_hook_name_discord": "Discord", + "repo.settings.web_hook_name_dingtalk": "DingTalk", + "repo.settings.web_hook_name_telegram": "Telegram", + "repo.settings.web_hook_name_matrix": "Matrix", + "repo.settings.web_hook_name_msteams": "Microsoft Teams", + "repo.settings.web_hook_name_feishu_or_larksuite": "Feishu / Lark Suite", + "repo.settings.web_hook_name_feishu": "Feishu", + "repo.settings.web_hook_name_larksuite": "Lark Suite", + "repo.settings.web_hook_name_wechatwork": "WeCom (Wechat Work)", + "repo.settings.web_hook_name_packagist": "Packagist", "repo.settings.packagist_username": "Packagist ユーザー名", "repo.settings.packagist_api_token": "API トークン", "repo.settings.packagist_package_url": "Packagist パッケージ URL", @@ -2385,7 +2446,8 @@ "repo.settings.block_outdated_branch_desc": "baseブランチがheadブランチより進んでいる場合、マージできないようにします。", "repo.settings.block_admin_merge_override": "管理者もブランチ保護のルールに従う", "repo.settings.block_admin_merge_override_desc": "管理者はブランチ保護のルールに従う必要があり、回避することはできません。", - "repo.settings.default_branch_desc": "プルリクエストやコミット表示のデフォルトのブランチを選択:", + "repo.settings.default_branch_desc": "コミット表示のデフォルトのブランチを選択します。", + "repo.settings.default_target_branch_desc": "プルリクエストでは、リポジトリ拡張設定の「プルリクエスト」セクションで設定することで、別のデフォルトターゲットブランチを使用できます。", "repo.settings.merge_style_desc": "マージ スタイル", "repo.settings.default_merge_style_desc": "デフォルトのマージスタイル", "repo.settings.choose_branch": "ブランチを選択…", @@ -2437,6 +2499,7 @@ "repo.settings.unarchive.success": "リポジトリのアーカイブを解除しました。", "repo.settings.unarchive.error": "リポジトリのアーカイブ解除でエラーが発生しました。 詳細はログを確認してください。", "repo.settings.update_avatar_success": "リポジトリのアバターを更新しました。", + "repo.settings.lfs": "LFS", "repo.settings.lfs_filelist": "このリポジトリに含まれているLFSファイル", "repo.settings.lfs_no_lfs_files": "このリポジトリにLFSファイルはありません", "repo.settings.lfs_findcommits": "コミットを検索", @@ -2455,6 +2518,8 @@ "repo.settings.lfs_lock_file_no_exist": "ロックしたファイルがデフォルトブランチにありません", "repo.settings.lfs_force_unlock": "強制ロック解除", "repo.settings.lfs_pointers.found": "%d件のblobポインタ — 登録済 %d件、未登録 %d件 (実体ファイルなし %d件)", + "repo.settings.lfs_pointers.sha": "Blob SHA", + "repo.settings.lfs_pointers.oid": "OID", "repo.settings.lfs_pointers.inRepo": "Repo内", "repo.settings.lfs_pointers.exists": "実ファイルあり", "repo.settings.lfs_pointers.accessible": "アクセス可", @@ -2468,6 +2533,7 @@ "repo.diff.browse_source": "ソースを参照", "repo.diff.parent": "親", "repo.diff.commit": "コミット", + "repo.diff.git-notes": "Notes", "repo.diff.data_not_available": "差分はありません", "repo.diff.options_button": "差分オプション", "repo.diff.download_patch": "Patchファイルをダウンロード", @@ -2494,6 +2560,8 @@ "repo.diff.too_many_files": "変更されたファイルが多すぎるため、一部のファイルは表示されません", "repo.diff.show_more": "さらに表示", "repo.diff.load": "差分を読み込み", + "repo.diff.generated": "生成ファイル", + "repo.diff.vendored": "ベンダーファイル", "repo.diff.comment.add_line_comment": "行コメントを追加", "repo.diff.comment.placeholder": "コメントを残す", "repo.diff.comment.add_single_comment": "単独のコメントを追加", @@ -2508,6 +2576,7 @@ "repo.diff.review.self_reject": "プルリクエストの作成者は自分のプルリクエストで変更要請できません", "repo.diff.review.reject": "変更要請", "repo.diff.review.self_approve": "プルリクエストの作成者は自分のプルリクエストを承認できません", + "repo.diff.committed_by": "committed by", "repo.diff.protected": "保護されているファイル", "repo.diff.image.side_by_side": "並べて表示", "repo.diff.image.swipe": "スワイプ", @@ -2566,6 +2635,13 @@ "repo.release.add_tag": "タグのみ作成", "repo.release.releases_for": "%s のリリース", "repo.release.tags_for": "%s のタグ", + "repo.release.notes": "リリースノート", + "repo.release.generate_notes": "リリースノートを生成", + "repo.release.generate_notes_desc": "マージされたプルリクエストと変更履歴のリンクを自動的に追加します。", + "repo.release.previous_tag": "前回のタグ", + "repo.release.generate_notes_tag_not_found": "このリポジトリにタグ \"%s\" は存在しません。", + "repo.release.generate_notes_target_not_found": "リリースターゲット \"%s\" が見つかりません。", + "repo.release.generate_notes_missing_tag": "リリースノートを生成するタグ名を入力してください。", "repo.branch.name": "ブランチ名", "repo.branch.already_exists": "ブランチ \"%s\" は既に存在します。", "repo.branch.delete_head": "削除", @@ -2585,7 +2661,7 @@ "repo.branch.restore_success": "ブランチ \"%s\" を復元しました。", "repo.branch.restore_failed": "ブランチ \"%s\" の復元に失敗しました。", "repo.branch.protected_deletion_failed": "ブランチ \"%s\" は保護されています。 削除できません。", - "repo.branch.default_deletion_failed": "ブランチ \"%s\" はデフォルトブランチです。 削除できません。", + "repo.branch.default_deletion_failed": "ブランチ \"%s\" は、デフォルトブランチまたはプルリクエストのターゲットブランチです。 削除できません。", "repo.branch.default_branch_not_exist": "デフォルトブランチ \"%s\" がありません。", "repo.branch.restore": "ブランチ \"%s\" の復元", "repo.branch.download": "ブランチ \"%s\" をダウンロード", @@ -2602,7 +2678,7 @@ "repo.branch.new_branch_from": "\"%s\" から新しいブランチを作成", "repo.branch.renamed": "ブランチ %s は %s にリネームされました。", "repo.branch.rename_default_or_protected_branch_error": "デフォルトブランチや保護ブランチのリネームが可能なのは管理者だけです。", - "repo.branch.rename_protected_branch_failed": "このブランチはglobベースの保護ルールに従って保護されています。", + "repo.branch.rename_protected_branch_failed": "ブランチ保護ルールにより、ブランチ名の変更は失敗しました。", "repo.branch.commits_divergence_from": "コミットの乖離: %[3]s より %[1]d 件遅れ %[2]d 件先行", "repo.branch.commits_no_divergence": "%[1]s ブランチと一致", "repo.tag.create_tag": "タグ %s を作成", @@ -2809,6 +2885,7 @@ "admin.dashboard.task.finished": "タスク: %[2]s が開始したタスク %[1]s が完了", "admin.dashboard.task.unknown": "不明なタスクです: %[1]s", "admin.dashboard.cron.started": "Cronを開始しました: %[1]s", + "admin.dashboard.cron.process": "Cron: %[1]s", "admin.dashboard.cron.cancelled": "Cron: %[1]s をキャンセル: %[3]s", "admin.dashboard.cron.error": "Cronでエラー: %s: %[3]s", "admin.dashboard.cron.finished": "Cron: %[1]s が完了", @@ -2886,7 +2963,9 @@ "admin.users.admin": "管理者", "admin.users.restricted": "制限あり", "admin.users.reserved": "予約済み", + "admin.users.bot": "Bot", "admin.users.remote": "リモート", + "admin.users.2fa": "2FA", "admin.users.repos": "リポジトリ", "admin.users.created": "作成日", "admin.users.last_login": "前回のサインイン", @@ -3008,6 +3087,7 @@ "admin.auths.attribute_mail": "メールアドレス", "admin.auths.attribute_ssh_public_key": "SSH公開鍵", "admin.auths.attribute_avatar": "アバター", + "admin.auths.ssh_keys_are_verified": "LDAPからのSSHキーを検証済みとする", "admin.auths.attributes_in_bind": "バインドDNのコンテクストから属性を取得する", "admin.auths.allow_deactivate_all": "サーチ結果が空のときは全ユーザーを非アクティブ化", "admin.auths.use_paged_search": "ページ分割検索を使用", @@ -3141,6 +3221,7 @@ "admin.config.db_name": "データベース名", "admin.config.db_user": "ユーザー名", "admin.config.db_schema": "スキーマ", + "admin.config.db_ssl_mode": "SSL", "admin.config.db_path": "パス", "admin.config.service_config": "サービス設定", "admin.config.register_email_confirm": "登録にはメールによる確認が必要", @@ -3179,6 +3260,7 @@ "admin.config.mailer_sendmail_path": "Sendmailのパス", "admin.config.mailer_sendmail_args": "Sendmailの追加引数", "admin.config.mailer_sendmail_timeout": "Sendmail のタイムアウト", + "admin.config.mailer_use_dummy": "Dummy", "admin.config.test_email_placeholder": "メールアドレス (例 test@example.com)", "admin.config.send_test_mail": "テストメールを送信", "admin.config.send_test_mail_submit": "送信", @@ -3217,8 +3299,6 @@ "admin.config.git_gc_args": "GC引数", "admin.config.git_migrate_timeout": "移行タイムアウト", "admin.config.git_mirror_timeout": "ミラー更新タイムアウト", - "admin.config.git_clone_timeout": "クローン操作のタイムアウト", - "admin.config.git_pull_timeout": "プル操作のタイムアウト", "admin.config.git_gc_timeout": "GC操作のタイムアウト", "admin.config.log_config": "ログ設定", "admin.config.logger_name_fmt": "ロガー: %s", @@ -3393,6 +3473,7 @@ "packages.assets": "アセット", "packages.versions": "バージョン", "packages.versions.view_all": "すべて表示", + "packages.dependency.id": "ID", "packages.dependency.version": "バージョン", "packages.search_in_external_registry": "%s で検索", "packages.alpine.registry": "あなたの /etc/apk/repositories ファイルにURLを追加して、このレジストリをセットアップします:", @@ -3402,10 +3483,12 @@ "packages.alpine.repository": "リポジトリ情報", "packages.alpine.repository.branches": "ブランチ", "packages.alpine.repository.repositories": "リポジトリ", + "packages.alpine.repository.architectures": "Architectures", "packages.arch.registry": "/etc/pacman.conf にリポジトリとアーキテクチャを含めてサーバーを追加します:", "packages.arch.install": "pacmanでパッケージを同期します:", "packages.arch.repository": "リポジトリ情報", "packages.arch.repository.repositories": "リポジトリ", + "packages.arch.repository.architectures": "Architectures", "packages.cargo.registry": "Cargo 設定ファイルでこのレジストリをセットアップします。(例 ~/.cargo/config.toml):", "packages.cargo.install": "Cargo を使用してパッケージをインストールするには、次のコマンドを実行します:", "packages.chef.registry": "あなたの ~/.chef/config.rb ファイルに、このレジストリをセットアップします:", @@ -3435,6 +3518,9 @@ "packages.debian.registry.info": "$distribution と $component は下にあるリストから選んでください。", "packages.debian.install": "パッケージをインストールするには、次のコマンドを実行します:", "packages.debian.repository": "リポジトリ情報", + "packages.debian.repository.distributions": "Distributions", + "packages.debian.repository.components": "Components", + "packages.debian.repository.architectures": "Architectures", "packages.generic.download": "コマンドラインでパッケージをダウンロードします:", "packages.go.install": "コマンドラインでパッケージをインストール:", "packages.helm.registry": "このレジストリをコマンドラインからセットアップします:", @@ -3463,6 +3549,7 @@ "packages.rpm.distros.suse": "SUSE系ディストリビューションの場合", "packages.rpm.install": "パッケージをインストールするには、次のコマンドを実行します:", "packages.rpm.repository": "リポジトリ情報", + "packages.rpm.repository.architectures": "Architectures", "packages.rpm.repository.multiple_groups": "このパッケージは複数のグループで利用可能です。", "packages.rubygems.install": "gem を使用してパッケージをインストールするには、次のコマンドを実行します:", "packages.rubygems.install2": "または Gemfile に追加します:", @@ -3536,6 +3623,7 @@ "secrets.deletion.success": "シークレットを削除しました。", "secrets.deletion.failed": "シークレットの削除に失敗しました。", "secrets.management": "シークレット管理", + "actions.actions": "Actions", "actions.unit.desc": "Actionsの管理", "actions.status.unknown": "不明", "actions.status.waiting": "待機中", @@ -3550,6 +3638,7 @@ "actions.runners.new": "新しいランナーを作成", "actions.runners.new_notice": "ランナーの開始方法", "actions.runners.status": "ステータス", + "actions.runners.id": "ID", "actions.runners.name": "名称", "actions.runners.owner_type": "タイプ", "actions.runners.description": "説明", @@ -3584,6 +3673,7 @@ "actions.runs.all_workflows": "すべてのワークフロー", "actions.runs.commit": "コミット", "actions.runs.scheduled": "スケジュール済み", + "actions.runs.pushed_by": "pushed by", "actions.runs.invalid_workflow_helper": "ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s", "actions.runs.no_matching_online_runner_helper": "ラベルに一致するオンラインのランナーが見つかりません: %s", "actions.runs.no_job_without_needs": "ワークフローには依存関係のないジョブが少なくとも1つ含まれている必要があります。", @@ -3648,6 +3738,7 @@ "projects.type-3.display_name": "組織プロジェクト", "projects.enter_fullscreen": "フルスクリーン", "projects.exit_fullscreen": "フルスクリーンを終了", + "git.filemode.changed_filemode": "%[1]s → %[2]s", "git.filemode.directory": "ディレクトリ", "git.filemode.normal_file": "ノーマルファイル", "git.filemode.executable_file": "実行可能ファイル", From 08d98456352b7311846421364ee97ae428d4ca4d Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Mon, 16 Feb 2026 02:42:22 +0100 Subject: [PATCH 14/23] use proper subaddress (#36639) --- models/user/user.go | 8 +++++--- models/user/user_test.go | 6 +++--- tests/integration/editor_test.go | 2 +- tests/integration/repofiles_change_test.go | 12 ++++++------ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/models/user/user.go b/models/user/user.go index 38042631de..efed73e99d 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -213,7 +213,7 @@ func (u *User) SetLastLogin() { // GetPlaceholderEmail returns an noreply email func (u *User) GetPlaceholderEmail() string { - return fmt.Sprintf("%s+%d@%s", u.LowerName, u.ID, setting.Service.NoReplyAddress) + return fmt.Sprintf("%d+%s@%s", u.ID, u.LowerName, setting.Service.NoReplyAddress) } // GetEmail returns a noreply email, if the user has set to keep his @@ -1279,13 +1279,15 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro return &EmailUserMap{results}, nil } -// parseLocalPartToNameID attempts to unparse local-part of email that's in format user+id +// parseLocalPartToNameID attempts to unparse local-part of email that's in format id+user // returns user and id if possible func parseLocalPartToNameID(localPart string) (string, int64) { var id int64 - name, idstr, hasPlus := strings.Cut(localPart, "+") + idstr, name, hasPlus := strings.Cut(localPart, "+") if hasPlus { id, _ = strconv.ParseInt(idstr, 10, 64) + } else { + name = idstr } return name, id } diff --git a/models/user/user_test.go b/models/user/user_test.go index 378acc4180..956eaeafb4 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -66,9 +66,9 @@ func TestUserEmails(t *testing.T) { {"UseR1@example.com", 1}, {"user1-2@example.COM", 1}, {"USER2@" + setting.Service.NoReplyAddress, 2}, - {"user2+2@" + setting.Service.NoReplyAddress, 2}, - {"oldUser2UsernameWhichDoesNotMatterForQuery+2@" + setting.Service.NoReplyAddress, 2}, - {"badUser+99999@" + setting.Service.NoReplyAddress, 0}, + {"2+user2@" + setting.Service.NoReplyAddress, 2}, + {"2+oldUser2UsernameWhichDoesNotMatterForQuery@" + setting.Service.NoReplyAddress, 2}, + {"99999+badUser@" + setting.Service.NoReplyAddress, 0}, {"user4@example.com", 4}, {"no-such", 0}, } diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index 8368c91258..ac106bc776 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -258,7 +258,7 @@ func testEditorWebGitCommitEmail(t *testing.T) { t.Run("DefaultEmailKeepPrivate", func(t *testing.T) { defer tests.PrintCurrentTest(t)() paramsForKeepPrivate["commit_email"] = "" - resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2+2@noreply.example.org") + resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "2+user2@noreply.example.org") }) t.Run("ChooseEmail", func(t *testing.T) { defer tests.PrintCurrentTest(t)() diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index 390ec2ebeb..5de973aacc 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -132,14 +132,14 @@ func getExpectedFileResponseForRepoFilesCreate(commitID string, lastCommit *git. Author: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2+2@noreply.example.org", + Email: "2+user2@noreply.example.org", }, Date: time.Now().UTC().Format(time.RFC3339), }, Committer: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2+2@noreply.example.org", + Email: "2+user2@noreply.example.org", }, Date: time.Now().UTC().Format(time.RFC3339), }, @@ -202,14 +202,14 @@ func getExpectedFileResponseForRepoFilesUpdate(commitID, filename, lastCommitSHA Author: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2+2@noreply.example.org", + Email: "2+user2@noreply.example.org", }, Date: time.Now().UTC().Format(time.RFC3339), }, Committer: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2+2@noreply.example.org", + Email: "2+user2@noreply.example.org", }, Date: time.Now().UTC().Format(time.RFC3339), }, @@ -312,13 +312,13 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str Author: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2+2@noreply.example.org", + Email: "2+user2@noreply.example.org", }, }, Committer: &api.CommitUser{ Identity: api.Identity{ Name: "User Two", - Email: "user2+2@noreply.example.org", + Email: "2+user2@noreply.example.org", }, }, Parents: []*api.CommitMeta{ From 258754f2997ad4db11d198b72d9c9fe6af9fbcb3 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 16 Feb 2026 10:11:02 +0800 Subject: [PATCH 15/23] Fix chroma lexer mapping (#36629) Fix some edge cases for ".hcl" and ".v" files, and add more tests --- modules/highlight/lexerdetect.go | 52 ++++++++++++++++++++------- modules/highlight/lexerdetect_test.go | 34 +++++++++++++++--- 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/modules/highlight/lexerdetect.go b/modules/highlight/lexerdetect.go index 5b39617566..5d98578f35 100644 --- a/modules/highlight/lexerdetect.go +++ b/modules/highlight/lexerdetect.go @@ -21,7 +21,8 @@ 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 + conflictingExtLangMap map[string]string + conflictingAliasLangMap map[string]string lowerNameMap map[string]chroma.Lexer // lexer name (lang name) in lower-case fileBaseMap map[string]chroma.Lexer @@ -36,9 +37,9 @@ var chromaLexers = sync.OnceValue(func() (ret struct { ret.fileBaseMap = make(map[string]chroma.Lexer) ret.fileExtMap = make(map[string]chroma.Lexer) - // Chroma has overlaps in file extension for different languages, + // Chroma has conflicts 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. + // So we can only choose some default languages for the conflicted file extensions. ret.conflictingExtLangMap = map[string]string{ ".as": "ActionScript 3", // ActionScript ".asm": "NASM", // TASM, NASM, RGBDS Assembly, Z80 Assembly @@ -71,12 +72,17 @@ var chromaLexers = sync.OnceValue(func() (ret struct { ".v": "V", // verilog ".xslt": "HTML", // XML } + // use widely used language names as the default mapping to resolve name alias conflict + ret.conflictingAliasLangMap = map[string]string{ + "hcl": "HCL", // Terraform + "v": "V", // verilog + } 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) { + setFileNameMapWithLowerKey := 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") } @@ -87,7 +93,7 @@ var chromaLexers = sync.OnceValue(func() (ret struct { processFileName := func(fileName string, lexer chroma.Lexer) bool { if isPlainPattern(fileName) { // full base name match - setMapWithLowerKey(ret.fileBaseMap, fileName, lexer) + setFileNameMapWithLowerKey(ret.fileBaseMap, fileName, lexer) return true } if strings.HasPrefix(fileName, "*") { @@ -96,7 +102,7 @@ var chromaLexers = sync.OnceValue(func() (ret struct { if isPlainPattern(fileExt) { presetName := ret.conflictingExtLangMap[fileExt] if presetName == "" || lexer.Config().Name == presetName { - setMapWithLowerKey(ret.fileExtMap, fileExt, lexer) + setFileNameMapWithLowerKey(ret.fileExtMap, fileExt, lexer) } return true } @@ -134,13 +140,30 @@ var chromaLexers = sync.OnceValue(func() (ret struct { return patterns } - // add lexers to our map, for fast lookup + processLexerNameAliases := func(lexer chroma.Lexer) { + cfg := lexer.Config() + lowerName := strings.ToLower(cfg.Name) + if _, conflicted := ret.lowerNameMap[lowerName]; conflicted { + panic("duplicate language name in lexer map: " + lowerName) + } + ret.lowerNameMap[lowerName] = lexer + + for _, name := range cfg.Aliases { + lowerName := strings.ToLower(name) + if overriddenName, overridden := ret.conflictingAliasLangMap[lowerName]; overridden && overriddenName != cfg.Name { + continue + } + if existingLexer, conflict := ret.lowerNameMap[lowerName]; conflict && existingLexer.Config().Name != cfg.Name { + panic("duplicate alias in lexer map: " + name + ", conflict between " + existingLexer.Config().Name + " and " + cfg.Name) + } + ret.lowerNameMap[lowerName] = lexer + } + } + + // the main loop: build our lookup maps for lexers 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 - } + processLexerNameAliases(lexer) for _, s := range expandGlobPatterns(cfg.Filenames) { if !processFileName(s, lexer) { panic("unsupported file name pattern in lexer: " + s) @@ -153,7 +176,12 @@ var chromaLexers = sync.OnceValue(func() (ret struct { } } - // final check: make sure the default ext-lang mapping is correct, nothing is missing + // final check: make sure the default overriding mapping is correct, nothing is missing + for lowerName, lexerName := range ret.conflictingAliasLangMap { + if lexer, ok := ret.lowerNameMap[lowerName]; !ok || lexer.Config().Name != lexerName { + panic("missing default name-lang mapping for: " + lowerName) + } + } 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) diff --git a/modules/highlight/lexerdetect_test.go b/modules/highlight/lexerdetect_test.go index 868e793a68..a06053be0c 100644 --- a/modules/highlight/lexerdetect_test.go +++ b/modules/highlight/lexerdetect_test.go @@ -45,7 +45,7 @@ func BenchmarkRenderCodeByLexer(b *testing.B) { lexer := DetectChromaLexerByFileName("a.sql", "") b.StartTimer() for b.Loop() { - // Really slow ....... + // Really slow ....... the regexp2 used by Chroma takes most of the time // BenchmarkRenderCodeByLexer-12 22 47159038 ns/op RenderCodeByLexer(lexer, code) } @@ -55,13 +55,14 @@ func TestDetectChromaLexer(t *testing.T) { globalVars().highlightMapping[".my-html"] = "HTML" t.Cleanup(func() { delete(globalVars().highlightMapping, ".my-html") }) - cases := []struct { + casesWithContent := []struct { fileName string language string content string expected string }{ - {"test.py", "", "", "Python"}, + {"test.v", "", "", "V"}, + {"test.v", "any-lang-name", "", "V"}, {"any-file", "javascript", "", "JavaScript"}, {"any-file", "", "/* vim: set filetype=python */", "Python"}, @@ -80,11 +81,36 @@ func TestDetectChromaLexer(t *testing.T) { {"a.sql", "", "", "SQL"}, {"dhcpd.conf", "", "", "ISCdhcpd"}, {".env.my-production", "", "", "Bash"}, + + {"a.hcl", "", "", "HCL"}, // not the same as Chroma, enry detects "*.hcl" as "HCL" + {"a.hcl", "HCL", "", "HCL"}, + {"a.hcl", "Terraform", "", "Terraform"}, } - for _, c := range cases { + for _, c := range casesWithContent { 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) } } + + casesNameLang := []struct { + fileName string + language string + expected string + byLang bool + }{ + {"a.v", "", "V", false}, + {"a.v", "V", "V", true}, + {"a.v", "verilog", "verilog", true}, + {"a.v", "any-lang-name", "V", false}, + + {"a.hcl", "", "Terraform", false}, // not the same as enry + {"a.hcl", "HCL", "HCL", true}, + {"a.hcl", "Terraform", "Terraform", true}, + } + for _, c := range casesNameLang { + lexer, byLang := detectChromaLexerByFileName(c.fileName, c.language) + assert.Equal(t, c.expected, lexer.Config().Name, "case: %+v", c) + assert.Equal(t, c.byLang, byLang, "case: %+v", c) + } } From 8fdda2dd83f84dca6312f01018dd0a93f294adbb Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 16 Feb 2026 04:32:46 +0100 Subject: [PATCH 16/23] Fix linguist-detectable attribute being ignored for configuration files (#36640) Fixes: go-gitea/gitea#36637. `linguist-detectable` must be able to override the config classification. --------- Co-authored-by: Claude Opus 4.6 --- modules/git/languagestats/language_stats_gogit.go | 2 +- modules/git/languagestats/language_stats_nogogit.go | 2 +- tests/integration/linguist_test.go | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/git/languagestats/language_stats_gogit.go b/modules/git/languagestats/language_stats_gogit.go index 418c05b157..ec03ca3159 100644 --- a/modules/git/languagestats/language_stats_gogit.go +++ b/modules/git/languagestats/language_stats_gogit.go @@ -108,7 +108,7 @@ func GetLanguageStats(repo *git_module.Repository, commitID string) (map[string] if (!isVendored.Has() && analyze.IsVendor(f.Name)) || enry.IsDotFile(f.Name) || (!isDocumentation.Has() && enry.IsDocumentation(f.Name)) || - enry.IsConfiguration(f.Name) { + (!isDetectable.Has() && enry.IsConfiguration(f.Name)) { return nil } diff --git a/modules/git/languagestats/language_stats_nogogit.go b/modules/git/languagestats/language_stats_nogogit.go index 1dbf184af6..442313d495 100644 --- a/modules/git/languagestats/language_stats_nogogit.go +++ b/modules/git/languagestats/language_stats_nogogit.go @@ -132,7 +132,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, if (!isVendored.Has() && analyze.IsVendor(f.Name())) || enry.IsDotFile(f.Name()) || (!isDocumentation.Has() && enry.IsDocumentation(f.Name())) || - enry.IsConfiguration(f.Name()) { + (!isDetectable.Has() && enry.IsConfiguration(f.Name())) { continue } diff --git a/tests/integration/linguist_test.go b/tests/integration/linguist_test.go index 2c4c74d2a7..8f924f0ffb 100644 --- a/tests/integration/linguist_test.go +++ b/tests/integration/linguist_test.go @@ -214,6 +214,17 @@ func TestLinguist(t *testing.T) { }, ExpectedLanguageOrder: []string{"Markdown"}, }, + // case 14: linguist-detectable on a configuration/data file (YAML) without linguist-language + { + GitAttributesContent: "*.yaml linguist-detectable", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "config.yaml", + ContentReader: strings.NewReader("name: test\ndescription: A test yaml file\n"), + }, + }, + ExpectedLanguageOrder: []string{"YAML"}, + }, } for i, c := range cases { From d9d66d04d0dcd737592dbdaa5369210d47d188fe Mon Sep 17 00:00:00 2001 From: Alessandro Ferrari <48022579+alessandroferra@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:13:04 +0100 Subject: [PATCH 17/23] fix: duplicate startup warnings in admin panel (#36641) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #36630 ## Problem `StartupProblems` warnings (from `deprecatedSetting` and other `LogStartupProblem` calls) appear twice in the admin panel at `/-/admin` and `/-/admin/self_check`. `LoadCommonSettings()` is called twice during web server startup: 1. Early init via `cmd/main.go` → `InitWorkPathAndCommonConfig` → `LoadCommonSettings()` 2. Web server startup via `cmd/web.go` → `serveInstalled` → `LoadCommonSettings()` The second call re-initializes the config provider first (`InitCfgProvider`), but `StartupProblems` and `configuredPaths` are never cleared between loads, so every warning gets appended twice. ## Fix Clear `StartupProblems` and `configuredPaths` at the start of `LoadCommonSettings()` so only the final load's warnings are retained. This approach was chosen over clearing in `InitCfgProvider` because: - Warnings are produced during settings load, not provider init - Some callers set `CfgProvider` directly without calling `InitCfgProvider` - It avoids coupling correctness to a specific call ordering ## Screenshots **Result** (single warning as expected): Screenshot From 2026-02-16 01-27-01 ## testing [x] Added `TestLoadCommonSettingsClearsStartupProblems` — verifies no duplicate messages after consecutive loads [x] Added `TestLoadCommonSettingsClearsConfiguredPaths` — verifies path overlap map is identical after consecutive loads [x] All existing `modules/setting` tests pass [x] Manually verified in admin panel with deprecated `[oauth2].ENABLE` setting --------- Signed-off-by: silverwind Co-authored-by: silverwind Co-authored-by: wxiaoguang --- cmd/web.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/web.go b/cmd/web.go index 61cfb87130..5000e780c5 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -163,8 +163,6 @@ func serveInstall(cmd *cli.Command) error { } func serveInstalled(c *cli.Command) error { - setting.InitCfgProvider(setting.CustomConf) - setting.LoadCommonSettings() setting.MustInstalled() showWebStartupMessage("Prepare to run web server") From a0160694b90d131ebebf8b1fe435ff486a0d19cc Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 16 Feb 2026 10:57:18 +0100 Subject: [PATCH 18/23] Enable `nilnil` linter for new code (#36591) Fixes: https://github.com/go-gitea/gitea/issues/36152 Enable the `nilnil` linter while adding `//nolint` comments to existing violations. This will ensure no new issues enter the code base while we can fix existing issues gradually. Co-authored-by: Claude Opus 4.6 --- .golangci.yml | 1 + models/activities/action_list.go | 2 +- models/asymkey/gpg_key_commit_verification.go | 4 ++-- models/auth/oauth2.go | 8 ++++---- models/db/collation.go | 2 +- models/dbfs/dbfile.go | 2 +- models/git/lfs_lock.go | 4 ++-- models/git/protected_branch.go | 4 ++-- models/git/protected_tag.go | 4 ++-- models/issues/issue.go | 2 +- models/issues/review.go | 2 +- models/organization/org_user.go | 4 ++-- models/repo/archiver.go | 2 +- models/repo/fork.go | 2 +- models/repo/repo.go | 2 +- models/repo/topic.go | 2 +- models/unittest/fixtures_loader.go | 2 +- models/unittest/fixtures_test.go | 2 +- models/user/block.go | 2 +- models/user/email_address.go | 2 +- models/user/list.go | 2 +- models/user/user.go | 2 +- modules/git/commit_submodule.go | 4 ++-- modules/git/foreachref/parser.go | 2 +- modules/git/last_commit_cache.go | 4 ++-- modules/git/log_name_status.go | 4 ++-- modules/graceful/net_unix.go | 4 ++-- modules/optional/serialization.go | 2 +- routers/api/packages/auth.go | 4 ++-- routers/api/packages/chef/auth.go | 4 ++-- routers/api/packages/swift/swift.go | 2 +- routers/common/compare.go | 4 ++-- routers/web/repo/setting/secrets.go | 2 +- routers/web/shared/actions/runners.go | 2 +- routers/web/shared/actions/variables.go | 2 +- services/actions/context.go | 2 +- services/actions/job_emitter.go | 2 +- services/auth/auth_token.go | 2 +- services/auth/basic.go | 6 +++--- services/auth/httpsign.go | 8 ++++---- services/auth/oauth2.go | 4 ++-- services/auth/reverseproxy.go | 4 ++-- services/auth/session.go | 8 ++++---- services/auth/source/oauth2/providers_test.go | 4 ++-- services/auth/sspi.go | 6 +++--- services/contexttest/context_tests.go | 2 +- services/gitdiff/csv.go | 2 +- services/issue/assignee.go | 2 +- services/issue/commit.go | 2 +- services/issue/issue.go | 2 +- services/packages/auth.go | 2 +- services/packages/cargo/index.go | 2 +- services/pull/check.go | 2 +- services/pull/comment.go | 4 ++-- services/pull/review.go | 2 +- services/repository/archiver/archiver.go | 2 +- services/repository/files/update.go | 2 +- 57 files changed, 86 insertions(+), 85 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 62c1d005fa..ba4f81b521 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,6 +18,7 @@ linters: - mirror - modernize - nakedret + - nilnil - nolintlint - perfsprint - revive diff --git a/models/activities/action_list.go b/models/activities/action_list.go index b52cf7ee49..29ff2fdf7a 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -30,7 +30,7 @@ func (actions ActionList) getUserIDs() []int64 { func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) { if len(actions) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // returns nil when there are no actions } userIDs := actions.getUserIDs() diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go index 375b703f7b..ae5192de9f 100644 --- a/models/asymkey/gpg_key_commit_verification.go +++ b/models/asymkey/gpg_key_commit_verification.go @@ -70,7 +70,7 @@ func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, e // We will ignore errors in verification as they don't need to be propagated up err = verifySign(sig, hash, k) if err != nil { - return nil, nil + return nil, nil //nolint:nilnil // verification failed, not an error } return k, nil } @@ -86,7 +86,7 @@ func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) return verified, err } } - return nil, nil + return nil, nil //nolint:nilnil // verification failed, not an error } func HashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification { diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index d664841306..2f5aff0933 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -209,7 +209,7 @@ func (app *OAuth2Application) GetGrantByUserID(ctx context.Context, userID int64 if has, err := db.GetEngine(ctx).Where("user_id = ? AND application_id = ?", userID, app.ID).Get(grant); err != nil { return nil, err } else if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return grant, nil } @@ -431,13 +431,13 @@ func GetOAuth2AuthorizationByCode(ctx context.Context, code string) (auth *OAuth if has, err := db.GetEngine(ctx).Where("code = ?", code).Get(auth); err != nil { return nil, err } else if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } auth.Grant = new(OAuth2Grant) if has, err := db.GetEngine(ctx).ID(auth.GrantID).Get(auth.Grant); err != nil { return nil, err } else if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return auth, nil } @@ -521,7 +521,7 @@ func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err if has, err := db.GetEngine(ctx).ID(id).Get(grant); err != nil { return nil, err } else if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return grant, err } diff --git a/models/db/collation.go b/models/db/collation.go index 79ade87380..203f7cbfe4 100644 --- a/models/db/collation.go +++ b/models/db/collation.go @@ -98,7 +98,7 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) { return nil, err } } else { - return nil, nil + return nil, nil //nolint:nilnil // return nil for unsupported database types } if res.DatabaseCollation == "" { diff --git a/models/dbfs/dbfile.go b/models/dbfs/dbfile.go index eaf506fbe6..ccb13583e1 100644 --- a/models/dbfs/dbfile.go +++ b/models/dbfs/dbfile.go @@ -339,7 +339,7 @@ func findFileMetaByID(ctx context.Context, metaID int64) (*dbfsMeta, error) { } else if ok { return &fileMeta, nil } - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } func buildPath(path string) string { diff --git a/models/git/lfs_lock.go b/models/git/lfs_lock.go index aabed6b7fa..8e63598fb2 100644 --- a/models/git/lfs_lock.go +++ b/models/git/lfs_lock.go @@ -130,7 +130,7 @@ func GetLFSLockByRepoID(ctx context.Context, repoID int64, page, pageSize int) ( // GetTreePathLock returns LSF lock for the treePath func GetTreePathLock(ctx context.Context, repoID int64, treePath string) (*LFSLock, error) { if !setting.LFS.StartServer { - return nil, nil + return nil, nil //nolint:nilnil // return nil when LFS is not started } locks, err := GetLFSLockByRepoID(ctx, repoID, 0, 0) @@ -142,7 +142,7 @@ func GetTreePathLock(ctx context.Context, repoID int64, treePath string) (*LFSLo return lock, nil } } - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } // CountLFSLockByRepoID returns a count of all LFSLocks associated with a repository. diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index 1085c14cae..f4567a0aac 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -318,7 +318,7 @@ func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName st if err != nil { return nil, err } else if !exist { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return rel, nil } @@ -329,7 +329,7 @@ func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*Pro if err != nil { return nil, err } else if !exist { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return rel, nil } diff --git a/models/git/protected_tag.go b/models/git/protected_tag.go index 95642df593..dc38daf981 100644 --- a/models/git/protected_tag.go +++ b/models/git/protected_tag.go @@ -104,7 +104,7 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) { return nil, err } if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return tag, nil } @@ -117,7 +117,7 @@ func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern str return nil, err } if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return tag, nil } diff --git a/models/issues/issue.go b/models/issues/issue.go index f6f27588b3..655cdebdfc 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -498,7 +498,7 @@ func (issue *Issue) GetLastComment(ctx context.Context) (*Comment, error) { return nil, err } if !exist { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return &c, nil } diff --git a/models/issues/review.go b/models/issues/review.go index d8caa4d13a..10e3fcd664 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -384,7 +384,7 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error // GetCurrentReview returns the current pending review of reviewer for given issue func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Issue) (*Review, error) { if reviewer == nil { - return nil, nil + return nil, nil //nolint:nilnil // return nil when reviewer is nil } reviews, err := FindReviews(ctx, FindReviewOptions{ Types: []ReviewType{ReviewTypePending}, diff --git a/models/organization/org_user.go b/models/organization/org_user.go index 4d7527c15f..69cd960944 100644 --- a/models/organization/org_user.go +++ b/models/organization/org_user.go @@ -174,13 +174,13 @@ func GetOrgAssignees(ctx context.Context, orgID int64) (_ []*user_model.User, er func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) { if len(users) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // return nil when there are no users } ownerTeam, err := GetOwnerTeam(ctx, orgID) if err != nil { if IsErrTeamNotExist(err) { log.Error("Organization does not have owner team: %d", orgID) - return nil, nil + return nil, nil //nolint:nilnil // return nil when owner team does not exist } return nil, err } diff --git a/models/repo/archiver.go b/models/repo/archiver.go index 4f1b7238d7..ca981a178c 100644 --- a/models/repo/archiver.go +++ b/models/repo/archiver.go @@ -107,7 +107,7 @@ func GetRepoArchiver(ctx context.Context, repoID int64, tp ArchiveType, commitID if has { return &archiver, nil } - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } // ExistsRepoArchiverWithStoragePath checks if there is a RepoArchiver for a given storage path diff --git a/models/repo/fork.go b/models/repo/fork.go index 1c75e86458..80b3e5634e 100644 --- a/models/repo/fork.go +++ b/models/repo/fork.go @@ -49,7 +49,7 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error) return nil, err } if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return &forkedRepo, nil } diff --git a/models/repo/repo.go b/models/repo/repo.go index 0846dbcd05..07b9bf30cc 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -878,7 +878,7 @@ func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName st // non-generated repositories, and TemplateRepo will be left untouched) func GetTemplateRepo(ctx context.Context, repo *Repository) (*Repository, error) { if !repo.IsGenerated() { - return nil, nil + return nil, nil //nolint:nilnil // return nil for non-generated repositories } return GetRepositoryByID(ctx, repo.TemplateID) diff --git a/models/repo/topic.go b/models/repo/topic.go index f8f706fc1a..6d5209d821 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -257,7 +257,7 @@ func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, e } if topic == nil { // Repo doesn't have topic, can't be removed - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the topic does not exist } return db.WithTx2(ctx, func(ctx context.Context) (*Topic, error) { diff --git a/models/unittest/fixtures_loader.go b/models/unittest/fixtures_loader.go index d92b0cdb14..5b79cb5643 100644 --- a/models/unittest/fixtures_loader.go +++ b/models/unittest/fixtures_loader.go @@ -169,7 +169,7 @@ func (f *fixturesLoaderInternal) Load() error { func FixturesFileFullPaths(dir string, files []string) (map[string]*FixtureItem, error) { if files != nil && len(files) == 0 { - return nil, nil // load nothing + return nil, nil //nolint:nilnil // load nothing } files = slices.Clone(files) if len(files) == 0 { diff --git a/models/unittest/fixtures_test.go b/models/unittest/fixtures_test.go index ebf209f5f4..879277c9b1 100644 --- a/models/unittest/fixtures_test.go +++ b/models/unittest/fixtures_test.go @@ -16,7 +16,7 @@ import ( ) var NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) { - return nil, nil + return nil, nil //nolint:nilnil // no vendor fixtures loader configured } /* diff --git a/models/user/block.go b/models/user/block.go index 5f2b65a199..f4afd47d0f 100644 --- a/models/user/block.go +++ b/models/user/block.go @@ -90,7 +90,7 @@ func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, er return nil, err } if len(blocks) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return blocks[0], nil } diff --git a/models/user/email_address.go b/models/user/email_address.go index 2b58edaeb5..aa483d5f00 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -215,7 +215,7 @@ func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, err if has, err := db.GetEngine(ctx).ID(id).Get(email); err != nil { return nil, err } else if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return email, nil } diff --git a/models/user/list.go b/models/user/list.go index ca589d1e02..4337c34963 100644 --- a/models/user/list.go +++ b/models/user/list.go @@ -48,7 +48,7 @@ func (users UserList) GetTwoFaStatus(ctx context.Context) map[int64]bool { func (users UserList) loadTwoFactorStatus(ctx context.Context) (map[int64]*auth.TwoFactor, error) { if len(users) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // returns nil when there are no users } userIDs := users.GetUserIDs() diff --git a/models/user/user.go b/models/user/user.go index efed73e99d..f8e8b5c64a 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1193,7 +1193,7 @@ func (eum *EmailUserMap) GetByEmail(email string) *User { func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) { if len(emails) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // return nil when there are no emails to look up } needCheckEmails := make(container.Set[string]) diff --git a/modules/git/commit_submodule.go b/modules/git/commit_submodule.go index ff253b7eca..5e5f90c20e 100644 --- a/modules/git/commit_submodule.go +++ b/modules/git/commit_submodule.go @@ -16,7 +16,7 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) { entry, err := c.GetTreeEntryByPath(".gitmodules") if err != nil { if _, ok := err.(ErrNotExist); ok { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist } return nil, err } @@ -48,5 +48,5 @@ func (c *Commit) GetSubModule(entryName string) (*SubModule, error) { return module, nil } } - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist } diff --git a/modules/git/foreachref/parser.go b/modules/git/foreachref/parser.go index 913431795f..91868076b4 100644 --- a/modules/git/foreachref/parser.go +++ b/modules/git/foreachref/parser.go @@ -100,7 +100,7 @@ func (p *Parser) Err() error { func (p *Parser) parseRef(refBlock string) (map[string]string, error) { if refBlock == "" { // must be at EOF - return nil, nil + return nil, nil //nolint:nilnil // return nil to signal EOF } fieldValues := make(map[string]string) diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index cff2556083..a013773b47 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -55,12 +55,12 @@ func (c *LastCommitCache) Put(ref, entryPath, commitID string) error { // Get gets the last commit information by commit id and entry path func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) { if c == nil || c.cache == nil { - return nil, nil + return nil, nil //nolint:nilnil // return nil when cache is not available } commitID, ok := c.cache.Get(getCacheKey(c.repoPath, ref, entryPath)) if !ok || commitID == "" { - return nil, nil + return nil, nil //nolint:nilnil // return nil when cache miss } log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, commitID) diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 8acfc96f26..6e6d9985ae 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -106,7 +106,7 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int case bufio.ErrBufferFull: g.buffull = true case io.EOF: - return nil, nil + return nil, nil //nolint:nilnil // return nil to signal EOF default: return nil, err } @@ -121,7 +121,7 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int case bufio.ErrBufferFull: g.buffull = true case io.EOF: - return nil, nil + return nil, nil //nolint:nilnil // return nil to signal EOF default: return nil, err } diff --git a/modules/graceful/net_unix.go b/modules/graceful/net_unix.go index 796e00507c..a06f78dafe 100644 --- a/modules/graceful/net_unix.go +++ b/modules/graceful/net_unix.go @@ -290,11 +290,11 @@ func getActiveListenersToUnlink() []bool { func getNotifySocket() (*net.UnixConn, error) { if err := getProvidedFDs(); err != nil { // This error will be logged elsewhere - return nil, nil + return nil, nil //nolint:nilnil // return nil when no provided FDs are available } if notifySocketAddr == "" { - return nil, nil + return nil, nil //nolint:nilnil // return nil when notify socket is not configured } socketAddr := &net.UnixAddr{ diff --git a/modules/optional/serialization.go b/modules/optional/serialization.go index b120a0edf6..345ce56268 100644 --- a/modules/optional/serialization.go +++ b/modules/optional/serialization.go @@ -37,7 +37,7 @@ func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error { func (o Option[T]) MarshalYAML() (any, error) { if !o.Has() { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate no value to marshal } value := new(yaml.Node) diff --git a/routers/api/packages/auth.go b/routers/api/packages/auth.go index b7bf381241..28e5be1e4d 100644 --- a/routers/api/packages/auth.go +++ b/routers/api/packages/auth.go @@ -32,14 +32,14 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS } if packageMeta == nil || packageMeta.UserID == 0 { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } var u *user_model.User switch packageMeta.UserID { case user_model.GhostUserID: if !a.AllowGhostUser { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } u = user_model.NewGhostUser() case user_model.ActionsUserID: diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go index c6808300a2..5f7dad9d2d 100644 --- a/routers/api/packages/chef/auth.go +++ b/routers/api/packages/chef/auth.go @@ -61,7 +61,7 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS return nil, err } if u == nil { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } pub, err := getUserPublicKey(req.Context(), u) @@ -88,7 +88,7 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS func getUserFromRequest(req *http.Request) (*user_model.User, error) { username := req.Header.Get("X-Ops-Userid") if username == "" { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } return user_model.GetUserByName(req.Context(), username) diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index d84f79a0a8..66c28c9772 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -300,7 +300,7 @@ func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCl content := ctx.Req.FormValue(formKey) if content == "" { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the content does not exist } return io.NopCloser(strings.NewReader(content)), nil } diff --git a/routers/common/compare.go b/routers/common/compare.go index 5b6fdba4e0..7e917c4df8 100644 --- a/routers/common/compare.go +++ b/routers/common/compare.go @@ -161,7 +161,7 @@ func GetHeadOwnerAndRepo(ctx context.Context, baseRepo *repo_model.Repository, c func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) { if traverseLevel == 0 { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } // test if we are lucky repo, err := repo_model.GetUserFork(ctx, baseRepo.ID, headUserID) @@ -185,5 +185,5 @@ func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Reposito return forked, nil } } - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go index c6e2d18249..cd32a7dbb7 100644 --- a/routers/web/repo/setting/secrets.go +++ b/routers/web/repo/setting/secrets.go @@ -46,7 +46,7 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) { if ctx.Data["PageIsOrgSettings"] == true { if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { ctx.ServerError("RenderUserOrgHeader", err) - return nil, nil + return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError } return &secretsCtx{ OwnerID: ctx.ContextUser.ID, diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index 648f8046a4..9dca366123 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -59,7 +59,7 @@ func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) { if ctx.Data["PageIsOrgSettings"] == true { if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { ctx.ServerError("RenderUserOrgHeader", err) - return nil, nil + return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError } return &runnersCtx{ RepoID: 0, diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index a43c2c2690..8a8c49f415 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -51,7 +51,7 @@ func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) { if ctx.Data["PageIsOrgSettings"] == true { if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { ctx.ServerError("RenderUserOrgHeader", err) - return nil, nil + return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError } return &variablesCtx{ OwnerID: ctx.ContextUser.ID, diff --git a/services/actions/context.go b/services/actions/context.go index b6de429ccf..626ae6ee6b 100644 --- a/services/actions/context.go +++ b/services/actions/context.go @@ -104,7 +104,7 @@ type TaskNeed struct { // FindTaskNeeds finds the `needs` for the task by the task's job func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*TaskNeed, error) { if len(job.Needs) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // return nil when the job has no needs } needs := container.SetOf(job.Needs...) diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index 27e540f5cc..0e14c3cb17 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -123,7 +123,7 @@ func checkJobsByRunID(ctx context.Context, runID int64) error { // findBlockedRunByConcurrency finds the blocked concurrent run in a repo and returns `nil, nil` when there is no blocked run. func findBlockedRunByConcurrency(ctx context.Context, repoID int64, concurrencyGroup string) (*actions_model.ActionRun, error) { if concurrencyGroup == "" { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that no blocked run exists } cRuns, cJobs, err := actions_model.GetConcurrentRunsAndJobs(ctx, repoID, concurrencyGroup, []actions_model.Status{actions_model.StatusBlocked}) if err != nil { diff --git a/services/auth/auth_token.go b/services/auth/auth_token.go index 6b59238c98..8897bbd19c 100644 --- a/services/auth/auth_token.go +++ b/services/auth/auth_token.go @@ -32,7 +32,7 @@ var ( func CheckAuthToken(ctx context.Context, value string) (*auth_model.AuthToken, error) { if len(value) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } parts := strings.SplitN(value, ":", 2) diff --git a/services/auth/basic.go b/services/auth/basic.go index de5c7730cc..3161d7f33d 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -121,7 +121,7 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store store.GetData()["LoginMethod"] = ActionTokenMethodName return user_model.NewActionsUserWithTaskID(task.ID), nil } - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } // Verify extracts and validates Basic data (username and password/token) from the @@ -132,7 +132,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore parseBasicRet := b.parseAuthBasic(req) authToken, uname, passwd := parseBasicRet.authToken, parseBasicRet.uname, parseBasicRet.passwd if authToken == "" && uname == "" { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } u, err := b.VerifyAuthToken(req, w, store, sess, authToken) if u != nil || err != nil { @@ -140,7 +140,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore } if !setting.Service.EnableBasicAuth { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } log.Trace("Basic Authorization: Attempting SignIn for %s", uname) diff --git a/services/auth/httpsign.go b/services/auth/httpsign.go index 25e96ff32d..130207c0ea 100644 --- a/services/auth/httpsign.go +++ b/services/auth/httpsign.go @@ -42,7 +42,7 @@ func (h *HTTPSign) Name() string { func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { sigHead := req.Header.Get("Signature") if len(sigHead) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } var ( @@ -53,14 +53,14 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataSt if len(req.Header.Get("X-Ssh-Certificate")) != 0 { // Handle Signature signed by SSH certificates if len(setting.SSH.TrustedUserCAKeys) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } publicKey, err = VerifyCert(req) if err != nil { log.Debug("VerifyCert on request from %s: failed: %v", req.RemoteAddr, err) log.Warn("Failed authentication attempt from %s", req.RemoteAddr) - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } } else { // Handle Signature signed by Public Key @@ -68,7 +68,7 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataSt if err != nil { log.Debug("VerifyPubKey on request from %s: failed: %v", req.RemoteAddr, err) log.Warn("Failed authentication attempt from %s", req.RemoteAddr) - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } } diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 13cbc77f7a..86903b0ce1 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -156,12 +156,12 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor detector := newAuthPathDetector(req) if !detector.isAPIPath() && !detector.isAttachmentDownload() && !detector.isAuthenticatedTokenRequest() && !detector.isGitRawOrAttachPath() && !detector.isArchivePath() { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } token, ok := parseToken(req) if !ok { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } user, err := o.userFromToken(req.Context(), token, store) diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go index d6664d738d..064b263a67 100644 --- a/services/auth/reverseproxy.go +++ b/services/auth/reverseproxy.go @@ -51,7 +51,7 @@ func (r *ReverseProxy) Name() string { func (r *ReverseProxy) getUserFromAuthUser(req *http.Request) (*user_model.User, error) { username := r.getUserName(req) if len(username) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } log.Trace("ReverseProxy Authorization: Found username: %s", username) @@ -111,7 +111,7 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da if user == nil { user = r.getUserFromAuthEmail(req) if user == nil { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } } diff --git a/services/auth/session.go b/services/auth/session.go index 35d97e42da..5b6e4599b8 100644 --- a/services/auth/session.go +++ b/services/auth/session.go @@ -29,19 +29,19 @@ func (s *Session) Name() string { // Returns nil if there is no user uid stored in the session. func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { if sess == nil { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } // Get user ID uid := sess.Get("uid") if uid == nil { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } log.Trace("Session Authorization: Found user[%d]", uid) id, ok := uid.(int64) if !ok { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } // Get user object @@ -52,7 +52,7 @@ func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataSto // Return the err as-is to keep current signed-in session, in case the err is something like context.Canceled. Otherwise non-existing user (nil, nil) will make the caller clear the signed-in session. return nil, err } - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } log.Trace("Session Authorization: Logged in user %-v", user) diff --git a/services/auth/source/oauth2/providers_test.go b/services/auth/source/oauth2/providers_test.go index 353816c71e..08c50b12a9 100644 --- a/services/auth/source/oauth2/providers_test.go +++ b/services/auth/source/oauth2/providers_test.go @@ -19,11 +19,11 @@ func (p *fakeProvider) Name() string { func (p *fakeProvider) SetName(name string) {} func (p *fakeProvider) BeginAuth(state string) (goth.Session, error) { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } func (p *fakeProvider) UnmarshalSession(string) (goth.Session, error) { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } func (p *fakeProvider) FetchUser(goth.Session) (goth.User, error) { diff --git a/services/auth/sspi.go b/services/auth/sspi.go index 8cb39886c4..6450753935 100644 --- a/services/auth/sspi.go +++ b/services/auth/sspi.go @@ -63,7 +63,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, return nil, sspiAuthErrInit } if !s.shouldAuthenticate(req) { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } cfg, err := s.getConfig(req.Context()) @@ -97,7 +97,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, username := sanitizeUsername(userInfo.Username, cfg) if len(username) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } log.Info("Authenticated as %s\n", username) @@ -109,7 +109,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, } if !cfg.AutoCreateUsers { log.Error("User '%s' not found", username) - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } user, err = s.newUser(req.Context(), username, cfg) if err != nil { diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index 33e632ea4d..701c25e442 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -192,7 +192,7 @@ func LoadGitRepo(t *testing.T, ctx gocontext.Context) { type MockRender struct{} func (tr *MockRender) TemplateLookup(tmpl string, _ gocontext.Context) (templates.TemplateExecutor, error) { - return nil, nil + return nil, nil //nolint:nilnil // mock implementation returns nil to indicate no template found } func (tr *MockRender) HTML(w io.Writer, status int, _ templates.TplName, _ any, _ gocontext.Context) error { diff --git a/services/gitdiff/csv.go b/services/gitdiff/csv.go index c10ee14490..3f62f15ca5 100644 --- a/services/gitdiff/csv.go +++ b/services/gitdiff/csv.go @@ -193,7 +193,7 @@ func createCsvDiff(diffFile *DiffFile, baseReader, headReader *csv.Reader) ([]*T } if aRow == nil && bRow == nil { // No content - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the row has no content } aIndex := 0 // tracks where we are in the a2bColMap diff --git a/services/issue/assignee.go b/services/issue/assignee.go index 97b32d5865..ae4b7138ee 100644 --- a/services/issue/assignee.go +++ b/services/issue/assignee.go @@ -234,7 +234,7 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use } if comment == nil || !isAdd { - return nil, nil + return nil, nil //nolint:nilnil // return nil because no comment was created or it is a removal } return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment) diff --git a/services/issue/commit.go b/services/issue/commit.go index 66ad93a97d..6cc120697a 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -113,7 +113,7 @@ func getIssueFromRef(ctx context.Context, repo *repo_model.Repository, index int issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, index) if err != nil { if issues_model.IsErrIssueNotExist(err) { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return nil, err } diff --git a/services/issue/issue.go b/services/issue/issue.go index bb208e43a9..9beb4c46ec 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -229,7 +229,7 @@ func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, do } if isAssigned { // nothing to do - return nil, nil + return nil, nil //nolint:nilnil // return nil because the user is already assigned } valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull) diff --git a/services/packages/auth.go b/services/packages/auth.go index 6fcc408adc..dd1f68a7ee 100644 --- a/services/packages/auth.go +++ b/services/packages/auth.go @@ -56,7 +56,7 @@ func CreateAuthorizationToken(u *user_model.User, packageScope auth_model.Access func ParseAuthorizationRequest(req *http.Request) (*PackageMeta, error) { h := req.Header.Get("Authorization") if h == "" { - return nil, nil + return nil, nil //nolint:nilnil // the auth method is not applicable } parts := strings.SplitN(h, " ", 2) diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index ebcaa3e56d..580d84ebc2 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -152,7 +152,7 @@ func BuildPackageIndex(ctx context.Context, p *packages_model.Package) (*bytes.B return nil, fmt.Errorf("SearchVersions[%s]: %w", p.Name, err) } if len(pvs) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the package has no versions } pds, err := packages_model.GetPackageDescriptors(ctx, pvs) diff --git a/services/pull/check.go b/services/pull/check.go index 8826fca280..f6e8433cf2 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -291,7 +291,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com if err := gitrepo.RunCmdWithStderr(ctx, pr.BaseRepo, cmd); err != nil { if gitcmd.IsErrorExitCode(err, 1) { // prHeadRef is not an ancestor of the base branch - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the PR head is not merged } // Errors are signaled by a non-zero status that is not 1 return nil, fmt.Errorf("%-v git merge-base --is-ancestor: %w", pr, err) diff --git a/services/pull/comment.go b/services/pull/comment.go index f24e8128e9..6c10bf2aa8 100644 --- a/services/pull/comment.go +++ b/services/pull/comment.go @@ -49,7 +49,7 @@ func getCommitIDsFromRepo(ctx context.Context, repo *repo_model.Repository, oldC // CreatePushPullComment create push code to pull base comment func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *issues_model.PullRequest, oldCommitID, newCommitID string, isForcePush bool) (comment *issues_model.Comment, err error) { if pr.HasMerged || oldCommitID == "" || newCommitID == "" { - return nil, nil + return nil, nil //nolint:nilnil // return nil because no comment needs to be created } opts := &issues_model.CreateCommentOptions{ @@ -71,7 +71,7 @@ func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *iss } // It maybe an empty pull request. Only non-empty pull request need to create push comment if len(data.CommitIDs) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // return nil because no comment needs to be created } } diff --git a/services/pull/review.go b/services/pull/review.go index acbb620e92..261cf234b3 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -465,7 +465,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string, } if !isDismiss { - return nil, nil + return nil, nil //nolint:nilnil // return nil because this is not a dismiss action } if err := review.Issue.LoadAttributes(ctx); err != nil { diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index bfd941ebf6..07214d0bfa 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -156,7 +156,7 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver // FIXME: If another process are generating it, we think it's not ready and just return // Or we should wait until the archive generated. if archiver.Status == repo_model.ArchiverGenerating { - return nil, nil + return nil, nil //nolint:nilnil // return nil because the archive is still being generated } } else { archiver = &repo_model.RepoArchiver{ diff --git a/services/repository/files/update.go b/services/repository/files/update.go index bd992d06de..3523b2d342 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -510,7 +510,7 @@ func modifyFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeR } if writeObjectRet.LfsContent == nil { - return nil, nil // No LFS pointer, so nothing to do + return nil, nil //nolint:nilnil // No LFS pointer, so nothing to do } defer writeObjectRet.LfsContent.Close() From 0e9993253004665fe34bd6e794f763894927e0b4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 16 Feb 2026 11:27:49 +0100 Subject: [PATCH 19/23] Only turn links to current instance into hash links (#36237) Given the following markdown: ``` http://localhost:3500/silverwind/symlink-test/commit/a832c723cd116df44cce6271c4a89afa4d8ec670 http://localhost:3500/silverwind/remap-css/commit/19fe6cdf81f7ec50b8cac2d6c28fe7c42c1ffe14 http://github.com/silverwind/symlink-test/commit/a832c723cd116df44cce6271c4a89afa4d8ec670 ``` Previously, all links would turn into hash link, even ones to external sites: Screenshot 2025-12-23 at 19 19 13 After this change, only links to the current instance, as identified by `setting.AppURL` are turned into hash links: Screenshot 2025-12-23 at 19 18 56 There is still one notable [difference with GitHub](https://github.com/silverwind/symlink-test/issues/20#issuecomment-3687535938) where the second link should render like `user/repo@`, not `` as currently, I would like to fix that here as well. --------- Co-authored-by: Claude Opus 4.6 --- modules/markup/html_commit.go | 12 ++++++++++++ modules/markup/html_internal_test.go | 2 +- modules/markup/html_test.go | 13 ++++++++----- modules/markup/markdown/markdown_test.go | 20 +++++++++++++++++--- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go index 0a9b329589..c319374a38 100644 --- a/modules/markup/html_commit.go +++ b/modules/markup/html_commit.go @@ -8,6 +8,7 @@ import ( "strings" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/util" @@ -121,6 +122,11 @@ func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) { if ret.QueryHash != "" { text += " (" + ret.QueryHash + ")" } + // only turn commit links to the current instance into hash link + if !httplib.IsCurrentGiteaSiteURL(ctx, ret.FullURL) { + node = node.NextSibling + continue + } replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit")) node = node.NextSibling.NextSibling } @@ -167,6 +173,12 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) { } } + // only turn compare links to the current instance into hash link + if !httplib.IsCurrentGiteaSiteURL(ctx, urlFull) { + node = node.NextSibling + continue + } + text := text1 + textDots + text2 if hash != "" { text += " (" + hash + ")" diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 467cc509d0..ca2579c8ea 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -299,7 +299,7 @@ func TestRender_AutoLink(t *testing.T) { // render other commit URLs tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2" - test(tmp, "d8a994ef24 (diff-2)") + test(tmp, ""+tmp+"") } func TestRender_FullIssueURLs(t *testing.T) { diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 76013ccd13..5f873d2985 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -71,6 +71,7 @@ func TestRender_Commits(t *testing.T) { } func TestRender_CrossReferences(t *testing.T) { + defer testModule.MockVariableValue(&setting.AppURL, markup.TestAppURL)() defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() test := func(input, expected string) { rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md") @@ -98,17 +99,17 @@ func TestRender_CrossReferences(t *testing.T) { util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"), `

gogitea/some-repo-name#12345

`) - inputURL := "https://host/a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3" + inputURL := setting.AppURL + "a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3" test( inputURL, `

0123456789/foo.txt (L2-L3)

`) - inputURL = "https://example.com/repo/owner/archive/0123456789012345678901234567890123456789.tar.gz" + inputURL = setting.AppURL + "repo/owner/archive/0123456789012345678901234567890123456789.tar.gz" test( inputURL, `

0123456789.tar.gz

`) - inputURL = "https://example.com/owner/repo/commit/0123456789012345678901234567890123456789.patch?key=val" + inputURL = setting.AppURL + "owner/repo/commit/0123456789012345678901234567890123456789.patch?key=val" test( inputURL, `

0123456789.patch

`) @@ -575,13 +576,15 @@ func TestFuzz(t *testing.T) { } func TestIssue18471(t *testing.T) { - data := `http://domain/org/repo/compare/783b039...da951ce` + defer testModule.MockVariableValue(&setting.AppURL, markup.TestAppURL)() + + data := markup.TestAppURL + `org/repo/compare/783b039...da951ce` var res strings.Builder err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res) assert.NoError(t, err) - assert.Equal(t, `783b039...da951ce`, res.String()) + assert.Equal(t, `783b039...da951ce`, res.String()) } func TestIsFullURL(t *testing.T) { diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 47b293e1e9..8d6b3b3c80 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -483,6 +483,9 @@ foo: bar } func TestRenderLinks(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, AppURL)() + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + input := ` space @mention-user${SPACE}${SPACE} /just/a/path.bin https://example.com/file.bin @@ -520,9 +523,9 @@ mail@domain.com remote image local image remote link -88fc37a3c0...12fc37a3c0 (hash) +https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare -88fc37a3c0 +https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit 👍 mail@domain.com @@ -530,10 +533,21 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit #123 space

` - defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input) assert.NoError(t, err) assert.Equal(t, expected, string(result)) + + t.Run("LocalCommitAndCompare", func(t *testing.T) { + input := `http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb +http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash` + + expected := `

88fc37a3c0 +88fc37a3c0...12fc37a3c0 (hash)

+` + result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input) + assert.NoError(t, err) + assert.Equal(t, expected, string(result)) + }) } func TestMarkdownLink(t *testing.T) { From cfc60b2142af18f843325b94313a8aafbbfe6908 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 16 Feb 2026 11:58:04 +0100 Subject: [PATCH 20/23] Use `relative-time` to render absolute dates (#36238) `` can render absolute dates when passed [`threshold="P0Y"`](https://github.com/github/relative-time-element#threshold-string-default-p30d) and `prefix=""`, so remove the previously used `` element in its favor. Devtest before: Screenshot 2025-12-23 at 20 22 44 Devtest after: Screenshot 2025-12-23 at 20 22 49 Repo activity (rendering unchanged): image --------- Signed-off-by: silverwind Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- modules/templates/util_date.go | 6 +-- modules/templates/util_date_test.go | 4 +- templates/devtest/gitea-ui.tmpl | 13 +++--- .../js/webcomponents/absolute-date.test.ts | 26 ------------ web_src/js/webcomponents/absolute-date.ts | 41 ------------------- web_src/js/webcomponents/index.ts | 1 - webpack.config.ts | 1 - 7 files changed, 11 insertions(+), 81 deletions(-) delete mode 100644 web_src/js/webcomponents/absolute-date.test.ts delete mode 100644 web_src/js/webcomponents/absolute-date.ts diff --git a/modules/templates/util_date.go b/modules/templates/util_date.go index fc3f3f2339..1b36722c43 100644 --- a/modules/templates/util_date.go +++ b/modules/templates/util_date.go @@ -93,14 +93,14 @@ func dateTimeFormat(format string, datetime any) template.HTML { attrs := []string{`weekday=""`, `year="numeric"`} switch format { case "short", "long": // date only - attrs = append(attrs, `month="`+format+`"`, `day="numeric"`) - return template.HTML(fmt.Sprintf(`%s`, strings.Join(attrs, " "), datetimeEscaped, textEscaped)) + attrs = append(attrs, `threshold="P0Y"`, `month="`+format+`"`, `day="numeric"`, `prefix=""`) case "full": // full date including time attrs = append(attrs, `format="datetime"`, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`, `data-tooltip-content`, `data-tooltip-interactive="true"`) - return template.HTML(fmt.Sprintf(`%s`, strings.Join(attrs, " "), datetimeEscaped, textEscaped)) default: panic("Unsupported format " + format) } + + return template.HTML(fmt.Sprintf(`%s`, strings.Join(attrs, " "), datetimeEscaped, textEscaped)) } func timeSinceTo(then any, now time.Time) template.HTML { diff --git a/modules/templates/util_date_test.go b/modules/templates/util_date_test.go index 2c1f2d242e..b74bbb0cee 100644 --- a/modules/templates/util_date_test.go +++ b/modules/templates/util_date_test.go @@ -32,10 +32,10 @@ func TestDateTime(t *testing.T) { assert.EqualValues(t, "-", du.AbsoluteShort(timeutil.TimeStamp(0))) actual := du.AbsoluteShort(refTime) - assert.EqualValues(t, `2018-01-01`, actual) + assert.EqualValues(t, `2018-01-01`, actual) actual = du.AbsoluteShort(refTimeStamp) - assert.EqualValues(t, `2017-12-31`, actual) + assert.EqualValues(t, `2017-12-31`, actual) actual = du.FullTime(refTimeStamp) assert.EqualValues(t, `2017-12-31 19:00:00 -05:00`, actual) diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl index cb5aad7b0c..c1e6590a43 100644 --- a/templates/devtest/gitea-ui.tmpl +++ b/templates/devtest/gitea-ui.tmpl @@ -118,13 +118,12 @@
-

GiteaAbsoluteDate

-
-
-
-
-
-
relative-time:
+

Absolute Dates

+
+
+
+
+
diff --git a/web_src/js/webcomponents/absolute-date.test.ts b/web_src/js/webcomponents/absolute-date.test.ts deleted file mode 100644 index 4eee80048d..0000000000 --- a/web_src/js/webcomponents/absolute-date.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {toAbsoluteLocaleDate} from './absolute-date.ts'; - -test('toAbsoluteLocaleDate', () => { - expect(toAbsoluteLocaleDate('2024-03-15', 'en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - })).toEqual('March 15, 2024'); - - expect(toAbsoluteLocaleDate('2024-03-15T01:02:03', 'de-DE', { - year: 'numeric', - month: 'long', - day: 'numeric', - })).toEqual('15. März 2024'); - - // these cases shouldn't happen - expect(toAbsoluteLocaleDate('2024-03-15 01:02:03', '', {})).toEqual('Invalid Date'); - expect(toAbsoluteLocaleDate('10000-01-01', '', {})).toEqual('Invalid Date'); - - // test different timezone - const oldTZ = import.meta.env.TZ; - import.meta.env.TZ = 'America/New_York'; - expect(new Date('2024-03-15').toLocaleString('en-US')).toEqual('3/14/2024, 8:00:00 PM'); - expect(toAbsoluteLocaleDate('2024-03-15', 'en-US')).toEqual('3/15/2024, 12:00:00 AM'); - import.meta.env.TZ = oldTZ; -}); diff --git a/web_src/js/webcomponents/absolute-date.ts b/web_src/js/webcomponents/absolute-date.ts deleted file mode 100644 index 5ab4deaa14..0000000000 --- a/web_src/js/webcomponents/absolute-date.ts +++ /dev/null @@ -1,41 +0,0 @@ -export function toAbsoluteLocaleDate(date: string, lang?: string, opts?: Intl.DateTimeFormatOptions) { - // only use the date part, it is guaranteed to be in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ) or (YYYY-MM-DD) - // if there is an "Invalid Date" error, there must be something wrong in code and should be fixed. - // TODO: there is a root problem in backend code: the date "YYYY-MM-DD" is passed to backend without timezone (eg: deadline), - // then backend parses it in server's timezone and stores the parsed timestamp into database. - // If the user's timezone is different from the server's, the date might be displayed in the wrong day. - const dateSep = date.indexOf('T'); - date = dateSep === -1 ? date : date.substring(0, dateSep); - return new Date(`${date}T00:00:00`).toLocaleString(lang || [], opts); -} - -window.customElements.define('absolute-date', class extends HTMLElement { - static observedAttributes = ['date', 'year', 'month', 'weekday', 'day']; - - initialized = false; - - update = () => { - const opts: Record = {}; - for (const attr of ['year', 'month', 'weekday', 'day']) { - if (this.getAttribute(attr)) { - opts[attr] = this.getAttribute(attr)!; - } - } - const lang = this.closest('[lang]')?.getAttribute('lang') || - this.ownerDocument.documentElement.getAttribute('lang') || ''; - - if (!this.shadowRoot) this.attachShadow({mode: 'open'}); - this.shadowRoot!.textContent = toAbsoluteLocaleDate(this.getAttribute('date')!, lang, opts); - }; - - attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null) { - if (!this.initialized || oldValue === newValue) return; - this.update(); - } - - connectedCallback() { - this.initialized = false; - this.update(); - this.initialized = true; - } -}); diff --git a/web_src/js/webcomponents/index.ts b/web_src/js/webcomponents/index.ts index 6c0f555864..8251f6ddae 100644 --- a/web_src/js/webcomponents/index.ts +++ b/web_src/js/webcomponents/index.ts @@ -2,4 +2,3 @@ import './polyfills.ts'; import '@github/relative-time-element'; import './origin-url.ts'; import './overflow-menu.ts'; -import './absolute-date.ts'; diff --git a/webpack.config.ts b/webpack.config.ts index 6902d182d7..ef08239354 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -39,7 +39,6 @@ const webComponents = new Set([ // our own, in web_src/js/webcomponents 'overflow-menu', 'origin-url', - 'absolute-date', // from dependencies 'markdown-toolbar', 'relative-time', From 1b874d14035ee8fda5962040dd144e532cd0701c Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 17 Feb 2026 09:06:27 +0100 Subject: [PATCH 21/23] Use first commit title for multi-commit PRs and fix auto-focus title field (#36606) Fixes: https://github.com/go-gitea/gitea/issues/34865 1. When opening a PR from a branch with multiple commits, use the first (oldest) commit's title as the default title instead of the branch name 2. Fix autofocus on PR title input field Co-authored-by: Claude Opus 4.6 Co-authored-by: wxiaoguang --- modules/git/commit.go | 4 ++ routers/web/repo/compare.go | 56 +++++++++++++++------------- routers/web/repo/compare_test.go | 51 +++++++++++++++++++++++++ templates/repo/issue/new_form.tmpl | 2 +- web_src/js/features/common-button.ts | 3 +- web_src/js/features/common-page.ts | 26 +++++++++++-- web_src/js/modules/observer.ts | 1 + 7 files changed, 112 insertions(+), 31 deletions(-) diff --git a/modules/git/commit.go b/modules/git/commit.go index e66a33ef98..b98d36d946 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -37,6 +37,10 @@ type CommitSignature struct { // Message returns the commit message. Same as retrieving CommitMessage directly. func (c *Commit) Message() string { + // FIXME: GIT-COMMIT-MESSAGE-ENCODING: this logic is not right + // * When need to use commit message in templates/database, it should be valid UTF-8 + // * When need to get the original commit message, it should just use "c.CommitMessage" + // It's not easy to refactor at the moment, many templates need to be updated and tested return c.CommitMessage } diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 1e2486f5f1..e034731e5c 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -7,7 +7,6 @@ import ( gocontext "context" "encoding/csv" "errors" - "fmt" "io" "net/http" "net/url" @@ -426,6 +425,36 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { return compareInfo } +func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses) (title, content string) { + title = ci.HeadRef.ShortName() + + if len(commits) > 0 { + // the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one + c := commits[len(commits)-1] + title = strings.TrimSpace(c.UserCommit.Summary()) + } + + if len(commits) == 1 { + // FIXME: GIT-COMMIT-MESSAGE-ENCODING: try to convert the encoding for commit message explicitly, ideally it should be done by a git commit struct method + c := commits[0] + _, content, _ = strings.Cut(strings.TrimSpace(c.UserCommit.CommitMessage), "\n") + content = strings.TrimSpace(content) + content = string(charset.ToUTF8([]byte(content), charset.ConvertOpts{})) + } + + var titleTrailer string + // TODO: 255 doesn't seem to be a good limit for title, just keep the old behavior + title, titleTrailer = util.EllipsisDisplayStringX(title, 255) + if titleTrailer != "" { + if content != "" { + content = titleTrailer + "\n\n" + content + } else { + content = titleTrailer + "\n" + } + } + return title, content +} + // PrepareCompareDiff renders compare diff page func PrepareCompareDiff( ctx *context.Context, @@ -539,30 +568,7 @@ func PrepareCompareDiff( ctx.Data["Commits"] = commits ctx.Data["CommitCount"] = len(commits) - title := ci.HeadRef.ShortName() - if len(commits) == 1 { - c := commits[0] - title = strings.TrimSpace(c.UserCommit.Summary()) - - body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n") - if len(body) > 1 { - ctx.Data["content"] = strings.Join(body[1:], "\n") - } - } - - if len(title) > 255 { - var trailer string - title, trailer = util.EllipsisDisplayStringX(title, 255) - if len(trailer) > 0 { - if ctx.Data["content"] != nil { - ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"]) - } else { - ctx.Data["content"] = trailer + "\n" - } - } - } - - ctx.Data["title"] = title + ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits) ctx.Data["Username"] = ci.HeadRepo.OwnerName ctx.Data["Reponame"] = ci.HeadRepo.Name diff --git a/routers/web/repo/compare_test.go b/routers/web/repo/compare_test.go index 61472dc71e..700aba8821 100644 --- a/routers/web/repo/compare_test.go +++ b/routers/web/repo/compare_test.go @@ -4,9 +4,16 @@ package repo import ( + "strings" "testing" + "unicode/utf8" + asymkey_model "code.gitea.io/gitea/models/asymkey" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + git_service "code.gitea.io/gitea/services/git" "code.gitea.io/gitea/services/gitdiff" "github.com/stretchr/testify/assert" @@ -38,3 +45,47 @@ func TestAttachCommentsToLines(t *testing.T) { assert.Equal(t, int64(300), section.Lines[1].Comments[0].ID) assert.Equal(t, int64(301), section.Lines[1].Comments[1].ID) } + +func TestNewPullRequestTitleContent(t *testing.T) { + ci := &git_service.CompareInfo{HeadRef: "refs/heads/head-branch"} + + mockCommit := func(msg string) *git_model.SignCommitWithStatuses { + return &git_model.SignCommitWithStatuses{ + SignCommit: &asymkey_model.SignCommit{ + UserCommit: &user_model.UserCommit{ + Commit: &git.Commit{ + CommitMessage: msg, + }, + }, + }, + } + } + + title, content := prepareNewPullRequestTitleContent(ci, nil) + assert.Equal(t, "head-branch", title) + assert.Empty(t, content) + + title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-only")}) + assert.Equal(t, "title-only", title) + assert.Empty(t, content) + + title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))}) + assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title) + assert.Equal(t, "…aaaaaaaaa\n", content) + + title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title\nbody")}) + assert.Equal(t, "title", title) + assert.Equal(t, "body", content) + + title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")}) + assert.Equal(t, "a?", title) // FIXME: GIT-COMMIT-MESSAGE-ENCODING: "title" doesn't use the same charset converting logic as "content" + assert.Equal(t, "b"+string(utf8.RuneError)+string(utf8.RuneError), content) + + title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{ + // ordered from newest to oldest + mockCommit("title2\nbody2"), + mockCommit("title1\nbody1"), + }) + assert.Equal(t, "title1", title) + assert.Empty(t, content) +} diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index e4c8008c19..5475224750 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -6,7 +6,7 @@
{{ctx.AvatarUtils.Avatar .SignedUser 40}}
- diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts index 72044d2d4e..4483060ade 100644 --- a/web_src/js/features/common-button.ts +++ b/web_src/js/features/common-button.ts @@ -2,6 +2,7 @@ import {POST} from '../modules/fetch.ts'; import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {camelize} from 'vue'; +import {applyAutoFocus} from './common-page.ts'; export function initGlobalButtonClickOnEnter(): void { addDelegatedEventListener(document, 'keypress', 'div.ui.button, span.ui.button', (el, e: KeyboardEvent) => { @@ -88,7 +89,7 @@ function onShowPanelClick(el: HTMLElement, e: MouseEvent) { const elems = el.classList.contains('toggle') ? toggleElem(sel) : showElem(sel); for (const elem of elems) { if (isElemVisible(elem as HTMLElement)) { - elem.querySelector('[autofocus]')?.focus(); + applyAutoFocus(elem); } } } diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts index 5ecc271c0b..36af087089 100644 --- a/web_src/js/features/common-page.ts +++ b/web_src/js/features/common-page.ts @@ -116,12 +116,30 @@ function attachInputDirAuto(el: Partial) } } +function autoFocusEnd(el: HTMLInputElement | HTMLTextAreaElement) { + el.focus(); + el.setSelectionRange(el.value.length, el.value.length); +} + +export function applyAutoFocus(container: Element) { + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autofocus + // "autofocus" behavior is defined by the standard: when a container (e.g.: dialog) becomes visible, focus the element with "autofocus" attribute + // Fomantic UI already supports it for its modal dialog, we need to cover more cases (e.g.: ".show-panel" button) + // Here is just a simple support, we don't expect more than one element that need "autofocus" appearing in the same container + container.querySelector('[autofocus]')?.focus(); + // Also, apply our autoFocusEnd behavior + // TODO: GLOBAL-INIT-MULTIPLE-FUNCTIONS: use "~=" operator in case we would extend the "data-global-init" to support more functions in the future. + const el = container.querySelector('[data-global-init~="autoFocusEnd"]'); + if (el) autoFocusEnd(el); +} + export function initGlobalInput() { registerGlobalSelectorFunc('input, textarea', attachInputDirAuto); - registerGlobalInitFunc('initInputAutoFocusEnd', (el: HTMLInputElement) => { - el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus. - el.setSelectionRange(el.value.length, el.value.length); - }); + + // autoFocusEnd is used for autofocus an input/textarea and move the cursor to the end of the text. + // It is useful for "New Issue"/"New PR" pages when the title is pre-filled with prefix text (e.g.: from template or commit message) + // The native "autofocus" isn't used because there is a delay between "focused (DOM rendering)" and "move cursor to end (our JS)", it causes flickers. + registerGlobalInitFunc('autoFocusEnd', autoFocusEnd); } /** diff --git a/web_src/js/modules/observer.ts b/web_src/js/modules/observer.ts index 1bbf304b27..1dbad1aed1 100644 --- a/web_src/js/modules/observer.ts +++ b/web_src/js/modules/observer.ts @@ -42,6 +42,7 @@ export function registerGlobalInitFunc(name: string, hand } function callGlobalInitFunc(el: HTMLElement) { + // TODO: GLOBAL-INIT-MULTIPLE-FUNCTIONS: maybe in the future we need to extend it to support multiple functions, for example: `data-global-init="func1 func2 func3"` const initFunc = el.getAttribute('data-global-init')!; const func = globalInitFuncs[initFunc]; if (!func) throw new Error(`Global init function "${initFunc}" not found`); From 883af8d42d49b9eb9f287baf25b1e363f469e370 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 17 Feb 2026 09:25:07 +0100 Subject: [PATCH 22/23] Fix multi-arch Docker build SIGILL by splitting frontend stage (#36646) ## Summary - Split Dockerfile and Dockerfile.rootless into a two-stage build: frontend assets are built on the native platform (`$BUILDPLATFORM`) then copied to the per-architecture backend build stage - This avoids running esbuild/webpack under QEMU emulation which causes SIGILL (Invalid machine instruction) on arm64/riscv64 - Frontend assets (JS/CSS/fonts) are platform-independent so they only need to be built once - The `build-env` stage no longer needs `nodejs`/`pnpm` since it only builds the Go backend Signed-off-by: silverwind Co-authored-by: Claude Opus 4.6 Co-authored-by: TheFox0x7 --- Dockerfile | 22 +++++++++++++--------- Dockerfile.rootless | 17 +++++++++++------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index 79f507dbc6..f71b13e8f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,12 @@ # syntax=docker/dockerfile:1 -# Build stage +# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack +FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build +RUN apk --no-cache add build-base git nodejs pnpm +WORKDIR /src +COPY --exclude=.git/ . . +RUN --mount=type=cache,target=/root/.local/share/pnpm/store make frontend + +# Build backend for each target platform FROM docker.io/library/golang:1.26-alpine3.23 AS build-env ARG GOPROXY=direct @@ -12,22 +19,19 @@ ARG CGO_EXTRA_CFLAGS # Build deps RUN apk --no-cache add \ build-base \ - git \ - nodejs \ - pnpm + git WORKDIR ${GOPATH}/src/code.gitea.io/gitea -# Use COPY but not "mount" because some directories like "node_modules" contain platform-depended contents and these directories need to be ignored. -# ".git" directory will be mounted later separately for getting version data. -# TODO: in the future, maybe we can pre-build the frontend assets on one platform and share them for different platforms, the benefit is that it won't be affected by webpack plugin compatibility problems, then the working directory can be fully mounted and the COPY is not needed. +# Use COPY instead of bind mount as read-only one breaks makefile state tracking and read-write one needs binary to be moved as it's discarded. +# ".git" directory is mounted separately later only for version data extraction. COPY --exclude=.git/ . . +COPY --from=frontend-build /src/public/assets public/assets # Build gitea, .git mount is required for version data RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target="/root/.cache/go-build" \ - --mount=type=cache,target=/root/.local/share/pnpm/store \ --mount=type=bind,source=".git/",target=".git/" \ - make + make backend COPY docker/root /tmp/local diff --git a/Dockerfile.rootless b/Dockerfile.rootless index fe94774add..bc210132c5 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,5 +1,12 @@ # syntax=docker/dockerfile:1 -# Build stage +# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack +FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build +RUN apk --no-cache add build-base git nodejs pnpm +WORKDIR /src +COPY --exclude=.git/ . . +RUN --mount=type=cache,target=/root/.local/share/pnpm/store make frontend + +# Build backend for each target platform FROM docker.io/library/golang:1.26-alpine3.23 AS build-env ARG GOPROXY=direct @@ -12,20 +19,18 @@ ARG CGO_EXTRA_CFLAGS # Build deps RUN apk --no-cache add \ build-base \ - git \ - nodejs \ - pnpm + git WORKDIR ${GOPATH}/src/code.gitea.io/gitea # See the comments in Dockerfile COPY --exclude=.git/ . . +COPY --from=frontend-build /src/public/assets public/assets # Build gitea, .git mount is required for version data RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target="/root/.cache/go-build" \ - --mount=type=cache,target=/root/.local/share/pnpm/store \ --mount=type=bind,source=".git/",target=".git/" \ - make + make backend COPY docker/rootless /tmp/local From d6be18e87060b0bd5150b848327aff2f7d874cf7 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 17 Feb 2026 15:03:55 +0100 Subject: [PATCH 23/23] Load heatmap data asynchronously (#36622) Fixes: https://github.com/go-gitea/gitea/issues/21045 - Move heatmap data loading from synchronous server-side rendering to async client-side fetch via dedicated JSON endpoints - Dashboard and user profile pages no longer block on the expensive heatmap DB query during HTML generation - Use compact `[[timestamp,count]]` JSON format instead of `[{"timestamp":N,"contributions":N}]` to reduce payload size - Public API (`/api/v1/users/{username}/heatmap`) remains unchanged - Heatmap rendering is unchanged, still shows a spinner as before, which will now spin a litte bit longer. Signed-off-by: silverwind Co-authored-by: Claude Opus 4.6 Co-authored-by: wxiaoguang --- models/activities/user_heatmap.go | 17 ++------ routers/web/user/heatmap.go | 66 +++++++++++++++++++++++++++++++ routers/web/user/home.go | 20 +++------- routers/web/user/profile.go | 12 ++---- routers/web/web.go | 3 ++ templates/user/heatmap.tmpl | 6 +-- tests/integration/heatmap_test.go | 59 +++++++++++++++++++++++++++ web_src/js/features/heatmap.ts | 22 +++++++++-- 8 files changed, 162 insertions(+), 43 deletions(-) create mode 100644 routers/web/user/heatmap.go create mode 100644 tests/integration/heatmap_test.go diff --git a/models/activities/user_heatmap.go b/models/activities/user_heatmap.go index ef67838be7..e24d44c519 100644 --- a/models/activities/user_heatmap.go +++ b/models/activities/user_heatmap.go @@ -19,14 +19,14 @@ type UserHeatmapData struct { Contributions int64 `json:"contributions"` } -// GetUserHeatmapDataByUser returns an array of UserHeatmapData +// GetUserHeatmapDataByUser returns an array of UserHeatmapData, it checks whether doer can access user's activity func GetUserHeatmapDataByUser(ctx context.Context, user, doer *user_model.User) ([]*UserHeatmapData, error) { return getUserHeatmapData(ctx, user, nil, doer) } -// GetUserHeatmapDataByUserTeam returns an array of UserHeatmapData -func GetUserHeatmapDataByUserTeam(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { - return getUserHeatmapData(ctx, user, team, doer) +// GetUserHeatmapDataByOrgTeam returns an array of UserHeatmapData, it checks whether doer can access org's activity +func GetUserHeatmapDataByOrgTeam(ctx context.Context, org *organization.Organization, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { + return getUserHeatmapData(ctx, org.AsUser(), team, doer) } func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { @@ -71,12 +71,3 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi OrderBy("timestamp"). Find(&hdata) } - -// GetTotalContributionsInHeatmap returns the total number of contributions in a heatmap -func GetTotalContributionsInHeatmap(hdata []*UserHeatmapData) int64 { - var total int64 - for _, v := range hdata { - total += v.Contributions - } - return total -} diff --git a/routers/web/user/heatmap.go b/routers/web/user/heatmap.go new file mode 100644 index 0000000000..e81739e5b8 --- /dev/null +++ b/routers/web/user/heatmap.go @@ -0,0 +1,66 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "net/http" + "net/url" + + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/context" +) + +func prepareHeatmapURL(ctx *context.Context) { + ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap + if !setting.Service.EnableUserHeatmap { + return + } + + if ctx.Org.Organization == nil { + // for individual user + ctx.Data["HeatmapURL"] = ctx.Doer.HomeLink() + "/-/heatmap" + return + } + + // for org or team + heatmapURL := ctx.Org.Organization.OrganisationLink() + "/dashboard/-/heatmap" + if ctx.Org.Team != nil { + heatmapURL += "/" + url.PathEscape(ctx.Org.Team.LowerName) + } + ctx.Data["HeatmapURL"] = heatmapURL +} + +func writeHeatmapJSON(ctx *context.Context, hdata []*activities_model.UserHeatmapData) { + data := make([][2]int64, len(hdata)) + var total int64 + for i, v := range hdata { + data[i] = [2]int64{int64(v.Timestamp), v.Contributions} + total += v.Contributions + } + ctx.JSON(http.StatusOK, map[string]any{ + "heatmapData": data, + "totalContributions": total, + }) +} + +// DashboardHeatmap returns heatmap data as JSON, for the individual user, organization or team dashboard. +func DashboardHeatmap(ctx *context.Context) { + if !setting.Service.EnableUserHeatmap { + ctx.NotFound(nil) + return + } + var data []*activities_model.UserHeatmapData + var err error + if ctx.Org.Organization == nil { + data, err = activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) + } else { + data, err = activities_model.GetUserHeatmapDataByOrgTeam(ctx, ctx.Org.Organization, ctx.Org.Team, ctx.Doer) + } + if err != nil { + ctx.ServerError("GetUserHeatmapData", err) + return + } + writeHeatmapJSON(ctx, data) +} diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 9e77c51d12..afdba9a75f 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -54,8 +54,8 @@ const ( tplProfile templates.TplName = "user/profile" ) -// getDashboardContextUser finds out which context user dashboard is being viewed as . -func getDashboardContextUser(ctx *context.Context) *user_model.User { +// prepareDashboardContextUserOrgTeams finds out which context user dashboard is being viewed as . +func prepareDashboardContextUserOrgTeams(ctx *context.Context) *user_model.User { ctxUser := ctx.Doer orgName := ctx.PathParam("org") if len(orgName) > 0 { @@ -76,7 +76,7 @@ func getDashboardContextUser(ctx *context.Context) *user_model.User { // Dashboard render the dashboard page func Dashboard(ctx *context.Context) { - ctxUser := getDashboardContextUser(ctx) + ctxUser := prepareDashboardContextUserOrgTeams(ctx) if ctx.Written() { return } @@ -109,15 +109,7 @@ func Dashboard(ctx *context.Context) { "uid": uid, } - if setting.Service.EnableUserHeatmap { - data, err := activities_model.GetUserHeatmapDataByUserTeam(ctx, ctxUser, ctx.Org.Team, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserHeatmapDataByUserTeam", err) - return - } - ctx.Data["HeatmapData"] = data - ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) - } + prepareHeatmapURL(ctx) feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctxUser, @@ -156,7 +148,7 @@ func Milestones(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("milestones") ctx.Data["PageIsMilestonesDashboard"] = true - ctxUser := getDashboardContextUser(ctx) + ctxUser := prepareDashboardContextUserOrgTeams(ctx) if ctx.Written() { return } @@ -371,7 +363,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // Return with NotFound or ServerError if unsuccessful. // ---------------------------------------------------- - ctxUser := getDashboardContextUser(ctx) + ctxUser := prepareDashboardContextUserOrgTeams(ctx) if ctx.Written() { return } diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index d7052914b6..26c5884bd5 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -161,15 +161,9 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R ctx.Data["Cards"] = following total = int(numFollowing) case "activity": - // prepare heatmap data - if setting.Service.EnableUserHeatmap { - data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserHeatmapDataByUser", err) - return - } - ctx.Data["HeatmapData"] = data - ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) + if setting.Service.EnableUserHeatmap && activities_model.ActivityReadable(ctx.ContextUser, ctx.Doer) { + ctx.Data["EnableHeatmap"] = true + ctx.Data["HeatmapURL"] = ctx.ContextUser.HomeLink() + "/-/heatmap" } date := ctx.FormString("date") diff --git a/routers/web/web.go b/routers/web/web.go index 22b78793ef..9e6354e138 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -888,6 +888,8 @@ func registerWebRoutes(m *web.Router) { m.Group("/{org}", func() { m.Get("/dashboard", user.Dashboard) m.Get("/dashboard/{team}", user.Dashboard) + m.Get("/dashboard/-/heatmap", user.DashboardHeatmap) + m.Get("/dashboard/-/heatmap/{team}", user.DashboardHeatmap) m.Get("/issues", user.Issues) m.Get("/issues/{team}", user.Issues) m.Get("/pulls", user.Pulls) @@ -1024,6 +1026,7 @@ func registerWebRoutes(m *web.Router) { } m.Get("/repositories", org.Repositories) + m.Get("/heatmap", user.DashboardHeatmap) m.Group("/projects", func() { m.Group("", func() { diff --git a/templates/user/heatmap.tmpl b/templates/user/heatmap.tmpl index 6186edd4dd..22368e78c1 100644 --- a/templates/user/heatmap.tmpl +++ b/templates/user/heatmap.tmpl @@ -1,8 +1,8 @@ -{{if .HeatmapData}} +{{if .EnableHeatmap}}
; // [[1617235200, 2]] = [unix timestamp, count] + totalContributions: number; +}; + +export async function initHeatmap() { + const el = document.querySelector('#user-heatmap'); if (!el) return; try { + const url = el.getAttribute('data-heatmap-url')!; + const resp = await GET(url); + if (!resp.ok) throw new Error(`Failed to load heatmap data: ${resp.status} ${resp.statusText}`); + const {heatmapData, totalContributions} = await resp.json() as HeatmapResponse; + const heatmap: Record = {}; - for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data')!)) { + for (const [timestamp, contributions] of heatmapData) { // Convert to user timezone and sum contributions by date const dateStr = new Date(timestamp * 1000).toDateString(); heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions; @@ -18,6 +29,9 @@ export function initHeatmap() { return {date: new Date(v), count: heatmap[v]}; }); + const totalFormatted = totalContributions.toLocaleString(); + const textTotalContributions = el.getAttribute('data-locale-total-contributions')!.replace('%s', totalFormatted); + // last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8 const locale = { heatMapLocale: { @@ -28,7 +42,7 @@ export function initHeatmap() { less: el.getAttribute('data-locale-less'), }, tooltipUnit: 'contributions', - textTotalContributions: el.getAttribute('data-locale-total-contributions'), + textTotalContributions, noDataText: el.getAttribute('data-locale-no-contributions'), };