From d9c0f86de8bcfd566c6ddd97ea36860ab39ab71b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 3 Nov 2025 12:52:13 -0800 Subject: [PATCH 01/52] Fix incorrect pull request counter (#35819) Fix #35781, #27472 The PR will not correct the wrong numbers automatically. There is a cron task `check_repo_stats` which will be run when Gitea start or midnight. It will correct the numbers. --- models/actions/main_test.go | 2 + models/actions/run.go | 2 + models/actions/run_test.go | 35 +++++++++++++++ models/activities/notification.go | 2 +- models/asymkey/gpg_key_verify.go | 2 +- models/fixtures/repository.yml | 2 + models/git/branch.go | 2 +- models/issues/comment.go | 5 +-- models/issues/issue_update.go | 55 ++++++++++++++++++++++-- models/issues/milestone.go | 1 + models/issues/pull.go | 2 +- models/migrations/v1_18/v229.go | 1 + models/repo/topic.go | 2 +- services/issue/issue.go | 9 +--- tests/integration/pull_create_test.go | 56 ++++++++++++++++++++++++ tests/integration/pull_merge_test.go | 61 +++++++++++++++++++++++++++ 16 files changed, 218 insertions(+), 21 deletions(-) create mode 100644 models/actions/run_test.go diff --git a/models/actions/main_test.go b/models/actions/main_test.go index 5d5089e3bb..4af483813a 100644 --- a/models/actions/main_test.go +++ b/models/actions/main_test.go @@ -13,6 +13,8 @@ func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ FixtureFiles: []string{ "action_runner_token.yml", + "action_run.yml", + "repository.yml", }, }) } diff --git a/models/actions/run.go b/models/actions/run.go index 4da6958e2d..be332d6857 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -193,9 +193,11 @@ func (run *ActionRun) IsSchedule() bool { return run.ScheduleID > 0 } +// UpdateRepoRunsNumbers updates the number of runs and closed runs of a repository. func UpdateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { _, err := db.GetEngine(ctx).ID(repo.ID). NoAutoTime(). + Cols("num_action_runs", "num_closed_action_runs"). SetExpr("num_action_runs", builder.Select("count(*)").From("action_run"). Where(builder.Eq{"repo_id": repo.ID}), diff --git a/models/actions/run_test.go b/models/actions/run_test.go new file mode 100644 index 0000000000..0986f87516 --- /dev/null +++ b/models/actions/run_test.go @@ -0,0 +1,35 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestUpdateRepoRunsNumbers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // update the number to a wrong one, the original is 3 + _, err := db.GetEngine(t.Context()).ID(4).Cols("num_closed_action_runs").Update(&repo_model.Repository{ + NumClosedActionRuns: 2, + }) + assert.NoError(t, err) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + assert.Equal(t, 4, repo.NumActionRuns) + assert.Equal(t, 2, repo.NumClosedActionRuns) + + // now update will correct them, only num_actionr_runs and num_closed_action_runs should be updated + err = UpdateRepoRunsNumbers(t.Context(), repo) + assert.NoError(t, err) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + assert.Equal(t, 4, repo.NumActionRuns) + assert.Equal(t, 3, repo.NumClosedActionRuns) +} diff --git a/models/activities/notification.go b/models/activities/notification.go index b482e6020a..8a830c5aa2 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -386,7 +386,7 @@ func SetNotificationStatus(ctx context.Context, notificationID int64, user *user notification.Status = status - _, err = db.GetEngine(ctx).ID(notificationID).Update(notification) + _, err = db.GetEngine(ctx).ID(notificationID).Cols("status").Update(notification) return notification, err } diff --git a/models/asymkey/gpg_key_verify.go b/models/asymkey/gpg_key_verify.go index 55c64973b4..5df0265c88 100644 --- a/models/asymkey/gpg_key_verify.go +++ b/models/asymkey/gpg_key_verify.go @@ -78,7 +78,7 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st } key.Verified = true - if _, err := db.GetEngine(ctx).ID(key.ID).SetExpr("verified", true).Update(new(GPGKey)); err != nil { + if _, err := db.GetEngine(ctx).ID(key.ID).Cols("verified").Update(key); err != nil { return "", err } diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 552a78cbd2..dfa514db37 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -110,6 +110,8 @@ num_closed_milestones: 0 num_projects: 0 num_closed_projects: 1 + num_action_runs: 4 + num_closed_action_runs: 3 is_private: false is_empty: false is_archived: false diff --git a/models/git/branch.go b/models/git/branch.go index 54351649cc..7fef9f5ca3 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -368,7 +368,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str } // 1. update branch in database - if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{ + if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Cols("name").Update(&Branch{ Name: to, }); err != nil { return err diff --git a/models/issues/comment.go b/models/issues/comment.go index 3a4049700d..fd0500833e 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -862,10 +862,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment if err = UpdateCommentAttachments(ctx, comment, opts.Attachments); err != nil { return err } - case CommentTypeReopen, CommentTypeClose: - if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil { - return err - } + // comment type reopen and close event have their own logic to update numbers but not here } // update the issue's updated_unix column return UpdateIssueCols(ctx, opts.Issue, "updated_unix") diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 553e99aece..0a320ffc56 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -146,8 +146,19 @@ func updateIssueNumbers(ctx context.Context, issue *Issue, doer *user_model.User } // update repository's issue closed number - if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil { - return nil, err + switch cmtType { + case CommentTypeClose, CommentTypeMergePull: + // only increase closed count + if err := IncrRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil { + return nil, err + } + case CommentTypeReopen: + // only decrease closed count + if err := DecrRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false, true); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("invalid comment type: %d", cmtType) } return CreateComment(ctx, &CreateCommentOptions{ @@ -318,7 +329,6 @@ type NewIssueOptions struct { Issue *Issue LabelIDs []int64 Attachments []string // In UUID format. - IsPull bool } // NewIssueWithIndex creates issue with given index @@ -369,7 +379,8 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue } } - if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil { + // Update repository issue total count + if err := IncrRepoIssueNumbers(ctx, opts.Repo.ID, opts.Issue.IsPull, true); err != nil { return err } @@ -439,6 +450,42 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la }) } +// IncrRepoIssueNumbers increments repository issue numbers. +func IncrRepoIssueNumbers(ctx context.Context, repoID int64, isPull, totalOrClosed bool) error { + dbSession := db.GetEngine(ctx) + var colName string + if totalOrClosed { + colName = util.Iif(isPull, "num_pulls", "num_issues") + } else { + colName = util.Iif(isPull, "num_closed_pulls", "num_closed_issues") + } + _, err := dbSession.Incr(colName).ID(repoID). + NoAutoCondition().NoAutoTime(). + Update(new(repo_model.Repository)) + return err +} + +// DecrRepoIssueNumbers decrements repository issue numbers. +func DecrRepoIssueNumbers(ctx context.Context, repoID int64, isPull, includeTotal, includeClosed bool) error { + if !includeTotal && !includeClosed { + return fmt.Errorf("no numbers to decrease for repo id %d", repoID) + } + + dbSession := db.GetEngine(ctx) + if includeTotal { + colName := util.Iif(isPull, "num_pulls", "num_issues") + dbSession = dbSession.Decr(colName) + } + if includeClosed { + closedColName := util.Iif(isPull, "num_closed_pulls", "num_closed_issues") + dbSession = dbSession.Decr(closedColName) + } + _, err := dbSession.ID(repoID). + NoAutoCondition().NoAutoTime(). + Update(new(repo_model.Repository)) + return err +} + // UpdateIssueMentions updates issue-user relations for mentioned users. func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error { if len(mentions) == 0 { diff --git a/models/issues/milestone.go b/models/issues/milestone.go index 373f39f4ff..82a82ac913 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -181,6 +181,7 @@ func updateMilestone(ctx context.Context, m *Milestone) error { func UpdateMilestoneCounters(ctx context.Context, id int64) error { e := db.GetEngine(ctx) _, err := e.ID(id). + Cols("num_issues", "num_closed_issues"). SetExpr("num_issues", builder.Select("count(*)").From("issue").Where( builder.Eq{"milestone_id": id}, )). diff --git a/models/issues/pull.go b/models/issues/pull.go index fb7dff3cc9..1ffcd683d5 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -467,13 +467,13 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss issue.Index = idx issue.Title = util.EllipsisDisplayString(issue.Title, 255) + issue.IsPull = true if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, Issue: issue, LabelIDs: labelIDs, Attachments: uuids, - IsPull: true, }); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { return err diff --git a/models/migrations/v1_18/v229.go b/models/migrations/v1_18/v229.go index bc15e01390..1f69724365 100644 --- a/models/migrations/v1_18/v229.go +++ b/models/migrations/v1_18/v229.go @@ -21,6 +21,7 @@ func UpdateOpenMilestoneCounts(x *xorm.Engine) error { for _, id := range openMilestoneIDs { _, err := x.ID(id). + Cols("num_issues", "num_closed_issues"). SetExpr("num_issues", builder.Select("count(*)").From("issue").Where( builder.Eq{"milestone_id": id}, )). diff --git a/models/repo/topic.go b/models/repo/topic.go index baeae01efa..f8f706fc1a 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -159,7 +159,7 @@ func RemoveTopicsFromRepo(ctx context.Context, repoID int64) error { builder.In("id", builder.Select("topic_id").From("repo_topic").Where(builder.Eq{"repo_id": repoID}), ), - ).Cols("repo_count").SetExpr("repo_count", "repo_count-1").Update(&Topic{}) + ).Decr("repo_count").Update(&Topic{}) if err != nil { return err } diff --git a/services/issue/issue.go b/services/issue/issue.go index 62b330f8e2..85e70d0761 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -270,16 +270,9 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) ([]string, erro return nil, err } - // update the total issue numbers - if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil { + if err := issues_model.DecrRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true, issue.IsClosed); err != nil { return nil, err } - // if the issue is closed, update the closed issue numbers - if issue.IsClosed { - if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil { - return nil, err - } - } if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { return nil, fmt.Errorf("error updating counters for milestone id %d: %w", diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index d9811d000f..ddafdf33b8 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -10,9 +10,12 @@ import ( "net/url" "path" "strings" + "sync" "testing" auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" @@ -137,8 +140,15 @@ func TestPullCreate(t *testing.T) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 3, repo1.NumPulls) + assert.Equal(t, 3, repo1.NumOpenPulls) resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title") + repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 4, repo1.NumPulls) + assert.Equal(t, 4, repo1.NumOpenPulls) + // check the redirected URL url := test.RedirectURL(resp) assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url) @@ -285,6 +295,44 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) { }) } +func TestPullCreateParallel(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + sessionFork := loginUser(t, "user1") + testRepoFork(t, sessionFork, "user2", "repo1", "user1", "repo1", "") + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 3, repo1.NumPulls) + assert.Equal(t, 3, repo1.NumOpenPulls) + + var wg sync.WaitGroup + for i := range 5 { + wg.Go(func() { + branchName := fmt.Sprintf("new-branch-%d", i) + testEditFileToNewBranch(t, sessionFork, "user1", "repo1", "master", branchName, "README.md", fmt.Sprintf("Hello, World (Edited) %d\n", i)) + + // Create a PR + resp := testPullCreateDirectly(t, sessionFork, createPullRequestOptions{ + BaseRepoOwner: "user2", + BaseRepoName: "repo1", + BaseBranch: "master", + HeadRepoOwner: "user1", + HeadRepoName: "repo1", + HeadBranch: branchName, + Title: fmt.Sprintf("This is a pull title %d", i), + }) + // check the redirected URL + url := test.RedirectURL(resp) + assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url) + }) + } + wg.Wait() + + repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 8, repo1.NumPulls) + assert.Equal(t, 8, repo1.NumOpenPulls) + }) +} + func TestCreateAgitPullWithReadPermission(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { dstPath := t.TempDir() @@ -300,11 +348,19 @@ func TestCreateAgitPullWithReadPermission(t *testing.T) { TreeFileContent: "temp content", })(t) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 3, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) + err := gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master", "-o"). AddDynamicArguments("topic=test-topic"). WithDir(dstPath). Run(t.Context()) assert.NoError(t, err) + + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 4, repo.NumOpenPulls) }) } diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 062be3ae7a..f273d9fb3a 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -113,8 +113,16 @@ func TestPullMerge(t *testing.T) { testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 3, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) + resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title") + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 4, repo.NumOpenPulls) + elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ @@ -122,6 +130,10 @@ func TestPullMerge(t *testing.T) { DeleteBranch: false, }) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) + hookTasks, err = webhook.HookTasks(t.Context(), 1, 1) assert.NoError(t, err) assert.Len(t, hookTasks, hookTasksLenBefore+1) @@ -138,8 +150,16 @@ func TestPullRebase(t *testing.T) { testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 3, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) + resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title") + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 4, repo.NumOpenPulls) + elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ @@ -147,6 +167,10 @@ func TestPullRebase(t *testing.T) { DeleteBranch: false, }) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) + hookTasks, err = webhook.HookTasks(t.Context(), 1, 1) assert.NoError(t, err) assert.Len(t, hookTasks, hookTasksLenBefore+1) @@ -163,8 +187,16 @@ func TestPullRebaseMerge(t *testing.T) { testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 3, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) + resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title") + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 4, repo.NumOpenPulls) + elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ @@ -172,6 +204,10 @@ func TestPullRebaseMerge(t *testing.T) { DeleteBranch: false, }) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) + hookTasks, err = webhook.HookTasks(t.Context(), 1, 1) assert.NoError(t, err) assert.Len(t, hookTasks, hookTasksLenBefore+1) @@ -215,6 +251,10 @@ func TestPullSquashWithHeadCommitID(t *testing.T) { testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n") + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 3, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) + resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title") repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) @@ -224,11 +264,19 @@ func TestPullSquashWithHeadCommitID(t *testing.T) { elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) + + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 4, repo.NumOpenPulls) + testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ Style: repo_model.MergeStyleSquash, DeleteBranch: false, HeadCommitID: headBranch.CommitID, }) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) hookTasks, err = webhook.HookTasks(t.Context(), 1, 1) assert.NoError(t, err) @@ -242,15 +290,28 @@ func TestPullCleanUpAfterMerge(t *testing.T) { testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n") + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + assert.Equal(t, 3, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) + resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title") elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) + + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 4, repo.NumOpenPulls) + testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ Style: repo_model.MergeStyleMerge, DeleteBranch: false, }) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Equal(t, 4, repo.NumPulls) + assert.Equal(t, 3, repo.NumOpenPulls) + // Check PR branch deletion resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4]) respJSON := struct { From de26c8accee744eb8d04688d95ece3988767ab1b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 3 Nov 2025 13:34:52 -0800 Subject: [PATCH 02/52] Fix viewed files number is not right if not all files loaded (#35821) Fix #35803 --------- Signed-off-by: silverwind Co-authored-by: silverwind --- models/pull/review_state.go | 13 +++++++++++++ routers/web/repo/pull.go | 7 ++++++- services/gitdiff/gitdiff.go | 8 +++----- templates/repo/diff/box.tmpl | 4 ++-- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/models/pull/review_state.go b/models/pull/review_state.go index 137af00eab..e8b759c0cc 100644 --- a/models/pull/review_state.go +++ b/models/pull/review_state.go @@ -49,6 +49,19 @@ func init() { db.RegisterModel(new(ReviewState)) } +func (rs *ReviewState) GetViewedFileCount() int { + if len(rs.UpdatedFiles) == 0 { + return 0 + } + var numViewedFiles int + for _, state := range rs.UpdatedFiles { + if state == Viewed { + numViewedFiles++ + } + } + return numViewedFiles +} + // GetReviewState returns the ReviewState with all given values prefilled, whether or not it exists in the database. // If the review didn't exist before in the database, it won't afterwards either. // The returned boolean shows whether the review exists in the database diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 0f9f551e12..17e3bf2b78 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -782,12 +782,16 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { // as the viewed information is designed to be loaded only on latest PR // diff and if you're signed in. var reviewState *pull_model.ReviewState + var numViewedFiles int if ctx.IsSigned && isShowAllCommits { reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions) if err != nil { ctx.ServerError("SyncUserSpecificDiff", err) return } + if reviewState != nil { + numViewedFiles = reviewState.GetViewedFileCount() + } } diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, beforeCommitID, afterCommitID) @@ -796,10 +800,11 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { return } ctx.Data["DiffShortStat"] = diffShortStat + ctx.Data["NumViewedFiles"] = numViewedFiles ctx.PageData["prReview"] = map[string]any{ "numberOfFiles": diffShortStat.NumFiles, - "numberOfViewedFiles": diff.NumViewedFiles, + "numberOfViewedFiles": numViewedFiles, } if err = diff.LoadComments(ctx, issue, ctx.Doer, ctx.Data["ShowOutdatedComments"].(bool)); err != nil { diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 96aea8308c..830bb1131b 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -520,10 +520,9 @@ func getCommitFileLineCountAndLimitedContent(commit *git.Commit, filePath string // Diff represents a difference between two git trees. type Diff struct { - Start, End string - Files []*DiffFile - IsIncomplete bool - NumViewedFiles int // user-specific + Start, End string + Files []*DiffFile + IsIncomplete bool } // LoadComments loads comments into each line @@ -1412,7 +1411,6 @@ outer: // Check whether the file has already been viewed if fileViewedState == pull_model.Viewed { diffFile.IsViewed = true - diff.NumViewedFiles++ } } diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 7eb96e1ddc..ff9bd2e792 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -27,9 +27,9 @@ {{if and .PageIsPullFiles $.SignedUserID (not .DiffNotAvailable)}}
- +
{{end}} {{template "repo/diff/whitespace_dropdown" .}} From bb1f52347a2313991bdbd34a9888cf03a800cfb6 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Mon, 3 Nov 2025 19:32:26 -0700 Subject: [PATCH 03/52] Add a doctor command to fix inconsistent run status (#35840) #35783 fixes an actions rerun bug. Due to this bug, some runs may be incorrectly marked as `StatusWaiting` even though all the jobs are in done status. These runs cannot be run or cancelled. This PR adds a new doctor command to fix the inconsistent run status. ``` gitea doctor check --run fix-actions-unfinished-run-status --fix ``` Thanks to @ChristopherHX for the test. --- models/actions/run_test.go | 2 +- models/fixtures/action_run.yml | 20 ++++++ models/fixtures/action_run_job.yml | 14 ++++ models/fixtures/action_task.yml | 19 ++++++ models/fixtures/repo_unit.yml | 7 ++ services/doctor/actions.go | 101 +++++++++++++++++++++++++++++ services/doctor/actions_test.go | 24 +++++++ 7 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 services/doctor/actions_test.go diff --git a/models/actions/run_test.go b/models/actions/run_test.go index 0986f87516..bd2b92f4f6 100644 --- a/models/actions/run_test.go +++ b/models/actions/run_test.go @@ -30,6 +30,6 @@ func TestUpdateRepoRunsNumbers(t *testing.T) { err = UpdateRepoRunsNumbers(t.Context(), repo) assert.NoError(t, err) repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - assert.Equal(t, 4, repo.NumActionRuns) + assert.Equal(t, 5, repo.NumActionRuns) assert.Equal(t, 3, repo.NumClosedActionRuns) } diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index b9688dd5f5..44b131c961 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -159,3 +159,23 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 805 + title: "update actions" + repo_id: 4 + owner_id: 1 + workflow_id: "artifact.yaml" + index: 191 + trigger_user_id: 1 + ref: "refs/heads/master" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 5 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 337e83605a..c5aeb4931c 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -143,3 +143,17 @@ status: 1 started: 1683636528 stopped: 1683636626 +- + id: 206 + run_id: 805 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 56 + status: 3 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index e09fd6f2ec..a28ddd0add 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -197,3 +197,22 @@ log_length: 707 log_size: 90179 log_expired: 0 +- + id: 56 + attempt: 1 + runner_id: 1 + status: 3 # 3 is the status code for "cancelled" + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4240c64a69a2cc1508825121b7b8394e48e00b1bf3718b2aaaab + token_salt: eeeeeeee + token_last_eight: eeeeeeee + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index f8bb8ef0d3..4c3e37500f 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -740,3 +740,10 @@ type: 10 config: "{}" created_unix: 946684810 + +- + id: 112 + repo_id: 4 + type: 10 + config: "{}" + created_unix: 946684810 diff --git a/services/doctor/actions.go b/services/doctor/actions.go index 28e26c88eb..cd3d19b724 100644 --- a/services/doctor/actions.go +++ b/services/doctor/actions.go @@ -7,12 +7,17 @@ import ( "context" "fmt" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" repo_service "code.gitea.io/gitea/services/repository" + + "xorm.io/builder" ) func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bool) error { @@ -59,6 +64,95 @@ func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bo return nil } +func fixUnfinishedRunStatus(ctx context.Context, logger log.Logger, autofix bool) error { + total := 0 + inconsistent := 0 + fixed := 0 + + cond := builder.In("status", []actions_model.Status{ + actions_model.StatusWaiting, + actions_model.StatusRunning, + actions_model.StatusBlocked, + }).And(builder.Lt{"updated": timeutil.TimeStampNow().AddDuration(-setting.Actions.ZombieTaskTimeout)}) + + err := db.Iterate( + ctx, + cond, + func(ctx context.Context, run *actions_model.ActionRun) error { + total++ + + jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + if err != nil { + return fmt.Errorf("GetRunJobsByRunID: %w", err) + } + expected := actions_model.AggregateJobStatus(jobs) + if expected == run.Status { + return nil + } + + inconsistent++ + logger.Warn("Run %d (repo_id=%d, index=%d) has status %s, expected %s", run.ID, run.RepoID, run.Index, run.Status, expected) + + if !autofix { + return nil + } + + run.Started, run.Stopped = getRunTimestampsFromJobs(run, expected, jobs) + run.Status = expected + + if err := actions_model.UpdateRun(ctx, run, "status", "started", "stopped"); err != nil { + return fmt.Errorf("UpdateRun: %w", err) + } + fixed++ + + return nil + }, + ) + if err != nil { + logger.Critical("Unable to iterate unfinished runs: %v", err) + return err + } + + if inconsistent == 0 { + logger.Info("Checked %d unfinished runs; all statuses are consistent.", total) + return nil + } + + if autofix { + logger.Info("Checked %d unfinished runs; fixed %d of %d runs.", total, fixed, inconsistent) + } else { + logger.Warn("Checked %d unfinished runs; found %d runs need to be fixed", total, inconsistent) + } + + return nil +} + +func getRunTimestampsFromJobs(run *actions_model.ActionRun, newStatus actions_model.Status, jobs actions_model.ActionJobList) (started, stopped timeutil.TimeStamp) { + started = run.Started + if (newStatus.IsRunning() || newStatus.IsDone()) && started.IsZero() { + var earliest timeutil.TimeStamp + for _, job := range jobs { + if job.Started > 0 && (earliest.IsZero() || job.Started < earliest) { + earliest = job.Started + } + } + started = earliest + } + + stopped = run.Stopped + if newStatus.IsDone() && stopped.IsZero() { + var latest timeutil.TimeStamp + for _, job := range jobs { + if job.Stopped > latest { + latest = job.Stopped + } + } + stopped = latest + } + + return started, stopped +} + func init() { Register(&Check{ Title: "Disable the actions unit for all mirrors", @@ -67,4 +161,11 @@ func init() { Run: disableMirrorActionsUnit, Priority: 9, }) + Register(&Check{ + Title: "Fix inconsistent status for unfinished actions runs", + Name: "fix-actions-unfinished-run-status", + IsDefault: false, + Run: fixUnfinishedRunStatus, + Priority: 9, + }) } diff --git a/services/doctor/actions_test.go b/services/doctor/actions_test.go new file mode 100644 index 0000000000..b2fd3d0d55 --- /dev/null +++ b/services/doctor/actions_test.go @@ -0,0 +1,24 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package doctor + +import ( + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/log" + + "github.com/stretchr/testify/assert" +) + +func Test_fixUnfinishedRunStatus(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + fixUnfinishedRunStatus(t.Context(), log.GetLogger(log.DEFAULT), true) + + // check if the run is cancelled by id + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 805}) + assert.Equal(t, actions_model.StatusCancelled, run.Status) +} From 850012bf5c0807908771d3cb155afaebf2742cc8 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 4 Nov 2025 04:03:06 +0100 Subject: [PATCH 04/52] Update golangci-lint to v2.6.0 (#35801) https://github.com/golangci/golangci-lint/releases/tag/v2.6.0 - `modernize` linter is enabled, this is the same as `gopls modernize` - ~~`perfsprint` linter is disabled because it conflicts with `modernize` (maybe there is a middle ground)~~ - gocritic `deprecatedComment` is disabled as it conflicts with `go-swagger` --- .golangci.yml | 7 +++++++ Makefile | 2 +- models/user/user.go | 4 ++-- services/wiki/wiki_path.go | 4 ++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 483843bc55..60482c415f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,6 +14,7 @@ linters: - govet - ineffassign - mirror + - modernize - nakedret - nolintlint - perfsprint @@ -55,6 +56,7 @@ linters: disabled-checks: - ifElseChain - singleCaseSwitch # Every time this occurred in the code, there was no other way. + - deprecatedComment # conflicts with go-swagger comments revive: severity: error rules: @@ -107,6 +109,11 @@ linters: - require-error usetesting: os-temp-dir: true + modernize: + disable: + - stringsbuilder + perfsprint: + concat-loop: false exclusions: generated: lax presets: diff --git a/Makefile b/Makefile index 7531e56d83..ffa7471aa0 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ XGO_VERSION := go-1.25.x AIR_PACKAGE ?= github.com/air-verse/air@v1 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.0 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 diff --git a/models/user/user.go b/models/user/user.go index d6e1eec276..925be83713 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1262,8 +1262,8 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { } // Finally, if email address is the protected email address: - if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) { - username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress) + 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 { diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go index 212a35ea25..fc032244b5 100644 --- a/services/wiki/wiki_path.go +++ b/services/wiki/wiki_path.go @@ -129,8 +129,8 @@ func GitPathToWebPath(s string) (wp WebPath, err error) { func WebPathToUserTitle(s WebPath) (dir, display string) { dir = path.Dir(string(s)) display = path.Base(string(s)) - if strings.HasSuffix(display, ".md") { - display = strings.TrimSuffix(display, ".md") + if before, ok := strings.CutSuffix(display, ".md"); ok { + display = before display, _ = url.PathUnescape(display) } display, _ = unescapeSegment(display) From 2be51d0b2733bbc3ef9313f3a18ea4a89d6f122c Mon Sep 17 00:00:00 2001 From: Naxdy Date: Tue, 4 Nov 2025 17:28:59 +0100 Subject: [PATCH 05/52] Port away from `flake-utils` (#35675) `flake-utils` is currently only used for outputting system-specific dev shells. This can actually be achieved only using functionality already present within `nixpkgs`, thus there is no need for an extra dependency. Additionally, we move to use the `packages` and `env` args for `mkShell` to more clearly outline what they are used for. --- Further reading: https://determinate.systems/blog/best-practices-for-nix-at-work/#avoid-flake-helper-libraries-if-possible As a side note, using `with` to import large scopes is [discouraged by official Nix resources](https://nix.dev/guides/best-practices#with-scopes), so an alternative approach to list installed packages could be something like this: ```nix packages = (builtins.attrValues { inherit (pkgs) # generic git git-lfs gnumake gnused gnutar gzip zip # frontend cairo pixman pkg-config # linting uv # backend gofumpt sqlite ; inherit # frontend nodejs pnpm # linting python3 # backend go ; }) ++ linuxOnlyInputs; ``` But I saw this as too pedantic to include in the initial PR. Co-authored-by: 6543 <6543@obermui.de> --- flake.lock | 34 ------------- flake.nix | 139 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 80 insertions(+), 93 deletions(-) diff --git a/flake.lock b/flake.lock index 5cb95c1aed..4cbc85b87a 100644 --- a/flake.lock +++ b/flake.lock @@ -1,23 +1,5 @@ { "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1760038930, @@ -36,24 +18,8 @@ }, "root": { "inputs": { - "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 588f608ffc..6fb3891963 100644 --- a/flake.nix +++ b/flake.nix @@ -1,73 +1,94 @@ { inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; }; outputs = - { nixpkgs, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem ( - system: - let - pkgs = nixpkgs.legacyPackages.${system}; - in - { - devShells.default = - with pkgs; - let - # only bump toolchain versions here - go = go_1_25; - nodejs = nodejs_24; - python3 = python312; - pnpm = pnpm_10; - - # Platform-specific dependencies - linuxOnlyInputs = lib.optionals pkgs.stdenv.isLinux [ - glibc.static - ]; + { nixpkgs, ... }: + let + supportedSystems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-darwin" + "x86_64-linux" + ]; - linuxOnlyEnv = lib.optionalAttrs pkgs.stdenv.isLinux { - CFLAGS = "-I${glibc.static.dev}/include"; - LDFLAGS = "-L ${glibc.static}/lib"; + forEachSupportedSystem = + f: + nixpkgs.lib.genAttrs supportedSystems ( + system: + let + pkgs = import nixpkgs { + inherit system; }; in - pkgs.mkShell ( - { - buildInputs = [ - # generic - git - git-lfs - gnumake - gnused - gnutar - gzip - zip + f { inherit pkgs; } + ); + in + { + devShells = forEachSupportedSystem ( + { pkgs, ... }: + { + default = + let + inherit (pkgs) lib; + + # only bump toolchain versions here + go = pkgs.go_1_25; + nodejs = pkgs.nodejs_24; + python3 = pkgs.python312; + pnpm = pkgs.pnpm_10; - # frontend - nodejs - pnpm - cairo - pixman - pkg-config + # Platform-specific dependencies + linuxOnlyInputs = lib.optionals pkgs.stdenv.isLinux [ + pkgs.glibc.static + ]; - # linting - python3 - uv + linuxOnlyEnv = lib.optionalAttrs pkgs.stdenv.isLinux { + CFLAGS = "-I${pkgs.glibc.static.dev}/include"; + LDFLAGS = "-L ${pkgs.glibc.static}/lib"; + }; + in + pkgs.mkShell { + packages = + with pkgs; + [ + # generic + git + git-lfs + gnumake + gnused + gnutar + gzip + zip - # backend - go - gofumpt - sqlite - ] - ++ linuxOnlyInputs; + # frontend + nodejs + pnpm + cairo + pixman + pkg-config - GO = "${go}/bin/go"; - GOROOT = "${go}/share/go"; + # linting + python3 + uv - TAGS = "sqlite sqlite_unlock_notify"; - STATIC = "true"; - } - // linuxOnlyEnv - ); - } - ); + # backend + go + gofumpt + sqlite + ] + ++ linuxOnlyInputs; + + env = { + GO = "${go}/bin/go"; + GOROOT = "${go}/share/go"; + + TAGS = "sqlite sqlite_unlock_notify"; + STATIC = "true"; + } + // linuxOnlyEnv; + }; + } + ); + }; } From 851db772566fb23dc650614b840270d7d8647d00 Mon Sep 17 00:00:00 2001 From: Cory Sanin Date: Tue, 4 Nov 2025 11:07:04 -0600 Subject: [PATCH 06/52] Fix Arch repo pacman.conf snippet (#35825) Current template uses the owner followed by the instance URL as the repo name. Technically this can work if the repo happens to be named the exact same way. But if, for example, you follow [the docs](https://docs.gitea.com/usage/packages/arch/#publish-a-package), you'll end up with a package in `core` while the pacman conf refers to a non-existent repo `testuser.gitea.example.com`. Whatever is in the square brackets get substituted in for `$repo`, so we do not want anything except the exact repo name there. And since it's now referring to the repo and not the owner, I've updated the pacman conf to show all repositories. --------- Co-authored-by: Giteabot --- templates/package/content/arch.tmpl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/package/content/arch.tmpl b/templates/package/content/arch.tmpl index 1c568cbb78..6ce18affac 100644 --- a/templates/package/content/arch.tmpl +++ b/templates/package/content/arch.tmpl @@ -4,9 +4,11 @@
-
[{{.PackageDescriptor.Owner.LowerName}}.{{.PackageRegistryHost}}]
+				
{{range $i, $repo := .Repositories}}{{if $i}}
+{{end}}[{{$repo}}]
 SigLevel = Optional TrustAll
-Server = 
+Server = +{{end}}
From 206f4c88b1adc4d45671d7be8aa90949e36bdb89 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 4 Nov 2025 09:46:17 -0800 Subject: [PATCH 07/52] Remove wrong code (#35846) Follow #35821 Fix https://github.com/go-gitea/gitea/pull/35844#issuecomment-3483521045 The reviewed file numbers and progress have been set from backend so that we don't need to update the numbers when clicking `load more`. --- web_src/js/features/pull-view-file.ts | 8 -------- web_src/js/features/repo-diff.ts | 3 +-- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/web_src/js/features/pull-view-file.ts b/web_src/js/features/pull-view-file.ts index 1124886238..e563c13ef5 100644 --- a/web_src/js/features/pull-view-file.ts +++ b/web_src/js/features/pull-view-file.ts @@ -20,14 +20,6 @@ function refreshViewedFilesSummary() { .replace('%[2]d', prReview.numberOfFiles); } -// Explicitly recounts how many files the user has currently reviewed by counting the number of checked "viewed" checkboxes -// Additionally, the viewed files summary will be updated if it exists -export function countAndUpdateViewedFiles() { - // The number of files is constant, but the number of viewed files can change because files can be loaded dynamically - prReview.numberOfViewedFiles = document.querySelectorAll(`${viewedCheckboxSelector} > input[type=checkbox][checked]`).length; - refreshViewedFilesSummary(); -} - // Initializes a listener for all children of the given html element // (for example 'document' in the most basic case) // to watch for changes of viewed-file checkboxes diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index 20cec2939d..6f5cb2f63b 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -2,7 +2,7 @@ import {initRepoIssueContentHistory} from './repo-issue-content.ts'; import {initDiffFileTree} from './repo-diff-filetree.ts'; import {initDiffCommitSelect} from './repo-diff-commitselect.ts'; import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.ts'; -import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.ts'; +import {initViewedCheckboxListenerFor, initExpandAndCollapseFilesButton} from './pull-view-file.ts'; import {initImageDiff} from './imagediff.ts'; import {showErrorToast} from '../modules/toast.ts'; import {submitEventSubmitter, queryElemSiblings, hideElem, showElem, animateOnce, addDelegatedEventListener, createElementFromHTML, queryElems} from '../utils/dom.ts'; @@ -152,7 +152,6 @@ function onShowMoreFiles() { // TODO: replace these calls with the "observer.ts" methods initRepoIssueContentHistory(); initViewedCheckboxListenerFor(); - countAndUpdateViewedFiles(); initImageDiff(); initDiffHeaderPopup(); } From a0f492d9f43a915262256e6c8a5afdecce180a7d Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 5 Nov 2025 02:17:50 +0800 Subject: [PATCH 08/52] Make ACME email optional (#35849) Fix a regression from #33668 Fix #35847 --- modules/setting/server.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/setting/server.go b/modules/setting/server.go index 38e166e02a..cedca32da9 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -235,9 +235,6 @@ func loadServerFrom(rootCfg ConfigProvider) { deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0") AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("") } - if AcmeEmail == "" { - log.Fatal("ACME Email is not set (ACME_EMAIL).") - } } else { CertFile = sec.Key("CERT_FILE").String() KeyFile = sec.Key("KEY_FILE").String() From d0ca2f6bc316b653829fa0d0890fd59698fac3bc Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 5 Nov 2025 18:18:26 +0100 Subject: [PATCH 09/52] Fix pull description code label background (#35865) Fix visual regression from https://github.com/go-gitea/gitea/pull/35567: Before: image After: image --- web_src/css/repo.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 070623d24e..206a591fbd 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -387,6 +387,7 @@ td .commit-summary { .repository.view.issue .pull-desc code { color: var(--color-primary); + background: transparent; } .repository.view.issue .pull-desc a[data-clipboard-text] { From 525265c1a8fda77b265bb4bdc59ca77bff003620 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 6 Nov 2025 01:48:38 +0800 Subject: [PATCH 10/52] Refactor ls-tree and git path related problems (#35858) Fix #35852, the root problem is that the "name" field is heavily abused (since #6816, and no way to get a clear fix) There are still a lot of legacy problems in old code. Co-authored-by: Giteabot --- modules/base/natural_sort.go | 12 +- modules/base/natural_sort_test.go | 6 +- modules/git/commit_info_test.go | 1 - modules/git/notes_gogit.go | 7 +- modules/git/parse_gogit.go | 96 ---------------- modules/git/parse_gogit_test.go | 78 ------------- .../{parse_nogogit.go => parse_treeentry.go} | 2 - ...ogogit_test.go => parse_treeentry_test.go} | 2 - modules/git/repo_commit_gogit.go | 2 +- modules/git/repo_tree_gogit.go | 2 +- modules/git/tree.go | 14 ++- modules/git/tree_blob_gogit.go | 14 +-- modules/git/tree_entry.go | 105 ++++++++++-------- modules/git/tree_entry_gogit.go | 61 +++------- modules/git/tree_entry_gogit_test.go | 27 +++++ modules/git/tree_entry_nogogit.go | 46 -------- modules/git/tree_entry_test.go | 58 +++------- modules/git/tree_gogit.go | 74 +++++------- modules/git/tree_nogogit.go | 24 +--- routers/web/repo/commit.go | 2 + routers/web/repo/treelist.go | 2 +- routers/web/repo/view.go | 2 +- routers/web/repo/view_readme.go | 2 +- routers/web/repo/wiki.go | 2 +- 24 files changed, 192 insertions(+), 449 deletions(-) delete mode 100644 modules/git/parse_gogit.go delete mode 100644 modules/git/parse_gogit_test.go rename modules/git/{parse_nogogit.go => parse_treeentry.go} (99%) rename modules/git/{parse_nogogit_test.go => parse_treeentry_test.go} (99%) create mode 100644 modules/git/tree_entry_gogit_test.go diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go index acb9002276..d1ee7b04ec 100644 --- a/modules/base/natural_sort.go +++ b/modules/base/natural_sort.go @@ -41,8 +41,8 @@ func naturalSortAdvance(str string, pos int) (end int, isNumber bool) { return end, isNumber } -// NaturalSortLess compares two strings so that they could be sorted in natural order -func NaturalSortLess(s1, s2 string) bool { +// NaturalSortCompare compares two strings so that they could be sorted in natural order +func NaturalSortCompare(s1, s2 string) int { // There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997 // text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997 // So we need to handle the number parts by ourselves @@ -55,16 +55,16 @@ func NaturalSortLess(s1, s2 string) bool { if isNum1 && isNum2 { if part1 != part2 { if len(part1) != len(part2) { - return len(part1) < len(part2) + return len(part1) - len(part2) } - return part1 < part2 + return c.CompareString(part1, part2) } } else { if cmp := c.CompareString(part1, part2); cmp != 0 { - return cmp < 0 + return cmp } } pos1, pos2 = end1, end2 } - return len(s1) < len(s2) + return len(s1) - len(s2) } diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go index b001bc4ac9..451aba6618 100644 --- a/modules/base/natural_sort_test.go +++ b/modules/base/natural_sort_test.go @@ -11,12 +11,10 @@ import ( func TestNaturalSortLess(t *testing.T) { testLess := func(s1, s2 string) { - assert.True(t, NaturalSortLess(s1, s2), "s1 2 { diff --git a/modules/git/parse_gogit.go b/modules/git/parse_gogit.go deleted file mode 100644 index 74d258de8e..0000000000 --- a/modules/git/parse_gogit.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build gogit - -package git - -import ( - "bytes" - "fmt" - "strconv" - "strings" - - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/hash" - "github.com/go-git/go-git/v5/plumbing/object" -) - -// ParseTreeEntries parses the output of a `git ls-tree -l` command. -func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { - return parseTreeEntries(data, nil) -} - -func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { - entries := make([]*TreeEntry, 0, 10) - for pos := 0; pos < len(data); { - // expect line to be of the form " \t" - entry := new(TreeEntry) - entry.gogitTreeEntry = &object.TreeEntry{} - entry.ptree = ptree - if pos+6 > len(data) { - return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) - } - switch string(data[pos : pos+6]) { - case "100644": - entry.gogitTreeEntry.Mode = filemode.Regular - pos += 12 // skip over "100644 blob " - case "100755": - entry.gogitTreeEntry.Mode = filemode.Executable - pos += 12 // skip over "100755 blob " - case "120000": - entry.gogitTreeEntry.Mode = filemode.Symlink - pos += 12 // skip over "120000 blob " - case "160000": - entry.gogitTreeEntry.Mode = filemode.Submodule - pos += 14 // skip over "160000 object " - case "040000": - entry.gogitTreeEntry.Mode = filemode.Dir - pos += 12 // skip over "040000 tree " - default: - return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6])) - } - - // in hex format, not byte format .... - if pos+hash.Size*2 > len(data) { - return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) - } - var err error - entry.ID, err = NewIDFromString(string(data[pos : pos+hash.Size*2])) - if err != nil { - return nil, fmt.Errorf("invalid ls-tree output: %w", err) - } - entry.gogitTreeEntry.Hash = plumbing.Hash(entry.ID.RawValue()) - pos += 41 // skip over sha and trailing space - - end := pos + bytes.IndexByte(data[pos:], '\t') - if end < pos { - return nil, fmt.Errorf("Invalid ls-tree -l output: %s", string(data)) - } - entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(data[pos:end])), 10, 64) - entry.sized = true - - pos = end + 1 - - end = pos + bytes.IndexByte(data[pos:], '\n') - if end < pos { - return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) - } - - // In case entry name is surrounded by double quotes(it happens only in git-shell). - if data[pos] == '"' { - var err error - entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end])) - if err != nil { - return nil, fmt.Errorf("Invalid ls-tree output: %w", err) - } - } else { - entry.gogitTreeEntry.Name = string(data[pos:end]) - } - - pos = end + 1 - entries = append(entries, entry) - } - return entries, nil -} diff --git a/modules/git/parse_gogit_test.go b/modules/git/parse_gogit_test.go deleted file mode 100644 index 3e171d7e56..0000000000 --- a/modules/git/parse_gogit_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build gogit - -package git - -import ( - "fmt" - "testing" - - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/stretchr/testify/assert" -) - -func TestParseTreeEntries(t *testing.T) { - testCases := []struct { - Input string - Expected []*TreeEntry - }{ - { - Input: "", - Expected: []*TreeEntry{}, - }, - { - Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 1022\texample/file2.txt\n", - Expected: []*TreeEntry{ - { - ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), - gogitTreeEntry: &object.TreeEntry{ - Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()), - Name: "example/file2.txt", - Mode: filemode.Regular, - }, - size: 1022, - sized: true, - }, - }, - }, - { - Input: "120000 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 234131\t\"example/\\n.txt\"\n" + - "040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8 -\texample\n", - Expected: []*TreeEntry{ - { - ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), - gogitTreeEntry: &object.TreeEntry{ - Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()), - Name: "example/\n.txt", - Mode: filemode.Symlink, - }, - size: 234131, - sized: true, - }, - { - ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"), - sized: true, - gogitTreeEntry: &object.TreeEntry{ - Hash: plumbing.Hash(MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8").RawValue()), - Name: "example", - Mode: filemode.Dir, - }, - }, - }, - }, - } - - for _, testCase := range testCases { - entries, err := ParseTreeEntries([]byte(testCase.Input)) - assert.NoError(t, err) - if len(entries) > 1 { - fmt.Println(testCase.Expected[0].ID) - fmt.Println(entries[0].ID) - } - assert.EqualValues(t, testCase.Expected, entries) - } -} diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_treeentry.go similarity index 99% rename from modules/git/parse_nogogit.go rename to modules/git/parse_treeentry.go index 78a0162889..e14d9f17b5 100644 --- a/modules/git/parse_nogogit.go +++ b/modules/git/parse_treeentry.go @@ -1,8 +1,6 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build !gogit - package git import ( diff --git a/modules/git/parse_nogogit_test.go b/modules/git/parse_treeentry_test.go similarity index 99% rename from modules/git/parse_nogogit_test.go rename to modules/git/parse_treeentry_test.go index 6594c84269..4223cbb3d7 100644 --- a/modules/git/parse_nogogit_test.go +++ b/modules/git/parse_treeentry_test.go @@ -1,8 +1,6 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build !gogit - package git import ( diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go index 896d656039..c84aabde1a 100644 --- a/modules/git/repo_commit_gogit.go +++ b/modules/git/repo_commit_gogit.go @@ -107,7 +107,7 @@ func (repo *Repository) getCommit(id ObjectID) (*Commit, error) { } commit.Tree.ID = ParseGogitHash(tree.Hash) - commit.Tree.gogitTree = tree + commit.Tree.resolvedGogitTreeObject = tree return commit, nil } diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go index e15663a32a..89d34e87da 100644 --- a/modules/git/repo_tree_gogit.go +++ b/modules/git/repo_tree_gogit.go @@ -26,7 +26,7 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { } tree := NewTree(repo, id) - tree.gogitTree = gogitTree + tree.resolvedGogitTreeObject = gogitTree return tree, nil } diff --git a/modules/git/tree.go b/modules/git/tree.go index 9c73aec735..c1898b20cb 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -11,11 +11,21 @@ import ( "code.gitea.io/gitea/modules/git/gitcmd" ) +type TreeCommon struct { + ID ObjectID + ResolvedID ObjectID + + repo *Repository + ptree *Tree // parent tree +} + // NewTree create a new tree according the repository and tree id func NewTree(repo *Repository, id ObjectID) *Tree { return &Tree{ - ID: id, - repo: repo, + TreeCommon: TreeCommon{ + ID: id, + repo: repo, + }, } } diff --git a/modules/git/tree_blob_gogit.go b/modules/git/tree_blob_gogit.go index f29e8f8b9e..2c0ff0e1b0 100644 --- a/modules/git/tree_blob_gogit.go +++ b/modules/git/tree_blob_gogit.go @@ -11,22 +11,16 @@ import ( "strings" "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/object" ) // GetTreeEntryByPath get the tree entries according the sub dir func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { if len(relpath) == 0 { return &TreeEntry{ - ID: t.ID, - // Type: ObjectTree, - ptree: t, - gogitTreeEntry: &object.TreeEntry{ - Name: "", - Mode: filemode.Dir, - Hash: plumbing.Hash(t.ID.RawValue()), - }, + ID: t.ID, + ptree: t, + name: "", + entryMode: EntryModeTree, }, nil } diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go index 5099d8ee79..e7e4ea2d5b 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -6,12 +6,60 @@ package git import ( "path" - "sort" + "slices" "strings" "code.gitea.io/gitea/modules/util" ) +// TreeEntry the leaf in the git tree +type TreeEntry struct { + ID ObjectID + + name string + ptree *Tree + + entryMode EntryMode + + size int64 + sized bool +} + +// Name returns the name of the entry (base name) +func (te *TreeEntry) Name() string { + return te.name +} + +// Mode returns the mode of the entry +func (te *TreeEntry) Mode() EntryMode { + return te.entryMode +} + +// IsSubModule if the entry is a submodule +func (te *TreeEntry) IsSubModule() bool { + return te.entryMode.IsSubModule() +} + +// IsDir if the entry is a sub dir +func (te *TreeEntry) IsDir() bool { + return te.entryMode.IsDir() +} + +// IsLink if the entry is a symlink +func (te *TreeEntry) IsLink() bool { + return te.entryMode.IsLink() +} + +// IsRegular if the entry is a regular file +func (te *TreeEntry) IsRegular() bool { + return te.entryMode.IsRegular() +} + +// IsExecutable if the entry is an executable file (not necessarily binary) +func (te *TreeEntry) IsExecutable() bool { + return te.entryMode.IsExecutable() +} + // Type returns the type of the entry (commit, tree, blob) func (te *TreeEntry) Type() string { switch te.Mode() { @@ -109,49 +157,16 @@ func (te *TreeEntry) GetSubJumpablePathName() string { // Entries a list of entry type Entries []*TreeEntry -type customSortableEntries struct { - Comparer func(s1, s2 string) bool - Entries -} - -var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{ - func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool { - return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule() - }, - func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool { - return cmp(t1.Name(), t2.Name()) - }, -} - -func (ctes customSortableEntries) Len() int { return len(ctes.Entries) } - -func (ctes customSortableEntries) Swap(i, j int) { - ctes.Entries[i], ctes.Entries[j] = ctes.Entries[j], ctes.Entries[i] -} - -func (ctes customSortableEntries) Less(i, j int) bool { - t1, t2 := ctes.Entries[i], ctes.Entries[j] - var k int - for k = 0; k < len(sorter)-1; k++ { - s := sorter[k] - switch { - case s(t1, t2, ctes.Comparer): - return true - case s(t2, t1, ctes.Comparer): - return false - } - } - return sorter[k](t1, t2, ctes.Comparer) -} - -// Sort sort the list of entry -func (tes Entries) Sort() { - sort.Sort(customSortableEntries{func(s1, s2 string) bool { - return s1 < s2 - }, tes}) -} - // CustomSort customizable string comparing sort entry list -func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) { - sort.Sort(customSortableEntries{cmp, tes}) +func (tes Entries) CustomSort(cmp func(s1, s2 string) int) { + slices.SortFunc(tes, func(a, b *TreeEntry) int { + s1Dir, s2Dir := a.IsDir() || a.IsSubModule(), b.IsDir() || b.IsSubModule() + if s1Dir != s2Dir { + if s1Dir { + return -1 + } + return 1 + } + return cmp(a.Name(), b.Name()) + }) } diff --git a/modules/git/tree_entry_gogit.go b/modules/git/tree_entry_gogit.go index e6845f1c77..27877a2e28 100644 --- a/modules/git/tree_entry_gogit.go +++ b/modules/git/tree_entry_gogit.go @@ -12,25 +12,21 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" ) -// TreeEntry the leaf in the git tree -type TreeEntry struct { - ID ObjectID - - gogitTreeEntry *object.TreeEntry - ptree *Tree - - size int64 - sized bool +// gogitFileModeToEntryMode converts go-git filemode to EntryMode +func gogitFileModeToEntryMode(mode filemode.FileMode) EntryMode { + return EntryMode(mode) } -// Name returns the name of the entry -func (te *TreeEntry) Name() string { - return te.gogitTreeEntry.Name +func entryModeToGogitFileMode(mode EntryMode) filemode.FileMode { + return filemode.FileMode(mode) } -// Mode returns the mode of the entry -func (te *TreeEntry) Mode() EntryMode { - return EntryMode(te.gogitTreeEntry.Mode) +func (te *TreeEntry) toGogitTreeEntry() *object.TreeEntry { + return &object.TreeEntry{ + Name: te.name, + Mode: entryModeToGogitFileMode(te.entryMode), + Hash: plumbing.Hash(te.ID.RawValue()), + } } // Size returns the size of the entry @@ -41,7 +37,11 @@ func (te *TreeEntry) Size() int64 { return te.size } - file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry) + ptreeGogitTree, err := te.ptree.gogitTreeObject() + if err != nil { + return 0 + } + file, err := ptreeGogitTree.TreeEntryFile(te.toGogitTreeEntry()) if err != nil { return 0 } @@ -51,40 +51,15 @@ func (te *TreeEntry) Size() int64 { return te.size } -// IsSubModule if the entry is a submodule -func (te *TreeEntry) IsSubModule() bool { - return te.gogitTreeEntry.Mode == filemode.Submodule -} - -// IsDir if the entry is a sub dir -func (te *TreeEntry) IsDir() bool { - return te.gogitTreeEntry.Mode == filemode.Dir -} - -// IsLink if the entry is a symlink -func (te *TreeEntry) IsLink() bool { - return te.gogitTreeEntry.Mode == filemode.Symlink -} - -// IsRegular if the entry is a regular file -func (te *TreeEntry) IsRegular() bool { - return te.gogitTreeEntry.Mode == filemode.Regular -} - -// IsExecutable if the entry is an executable file (not necessarily binary) -func (te *TreeEntry) IsExecutable() bool { - return te.gogitTreeEntry.Mode == filemode.Executable -} - // Blob returns the blob object the entry func (te *TreeEntry) Blob() *Blob { - encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash) + encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.toGogitTreeEntry().Hash) if err != nil { return nil } return &Blob{ - ID: ParseGogitHash(te.gogitTreeEntry.Hash), + ID: te.ID, gogitEncodedObj: encodedObj, name: te.Name(), } diff --git a/modules/git/tree_entry_gogit_test.go b/modules/git/tree_entry_gogit_test.go new file mode 100644 index 0000000000..ed14b45e9e --- /dev/null +++ b/modules/git/tree_entry_gogit_test.go @@ -0,0 +1,27 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build gogit + +package git + +import ( + "testing" + + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/stretchr/testify/assert" +) + +func TestEntryGogit(t *testing.T) { + cases := map[EntryMode]filemode.FileMode{ + EntryModeBlob: filemode.Regular, + EntryModeCommit: filemode.Submodule, + EntryModeExec: filemode.Executable, + EntryModeSymlink: filemode.Symlink, + EntryModeTree: filemode.Dir, + } + for emode, fmode := range cases { + assert.EqualValues(t, fmode, entryModeToGogitFileMode(emode)) + assert.EqualValues(t, emode, gogitFileModeToEntryMode(fmode)) + } +} diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 8fad96cdf8..fd2f3c567f 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -7,27 +7,6 @@ package git import "code.gitea.io/gitea/modules/log" -// TreeEntry the leaf in the git tree -type TreeEntry struct { - ID ObjectID - ptree *Tree - - entryMode EntryMode - name string - size int64 - sized bool -} - -// Name returns the name of the entry (base name) -func (te *TreeEntry) Name() string { - return te.name -} - -// Mode returns the mode of the entry -func (te *TreeEntry) Mode() EntryMode { - return te.entryMode -} - // Size returns the size of the entry func (te *TreeEntry) Size() int64 { if te.IsDir() { @@ -57,31 +36,6 @@ func (te *TreeEntry) Size() int64 { return te.size } -// IsSubModule if the entry is a submodule -func (te *TreeEntry) IsSubModule() bool { - return te.entryMode.IsSubModule() -} - -// IsDir if the entry is a sub dir -func (te *TreeEntry) IsDir() bool { - return te.entryMode.IsDir() -} - -// IsLink if the entry is a symlink -func (te *TreeEntry) IsLink() bool { - return te.entryMode.IsLink() -} - -// IsRegular if the entry is a regular file -func (te *TreeEntry) IsRegular() bool { - return te.entryMode.IsRegular() -} - -// IsExecutable if the entry is an executable file (not necessarily binary) -func (te *TreeEntry) IsExecutable() bool { - return te.entryMode.IsExecutable() -} - // Blob returns the blob object the entry func (te *TreeEntry) Blob() *Blob { return &Blob{ diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go index 9ca82675e0..b28abfb545 100644 --- a/modules/git/tree_entry_test.go +++ b/modules/git/tree_entry_test.go @@ -1,55 +1,29 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build gogit - package git import ( + "math/rand/v2" + "slices" + "strings" "testing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/object" "github.com/stretchr/testify/assert" ) -func getTestEntries() Entries { - return Entries{ - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v1.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.1", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.12", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.2", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v12.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "abc", Mode: filemode.Regular}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "bcd", Mode: filemode.Regular}}, - } -} - -func TestEntriesSort(t *testing.T) { - entries := getTestEntries() - entries.Sort() - assert.Equal(t, "v1.0", entries[0].Name()) - assert.Equal(t, "v12.0", entries[1].Name()) - assert.Equal(t, "v2.0", entries[2].Name()) - assert.Equal(t, "v2.1", entries[3].Name()) - assert.Equal(t, "v2.12", entries[4].Name()) - assert.Equal(t, "v2.2", entries[5].Name()) - assert.Equal(t, "abc", entries[6].Name()) - assert.Equal(t, "bcd", entries[7].Name()) -} - func TestEntriesCustomSort(t *testing.T) { - entries := getTestEntries() - entries.CustomSort(func(s1, s2 string) bool { - return s1 > s2 - }) - assert.Equal(t, "v2.2", entries[0].Name()) - assert.Equal(t, "v2.12", entries[1].Name()) - assert.Equal(t, "v2.1", entries[2].Name()) - assert.Equal(t, "v2.0", entries[3].Name()) - assert.Equal(t, "v12.0", entries[4].Name()) - assert.Equal(t, "v1.0", entries[5].Name()) - assert.Equal(t, "bcd", entries[6].Name()) - assert.Equal(t, "abc", entries[7].Name()) + entries := Entries{ + &TreeEntry{name: "a-dir", entryMode: EntryModeTree}, + &TreeEntry{name: "a-submodule", entryMode: EntryModeCommit}, + &TreeEntry{name: "b-dir", entryMode: EntryModeTree}, + &TreeEntry{name: "b-submodule", entryMode: EntryModeCommit}, + &TreeEntry{name: "a-file", entryMode: EntryModeBlob}, + &TreeEntry{name: "b-file", entryMode: EntryModeBlob}, + } + expected := slices.Clone(entries) + rand.Shuffle(len(entries), func(i, j int) { entries[i], entries[j] = entries[j], entries[i] }) + assert.NotEqual(t, expected, entries) + entries.CustomSort(strings.Compare) + assert.Equal(t, expected, entries) } diff --git a/modules/git/tree_gogit.go b/modules/git/tree_gogit.go index 272b018ffd..fec6e2704e 100644 --- a/modules/git/tree_gogit.go +++ b/modules/git/tree_gogit.go @@ -15,41 +15,34 @@ import ( // Tree represents a flat directory listing. type Tree struct { - ID ObjectID - ResolvedID ObjectID - repo *Repository + TreeCommon - gogitTree *object.Tree - - // parent tree - ptree *Tree + resolvedGogitTreeObject *object.Tree } -func (t *Tree) loadTreeObject() error { - gogitTree, err := t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue())) - if err != nil { - return err +func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) { + if t.resolvedGogitTreeObject == nil { + t.resolvedGogitTreeObject, err = t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue())) + if err != nil { + return nil, err + } } - - t.gogitTree = gogitTree - return nil + return t.resolvedGogitTreeObject, nil } // ListEntries returns all entries of current tree. func (t *Tree) ListEntries() (Entries, error) { - if t.gogitTree == nil { - err := t.loadTreeObject() - if err != nil { - return nil, err - } + gogitTree, err := t.gogitTreeObject() + if err != nil { + return nil, err } - - entries := make([]*TreeEntry, len(t.gogitTree.Entries)) - for i, entry := range t.gogitTree.Entries { + entries := make([]*TreeEntry, len(gogitTree.Entries)) + for i, gogitTreeEntry := range gogitTree.Entries { entries[i] = &TreeEntry{ - ID: ParseGogitHash(entry.Hash), - gogitTreeEntry: &t.gogitTree.Entries[i], - ptree: t, + ID: ParseGogitHash(gogitTreeEntry.Hash), + ptree: t, + name: gogitTreeEntry.Name, + entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode), } } @@ -57,37 +50,28 @@ func (t *Tree) ListEntries() (Entries, error) { } // ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees -func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) { - if t.gogitTree == nil { - err := t.loadTreeObject() - if err != nil { - return nil, err - } +func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) { + gogitTree, err := t.gogitTreeObject() + if err != nil { + return nil, err } - var entries []*TreeEntry - seen := map[plumbing.Hash]bool{} - walker := object.NewTreeWalker(t.gogitTree, true, seen) + walker := object.NewTreeWalker(gogitTree, true, nil) for { - _, entry, err := walker.Next() + fullName, gogitTreeEntry, err := walker.Next() if err == io.EOF { break - } - if err != nil { + } else if err != nil { return nil, err } - if seen[entry.Hash] { - continue - } - convertedEntry := &TreeEntry{ - ID: ParseGogitHash(entry.Hash), - gogitTreeEntry: &entry, - ptree: t, + ID: ParseGogitHash(gogitTreeEntry.Hash), + name: fullName, // FIXME: the "name" field is abused, here it is a full path + ptree: t, // FIXME: this ptree is not right, fortunately it isn't really used + entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode), } entries = append(entries, convertedEntry) } - return entries, nil } diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index 956a5938f0..d0ddb1d041 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -14,18 +14,10 @@ import ( // Tree represents a flat directory listing. type Tree struct { - ID ObjectID - ResolvedID ObjectID - repo *Repository - - // parent tree - ptree *Tree + TreeCommon entries Entries entriesParsed bool - - entriesRecursive Entries - entriesRecursiveParsed bool } // ListEntries returns all entries of current tree. @@ -94,10 +86,6 @@ func (t *Tree) ListEntries() (Entries, error) { // listEntriesRecursive returns all entries of current tree recursively including all subtrees // extraArgs could be "-l" to get the size, which is slower func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, error) { - if t.entriesRecursiveParsed { - return t.entriesRecursive, nil - } - stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r"). AddArguments(extraArgs...). AddDynamicArguments(t.ID.String()). @@ -107,13 +95,9 @@ func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, e return nil, runErr } - var err error - t.entriesRecursive, err = parseTreeEntries(stdout, t) - if err == nil { - t.entriesRecursiveParsed = true - } - - return t.entriesRecursive, err + // FIXME: the "name" field is abused, here it is a full path + // FIXME: this ptree is not right, fortunately it isn't really used + return parseTreeEntries(stdout, t) } // ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 0383e4ca9e..6bb9a8ae77 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -415,6 +415,8 @@ func Diff(ctx *context.Context) { ctx.ServerError("PostProcessCommitMessage", err) return } + } else if !git.IsErrNotExist(err) { + log.Error("GetNote: %v", err) } pr, _ := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, commitID) diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go index 340b2bc091..8a3ed0a1c9 100644 --- a/routers/web/repo/treelist.go +++ b/routers/web/repo/treelist.go @@ -33,7 +33,7 @@ func TreeList(ctx *context.Context) { ctx.ServerError("ListEntriesRecursiveFast", err) return } - entries.CustomSort(base.NaturalSortLess) + entries.CustomSort(base.NaturalSortCompare) files := make([]string, 0, len(entries)) for _, entry := range entries { diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 1d05a3aa51..79357bfd76 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -307,7 +307,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri ctx.ServerError("ListEntries", err) return nil } - allEntries.CustomSort(base.NaturalSortLess) + allEntries.CustomSort(base.NaturalSortCompare) commitInfoCtx := gocontext.Context(ctx) if timeout > 0 { diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index edf38b7892..f1fa5732f0 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -67,7 +67,7 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []* for _, entry := range entries { if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok { fullPath := path.Join(parentDir, entry.Name()) - if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) { + if readmeFiles[i] == nil || base.NaturalSortCompare(readmeFiles[i].Name(), entry.Blob().Name()) < 0 { if entry.IsLink() { res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry) if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 289db11a4f..e7c34ba1d6 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -567,7 +567,7 @@ func WikiPages(ctx *context.Context) { ctx.ServerError("ListEntries", err) return } - allEntries.CustomSort(base.NaturalSortLess) + allEntries.CustomSort(base.NaturalSortCompare) entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath) if err != nil { From 84d7496b9d3280072d838a7b5ddc00b1fa86ee09 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 5 Nov 2025 19:20:20 +0100 Subject: [PATCH 11/52] Remove `fix` Make targets (#35868) Since `modernize` is now included in `golangci-lint` since https://github.com/go-gitea/gitea/commit/850012bf5c0807908771d3cb155afaebf2742cc8, it makes not sense to have this as a separate make target anymore. --- Makefile | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Makefile b/Makefile index ffa7471aa0..9e7a4ed166 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,6 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1 GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0 -GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest @@ -276,19 +275,6 @@ fmt-check: fmt exit 1; \ fi -.PHONY: fix -fix: ## apply automated fixes to Go code - $(GO) run $(GOPLS_MODERNIZE_PACKAGE) -fix ./... - -.PHONY: fix-check -fix-check: fix - @diff=$$(git diff --color=always $(GO_SOURCES)); \ - if [ -n "$$diff" ]; then \ - echo "Please run 'make fix' and commit the result:"; \ - printf "%s" "$${diff}"; \ - exit 1; \ - fi - .PHONY: $(TAGS_EVIDENCE) $(TAGS_EVIDENCE): @mkdir -p $(MAKE_EVIDENCE_DIR) @@ -328,7 +314,7 @@ checks: checks-frontend checks-backend ## run various consistency checks checks-frontend: lockfile-check svg-check ## check frontend files .PHONY: checks-backend -checks-backend: tidy-check swagger-check fmt-check fix-check swagger-validate security-check ## check backend files +checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files .PHONY: lint lint: lint-frontend lint-backend lint-spell ## lint everything @@ -852,7 +838,6 @@ deps-tools: ## install tool dependencies $(GO) install $(GOVULNCHECK_PACKAGE) & \ $(GO) install $(ACTIONLINT_PACKAGE) & \ $(GO) install $(GOPLS_PACKAGE) & \ - $(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \ wait node_modules: pnpm-lock.yaml From 61e5cc173e011b59d6176afb7803e8112081dff1 Mon Sep 17 00:00:00 2001 From: Divyun Raje Vaid Date: Thu, 6 Nov 2025 00:22:24 +0530 Subject: [PATCH 12/52] fix(api/repo/contents): set the dates to now when not specified by the caller (#35861) Since 1.25.0, the dates get set to `2001-01-01T00:00:00Z`, when not specified by the caller. Fixes #35860 Co-authored-by: Giteabot --- routers/api/v1/repo/file.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index ba98263819..ec34d54d22 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -370,11 +370,11 @@ func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) { }, Signoff: commonOpts.Signoff, } - if commonOpts.Dates.Author.IsZero() { - commonOpts.Dates.Author = time.Now() + if changeFileOpts.Dates.Author.IsZero() { + changeFileOpts.Dates.Author = time.Now() } - if commonOpts.Dates.Committer.IsZero() { - commonOpts.Dates.Committer = time.Now() + if changeFileOpts.Dates.Committer.IsZero() { + changeFileOpts.Dates.Committer = time.Now() } ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts } From 23a37b4b77ba7ec7f1079fe5b573fe4cc10fb142 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 6 Nov 2025 02:32:39 +0100 Subject: [PATCH 13/52] Remove padding override on `.ui .sha.label` (#35864) Since upgrading to v1.25, I noticed the SHA labels have slightly different padding than before. I can't pinpoint exactly which change it was. Fix it by removing the padding override on `.ui .sha.label` and make the one on`.ui.label` (`2px 6px`) take effect which matches 1.24 rendering. Before: image After: image --- web_src/css/base.css | 1 - 1 file changed, 1 deletion(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index a09839ea1e..be28cd6fea 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -626,7 +626,6 @@ img.ui.avatar, font-family: var(--fonts-monospace); font-size: 13px; font-weight: var(--font-weight-normal); - padding: 3px 5px; flex-shrink: 0; } From aaa8033ee943691ed7a43c61c09781f7acd72b8f Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 6 Nov 2025 07:04:38 +0100 Subject: [PATCH 14/52] Update to go 1.25.4 (#35877) https://tip.golang.org/doc/devel/release#go1.25.4 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 81187804a3..bfdfb06e2a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module code.gitea.io/gitea -go 1.25.3 +go 1.25.4 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: From eef9406c6b373afdb701e89fc1f1c1a15b0ced58 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 6 Nov 2025 09:23:48 +0100 Subject: [PATCH 15/52] Contribution heatmap improvements (#35876) 1. Set a fixed height on the element, preventing the content after the element from shifting on page load. This uses CSS [container query length units](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries#container_query_length_units) as I saw no other way because of the non-linear scaling of the element. 2. Move the "total-contributions" text into the existing vue slot, eliminating the need for absolute positioning. --------- Co-authored-by: wxiaoguang --- templates/user/heatmap.tmpl | 4 +- web_src/css/features/heatmap.css | 57 +++++++++++------------ web_src/js/components/ActivityHeatmap.vue | 7 ++- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/templates/user/heatmap.tmpl b/templates/user/heatmap.tmpl index b604b929a3..6186edd4dd 100644 --- a/templates/user/heatmap.tmpl +++ b/templates/user/heatmap.tmpl @@ -1,4 +1,5 @@ {{if .HeatmapData}} +
-
+
+
{{end}} diff --git a/web_src/css/features/heatmap.css b/web_src/css/features/heatmap.css index c064590c46..e40adf1fe4 100644 --- a/web_src/css/features/heatmap.css +++ b/web_src/css/features/heatmap.css @@ -4,23 +4,44 @@ position: relative; } -/* before the Vue component is mounted, show a loading indicator with dummy size */ -/* the ratio is guesswork, see https://github.com/razorness/vue3-calendar-heatmap/issues/26 */ -#user-heatmap.is-loading { - aspect-ratio: 5.415; /* the size is about 790 x 145 */ +.activity-heatmap-container { + container-type: inline-size; } -.user.profile #user-heatmap.is-loading { - aspect-ratio: 5.645; /* the size is about 953 x 169 */ + +@container (width > 0) { + #user-heatmap { + /* Set element to fixed height so that it does not resize after load. The calculation is complex + because the element does not scale with a fixed aspect ratio. */ + height: calc((100cqw / 5) - (100cqw / 25) + 20px); + } +} + +/* Fallback height adjustment above for browsers that don't support container queries */ +@supports not (container-type: inline-size) { + /* Before the Vue component is mounted, show a loading indicator with dummy size */ + /* The ratio is guesswork for legacy browsers, new browsers use the "@container" approach above */ + #user-heatmap.is-loading { + aspect-ratio: 5.4823972051; /* the size is about 816 x 148.84 */ + } + .user.profile #user-heatmap.is-loading { + aspect-ratio: 5.6290608387; /* the size is about 953 x 169.3 */ + } } #user-heatmap text { fill: currentcolor !important; } +/* root legend */ +#user-heatmap .vch__container > .vch__legend { + display: flex; + font-size: 11px; + justify-content: space-between; +} + /* for the "Less" and "More" legend */ #user-heatmap .vch__legend .vch__legend { display: flex; - font-size: 11px; align-items: center; justify-content: right; } @@ -34,25 +55,3 @@ #user-heatmap .vch__day__square:hover { outline: 1.5px solid var(--color-text); } - -/* move the "? contributions in the last ? months" text from top to bottom */ -#user-heatmap .total-contributions { - font-size: 11px; - position: absolute; - bottom: 0; - left: 25px; -} - -@media (max-width: 1200px) { - #user-heatmap .total-contributions { - left: 21px; - } -} - -@media (max-width: 1000px) { - #user-heatmap .total-contributions { - font-size: 10px; - left: 17px; - bottom: -4px; - } -} diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue index 296cb61cff..d805817630 100644 --- a/web_src/js/components/ActivityHeatmap.vue +++ b/web_src/js/components/ActivityHeatmap.vue @@ -53,9 +53,6 @@ function handleDayClick(e: Event & {date: Date}) { } From b2feeddf42622388450b04b710f2bf5487f8b950 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 6 Nov 2025 21:09:31 +0100 Subject: [PATCH 16/52] Move `gitea-vet` to use `go tool` (#35878) Add it as a [tool dependency](https://go.dev/doc/modules/managing-dependencies#tools), eliminating the need for `build.go`. --- .github/labeler.yml | 1 - Makefile | 3 +-- build.go | 14 -------------- go.mod | 4 +++- 4 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 build.go diff --git a/.github/labeler.yml b/.github/labeler.yml index 49679d28cf..750f2b2cfb 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -51,7 +51,6 @@ modifies/internal: - ".github/**" - ".gitea/**" - ".devcontainer/**" - - "build.go" - "build/**" - "contrib/**" diff --git a/Makefile b/Makefile index 9e7a4ed166..de2a486ea1 100644 --- a/Makefile +++ b/Makefile @@ -386,8 +386,7 @@ lint-go-windows: .PHONY: lint-go-gitea-vet lint-go-gitea-vet: ## lint go files with gitea-vet @echo "Running gitea-vet..." - @GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet - @$(GO) vet -vettool=gitea-vet ./... + @$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./... .PHONY: lint-go-gopls lint-go-gopls: ## lint go files with gopls diff --git a/build.go b/build.go deleted file mode 100644 index e81ba54690..0000000000 --- a/build.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build vendor - -package main - -// Libraries that are included to vendor utilities used during Makefile build. -// These libraries will not be included in a normal compilation. - -import ( - // for vet - _ "code.gitea.io/gitea-vet" -) diff --git a/go.mod b/go.mod index bfdfb06e2a..2be7558e55 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ godebug x509negativeserial=1 require ( code.gitea.io/actions-proto-go v0.4.1 - code.gitea.io/gitea-vet v0.2.3 code.gitea.io/sdk/gitea v0.22.0 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 connectrpc.com/connect v1.18.1 @@ -135,6 +134,7 @@ require ( require ( cloud.google.com/go/compute/metadata v0.8.0 // indirect + code.gitea.io/gitea-vet v0.2.3 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect @@ -307,3 +307,5 @@ exclude github.com/gofrs/uuid v4.0.0+incompatible exclude github.com/goccy/go-json v0.4.11 exclude github.com/satori/go.uuid v1.2.0 + +tool code.gitea.io/gitea-vet From 0ce7d66368128aea9976b9f326fc82f3f0b2fc60 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 7 Nov 2025 09:44:09 +0800 Subject: [PATCH 17/52] Fix avatar upload error handling (#35887) Fix #35884 --- services/auth/source/ldap/source_authenticate.go | 4 +--- services/repository/avatar.go | 8 ++++---- services/user/avatar.go | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 6005a4744a..4463bcc054 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -105,9 +105,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u } } if source.AttributeAvatar != "" { - if err := user_service.UploadAvatar(ctx, user, sr.Avatar); err != nil { - return user, err - } + _ = user_service.UploadAvatar(ctx, user, sr.Avatar) } } diff --git a/services/repository/avatar.go b/services/repository/avatar.go index 79da629aa6..7ab6badfc3 100644 --- a/services/repository/avatar.go +++ b/services/repository/avatar.go @@ -21,7 +21,7 @@ import ( func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) error { avatarData, err := avatar.ProcessAvatarImage(data) if err != nil { - return err + return fmt.Errorf("UploadAvatar: failed to process repo avatar image: %w", err) } newAvatar := avatar.HashAvatar(repo.ID, data) @@ -36,19 +36,19 @@ func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) // Then repo will be removed - only it avatar file will be removed repo.Avatar = newAvatar if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "avatar"); err != nil { - return fmt.Errorf("UploadAvatar: Update repository avatar: %w", err) + return fmt.Errorf("UploadAvatar: failed to update repository avatar: %w", err) } if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error { _, err := w.Write(avatarData) return err }); err != nil { - return fmt.Errorf("UploadAvatar %s failed: Failed to remove old repo avatar %s: %w", repo.RelativePath(), newAvatar, err) + return fmt.Errorf("UploadAvatar: failed to save repo avatar %s: %w", newAvatar, err) } if len(oldAvatarPath) > 0 { if err := storage.RepoAvatars.Delete(oldAvatarPath); err != nil { - return fmt.Errorf("UploadAvatar: Failed to remove old repo avatar %s: %w", oldAvatarPath, err) + return fmt.Errorf("UploadAvatar: failed to remove old repo avatar %s: %w", oldAvatarPath, err) } } return nil diff --git a/services/user/avatar.go b/services/user/avatar.go index df188e5adc..6a43681e9e 100644 --- a/services/user/avatar.go +++ b/services/user/avatar.go @@ -21,21 +21,21 @@ import ( func UploadAvatar(ctx context.Context, u *user_model.User, data []byte) error { avatarData, err := avatar.ProcessAvatarImage(data) if err != nil { - return err + return fmt.Errorf("UploadAvatar: failed to process user avatar image: %w", err) } return db.WithTx(ctx, func(ctx context.Context) error { u.UseCustomAvatar = true u.Avatar = avatar.HashAvatar(u.ID, data) if err = user_model.UpdateUserCols(ctx, u, "use_custom_avatar", "avatar"); err != nil { - return fmt.Errorf("updateUser: %w", err) + return fmt.Errorf("UploadAvatar: failed to update user avatar: %w", err) } if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { _, err := w.Write(avatarData) return err }); err != nil { - return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err) + return fmt.Errorf("UploadAvatar: failed to save user avatar %s: %w", u.CustomAvatarRelativePath(), err) } return nil From bfaddbcd0df38fb7bc23b573475b78e6356e786e Mon Sep 17 00:00:00 2001 From: Luohao Wang Date: Sat, 8 Nov 2025 23:29:17 +0800 Subject: [PATCH 18/52] Fix conda null depend issue (#35900) Fix #35895 --- routers/api/packages/conda/conda.go | 2 +- tests/integration/api_packages_conda_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go index f496002bb5..8519ae3e08 100644 --- a/routers/api/packages/conda/conda.go +++ b/routers/api/packages/conda/conda.go @@ -148,7 +148,7 @@ func EnumeratePackages(ctx *context.Context) { Timestamp: fileMetadata.Timestamp, Build: fileMetadata.Build, BuildNumber: fileMetadata.BuildNumber, - Dependencies: fileMetadata.Dependencies, + Dependencies: util.SliceNilAsEmpty(fileMetadata.Dependencies), License: versionMetadata.License, LicenseFamily: versionMetadata.LicenseFamily, HashMD5: pfd.Blob.HashMD5, diff --git a/tests/integration/api_packages_conda_test.go b/tests/integration/api_packages_conda_test.go index b69a8c9066..8dbcba5b54 100644 --- a/tests/integration/api_packages_conda_test.go +++ b/tests/integration/api_packages_conda_test.go @@ -237,6 +237,8 @@ func TestPackageConda(t *testing.T) { assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5) assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256) assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size) + assert.NotNil(t, packageInfo.Dependencies) + assert.Empty(t, packageInfo.Dependencies) }) t.Run(".conda", func(t *testing.T) { @@ -268,6 +270,8 @@ func TestPackageConda(t *testing.T) { assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5) assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256) assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size) + assert.NotNil(t, packageInfo.Dependencies) + assert.Empty(t, packageInfo.Dependencies) }) }) } From 367a289b290ec02731a96f227631a41543527f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=B1=80?= <131967983+lutinglt@users.noreply.github.com> Date: Sun, 9 Nov 2025 00:08:59 +0800 Subject: [PATCH 19/52] Display source code downloads last for release attachments (#35897) --- templates/repo/release/list.tmpl | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index 882ffe40b7..b7a60a44ed 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -78,18 +78,6 @@ {{ctx.Locale.Tr "repo.release.downloads"}}
{{end}} + {{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}} +
  • + + {{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP) + +
  • +
  • + + {{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ) + +
  • + {{end}}
    From c12bc4aa3081b7f990bf557282cf4443bc2efd5a Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 8 Nov 2025 20:48:16 +0100 Subject: [PATCH 20/52] Add toolchain directive to go.mod (#35901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From [docs](https://go.dev/doc/toolchain#config): > The go line declares the minimum required Go version for using the module or workspace. For compatibility reasons, if the go line is omitted from a go.mod file, the module is considered to have an implicit go 1.16 line, and if the go line is omitted from a go.work file, the workspace is considered to have an implicit go 1.18 line. > The toolchain line declares a suggested toolchain to use with the module or workspace. As described in “[Go toolchain selection](https://go.dev/doc/toolchain#select)” below, the go command may run this specific toolchain when operating in that module or workspace if the default toolchain’s version is less than the suggested toolchain’s version. If the toolchain line is omitted, the module or workspace is considered to have an implicit toolchain goV line, where V is the Go version from the go line. This is better than setting `go` to the latest version which may break builds when that go version is unavailable, for example with `GOTOOLCHAIN=local` in the official go docker images. --- go.mod | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2be7558e55..7d787cf11b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module code.gitea.io/gitea -go 1.25.4 +go 1.25.0 + +toolchain go1.25.4 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: From 919348665b434c5da6a716655fbda7cac0534960 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sat, 8 Nov 2025 15:23:55 -0500 Subject: [PATCH 21/52] Add ability for local makefile with personal customizations that wouldnt affect remote repo (#35836) This would allow developers to keep a local file that'd add personal makefile targets for niche convenience customization without having to have the git workspace polluted with uncommitted changes. --------- Signed-off-by: techknowlogick --- .gitattributes | 1 + .gitignore | 3 +++ Makefile | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/.gitattributes b/.gitattributes index e218bbe25d..afd02555f5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,4 @@ /vendor/** -text -eol linguist-vendored /web_src/js/vendor/** -text -eol linguist-vendored Dockerfile.* linguist-language=Dockerfile +Makefile.* linguist-language=Makefile diff --git a/.gitignore b/.gitignore index 7e8e5f84a7..11af4543bd 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ prime/ # Ignore worktrees when working on multiple branches .worktrees/ + +# A Makefile for custom make targets +Makefile.local diff --git a/Makefile b/Makefile index de2a486ea1..5dbf141723 100644 --- a/Makefile +++ b/Makefile @@ -198,6 +198,10 @@ TEST_MSSQL_DBNAME ?= gitea TEST_MSSQL_USERNAME ?= sa TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1 +# Include local Makefile +# Makefile.local is listed in .gitignore +sinclude Makefile.local + .PHONY: all all: build From 050c9485dface2ea5d16343d8b46052c2f4be7fb Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 9 Nov 2025 11:13:31 +0800 Subject: [PATCH 22/52] Fix team member access check (#35899) Fix #35499 --- models/git/protected_branch.go | 8 ++- models/organization/team_repo.go | 37 ++++++++--- models/perm/access/repo_permission.go | 65 +++++++++----------- models/perm/access/repo_permission_test.go | 16 ++++- routers/web/repo/setting/protected_branch.go | 5 +- routers/web/repo/setting/protected_tag.go | 4 +- services/convert/convert.go | 4 +- 7 files changed, 83 insertions(+), 56 deletions(-) diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index 13e1ced0e1..1085c14cae 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -466,11 +466,13 @@ func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, c return currentWhitelist, nil } + prUserIDs, err := access_model.GetUserIDsWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests) + if err != nil { + return nil, err + } whitelist = make([]int64, 0, len(newWhitelist)) for _, userID := range newWhitelist { - if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil { - return nil, err - } else if !reader { + if !prUserIDs.Contains(userID) { continue } whitelist = append(whitelist, userID) diff --git a/models/organization/team_repo.go b/models/organization/team_repo.go index b3e266dbc7..2652b34c6f 100644 --- a/models/organization/team_repo.go +++ b/models/organization/team_repo.go @@ -53,24 +53,45 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error { // GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit. // This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control. // FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details -func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) { - teams := make([]*Team, 0, 5) +func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) { + teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...) + if err != nil { + return nil, err + } + if len(teamIDs) == 0 { + return teams, nil + } + err = db.GetEngine(ctx).Where(builder.In("id", teamIDs)).OrderBy("team.name").Find(&teams) + return teams, err +} +func getTeamIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teamIDs []int64, err error) { sub := builder.Select("team_id").From("team_unit"). Where(builder.Expr("team_unit.team_id = team.id")). And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))). And(builder.Expr("team_unit.access_mode >= ?", mode)) - err := db.GetEngine(ctx). + err = db.GetEngine(ctx). + Select("team.id"). + Table("team"). Join("INNER", "team_repo", "team_repo.team_id = team.id"). - And("team_repo.org_id = ?", orgID). - And("team_repo.repo_id = ?", repoID). + And("team_repo.org_id = ? AND team_repo.repo_id = ?", orgID, repoID). And(builder.Or( builder.Expr("team.authorize >= ?", mode), builder.In("team.id", sub), )). - OrderBy("name"). - Find(&teams) + Find(&teamIDs) + return teamIDs, err +} - return teams, err +func GetTeamUserIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (userIDs []int64, err error) { + teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...) + if err != nil { + return nil, err + } + if len(teamIDs) == 0 { + return userIDs, nil + } + err = db.GetEngine(ctx).Table("team_user").Select("uid").Where(builder.In("team_id", teamIDs)).Find(&userIDs) + return userIDs, err } diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 034557db33..15526cb1e6 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -16,6 +16,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -498,54 +499,44 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi return perm.HasAnyUnitAccess(), nil } -// getUsersWithAccessMode returns users that have at least given access mode to the repository. -func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) { - if err = repo.LoadOwner(ctx); err != nil { +func GetUsersWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (users []*user_model.User, err error) { + userIDs, err := GetUserIDsWithUnitAccess(ctx, repo, mode, unitType) + if err != nil { return nil, err } + if len(userIDs) == 0 { + return users, nil + } + if err = db.GetEngine(ctx).In("id", userIDs.Values()).OrderBy("`name`").Find(&users); err != nil { + return nil, err + } + return users, nil +} +func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (container.Set[int64], error) { + userIDs := container.Set[int64]{} e := db.GetEngine(ctx) accesses := make([]*Access, 0, 10) - if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil { + if err := e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil { return nil, err } + for _, a := range accesses { + userIDs.Add(a.UserID) + } - // Leave a seat for owner itself to append later, but if owner is an organization - // and just waste 1 unit is cheaper than re-allocate memory once. - users := make([]*user_model.User, 0, len(accesses)+1) - if len(accesses) > 0 { - userIDs := make([]int64, len(accesses)) - for i := 0; i < len(accesses); i++ { - userIDs[i] = accesses[i].UserID - } - - if err = e.In("id", userIDs).Find(&users); err != nil { - return nil, err - } + if err := repo.LoadOwner(ctx); err != nil { + return nil, err } if !repo.Owner.IsOrganization() { - users = append(users, repo.Owner) - } - - return users, nil -} - -// GetRepoReaders returns all users that have explicit read access or higher to the repository. -func GetRepoReaders(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) { - return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeRead) -} - -// GetRepoWriters returns all users that have write access to the repository. -func GetRepoWriters(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) { - return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeWrite) -} - -// IsRepoReader returns true if user has explicit read access or higher to the repository. -func IsRepoReader(ctx context.Context, repo *repo_model.Repository, userID int64) (bool, error) { - if repo.OwnerID == userID { - return true, nil + userIDs.Add(repo.Owner.ID) + } else { + teamUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, mode, unitType) + if err != nil { + return nil, err + } + userIDs.AddMultiple(teamUserIDs...) } - return db.GetEngine(ctx).Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, perm_model.AccessModeRead).Get(&Access{}) + return userIDs, nil } // CheckRepoUnitUser check whether user could visit the unit of this repository diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go index d81dfba288..a36be213ec 100644 --- a/models/perm/access/repo_permission_test.go +++ b/models/perm/access/repo_permission_test.go @@ -169,9 +169,9 @@ func TestGetUserRepoPermission(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) team := &organization.Team{OrgID: org.ID, LowerName: "test_team"} require.NoError(t, db.Insert(ctx, team)) + require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID})) t.Run("DoerInTeamWithNoRepo", func(t *testing.T) { - require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID})) perm, err := GetUserRepoPermission(ctx, repo32, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) @@ -219,6 +219,15 @@ func TestGetUserRepoPermission(t *testing.T) { assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode) assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode]) assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues]) + + users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeRead, unit.TypeIssues) + require.NoError(t, err) + require.Len(t, users, 1) + assert.Equal(t, user.ID, users[0].ID) + + users, err = GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues) + require.NoError(t, err) + require.Empty(t, users) }) require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite})) @@ -229,5 +238,10 @@ func TestGetUserRepoPermission(t *testing.T) { assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode) assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode]) assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeIssues]) + + users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues) + require.NoError(t, err) + require.Len(t, users, 1) + assert.Equal(t, user.ID, users[0].ID) }) } diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index c7bde087a2..4374e95340 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -73,10 +73,9 @@ func SettingsProtectedBranch(c *context.Context) { c.Data["PageIsSettingsBranches"] = true c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName - - users, err := access_model.GetRepoReaders(c, c.Repo.Repository) + users, err := access_model.GetUsersWithUnitAccess(c, c.Repo.Repository, perm.AccessModeRead, unit.TypePullRequests) if err != nil { - c.ServerError("Repo.Repository.GetReaders", err) + c.ServerError("GetUsersWithUnitAccess", err) return } c.Data["Users"] = users diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go index 50f5a28c4c..4b560e6f22 100644 --- a/routers/web/repo/setting/protected_tag.go +++ b/routers/web/repo/setting/protected_tag.go @@ -149,9 +149,9 @@ func setTagsContext(ctx *context.Context) error { } ctx.Data["ProtectedTags"] = protectedTags - users, err := access_model.GetRepoReaders(ctx, ctx.Repo.Repository) + users, err := access_model.GetUsersWithUnitAccess(ctx, ctx.Repo.Repository, perm.AccessModeRead, unit.TypePullRequests) if err != nil { - ctx.ServerError("Repo.Repository.GetReaders", err) + ctx.ServerError("GetUsersWithUnitAccess", err) return err } ctx.Data["Users"] = users diff --git a/services/convert/convert.go b/services/convert/convert.go index 9f8fff970c..8b10d93640 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -139,7 +139,7 @@ func getWhitelistEntities[T *user_model.User | *organization.Team](entities []T, // ToBranchProtection convert a ProtectedBranch to api.BranchProtection func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo *repo_model.Repository) *api.BranchProtection { - readers, err := access_model.GetRepoReaders(ctx, repo) + readers, err := access_model.GetUsersWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests) if err != nil { log.Error("GetRepoReaders: %v", err) } @@ -720,7 +720,7 @@ func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api. // ToTagProtection convert a git.ProtectedTag to an api.TagProtection func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo_model.Repository) *api.TagProtection { - readers, err := access_model.GetRepoReaders(ctx, repo) + readers, err := access_model.GetUsersWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests) if err != nil { log.Error("GetRepoReaders: %v", err) } From c4c4cf5687ce6813996070b80ec7609a7cce26ec Mon Sep 17 00:00:00 2001 From: Alberty Pascal Date: Sun, 9 Nov 2025 22:23:46 +0100 Subject: [PATCH 23/52] Use correct form field for allowed force push users in branch protection API (#35894) Test was wrong and preventing update of force push allow users list by the API Resolves #35893 Signed-off-by: Alberty Pascal --- routers/api/v1/repo/branch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index a337ed7938..b9060e9cbd 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -897,7 +897,7 @@ func EditBranchProtection(ctx *context.APIContext) { } else { whitelistUsers = protectBranch.WhitelistUserIDs } - if form.ForcePushAllowlistDeployKeys != nil { + if form.ForcePushAllowlistUsernames != nil { forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { From 60314cb6889b44bba4e86c6676d70c90556b0462 Mon Sep 17 00:00:00 2001 From: Mithilesh Gupta Date: Mon, 10 Nov 2025 03:24:34 +0530 Subject: [PATCH 24/52] Add proper page title for project pages (#35773) Fix #35763 Co-authored-by: Mithilesh Gupta --- routers/web/org/projects.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 059cce8281..d524409c41 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -436,6 +436,7 @@ func ViewProject(ctx *context.Context) { ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Columns"] = columns + ctx.Data["Title"] = fmt.Sprintf("%s - %s", project.Title, ctx.ContextUser.DisplayName()) if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { ctx.ServerError("RenderUserOrgHeader", err) From 1c8c56503f99365a93fbbb653343f464643ac9e3 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 10 Nov 2025 13:31:25 +0800 Subject: [PATCH 25/52] Allow to display embed images/pdfs when SERVE_DIRECT was enabled on MinIO storage (#35882) Releated issue: https://github.com/go-gitea/gitea/issues/30487 --------- Co-authored-by: wxiaoguang --- modules/storage/azureblob.go | 1 + modules/storage/minio.go | 34 +++++++++++++++++++++++++++++----- routers/web/repo/view.go | 1 + 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/modules/storage/azureblob.go b/modules/storage/azureblob.go index 6860d81131..e7297cec77 100644 --- a/modules/storage/azureblob.go +++ b/modules/storage/azureblob.go @@ -250,6 +250,7 @@ func (a *AzureBlobStorage) Delete(path string) error { func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) { blobClient := a.getBlobClient(path) + // TODO: OBJECT-STORAGE-CONTENT-TYPE: "browser inline rendering images/PDF" needs proper Content-Type header from storage startTime := time.Now() u, err := blobClient.GetSASURL(sas.BlobPermissions{ Read: true, diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 01f2c16267..6993ac2d92 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -279,20 +279,44 @@ func (m *MinioStorage) Delete(path string) error { } // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. -func (m *MinioStorage) URL(path, name, method string, serveDirectReqParams url.Values) (*url.URL, error) { +func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams url.Values) (*url.URL, error) { // copy serveDirectReqParams reqParams, err := url.ParseQuery(serveDirectReqParams.Encode()) if err != nil { return nil, err } - // TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we? - reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"") + + // Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head. + // So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI. + // Detect content type by extension name, only support the well-known safe types for inline rendering. + // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future + ext := path.Ext(name) + inlineExtMimeTypes := map[string]string{ + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".avif": "image/avif", + // ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy + // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline + ".pdf": "application/pdf", + + // TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType" + } + if mimeType, ok := inlineExtMimeTypes[ext]; ok { + reqParams.Set("response-content-type", mimeType) + reqParams.Set("response-content-disposition", "inline") + } else { + reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name))) + } + expires := 5 * time.Minute if method == http.MethodHead { - u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams) + u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams) return u, convertMinioErr(err) } - u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams) + u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams) return u, convertMinioErr(err) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 79357bfd76..09ac33cff4 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -95,6 +95,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []b meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid) if err != nil { // fallback to a plain file + fi.lfsMeta = &pointer log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err) return buf, dataRc, fi, nil } From e31f224ad2af66a24d87bf9224a00932f164a632 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 10 Nov 2025 23:45:01 +0800 Subject: [PATCH 26/52] Make OAuth2 issuer configurable (#35915) The new (correct) behavior breaks the old (incorrect) logins. Add a config option to support legacy "issuer". Fix #35830 --- custom/conf/app.example.ini | 5 ++++ modules/setting/oauth2.go | 1 + services/oauth2_provider/access_token.go | 6 +++- tests/integration/oauth_test.go | 36 ++++++++++++++++-------- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5fee78af54..33bfe752a0 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -567,6 +567,11 @@ ENABLED = true ;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one ;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret ;; +;; The "issuer" claim identifies the principal that issued the JWT. +;; Gitea 1.25 makes it default to "ROOT_URL without the last slash" to follow the standard. +;; If you have old logins from before 1.25, you may want to set it to the old (non-standard) value "ROOT_URL with the last slash". +;JWT_CLAIM_ISSUER = +;; ;; Lifetime of an OAuth2 access token in seconds ;ACCESS_TOKEN_EXPIRATION_TIME = 3600 ;; diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 1a88f3cb08..ae2a9d7bee 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -96,6 +96,7 @@ var OAuth2 = struct { InvalidateRefreshTokens bool JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"` JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` + JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"` MaxTokenLength int DefaultApplications []string }{ diff --git a/services/oauth2_provider/access_token.go b/services/oauth2_provider/access_token.go index dce4ac765b..3a77c86d9e 100644 --- a/services/oauth2_provider/access_token.go +++ b/services/oauth2_provider/access_token.go @@ -112,8 +112,12 @@ func NewJwtRegisteredClaimsFromUser(clientID string, grantUserID int64, exp *jwt // to retrieve the configuration information. This MUST also be identical to the "iss" Claim value in ID Tokens issued from this Issuer. // * https://accounts.google.com/.well-known/openid-configuration // * https://github.com/login/oauth/.well-known/openid-configuration + issuer := setting.OAuth2.JWTClaimIssuer + if issuer == "" { + issuer = strings.TrimSuffix(setting.AppURL, "/") + } return jwt.RegisteredClaims{ - Issuer: strings.TrimSuffix(setting.AppURL, "/"), + Issuer: issuer, Audience: []string{clientID}, Subject: strconv.FormatInt(grantUserID, 10), ExpiresAt: exp, diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index eab95ba688..e7edace653 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -919,20 +919,32 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) { } func testOAuth2WellKnown(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")() urlOpenidConfiguration := "/.well-known/openid-configuration" - defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")() - req := NewRequest(t, "GET", urlOpenidConfiguration) - resp := MakeRequest(t, req, http.StatusOK) - var respMap map[string]any - DecodeJSON(t, resp, &respMap) - assert.Equal(t, "https://try.gitea.io", respMap["issuer"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"]) - assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"]) + t.Run("WellKnown", func(t *testing.T) { + req := NewRequest(t, "GET", urlOpenidConfiguration) + resp := MakeRequest(t, req, http.StatusOK) + var respMap map[string]any + DecodeJSON(t, resp, &respMap) + assert.Equal(t, "https://try.gitea.io", respMap["issuer"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"]) + assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"]) + }) + + t.Run("WellKnownWithIssuer", func(t *testing.T) { + defer test.MockVariableValue(&setting.OAuth2.JWTClaimIssuer, "https://try.gitea.io/")() + req := NewRequest(t, "GET", urlOpenidConfiguration) + resp := MakeRequest(t, req, http.StatusOK) + var respMap map[string]any + DecodeJSON(t, resp, &respMap) + assert.Equal(t, "https://try.gitea.io/", respMap["issuer"]) // has trailing by JWTClaimIssuer + assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"]) + }) defer test.MockVariableValue(&setting.OAuth2.Enabled, false)() MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound) From 9affb513a8fbbf323266531e95855ad1fad663e0 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 12 Nov 2025 00:11:46 +0800 Subject: [PATCH 27/52] Load jQuery as early as possible to support custom scripts (#35926) Fix #35923 --- web_src/js/index-domready.ts | 1 - web_src/js/index.ts | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index ecd8cb1baa..df56c85c86 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -1,4 +1,3 @@ -import './globals.ts'; import '../fomantic/build/fomantic.js'; import '../../node_modules/easymde/dist/easymde.min.css'; // TODO: lazy load in "switchToEasyMDE" diff --git a/web_src/js/index.ts b/web_src/js/index.ts index af53cc488c..153b8049c9 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -1,5 +1,10 @@ // bootstrap module must be the first one to be imported, it handles webpack lazy-loading and global errors import './bootstrap.ts'; + +// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml) +// so load globals (including jQuery) as early as possible +import './globals.ts'; + import './webcomponents/index.ts'; import {onDomReady} from './utils/dom.ts'; From 2223be2cc455d678ba45a64758108c1508f7a7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=B1=80?= <131967983+lutinglt@users.noreply.github.com> Date: Wed, 12 Nov 2025 02:21:15 +0800 Subject: [PATCH 28/52] Support blue yellow colorblind theme (#35910) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This icon is from GitHub: image --------- Signed-off-by: 鲁汀 <131967983+lutinglt@users.noreply.github.com> Co-authored-by: lutinglt --- .../img/svg/gitea-colorblind-blueyellow.svg | 1 + services/webtheme/webtheme.go | 6 ++++++ .../css/themes/theme-gitea-auto-tritanopia.css | 8 ++++++++ .../css/themes/theme-gitea-dark-tritanopia.css | 15 +++++++++++++++ .../css/themes/theme-gitea-light-tritanopia.css | 15 +++++++++++++++ web_src/svg/gitea-colorblind-blueyellow.svg | 13 +++++++++++++ 6 files changed, 58 insertions(+) create mode 100644 public/assets/img/svg/gitea-colorblind-blueyellow.svg create mode 100644 web_src/css/themes/theme-gitea-auto-tritanopia.css create mode 100644 web_src/css/themes/theme-gitea-dark-tritanopia.css create mode 100644 web_src/css/themes/theme-gitea-light-tritanopia.css create mode 100644 web_src/svg/gitea-colorblind-blueyellow.svg diff --git a/public/assets/img/svg/gitea-colorblind-blueyellow.svg b/public/assets/img/svg/gitea-colorblind-blueyellow.svg new file mode 100644 index 0000000000..63a101b50d --- /dev/null +++ b/public/assets/img/svg/gitea-colorblind-blueyellow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go index 72f01a76c7..57d63f4e07 100644 --- a/services/webtheme/webtheme.go +++ b/services/webtheme/webtheme.go @@ -39,6 +39,9 @@ func (info *ThemeMetaInfo) GetDescription() string { if info.ColorblindType == "red-green" { return "Red-green colorblind friendly" } + if info.ColorblindType == "blue-yellow" { + return "Blue-yellow colorblind friendly" + } return "" } @@ -46,6 +49,9 @@ func (info *ThemeMetaInfo) GetExtraIconName() string { if info.ColorblindType == "red-green" { return "gitea-colorblind-redgreen" } + if info.ColorblindType == "blue-yellow" { + return "gitea-colorblind-blueyellow" + } return "" } diff --git a/web_src/css/themes/theme-gitea-auto-tritanopia.css b/web_src/css/themes/theme-gitea-auto-tritanopia.css new file mode 100644 index 0000000000..178a23983b --- /dev/null +++ b/web_src/css/themes/theme-gitea-auto-tritanopia.css @@ -0,0 +1,8 @@ +@import "./theme-gitea-light-tritanopia.css" (prefers-color-scheme: light); +@import "./theme-gitea-dark-tritanopia.css" (prefers-color-scheme: dark); + +gitea-theme-meta-info { + --theme-display-name: "Auto"; + --theme-colorblind-type: "blue-yellow"; + --theme-color-scheme: "auto"; +} diff --git a/web_src/css/themes/theme-gitea-dark-tritanopia.css b/web_src/css/themes/theme-gitea-dark-tritanopia.css new file mode 100644 index 0000000000..1dbd9967dd --- /dev/null +++ b/web_src/css/themes/theme-gitea-dark-tritanopia.css @@ -0,0 +1,15 @@ +@import "./theme-gitea-dark-protanopia-deuteranopia.css"; + +gitea-theme-meta-info { + --theme-display-name: "Dark"; + --theme-colorblind-type: "blue-yellow"; + --theme-color-scheme: "dark"; +} + +/* blue/yellow colorblind-friendly colors */ +/* from GitHub: blue yellow blindness is based on red green blindness, and --diffBlob-deletion-* restored to the normal theme color */ +:root { + --color-diff-removed-linenum-bg: #482121; + --color-diff-removed-row-bg: #301e1e; + --color-diff-removed-word-bg: #6f3333; +} diff --git a/web_src/css/themes/theme-gitea-light-tritanopia.css b/web_src/css/themes/theme-gitea-light-tritanopia.css new file mode 100644 index 0000000000..a50fd9c1a4 --- /dev/null +++ b/web_src/css/themes/theme-gitea-light-tritanopia.css @@ -0,0 +1,15 @@ +@import "./theme-gitea-light-protanopia-deuteranopia.css"; + +gitea-theme-meta-info { + --theme-display-name: "Light"; + --theme-colorblind-type: "blue-yellow"; + --theme-color-scheme: "light"; +} + +/* blue/yellow colorblind-friendly colors */ +/* from GitHub: blue yellow blindness is based on red green blindness, and --diffBlob-deletion-* restored to the normal theme color */ +:root { + --color-diff-removed-linenum-bg: #ffcecb; + --color-diff-removed-row-bg: #ffeef0; + --color-diff-removed-word-bg: #fdb8c0; +} diff --git a/web_src/svg/gitea-colorblind-blueyellow.svg b/web_src/svg/gitea-colorblind-blueyellow.svg new file mode 100644 index 0000000000..752ff88432 --- /dev/null +++ b/web_src/svg/gitea-colorblind-blueyellow.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file From 372d24b84bc6f4c5562792009c6b6e6a4aeb85f8 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 12 Nov 2025 19:44:49 +0800 Subject: [PATCH 29/52] Limit reading bytes instead of ReadAll (#35928) --- modules/actions/workflows.go | 4 +-- modules/issue/template/unmarshal.go | 3 +- modules/packages/nuget/metadata.go | 2 +- modules/packages/pub/metadata.go | 2 +- modules/util/io.go | 2 +- routers/web/repo/wiki.go | 2 +- services/issue/template.go | 4 +-- services/repository/generate.go | 46 ++++++++++++++++------------ services/repository/generate_test.go | 26 ++++++++++++++++ services/webhook/deliver.go | 3 +- 10 files changed, 64 insertions(+), 30 deletions(-) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 69f71bf651..26a6ebc370 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -5,7 +5,6 @@ package actions import ( "bytes" - "io" "slices" "strings" @@ -13,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/nektos/act/pkg/jobparser" @@ -77,7 +77,7 @@ func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { if err != nil { return nil, err } - content, err := io.ReadAll(f) + content, err := util.ReadWithLimit(f, 1024*1024) _ = f.Close() if err != nil { return nil, err diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go index 1d8e9dd02d..ceab6babf4 100644 --- a/modules/issue/template/unmarshal.go +++ b/modules/issue/template/unmarshal.go @@ -5,7 +5,6 @@ package template import ( "fmt" - "io" "path" "strconv" @@ -76,7 +75,7 @@ func unmarshalFromEntry(entry *git.TreeEntry, filename string) (*api.IssueTempla } defer r.Close() - content, err := io.ReadAll(r) + content, err := util.ReadWithLimit(r, 1024*1024) if err != nil { return nil, fmt.Errorf("read all: %w", err) } diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 513b4dd2b9..5124627395 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -216,7 +216,7 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) { if p.Metadata.Readme != "" { f, err := archive.Open(p.Metadata.Readme) if err == nil { - buf, _ := io.ReadAll(f) + buf, _ := util.ReadWithLimit(f, 1024*1024) m.Readme = string(buf) _ = f.Close() } diff --git a/modules/packages/pub/metadata.go b/modules/packages/pub/metadata.go index 9b00472eb2..a2cf6b728a 100644 --- a/modules/packages/pub/metadata.go +++ b/modules/packages/pub/metadata.go @@ -89,7 +89,7 @@ func ParsePackage(r io.Reader) (*Package, error) { return nil, err } } else if strings.EqualFold(hd.Name, "readme.md") { - data, err := io.ReadAll(tr) + data, err := util.ReadWithLimit(tr, 1024*1024) if err != nil { return nil, err } diff --git a/modules/util/io.go b/modules/util/io.go index b3dde9d1f6..f5a3d320e5 100644 --- a/modules/util/io.go +++ b/modules/util/io.go @@ -29,7 +29,7 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) { // ReadWithLimit reads at most "limit" bytes from r into buf. // If EOF or ErrUnexpectedEOF occurs while reading, err will be nil. func ReadWithLimit(r io.Reader, n int) (buf []byte, err error) { - return readWithLimit(r, 1024, n) + return readWithLimit(r, 4*1024, n) } func readWithLimit(r io.Reader, batch, limit int) ([]byte, error) { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index e7c34ba1d6..542ac9c731 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -133,7 +133,7 @@ func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte { return nil } defer reader.Close() - content, err := io.ReadAll(reader) + content, err := util.ReadWithLimit(reader, 5*1024*1024) // 5MB should be enough for a wiki page if err != nil { ctx.ServerError("ReadAll", err) return nil diff --git a/services/issue/template.go b/services/issue/template.go index 4b0f1aa987..99977c67cf 100644 --- a/services/issue/template.go +++ b/services/issue/template.go @@ -5,7 +5,6 @@ package issue import ( "fmt" - "io" "net/url" "path" "strings" @@ -15,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/issue/template" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "gopkg.in/yaml.v3" ) @@ -65,7 +65,7 @@ func GetTemplateConfig(gitRepo *git.Repository, path string, commit *git.Commit) defer reader.Close() - configContent, err := io.ReadAll(reader) + configContent, err := util.ReadWithLimit(reader, 1024*1024) if err != nil { return GetDefaultTemplateConfig(), err } diff --git a/services/repository/generate.go b/services/repository/generate.go index 062c6f4fb1..caf15265a0 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -7,7 +7,9 @@ import ( "bufio" "bytes" "context" + "errors" "fmt" + "io/fs" "os" "path/filepath" "regexp" @@ -138,31 +140,37 @@ func (gt *giteaTemplateFileMatcher) Match(s string) bool { return false } -func readGiteaTemplateFile(tmpDir string) (*giteaTemplateFileMatcher, error) { - localPath := filepath.Join(tmpDir, ".gitea", "template") - if _, err := os.Stat(localPath); os.IsNotExist(err) { - return nil, nil - } else if err != nil { +func readLocalTmpRepoFileContent(localPath string, limit int) ([]byte, error) { + ok, err := util.IsRegularFile(localPath) + if err != nil { return nil, err + } else if !ok { + return nil, fs.ErrNotExist } - content, err := os.ReadFile(localPath) + f, err := os.Open(localPath) if err != nil { return nil, err } + defer f.Close() + + return util.ReadWithLimit(f, limit) +} +func readGiteaTemplateFile(tmpDir string) (*giteaTemplateFileMatcher, error) { + localPath := filepath.Join(tmpDir, ".gitea", "template") + content, err := readLocalTmpRepoFileContent(localPath, 1024*1024) + if err != nil { + return nil, err + } return newGiteaTemplateFileMatcher(localPath, content), nil } func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, templateRepo, generateRepo *repo_model.Repository) error { tmpFullPath := filepath.Join(tmpDir, tmpDirSubPath) - if ok, err := util.IsRegularFile(tmpFullPath); !ok { - return err - } - - content, err := os.ReadFile(tmpFullPath) + content, err := readLocalTmpRepoFileContent(tmpFullPath, 1024*1024) if err != nil { - return err + return util.Iif(errors.Is(err, fs.ErrNotExist), nil, err) } if err := util.Remove(tmpFullPath); err != nil { return err @@ -172,7 +180,7 @@ func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, t substSubPath := filepath.Clean(filePathSanitize(generateExpansion(ctx, tmpDirSubPath, templateRepo, generateRepo))) newLocalPath := filepath.Join(tmpDir, substSubPath) regular, err := util.IsRegularFile(newLocalPath) - if canWrite := regular || os.IsNotExist(err); !canWrite { + if canWrite := regular || errors.Is(err, fs.ErrNotExist); !canWrite { return nil } if err := os.MkdirAll(filepath.Dir(newLocalPath), 0o755); err != nil { @@ -242,15 +250,15 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r // Variable expansion fileMatcher, err := readGiteaTemplateFile(tmpDir) - if err != nil { - return fmt.Errorf("readGiteaTemplateFile: %w", err) - } - - if fileMatcher != nil { + if err == nil { err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, fileMatcher) if err != nil { - return err + return fmt.Errorf("processGiteaTemplateFile: %w", err) } + } else if errors.Is(err, fs.ErrNotExist) { + log.Debug("skip processing repo template files: no available .gitea/template") + } else { + return fmt.Errorf("readGiteaTemplateFile: %w", err) } if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil { diff --git a/services/repository/generate_test.go b/services/repository/generate_test.go index 19b84c7bde..9c01911ded 100644 --- a/services/repository/generate_test.go +++ b/services/repository/generate_test.go @@ -4,6 +4,7 @@ package repository import ( + "io/fs" "os" "path/filepath" "testing" @@ -175,6 +176,31 @@ func TestProcessGiteaTemplateFile(t *testing.T) { // subst from a link, skip, and the target is unchanged assertSymLink("subst-${TEMPLATE_NAME}-from-link", tmpDir+"/sub/link-target") } + + { + templateFilePath := tmpDir + "/.gitea/template" + + _ = os.Remove(templateFilePath) + _, err := os.Lstat(templateFilePath) + require.ErrorIs(t, err, fs.ErrNotExist) + _, err = readGiteaTemplateFile(tmpDir) // no template file + require.ErrorIs(t, err, fs.ErrNotExist) + + _ = os.WriteFile(templateFilePath+".target", []byte("test-data-target"), 0o644) + _ = os.Symlink(templateFilePath+".target", templateFilePath) + content, _ := os.ReadFile(templateFilePath) + require.Equal(t, "test-data-target", string(content)) + _, err = readGiteaTemplateFile(tmpDir) // symlinked template file + require.ErrorIs(t, err, fs.ErrNotExist) + + _ = os.Remove(templateFilePath) + _ = os.WriteFile(templateFilePath, []byte("test-data-regular"), 0o644) + content, _ = os.ReadFile(templateFilePath) + require.Equal(t, "test-data-regular", string(content)) + fm, err := readGiteaTemplateFile(tmpDir) // regular template file + require.NoError(t, err) + assert.Len(t, fm.globs, 1) + } } func TestTransformers(t *testing.T) { diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index b6611a3576..58fba9f68d 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" ) @@ -264,7 +265,7 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { t.ResponseInfo.Headers[k] = strings.Join(vals, ",") } - p, err := io.ReadAll(resp.Body) + p, err := util.ReadWithLimit(resp.Body, 1024*1024) if err != nil { t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err) return fmt.Errorf("unable to deliver webhook task[%d] in %s as unable to read response body: %w", t.ID, w.URL, err) From b95fd7e13ed4063d123b488df74a98cbfcfed85b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 13 Nov 2025 07:03:13 +0800 Subject: [PATCH 30/52] Don't show unnecessary error message to end users for DeleteBranchAfterMerge (#35937) --- routers/web/repo/pull.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 17e3bf2b78..4353e00840 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1243,7 +1243,11 @@ func MergePullRequest(ctx *context.Context) { func deleteBranchAfterMergeAndFlashMessage(ctx *context.Context, prID int64) { var fullBranchName string err := repo_service.DeleteBranchAfterMerge(ctx, ctx.Doer, prID, &fullBranchName) - if errTr := util.ErrorAsTranslatable(err); errTr != nil { + if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) { + // no need to show error to end users if no permission or branch not exist + log.Debug("DeleteBranchAfterMerge (ignore unnecessary error): %v", err) + return + } else if errTr := util.ErrorAsTranslatable(err); errTr != nil { ctx.Flash.Error(errTr.Translate(ctx.Locale)) return } else if err == nil { From 1f3558b65c0c7121bc0b7b75316e1ec9317bd703 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 14 Nov 2025 08:31:11 +0800 Subject: [PATCH 31/52] Fix corrupted external render content (#35946) Fix #35944 --- routers/web/repo/view_file.go | 11 +-- .../user30/renderer.git/HEAD | 1 - .../user30/renderer.git/config | 6 -- .../user30/renderer.git/description | 1 - .../user30/renderer.git/hooks/post-receive | 15 ---- .../renderer.git/hooks/post-receive.d/gitea | 2 - .../user30/renderer.git/hooks/pre-receive | 15 ---- .../renderer.git/hooks/pre-receive.d/gitea | 2 - .../user30/renderer.git/hooks/update | 14 ---- .../user30/renderer.git/hooks/update.d/gitea | 2 - .../06/0d5c2acd8bf4b6f14010acd1a73d73392ec46e | Bin 56 -> 0 bytes .../45/14a93050edb2c3165bdd0a3c03be063e879e68 | Bin 50 -> 0 bytes .../c9/61cc4d1ba6b7ee1ba228a9a02b00b7746d8033 | Bin 789 -> 0 bytes .../user30/renderer.git/packed-refs | 2 - .../user30/renderer.git/refs/.keep | 0 tests/integration/markup_external_test.go | 75 +++++++++++++----- tests/sqlite.ini.tmpl | 2 +- 17 files changed, 60 insertions(+), 88 deletions(-) delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/HEAD delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/config delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/description delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive.d/gitea delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive.d/gitea delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/update delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/update.d/gitea delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/objects/06/0d5c2acd8bf4b6f14010acd1a73d73392ec46e delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/objects/45/14a93050edb2c3165bdd0a3c03be063e879e68 delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/objects/c9/61cc4d1ba6b7ee1ba228a9a02b00b7746d8033 delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/packed-refs delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/refs/.keep diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index ea3920439d..167cd5f927 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -92,8 +92,6 @@ func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedTy ctx.ServerError("Render", err) return true } - // to prevent iframe from loading third-party url - ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") return true } @@ -241,14 +239,17 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { // * IsRenderableXxx: some files are rendered by backend "markup" engine, some are rendered by frontend (pdf, 3d) // * DefaultViewMode: when there is no "display" query parameter, which view mode should be used by default, source or rendered - utf8Reader := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) + contentReader := io.MultiReader(bytes.NewReader(buf), dataRc) + if fInfo.st.IsRepresentableAsText() { + contentReader = charset.ToUTF8WithFallbackReader(contentReader, charset.ConvertOpts{}) + } switch { case fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize: ctx.Data["IsFileTooLarge"] = true - case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, utf8Reader): + case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, contentReader): // it also sets ctx.Data["FileContent"] and more ctx.Data["IsMarkup"] = true - case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, utf8Reader): + case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, contentReader): // it also sets ctx.Data["FileContent"] and more ctx.Data["IsDisplayingSource"] = true case handleFileViewRenderImage(ctx, fInfo, buf): diff --git a/tests/gitea-repositories-meta/user30/renderer.git/HEAD b/tests/gitea-repositories-meta/user30/renderer.git/HEAD deleted file mode 100644 index cb089cd89a..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/master diff --git a/tests/gitea-repositories-meta/user30/renderer.git/config b/tests/gitea-repositories-meta/user30/renderer.git/config deleted file mode 100644 index e6da231579..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/config +++ /dev/null @@ -1,6 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = true - ignorecase = true - precomposeunicode = true diff --git a/tests/gitea-repositories-meta/user30/renderer.git/description b/tests/gitea-repositories-meta/user30/renderer.git/description deleted file mode 100644 index 04c23973b8..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/description +++ /dev/null @@ -1 +0,0 @@ -The repository will be used to test third-party renderer in TestExternalMarkupRenderer diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive b/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive deleted file mode 100644 index f1f2709ddd..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -data=$(cat) -exitcodes="" -hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)} - -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do -test -x "${hook}" && test -f "${hook}" || continue -echo "${data}" | "${hook}" -exitcodes="${exitcodes} $?" -done - -for i in ${exitcodes}; do -[ ${i} -eq 0 ] || exit ${i} -done diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive.d/gitea deleted file mode 100644 index 43a948da3a..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive.d/gitea +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive b/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive deleted file mode 100644 index f1f2709ddd..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -data=$(cat) -exitcodes="" -hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)} - -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do -test -x "${hook}" && test -f "${hook}" || continue -echo "${data}" | "${hook}" -exitcodes="${exitcodes} $?" -done - -for i in ${exitcodes}; do -[ ${i} -eq 0 ] || exit ${i} -done diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive.d/gitea deleted file mode 100644 index 49d0940636..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive.d/gitea +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/update b/tests/gitea-repositories-meta/user30/renderer.git/hooks/update deleted file mode 100644 index df5bd27f10..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/update +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -exitcodes="" -hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)} - -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do -test -x "${hook}" && test -f "${hook}" || continue -"${hook}" $1 $2 $3 -exitcodes="${exitcodes} $?" -done - -for i in ${exitcodes}; do -[ ${i} -eq 0 ] || exit ${i} -done diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user30/renderer.git/hooks/update.d/gitea deleted file mode 100644 index 38101c2426..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/update.d/gitea +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3 diff --git a/tests/gitea-repositories-meta/user30/renderer.git/objects/06/0d5c2acd8bf4b6f14010acd1a73d73392ec46e b/tests/gitea-repositories-meta/user30/renderer.git/objects/06/0d5c2acd8bf4b6f14010acd1a73d73392ec46e deleted file mode 100644 index 994f25602cd6d8b6a48735b7520ee0dc1f6c307b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56 zcmV-80LTA$0V^p=O;s?qWH2-^Ff%bx2y%6F@paY9D9O!Xa1~i;5b$=>VX^4DTsF-6 O*zDTpWdHz1$qy%Vr5Mrx diff --git a/tests/gitea-repositories-meta/user30/renderer.git/objects/45/14a93050edb2c3165bdd0a3c03be063e879e68 b/tests/gitea-repositories-meta/user30/renderer.git/objects/45/14a93050edb2c3165bdd0a3c03be063e879e68 deleted file mode 100644 index b1fff27753835c235faa542aeeb6708413501109..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50 zcmV-20L}k+0ZYosPf{>8XRt}hEVJX{EJ-acQAn*QNiE7t%uy&x%}YrwN-g5D(FaQd I08PXWv=3tx761SM diff --git a/tests/gitea-repositories-meta/user30/renderer.git/objects/c9/61cc4d1ba6b7ee1ba228a9a02b00b7746d8033 b/tests/gitea-repositories-meta/user30/renderer.git/objects/c9/61cc4d1ba6b7ee1ba228a9a02b00b7746d8033 deleted file mode 100644 index 66488767ae90c8665d6a1ce53faa21a44d6421d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 789 zcmV+w1M2*E0iBdd6RJiOg|p^Y^qxwXS{keJQelum6a#up&z~m`^d0ZaKV(IR{ye{jOY!1cC%%%V!vh8I{gc z*^l5bcijiXRoa8!-(PL!?v-_a7HwKJzu~_9190MIBXTS+ZUD_nEK7aqH*?DV*Ksiv z&#C~w{x!-Ts!9jzD2 z_XdAlFzj`?wdZzI%W6@3|8fdoI3llLm>|NZy5Am&(qDU$+gDRbcjWL2FDGcxmT+Lr zW;S5sCFrAKt4n$QxJjW*0m%`^hlS>2K(Cj%*3coTqgpC;yKOu?8Cplmgo~cYEp2GD zuM*i^8yyrc(N?QA1PT!QgADnzEbIR35UGbkbKP~h3GKo{?VV(sa{G9g$DU11*+p+# z{gBNn`rKML*`(1FxW~`okU%0jrS=UPd{0~-G96qUuaEroxD~j(+(I*Ev}adVzA7kA z`AVei*)@oao)l=vLsxG7vcIzD=Um@k(mNRIv@iStKN%*v#gpt}Ee-qEr8b%EWVLP5 zjZEe^?ly)e3K)w!Kk4E^xOZAus#65kvHCPurWKy8bBcV+@V!t+L^?I7=aQ?DPsYrv zo4wT_hTJGHiSSKN`T^3K_xPB^Cw9-4ZY00RNGgq&=476$3=d(jd&xoERPSZxTA`D3 z7}9k`0l}`<4h@)92n^ZK;{E&FSm|ZqCkDF0(t2Cc##>WKJUT6Bt449p#K(hKS`U&) zm;x7=VO7;-P3v@ZRIyvZd$Old<{bUp4Xf7Vve367%MKO3Xj))P%^5CF`3iOJy<92K z(aS-&B;3l`F~gZ2$I{P@NX|UcFY7QjAsXwfd>n>oqV*$vRz&Nu&wU#DcF`O64+Y{S znUiaT3=-DiZ48*)JR=*;3ER|a;Jf$^swEt=h TRdwGtFpT{@!xQxrb9G4{#m9=~ diff --git a/tests/gitea-repositories-meta/user30/renderer.git/packed-refs b/tests/gitea-repositories-meta/user30/renderer.git/packed-refs deleted file mode 100644 index 63f8af0f12..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/packed-refs +++ /dev/null @@ -1,2 +0,0 @@ -# pack-refs with: peeled fully-peeled sorted -c961cc4d1ba6b7ee1ba228a9a02b00b7746d8033 refs/heads/master diff --git a/tests/gitea-repositories-meta/user30/renderer.git/refs/.keep b/tests/gitea-repositories-meta/user30/renderer.git/refs/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index 9985333cd7..b965766b5c 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -12,6 +12,7 @@ import ( 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/charset" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/setting" @@ -25,29 +26,45 @@ import ( func TestExternalMarkupRenderer(t *testing.T) { defer tests.PrepareTestEnv(t)() if !setting.Database.Type.IsSQLite3() { - t.Skip() + t.Skip("only SQLite3 test config supports external markup renderer") return } + const binaryContentPrefix = "any prefix text." + const binaryContent = binaryContentPrefix + "\xfe\xfe\xfe\x00\xff\xff" + detectedEncoding, _ := charset.DetectEncoding([]byte(binaryContent)) + assert.NotEqual(t, binaryContent, strings.ToValidUTF8(binaryContent, "?")) + assert.Equal(t, "ISO-8859-2", detectedEncoding) // even if the binary content can be detected as text encoding, it shouldn't affect the raw rendering + onGiteaRun(t, func(t *testing.T, _ *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - _, err := createFile(user2, repo1, "file.no-sanitizer", "master", `any content`) + _, err := createFileInBranch(user2, repo1, createFileInBranchOptions{}, map[string]string{ + "test.html": `
    `, + "html.no-sanitizer": ``, + "bin.no-sanitizer": binaryContent, + }) require.NoError(t, err) t.Run("RenderNoSanitizer", func(t *testing.T) { - req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/file.no-sanitizer") + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/html.no-sanitizer") resp := MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body) - div := doc.Find("div.file-view") + div := NewHTMLParser(t, resp.Body).Find("div.file-view") data, err := div.Html() assert.NoError(t, err) - assert.Equal(t, ``, strings.TrimSpace(data)) + assert.Equal(t, ``, strings.TrimSpace(data)) + + req = NewRequest(t, "GET", "/user2/repo1/src/branch/master/bin.no-sanitizer") + resp = MakeRequest(t, req, http.StatusOK) + div = NewHTMLParser(t, resp.Body).Find("div.file-view") + data, err = div.Html() + assert.NoError(t, err) + assert.Equal(t, strings.ReplaceAll(binaryContent, "\x00", ""), strings.TrimSpace(data)) // HTML template engine removes the null bytes }) }) t.Run("RenderContentDirectly", func(t *testing.T) { - req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/test.html") resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) @@ -55,18 +72,21 @@ func TestExternalMarkupRenderer(t *testing.T) { div := doc.Find("div.file-view") data, err := div.Html() assert.NoError(t, err) - assert.Equal(t, "
    \n\ttest external renderer\n
    ", strings.TrimSpace(data)) + // the content is fully sanitized + assert.Equal(t, `
    <script></script>
    `, strings.TrimSpace(data)) }) - // above tested "no-sanitizer" mode, then we test iframe mode below + // above tested in-page rendering (no iframe), then we test iframe mode below r := markup.GetRendererByFileName("any-file.html").(*external.Renderer) defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)() + assert.True(t, r.NeedPostProcess()) r = markup.GetRendererByFileName("any-file.no-sanitizer").(*external.Renderer) defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)() + assert.False(t, r.NeedPostProcess()) t.Run("RenderContentInIFrame", func(t *testing.T) { t.Run("DefaultSandbox", func(t *testing.T) { - req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/test.html") t.Run("ParentPage", func(t *testing.T) { respParent := MakeRequest(t, req, http.StatusOK) @@ -77,31 +97,42 @@ func TestExternalMarkupRenderer(t *testing.T) { // default sandbox on parent page assert.Equal(t, "allow-scripts allow-popups", iframe.AttrOr("sandbox", "")) - assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("data-src", "")) + assert.Equal(t, "/user2/repo1/render/branch/master/test.html", iframe.AttrOr("data-src", "")) }) t.Run("SubPage", func(t *testing.T) { - req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html") + req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/test.html") respSub := MakeRequest(t, req, http.StatusOK) assert.Equal(t, "text/html; charset=utf-8", respSub.Header().Get("Content-Type")) // default sandbox in sub page response assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) - assert.Equal(t, "
    \n\ttest external renderer\n
    \n", respSub.Body.String()) + // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "
    <script></script>
    `, respSub.Body.String()) }) }) t.Run("NoSanitizerNoSandbox", func(t *testing.T) { - req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/file.no-sanitizer") - respParent := MakeRequest(t, req, http.StatusOK) - iframe := NewHTMLParser(t, respParent.Body).Find("iframe.external-render-iframe") - assert.Equal(t, "/user2/repo1/render/branch/master/file.no-sanitizer", iframe.AttrOr("data-src", "")) + t.Run("BinaryContent", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/bin.no-sanitizer") + respParent := MakeRequest(t, req, http.StatusOK) + iframe := NewHTMLParser(t, respParent.Body).Find("iframe.external-render-iframe") + assert.Equal(t, "/user2/repo1/render/branch/master/bin.no-sanitizer", iframe.AttrOr("data-src", "")) - req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/file.no-sanitizer") - respSub := MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/bin.no-sanitizer") + respSub := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, binaryContent, respSub.Body.String()) // raw content should keep the raw bytes (including invalid UTF-8 bytes), and no "external-render-iframe" helpers + + // no sandbox (disabled by RENDER_CONTENT_SANDBOX) + assert.Empty(t, iframe.AttrOr("sandbox", "")) + assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) + }) - // no sandbox (disabled by RENDER_CONTENT_SANDBOX) - assert.Empty(t, iframe.AttrOr("sandbox", "")) - assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) + t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer") + respSub := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, ``, respSub.Body.String()) + assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) + }) }) }) } diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 9d184bce6a..61f7e2a46d 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -122,7 +122,7 @@ RENDER_CONTENT_MODE = sanitized [markup.no-sanitizer] ENABLED = true FILE_EXTENSIONS = .no-sanitizer -RENDER_COMMAND = echo '' +RENDER_COMMAND = go run tools/test-echo.go ; This test case is reused, at first it is used to test "no-sanitizer" (sandbox doesn't take effect here) ; Then it will be updated and used to test "iframe + sandbox-disabled" RENDER_CONTENT_MODE = no-sanitizer From 018156079ba62f2b35daa60da9520e206867325a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 13 Nov 2025 19:19:51 -0800 Subject: [PATCH 32/52] Upgrade deps golang.org/x/crypto (#35952) --- go.mod | 4 ++-- go.sum | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 7d787cf11b..11ab491946 100644 --- a/go.mod +++ b/go.mod @@ -117,9 +117,9 @@ require ( github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 gitlab.com/gitlab-org/api/client-go v0.142.4 - golang.org/x/crypto v0.42.0 + golang.org/x/crypto v0.43.0 golang.org/x/image v0.30.0 - golang.org/x/net v0.44.0 + golang.org/x/net v0.45.0 golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.17.0 golang.org/x/sys v0.37.0 diff --git a/go.sum b/go.sum index 02a710e7f0..29f5ba745f 100644 --- a/go.sum +++ b/go.sum @@ -840,8 +840,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -908,8 +908,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -987,8 +987,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 358de23a5057c9746b9349f73000d213958ae485 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 14 Nov 2025 11:49:57 +0800 Subject: [PATCH 33/52] Fix container push tag overwriting (#35936) Fix #35853 --- routers/api/packages/container/manifest.go | 37 +++++++++---------- .../api_packages_container_test.go | 36 ++++++++++++------ 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index de40215aa7..e408f6ee3b 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -10,7 +10,6 @@ import ( "io" "os" "strings" - "time" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" @@ -260,6 +259,13 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met return nil, err } + // "docker buildx imagetools create" multi-arch operations: + // {"type":"oci","is_tagged":false,"platform":"unknown/unknown"} + // {"type":"oci","is_tagged":false,"platform":"linux/amd64","layer_creation":["ADD file:9233f6f2237d79659a9521f7e390df217cec49f1a8aa3a12147bbca1956acdb9 in /","CMD [\"/bin/sh\"]"]} + // {"type":"oci","is_tagged":false,"platform":"unknown/unknown"} + // {"type":"oci","is_tagged":false,"platform":"linux/arm64","layer_creation":["ADD file:df53811312284306901fdaaff0a357a4bf40d631e662fe9ce6d342442e494b6c in /","CMD [\"/bin/sh\"]"]} + // {"type":"oci","is_tagged":true,"manifests":[{"platform":"linux/amd64","digest":"sha256:72bb73e706c0dec424d00a1febb21deaf1175a70ead009ad8b159729cfcf5769","size":2819478},{"platform":"linux/arm64","digest":"sha256:9e1426dd084a3221663b85ca1ee99d140c50b153917a5c5604c1f9b78229fd24","size":2716499},{"platform":"unknown/unknown","digest":"sha256:b93f03d0ae11b988243e1b2cd8d29accf5b9670547b7bd8c7d96abecc7283e6e","size":1798},{"platform":"unknown/unknown","digest":"sha256:f034b182ba66366c63a5d195c6dfcd3333c027409c0ac98e55ade36aaa3b2963","size":1798}]} + _pv := &packages_model.PackageVersion{ PackageID: p.ID, CreatorID: mci.Creator.ID, @@ -273,25 +279,16 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met log.Error("Error inserting package: %v", err) return nil, err } - - if container_module.IsMediaTypeImageIndex(mci.MediaType) { - if pv.CreatedUnix.AsTime().Before(time.Now().Add(-24 * time.Hour)) { - if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { - return nil, err - } - // keep download count on overwriting - _pv.DownloadCount = pv.DownloadCount - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { - if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { - log.Error("Error inserting package: %v", err) - return nil, err - } - } - } else { - err = packages_model.UpdateVersion(ctx, &packages_model.PackageVersion{ID: pv.ID, MetadataJSON: _pv.MetadataJSON}) - if err != nil { - return nil, err - } + if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + return nil, err + } + // keep download count on overwriting + _pv.DownloadCount = pv.DownloadCount + pv, err = packages_model.GetOrInsertVersion(ctx, _pv) + if err != nil { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { + log.Error("Error inserting package: %v", err) + return nil, err } } } diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index 7e93cb47a2..3c2d8bac33 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -28,6 +28,7 @@ import ( oci "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPackageContainer(t *testing.T) { @@ -70,13 +71,12 @@ func TestPackageContainer(t *testing.T) { manifestDigest := "sha256:4f10484d1c1bb13e3956b4de1cd42db8e0f14a75be1617b60f2de3cd59c803c6" manifestContent := `{"schemaVersion":2,"mediaType":"` + container_module.ContentTypeDockerDistributionManifestV2 + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` - manifestContentType := container_module.ContentTypeDockerDistributionManifestV2 untaggedManifestDigest := "sha256:4305f5f5572b9a426b88909b036e52ee3cf3d7b9c1b01fac840e90747f56623d" untaggedManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` - indexManifestDigest := "sha256:bab112d6efb9e7f221995caaaa880352feb5bd8b1faf52fae8d12c113aa123ec" - indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}` + indexManifestDigest := "sha256:2c6b5afb967d5de02795ee1d177c3746d005df4b4c2b829385b0d186b3414b6b" + indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","is_tagged":true,"manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}` anonymousToken := "" userToken := "" @@ -467,15 +467,16 @@ func TestPackageContainer(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, 1, pv.DownloadCount) - // Overwrite existing tag should keep the download count - req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). - AddTokenAuth(userToken). - SetHeader("Content-Type", oci.MediaTypeImageManifest) - MakeRequest(t, req, http.StatusCreated) + t.Run("OverwriteTagKeepDownloadCount", func(t *testing.T) { + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). + AddTokenAuth(userToken). + SetHeader("Content-Type", oci.MediaTypeImageManifest) + MakeRequest(t, req, http.StatusCreated) - pv, err = packages_model.GetVersionByNameAndVersion(t.Context(), user.ID, packages_model.TypeContainer, image, tag) - assert.NoError(t, err) - assert.EqualValues(t, 1, pv.DownloadCount) + pv, err = packages_model.GetVersionByNameAndVersion(t.Context(), user.ID, packages_model.TypeContainer, image, tag) + assert.NoError(t, err) + assert.EqualValues(t, 1, pv.DownloadCount) + }) }) t.Run("HeadManifest", func(t *testing.T) { @@ -505,7 +506,7 @@ func TestPackageContainer(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, strconv.Itoa(len(manifestContent)), resp.Header().Get("Content-Length")) - assert.Equal(t, manifestContentType, resp.Header().Get("Content-Type")) + assert.Equal(t, oci.MediaTypeImageManifest, resp.Header().Get("Content-Type")) // the manifest is overwritten by above OverwriteTagKeepDownloadCount assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest")) assert.Equal(t, manifestContent, resp.Body.String()) }) @@ -599,6 +600,17 @@ func TestPackageContainer(t *testing.T) { assert.True(t, pd.Files[0].File.IsLead) assert.Equal(t, oci.MediaTypeImageIndex, pd.Files[0].Properties.GetByName(container_module.PropertyMediaType)) assert.Equal(t, indexManifestDigest, pd.Files[0].Properties.GetByName(container_module.PropertyDigest)) + + lastPackageVersionID := pv.ID + t.Run("UploadAgain", func(t *testing.T) { + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, multiTag), strings.NewReader(indexManifestContent)). + AddTokenAuth(userToken). + SetHeader("Content-Type", oci.MediaTypeImageIndex) + MakeRequest(t, req, http.StatusCreated) + pv, err := packages_model.GetVersionByNameAndVersion(t.Context(), user.ID, packages_model.TypeContainer, image, multiTag) + require.NoError(t, err) + assert.NotEqual(t, lastPackageVersionID, pv.ID) + }) }) t.Run("HeadBlob", func(t *testing.T) { From d6dc531d4be4f94dab1ef3b8e92ac9daa6fbb270 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 14 Nov 2025 05:21:05 +0100 Subject: [PATCH 34/52] Add GITEA_PR_INDEX env variable to githooks (#35938) `GITEA_PR_ID` is already part of the env variables available in the githooks, but it contains a database ID instead of commonly used index that is part of `owner/repo!index` --- modules/repository/env.go | 6 ++++-- services/pull/merge.go | 1 + services/pull/update_rebase.go | 1 + services/wiki/wiki.go | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/repository/env.go b/modules/repository/env.go index 78e06f86fb..55a81f006e 100644 --- a/modules/repository/env.go +++ b/modules/repository/env.go @@ -25,6 +25,7 @@ const ( EnvKeyID = "GITEA_KEY_ID" // public key ID EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID" EnvPRID = "GITEA_PR_ID" + EnvPRIndex = "GITEA_PR_INDEX" // not used by Gitea at the moment, it is for custom git hooks EnvPushTrigger = "GITEA_PUSH_TRIGGER" EnvIsInternal = "GITEA_INTERNAL_PUSH" EnvAppURL = "GITEA_ROOT_URL" @@ -50,11 +51,11 @@ func InternalPushingEnvironment(doer *user_model.User, repo *repo_model.Reposito // PushingEnvironment returns an os environment to allow hooks to work on push func PushingEnvironment(doer *user_model.User, repo *repo_model.Repository) []string { - return FullPushingEnvironment(doer, doer, repo, repo.Name, 0) + return FullPushingEnvironment(doer, doer, repo, repo.Name, 0, 0) } // FullPushingEnvironment returns an os environment to allow hooks to work on push -func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model.Repository, repoName string, prID int64) []string { +func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model.Repository, repoName string, prID, prIndex int64) []string { isWiki := "false" if strings.HasSuffix(repoName, ".wiki") { isWiki = "true" @@ -75,6 +76,7 @@ func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model EnvPusherID+"="+strconv.FormatInt(committer.ID, 10), EnvRepoID+"="+strconv.FormatInt(repo.ID, 10), EnvPRID+"="+strconv.FormatInt(prID, 10), + EnvPRIndex+"="+strconv.FormatInt(prIndex, 10), EnvAppURL+"="+setting.AppURL, "SSH_ORIGINAL_COMMAND=gitea-internal", ) diff --git a/services/pull/merge.go b/services/pull/merge.go index 9c7e09a227..f5430546a3 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -403,6 +403,7 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use pr.BaseRepo, pr.BaseRepo.Name, pr.ID, + pr.Index, ) mergeCtx.env = append(mergeCtx.env, repo_module.EnvPushTrigger+"="+string(pushTrigger)) diff --git a/services/pull/update_rebase.go b/services/pull/update_rebase.go index e6845f6b14..6a70c03467 100644 --- a/services/pull/update_rebase.go +++ b/services/pull/update_rebase.go @@ -80,6 +80,7 @@ func updateHeadByRebaseOnToBase(ctx context.Context, pr *issues_model.PullReques pr.HeadRepo, pr.HeadRepo.Name, pr.ID, + pr.Index, )). WithDir(mergeCtx.tmpBasePath). WithStdout(mergeCtx.outbuf). diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 25f836dd5d..a9dc726982 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -223,6 +223,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model repo, repo.Name+".wiki", 0, + 0, ), }); err != nil { log.Error("Push failed: %v", err) @@ -341,6 +342,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model repo, repo.Name+".wiki", 0, + 0, ), }); err != nil { if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { From 0fb3be7f0e5720915fd7866d29fbf66828aff71a Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 14 Nov 2025 12:50:48 +0800 Subject: [PATCH 35/52] Fix diff blob excerpt expansion (#35922) And add comments and tests --- routers/web/repo/compare.go | 39 +++++----- services/gitdiff/gitdiff.go | 42 ++++++++--- services/gitdiff/gitdiff_test.go | 123 +++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 33 deletions(-) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index f3375e4898..7750278a8d 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -9,7 +9,6 @@ import ( "encoding/csv" "errors" "fmt" - "html" "io" "net/http" "net/url" @@ -957,30 +956,26 @@ func ExcerptBlob(ctx *context.Context) { ctx.HTTPError(http.StatusInternalServerError, "getExcerptLines") return } - if idxRight > lastRight { - lineText := " " - if rightHunkSize > 0 || leftHunkSize > 0 { - lineText = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize) - } - lineText = html.EscapeString(lineText) - lineSection := &gitdiff.DiffLine{ - Type: gitdiff.DiffLineSection, - Content: lineText, - SectionInfo: &gitdiff.DiffLineSectionInfo{ - Path: filePath, - LastLeftIdx: lastLeft, - LastRightIdx: lastRight, - LeftIdx: idxLeft, - RightIdx: idxRight, - LeftHunkSize: leftHunkSize, - RightHunkSize: rightHunkSize, - }, - } + + newLineSection := &gitdiff.DiffLine{ + Type: gitdiff.DiffLineSection, + SectionInfo: &gitdiff.DiffLineSectionInfo{ + Path: filePath, + LastLeftIdx: lastLeft, + LastRightIdx: lastRight, + LeftIdx: idxLeft, + RightIdx: idxRight, + LeftHunkSize: leftHunkSize, + RightHunkSize: rightHunkSize, + }, + } + if newLineSection.GetExpandDirection() != "" { + newLineSection.Content = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize) switch direction { case "up": - section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...) + section.Lines = append([]*gitdiff.DiffLine{newLineSection}, section.Lines...) case "down": - section.Lines = append(section.Lines, lineSection) + section.Lines = append(section.Lines, newLineSection) } } diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 830bb1131b..4ad06bc04f 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -82,14 +82,34 @@ type DiffLine struct { // DiffLineSectionInfo represents diff line section meta data type DiffLineSectionInfo struct { - Path string - LastLeftIdx int - LastRightIdx int - LeftIdx int - RightIdx int + Path string + + // These line "idx" are 1-based line numbers + // Left/Right refer to the left/right side of the diff: + // + // LastLeftIdx | LastRightIdx + // [up/down expander] @@ hunk info @@ + // LeftIdx | RightIdx + + LastLeftIdx int + LastRightIdx int + LeftIdx int + RightIdx int + + // Hunk sizes of the hidden lines LeftHunkSize int RightHunkSize int + // For example: + // 17 | 31 + // [up/down] @@ -40,23 +54,9 @@ .... + // 40 | 54 + // + // In this case: + // LastLeftIdx = 17, LastRightIdx = 31 + // LeftHunkSize = 23, RightHunkSize = 9 + // LeftIdx = 40, RightIdx = 54 + HiddenCommentIDs []int64 // IDs of hidden comments in this section } @@ -158,13 +178,13 @@ func (d *DiffLine) getBlobExcerptQuery() string { return query } -func (d *DiffLine) getExpandDirection() string { +func (d *DiffLine) GetExpandDirection() string { if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.LeftIdx-d.SectionInfo.LastLeftIdx <= 1 || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 { return "" } if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 { return "up" - } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 { + } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx-1 > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 { return "updown" } else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 { return "down" @@ -202,13 +222,13 @@ func (d *DiffLine) RenderBlobExcerptButtons(fileNameHash string, data *DiffBlobE content += htmlutil.HTMLFormat(`%d`, tooltip, len(d.SectionInfo.HiddenCommentIDs)) } - expandDirection := d.getExpandDirection() - if expandDirection == "up" || expandDirection == "updown" { - content += makeButton("up", "octicon-fold-up") - } + expandDirection := d.GetExpandDirection() if expandDirection == "updown" || expandDirection == "down" { content += makeButton("down", "octicon-fold-down") } + if expandDirection == "up" || expandDirection == "updown" { + content += makeButton("up", "octicon-fold-up") + } if expandDirection == "single" { content += makeButton("single", "octicon-fold") } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 51fb9b58d6..721ae0dfc7 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -983,3 +983,126 @@ func TestDiffLine_RenderBlobExcerptButtons(t *testing.T) { }) } } + +func TestDiffLine_GetExpandDirection(t *testing.T) { + cases := []struct { + name string + diffLine *DiffLine + direction string + }{ + { + name: "NotSectionLine", + diffLine: &DiffLine{Type: DiffLineAdd, SectionInfo: &DiffLineSectionInfo{}}, + direction: "", + }, + { + name: "NilSectionInfo", + diffLine: &DiffLine{Type: DiffLineSection, SectionInfo: nil}, + direction: "", + }, + { + name: "NoHiddenLines", + // last block stops at line 100, next block starts at line 101, so no hidden lines, no expansion. + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 100, + LastLeftIdx: 100, + RightIdx: 101, + LeftIdx: 101, + }, + }, + direction: "", + }, + { + name: "FileHead", + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 0, // LastXxxIdx = 0 means this is the first section in the file. + LastLeftIdx: 0, + RightIdx: 1, + LeftIdx: 1, + }, + }, + direction: "", + }, + { + name: "FileHeadHiddenLines", + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 0, + LastLeftIdx: 0, + RightIdx: 101, + LeftIdx: 101, + }, + }, + direction: "up", + }, + { + name: "HiddenSingleHunk", + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 100, + LastLeftIdx: 100, + RightIdx: 102, + LeftIdx: 102, + RightHunkSize: 1234, // non-zero dummy value + LeftHunkSize: 5678, // non-zero dummy value + }, + }, + direction: "single", + }, + { + name: "HiddenSingleFullHunk", + // the hidden lines can exactly fit into one hunk + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 100, + LastLeftIdx: 100, + RightIdx: 100 + BlobExcerptChunkSize + 1, + LeftIdx: 100 + BlobExcerptChunkSize + 1, + RightHunkSize: 1234, // non-zero dummy value + LeftHunkSize: 5678, // non-zero dummy value + }, + }, + direction: "single", + }, + { + name: "HiddenUpDownHunks", + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 100, + LastLeftIdx: 100, + RightIdx: 100 + BlobExcerptChunkSize + 2, + LeftIdx: 100 + BlobExcerptChunkSize + 2, + RightHunkSize: 1234, // non-zero dummy value + LeftHunkSize: 5678, // non-zero dummy value + }, + }, + direction: "updown", + }, + { + name: "FileTail", + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 100, + LastLeftIdx: 100, + RightIdx: 102, + LeftIdx: 102, + RightHunkSize: 0, + LeftHunkSize: 0, + }, + }, + direction: "down", + }, + } + for _, c := range cases { + assert.Equal(t, c.direction, c.diffLine.GetExpandDirection(), "case %s expected direction: %s", c.name, c.direction) + } +} From de69e7f16a9b4158c4a03b5d5ef9654c766f9518 Mon Sep 17 00:00:00 2001 From: DrMaxNix Date: Tue, 18 Nov 2025 08:55:27 +0000 Subject: [PATCH 36/52] Change project default column icon to 'star' (#35967) Consistently use a `star` icon to highlight the default column of a project. The icon is both shown while viewing the project, as well as while changing the default status of this column. image --- options/locale/locale_en-US.ini | 1 + templates/projects/view.tmpl | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ddc12aefaa..ea69d45fa2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1482,6 +1482,7 @@ projects.column.new_submit = "Create Column" projects.column.new = "New Column" projects.column.set_default = "Set Default" projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls" +projects.column.default_column_hint = "New issues added to this project will be added to this column" projects.column.delete = "Delete Column" projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?" projects.column.color = "Color" diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 21bc287643..5801396e3c 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -78,7 +78,9 @@
    {{.NumIssues}}
    -
    {{.Title}}
    +
    + {{if .Default}}{{svg "octicon-star"}} {{end}}{{.Title}} +
    {{if $canWriteProject}}