From d061f6b70a65de50ff1a7577e4acceeb4f5cfd69 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 10 Dec 2024 09:44:39 +0100 Subject: [PATCH 1/8] Change typescript `module` to `nodenext` (#32757) Typescript 5.7 changed semantics around JSON imports and `nodenext` is now [treated differently](https://devblogs.microsoft.com/typescript/announcing-typescript-5-7-beta/#validated-json-imports-in---module-nodenext) than `node16` for JSON imports and it requires the import attribute, so change the value to that and add the attribute to eliminate this typescript error. [`moduleResolution`](https://www.typescriptlang.org/tsconfig/#moduleResolution) is treated as an alias when `module` is `nodenext`, so we don't need to specify it. Also see https://github.com/microsoft/TypeScript/issues/60589. It appears the next Typescript release will fix this for `node16`, but I guess it'll still be good to switch to `nodenext`. --- tsconfig.json | 3 +-- web_src/js/utils/match.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 744f1511e951c..e006535c02ca2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,7 @@ ], "compilerOptions": { "target": "es2020", - "module": "node16", - "moduleResolution": "node16", + "module": "nodenext", "lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext"], "allowImportingTsExtensions": true, "allowJs": true, diff --git a/web_src/js/utils/match.ts b/web_src/js/utils/match.ts index 274c9322ffac6..af669116a2738 100644 --- a/web_src/js/utils/match.ts +++ b/web_src/js/utils/match.ts @@ -1,4 +1,4 @@ -import emojis from '../../../assets/emoji.json'; +import emojis from '../../../assets/emoji.json' with {type: 'json'}; import {GET} from '../modules/fetch.ts'; import type {Issue} from '../types.ts'; From 8f271c60366ece02f44649de5f8ba57388adb854 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 11 Dec 2024 00:41:44 +0800 Subject: [PATCH 2/8] Fix wiki ui (#32781) Fix #32774 --- templates/repo/wiki/view.tmpl | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 781c9326ad64f..c8e0b4254ca8e 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -34,26 +34,26 @@
-
-
+
+
{{.CommitCount}} {{svg "octicon-history"}} - {{$title}} -
- {{$timeSince := DateUtils.TimeSince .Author.When}} - {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}} +
+ {{$title}} +
+ {{$timeSince := DateUtils.TimeSince .Author.When}} + {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}} +
- From 2ac6f2b129fd6d955ac0fdb4dcf46efd5163f3b3 Mon Sep 17 00:00:00 2001 From: yp05327 <576951401@qq.com> Date: Wed, 11 Dec 2024 03:42:52 +0900 Subject: [PATCH 3/8] Fix internal server error when updating labels without write permission (#32776) Fix #32775 if permission denined, `prepareForReplaceOrAdd` will return nothing, and this case is not handled. --- routers/api/v1/repo/issue_label.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index 2f5ea8931b148..cc517619e97d0 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -319,6 +319,11 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) return nil, nil, err } + if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + ctx.Error(http.StatusForbidden, "CanWriteIssuesOrPulls", "write permission is required") + return nil, nil, fmt.Errorf("permission denied") + } + var ( labelIDs []int64 labelNames []string @@ -350,10 +355,5 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) return nil, nil, err } - if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.Status(http.StatusForbidden) - return nil, nil, nil - } - return issue, labels, err } From fbe6d9dc6b9f04e8be219ad4b442df9e08c16b7b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 10 Dec 2024 13:15:06 -0800 Subject: [PATCH 4/8] Use batch database operations instead of one by one to optimze api pulls (#32680) Resolve #31492 The response time for the Pull Requests API has improved significantly, dropping from over `2000ms` to about `350ms` on my local machine. It's about `6` times faster. A key area for further optimization lies in batch-fetching data for `apiPullRequest.ChangedFiles, apiPullRequest.Additions, and apiPullRequest.Deletions`. Tests `TestAPIViewPulls` does exist and new tests added. - This PR also fixes some bugs in `GetDiff` functions. - This PR also fixes data inconsistent in test data. For a pull request, the head branch's reference should be equal to the reference in `pull/xxx/head`. --- models/fixtures/review.yml | 20 +- models/issues/pull_list.go | 59 ++++ models/issues/pull_list_test.go | 64 +++++ models/issues/pull_test.go | 20 +- models/issues/review_list.go | 11 +- models/issues/review_test.go | 32 ++- models/organization/team_list.go | 5 + models/organization/team_list_test.go | 25 ++ modules/git/repo_compare.go | 38 ++- routers/api/v1/repo/pull.go | 35 +-- services/convert/pull.go | 251 ++++++++++++++++++ .../user2/repo1.git/refs/pull/3/head | 2 +- tests/integration/api_pull_commits_test.go | 4 +- tests/integration/api_pull_test.go | 89 ++++++- tests/integration/pull_commit_test.go | 4 +- 15 files changed, 566 insertions(+), 93 deletions(-) create mode 100644 models/issues/pull_list_test.go create mode 100644 models/organization/team_list_test.go diff --git a/models/fixtures/review.yml b/models/fixtures/review.yml index 0438ceadae289..5b8bbceca9edf 100644 --- a/models/fixtures/review.yml +++ b/models/fixtures/review.yml @@ -4,6 +4,7 @@ reviewer_id: 1 issue_id: 2 content: "Demo Review" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -12,6 +13,7 @@ reviewer_id: 534543 issue_id: 534543 content: "Invalid Review #1" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -20,6 +22,7 @@ reviewer_id: 1 issue_id: 343545 content: "Invalid Review #2" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -28,6 +31,7 @@ reviewer_id: 1 issue_id: 2 content: "Pending Review" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -36,6 +40,7 @@ reviewer_id: 1 issue_id: 3 content: "New review 1" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -61,8 +66,8 @@ type: 1 reviewer_id: 4 issue_id: 3 - original_author_id: 0 content: "New review 5" + original_author_id: 0 commit_id: 8091a55037cd59e47293aca02981b5a67076b364 stale: true updated_unix: 946684813 @@ -73,9 +78,9 @@ reviewer_id: 2 issue_id: 3 content: "New review 3 rejected" + original_author_id: 0 updated_unix: 946684814 created_unix: 946684814 - original_author_id: 0 - id: 10 @@ -83,6 +88,7 @@ reviewer_id: 100 issue_id: 3 content: "a deleted user's review" + original_author_id: 0 official: true updated_unix: 946684815 created_unix: 946684815 @@ -112,6 +118,7 @@ reviewer_id: 5 issue_id: 11 content: "old review from user5" + original_author_id: 0 updated_unix: 946684820 created_unix: 946684820 @@ -121,6 +128,7 @@ reviewer_id: 5 issue_id: 11 content: "duplicate review from user5 (latest)" + original_author_id: 0 updated_unix: 946684830 created_unix: 946684830 @@ -130,6 +138,7 @@ reviewer_id: 6 issue_id: 11 content: "singular review from org6 and final review for this pr" + original_author_id: 0 updated_unix: 946684831 created_unix: 946684831 @@ -139,6 +148,7 @@ reviewer_id: 20 issue_id: 20 content: "review request for user20" + original_author_id: 0 updated_unix: 946684832 created_unix: 946684832 @@ -148,6 +158,7 @@ reviewer_id: 20 issue_id: 20 content: "review approved by user20" + original_author_id: 0 updated_unix: 946684833 created_unix: 946684833 @@ -158,6 +169,7 @@ reviewer_team_id: 5 issue_id: 20 content: "review request for team5" + original_author_id: 0 updated_unix: 946684834 created_unix: 946684834 @@ -168,6 +180,7 @@ reviewer_team_id: 0 issue_id: 20 content: "review request for user15" + original_author_id: 0 updated_unix: 946684835 created_unix: 946684835 @@ -177,6 +190,7 @@ reviewer_id: 1 issue_id: 2 content: "Review Comment" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 @@ -186,6 +200,7 @@ reviewer_id: 5 issue_id: 3 content: "reviewed by user5" + original_author_id: 0 commit_id: 4a357436d925b5c974181ff12a994538ddc5a269 updated_unix: 946684816 created_unix: 946684816 @@ -196,5 +211,6 @@ reviewer_id: 5 issue_id: 3 content: "review request for user5" + original_author_id: 0 updated_unix: 946684817 created_unix: 946684817 diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index 9155ea0834685..59010aa9d069a 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" + "xorm.io/builder" "xorm.io/xorm" ) @@ -240,6 +241,64 @@ func (prs PullRequestList) GetIssueIDs() []int64 { }) } +func (prs PullRequestList) LoadReviewCommentsCounts(ctx context.Context) (map[int64]int, error) { + issueIDs := prs.GetIssueIDs() + countsMap := make(map[int64]int, len(issueIDs)) + counts := make([]struct { + IssueID int64 + Count int + }, 0, len(issueIDs)) + if err := db.GetEngine(ctx).Select("issue_id, count(*) as count"). + Table("comment").In("issue_id", issueIDs).And("type = ?", CommentTypeReview). + GroupBy("issue_id").Find(&counts); err != nil { + return nil, err + } + for _, c := range counts { + countsMap[c.IssueID] = c.Count + } + return countsMap, nil +} + +func (prs PullRequestList) LoadReviews(ctx context.Context) (ReviewList, error) { + issueIDs := prs.GetIssueIDs() + reviews := make([]*Review, 0, len(issueIDs)) + + subQuery := builder.Select("max(id) as id"). + From("review"). + Where(builder.In("issue_id", issueIDs)). + And(builder.In("`type`", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest)). + And(builder.Eq{ + "dismissed": false, + "original_author_id": 0, + "reviewer_team_id": 0, + }). + GroupBy("issue_id, reviewer_id") + // Get latest review of each reviewer, sorted in order they were made + if err := db.GetEngine(ctx).In("id", subQuery).OrderBy("review.updated_unix ASC").Find(&reviews); err != nil { + return nil, err + } + + teamReviewRequests := make([]*Review, 0, 5) + subQueryTeam := builder.Select("max(id) as id"). + From("review"). + Where(builder.In("issue_id", issueIDs)). + And(builder.Eq{ + "original_author_id": 0, + }).And(builder.Neq{ + "reviewer_team_id": 0, + }). + GroupBy("issue_id, reviewer_team_id") + if err := db.GetEngine(ctx).In("id", subQueryTeam).OrderBy("review.updated_unix ASC").Find(&teamReviewRequests); err != nil { + return nil, err + } + + if len(teamReviewRequests) > 0 { + reviews = append(reviews, teamReviewRequests...) + } + + return reviews, nil +} + // HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) { return db.GetEngine(ctx). diff --git a/models/issues/pull_list_test.go b/models/issues/pull_list_test.go new file mode 100644 index 0000000000000..8b814a0d0fc59 --- /dev/null +++ b/models/issues/pull_list_test.go @@ -0,0 +1,64 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestPullRequestList_LoadAttributes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + prs := []*issues_model.PullRequest{ + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), + } + assert.NoError(t, issues_model.PullRequestList(prs).LoadAttributes(db.DefaultContext)) + for _, pr := range prs { + assert.NotNil(t, pr.Issue) + assert.Equal(t, pr.IssueID, pr.Issue.ID) + } + + assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(db.DefaultContext)) +} + +func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + prs := []*issues_model.PullRequest{ + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), + } + reviewComments, err := issues_model.PullRequestList(prs).LoadReviewCommentsCounts(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, reviewComments, 2) + for _, pr := range prs { + assert.EqualValues(t, reviewComments[pr.IssueID], 1) + } +} + +func TestPullRequestList_LoadReviews(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + prs := []*issues_model.PullRequest{ + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), + } + reviewList, err := issues_model.PullRequestList(prs).LoadReviews(db.DefaultContext) + assert.NoError(t, err) + // 1, 7, 8, 9, 10, 22 + assert.Len(t, reviewList, 6) + assert.EqualValues(t, 1, reviewList[0].ID) + assert.EqualValues(t, 7, reviewList[1].ID) + assert.EqualValues(t, 8, reviewList[2].ID) + assert.EqualValues(t, 9, reviewList[3].ID) + assert.EqualValues(t, 10, reviewList[4].ID) + assert.EqualValues(t, 22, reviewList[5].ID) +} diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 675c90527d8d7..cb7b47263d884 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -79,7 +79,7 @@ func TestPullRequestsNewest(t *testing.T) { func TestLoadRequestedReviewers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) assert.NoError(t, pull.LoadIssue(db.DefaultContext)) issue := pull.Issue assert.NoError(t, issue.LoadRepo(db.DefaultContext)) @@ -93,7 +93,7 @@ func TestLoadRequestedReviewers(t *testing.T) { assert.NotNil(t, comment) assert.NoError(t, pull.LoadRequestedReviewers(db.DefaultContext)) - assert.Len(t, pull.RequestedReviewers, 1) + assert.Len(t, pull.RequestedReviewers, 6) comment, err = issues_model.RemoveReviewRequest(db.DefaultContext, issue, user1, &user_model.User{}) assert.NoError(t, err) @@ -234,22 +234,6 @@ func TestPullRequest_UpdateCols(t *testing.T) { unittest.CheckConsistencyFor(t, pr) } -func TestPullRequestList_LoadAttributes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - prs := []*issues_model.PullRequest{ - unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), - unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), - } - assert.NoError(t, issues_model.PullRequestList(prs).LoadAttributes(db.DefaultContext)) - for _, pr := range prs { - assert.NotNil(t, pr.Issue) - assert.Equal(t, pr.IssueID, pr.Issue.ID) - } - - assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(db.DefaultContext)) -} - // TODO TestAddTestPullRequestTask func TestPullRequest_IsWorkInProgress(t *testing.T) { diff --git a/models/issues/review_list.go b/models/issues/review_list.go index a5ceb2179116c..bc7d7ec0f0168 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -47,14 +47,9 @@ func (reviews ReviewList) LoadReviewersTeams(ctx context.Context) error { } } - teamsMap := make(map[int64]*organization_model.Team, 0) - for _, teamID := range reviewersTeamsIDs { - team, err := organization_model.GetTeamByID(ctx, teamID) - if err != nil { - return err - } - - teamsMap[teamID] = team + teamsMap, err := organization_model.GetTeamsByIDs(ctx, reviewersTeamsIDs) + if err != nil { + return err } for _, review := range reviews { diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 942121fd8f257..50330e3ff2c60 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -126,42 +126,48 @@ func TestGetReviewersByIssueID(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) expectedReviews := []*issues_model.Review{} expectedReviews = append(expectedReviews, &issues_model.Review{ + ID: 7, Reviewer: org3, Type: issues_model.ReviewTypeReject, UpdatedUnix: 946684812, }, &issues_model.Review{ + ID: 8, Reviewer: user4, Type: issues_model.ReviewTypeApprove, UpdatedUnix: 946684813, }, &issues_model.Review{ + ID: 9, Reviewer: user2, Type: issues_model.ReviewTypeReject, UpdatedUnix: 946684814, - }) + }, + &issues_model.Review{ + ID: 10, + Reviewer: user_model.NewGhostUser(), + Type: issues_model.ReviewTypeReject, + UpdatedUnix: 946684815, + }, + &issues_model.Review{ + ID: 22, + Reviewer: user5, + Type: issues_model.ReviewTypeRequest, + UpdatedUnix: 946684817, + }, + ) allReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) assert.NoError(t, err) for _, review := range allReviews { assert.NoError(t, review.LoadReviewer(db.DefaultContext)) } - if assert.Len(t, allReviews, 3) { - for i, review := range allReviews { - assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) - assert.Equal(t, expectedReviews[i].Type, review.Type) - assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix) - } - } - - allReviews, err = issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) - assert.NoError(t, err) - assert.NoError(t, allReviews.LoadReviewers(db.DefaultContext)) - if assert.Len(t, allReviews, 3) { + if assert.Len(t, allReviews, 5) { for i, review := range allReviews { assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) assert.Equal(t, expectedReviews[i].Type, review.Type) diff --git a/models/organization/team_list.go b/models/organization/team_list.go index cc2a50236a447..4ceb405e31004 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -131,3 +131,8 @@ func GetTeamsByOrgIDs(ctx context.Context, orgIDs []int64) (TeamList, error) { teams := make([]*Team, 0, 10) return teams, db.GetEngine(ctx).Where(builder.In("org_id", orgIDs)).Find(&teams) } + +func GetTeamsByIDs(ctx context.Context, teamIDs []int64) (map[int64]*Team, error) { + teams := make(map[int64]*Team, len(teamIDs)) + return teams, db.GetEngine(ctx).Where(builder.In("`id`", teamIDs)).Find(&teams) +} diff --git a/models/organization/team_list_test.go b/models/organization/team_list_test.go new file mode 100644 index 0000000000000..5526446e221d0 --- /dev/null +++ b/models/organization/team_list_test.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package organization_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + org_model "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func Test_GetTeamsByIDs(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // 1 owner team, 2 normal team + teams, err := org_model.GetTeamsByIDs(db.DefaultContext, []int64{1, 2}) + assert.NoError(t, err) + assert.Len(t, teams, 2) + assert.Equal(t, "Owners", teams[1].Name) + assert.Equal(t, "team1", teams[2].Name) +} diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index b6e9d2b44a114..16fcdcf4c8f96 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -246,18 +246,40 @@ func (repo *Repository) GetDiffOrPatch(base, head string, w io.Writer, patch, bi // GetDiff generates and returns patch data between given revisions, optimized for human readability func (repo *Repository) GetDiff(base, head string, w io.Writer) error { - return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base, head).Run(&RunOpts{ - Dir: repo.Path, - Stdout: w, - }) + stderr := new(bytes.Buffer) + err := NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base + "..." + head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + Stderr: stderr, + }) + if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { + return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base, head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + }) + } + return err } // GetDiffBinary generates and returns patch data between given revisions, including binary diffs. func (repo *Repository) GetDiffBinary(base, head string, w io.Writer) error { - return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base, head).Run(&RunOpts{ - Dir: repo.Path, - Stdout: w, - }) + stderr := new(bytes.Buffer) + err := NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base + "..." + head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + Stderr: stderr, + }) + if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { + return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base, head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + }) + } + return err } // GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 1116a4e9b1d25..86b204f51e250 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -139,42 +139,11 @@ func ListPullRequests(ctx *context.APIContext) { return } - apiPrs := make([]*api.PullRequest, len(prs)) - // NOTE: load repository first, so that issue.Repo will be filled with pr.BaseRepo - if err := prs.LoadRepositories(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadRepositories", err) - return - } - issueList, err := prs.LoadIssues(ctx) + apiPrs, err := convert.ToAPIPullRequests(ctx, ctx.Repo.Repository, prs, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssues", err) - return - } - - if err := issueList.LoadLabels(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadLabels", err) - return - } - if err := issueList.LoadPosters(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadPoster", err) + ctx.Error(http.StatusInternalServerError, "ToAPIPullRequests", err) return } - if err := issueList.LoadAttachments(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) - return - } - if err := issueList.LoadMilestones(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadMilestones", err) - return - } - if err := issueList.LoadAssignees(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAssignees", err) - return - } - - for i := range prs { - apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.Doer) - } ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) diff --git a/services/convert/pull.go b/services/convert/pull.go index 4ec24a8276a13..ddaaa300a4420 100644 --- a/services/convert/pull.go +++ b/services/convert/pull.go @@ -7,9 +7,11 @@ import ( "context" "fmt" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" @@ -259,3 +261,252 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u return apiPullRequest } + +func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs issues_model.PullRequestList, doer *user_model.User) ([]*api.PullRequest, error) { + for _, pr := range prs { + pr.BaseRepo = baseRepo + if pr.BaseRepoID == pr.HeadRepoID { + pr.HeadRepo = baseRepo + } + } + + // NOTE: load head repositories + if err := prs.LoadRepositories(ctx); err != nil { + return nil, err + } + issueList, err := prs.LoadIssues(ctx) + if err != nil { + return nil, err + } + + if err := issueList.LoadLabels(ctx); err != nil { + return nil, err + } + if err := issueList.LoadPosters(ctx); err != nil { + return nil, err + } + if err := issueList.LoadAttachments(ctx); err != nil { + return nil, err + } + if err := issueList.LoadMilestones(ctx); err != nil { + return nil, err + } + if err := issueList.LoadAssignees(ctx); err != nil { + return nil, err + } + + reviews, err := prs.LoadReviews(ctx) + if err != nil { + return nil, err + } + if err = reviews.LoadReviewers(ctx); err != nil { + return nil, err + } + + reviewersMap := make(map[int64][]*user_model.User) + for _, review := range reviews { + if review.Reviewer != nil { + reviewersMap[review.IssueID] = append(reviewersMap[review.IssueID], review.Reviewer) + } + } + + reviewCounts, err := prs.LoadReviewCommentsCounts(ctx) + if err != nil { + return nil, err + } + + gitRepo, err := gitrepo.OpenRepository(ctx, baseRepo) + if err != nil { + return nil, err + } + defer gitRepo.Close() + + baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, baseRepo, doer) + if err != nil { + log.Error("GetUserRepoPermission[%d]: %v", baseRepo.ID, err) + baseRepoPerm.AccessMode = perm.AccessModeNone + } + + apiRepo := ToRepo(ctx, baseRepo, baseRepoPerm) + baseBranchCache := make(map[string]*git_model.Branch) + apiPullRequests := make([]*api.PullRequest, 0, len(prs)) + for _, pr := range prs { + apiIssue := ToAPIIssue(ctx, doer, pr.Issue) + + apiPullRequest := &api.PullRequest{ + ID: pr.ID, + URL: pr.Issue.HTMLURL(), + Index: pr.Index, + Poster: apiIssue.Poster, + Title: apiIssue.Title, + Body: apiIssue.Body, + Labels: apiIssue.Labels, + Milestone: apiIssue.Milestone, + Assignee: apiIssue.Assignee, + Assignees: apiIssue.Assignees, + State: apiIssue.State, + Draft: pr.IsWorkInProgress(ctx), + IsLocked: apiIssue.IsLocked, + Comments: apiIssue.Comments, + ReviewComments: reviewCounts[pr.IssueID], + HTMLURL: pr.Issue.HTMLURL(), + DiffURL: pr.Issue.DiffURL(), + PatchURL: pr.Issue.PatchURL(), + HasMerged: pr.HasMerged, + MergeBase: pr.MergeBase, + Mergeable: pr.Mergeable(ctx), + Deadline: apiIssue.Deadline, + Created: pr.Issue.CreatedUnix.AsTimePtr(), + Updated: pr.Issue.UpdatedUnix.AsTimePtr(), + PinOrder: apiIssue.PinOrder, + + AllowMaintainerEdit: pr.AllowMaintainerEdit, + + Base: &api.PRBranchInfo{ + Name: pr.BaseBranch, + Ref: pr.BaseBranch, + RepoID: pr.BaseRepoID, + Repository: apiRepo, + }, + Head: &api.PRBranchInfo{ + Name: pr.HeadBranch, + Ref: fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index), + RepoID: -1, + }, + } + + pr.RequestedReviewers = reviewersMap[pr.IssueID] + for _, reviewer := range pr.RequestedReviewers { + apiPullRequest.RequestedReviewers = append(apiPullRequest.RequestedReviewers, ToUser(ctx, reviewer, nil)) + } + + for _, reviewerTeam := range pr.RequestedReviewersTeams { + convertedTeam, err := ToTeam(ctx, reviewerTeam, true) + if err != nil { + log.Error("LoadRequestedReviewersTeams[%d]: %v", pr.ID, err) + return nil, err + } + + apiPullRequest.RequestedReviewersTeams = append(apiPullRequest.RequestedReviewersTeams, convertedTeam) + } + + if pr.Issue.ClosedUnix != 0 { + apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr() + } + + baseBranch, ok := baseBranchCache[pr.BaseBranch] + if !ok { + baseBranch, err = git_model.GetBranch(ctx, baseRepo.ID, pr.BaseBranch) + if err == nil { + baseBranchCache[pr.BaseBranch] = baseBranch + } else if !git_model.IsErrBranchNotExist(err) { + return nil, err + } + } + + if baseBranch != nil { + apiPullRequest.Base.Sha = baseBranch.CommitID + } + + if pr.Flow == issues_model.PullRequestFlowAGit { + apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(pr.GetGitRefName()) + if err != nil { + log.Error("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err) + return nil, err + } + apiPullRequest.Head.RepoID = pr.BaseRepoID + apiPullRequest.Head.Repository = apiPullRequest.Base.Repository + apiPullRequest.Head.Name = "" + } + + var headGitRepo *git.Repository + if pr.HeadRepo != nil && pr.Flow == issues_model.PullRequestFlowGithub { + if pr.HeadRepoID == pr.BaseRepoID { + apiPullRequest.Head.RepoID = pr.HeadRepo.ID + apiPullRequest.Head.Repository = apiRepo + headGitRepo = gitRepo + } else { + p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer) + if err != nil { + log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err) + p.AccessMode = perm.AccessModeNone + } + + apiPullRequest.Head.RepoID = pr.HeadRepo.ID + apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) + + headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo) + if err != nil { + log.Error("OpenRepository[%s]: %v", pr.HeadRepo.RepoPath(), err) + return nil, err + } + defer headGitRepo.Close() + } + + headBranch, err := headGitRepo.GetBranch(pr.HeadBranch) + if err != nil && !git.IsErrBranchNotExist(err) { + log.Error("GetBranch[%s]: %v", pr.HeadBranch, err) + return nil, err + } + + // Outer scope variables to be used in diff calculation + var ( + startCommitID string + endCommitID string + ) + + if git.IsErrBranchNotExist(err) { + headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref) + if err != nil && !git.IsErrNotExist(err) { + log.Error("GetCommit[%s]: %v", pr.HeadBranch, err) + return nil, err + } + if err == nil { + apiPullRequest.Head.Sha = headCommitID + endCommitID = headCommitID + } + } else { + commit, err := headBranch.GetCommit() + if err != nil && !git.IsErrNotExist(err) { + log.Error("GetCommit[%s]: %v", headBranch.Name, err) + return nil, err + } + if err == nil { + apiPullRequest.Head.Ref = pr.HeadBranch + apiPullRequest.Head.Sha = commit.ID.String() + endCommitID = commit.ID.String() + } + } + + // Calculate diff + startCommitID = pr.MergeBase + + apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID) + if err != nil { + log.Error("GetDiffShortStat: %v", err) + } + } + + if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 { + refs, err := gitRepo.GetRefsFiltered(apiPullRequest.Head.Ref) + if err != nil { + log.Error("GetRefsFiltered[%s]: %v", apiPullRequest.Head.Ref, err) + return nil, err + } else if len(refs) == 0 { + log.Error("unable to resolve PR head ref") + } else { + apiPullRequest.Head.Sha = refs[0].Object.String() + } + } + + if pr.HasMerged { + apiPullRequest.Merged = pr.MergedUnix.AsTimePtr() + apiPullRequest.MergedCommitID = &pr.MergedCommitID + apiPullRequest.MergedBy = ToUser(ctx, pr.Merger, nil) + } + + apiPullRequests = append(apiPullRequests, apiPullRequest) + } + + return apiPullRequests, nil +} diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/3/head b/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/3/head index 33a9eaa7f162f..5add7256cd9e6 100644 --- a/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/3/head +++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/3/head @@ -1 +1 @@ -5f22f7d0d95d614d25a5b68592adb345a4b5c7fd +985f0301dba5e7b34be866819cd15ad3d8f508ee diff --git a/tests/integration/api_pull_commits_test.go b/tests/integration/api_pull_commits_test.go index ad0cb0329ccd9..5ffc8158f356b 100644 --- a/tests/integration/api_pull_commits_test.go +++ b/tests/integration/api_pull_commits_test.go @@ -33,8 +33,8 @@ func TestAPIPullCommits(t *testing.T) { return } - assert.Equal(t, "5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", commits[0].SHA) - assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", commits[1].SHA) + assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", commits[0].SHA) + assert.Equal(t, "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2", commits[1].SHA) assert.NotEmpty(t, commits[0].Files) assert.NotEmpty(t, commits[1].Files) diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go index b7e01d4a20c94..d26b285a1a8ee 100644 --- a/tests/integration/api_pull_test.go +++ b/tests/integration/api_pull_test.go @@ -4,6 +4,8 @@ package integration import ( + "bytes" + "context" "fmt" "io" "net/http" @@ -19,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/gitdiff" issue_service "code.gitea.io/gitea/services/issue" "code.gitea.io/gitea/tests" @@ -41,25 +44,99 @@ func TestAPIViewPulls(t *testing.T) { expectedLen := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true)) assert.Len(t, pulls, expectedLen) + assert.Len(t, pulls, 3) pull := pulls[0] + assert.EqualValues(t, 1, pull.Poster.ID) + assert.Len(t, pull.RequestedReviewers, 2) + assert.Len(t, pull.RequestedReviewersTeams, 0) + assert.EqualValues(t, 5, pull.RequestedReviewers[0].ID) + assert.EqualValues(t, 6, pull.RequestedReviewers[1].ID) + assert.EqualValues(t, 1, pull.ChangedFiles) + if assert.EqualValues(t, 5, pull.ID) { resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK) - _, err := io.ReadAll(resp.Body) + bs, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + patch, err := gitdiff.ParsePatch(context.Background(), 1000, 5000, 10, bytes.NewReader(bs), "") assert.NoError(t, err) - // TODO: use diff to generate stats to test against + if assert.Len(t, patch.Files, pull.ChangedFiles) { + assert.Equal(t, "File-WoW", patch.Files[0].Name) + // FIXME: The old name should be empty if it's a file add type + assert.Equal(t, "File-WoW", patch.Files[0].OldName) + assert.EqualValues(t, pull.Additions, patch.Files[0].Addition) + assert.EqualValues(t, pull.Deletions, patch.Files[0].Deletion) + assert.Equal(t, gitdiff.DiffFileAdd, patch.Files[0].Type) + } t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID), doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) { - if assert.Len(t, files, 1) { + if assert.Len(t, files, pull.ChangedFiles) { assert.Equal(t, "File-WoW", files[0].Filename) assert.Empty(t, files[0].PreviousFilename) - assert.EqualValues(t, 1, files[0].Additions) - assert.EqualValues(t, 1, files[0].Changes) - assert.EqualValues(t, 0, files[0].Deletions) + assert.EqualValues(t, pull.Additions, files[0].Additions) + assert.EqualValues(t, pull.Deletions, files[0].Deletions) assert.Equal(t, "added", files[0].Status) } })) } + + pull = pulls[1] + assert.EqualValues(t, 1, pull.Poster.ID) + assert.Len(t, pull.RequestedReviewers, 4) + assert.Len(t, pull.RequestedReviewersTeams, 0) + assert.EqualValues(t, 3, pull.RequestedReviewers[0].ID) + assert.EqualValues(t, 4, pull.RequestedReviewers[1].ID) + assert.EqualValues(t, 2, pull.RequestedReviewers[2].ID) + assert.EqualValues(t, 5, pull.RequestedReviewers[3].ID) + assert.EqualValues(t, 1, pull.ChangedFiles) + + if assert.EqualValues(t, 2, pull.ID) { + resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK) + bs, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + patch, err := gitdiff.ParsePatch(context.Background(), 1000, 5000, 10, bytes.NewReader(bs), "") + assert.NoError(t, err) + if assert.Len(t, patch.Files, pull.ChangedFiles) { + assert.Equal(t, "README.md", patch.Files[0].Name) + assert.Equal(t, "README.md", patch.Files[0].OldName) + assert.EqualValues(t, pull.Additions, patch.Files[0].Addition) + assert.EqualValues(t, pull.Deletions, patch.Files[0].Deletion) + assert.Equal(t, gitdiff.DiffFileChange, patch.Files[0].Type) + } + + t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID), + doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) { + if assert.Len(t, files, pull.ChangedFiles) { + assert.Equal(t, "README.md", files[0].Filename) + // FIXME: The PreviousFilename name should be the same as Filename if it's a file change + assert.Equal(t, "", files[0].PreviousFilename) + assert.EqualValues(t, pull.Additions, files[0].Additions) + assert.EqualValues(t, pull.Deletions, files[0].Deletions) + assert.Equal(t, "changed", files[0].Status) + } + })) + } + + pull = pulls[2] + assert.EqualValues(t, 1, pull.Poster.ID) + assert.Len(t, pull.RequestedReviewers, 1) + assert.Len(t, pull.RequestedReviewersTeams, 0) + assert.EqualValues(t, 1, pull.RequestedReviewers[0].ID) + assert.EqualValues(t, 0, pull.ChangedFiles) + + if assert.EqualValues(t, 1, pull.ID) { + resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK) + bs, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + patch, err := gitdiff.ParsePatch(context.Background(), 1000, 5000, 10, bytes.NewReader(bs), "") + assert.NoError(t, err) + assert.EqualValues(t, pull.ChangedFiles, patch.NumFiles) + + t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID), + doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) { + assert.Len(t, files, pull.ChangedFiles) + })) + } } func TestAPIViewPullsByBaseHead(t *testing.T) { diff --git a/tests/integration/pull_commit_test.go b/tests/integration/pull_commit_test.go index 477f01725d3ab..8d98349fd3560 100644 --- a/tests/integration/pull_commit_test.go +++ b/tests/integration/pull_commit_test.go @@ -26,8 +26,8 @@ func TestListPullCommits(t *testing.T) { DecodeJSON(t, resp, &pullCommitList) if assert.Len(t, pullCommitList.Commits, 2) { - assert.Equal(t, "5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", pullCommitList.Commits[0].ID) - assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", pullCommitList.Commits[1].ID) + assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", pullCommitList.Commits[0].ID) + assert.Equal(t, "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2", pullCommitList.Commits[1].ID) } assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", pullCommitList.LastReviewCommitSha) }) From 734ddf71180a9bf843d12dd9664eed28ba1a5748 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 11 Dec 2024 00:34:48 +0000 Subject: [PATCH 5/8] [skip ci] Updated translations via Crowdin --- options/locale/locale_cs-CZ.ini | 2 -- options/locale/locale_de-DE.ini | 2 -- options/locale/locale_el-GR.ini | 2 -- options/locale/locale_es-ES.ini | 2 -- options/locale/locale_fr-FR.ini | 2 -- options/locale/locale_ga-IE.ini | 2 -- options/locale/locale_it-IT.ini | 1 - options/locale/locale_ja-JP.ini | 2 -- options/locale/locale_lv-LV.ini | 2 -- options/locale/locale_nl-NL.ini | 1 - options/locale/locale_pt-BR.ini | 2 -- options/locale/locale_pt-PT.ini | 2 -- options/locale/locale_ru-RU.ini | 2 -- options/locale/locale_tr-TR.ini | 2 -- options/locale/locale_zh-CN.ini | 2 -- options/locale/locale_zh-TW.ini | 2 -- 16 files changed, 30 deletions(-) diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 1c5c7ce89826d..2abe3672cd834 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1102,7 +1102,6 @@ delete_preexisting_success=Smazány nepřijaté soubory v %s blame_prior=Zobrazit blame před touto změnou blame.ignore_revs=Ignorování revizí v .git-blame-ignorerevs. Klikněte zde pro obejití a zobrazení normálního pohledu blame. blame.ignore_revs.failed=Nepodařilo se ignorovat revize v .git-blame-ignore-revs. -author_search_tooltip=Zobrazí maximálně 30 uživatelů tree_path_not_found_commit=Cesta %[1]s v commitu %[2]s neexistuje tree_path_not_found_branch=Cesta %[1]s ve větvi %[2]s neexistuje @@ -1521,7 +1520,6 @@ issues.filter_assignee=Zpracovatel issues.filter_assginee_no_select=Všichni zpracovatelé issues.filter_assginee_no_assignee=Bez zpracovatele issues.filter_poster=Autor -issues.filter_poster_no_select=Všichni autoři issues.filter_type=Typ issues.filter_type.all_issues=Všechny úkoly issues.filter_type.assigned_to_you=Přiřazené vám diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 1fe9076be496e..c8dd3f71a23ba 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1058,7 +1058,6 @@ delete_preexisting_success=Nicht übernommene Dateien in %s gelöscht blame_prior=Blame vor dieser Änderung anzeigen blame.ignore_revs=Revisionen in .git-blame-ignore-revs werden ignoriert. Klicke hier, um das zu umgehen und die normale Blame-Ansicht zu sehen. blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in .git-blame-ignore-revs. -author_search_tooltip=Zeigt maximal 30 Benutzer tree_path_not_found_commit=Pfad %[1]s existiert nicht in Commit%[2]s tree_path_not_found_branch=Pfad %[1]s existiert nicht in Branch %[2]s @@ -1460,7 +1459,6 @@ issues.filter_assignee=Zuständig issues.filter_assginee_no_select=Alle Zuständigen issues.filter_assginee_no_assignee=Niemand zuständig issues.filter_poster=Autor -issues.filter_poster_no_select=Alle Autoren issues.filter_type=Typ issues.filter_type.all_issues=Alle Issues issues.filter_type.assigned_to_you=Dir zugewiesen diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index f58819fc9510c..193441828ae1e 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -991,7 +991,6 @@ delete_preexisting_success=Διαγράφηκαν τα μη υιοθετημέν blame_prior=Προβολή ευθύνης πριν από αυτή την αλλαγή blame.ignore_revs=Αγνόηση των αναθεωρήσεων στο .git-blame-ignore-revs. Πατήστε εδώ για να το παρακάμψετε και να δείτε την κανονική προβολή ευθυνών. blame.ignore_revs.failed=Αποτυχία αγνόησης των αναθεωρήσεων στο .git-blame-ignore-revs. -author_search_tooltip=Εμφάνιση το πολύ 30 χρηστών tree_path_not_found_commit=Η διαδρομή %[1]s δεν υπάρχει στην υποβολή %[2]s tree_path_not_found_branch=Η διαδρομή %[1]s δεν υπάρχει στον κλάδο %[2]s @@ -1383,7 +1382,6 @@ issues.filter_assignee=Αποδέκτης issues.filter_assginee_no_select=Όλοι οι αποδέκτες issues.filter_assginee_no_assignee=Κανένας Αποδέκτης issues.filter_poster=Συγγραφέας -issues.filter_poster_no_select=Όλοι οι συγγραφείς issues.filter_type=Τύπος issues.filter_type.all_issues=Όλα τα ζητήματα issues.filter_type.assigned_to_you=Ανατέθηκαν σε εσάς diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 996774dadffb4..e95513766bb3b 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -981,7 +981,6 @@ delete_preexisting_success=Eliminó archivos no adoptados en %s blame_prior=Ver la culpa antes de este cambio blame.ignore_revs=Ignorando revisiones en .git-blame-ignore-revs. Haga clic aquí para saltar y para a la vista normal. blame.ignore_revs.failed=No se pudieron ignorar las revisiones en .git-blame-ignore-revs. -author_search_tooltip=Muestra un máximo de 30 usuarios tree_path_not_found_commit=La ruta %[1]s no existe en el commit %[2]s tree_path_not_found_branch=La ruta %[1]s no existe en la rama %[2]s @@ -1373,7 +1372,6 @@ issues.filter_assignee=Asignada a issues.filter_assginee_no_select=Todos los asignados issues.filter_assginee_no_assignee=Sin asignado issues.filter_poster=Autor -issues.filter_poster_no_select=Todos los autores issues.filter_type=Tipo issues.filter_type.all_issues=Todas las incidencias issues.filter_type.assigned_to_you=Asignadas a ti diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index c943c924c8f34..4d4940cff5af1 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1109,7 +1109,6 @@ delete_preexisting_success=Fichiers dépossédés supprimés dans %s. blame_prior=Voir le blame avant cette modification blame.ignore_revs=Les révisions dans .git-blame-ignore-revs sont ignorées. Vous pouvez quand même voir ces blâmes. blame.ignore_revs.failed=Impossible d'ignorer les révisions dans .git-blame-ignore-revs. -author_search_tooltip=Affiche un maximum de 30 utilisateurs tree_path_not_found_commit=Le chemin %[1]s n’existe pas dans la révision %[2]s. tree_path_not_found_branch=Le chemin %[1]s n’existe pas dans la branche %[2]s. @@ -1528,7 +1527,6 @@ issues.filter_assignee=Assigné issues.filter_assginee_no_select=Tous les assignés issues.filter_assginee_no_assignee=Aucun assigné issues.filter_poster=Auteur -issues.filter_poster_no_select=Tous les auteurs issues.filter_type=Type issues.filter_type.all_issues=Tous les tickets issues.filter_type.assigned_to_you=Qui vous sont assignés diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index b46a8f75f30c9..f40e0037d2c55 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -1109,7 +1109,6 @@ delete_preexisting_success=Scriosta comhaid neamhghlactha i %s blame_prior=Féach ar an milleán roimh an athrú seo blame.ignore_revs=Ag déanamh neamhairde de leasuithe i .git-blame-ignore-revs. Cliceáil anseo chun seachaint agus an gnáth-amharc milleán a fheiceáil. blame.ignore_revs.failed=Theip ar neamhaird a dhéanamh ar leasuithe i .git-blame-ignore-revs. -author_search_tooltip=Taispeánann 30 úsáideoir ar a mhéad tree_path_not_found_commit=Níl cosán %[1]s ann i dtiomantas %[2]s tree_path_not_found_branch=Níl cosán %[1]s ann i mbrainse %[2]s @@ -1528,7 +1527,6 @@ issues.filter_assignee=Sannaitheoir issues.filter_assginee_no_select=Gach sannaithe issues.filter_assginee_no_assignee=Gan sannaitheoir issues.filter_poster=Údar -issues.filter_poster_no_select=Gach údair issues.filter_type=Cineál issues.filter_type.all_issues=Gach saincheist issues.filter_type.assigned_to_you=Sannta duit diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index de89126952173..567e6acdcefc5 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -1144,7 +1144,6 @@ issues.filter_assignee=Assegnatario issues.filter_assginee_no_select=Tutte le assegnazioni issues.filter_assginee_no_assignee=Nessun assegnatario issues.filter_poster=Autore -issues.filter_poster_no_select=Tutti gli autori issues.filter_type=Tipo issues.filter_type.all_issues=Tutti i problemi issues.filter_type.assigned_to_you=Assegnati a te diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 3785f8d02eca6..1d8b33bef68aa 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -1104,7 +1104,6 @@ delete_preexisting_success=%s の未登録ファイルを削除しました blame_prior=この変更より前のBlameを表示 blame.ignore_revs=.git-blame-ignore-revs で指定されたリビジョンは除外しています。 これを迂回して通常のBlame表示を見るには ここをクリック。 blame.ignore_revs.failed=.git-blame-ignore-revs によるリビジョンの無視は失敗しました。 -author_search_tooltip=最大30人までのユーザーを表示 tree_path_not_found_commit=パス %[1]s はコミット %[2]s に存在しません tree_path_not_found_branch=パス %[1]s はブランチ %[2]s に存在しません @@ -1523,7 +1522,6 @@ issues.filter_assignee=担当者 issues.filter_assginee_no_select=すべての担当者 issues.filter_assginee_no_assignee=担当者なし issues.filter_poster=作成者 -issues.filter_poster_no_select=すべての作成者 issues.filter_type=タイプ issues.filter_type.all_issues=すべてのイシュー issues.filter_type.assigned_to_you=自分が担当 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index c0c8ef74b1dfb..fd412b95b416b 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -996,7 +996,6 @@ delete_preexisting_success=Dzēst nepārņemtos failus direktorijā %s blame_prior=Aplūkot vainīgo par izmaiņām pirms šīs revīzijas blame.ignore_revs=Neņem vērā izmaiņas no .git-blame-ignore-revs. Nospiediet šeit, lai to apietu un redzētu visu izmaiņu skatu. blame.ignore_revs.failed=Neizdevās neņemt vērā izmaiņas no .git-blam-ignore-revs. -author_search_tooltip=Tiks attēloti ne vairāk kā 30 lietotāji tree_path_not_found_commit=Revīzijā %[2]s neeksistē ceļš %[1]s tree_path_not_found_branch=Atzarā %[2]s nepastāv ceļš %[1]s @@ -1389,7 +1388,6 @@ issues.filter_assignee=Atbildīgais issues.filter_assginee_no_select=Visi atbildīgie issues.filter_assginee_no_assignee=Nav atbildīgā issues.filter_poster=Autors -issues.filter_poster_no_select=Visi autori issues.filter_type=Veids issues.filter_type.all_issues=Visas problēmas issues.filter_type.assigned_to_you=Piešķirtās Jums diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index dfb35661712a3..a4da8177bce30 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -1142,7 +1142,6 @@ issues.filter_assignee=Aangewezene issues.filter_assginee_no_select=Alle toegewezen personen issues.filter_assginee_no_assignee=Geen verantwoordelijke issues.filter_poster=Auteur -issues.filter_poster_no_select=Alle auteurs issues.filter_type=Type issues.filter_type.all_issues=Alle kwesties issues.filter_type.assigned_to_you=Aan jou toegewezen diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index ba37c87bcf2eb..8fb869898b56c 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -990,7 +990,6 @@ delete_preexisting=Excluir arquivos pré-existentes delete_preexisting_content=Excluir arquivos em %s delete_preexisting_success=Arquivos órfãos excluídos em %s blame_prior=Ver a responsabilização anterior a esta modificação -author_search_tooltip=Mostra um máximo de 30 usuários transfer.accept=Aceitar transferência @@ -1381,7 +1380,6 @@ issues.filter_assignee=Atribuído issues.filter_assginee_no_select=Todos os responsáveis issues.filter_assginee_no_assignee=Sem responsável issues.filter_poster=Autor -issues.filter_poster_no_select=Todos os autores issues.filter_type=Tipo issues.filter_type.all_issues=Todas as issues issues.filter_type.assigned_to_you=Atribuídos a você diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index ed927948bbb8b..f0abce0d4bfa8 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1109,7 +1109,6 @@ delete_preexisting_success=Eliminados os ficheiros não adoptados em %s blame_prior=Ver a responsabilização anterior a esta modificação blame.ignore_revs=Ignorando as revisões em .git-blame-ignore-revs. Clique aqui para contornar e ver a vista normal de responsabilização. blame.ignore_revs.failed=Falhou ao ignorar as revisões em .git-blame-ignore-revs. -author_search_tooltip=Mostra um máximo de 30 utilizadores tree_path_not_found_commit=A localização %[1]s não existe no cometimento %[2]s tree_path_not_found_branch=A localização %[1]s não existe no ramo %[2]s @@ -1528,7 +1527,6 @@ issues.filter_assignee=Encarregado issues.filter_assginee_no_select=Todos os encarregados issues.filter_assginee_no_assignee=Sem encarregado issues.filter_poster=Autor(a) -issues.filter_poster_no_select=Todos os autores issues.filter_type=Tipo issues.filter_type.all_issues=Todas as questões issues.filter_type.assigned_to_you=Atribuídas a si diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index eef48bd682697..735077bd41407 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -977,7 +977,6 @@ delete_preexisting=Удалить уже существующие файлы delete_preexisting_content=Удалить файлы из %s delete_preexisting_success=Удалены непринятые файлы в %s blame_prior=Показать авторство предшествующих изменений -author_search_tooltip=Показывает максимум 30 пользователей tree_path_not_found_commit=Путь %[1]s не существует в коммите %[2]s tree_path_not_found_branch=Путь %[1]s не существует в ветке %[2]s @@ -1360,7 +1359,6 @@ issues.filter_assignee=Назначено issues.filter_assginee_no_select=Все назначения issues.filter_assginee_no_assignee=Нет ответственного issues.filter_poster=Автор -issues.filter_poster_no_select=Все авторы issues.filter_type=Тип issues.filter_type.all_issues=Все задачи issues.filter_type.assigned_to_you=Назначено вам diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index f2fe75d39d14a..9b7f2cb5c6d13 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1073,7 +1073,6 @@ delete_preexisting_success=%s içindeki kabul edilmeyen dosyalar silindi blame_prior=Bu değişiklikten önceki suçu görüntüle blame.ignore_revs=.git-blame-ignore-revs dosyasındaki sürümler yok sayılıyor. Bunun yerine normal sorumlu görüntüsü için buraya tıklayın. blame.ignore_revs.failed=.git-blame-ignore-revs dosyasındaki sürümler yok sayılamadı. -author_search_tooltip=En fazla 30 kullanıcı görüntüler tree_path_not_found_commit=%[1] yolu, %[2]s işlemesinde mevcut değil tree_path_not_found_branch=%[1] yolu, %[2]s dalında mevcut değil @@ -1481,7 +1480,6 @@ issues.filter_assignee=Atanan issues.filter_assginee_no_select=Tüm atananlar issues.filter_assginee_no_assignee=Atanan yok issues.filter_poster=Yazar -issues.filter_poster_no_select=Tüm yazarlar issues.filter_type=Tür issues.filter_type.all_issues=Tüm konular issues.filter_type.assigned_to_you=Size atanan diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index e723927561ff1..3add7f8be38cf 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1074,7 +1074,6 @@ delete_preexisting_success=删除 %s 中未收录的文件 blame_prior=查看此更改前的 blame blame.ignore_revs=忽略 .git-blame-ignore-revs 的修订。点击 绕过 并查看正常的 Blame 视图。 blame.ignore_revs.failed=忽略 .git-blame-ignore-revs 版本失败。 -author_search_tooltip=最多显示30个用户 tree_path_not_found_commit=路径%[1]s 在提交 %[2]s 中不存在 tree_path_not_found_branch=路径 %[1]s 不存在于分支 %[2]s 中。 @@ -1489,7 +1488,6 @@ issues.filter_assignee=指派人筛选 issues.filter_assginee_no_select=所有指派成员 issues.filter_assginee_no_assignee=未指派 issues.filter_poster=作者 -issues.filter_poster_no_select=所有作者 issues.filter_type=类型筛选 issues.filter_type.all_issues=所有工单 issues.filter_type.assigned_to_you=指派给您的 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index f9e9428ff7882..3b1d37a322afc 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -904,7 +904,6 @@ delete_preexisting=刪除既有的檔案 delete_preexisting_content=刪除 %s 中的檔案 delete_preexisting_success=刪除 %s 中未接管的檔案 blame_prior=檢視此變更前的 Blame -author_search_tooltip=最多顯示 30 位使用者 transfer.accept=同意轉移 @@ -1266,7 +1265,6 @@ issues.filter_assignee=負責人 issues.filter_assginee_no_select=所有負責人 issues.filter_assginee_no_assignee=沒有負責人 issues.filter_poster=作者 -issues.filter_poster_no_select=所有作者 issues.filter_type=類型 issues.filter_type.all_issues=所有問題 issues.filter_type.assigned_to_you=指派給您的 From e619384098419569e570796a57ee6af4948067ae Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 11 Dec 2024 14:33:24 +0800 Subject: [PATCH 6/8] Add label/author/assignee filters to the user/org home issue list (#32779) Replace #26661, fix #25979 Not perfect, but usable and much better than before. Since it is quite complex, I am not quite sure whether there would be any regression, if any, I will fix in first time. I have tested the related pages many times: issue list, milestone issue list, project view, user issue list, org issue list. --- models/db/search.go | 12 +- models/issues/issue_search.go | 39 +++--- models/issues/issue_stats.go | 10 +- models/issues/issue_test.go | 3 +- modules/indexer/issues/db/options.go | 4 +- modules/indexer/issues/dboptions.go | 12 +- modules/indexer/issues/indexer_test.go | 2 +- routers/web/org/projects.go | 21 +--- routers/web/repo/issue_list.go | 118 +++--------------- routers/web/repo/projects.go | 19 +-- routers/web/shared/issue/issue_label.go | 71 +++++++++++ routers/web/shared/user/helper.go | 17 ++- routers/web/user/home.go | 62 ++++----- routers/web/web.go | 2 +- services/context/pagination.go | 13 ++ templates/projects/list.tmpl | 23 ++-- templates/repo/issue/filter_item_label.tmpl | 2 +- .../repo/issue/filter_item_user_fetch.tmpl | 4 +- templates/repo/issue/filter_list.tmpl | 10 +- .../repo/issue/milestone/filter_list.tmpl | 2 +- templates/repo/issue/search.tmpl | 2 +- templates/user/dashboard/issues.tmpl | 69 ++++++---- templates/user/dashboard/milestones.tmpl | 30 ++--- web_src/css/repo.css | 18 --- web_src/css/repo/issue-list.css | 18 +++ web_src/css/repo/list-header.css | 26 ++-- web_src/js/features/repo-issue-list.ts | 48 +++---- 27 files changed, 338 insertions(+), 319 deletions(-) create mode 100644 routers/web/shared/issue/issue_label.go diff --git a/models/db/search.go b/models/db/search.go index 37565f45e1f9e..e0a1b6bde9ffd 100644 --- a/models/db/search.go +++ b/models/db/search.go @@ -26,8 +26,10 @@ const ( SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" ) -const ( - // Which means a condition to filter the records which don't match any id. - // It's different from zero which means the condition could be ignored. - NoConditionID = -1 -) +// NoConditionID means a condition to filter the records which don't match any id. +// eg: "milestone_id=-1" means "find the items without any milestone. +const NoConditionID int64 = -1 + +// NonExistingID means a condition to match no result (eg: a non-existing user) +// It doesn't use -1 or -2 because they are used as builtin users. +const NonExistingID int64 = -1000000 diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 5948a67d4ebc6..f1cd125d495c4 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -27,8 +27,8 @@ type IssuesOptions struct { //nolint RepoIDs []int64 // overwrites RepoCond if the length is not 0 AllPublic bool // include also all public repositories RepoCond builder.Cond - AssigneeID int64 - PosterID int64 + AssigneeID optional.Option[int64] + PosterID optional.Option[int64] MentionedID int64 ReviewRequestedID int64 ReviewedID int64 @@ -231,15 +231,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) { sess.And("issue.is_closed=?", opts.IsClosed.Value()) } - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } else if opts.AssigneeID == db.NoConditionID { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") - } - - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } + applyAssigneeCondition(sess, opts.AssigneeID) + applyPosterCondition(sess, opts.PosterID) if opts.MentionedID > 0 { applyMentionedCondition(sess, opts.MentionedID) @@ -359,13 +352,27 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati return cond } -func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) { - sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). - And("issue_assignees.assignee_id = ?", assigneeID) +func applyAssigneeCondition(sess *xorm.Session, assigneeID optional.Option[int64]) { + // old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64 + if !assigneeID.Has() || assigneeID.Value() == 0 { + return + } + if assigneeID.Value() == db.NoConditionID { + sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") + } else { + sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", assigneeID.Value()) + } } -func applyPosterCondition(sess *xorm.Session, posterID int64) { - sess.And("issue.poster_id=?", posterID) +func applyPosterCondition(sess *xorm.Session, posterID optional.Option[int64]) { + if !posterID.Has() { + return + } + // poster doesn't need to support db.NoConditionID(-1), so just use the value as-is + if posterID.Has() { + sess.And("issue.poster_id=?", posterID.Value()) + } } func applyMentionedCondition(sess *xorm.Session, mentionedID int64) { diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index 39326616f8892..9ef9347a16c5a 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -151,15 +151,9 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6 applyProjectCondition(sess, opts) - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } else if opts.AssigneeID == db.NoConditionID { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") - } + applyAssigneeCondition(sess, opts.AssigneeID) - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } + applyPosterCondition(sess, opts.PosterID) if opts.MentionedID > 0 { applyMentionedCondition(sess, opts.MentionedID) diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 1bbc0eee564fd..548f137f394ce 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -16,6 +16,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/optional" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -155,7 +156,7 @@ func TestIssues(t *testing.T) { }{ { issues_model.IssuesOptions{ - AssigneeID: 1, + AssigneeID: optional.Some(int64(1)), SortType: "oldest", }, []int64{1, 6}, diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 875a4ca279dd6..98b097f8713d3 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -54,8 +54,8 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m RepoIDs: options.RepoIDs, AllPublic: options.AllPublic, RepoCond: nil, - AssigneeID: convertID(options.AssigneeID), - PosterID: convertID(options.PosterID), + AssigneeID: optional.Some(convertID(options.AssigneeID)), + PosterID: options.PosterID, MentionedID: convertID(options.MentionID), ReviewRequestedID: convertID(options.ReviewRequestedID), ReviewedID: convertID(options.ReviewedID), diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index c1f454eeee563..1a0f241e61590 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -40,14 +40,14 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp if opts.ProjectID > 0 { searchOpt.ProjectID = optional.Some(opts.ProjectID) - } else if opts.ProjectID == -1 { // FIXME: this is inconsistent from other places + } else if opts.ProjectID == db.NoConditionID { // FIXME: this is inconsistent from other places searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0) } - if opts.AssigneeID > 0 { - searchOpt.AssigneeID = optional.Some(opts.AssigneeID) - } else if opts.AssigneeID == -1 { // FIXME: this is inconsistent from other places - searchOpt.AssigneeID = optional.Some[int64](0) + if opts.AssigneeID.Value() == db.NoConditionID { + searchOpt.AssigneeID = optional.Some[int64](0) // FIXME: this is inconsistent from other places, 0 means "no assignee" + } else if opts.AssigneeID.Value() != 0 { + searchOpt.AssigneeID = opts.AssigneeID } // See the comment of issues_model.SearchOptions for the reason why we need to convert @@ -62,7 +62,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp } searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID) - searchOpt.PosterID = convertID(opts.PosterID) + searchOpt.PosterID = opts.PosterID searchOpt.MentionID = convertID(opts.MentionedID) searchOpt.ReviewedID = convertID(opts.ReviewedID) searchOpt.ReviewRequestedID = convertID(opts.ReviewRequestedID) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 0dce6541816dd..7c3ba75bb072f 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -191,7 +191,7 @@ func searchIssueByID(t *testing.T) { }, { // NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1. - opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: -1}), + opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}), expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2}, }, { diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index b94344f2ecf3c..3b9ec2a7b85f0 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/web/shared/issue" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -334,23 +335,15 @@ func ViewProject(ctx *context.Context) { return } - var labelIDs []int64 - // 1,-2 means including label 1 and excluding label 2 - // 0 means issues with no label - // blank means labels will not be filtered for issues - selectLabels := ctx.FormString("labels") - if selectLabels != "" { - labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) - if err != nil { - ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) - } + labelIDs := issue.PrepareFilterIssueLabels(ctx, project.RepoID, project.Owner) + if ctx.Written() { + return } - - assigneeID := ctx.FormInt64("assignee") + assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{ LabelIDs: labelIDs, - AssigneeID: assigneeID, + AssigneeID: optional.Some(assigneeID), }) if err != nil { ctx.ServerError("LoadIssuesOfColumns", err) @@ -426,8 +419,6 @@ func ViewProject(ctx *context.Context) { return } ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) - - ctx.Data["SelectLabels"] = selectLabels ctx.Data["AssigneeID"] = assigneeID project.RenderedContent = templates.NewRenderUtils(ctx).MarkdownToHtml(project.Description) diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index 6451f7ac7651e..ff98bf8ec8ecf 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -17,12 +17,12 @@ 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/base" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/web/shared/issue" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -263,8 +263,10 @@ func getUserIDForFilter(ctx *context.Context, queryName string) int64 { return user.ID } -// ListIssues list the issues of a repository -func ListIssues(ctx *context.Context) { +// SearchRepoIssuesJSON lists the issues of a repository +// This function was copied from API (decouple the web and API routes), +// it is only used by frontend to search some dependency or related issues +func SearchRepoIssuesJSON(ctx *context.Context) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { ctx.Error(http.StatusUnprocessableEntity, err.Error()) @@ -286,20 +288,11 @@ func ListIssues(ctx *context.Context) { keyword = "" } - var labelIDs []int64 - if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 { - labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - } - var mileIDs []int64 if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 { for i := range part { // uses names and fall back to ids - // non existent milestones are discarded + // non-existent milestones are discarded mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i]) if err == nil { mileIDs = append(mileIDs, mile.ID) @@ -370,17 +363,8 @@ func ListIssues(ctx *context.Context) { if before != 0 { searchOpt.UpdatedBeforeUnix = optional.Some(before) } - if len(labelIDs) == 1 && labelIDs[0] == 0 { - searchOpt.NoLabelOnly = true - } else { - for _, labelID := range labelIDs { - if labelID > 0 { - searchOpt.IncludedLabelIDs = append(searchOpt.IncludedLabelIDs, labelID) - } else { - searchOpt.ExcludedLabelIDs = append(searchOpt.ExcludedLabelIDs, -labelID) - } - } - } + + // TODO: the "labels" query parameter is never used, so no need to handle it if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID { searchOpt.MilestoneIDs = []int64{0} @@ -503,8 +487,8 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt if !util.SliceContainsString(types, viewType, true) { viewType = "all" } - // TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly - assigneeID := ctx.FormInt64("assignee") + + assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future posterUsername := ctx.FormString("poster") posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername) var mentionedID, reviewRequestedID, reviewedID int64 @@ -512,7 +496,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt if ctx.IsSigned { switch viewType { case "created_by": - posterUserID = ctx.Doer.ID + posterUserID = optional.Some(ctx.Doer.ID) case "mentioned": mentionedID = ctx.Doer.ID case "assigned": @@ -525,18 +509,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt } repo := ctx.Repo.Repository - var labelIDs []int64 - // 1,-2 means including label 1 and excluding label 2 - // 0 means issues with no label - // blank means labels will not be filtered for issues - selectLabels := ctx.FormString("labels") - if selectLabels != "" { - labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) - if err != nil { - ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) - } - } - keyword := strings.Trim(ctx.FormString("q"), " ") if bytes.Contains([]byte(keyword), []byte{0x00}) { keyword = "" @@ -547,13 +519,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt mileIDs = []int64{milestoneID} } + labelIDs := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner) + if ctx.Written() { + return + } + var issueStats *issues_model.IssueStats statsOpts := &issues_model.IssuesOptions{ RepoIDs: []int64{repo.ID}, LabelIDs: labelIDs, MilestoneIDs: mileIDs, ProjectID: projectID, - AssigneeID: assigneeID, + AssigneeID: optional.Some(assigneeID), MentionedID: mentionedID, PosterID: posterUserID, ReviewRequestedID: reviewRequestedID, @@ -634,7 +611,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt PageSize: setting.UI.IssuePagingNum, }, RepoIDs: []int64{repo.ID}, - AssigneeID: assigneeID, + AssigneeID: optional.Some(assigneeID), PosterID: posterUserID, MentionedID: mentionedID, ReviewRequestedID: reviewRequestedID, @@ -709,49 +686,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt return } - labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) - if err != nil { - ctx.ServerError("GetLabelsByRepoID", err) - return - } - - if repo.Owner.IsOrganization() { - orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) - if err != nil { - ctx.ServerError("GetLabelsByOrgID", err) - return - } - - ctx.Data["OrgLabels"] = orgLabels - labels = append(labels, orgLabels...) - } - - // Get the exclusive scope for every label ID - labelExclusiveScopes := make([]string, 0, len(labelIDs)) - for _, labelID := range labelIDs { - foundExclusiveScope := false - for _, label := range labels { - if label.ID == labelID || label.ID == -labelID { - labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope()) - foundExclusiveScope = true - break - } - } - if !foundExclusiveScope { - labelExclusiveScopes = append(labelExclusiveScopes, "") - } - } - - for _, l := range labels { - l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes) - } - ctx.Data["Labels"] = labels - ctx.Data["NumLabels"] = len(labels) - - if ctx.FormInt64("assignee") == 0 { - assigneeID = 0 // Reset ID to prevent unexpected selection of assignee. - } - ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink) ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 { @@ -792,13 +726,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.Data["OpenCount"] = issueStats.OpenCount ctx.Data["ClosedCount"] = issueStats.ClosedCount ctx.Data["SelLabelIDs"] = labelIDs - ctx.Data["SelectLabels"] = selectLabels ctx.Data["ViewType"] = viewType ctx.Data["SortType"] = sortType ctx.Data["MilestoneID"] = milestoneID ctx.Data["ProjectID"] = projectID ctx.Data["AssigneeID"] = assigneeID - ctx.Data["PosterUserID"] = posterUserID ctx.Data["PosterUsername"] = posterUsername ctx.Data["Keyword"] = keyword ctx.Data["IsShowClosed"] = isShowClosed @@ -810,19 +742,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt default: ctx.Data["State"] = "open" } - - pager.AddParamString("q", keyword) - pager.AddParamString("type", viewType) - pager.AddParamString("sort", sortType) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) - pager.AddParamString("labels", fmt.Sprint(selectLabels)) - pager.AddParamString("milestone", fmt.Sprint(milestoneID)) - pager.AddParamString("project", fmt.Sprint(projectID)) - pager.AddParamString("assignee", fmt.Sprint(assigneeID)) - pager.AddParamString("poster", posterUsername) - if showArchivedLabels { - pager.AddParamString("archived_labels", "true") - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 168da2ca1f5e2..3be9578670602 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/web/shared/issue" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -307,23 +308,13 @@ func ViewProject(ctx *context.Context) { return } - var labelIDs []int64 - // 1,-2 means including label 1 and excluding label 2 - // 0 means issues with no label - // blank means labels will not be filtered for issues - selectLabels := ctx.FormString("labels") - if selectLabels != "" { - labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) - if err != nil { - ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) - } - } + labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner) - assigneeID := ctx.FormInt64("assignee") + assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{ LabelIDs: labelIDs, - AssigneeID: assigneeID, + AssigneeID: optional.Some(assigneeID), }) if err != nil { ctx.ServerError("LoadIssuesOfColumns", err) @@ -409,8 +400,6 @@ func ViewProject(ctx *context.Context) { return } ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) - - ctx.Data["SelectLabels"] = selectLabels ctx.Data["AssigneeID"] = assigneeID rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) diff --git a/routers/web/shared/issue/issue_label.go b/routers/web/shared/issue/issue_label.go new file mode 100644 index 0000000000000..eacea36b02436 --- /dev/null +++ b/routers/web/shared/issue/issue_label.go @@ -0,0 +1,71 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issue + +import ( + "strings" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/services/context" +) + +// PrepareFilterIssueLabels reads the "labels" query parameter, sets `ctx.Data["Labels"]` and `ctx.Data["SelectLabels"]` +func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_model.User) (labelIDs []int64) { + // 1,-2 means including label 1 and excluding label 2 + // 0 means issues with no label + // blank means labels will not be filtered for issues + selectLabels := ctx.FormString("labels") + if selectLabels != "" { + var err error + labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) + if err != nil { + ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) + } + } + + var allLabels []*issues_model.Label + if repoID != 0 { + repoLabels, err := issues_model.GetLabelsByRepoID(ctx, repoID, "", db.ListOptions{}) + if err != nil { + ctx.ServerError("GetLabelsByRepoID", err) + return nil + } + allLabels = append(allLabels, repoLabels...) + } + + if owner != nil && owner.IsOrganization() { + orgLabels, err := issues_model.GetLabelsByOrgID(ctx, owner.ID, "", db.ListOptions{}) + if err != nil { + ctx.ServerError("GetLabelsByOrgID", err) + return nil + } + allLabels = append(allLabels, orgLabels...) + } + + // Get the exclusive scope for every label ID + labelExclusiveScopes := make([]string, 0, len(labelIDs)) + for _, labelID := range labelIDs { + foundExclusiveScope := false + for _, label := range allLabels { + if label.ID == labelID || label.ID == -labelID { + labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope()) + foundExclusiveScope = true + break + } + } + if !foundExclusiveScope { + labelExclusiveScopes = append(labelExclusiveScopes, "") + } + } + + for _, l := range allLabels { + l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes) + } + ctx.Data["Labels"] = allLabels + ctx.Data["SelectLabels"] = selectLabels + return labelIDs +} diff --git a/routers/web/shared/user/helper.go b/routers/web/shared/user/helper.go index 7268767e0aba3..b82181a1df169 100644 --- a/routers/web/shared/user/helper.go +++ b/routers/web/shared/user/helper.go @@ -8,7 +8,9 @@ import ( "slices" "strconv" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" ) func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { @@ -31,17 +33,20 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { // Before, the "issue filter" passes user ID to query the list, but in many cases, it's impossible to pre-fetch the full user list. // So it's better to make it work like GitHub: users could input username directly. // Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed. -// Old usage: poster=123, new usage: poster=the-username (at the moment, non-existing username is treated as poster=0, not ideal but acceptable) -func GetFilterUserIDByName(ctx context.Context, name string) int64 { +// Return values: +// * nil: no filter +// * some(id): match the id, the id could be -1 to match the issues without assignee +// * some(NonExistingID): match no issue (due to the user doesn't exist) +func GetFilterUserIDByName(ctx context.Context, name string) optional.Option[int64] { if name == "" { - return 0 + return optional.None[int64]() } u, err := user.GetUserByName(ctx, name) if err != nil { if id, err := strconv.ParseInt(name, 10, 64); err == nil { - return id + return optional.Some(id) } - return 0 + return optional.Some(db.NonExistingID) } - return u.ID + return optional.Some(u.ID) } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 5a0d46869f525..befa33b0c0d66 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -33,6 +33,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" + "code.gitea.io/gitea/routers/web/shared/issue" "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" feed_service "code.gitea.io/gitea/services/feed" @@ -413,6 +414,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { viewType = "your_repositories" } + isPullList := unitType == unit.TypePullRequests + opts := &issues_model.IssuesOptions{ + IsPull: optional.Some(isPullList), + SortType: sortType, + IsArchived: optional.Some(false), + User: ctx.Doer, + } // -------------------------------------------------------------------------- // Build opts (IssuesOptions), which contains filter information. // Will eventually be used to retrieve issues relevant for the overview page. @@ -422,30 +430,24 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // -------------------------------------------------------------------------- // Get repository IDs where User/Org/Team has access. - var team *organization.Team - var org *organization.Organization - if ctx.Org != nil { - org = ctx.Org.Organization - team = ctx.Org.Team - } + if ctx.Org != nil && ctx.Org.Organization != nil { + opts.Org = ctx.Org.Organization + opts.Team = ctx.Org.Team - isPullList := unitType == unit.TypePullRequests - opts := &issues_model.IssuesOptions{ - IsPull: optional.Some(isPullList), - SortType: sortType, - IsArchived: optional.Some(false), - Org: org, - Team: team, - User: ctx.Doer, + issue.PrepareFilterIssueLabels(ctx, 0, ctx.Org.Organization.AsUser()) + if ctx.Written() { + return + } } // Get filter by author id & assignee id - // FIXME: this feature doesn't work at the moment, because frontend can't use a "user-remote-search" dropdown directly // the existing "/posters" handlers doesn't work for this case, it is unable to list the related users correctly. // In the future, we need something like github: "author:user1" to accept usernames directly. posterUsername := ctx.FormString("poster") + ctx.Data["FilterPosterUsername"] = posterUsername opts.PosterID = user.GetFilterUserIDByName(ctx, posterUsername) - // TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly - opts.AssigneeID, _ = strconv.ParseInt(ctx.FormString("assignee"), 10, 64) + assigneeUsername := ctx.FormString("assignee") + ctx.Data["FilterAssigneeUsername"] = assigneeUsername + opts.AssigneeID = user.GetFilterUserIDByName(ctx, assigneeUsername) isFuzzy := ctx.FormBool("fuzzy") @@ -471,8 +473,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { UnitType: unitType, Archived: optional.Some(false), } - if team != nil { - repoOpts.TeamID = team.ID + if opts.Team != nil { + repoOpts.TeamID = opts.Team.ID } accessibleRepos := container.Set[int64]{} { @@ -500,9 +502,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { case issues_model.FilterModeAll: case issues_model.FilterModeYourRepositories: case issues_model.FilterModeAssign: - opts.AssigneeID = ctx.Doer.ID + opts.AssigneeID = optional.Some(ctx.Doer.ID) case issues_model.FilterModeCreate: - opts.PosterID = ctx.Doer.ID + opts.PosterID = optional.Some(ctx.Doer.ID) case issues_model.FilterModeMention: opts.MentionedID = ctx.Doer.ID case issues_model.FilterModeReviewRequested: @@ -584,10 +586,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // because the doer may create issues or be mentioned in any public repo. // So we need search issues in all public repos. o.AllPublic = ctx.Doer.ID == ctxUser.ID - // TODO: to make it work with poster/assignee filter, then these IDs should be kept - o.AssigneeID = nil - o.PosterID = nil - o.MentionID = nil o.ReviewRequestedID = nil o.ReviewedID = nil @@ -645,10 +643,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { ctx.Data["ViewType"] = viewType ctx.Data["SortType"] = sortType ctx.Data["IsShowClosed"] = isShowClosed - ctx.Data["SelectLabels"] = selectedLabels ctx.Data["IsFuzzy"] = isFuzzy - ctx.Data["SearchFilterPosterID"] = util.Iif[any](opts.PosterID != 0, opts.PosterID, nil) - ctx.Data["SearchFilterAssigneeID"] = util.Iif[any](opts.AssigneeID != 0, opts.AssigneeID, nil) if isShowClosed { ctx.Data["State"] = "closed" @@ -657,16 +652,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { } pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5) - pager.AddParamString("q", keyword) - pager.AddParamString("type", viewType) - pager.AddParamString("sort", sortType) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) - pager.AddParamString("labels", selectedLabels) - pager.AddParamString("fuzzy", fmt.Sprint(isFuzzy)) - pager.AddParamString("poster", posterUsername) - if opts.AssigneeID != 0 { - pager.AddParamString("assignee", fmt.Sprint(opts.AssigneeID)) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplIssues) diff --git a/routers/web/web.go b/routers/web/web.go index c87c01ea0f003..72ee47bb4c962 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1208,7 +1208,7 @@ func registerRoutes(m *web.Router) { Post(web.Bind(forms.CreateIssueForm{}), repo.NewIssuePost) m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate) }) - m.Get("/search", repo.ListIssues) + m.Get("/search", repo.SearchRepoIssuesJSON) }, context.RepoMustNotBeArchived(), reqRepoIssueReader) // FIXME: should use different URLs but mostly same logic for comments of issue and pull request. diff --git a/services/context/pagination.go b/services/context/pagination.go index fb2ef699ce40e..42117cf96de88 100644 --- a/services/context/pagination.go +++ b/services/context/pagination.go @@ -6,6 +6,7 @@ package context import ( "fmt" "html/template" + "net/http" "net/url" "strings" @@ -32,6 +33,18 @@ func (p *Pagination) AddParamString(key, value string) { p.urlParams = append(p.urlParams, urlParam) } +func (p *Pagination) AddParamFromRequest(req *http.Request) { + for key, values := range req.URL.Query() { + if key == "page" || len(values) == 0 { + continue + } + for _, value := range values { + urlParam := fmt.Sprintf("%s=%v", key, url.QueryEscape(value)) + p.urlParams = append(p.urlParams, urlParam) + } + } +} + // GetParams returns the configured URL params func (p *Pagination) GetParams() template.URL { return template.URL(strings.Join(p.urlParams, "&")) diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index b2f48fe2c9c47..f5a48f72414c1 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -24,16 +24,19 @@ {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.project_kind")}} - -