0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-04 22:57:34 -04:00

Merge 8b0071d4efdb68cfd1d8ccd4db0997ac8abbb567 into 70685a948979469a8086b2e8d1784a8f27c40f33

This commit is contained in:
Lunny Xiao 2025-07-04 08:47:49 -07:00 committed by GitHub
commit 97fcef9b93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 804 additions and 158 deletions

View File

@ -6,15 +6,18 @@ package activities
import (
"context"
"fmt"
"html/template"
"net/url"
"strconv"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
@ -46,6 +49,8 @@ const (
NotificationSourceCommit
// NotificationSourceRepository is a notification for a repository
NotificationSourceRepository
// NotificationSourceRelease is a notification for a release
NotificationSourceRelease
)
// Notification represents a notification
@ -60,6 +65,7 @@ type Notification struct {
IssueID int64 `xorm:"NOT NULL"`
CommitID string
CommentID int64
ReleaseID int64
UpdatedBy int64 `xorm:"NOT NULL"`
@ -67,6 +73,8 @@ type Notification struct {
Repository *repo_model.Repository `xorm:"-"`
Comment *issues_model.Comment `xorm:"-"`
User *user_model.User `xorm:"-"`
Release *repo_model.Release `xorm:"-"`
Commit *git.Commit `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
@ -104,6 +112,10 @@ func (n *Notification) TableIndices() []*schemas.Index {
commitIDIndex.AddColumn("commit_id")
indices = append(indices, commitIDIndex)
releaseIDIndex := schemas.NewIndex("idx_notification_release_id", schemas.IndexType)
releaseIDIndex.AddColumn("release_id")
indices = append(indices, releaseIDIndex)
updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType)
updatedByIndex.AddColumn("updated_by")
indices = append(indices, updatedByIndex)
@ -116,36 +128,55 @@ func init() {
}
// 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 {
var notify []*Notification
func CreateRepoTransferNotification(ctx context.Context, doerID, repoID, receiverID int64) error {
notify := &Notification{
UserID: receiverID,
RepoID: repoID,
Status: NotificationStatusUnread,
UpdatedBy: doerID,
Source: NotificationSourceRepository,
}
return db.Insert(ctx, notify)
}
if newOwner.IsOrganization() {
users, err := organization.GetUsersWhoCanCreateOrgRepo(ctx, newOwner.ID)
if err != nil || len(users) == 0 {
return err
}
for i := range users {
notify = append(notify, &Notification{
UserID: i,
RepoID: repo.ID,
Status: NotificationStatusUnread,
UpdatedBy: doer.ID,
Source: NotificationSourceRepository,
})
}
} else {
notify = []*Notification{{
UserID: newOwner.ID,
RepoID: repo.ID,
Status: NotificationStatusUnread,
UpdatedBy: doer.ID,
Source: NotificationSourceRepository,
}}
}
func CreateCommitNotifications(ctx context.Context, doerID, repoID int64, commitID string, receiverID int64) error {
notification := &Notification{
Source: NotificationSourceCommit,
UserID: receiverID,
RepoID: repoID,
CommitID: commitID,
Status: NotificationStatusUnread,
UpdatedBy: doerID,
}
return db.Insert(ctx, notify)
})
return db.Insert(ctx, notification)
}
func CreateOrUpdateReleaseNotifications(ctx context.Context, doerID, repoID, releaseID, receiverID int64) error {
notification := new(Notification)
if _, err := db.GetEngine(ctx).
Where("user_id = ?", receiverID).
And("repo_id = ?", repoID).
And("release_id = ?", releaseID).
Get(notification); err != nil {
return err
}
if notification.ID > 0 {
notification.Status = NotificationStatusUnread
notification.UpdatedBy = doerID
_, err := db.GetEngine(ctx).ID(notification.ID).Cols("status", "updated_by").Update(notification)
return err
}
notification = &Notification{
Source: NotificationSourceRelease,
RepoID: repoID,
UserID: receiverID,
Status: NotificationStatusUnread,
ReleaseID: releaseID,
UpdatedBy: doerID,
}
return db.Insert(ctx, notification)
}
func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error {
@ -213,6 +244,12 @@ func (n *Notification) LoadAttributes(ctx context.Context) (err error) {
if err = n.loadComment(ctx); err != nil {
return err
}
if err = n.loadCommit(ctx); err != nil {
return err
}
if err = n.loadRelease(ctx); err != nil {
return err
}
return err
}
@ -253,6 +290,41 @@ func (n *Notification) loadComment(ctx context.Context) (err error) {
return nil
}
func (n *Notification) loadCommit(ctx context.Context) (err error) {
if n.Source != NotificationSourceCommit || n.CommitID == "" || n.Commit != nil {
return nil
}
if n.Repository == nil {
_ = n.loadRepo(ctx)
if n.Repository == nil {
return fmt.Errorf("repository not found for notification %d", n.ID)
}
}
repo, err := gitrepo.OpenRepository(ctx, n.Repository)
if err != nil {
return fmt.Errorf("OpenRepository [%d]: %w", n.Repository.ID, err)
}
defer repo.Close()
n.Commit, err = repo.GetCommit(n.CommitID)
if err != nil {
return fmt.Errorf("Notification[%d]: Failed to get repo for commit %s: %v", n.ID, n.CommitID, err)
}
return nil
}
func (n *Notification) loadRelease(ctx context.Context) (err error) {
if n.Release == nil && n.ReleaseID != 0 {
n.Release, err = repo_model.GetReleaseByID(ctx, n.ReleaseID)
if err != nil {
return fmt.Errorf("GetReleaseByID [%d]: %w", n.ReleaseID, err)
}
}
return nil
}
func (n *Notification) loadUser(ctx context.Context) (err error) {
if n.User == nil {
n.User, err = user_model.GetUserByID(ctx, n.UserID)
@ -285,6 +357,8 @@ func (n *Notification) HTMLURL(ctx context.Context) string {
return n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID)
case NotificationSourceRepository:
return n.Repository.HTMLURL()
case NotificationSourceRelease:
return n.Release.HTMLURL()
}
return ""
}
@ -301,10 +375,28 @@ func (n *Notification) Link(ctx context.Context) string {
return n.Repository.Link() + "/commit/" + url.PathEscape(n.CommitID)
case NotificationSourceRepository:
return n.Repository.Link()
case NotificationSourceRelease:
return n.Release.Link()
}
return ""
}
func (n *Notification) IconHTML(ctx context.Context) template.HTML {
switch n.Source {
case NotificationSourceIssue, NotificationSourcePullRequest:
// n.Issue should be loaded before calling this method
return n.Issue.IconHTML(ctx)
case NotificationSourceCommit:
return svg.RenderHTML("octicon-commit", 16, "text grey")
case NotificationSourceRepository:
return svg.RenderHTML("octicon-repo", 16, "text grey")
case NotificationSourceRelease:
return svg.RenderHTML("octicon-tag", 16, "text grey")
default:
return ""
}
}
// APIURL formats a URL-string to the notification
func (n *Notification) APIURL() string {
return setting.AppURL + "api/v1/notifications/threads/" + strconv.FormatInt(n.ID, 10)
@ -373,6 +465,28 @@ func SetRepoReadBy(ctx context.Context, userID, repoID int64) error {
return err
}
// SetReleaseReadBy sets issue to be read by given user.
func SetReleaseReadBy(ctx context.Context, releaseID, userID int64) error {
_, err := db.GetEngine(ctx).Where(builder.Eq{
"user_id": userID,
"status": NotificationStatusUnread,
"source": NotificationSourceRelease,
"release_id": releaseID,
}).Cols("status").Update(&Notification{Status: NotificationStatusRead})
return err
}
// SetCommitReadBy sets issue to be read by given user.
func SetCommitReadBy(ctx context.Context, repoID, userID int64, commitID string) error {
_, err := db.GetEngine(ctx).Where(builder.Eq{
"user_id": userID,
"status": NotificationStatusUnread,
"source": NotificationSourceCommit,
"commit_id": commitID,
}).Cols("status").Update(&Notification{Status: NotificationStatusRead})
return err
}
// SetNotificationStatus change the notification status
func SetNotificationStatus(ctx context.Context, notificationID int64, user *user_model.User, status NotificationStatus) (*Notification, error) {
notification, err := GetNotificationByID(ctx, notificationID)
@ -385,8 +499,7 @@ func SetNotificationStatus(ctx context.Context, notificationID int64, user *user
}
notification.Status = status
_, err = db.GetEngine(ctx).ID(notificationID).Update(notification)
_, err = db.GetEngine(ctx).ID(notificationID).Cols("status").Update(notification)
return notification, err
}

View File

@ -13,6 +13,8 @@ import (
"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/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
@ -25,6 +27,7 @@ type FindNotificationOptions struct {
UserID int64
RepoID int64
IssueID int64
ReleaseID int64
Status []NotificationStatus
Source []NotificationSource
UpdatedAfterUnix int64
@ -43,6 +46,9 @@ func (opts FindNotificationOptions) ToConds() builder.Cond {
if opts.IssueID != 0 {
cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
}
if opts.ReleaseID != 0 {
cond = cond.And(builder.Eq{"notification.release_id": opts.ReleaseID})
}
if len(opts.Status) > 0 {
if len(opts.Status) == 1 {
cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
@ -70,17 +76,9 @@ func (opts FindNotificationOptions) ToOrders() string {
// 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()
return db.WithTx(ctx, func(ctx context.Context) error {
return createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID)
})
}
func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
@ -174,18 +172,31 @@ 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 {
repos, _, err := nl.LoadRepos(ctx)
if err != nil {
return err
}
if err := repos.LoadAttributes(ctx); err != nil {
return err
}
if _, err := nl.LoadIssues(ctx); err != nil {
return err
}
if err = nl.LoadIssuePullRequests(ctx); err != nil {
return err
}
if _, err := nl.LoadUsers(ctx); err != nil {
return err
}
if _, err := nl.LoadComments(ctx); err != nil {
return err
}
if _, err = nl.LoadCommits(ctx); err != nil {
return err
}
if _, err := nl.LoadReleases(ctx); err != nil {
return err
}
return nil
}
@ -458,6 +469,89 @@ func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
return failures, nil
}
func (nl NotificationList) getPendingReleaseIDs() []int64 {
ids := make(container.Set[int64], len(nl))
for _, notification := range nl {
if notification.Release != nil {
continue
}
if notification.ReleaseID > 0 {
ids.Add(notification.ReleaseID)
}
}
return ids.Values()
}
func (nl NotificationList) LoadReleases(ctx context.Context) ([]int, error) {
if len(nl) == 0 {
return []int{}, nil
}
releaseIDs := nl.getPendingReleaseIDs()
releases := make(map[int64]*repo_model.Release, len(releaseIDs))
if err := db.GetEngine(ctx).In("id", releaseIDs).Find(&releases); err != nil {
return nil, err
}
failures := []int{}
for i, notification := range nl {
if notification.ReleaseID > 0 && notification.Release == nil && releases[notification.ReleaseID] != nil {
notification.Release = releases[notification.ReleaseID]
if notification.Release == nil {
log.Error("Notification[%d]: ReleaseID[%d] failed to load", notification.ID, notification.ReleaseID)
failures = append(failures, i)
continue
}
notification.Release.Repo = notification.Repository
}
}
return failures, nil
}
func (nl NotificationList) LoadCommits(ctx context.Context) ([]int, error) {
if len(nl) == 0 {
return []int{}, nil
}
_, _, err := nl.LoadRepos(ctx)
if err != nil {
return nil, err
}
failures := []int{}
repos := make(map[int64]*git.Repository, len(nl))
for i, n := range nl {
if n.Source != NotificationSourceCommit || n.CommitID == "" {
continue
}
repo, ok := repos[n.RepoID]
if !ok {
repo, err = gitrepo.OpenRepository(ctx, n.Repository)
if err != nil {
log.Error("Notification[%d]: Failed to get repo for commit %s: %v", n.ID, n.CommitID, err)
failures = append(failures, i)
continue
}
repos[n.RepoID] = repo
}
n.Commit, err = repo.GetCommit(n.CommitID)
if err != nil {
log.Error("Notification[%d]: Failed to get repo for commit %s: %v", n.ID, n.CommitID, err)
failures = append(failures, i)
continue
}
}
for _, repo := range repos {
if err := repo.Close(); err != nil {
log.Error("Failed to close repository: %v", err)
}
}
return failures, nil
}
// LoadIssuePullRequests loads all issues' pull requests if possible
func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error {
issues := make(map[int64]*issues_model.Issue, len(nl))

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@ -442,6 +443,30 @@ func (issue *Issue) PatchURL() string {
return ""
}
/* the logic should be kept the same as getIssueIcon/getIssueColor in TS code */
func (issue *Issue) IconHTML(ctx context.Context) template.HTML {
if issue.IsPull {
if issue.PullRequest == nil { // pull request should be loaded before calling this function
return template.HTML("No PullRequest")
}
if issue.IsClosed {
if issue.PullRequest.HasMerged {
return svg.RenderHTML("octicon-git-merge", 16, "text purple")
}
return svg.RenderHTML("octicon-git-pull-request-closed", 16, "text red")
}
if issue.PullRequest.IsWorkInProgress(ctx) {
return svg.RenderHTML("octicon-git-pull-request-draft", 16, "text grey")
}
return svg.RenderHTML("octicon-git-pull-request", 16, "text green")
}
if issue.IsClosed {
return svg.RenderHTML("octicon-issue-closed", 16, "text red")
}
return svg.RenderHTML("octicon-issue-opened", 16, "text green")
}
// State returns string representation of issue status.
func (issue *Issue) State() api.StateType {
if issue.IsClosed {

View File

@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/models/migrations/v1_22"
"code.gitea.io/gitea/models/migrations/v1_23"
"code.gitea.io/gitea/models/migrations/v1_24"
"code.gitea.io/gitea/models/migrations/v1_25"
"code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8"
@ -382,6 +383,10 @@ func prepareMigrationTasks() []*migration {
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),
// Gitea 1.24.0-rc0 ends at migration ID number 320 (database version 321)
newMigration(321, "Add Index to action_task table", v1_25.AddReleaseNotification),
}
return preparedMigrations
}

View File

@ -0,0 +1,82 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_25
import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
// NotificationV321 represents a notification
type NotificationV321 struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL"`
RepoID int64 `xorm:"NOT NULL"`
Status uint8 `xorm:"SMALLINT NOT NULL"`
Source uint8 `xorm:"SMALLINT NOT NULL"`
IssueID int64 `xorm:"NOT NULL"`
CommitID string
CommentID int64
ReleaseID int64
UpdatedBy int64 `xorm:"NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
}
func (n *NotificationV321) TableName() string {
return "notification"
}
// TableIndices implements xorm's TableIndices interface
func (n *NotificationV321) TableIndices() []*schemas.Index {
indices := make([]*schemas.Index, 0, 8)
usuuIndex := schemas.NewIndex("u_s_uu", schemas.IndexType)
usuuIndex.AddColumn("user_id", "status", "updated_unix")
indices = append(indices, usuuIndex)
// Add the individual indices that were previously defined in struct tags
userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType)
userIDIndex.AddColumn("user_id")
indices = append(indices, userIDIndex)
repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType)
repoIDIndex.AddColumn("repo_id")
indices = append(indices, repoIDIndex)
statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType)
statusIndex.AddColumn("status")
indices = append(indices, statusIndex)
sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType)
sourceIndex.AddColumn("source")
indices = append(indices, sourceIndex)
issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType)
issueIDIndex.AddColumn("issue_id")
indices = append(indices, issueIDIndex)
commitIDIndex := schemas.NewIndex("idx_notification_commit_id", schemas.IndexType)
commitIDIndex.AddColumn("commit_id")
indices = append(indices, commitIDIndex)
releaseIDIndex := schemas.NewIndex("idx_notification_release_id", schemas.IndexType)
releaseIDIndex.AddColumn("release_id")
indices = append(indices, releaseIDIndex)
updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType)
updatedByIndex.AddColumn("updated_by")
indices = append(indices, updatedByIndex)
return indices
}
func AddReleaseNotification(x *xorm.Engine) error {
return x.Sync(new(NotificationV321))
}

View File

@ -93,6 +93,22 @@ func init() {
db.RegisterModel(new(Release))
}
func (r *Release) LoadPublisher(ctx context.Context) error {
if r.Publisher != nil {
return nil
}
var err error
r.Publisher, err = user_model.GetPossibleUserByID(ctx, r.PublisherID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
r.Publisher = user_model.NewGhostUser()
} else {
return err
}
}
return nil
}
// LoadAttributes load repo and publisher attributes for a release
func (r *Release) LoadAttributes(ctx context.Context) error {
var err error
@ -102,15 +118,8 @@ func (r *Release) LoadAttributes(ctx context.Context) error {
return err
}
}
if r.Publisher == nil {
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
r.Publisher = user_model.NewGhostUser()
} else {
return err
}
}
if err := r.LoadPublisher(ctx); err != nil {
return err
}
return GetReleaseAttachments(ctx, r)
}

View File

@ -6,6 +6,7 @@ package user
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
@ -81,3 +82,20 @@ func GetUsersByIDs(ctx context.Context, ids []int64) (UserList, error) {
Find(&ous)
return ous, err
}
// GetUsersByUsernames returns all resolved users from a list of user names.
func GetUsersByUsernames(ctx context.Context, userNames []string) (UserList, error) {
ous := make([]*User, 0, len(userNames))
if len(userNames) == 0 {
return ous, nil
}
for i, name := range userNames {
userNames[i] = strings.ToLower(name)
}
err := db.GetEngine(ctx).
Where("`type` = ?", UserTypeIndividual).
In("lower_name", userNames).
Find(&ous)
return ous, err
}

View File

@ -25,7 +25,7 @@ type NotificationSubject struct {
LatestCommentURL string `json:"latest_comment_url"`
HTMLURL string `json:"html_url"`
LatestCommentHTMLURL string `json:"latest_comment_html_url"`
Type NotifySubjectType `json:"type" binding:"In(Issue,Pull,Commit,Repository)"`
Type NotifySubjectType `json:"type" binding:"In(Issue,Pull,Commit,Repository,Release)"`
State StateType `json:"state"`
}
@ -46,4 +46,6 @@ const (
NotifySubjectCommit NotifySubjectType = "Commit"
// NotifySubjectRepository an repository is subject of an notification
NotifySubjectRepository NotifySubjectType = "Repository"
// NotifySubjectRelease an release is subject of an notification
NotifySubjectRelease NotifySubjectType = "Release"
)

View File

@ -71,6 +71,8 @@ func subjectToSource(value []string) (result []activities_model.NotificationSour
result = append(result, activities_model.NotificationSourceCommit)
case "repository":
result = append(result, activities_model.NotificationSourceRepository)
case "release":
result = append(result, activities_model.NotificationSourceRelease)
}
}
return result

View File

@ -80,7 +80,7 @@ func ListRepoNotifications(ctx *context.APIContext) {
// collectionFormat: multi
// items:
// type: string
// enum: [issue,pull,commit,repository]
// enum: [issue,pull,commit,repository,release]
// - name: since
// in: query
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
@ -214,14 +214,20 @@ func ReadRepoNotifications(ctx *context.APIContext) {
changed := make([]*structs.NotificationThread, 0, len(nl))
if err := activities_model.NotificationList(nl).LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
for _, n := range nl {
notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
if err != nil {
ctx.APIErrorInternal(err)
return
}
_ = notif.LoadAttributes(ctx)
changed = append(changed, convert.ToNotificationThread(ctx, notif))
n.Status = notif.Status
n.UpdatedUnix = notif.UpdatedUnix
changed = append(changed, convert.ToNotificationThread(ctx, n))
}
ctx.JSON(http.StatusResetContent, changed)
}

View File

@ -42,7 +42,7 @@ func ListNotifications(ctx *context.APIContext) {
// collectionFormat: multi
// items:
// type: string
// enum: [issue,pull,commit,repository]
// enum: [issue,pull,commit,repository,release]
// - name: since
// in: query
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
@ -161,14 +161,20 @@ func ReadNotifications(ctx *context.APIContext) {
changed := make([]*structs.NotificationThread, 0, len(nl))
if err := activities_model.NotificationList(nl).LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
for _, n := range nl {
notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
if err != nil {
ctx.APIErrorInternal(err)
return
}
_ = notif.LoadAttributes(ctx)
changed = append(changed, convert.ToNotificationThread(ctx, notif))
n.Status = notif.Status
n.UpdatedUnix = notif.UpdatedUnix
changed = append(changed, convert.ToNotificationThread(ctx, n))
}
ctx.JSON(http.StatusResetContent, changed)

View File

@ -12,6 +12,7 @@ import (
"path"
"strings"
activities_model "code.gitea.io/gitea/models/activities"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
@ -304,6 +305,14 @@ func Diff(ctx *context.Context) {
commitID = commit.ID.String()
}
if ctx.IsSigned {
err = activities_model.SetCommitReadBy(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, commitID)
if err != nil {
ctx.ServerError("SetCommitReadBy", err)
return
}
}
fileOnly := ctx.FormBool("file-only")
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
files := ctx.FormStrings("files")

View File

@ -11,6 +11,7 @@ import (
"strconv"
"strings"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/renderhelper"
@ -298,6 +299,14 @@ func SingleRelease(ctx *context.Context) {
release.Title = release.TagName
}
if ctx.IsSigned && !release.IsTag {
err = activities_model.SetReleaseReadBy(ctx, release.ID, ctx.Doer.ID)
if err != nil {
ctx.ServerError("SetReleaseReadBy", err)
return
}
}
ctx.Data["PageIsSingleTag"] = release.IsTag
ctx.Data["SingleReleaseTagName"] = release.TagName
if release.IsTag {

View File

@ -137,6 +137,22 @@ func getNotifications(ctx *context.Context) {
notifications = notifications.Without(failures)
failCount += len(failures)
failures, err = notifications.LoadCommits(ctx)
if err != nil {
ctx.ServerError("LoadCommits", err)
return
}
notifications = notifications.Without(failures)
failCount += len(failures)
failures, err = notifications.LoadReleases(ctx)
if err != nil {
ctx.ServerError("LoadReleases", err)
return
}
notifications = notifications.Without(failures)
failCount += len(failures)
if failCount > 0 {
ctx.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
}

View File

@ -6,6 +6,7 @@ package convert
import (
"context"
"net/url"
"strings"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/perm"
@ -71,7 +72,7 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification)
url := n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID)
result.Subject = &api.NotificationSubject{
Type: api.NotifySubjectCommit,
Title: n.CommitID,
Title: strings.TrimSpace(n.Commit.CommitMessage),
URL: url,
HTMLURL: url,
}
@ -83,6 +84,13 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification)
URL: n.Repository.Link(),
HTMLURL: n.Repository.HTMLURL(),
}
case activities_model.NotificationSourceRelease:
result.Subject = &api.NotificationSubject{
Type: api.NotifySubjectRelease,
Title: n.Release.Title,
URL: n.Release.Link(),
HTMLURL: n.Release.HTMLURL(),
}
}
return result

View File

@ -58,10 +58,6 @@ func renderRepoIssueIconTitle(ctx context.Context, opts markup.RenderIssueIconTi
}
}
htmlIcon, err := webCtx.RenderToHTML("shared/issueicon", issue)
if err != nil {
return "", err
}
return htmlutil.HTMLFormat(`<a href="%s">%s %s %s</a>`, opts.LinkHref, htmlIcon, issue.Title, textIssueIndex), nil
return htmlutil.HTMLFormat(`<a href="%s">%s %s %s</a>`, opts.LinkHref,
issue.IconHTML(ctx), issue.Title, textIssueIndex), nil
}

View File

@ -5,28 +5,39 @@ package uinotification
import (
"context"
"slices"
activities_model "code.gitea.io/gitea/models/activities"
"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/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
)
type (
notificationService struct {
notify_service.NullNotifier
issueQueue *queue.WorkerPoolQueue[issueNotificationOpts]
queue *queue.WorkerPoolQueue[notificationOpts]
}
issueNotificationOpts struct {
notificationOpts struct {
Source activities_model.NotificationSource
IssueID int64
CommentID int64
CommitID string // commit ID for commit notifications
RepoID int64
ReleaseID int64
NotificationAuthorID int64
ReceiverID int64 // 0 -- ALL Watcher
}
@ -43,66 +54,79 @@ var _ notify_service.Notifier = &notificationService{}
// NewNotifier create a new notificationService notifier
func NewNotifier() notify_service.Notifier {
ns := &notificationService{}
ns.issueQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "notification-service", handler)
if ns.issueQueue == nil {
ns.queue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "notification-service", handler)
if ns.queue == nil {
log.Fatal("Unable to create notification-service queue")
}
return ns
}
func handler(items ...issueNotificationOpts) []issueNotificationOpts {
func handler(items ...notificationOpts) []notificationOpts {
for _, opts := range items {
if err := activities_model.CreateOrUpdateIssueNotifications(db.DefaultContext, opts.IssueID, opts.CommentID, opts.NotificationAuthorID, opts.ReceiverID); err != nil {
log.Error("Was unable to create issue notification: %v", err)
switch opts.Source {
case activities_model.NotificationSourceRepository:
if err := activities_model.CreateRepoTransferNotification(db.DefaultContext, opts.NotificationAuthorID, opts.RepoID, opts.ReceiverID); err != nil {
log.Error("CreateRepoTransferNotification: %v", err)
}
case activities_model.NotificationSourceCommit:
if err := activities_model.CreateCommitNotifications(db.DefaultContext, opts.NotificationAuthorID, opts.RepoID, opts.CommitID, opts.ReceiverID); err != nil {
log.Error("Was unable to create commit notification: %v", err)
}
case activities_model.NotificationSourceRelease:
if err := activities_model.CreateOrUpdateReleaseNotifications(db.DefaultContext, opts.NotificationAuthorID, opts.RepoID, opts.ReleaseID, opts.ReceiverID); err != nil {
log.Error("Was unable to create release notification: %v", err)
}
case activities_model.NotificationSourceIssue, activities_model.NotificationSourcePullRequest:
fallthrough
default:
if err := activities_model.CreateOrUpdateIssueNotifications(db.DefaultContext, opts.IssueID, opts.CommentID, opts.NotificationAuthorID, opts.ReceiverID); err != nil {
log.Error("Was unable to create issue notification: %v", err)
}
}
}
return nil
}
func (ns *notificationService) Run() {
go graceful.GetManager().RunWithCancel(ns.issueQueue) // TODO: using "go" here doesn't seem right, just leave it as old code
go graceful.GetManager().RunWithCancel(ns.queue) // TODO: using "go" here doesn't seem right, just leave it as old code
}
func (ns *notificationService) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
) {
opts := issueNotificationOpts{
opts := notificationOpts{
Source: util.Iif(issue.IsPull, activities_model.NotificationSourcePullRequest, activities_model.NotificationSourceIssue),
IssueID: issue.ID,
RepoID: issue.RepoID,
NotificationAuthorID: doer.ID,
}
if comment != nil {
opts.CommentID = comment.ID
}
_ = ns.issueQueue.Push(opts)
_ = ns.queue.Push(opts)
for _, mention := range mentions {
opts := issueNotificationOpts{
IssueID: issue.ID,
NotificationAuthorID: doer.ID,
ReceiverID: mention.ID,
}
if comment != nil {
opts.CommentID = comment.ID
}
_ = ns.issueQueue.Push(opts)
opts.ReceiverID = mention.ID
_ = ns.queue.Push(opts)
}
}
func (ns *notificationService) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
_ = ns.issueQueue.Push(issueNotificationOpts{
opts := notificationOpts{
Source: activities_model.NotificationSourceIssue,
RepoID: issue.RepoID,
IssueID: issue.ID,
NotificationAuthorID: issue.Poster.ID,
})
}
_ = ns.queue.Push(opts)
for _, mention := range mentions {
_ = ns.issueQueue.Push(issueNotificationOpts{
IssueID: issue.ID,
NotificationAuthorID: issue.Poster.ID,
ReceiverID: mention.ID,
})
opts.ReceiverID = mention.ID
_ = ns.queue.Push(opts)
}
}
func (ns *notificationService) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) {
_ = ns.issueQueue.Push(issueNotificationOpts{
_ = ns.queue.Push(notificationOpts{
Source: util.Iif(issue.IsPull, activities_model.NotificationSourcePullRequest, activities_model.NotificationSourceIssue),
IssueID: issue.ID,
NotificationAuthorID: doer.ID,
CommentID: actionComment.ID,
@ -115,7 +139,8 @@ func (ns *notificationService) IssueChangeTitle(ctx context.Context, doer *user_
return
}
if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress(ctx) {
_ = ns.issueQueue.Push(issueNotificationOpts{
_ = ns.queue.Push(notificationOpts{
Source: util.Iif(issue.IsPull, activities_model.NotificationSourcePullRequest, activities_model.NotificationSourceIssue),
IssueID: issue.ID,
NotificationAuthorID: doer.ID,
})
@ -123,7 +148,8 @@ func (ns *notificationService) IssueChangeTitle(ctx context.Context, doer *user_
}
func (ns *notificationService) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
_ = ns.issueQueue.Push(issueNotificationOpts{
_ = ns.queue.Push(notificationOpts{
Source: activities_model.NotificationSourcePullRequest,
IssueID: pr.Issue.ID,
NotificationAuthorID: doer.ID,
})
@ -160,7 +186,8 @@ func (ns *notificationService) NewPullRequest(ctx context.Context, pr *issues_mo
toNotify.Add(mention.ID)
}
for receiverID := range toNotify {
_ = ns.issueQueue.Push(issueNotificationOpts{
_ = ns.queue.Push(notificationOpts{
Source: activities_model.NotificationSourcePullRequest,
IssueID: pr.Issue.ID,
NotificationAuthorID: pr.Issue.PosterID,
ReceiverID: receiverID,
@ -169,30 +196,25 @@ func (ns *notificationService) NewPullRequest(ctx context.Context, pr *issues_mo
}
func (ns *notificationService) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, r *issues_model.Review, c *issues_model.Comment, mentions []*user_model.User) {
opts := issueNotificationOpts{
opts := notificationOpts{
Source: activities_model.NotificationSourcePullRequest,
IssueID: pr.Issue.ID,
NotificationAuthorID: r.Reviewer.ID,
}
if c != nil {
opts.CommentID = c.ID
}
_ = ns.issueQueue.Push(opts)
_ = ns.queue.Push(opts)
for _, mention := range mentions {
opts := issueNotificationOpts{
IssueID: pr.Issue.ID,
NotificationAuthorID: r.Reviewer.ID,
ReceiverID: mention.ID,
}
if c != nil {
opts.CommentID = c.ID
}
_ = ns.issueQueue.Push(opts)
opts.ReceiverID = mention.ID
_ = ns.queue.Push(opts)
}
}
func (ns *notificationService) PullRequestCodeComment(ctx context.Context, pr *issues_model.PullRequest, c *issues_model.Comment, mentions []*user_model.User) {
for _, mention := range mentions {
_ = ns.issueQueue.Push(issueNotificationOpts{
_ = ns.queue.Push(notificationOpts{
Source: activities_model.NotificationSourcePullRequest,
IssueID: pr.Issue.ID,
NotificationAuthorID: c.Poster.ID,
CommentID: c.ID,
@ -202,26 +224,29 @@ func (ns *notificationService) PullRequestCodeComment(ctx context.Context, pr *i
}
func (ns *notificationService) PullRequestPushCommits(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) {
opts := issueNotificationOpts{
opts := notificationOpts{
Source: activities_model.NotificationSourcePullRequest,
IssueID: pr.IssueID,
NotificationAuthorID: doer.ID,
CommentID: comment.ID,
}
_ = ns.issueQueue.Push(opts)
_ = ns.queue.Push(opts)
}
func (ns *notificationService) PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
opts := issueNotificationOpts{
opts := notificationOpts{
Source: activities_model.NotificationSourcePullRequest,
IssueID: review.IssueID,
NotificationAuthorID: doer.ID,
CommentID: comment.ID,
}
_ = ns.issueQueue.Push(opts)
_ = ns.queue.Push(opts)
}
func (ns *notificationService) IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
if !removed && doer.ID != assignee.ID {
opts := issueNotificationOpts{
opts := notificationOpts{
Source: activities_model.NotificationSourceIssue,
IssueID: issue.ID,
NotificationAuthorID: doer.ID,
ReceiverID: assignee.ID,
@ -231,13 +256,14 @@ func (ns *notificationService) IssueChangeAssignee(ctx context.Context, doer *us
opts.CommentID = comment.ID
}
_ = ns.issueQueue.Push(opts)
_ = ns.queue.Push(opts)
}
}
func (ns *notificationService) PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
if isRequest {
opts := issueNotificationOpts{
opts := notificationOpts{
Source: activities_model.NotificationSourcePullRequest,
IssueID: issue.ID,
NotificationAuthorID: doer.ID,
ReceiverID: reviewer.ID,
@ -247,15 +273,117 @@ func (ns *notificationService) PullRequestReviewRequest(ctx context.Context, doe
opts.CommentID = comment.ID
}
_ = ns.issueQueue.Push(opts)
_ = ns.queue.Push(opts)
}
}
func (ns *notificationService) RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) {
err := db.WithTx(ctx, func(ctx context.Context) error {
return activities_model.CreateRepoTransferNotification(ctx, doer, newOwner, repo)
})
if err != nil {
log.Error("CreateRepoTransferNotification: %v", err)
opts := notificationOpts{
Source: activities_model.NotificationSourceRepository,
RepoID: repo.ID,
NotificationAuthorID: doer.ID,
}
if newOwner.IsOrganization() {
users, err := organization.GetUsersWhoCanCreateOrgRepo(ctx, newOwner.ID)
if err != nil {
log.Error("GetUsersWhoCanCreateOrgRepo: %v", err)
return
}
for i := range users {
opts.ReceiverID = users[i].ID
_ = ns.queue.Push(opts)
}
} else {
opts.ReceiverID = newOwner.ID
_ = ns.queue.Push(opts)
}
}
func (ns *notificationService) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
if len(commits.Commits) == 0 {
return
}
for _, commit := range commits.Commits {
mentions := references.FindAllMentionsMarkdown(commit.Message)
receivers, err := user_model.GetUsersByUsernames(ctx, mentions)
if err != nil {
log.Error("GetUserIDsByNames: %v", err)
return
}
notBlocked := make([]*user_model.User, 0, len(mentions))
for _, user := range receivers {
if !user_model.IsUserBlockedBy(ctx, repo.Owner, user.ID) {
notBlocked = append(notBlocked, user)
}
}
receivers = notBlocked
for _, receiver := range receivers {
perm, err := access_model.GetUserRepoPermission(ctx, repo, receiver)
if err != nil {
log.Error("GetUserRepoPermission [%d]: %w", receiver.ID, err)
return
}
if !perm.CanRead(unit.TypeCode) {
continue
}
opts := notificationOpts{
Source: activities_model.NotificationSourceCommit,
RepoID: repo.ID,
CommitID: commit.Sha1,
NotificationAuthorID: pusher.ID,
ReceiverID: receiver.ID,
}
if err := ns.queue.Push(opts); err != nil {
log.Error("PushCommits: %v", err)
}
}
}
}
func (ns *notificationService) NewRelease(ctx context.Context, rel *repo_model.Release) {
_ = rel.LoadPublisher(ctx)
ns.UpdateRelease(ctx, rel.Publisher, rel)
}
func (ns *notificationService) UpdateRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) {
opts := notificationOpts{
Source: activities_model.NotificationSourceRelease,
RepoID: rel.RepoID,
ReleaseID: rel.ID,
NotificationAuthorID: rel.PublisherID,
}
repoWatcherIDs, err := repo_model.GetRepoWatchersIDs(ctx, rel.RepoID)
if err != nil {
log.Error("GetRepoWatchersIDs: %v", err)
return
}
repo, err := repo_model.GetRepositoryByID(ctx, rel.RepoID)
if err != nil {
log.Error("GetRepositoryByID: %v", err)
return
}
if err := repo.LoadOwner(ctx); err != nil {
log.Error("LoadOwner: %v", err)
return
}
if !repo.Owner.IsOrganization() && !slices.Contains(repoWatcherIDs, repo.Owner.ID) && repo.Owner.ID != doer.ID {
repoWatcherIDs = append(repoWatcherIDs, repo.Owner.ID)
}
for _, watcherID := range repoWatcherIDs {
if watcherID == doer.ID {
// Do not notify the publisher of the release
continue
}
opts.ReceiverID = watcherID
_ = ns.queue.Push(opts)
}
}

View File

@ -187,7 +187,7 @@
{{end}}
{{if .HasPullRequest}}
<div class="ui segment flex-text-block tw-gap-4">
{{template "shared/issueicon" .}}
{{.IconHTML ctx}}
<div class="issue-title tw-break-anywhere">
{{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title $.Repository}}
<span class="index">#{{.PullRequest.Issue.Index}}</span>

View File

@ -12,7 +12,7 @@
<div class="content tw-w-full">
<div class="tw-flex tw-items-start tw-gap-[5px]">
<div class="issue-card-icon">
{{template "shared/issueicon" .}}
{{.IconHTML ctx}}
</div>
<a class="issue-card-title muted issue-title tw-break-anywhere" href="{{.Link}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}}

View File

@ -1,26 +0,0 @@
{{/* the logic should be kept the same as getIssueIcon/getIssueColor in JS code */}}
{{- if .IsPull -}}
{{- if not .PullRequest -}}
No PullRequest
{{- else -}}
{{- if .IsClosed -}}
{{- if .PullRequest.HasMerged -}}
{{- svg "octicon-git-merge" 16 "text purple" -}}
{{- else -}}
{{- svg "octicon-git-pull-request-closed" 16 "text red" -}}
{{- end -}}
{{- else -}}
{{- if .PullRequest.IsWorkInProgress ctx -}}
{{- svg "octicon-git-pull-request-draft" 16 "text grey" -}}
{{- else -}}
{{- svg "octicon-git-pull-request" 16 "text green" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- else -}}
{{- if .IsClosed -}}
{{- svg "octicon-issue-closed" 16 "text red" -}}
{{- else -}}
{{- svg "octicon-issue-opened" 16 "text green" -}}
{{- end -}}
{{- end -}}

View File

@ -9,7 +9,7 @@
{{if $.CanWriteIssuesOrPulls}}
<input type="checkbox" autocomplete="off" class="issue-checkbox tw-mr-[14px]" data-issue-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} &quot;{{.Title}}&quot;">
{{end}}
{{template "shared/issueicon" .}}
{{.IconHTML ctx}}
</div>
</div>

View File

@ -1523,7 +1523,8 @@
"issue",
"pull",
"commit",
"repository"
"repository",
"release"
],
"type": "string"
},
@ -13305,7 +13306,8 @@
"issue",
"pull",
"commit",
"repository"
"repository",
"release"
],
"type": "string"
},

View File

@ -37,11 +37,7 @@
{{range $notification := .Notifications}}
<div class="notifications-item tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-p-2" id="notification_{{.ID}}" data-status="{{.Status}}">
<div class="notifications-icon tw-ml-2 tw-mr-1 tw-self-start tw-mt-1">
{{if .Issue}}
{{template "shared/issueicon" .Issue}}
{{else}}
{{svg "octicon-repo" 16 "text grey"}}
{{end}}
{{.IconHTML ctx}}
</div>
<a class="notifications-link tw-flex tw-flex-1 tw-flex-col silenced" href="{{.Link ctx}}">
<div class="notifications-top-row tw-text-13 tw-break-anywhere">
@ -54,6 +50,10 @@
<span class="issue-title tw-break-anywhere">
{{if .Issue}}
{{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}
{{else if .Release}}
{{.Release.Title}}
{{else if .Commit}}
{{.Commit.Summary}}
{{else}}
{{.Repository.FullName}}
{{end}}
@ -63,6 +63,10 @@
<div class="notifications-updated tw-items-center tw-mr-2">
{{if .Issue}}
{{DateUtils.TimeSince .Issue.UpdatedUnix}}
{{else if .Release}}
{{DateUtils.TimeSince .Release.CreatedUnix}}
{{else if .Commit}}
{{DateUtils.TimeSince .Commit.Committer.When}}
{{else}}
{{DateUtils.TimeSince .UpdatedUnix}}
{{end}}

View File

@ -4,9 +4,12 @@
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"testing"
"time"
activities_model "code.gitea.io/gitea/models/activities"
auth_model "code.gitea.io/gitea/models/auth"
@ -15,6 +18,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@ -213,3 +217,137 @@ func TestAPINotificationPUT(t *testing.T) {
assert.True(t, apiNL[0].Unread)
assert.False(t, apiNL[0].Pinned)
}
func TestAPICommitNotification(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
session := loginUser(t, user2.Name)
token1 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
content := "This is a test commit"
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
// push a commit with @user2 in the commit message, it's expected to create a notification
createFileOptions := api.CreateFileOptions{
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "This is a test commit to mention @user2",
Author: api.Identity{
Name: "Anne Doe",
Email: "annedoe@example.com",
},
Committer: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
Dates: api.CommitDateOptions{
Author: time.Unix(946684810, 0),
Committer: time.Unix(978307190, 0),
},
},
ContentBase64: contentEncoded,
}
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/new_commit_notification.txt", user2.Name, repo1.Name), &createFileOptions).
AddTokenAuth(token1)
MakeRequest(t, req, http.StatusCreated)
// Check notifications are as expected
token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteNotification)
req = NewRequest(t, "GET", "/api/v1/notifications?all=true").
AddTokenAuth(token2)
resp := MakeRequest(t, req, http.StatusOK)
var apiNL []api.NotificationThread
DecodeJSON(t, resp, &apiNL)
assert.Equal(t, api.NotifySubjectCommit, apiNL[0].Subject.Type)
assert.Equal(t, "This is a test commit to mention @user2", apiNL[0].Subject.Title)
assert.True(t, apiNL[0].Unread)
assert.False(t, apiNL[0].Pinned)
})
}
func TestAPIReleaseNotification(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
session1 := loginUser(t, user1.Name)
token1 := getTokenForLoggedInUser(t, session1, auth_model.AccessTokenScopeWriteRepository)
// user1 create a release, it's expected to create a notification
createNewReleaseUsingAPI(t, token1, user2, repo1, "v0.0.2", "", "v0.0.2 is released", "test notification release")
// user2 login to check notifications
session2 := loginUser(t, user2.Name)
// Check notifications are as expected
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteNotification)
req := NewRequest(t, "GET", "/api/v1/notifications?all=true").
AddTokenAuth(token2)
resp := MakeRequest(t, req, http.StatusOK)
var apiNL []api.NotificationThread
DecodeJSON(t, resp, &apiNL)
assert.Equal(t, api.NotifySubjectRelease, apiNL[0].Subject.Type)
assert.Equal(t, "v0.0.2 is released", apiNL[0].Subject.Title)
assert.True(t, apiNL[0].Unread)
assert.False(t, apiNL[0].Pinned)
})
}
func TestAPIRepoTransferNotification(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session1 := loginUser(t, user2.Name)
token1 := getTokenForLoggedInUser(t, session1, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// create repo to move
repoName := "moveME"
apiRepo := new(api.Repository)
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: repoName,
Description: "repo move around",
Private: false,
Readme: "Default",
AutoInit: true,
}).AddTokenAuth(token1)
resp := MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, apiRepo)
defer func() {
_ = repo_service.DeleteRepositoryDirectly(db.DefaultContext, apiRepo.ID)
}()
// repo user1/moveME created, now transfer it to org6
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
session2 := loginUser(t, user2.Name)
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer", repo.OwnerName, repo.Name), &api.TransferRepoOption{
NewOwner: "org6",
TeamIDs: nil,
}).AddTokenAuth(token2)
MakeRequest(t, req, http.StatusCreated)
// user5 login to check notifications, because user5 is a member of org6's owners team
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
session5 := loginUser(t, user5.Name)
// Check notifications are as expected
token5 := getTokenForLoggedInUser(t, session5, auth_model.AccessTokenScopeWriteNotification)
req = NewRequest(t, "GET", "/api/v1/notifications?all=true").
AddTokenAuth(token5)
resp = MakeRequest(t, req, http.StatusOK)
var apiNL []api.NotificationThread
DecodeJSON(t, resp, &apiNL)
assert.Equal(t, api.NotifySubjectRepository, apiNL[0].Subject.Type)
assert.Equal(t, "user2/moveME", apiNL[0].Subject.Title)
assert.True(t, apiNL[0].Unread)
assert.False(t, apiNL[0].Pinned)
})
}

View File

@ -1,6 +1,6 @@
import type {Issue} from '../types.ts';
// the getIssueIcon/getIssueColor logic should be kept the same as "templates/shared/issueicon.tmpl"
// the getIssueIcon/getIssueColor logic should be kept the same as "models/activities/issue.IconHTML"
export function getIssueIcon(issue: Issue) {
if (issue.pull_request) {