diff --git a/.air.toml b/.air.toml index d13f8c4f99577..de97bd8b298be 100644 --- a/.air.toml +++ b/.air.toml @@ -8,6 +8,15 @@ delay = 1000 include_ext = ["go", "tmpl"] include_file = ["main.go"] include_dir = ["cmd", "models", "modules", "options", "routers", "services"] -exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"] +exclude_dir = [ + "models/fixtures", + "models/migrations/fixtures", + "modules/avatar/identicon/testdata", + "modules/avatar/testdata", + "modules/git/tests", + "modules/migration/file_format_testdata", + "routers/private/tests", + "services/gitdiff/testdata", +] exclude_regex = ["_test.go$", "_gen.go$"] stop_on_error = true diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a4a6ce8fcf608..d391cf78cf7da 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,7 @@ "features": { // installs nodejs into container "ghcr.io/devcontainers/features/node:1": { - "version":"20" + "version": "20" }, "ghcr.io/devcontainers/features/git-lfs:1.1.0": {}, "ghcr.io/devcontainers-contrib/features/poetry:2": {}, @@ -24,7 +24,7 @@ "DavidAnson.vscode-markdownlint", "Vue.volar", "ms-azuretools.vscode-docker", - "zixuanchen.vitest-explorer", + "vitest.explorer", "qwtel.sqlite-viewer", "GitHub.vscode-pull-request-github" ] diff --git a/.eslintrc.yaml b/.eslintrc.yaml index b65fe56cf284a..72039a601380a 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -286,7 +286,7 @@ rules: jquery/no-class: [0] jquery/no-clone: [2] jquery/no-closest: [0] - jquery/no-css: [0] + jquery/no-css: [2] jquery/no-data: [0] jquery/no-deferred: [2] jquery/no-delegate: [2] @@ -409,7 +409,7 @@ rules: no-jquery/no-constructor-attributes: [2] no-jquery/no-contains: [2] no-jquery/no-context-prop: [2] - no-jquery/no-css: [0] + no-jquery/no-css: [2] no-jquery/no-data: [0] no-jquery/no-deferred: [2] no-jquery/no-delegate: [2] diff --git a/.github/workflows/cron-lock.yml b/.github/workflows/cron-lock.yml deleted file mode 100644 index 665313135b679..0000000000000 --- a/.github/workflows/cron-lock.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: cron-lock - -on: - schedule: - - cron: "0 0 * * *" # every day at 00:00 UTC - workflow_dispatch: - -permissions: - issues: write - pull-requests: write - -concurrency: - group: lock - -jobs: - action: - runs-on: ubuntu-latest - if: github.repository == 'go-gitea/gitea' - steps: - - uses: dessant/lock-threads@v5 - with: - issue-inactive-days: 10 - pr-inactive-days: 7 diff --git a/.gitpod.yml b/.gitpod.yml index ed2f57f4bf6cd..f573d55a767cc 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -42,7 +42,7 @@ vscode: - DavidAnson.vscode-markdownlint - Vue.volar - ms-azuretools.vscode-docker - - zixuanchen.vitest-explorer + - vitest.explorer - qwtel.sqlite-viewer - GitHub.vscode-pull-request-github diff --git a/README.md b/README.md index 90474cbd94abc..f579449174472 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly") [![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") -[![](https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg)](https://app.codecov.io/gh/go-gitea/gitea "Codecov") [![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card") [![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc") [![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release") diff --git a/README_ZH.md b/README_ZH.md index deebb40cfb0bf..726c4273a6b34 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -2,7 +2,6 @@ [![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly") [![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") -[![](https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg)](https://app.codecov.io/gh/go-gitea/gitea "Codecov") [![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card") [![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc") [![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release") diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 04923acdcb9ab..2309021f94937 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -590,7 +590,7 @@ And the following unique queues: ## OpenID (`openid`) -- `ENABLE_OPENID_SIGNIN`: **false**: Allow authentication in via OpenID. +- `ENABLE_OPENID_SIGNIN`: **true**: Allow authentication in via OpenID. - `ENABLE_OPENID_SIGNUP`: **! DISABLE\_REGISTRATION**: Allow registering via OpenID. - `WHITELISTED_URIS`: **_empty_**: If non-empty, list of POSIX regex patterns matching OpenID URI's to permit. diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md index 1f98db78aafd8..3115e4cc06453 100644 --- a/docs/content/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/administration/config-cheat-sheet.zh-cn.md @@ -562,7 +562,7 @@ Gitea 创建以下非唯一队列: ## OpenID (`openid`) -- `ENABLE_OPENID_SIGNIN`: **false**:允许通过OpenID进行身份验证。 +- `ENABLE_OPENID_SIGNIN`: **true**:允许通过OpenID进行身份验证。 - `ENABLE_OPENID_SIGNUP`: **! DISABLE\_REGISTRATION**:允许通过OpenID进行注册。 - `WHITELISTED_URIS`: **_empty_**:如果非空,是一组匹配OpenID URI的POSIX正则表达式模式,用于允许访问。 - `BLACKLISTED_URIS`: **_empty_**:如果非空,是一组匹配OpenID URI的POSIX正则表达式模式,用于阻止访问。 diff --git a/models/actions/run.go b/models/actions/run.go index 7b3125949b82d..fa9db0b554754 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -170,15 +170,16 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err return err } -// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow. -func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { - // Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'. +// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event. +// It's useful when a new run is triggered, and all previous runs needn't be continued anymore. +func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { + // Find all runs in the specified repository, reference, and workflow with non-final status runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ RepoID: repoID, Ref: ref, WorkflowID: workflowID, TriggerEvent: event, - Status: []Status{StatusRunning, StatusWaiting}, + Status: []Status{StatusRunning, StatusWaiting, StatusBlocked}, }) if err != nil { return err diff --git a/models/actions/schedule.go b/models/actions/schedule.go index d450e7aa07291..3646a046a0f35 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -127,14 +127,14 @@ func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) er return fmt.Errorf("DeleteCronTaskByRepo: %v", err) } // cancel running cron jobs of this repository and delete old schedules - if err := CancelRunningJobs( + if err := CancelPreviousJobs( ctx, repo.ID, repo.DefaultBranch, "", webhook_module.HookEventSchedule, ); err != nil { - return fmt.Errorf("CancelRunningJobs: %v", err) + return fmt.Errorf("CancelPreviousJobs: %v", err) } return nil } diff --git a/models/activities/notification.go b/models/activities/notification.go index 230bcdd6e8053..dc1b8c6fae91a 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -12,12 +12,8 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" - access_model "code.gitea.io/gitea/models/perm/access" 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/timeutil" @@ -79,53 +75,6 @@ func init() { db.RegisterModel(new(Notification)) } -// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored. -type FindNotificationOptions struct { - db.ListOptions - UserID int64 - RepoID int64 - IssueID int64 - Status []NotificationStatus - Source []NotificationSource - UpdatedAfterUnix int64 - UpdatedBeforeUnix int64 -} - -// ToCond will convert each condition into a xorm-Cond -func (opts FindNotificationOptions) ToConds() builder.Cond { - cond := builder.NewCond() - if opts.UserID != 0 { - cond = cond.And(builder.Eq{"notification.user_id": opts.UserID}) - } - if opts.RepoID != 0 { - cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID}) - } - if opts.IssueID != 0 { - cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID}) - } - if len(opts.Status) > 0 { - if len(opts.Status) == 1 { - cond = cond.And(builder.Eq{"notification.status": opts.Status[0]}) - } else { - cond = cond.And(builder.In("notification.status", opts.Status)) - } - } - if len(opts.Source) > 0 { - cond = cond.And(builder.In("notification.source", opts.Source)) - } - if opts.UpdatedAfterUnix != 0 { - cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix}) - } - if opts.UpdatedBeforeUnix != 0 { - cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix}) - } - return cond -} - -func (opts FindNotificationOptions) ToOrders() string { - return "notification.updated_unix DESC" -} - // CreateRepoTransferNotification creates notification for the user a repository was transferred to func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error { return db.WithTx(ctx, func(ctx context.Context) error { @@ -159,109 +108,6 @@ func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_mo }) } -// CreateOrUpdateIssueNotifications creates an issue notification -// for each watcher, or updates it if already exists -// receiverID > 0 just send to receiver, else send to all watcher -func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil { - return err - } - - return committer.Commit() -} - -func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { - // init - var toNotify container.Set[int64] - notifications, err := db.Find[Notification](ctx, FindNotificationOptions{ - IssueID: issueID, - }) - if err != nil { - return err - } - - issue, err := issues_model.GetIssueByID(ctx, issueID) - if err != nil { - return err - } - - if receiverID > 0 { - toNotify = make(container.Set[int64], 1) - toNotify.Add(receiverID) - } else { - toNotify = make(container.Set[int64], 32) - issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true) - if err != nil { - return err - } - toNotify.AddMultiple(issueWatches...) - if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) { - repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID) - if err != nil { - return err - } - toNotify.AddMultiple(repoWatches...) - } - issueParticipants, err := issue.GetParticipantIDsByIssue(ctx) - if err != nil { - return err - } - toNotify.AddMultiple(issueParticipants...) - - // dont notify user who cause notification - delete(toNotify, notificationAuthorID) - // explicit unwatch on issue - issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false) - if err != nil { - return err - } - for _, id := range issueUnWatches { - toNotify.Remove(id) - } - } - - err = issue.LoadRepo(ctx) - if err != nil { - return err - } - - // notify - for userID := range toNotify { - issue.Repo.Units = nil - user, err := user_model.GetUserByID(ctx, userID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - continue - } - - return err - } - if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) { - continue - } - if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) { - continue - } - - if notificationExists(notifications, issue.ID, userID) { - if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil { - return err - } - continue - } - if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil { - return err - } - } - return nil -} - func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error { notification := &Notification{ UserID: userID, @@ -449,309 +295,6 @@ func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.Tim return res, db.GetEngine(ctx).SQL(sql, since, until, NotificationStatusUnread).Find(&res) } -// NotificationList contains a list of notifications -type NotificationList []*Notification - -// LoadAttributes load Repo Issue User and Comment if not loaded -func (nl NotificationList) LoadAttributes(ctx context.Context) error { - if _, _, err := nl.LoadRepos(ctx); err != nil { - return err - } - if _, err := nl.LoadIssues(ctx); err != nil { - return err - } - if _, err := nl.LoadUsers(ctx); err != nil { - return err - } - if _, err := nl.LoadComments(ctx); err != nil { - return err - } - return nil -} - -func (nl NotificationList) getPendingRepoIDs() []int64 { - ids := make(container.Set[int64], len(nl)) - for _, notification := range nl { - if notification.Repository != nil { - continue - } - ids.Add(notification.RepoID) - } - return ids.Values() -} - -// LoadRepos loads repositories from database -func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) { - if len(nl) == 0 { - return repo_model.RepositoryList{}, []int{}, nil - } - - repoIDs := nl.getPendingRepoIDs() - repos := make(map[int64]*repo_model.Repository, len(repoIDs)) - left := len(repoIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", repoIDs[:limit]). - Rows(new(repo_model.Repository)) - if err != nil { - return nil, nil, err - } - - for rows.Next() { - var repo repo_model.Repository - err = rows.Scan(&repo) - if err != nil { - rows.Close() - return nil, nil, err - } - - repos[repo.ID] = &repo - } - _ = rows.Close() - - left -= limit - repoIDs = repoIDs[limit:] - } - - failed := []int{} - - reposList := make(repo_model.RepositoryList, 0, len(repoIDs)) - for i, notification := range nl { - if notification.Repository == nil { - notification.Repository = repos[notification.RepoID] - } - if notification.Repository == nil { - log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID) - failed = append(failed, i) - continue - } - var found bool - for _, r := range reposList { - if r.ID == notification.RepoID { - found = true - break - } - } - if !found { - reposList = append(reposList, notification.Repository) - } - } - return reposList, failed, nil -} - -func (nl NotificationList) getPendingIssueIDs() []int64 { - ids := make(container.Set[int64], len(nl)) - for _, notification := range nl { - if notification.Issue != nil { - continue - } - ids.Add(notification.IssueID) - } - return ids.Values() -} - -// LoadIssues loads issues from database -func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { - if len(nl) == 0 { - return []int{}, nil - } - - issueIDs := nl.getPendingIssueIDs() - issues := make(map[int64]*issues_model.Issue, len(issueIDs)) - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", issueIDs[:limit]). - Rows(new(issues_model.Issue)) - if err != nil { - return nil, err - } - - for rows.Next() { - var issue issues_model.Issue - err = rows.Scan(&issue) - if err != nil { - rows.Close() - return nil, err - } - - issues[issue.ID] = &issue - } - _ = rows.Close() - - left -= limit - issueIDs = issueIDs[limit:] - } - - failures := []int{} - - for i, notification := range nl { - if notification.Issue == nil { - notification.Issue = issues[notification.IssueID] - if notification.Issue == nil { - if notification.IssueID != 0 { - log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID) - failures = append(failures, i) - } - continue - } - notification.Issue.Repo = notification.Repository - } - } - return failures, nil -} - -// Without returns the notification list without the failures -func (nl NotificationList) Without(failures []int) NotificationList { - if len(failures) == 0 { - return nl - } - remaining := make([]*Notification, 0, len(nl)) - last := -1 - var i int - for _, i = range failures { - remaining = append(remaining, nl[last+1:i]...) - last = i - } - if len(nl) > i { - remaining = append(remaining, nl[i+1:]...) - } - return remaining -} - -func (nl NotificationList) getPendingCommentIDs() []int64 { - ids := make(container.Set[int64], len(nl)) - for _, notification := range nl { - if notification.CommentID == 0 || notification.Comment != nil { - continue - } - ids.Add(notification.CommentID) - } - return ids.Values() -} - -func (nl NotificationList) getUserIDs() []int64 { - ids := make(container.Set[int64], len(nl)) - for _, notification := range nl { - if notification.UserID == 0 || notification.User != nil { - continue - } - ids.Add(notification.UserID) - } - return ids.Values() -} - -// LoadUsers loads users from database -func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { - if len(nl) == 0 { - return []int{}, nil - } - - userIDs := nl.getUserIDs() - users := make(map[int64]*user_model.User, len(userIDs)) - left := len(userIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", userIDs[:limit]). - Rows(new(user_model.User)) - if err != nil { - return nil, err - } - - for rows.Next() { - var user user_model.User - err = rows.Scan(&user) - if err != nil { - rows.Close() - return nil, err - } - - users[user.ID] = &user - } - _ = rows.Close() - - left -= limit - userIDs = userIDs[limit:] - } - - failures := []int{} - for i, notification := range nl { - if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil { - notification.User = users[notification.UserID] - if notification.User == nil { - log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID) - failures = append(failures, i) - continue - } - } - } - return failures, nil -} - -// LoadComments loads comments from database -func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { - if len(nl) == 0 { - return []int{}, nil - } - - commentIDs := nl.getPendingCommentIDs() - comments := make(map[int64]*issues_model.Comment, len(commentIDs)) - left := len(commentIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", commentIDs[:limit]). - Rows(new(issues_model.Comment)) - if err != nil { - return nil, err - } - - for rows.Next() { - var comment issues_model.Comment - err = rows.Scan(&comment) - if err != nil { - rows.Close() - return nil, err - } - - comments[comment.ID] = &comment - } - _ = rows.Close() - - left -= limit - commentIDs = commentIDs[limit:] - } - - failures := []int{} - for i, notification := range nl { - if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil { - notification.Comment = comments[notification.CommentID] - if notification.Comment == nil { - log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID) - failures = append(failures, i) - continue - } - notification.Comment.Issue = notification.Issue - } - } - return failures, nil -} - // SetIssueReadBy sets issue to be read by given user. func SetIssueReadBy(ctx context.Context, issueID, userID int64) error { if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil { diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go new file mode 100644 index 0000000000000..957f9456e7bdd --- /dev/null +++ b/models/activities/notification_list.go @@ -0,0 +1,472 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activities + +import ( + "context" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + access_model "code.gitea.io/gitea/models/perm/access" + 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" + + "xorm.io/builder" +) + +// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored. +type FindNotificationOptions struct { + db.ListOptions + UserID int64 + RepoID int64 + IssueID int64 + Status []NotificationStatus + Source []NotificationSource + UpdatedAfterUnix int64 + UpdatedBeforeUnix int64 +} + +// ToCond will convert each condition into a xorm-Cond +func (opts FindNotificationOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.UserID != 0 { + cond = cond.And(builder.Eq{"notification.user_id": opts.UserID}) + } + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID}) + } + if opts.IssueID != 0 { + cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID}) + } + if len(opts.Status) > 0 { + if len(opts.Status) == 1 { + cond = cond.And(builder.Eq{"notification.status": opts.Status[0]}) + } else { + cond = cond.And(builder.In("notification.status", opts.Status)) + } + } + if len(opts.Source) > 0 { + cond = cond.And(builder.In("notification.source", opts.Source)) + } + if opts.UpdatedAfterUnix != 0 { + cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix}) + } + if opts.UpdatedBeforeUnix != 0 { + cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix}) + } + return cond +} + +func (opts FindNotificationOptions) ToOrders() string { + return "notification.updated_unix DESC" +} + +// CreateOrUpdateIssueNotifications creates an issue notification +// for each watcher, or updates it if already exists +// receiverID > 0 just send to receiver, else send to all watcher +func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil { + return err + } + + return committer.Commit() +} + +func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { + // init + var toNotify container.Set[int64] + notifications, err := db.Find[Notification](ctx, FindNotificationOptions{ + IssueID: issueID, + }) + if err != nil { + return err + } + + issue, err := issues_model.GetIssueByID(ctx, issueID) + if err != nil { + return err + } + + if receiverID > 0 { + toNotify = make(container.Set[int64], 1) + toNotify.Add(receiverID) + } else { + toNotify = make(container.Set[int64], 32) + issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true) + if err != nil { + return err + } + toNotify.AddMultiple(issueWatches...) + if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) { + repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID) + if err != nil { + return err + } + toNotify.AddMultiple(repoWatches...) + } + issueParticipants, err := issue.GetParticipantIDsByIssue(ctx) + if err != nil { + return err + } + toNotify.AddMultiple(issueParticipants...) + + // dont notify user who cause notification + delete(toNotify, notificationAuthorID) + // explicit unwatch on issue + issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false) + if err != nil { + return err + } + for _, id := range issueUnWatches { + toNotify.Remove(id) + } + } + + err = issue.LoadRepo(ctx) + if err != nil { + return err + } + + // notify + for userID := range toNotify { + issue.Repo.Units = nil + user, err := user_model.GetUserByID(ctx, userID) + if err != nil { + if user_model.IsErrUserNotExist(err) { + continue + } + + return err + } + if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) { + continue + } + if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) { + continue + } + + if notificationExists(notifications, issue.ID, userID) { + if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil { + return err + } + continue + } + if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil { + return err + } + } + return nil +} + +// NotificationList contains a list of notifications +type NotificationList []*Notification + +// LoadAttributes load Repo Issue User and Comment if not loaded +func (nl NotificationList) LoadAttributes(ctx context.Context) error { + if _, _, err := nl.LoadRepos(ctx); err != nil { + return err + } + if _, err := nl.LoadIssues(ctx); err != nil { + return err + } + if _, err := nl.LoadUsers(ctx); err != nil { + return err + } + if _, err := nl.LoadComments(ctx); err != nil { + return err + } + return nil +} + +func (nl NotificationList) getPendingRepoIDs() []int64 { + ids := make(container.Set[int64], len(nl)) + for _, notification := range nl { + if notification.Repository != nil { + continue + } + ids.Add(notification.RepoID) + } + return ids.Values() +} + +// LoadRepos loads repositories from database +func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) { + if len(nl) == 0 { + return repo_model.RepositoryList{}, []int{}, nil + } + + repoIDs := nl.getPendingRepoIDs() + repos := make(map[int64]*repo_model.Repository, len(repoIDs)) + left := len(repoIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", repoIDs[:limit]). + Rows(new(repo_model.Repository)) + if err != nil { + return nil, nil, err + } + + for rows.Next() { + var repo repo_model.Repository + err = rows.Scan(&repo) + if err != nil { + rows.Close() + return nil, nil, err + } + + repos[repo.ID] = &repo + } + _ = rows.Close() + + left -= limit + repoIDs = repoIDs[limit:] + } + + failed := []int{} + + reposList := make(repo_model.RepositoryList, 0, len(repoIDs)) + for i, notification := range nl { + if notification.Repository == nil { + notification.Repository = repos[notification.RepoID] + } + if notification.Repository == nil { + log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID) + failed = append(failed, i) + continue + } + var found bool + for _, r := range reposList { + if r.ID == notification.RepoID { + found = true + break + } + } + if !found { + reposList = append(reposList, notification.Repository) + } + } + return reposList, failed, nil +} + +func (nl NotificationList) getPendingIssueIDs() []int64 { + ids := make(container.Set[int64], len(nl)) + for _, notification := range nl { + if notification.Issue != nil { + continue + } + ids.Add(notification.IssueID) + } + return ids.Values() +} + +// LoadIssues loads issues from database +func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { + if len(nl) == 0 { + return []int{}, nil + } + + issueIDs := nl.getPendingIssueIDs() + issues := make(map[int64]*issues_model.Issue, len(issueIDs)) + left := len(issueIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", issueIDs[:limit]). + Rows(new(issues_model.Issue)) + if err != nil { + return nil, err + } + + for rows.Next() { + var issue issues_model.Issue + err = rows.Scan(&issue) + if err != nil { + rows.Close() + return nil, err + } + + issues[issue.ID] = &issue + } + _ = rows.Close() + + left -= limit + issueIDs = issueIDs[limit:] + } + + failures := []int{} + + for i, notification := range nl { + if notification.Issue == nil { + notification.Issue = issues[notification.IssueID] + if notification.Issue == nil { + if notification.IssueID != 0 { + log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID) + failures = append(failures, i) + } + continue + } + notification.Issue.Repo = notification.Repository + } + } + return failures, nil +} + +// Without returns the notification list without the failures +func (nl NotificationList) Without(failures []int) NotificationList { + if len(failures) == 0 { + return nl + } + remaining := make([]*Notification, 0, len(nl)) + last := -1 + var i int + for _, i = range failures { + remaining = append(remaining, nl[last+1:i]...) + last = i + } + if len(nl) > i { + remaining = append(remaining, nl[i+1:]...) + } + return remaining +} + +func (nl NotificationList) getPendingCommentIDs() []int64 { + ids := make(container.Set[int64], len(nl)) + for _, notification := range nl { + if notification.CommentID == 0 || notification.Comment != nil { + continue + } + ids.Add(notification.CommentID) + } + return ids.Values() +} + +func (nl NotificationList) getUserIDs() []int64 { + ids := make(container.Set[int64], len(nl)) + for _, notification := range nl { + if notification.UserID == 0 || notification.User != nil { + continue + } + ids.Add(notification.UserID) + } + return ids.Values() +} + +// LoadUsers loads users from database +func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { + if len(nl) == 0 { + return []int{}, nil + } + + userIDs := nl.getUserIDs() + users := make(map[int64]*user_model.User, len(userIDs)) + left := len(userIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", userIDs[:limit]). + Rows(new(user_model.User)) + if err != nil { + return nil, err + } + + for rows.Next() { + var user user_model.User + err = rows.Scan(&user) + if err != nil { + rows.Close() + return nil, err + } + + users[user.ID] = &user + } + _ = rows.Close() + + left -= limit + userIDs = userIDs[limit:] + } + + failures := []int{} + for i, notification := range nl { + if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil { + notification.User = users[notification.UserID] + if notification.User == nil { + log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID) + failures = append(failures, i) + continue + } + } + } + return failures, nil +} + +// LoadComments loads comments from database +func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { + if len(nl) == 0 { + return []int{}, nil + } + + commentIDs := nl.getPendingCommentIDs() + comments := make(map[int64]*issues_model.Comment, len(commentIDs)) + left := len(commentIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", commentIDs[:limit]). + Rows(new(issues_model.Comment)) + if err != nil { + return nil, err + } + + for rows.Next() { + var comment issues_model.Comment + err = rows.Scan(&comment) + if err != nil { + rows.Close() + return nil, err + } + + comments[comment.ID] = &comment + } + _ = rows.Close() + + left -= limit + commentIDs = commentIDs[limit:] + } + + failures := []int{} + for i, notification := range nl { + if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil { + notification.Comment = comments[notification.CommentID] + if notification.Comment == nil { + log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID) + failures = append(failures, i) + continue + } + notification.Comment.Issue = notification.Issue + } + } + return failures, nil +} diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml index 17586caa2191f..74fc716180d1d 100644 --- a/models/fixtures/comment.yml +++ b/models/fixtures/comment.yml @@ -75,3 +75,11 @@ content: "comment in private pository" created_unix: 946684811 updated_unix: 946684811 + +- + id: 9 + type: 22 # review + poster_id: 2 + issue_id: 2 # in repo_id 1 + review_id: 20 + created_unix: 946684810 diff --git a/models/fixtures/review.yml b/models/fixtures/review.yml index 7a8808006849c..ac97e24c2be0c 100644 --- a/models/fixtures/review.yml +++ b/models/fixtures/review.yml @@ -170,3 +170,12 @@ content: "review request for user15" updated_unix: 946684835 created_unix: 946684835 + +- + id: 20 + type: 22 + reviewer_id: 1 + issue_id: 2 + content: "Review Comment" + updated_unix: 946684810 + created_unix: 946684810 diff --git a/modules/actions/task_state.go b/modules/actions/task_state.go index fe925bbb5d987..31a74be3fd360 100644 --- a/modules/actions/task_state.go +++ b/modules/actions/task_state.go @@ -41,6 +41,12 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep { } logIndex += preStep.LogLength + // lastHasRunStep is the last step that has run. + // For example, + // 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1. + // 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3. + // 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2. + // So its Stopped is the Started of postStep when there are no more steps to run. var lastHasRunStep *actions_model.ActionTaskStep for _, step := range task.Steps { if step.Status.HasRun() { @@ -56,11 +62,15 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep { Name: postStepName, Status: actions_model.StatusWaiting, } - if task.Status.IsDone() { + // If the lastHasRunStep is the last step, or it has failed, postStep has started. + if lastHasRunStep.Status.IsFailure() || lastHasRunStep == task.Steps[len(task.Steps)-1] { postStep.LogIndex = logIndex postStep.LogLength = task.LogLength - postStep.LogIndex - postStep.Status = task.Status postStep.Started = lastHasRunStep.Stopped + postStep.Status = actions_model.StatusRunning + } + if task.Status.IsDone() { + postStep.Status = task.Status postStep.Stopped = task.Stopped } ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2) diff --git a/modules/actions/task_state_test.go b/modules/actions/task_state_test.go index 3a599fbcbd2b0..28213d781befe 100644 --- a/modules/actions/task_state_test.go +++ b/modules/actions/task_state_test.go @@ -103,6 +103,40 @@ func TestFullSteps(t *testing.T) { {Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 100, LogLength: 0, Started: 10100, Stopped: 10100}, }, }, + { + name: "all steps finished but task is running", + task: &actions_model.ActionTask{ + Steps: []*actions_model.ActionTaskStep{ + {Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090}, + }, + Status: actions_model.StatusRunning, + Started: 10000, + Stopped: 0, + LogLength: 100, + }, + want: []*actions_model.ActionTaskStep{ + {Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010}, + {Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090}, + {Name: postStepName, Status: actions_model.StatusRunning, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 0}, + }, + }, + { + name: "skipped task", + task: &actions_model.ActionTask{ + Steps: []*actions_model.ActionTaskStep{ + {Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + }, + Status: actions_model.StatusSkipped, + Started: 0, + Stopped: 0, + LogLength: 0, + }, + want: []*actions_model.ActionTaskStep{ + {Name: preStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + {Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + {Name: postStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/modules/git/repo.go b/modules/git/repo.go index cef45c6af0cd9..4511e900e08a2 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -283,7 +283,7 @@ type DivergeObject struct { // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) { cmd := NewCommand(ctx, "rev-list", "--count", "--left-right"). - AddDynamicArguments(baseBranch + "..." + targetBranch) + AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--") stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) if err != nil { return do, err diff --git a/modules/httplib/url.go b/modules/httplib/url.go index 14b95898f5b5e..b679b445004ab 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -8,20 +8,40 @@ import ( "strings" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) -// IsRiskyRedirectURL returns true if the URL is considered risky for redirects -func IsRiskyRedirectURL(s string) bool { +func urlIsRelative(s string, u *url.URL) bool { // Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH" // Therefore we should ignore these redirect locations to prevent open redirects if len(s) > 1 && (s[0] == '/' || s[0] == '\\') && (s[1] == '/' || s[1] == '\\') { - return true + return false } + return u != nil && u.Scheme == "" && u.Host == "" +} +// IsRelativeURL detects if a URL is relative (no scheme or host) +func IsRelativeURL(s string) bool { u, err := url.Parse(s) - if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(s), strings.ToLower(setting.AppURL))) { - return true - } + return err == nil && urlIsRelative(s, u) +} - return false +func IsCurrentGiteaSiteURL(s string) bool { + u, err := url.Parse(s) + if err != nil { + return false + } + if u.Path != "" { + u.Path = "/" + util.PathJoinRelX(u.Path) + if !strings.HasSuffix(u.Path, "/") { + u.Path += "/" + } + } + if urlIsRelative(s, u) { + return u.Path == "" || strings.HasPrefix(strings.ToLower(u.Path), strings.ToLower(setting.AppSubURL+"/")) + } + if u.Path == "" { + u.Path = "/" + } + return strings.HasPrefix(strings.ToLower(u.String()), strings.ToLower(setting.AppURL)) } diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index 72033b1208cc4..9b7b2422981a2 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -7,32 +7,65 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) -func TestIsRiskyRedirectURL(t *testing.T) { - setting.AppURL = "http://localhost:3000/" - tests := []struct { - input string - want bool - }{ - {"", false}, - {"foo", false}, - {"/", false}, - {"/foo?k=%20#abc", false}, +func TestIsRelativeURL(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")() + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + rel := []string{ + "", + "foo", + "/", + "/foo?k=%20#abc", + } + for _, s := range rel { + assert.True(t, IsRelativeURL(s), "rel = %q", s) + } + abs := []string{ + "//", + "\\\\", + "/\\", + "\\/", + "mailto:a@b.com", + "https://test.com", + } + for _, s := range abs { + assert.False(t, IsRelativeURL(s), "abs = %q", s) + } +} - {"//", true}, - {"\\\\", true}, - {"/\\", true}, - {"\\/", true}, - {"mail:a@b.com", true}, - {"https://test.com", true}, - {setting.AppURL + "/foo", false}, - } - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - assert.Equal(t, tt.want, IsRiskyRedirectURL(tt.input)) - }) +func TestIsCurrentGiteaSiteURL(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")() + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + good := []string{ + "?key=val", + "/sub", + "/sub/", + "/sub/foo", + "/sub/foo/", + "http://localhost:3000/sub?key=val", + "http://localhost:3000/sub/", } + for _, s := range good { + assert.True(t, IsCurrentGiteaSiteURL(s), "good = %q", s) + } + bad := []string{ + "/", + "//", + "\\\\", + "/foo", + "http://localhost:3000/sub/..", + "http://localhost:3000/other", + "http://other/", + } + for _, s := range bad { + assert.False(t, IsCurrentGiteaSiteURL(s), "bad = %q", s) + } + + setting.AppURL = "http://localhost:3000/" + setting.AppSubURL = "" + assert.True(t, IsCurrentGiteaSiteURL("http://localhost:3000?key=val")) } diff --git a/routers/common/redirect.go b/routers/common/redirect.go index 9bf2025e19e7b..34044e814bc61 100644 --- a/routers/common/redirect.go +++ b/routers/common/redirect.go @@ -17,7 +17,7 @@ func FetchRedirectDelegate(resp http.ResponseWriter, req *http.Request) { // The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2", // then frontend needs this delegate to redirect to the new location with hash correctly. redirect := req.PostFormValue("redirect") - if httplib.IsRiskyRedirectURL(redirect) { + if !httplib.IsCurrentGiteaSiteURL(redirect) { resp.WriteHeader(http.StatusBadRequest) return } diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 3dad39f7b1df7..a09956f73865d 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -75,6 +75,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { updates = append(updates, option) if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") { // put the master/main branch first + // FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates. + // If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once. + // See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27 + // If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch. copy(updates[1:], updates) updates[0] = option } @@ -129,9 +133,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { commitIDs = append(commitIDs, update.NewCommitID) } - if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, func(commitID string) (*git.Commit, error) { - return gitRepo.GetCommit(commitID) - }); err != nil { + if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil { ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err), }) diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index da6bef207ad7c..ab81740e3f110 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -133,7 +133,7 @@ func RedirectAfterLogin(ctx *context.Context) { if setting.LandingPageURL == setting.LandingPageLogin { nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page } - ctx.RedirectToFirst(redirectTo, nextRedirectTo) + ctx.RedirectToCurrentSite(redirectTo, nextRedirectTo) } func CheckAutoLogin(ctx *context.Context) bool { @@ -371,7 +371,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) { middleware.DeleteRedirectToCookie(ctx.Resp) if obeyRedirect { - ctx.RedirectToFirst(redirectTo) + ctx.RedirectToCurrentSite(redirectTo) } return redirectTo } @@ -808,7 +808,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) { ctx.Flash.Success(ctx.Tr("auth.account_activated")) if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 { middleware.DeleteRedirectToCookie(ctx.Resp) - ctx.RedirectToFirst(redirectTo) + ctx.RedirectToCurrentSite(redirectTo) return } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index d5ca7397f06c1..3189d1372e259 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1157,7 +1157,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 { middleware.DeleteRedirectToCookie(ctx.Resp) - ctx.RedirectToFirst(redirectTo) + ctx.RedirectToCurrentSite(redirectTo) return } diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go index c9e03860412e1..3af8b7edf2b61 100644 --- a/routers/web/auth/password.go +++ b/routers/web/auth/password.go @@ -314,7 +314,7 @@ func MustChangePasswordPost(ctx *context.Context) { if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) { middleware.DeleteRedirectToCookie(ctx.Resp) - ctx.RedirectToFirst(redirectTo) + ctx.RedirectToCurrentSite(redirectTo) return } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a0a500f0b233b..fb03ed9d9c1e9 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -446,13 +446,13 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t" ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link, url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels), - mentionedID, projectID, assigneeID, posterID, archived) + milestoneID, projectID, assigneeID, posterID, archived) ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link, url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels), - mentionedID, projectID, assigneeID, posterID, archived) + milestoneID, projectID, assigneeID, posterID, archived) ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link, url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels), - mentionedID, projectID, assigneeID, posterID, archived) + milestoneID, projectID, assigneeID, posterID, archived) ctx.Data["SelLabelIDs"] = labelIDs ctx.Data["SelectLabels"] = selectLabels ctx.Data["ViewType"] = viewType diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go index 5f035f1eb04cb..8344ff409113f 100644 --- a/routers/web/repo/pull_review_test.go +++ b/routers/web/repo/pull_review_test.go @@ -4,6 +4,7 @@ package repo import ( + "net/http" "net/http/httptest" "testing" @@ -73,4 +74,20 @@ func TestRenderConversation(t *testing.T) { renderConversation(ctx, preparedComment, "timeline") assert.Contains(t, resp.Body.String(), `