mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-04 00:01:16 -04:00
Compare commits
16 Commits
8a0d8bc1b5
...
1971a41a54
Author | SHA1 | Date | |
---|---|---|---|
|
1971a41a54 | ||
|
8cbec63cc7 | ||
|
6455c8202b | ||
|
97fc87af89 | ||
|
7aaa2d6de4 | ||
|
6f79a1a0cf | ||
|
11bd0eaad4 | ||
|
6fe5c4c4d9 | ||
|
7a1cc340b8 | ||
|
fd17f057e5 | ||
|
60227c5cdd | ||
|
49e9ab0039 | ||
|
01a0b8af2d | ||
|
4163c6dc68 | ||
|
803a3a4426 | ||
|
6b055ddb9f |
@ -6,15 +6,18 @@ package activities
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/organization"
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
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/setting"
|
||||||
|
"code.gitea.io/gitea/modules/svg"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
@ -46,6 +49,8 @@ const (
|
|||||||
NotificationSourceCommit
|
NotificationSourceCommit
|
||||||
// NotificationSourceRepository is a notification for a repository
|
// NotificationSourceRepository is a notification for a repository
|
||||||
NotificationSourceRepository
|
NotificationSourceRepository
|
||||||
|
// NotificationSourceRelease is a notification for a release
|
||||||
|
NotificationSourceRelease
|
||||||
)
|
)
|
||||||
|
|
||||||
// Notification represents a notification
|
// Notification represents a notification
|
||||||
@ -60,6 +65,7 @@ type Notification struct {
|
|||||||
IssueID int64 `xorm:"NOT NULL"`
|
IssueID int64 `xorm:"NOT NULL"`
|
||||||
CommitID string
|
CommitID string
|
||||||
CommentID int64
|
CommentID int64
|
||||||
|
ReleaseID int64
|
||||||
|
|
||||||
UpdatedBy int64 `xorm:"NOT NULL"`
|
UpdatedBy int64 `xorm:"NOT NULL"`
|
||||||
|
|
||||||
@ -67,6 +73,8 @@ type Notification struct {
|
|||||||
Repository *repo_model.Repository `xorm:"-"`
|
Repository *repo_model.Repository `xorm:"-"`
|
||||||
Comment *issues_model.Comment `xorm:"-"`
|
Comment *issues_model.Comment `xorm:"-"`
|
||||||
User *user_model.User `xorm:"-"`
|
User *user_model.User `xorm:"-"`
|
||||||
|
Release *repo_model.Release `xorm:"-"`
|
||||||
|
Commit *git.Commit `xorm:"-"`
|
||||||
|
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
|
||||||
@ -104,6 +112,10 @@ func (n *Notification) TableIndices() []*schemas.Index {
|
|||||||
commitIDIndex.AddColumn("commit_id")
|
commitIDIndex.AddColumn("commit_id")
|
||||||
indices = append(indices, commitIDIndex)
|
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 := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType)
|
||||||
updatedByIndex.AddColumn("updated_by")
|
updatedByIndex.AddColumn("updated_by")
|
||||||
indices = append(indices, updatedByIndex)
|
indices = append(indices, updatedByIndex)
|
||||||
@ -116,36 +128,55 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateRepoTransferNotification creates notification for the user a repository was transferred to
|
// 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 {
|
func CreateRepoTransferNotification(ctx context.Context, doerID, repoID, receiverID int64) error {
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
notify := &Notification{
|
||||||
var notify []*Notification
|
UserID: receiverID,
|
||||||
|
RepoID: repoID,
|
||||||
|
Status: NotificationStatusUnread,
|
||||||
|
UpdatedBy: doerID,
|
||||||
|
Source: NotificationSourceRepository,
|
||||||
|
}
|
||||||
|
return db.Insert(ctx, notify)
|
||||||
|
}
|
||||||
|
|
||||||
if newOwner.IsOrganization() {
|
func CreateCommitNotifications(ctx context.Context, doerID, repoID int64, commitID string, receiverID int64) error {
|
||||||
users, err := organization.GetUsersWhoCanCreateOrgRepo(ctx, newOwner.ID)
|
notification := &Notification{
|
||||||
if err != nil || len(users) == 0 {
|
Source: NotificationSourceCommit,
|
||||||
|
UserID: receiverID,
|
||||||
|
RepoID: repoID,
|
||||||
|
CommitID: commitID,
|
||||||
|
Status: NotificationStatusUnread,
|
||||||
|
UpdatedBy: doerID,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
return err
|
||||||
}
|
}
|
||||||
for i := range users {
|
if notification.ID > 0 {
|
||||||
notify = append(notify, &Notification{
|
notification.Status = NotificationStatusUnread
|
||||||
UserID: i,
|
notification.UpdatedBy = doerID
|
||||||
RepoID: repo.ID,
|
_, err := db.GetEngine(ctx).ID(notification.ID).Cols("status", "updated_by").Update(notification)
|
||||||
Status: NotificationStatusUnread,
|
return err
|
||||||
UpdatedBy: doer.ID,
|
|
||||||
Source: NotificationSourceRepository,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
notify = []*Notification{{
|
|
||||||
UserID: newOwner.ID,
|
|
||||||
RepoID: repo.ID,
|
|
||||||
Status: NotificationStatusUnread,
|
|
||||||
UpdatedBy: doer.ID,
|
|
||||||
Source: NotificationSourceRepository,
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.Insert(ctx, notify)
|
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 {
|
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 {
|
if err = n.loadComment(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = n.loadCommit(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = n.loadRelease(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,6 +290,41 @@ func (n *Notification) loadComment(ctx context.Context) (err error) {
|
|||||||
return nil
|
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) {
|
func (n *Notification) loadUser(ctx context.Context) (err error) {
|
||||||
if n.User == nil {
|
if n.User == nil {
|
||||||
n.User, err = user_model.GetUserByID(ctx, n.UserID)
|
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)
|
return n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID)
|
||||||
case NotificationSourceRepository:
|
case NotificationSourceRepository:
|
||||||
return n.Repository.HTMLURL()
|
return n.Repository.HTMLURL()
|
||||||
|
case NotificationSourceRelease:
|
||||||
|
return n.Release.HTMLURL()
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -301,10 +375,28 @@ func (n *Notification) Link(ctx context.Context) string {
|
|||||||
return n.Repository.Link() + "/commit/" + url.PathEscape(n.CommitID)
|
return n.Repository.Link() + "/commit/" + url.PathEscape(n.CommitID)
|
||||||
case NotificationSourceRepository:
|
case NotificationSourceRepository:
|
||||||
return n.Repository.Link()
|
return n.Repository.Link()
|
||||||
|
case NotificationSourceRelease:
|
||||||
|
return n.Release.Link()
|
||||||
}
|
}
|
||||||
return ""
|
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
|
// APIURL formats a URL-string to the notification
|
||||||
func (n *Notification) APIURL() string {
|
func (n *Notification) APIURL() string {
|
||||||
return setting.AppURL + "api/v1/notifications/threads/" + strconv.FormatInt(n.ID, 10)
|
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
|
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
|
// SetNotificationStatus change the notification status
|
||||||
func SetNotificationStatus(ctx context.Context, notificationID int64, user *user_model.User, status NotificationStatus) (*Notification, error) {
|
func SetNotificationStatus(ctx context.Context, notificationID int64, user *user_model.User, status NotificationStatus) (*Notification, error) {
|
||||||
notification, err := GetNotificationByID(ctx, notificationID)
|
notification, err := GetNotificationByID(ctx, notificationID)
|
||||||
@ -385,8 +499,7 @@ func SetNotificationStatus(ctx context.Context, notificationID int64, user *user
|
|||||||
}
|
}
|
||||||
|
|
||||||
notification.Status = status
|
notification.Status = status
|
||||||
|
_, err = db.GetEngine(ctx).ID(notificationID).Cols("status").Update(notification)
|
||||||
_, err = db.GetEngine(ctx).ID(notificationID).Update(notification)
|
|
||||||
return notification, err
|
return notification, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"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/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
@ -25,6 +27,7 @@ type FindNotificationOptions struct {
|
|||||||
UserID int64
|
UserID int64
|
||||||
RepoID int64
|
RepoID int64
|
||||||
IssueID int64
|
IssueID int64
|
||||||
|
ReleaseID int64
|
||||||
Status []NotificationStatus
|
Status []NotificationStatus
|
||||||
Source []NotificationSource
|
Source []NotificationSource
|
||||||
UpdatedAfterUnix int64
|
UpdatedAfterUnix int64
|
||||||
@ -43,6 +46,9 @@ func (opts FindNotificationOptions) ToConds() builder.Cond {
|
|||||||
if opts.IssueID != 0 {
|
if opts.IssueID != 0 {
|
||||||
cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
|
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) > 0 {
|
||||||
if len(opts.Status) == 1 {
|
if len(opts.Status) == 1 {
|
||||||
cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
|
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
|
// for each watcher, or updates it if already exists
|
||||||
// receiverID > 0 just send to receiver, else send to all watcher
|
// receiverID > 0 just send to receiver, else send to all watcher
|
||||||
func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
|
func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if err != nil {
|
return createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID)
|
||||||
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 {
|
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
|
// LoadAttributes load Repo Issue User and Comment if not loaded
|
||||||
func (nl NotificationList) LoadAttributes(ctx context.Context) error {
|
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
|
return err
|
||||||
}
|
}
|
||||||
if _, err := nl.LoadIssues(ctx); err != nil {
|
if _, err := nl.LoadIssues(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = nl.LoadIssuePullRequests(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if _, err := nl.LoadUsers(ctx); err != nil {
|
if _, err := nl.LoadUsers(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := nl.LoadComments(ctx); err != nil {
|
if _, err := nl.LoadComments(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if _, err = nl.LoadCommits(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := nl.LoadReleases(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,6 +469,89 @@ func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
|
|||||||
return failures, nil
|
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
|
// LoadIssuePullRequests loads all issues' pull requests if possible
|
||||||
func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error {
|
func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error {
|
||||||
issues := make(map[int64]*issues_model.Issue, len(nl))
|
issues := make(map[int64]*issues_model.Issue, len(nl))
|
||||||
|
@ -715,7 +715,8 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Comment) loadReview(ctx context.Context) (err error) {
|
// LoadReview loads the associated review
|
||||||
|
func (c *Comment) LoadReview(ctx context.Context) (err error) {
|
||||||
if c.ReviewID == 0 {
|
if c.ReviewID == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -732,11 +733,6 @@ func (c *Comment) loadReview(ctx context.Context) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadReview loads the associated review
|
|
||||||
func (c *Comment) LoadReview(ctx context.Context) error {
|
|
||||||
return c.loadReview(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
|
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
|
||||||
func (c *Comment) DiffSide() string {
|
func (c *Comment) DiffSide() string {
|
||||||
if c.Line < 0 {
|
if c.Line < 0 {
|
||||||
@ -856,7 +852,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
|
|||||||
}
|
}
|
||||||
if comment.ReviewID != 0 {
|
if comment.ReviewID != 0 {
|
||||||
if comment.Review == nil {
|
if comment.Review == nil {
|
||||||
if err := comment.loadReview(ctx); err != nil {
|
if err := comment.LoadReview(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/svg"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
@ -442,6 +443,30 @@ func (issue *Issue) PatchURL() string {
|
|||||||
return ""
|
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.
|
// State returns string representation of issue status.
|
||||||
func (issue *Issue) State() api.StateType {
|
func (issue *Issue) State() api.StateType {
|
||||||
if issue.IsClosed {
|
if issue.IsClosed {
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/migrations/v1_22"
|
"code.gitea.io/gitea/models/migrations/v1_22"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_23"
|
"code.gitea.io/gitea/models/migrations/v1_23"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_24"
|
"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_6"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_7"
|
"code.gitea.io/gitea/models/migrations/v1_7"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_8"
|
"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(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
|
||||||
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
|
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
|
||||||
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),
|
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
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
82
models/migrations/v1_25/v321.go
Normal file
82
models/migrations/v1_25/v321.go
Normal 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))
|
||||||
|
}
|
@ -93,6 +93,22 @@ func init() {
|
|||||||
db.RegisterModel(new(Release))
|
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
|
// LoadAttributes load repo and publisher attributes for a release
|
||||||
func (r *Release) LoadAttributes(ctx context.Context) error {
|
func (r *Release) LoadAttributes(ctx context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
@ -102,16 +118,9 @@ func (r *Release) LoadAttributes(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.Publisher == nil {
|
if err := r.LoadPublisher(ctx); err != 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
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return GetReleaseAttachments(ctx, r)
|
return GetReleaseAttachments(ctx, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/auth"
|
"code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
@ -81,3 +82,20 @@ func GetUsersByIDs(ctx context.Context, ids []int64) (UserList, error) {
|
|||||||
Find(&ous)
|
Find(&ous)
|
||||||
return ous, err
|
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
|
||||||
|
}
|
||||||
|
@ -25,7 +25,7 @@ type NotificationSubject struct {
|
|||||||
LatestCommentURL string `json:"latest_comment_url"`
|
LatestCommentURL string `json:"latest_comment_url"`
|
||||||
HTMLURL string `json:"html_url"`
|
HTMLURL string `json:"html_url"`
|
||||||
LatestCommentHTMLURL string `json:"latest_comment_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"`
|
State StateType `json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,4 +46,6 @@ const (
|
|||||||
NotifySubjectCommit NotifySubjectType = "Commit"
|
NotifySubjectCommit NotifySubjectType = "Commit"
|
||||||
// NotifySubjectRepository an repository is subject of an notification
|
// NotifySubjectRepository an repository is subject of an notification
|
||||||
NotifySubjectRepository NotifySubjectType = "Repository"
|
NotifySubjectRepository NotifySubjectType = "Repository"
|
||||||
|
// NotifySubjectRelease an release is subject of an notification
|
||||||
|
NotifySubjectRelease NotifySubjectType = "Release"
|
||||||
)
|
)
|
||||||
|
@ -119,11 +119,14 @@ type ContentsResponse struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
SHA string `json:"sha"`
|
SHA string `json:"sha"`
|
||||||
LastCommitSHA string `json:"last_commit_sha"`
|
|
||||||
|
LastCommitSHA *string `json:"last_commit_sha,omitempty"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
LastCommitterDate time.Time `json:"last_committer_date"`
|
LastCommitterDate *time.Time `json:"last_committer_date,omitempty"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
LastAuthorDate time.Time `json:"last_author_date"`
|
LastAuthorDate *time.Time `json:"last_author_date,omitempty"`
|
||||||
|
LastCommitMessage *string `json:"last_commit_message,omitempty"`
|
||||||
|
|
||||||
// `type` will be `file`, `dir`, `symlink`, or `submodule`
|
// `type` will be `file`, `dir`, `symlink`, or `submodule`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
@ -141,8 +144,8 @@ type ContentsResponse struct {
|
|||||||
SubmoduleGitURL *string `json:"submodule_git_url"`
|
SubmoduleGitURL *string `json:"submodule_git_url"`
|
||||||
Links *FileLinksResponse `json:"_links"`
|
Links *FileLinksResponse `json:"_links"`
|
||||||
|
|
||||||
LfsOid *string `json:"lfs_oid"`
|
LfsOid *string `json:"lfs_oid,omitempty"`
|
||||||
LfsSize *int64 `json:"lfs_size"`
|
LfsSize *int64 `json:"lfs_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileCommitResponse contains information generated from a Git commit for a repo's file.
|
// FileCommitResponse contains information generated from a Git commit for a repo's file.
|
||||||
|
@ -1969,6 +1969,7 @@ pulls.cmd_instruction_checkout_title=Basculer
|
|||||||
pulls.cmd_instruction_checkout_desc=Depuis votre dépôt, basculer sur une nouvelle branche et tester des modifications.
|
pulls.cmd_instruction_checkout_desc=Depuis votre dépôt, basculer sur une nouvelle branche et tester des modifications.
|
||||||
pulls.cmd_instruction_merge_title=Fusionner
|
pulls.cmd_instruction_merge_title=Fusionner
|
||||||
pulls.cmd_instruction_merge_desc=Fusionner les modifications et mettre à jour sur Gitea.
|
pulls.cmd_instruction_merge_desc=Fusionner les modifications et mettre à jour sur Gitea.
|
||||||
|
pulls.cmd_instruction_merge_warning=Attention : cette opération ne peut pas fusionner la demande d’ajout car la « détection automatique de fusion manuelle » n’a pas été activée
|
||||||
pulls.clear_merge_message=Effacer le message de fusion
|
pulls.clear_merge_message=Effacer le message de fusion
|
||||||
pulls.clear_merge_message_hint=Effacer le message de fusion ne supprimera que le message de la révision, mais pas les pieds de révision générés tels que "Co-Authored-By:".
|
pulls.clear_merge_message_hint=Effacer le message de fusion ne supprimera que le message de la révision, mais pas les pieds de révision générés tels que "Co-Authored-By:".
|
||||||
|
|
||||||
@ -2768,6 +2769,8 @@ branch.new_branch_from=`Créer une nouvelle branche à partir de "%s"`
|
|||||||
branch.renamed=La branche %s à été renommée en %s.
|
branch.renamed=La branche %s à été renommée en %s.
|
||||||
branch.rename_default_or_protected_branch_error=Seuls les administrateurs peuvent renommer les branches par défaut ou protégées.
|
branch.rename_default_or_protected_branch_error=Seuls les administrateurs peuvent renommer les branches par défaut ou protégées.
|
||||||
branch.rename_protected_branch_failed=Cette branche est protégée par des règles de protection basées sur des globs.
|
branch.rename_protected_branch_failed=Cette branche est protégée par des règles de protection basées sur des globs.
|
||||||
|
branch.commits_divergence_from=Divergence de révisions : %[1]d en retard et %[2]d en avance sur %[3]s
|
||||||
|
branch.commits_no_divergence=Identique à la branche %[1]s
|
||||||
|
|
||||||
tag.create_tag=Créer l'étiquette %s
|
tag.create_tag=Créer l'étiquette %s
|
||||||
tag.create_tag_operation=Créer une étiquette
|
tag.create_tag_operation=Créer une étiquette
|
||||||
|
@ -2769,6 +2769,8 @@ branch.new_branch_from=`Cruthaigh brainse nua ó "%s"`
|
|||||||
branch.renamed=Ainmníodh brainse %s go %s.
|
branch.renamed=Ainmníodh brainse %s go %s.
|
||||||
branch.rename_default_or_protected_branch_error=Ní féidir ach le riarthóirí brainsí réamhshocraithe nó cosanta a athainmniú.
|
branch.rename_default_or_protected_branch_error=Ní féidir ach le riarthóirí brainsí réamhshocraithe nó cosanta a athainmniú.
|
||||||
branch.rename_protected_branch_failed=Tá an brainse seo faoi chosaint ag rialacha cosanta domhanda.
|
branch.rename_protected_branch_failed=Tá an brainse seo faoi chosaint ag rialacha cosanta domhanda.
|
||||||
|
branch.commits_divergence_from=Déanann sé dialltacht a thiomnú: %[1]d taobh thiar agus %[2]d chun tosaigh ar %[3]s
|
||||||
|
branch.commits_no_divergence=Mar an gcéanna le brainse %[1]s
|
||||||
|
|
||||||
tag.create_tag=Cruthaigh clib %s
|
tag.create_tag=Cruthaigh clib %s
|
||||||
tag.create_tag_operation=Cruthaigh clib
|
tag.create_tag_operation=Cruthaigh clib
|
||||||
|
@ -2769,6 +2769,8 @@ branch.new_branch_from=`Criar um novo ramo a partir do ramo "%s"`
|
|||||||
branch.renamed=O ramo %s foi renomeado para %s.
|
branch.renamed=O ramo %s foi renomeado para %s.
|
||||||
branch.rename_default_or_protected_branch_error=Só os administradores é que podem renomear o ramo principal ou ramos protegidos.
|
branch.rename_default_or_protected_branch_error=Só os administradores é que podem renomear o ramo principal ou ramos protegidos.
|
||||||
branch.rename_protected_branch_failed=Este ramo está protegido por regras de salvaguarda baseadas em padrões glob.
|
branch.rename_protected_branch_failed=Este ramo está protegido por regras de salvaguarda baseadas em padrões glob.
|
||||||
|
branch.commits_divergence_from=Divergência nos cometimentos: %[1]d atrás e %[2]d à frente de %[3]s
|
||||||
|
branch.commits_no_divergence=Idêntico ao ramo %[1]s
|
||||||
|
|
||||||
tag.create_tag=Criar etiqueta %s
|
tag.create_tag=Criar etiqueta %s
|
||||||
tag.create_tag_operation=Criar etiqueta
|
tag.create_tag_operation=Criar etiqueta
|
||||||
|
@ -71,6 +71,8 @@ func subjectToSource(value []string) (result []activities_model.NotificationSour
|
|||||||
result = append(result, activities_model.NotificationSourceCommit)
|
result = append(result, activities_model.NotificationSourceCommit)
|
||||||
case "repository":
|
case "repository":
|
||||||
result = append(result, activities_model.NotificationSourceRepository)
|
result = append(result, activities_model.NotificationSourceRepository)
|
||||||
|
case "release":
|
||||||
|
result = append(result, activities_model.NotificationSourceRelease)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
@ -80,7 +80,7 @@ func ListRepoNotifications(ctx *context.APIContext) {
|
|||||||
// collectionFormat: multi
|
// collectionFormat: multi
|
||||||
// items:
|
// items:
|
||||||
// type: string
|
// type: string
|
||||||
// enum: [issue,pull,commit,repository]
|
// enum: [issue,pull,commit,repository,release]
|
||||||
// - name: since
|
// - name: since
|
||||||
// in: query
|
// in: query
|
||||||
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
|
// 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))
|
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 {
|
for _, n := range nl {
|
||||||
notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
|
notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = notif.LoadAttributes(ctx)
|
n.Status = notif.Status
|
||||||
changed = append(changed, convert.ToNotificationThread(ctx, notif))
|
n.UpdatedUnix = notif.UpdatedUnix
|
||||||
|
changed = append(changed, convert.ToNotificationThread(ctx, n))
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusResetContent, changed)
|
ctx.JSON(http.StatusResetContent, changed)
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ func ListNotifications(ctx *context.APIContext) {
|
|||||||
// collectionFormat: multi
|
// collectionFormat: multi
|
||||||
// items:
|
// items:
|
||||||
// type: string
|
// type: string
|
||||||
// enum: [issue,pull,commit,repository]
|
// enum: [issue,pull,commit,repository,release]
|
||||||
// - name: since
|
// - name: since
|
||||||
// in: query
|
// in: query
|
||||||
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
|
// 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))
|
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 {
|
for _, n := range nl {
|
||||||
notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
|
notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = notif.LoadAttributes(ctx)
|
n.Status = notif.Status
|
||||||
changed = append(changed, convert.ToNotificationThread(ctx, notif))
|
n.UpdatedUnix = notif.UpdatedUnix
|
||||||
|
changed = append(changed, convert.ToNotificationThread(ctx, n))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusResetContent, changed)
|
ctx.JSON(http.StatusResetContent, changed)
|
||||||
|
@ -812,7 +812,8 @@ func GetContentsExt(ctx *context.APIContext) {
|
|||||||
// required: true
|
// required: true
|
||||||
// - name: filepath
|
// - name: filepath
|
||||||
// in: path
|
// in: path
|
||||||
// description: path of the dir, file, symlink or submodule in the repo
|
// description: path of the dir, file, symlink or submodule in the repo. Swagger requires path parameter to be "required",
|
||||||
|
// you can leave it empty or pass a single dot (".") to get the root directory.
|
||||||
// type: string
|
// type: string
|
||||||
// required: true
|
// required: true
|
||||||
// - name: ref
|
// - name: ref
|
||||||
@ -823,7 +824,8 @@ func GetContentsExt(ctx *context.APIContext) {
|
|||||||
// - name: includes
|
// - name: includes
|
||||||
// in: query
|
// in: query
|
||||||
// description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields.
|
// description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields.
|
||||||
// Option "file_content" will try to retrieve the file content, option "lfs_metadata" will try to retrieve LFS metadata.
|
// Option "file_content" will try to retrieve the file content, "lfs_metadata" will try to retrieve LFS metadata,
|
||||||
|
// "commit_metadata" will try to retrieve commit metadata, and "commit_message" will try to retrieve commit message.
|
||||||
// type: string
|
// type: string
|
||||||
// required: false
|
// required: false
|
||||||
// responses:
|
// responses:
|
||||||
@ -832,6 +834,9 @@ func GetContentsExt(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
if treePath := ctx.PathParam("*"); treePath == "." || treePath == "/" {
|
||||||
|
ctx.SetPathParam("*", "") // workaround for swagger, it requires path parameter to be "required", but we need to list root directory
|
||||||
|
}
|
||||||
opts := files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*")}
|
opts := files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*")}
|
||||||
for includeOpt := range strings.SplitSeq(ctx.FormString("includes"), ",") {
|
for includeOpt := range strings.SplitSeq(ctx.FormString("includes"), ",") {
|
||||||
if includeOpt == "" {
|
if includeOpt == "" {
|
||||||
@ -842,6 +847,10 @@ func GetContentsExt(ctx *context.APIContext) {
|
|||||||
opts.IncludeSingleFileContent = true
|
opts.IncludeSingleFileContent = true
|
||||||
case "lfs_metadata":
|
case "lfs_metadata":
|
||||||
opts.IncludeLfsMetadata = true
|
opts.IncludeLfsMetadata = true
|
||||||
|
case "commit_metadata":
|
||||||
|
opts.IncludeCommitMetadata = true
|
||||||
|
case "commit_message":
|
||||||
|
opts.IncludeCommitMessage = true
|
||||||
default:
|
default:
|
||||||
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("unknown include option %q", includeOpt))
|
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("unknown include option %q", includeOpt))
|
||||||
return
|
return
|
||||||
@ -883,7 +892,11 @@ func GetContents(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/ContentsResponse"
|
// "$ref": "#/responses/ContentsResponse"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*"), IncludeSingleFileContent: true})
|
ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{
|
||||||
|
TreePath: ctx.PathParam("*"),
|
||||||
|
IncludeSingleFileContent: true,
|
||||||
|
IncludeCommitMetadata: true,
|
||||||
|
})
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
@ -301,6 +302,14 @@ func Diff(ctx *context.Context) {
|
|||||||
commitID = commit.ID.String()
|
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("SetReleaseReadBy", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileOnly := ctx.FormBool("file-only")
|
fileOnly := ctx.FormBool("file-only")
|
||||||
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
|
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
|
||||||
files := ctx.FormStrings("files")
|
files := ctx.FormStrings("files")
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
"code.gitea.io/gitea/models/renderhelper"
|
"code.gitea.io/gitea/models/renderhelper"
|
||||||
@ -298,6 +299,14 @@ func SingleRelease(ctx *context.Context) {
|
|||||||
release.Title = release.TagName
|
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["PageIsSingleTag"] = release.IsTag
|
||||||
ctx.Data["SingleReleaseTagName"] = release.TagName
|
ctx.Data["SingleReleaseTagName"] = release.TagName
|
||||||
if release.IsTag {
|
if release.IsTag {
|
||||||
|
@ -137,6 +137,22 @@ func getNotifications(ctx *context.Context) {
|
|||||||
notifications = notifications.Without(failures)
|
notifications = notifications.Without(failures)
|
||||||
failCount += len(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 {
|
if failCount > 0 {
|
||||||
ctx.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
|
ctx.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ package convert
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"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)
|
url := n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID)
|
||||||
result.Subject = &api.NotificationSubject{
|
result.Subject = &api.NotificationSubject{
|
||||||
Type: api.NotifySubjectCommit,
|
Type: api.NotifySubjectCommit,
|
||||||
Title: n.CommitID,
|
Title: strings.TrimSpace(n.Commit.CommitMessage),
|
||||||
URL: url,
|
URL: url,
|
||||||
HTMLURL: url,
|
HTMLURL: url,
|
||||||
}
|
}
|
||||||
@ -83,6 +84,13 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification)
|
|||||||
URL: n.Repository.Link(),
|
URL: n.Repository.Link(),
|
||||||
HTMLURL: n.Repository.HTMLURL(),
|
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
|
return result
|
||||||
|
@ -58,10 +58,6 @@ func renderRepoIssueIconTitle(ctx context.Context, opts markup.RenderIssueIconTi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlIcon, err := webCtx.RenderToHTML("shared/issueicon", issue)
|
return htmlutil.HTMLFormat(`<a href="%s">%s %s %s</a>`, opts.LinkHref,
|
||||||
if err != nil {
|
issue.IconHTML(ctx), issue.Title, textIssueIndex), nil
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return htmlutil.HTMLFormat(`<a href="%s">%s %s %s</a>`, opts.LinkHref, htmlIcon, issue.Title, textIssueIndex), nil
|
|
||||||
}
|
}
|
||||||
|
@ -46,10 +46,25 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldSendCommentChangeNotification(ctx context.Context, comment *issues_model.Comment) bool {
|
||||||
|
if err := comment.LoadReview(ctx); err != nil {
|
||||||
|
log.Error("LoadReview: %v", err)
|
||||||
|
return false
|
||||||
|
} else if comment.Review != nil && comment.Review.Type == issues_model.ReviewTypePending {
|
||||||
|
// Pending review comments updating should not triggered
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// CreateIssueComment notifies issue comment related message to notifiers
|
// CreateIssueComment notifies issue comment related message to notifiers
|
||||||
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
|
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
|
||||||
issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
|
issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
|
||||||
) {
|
) {
|
||||||
|
if !shouldSendCommentChangeNotification(ctx, comment) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, notifier := range notifiers {
|
for _, notifier := range notifiers {
|
||||||
notifier.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
|
notifier.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
|
||||||
}
|
}
|
||||||
@ -156,6 +171,10 @@ func PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issue
|
|||||||
|
|
||||||
// UpdateComment notifies update comment to notifiers
|
// UpdateComment notifies update comment to notifiers
|
||||||
func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
|
func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
|
||||||
|
if !shouldSendCommentChangeNotification(ctx, c) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, notifier := range notifiers {
|
for _, notifier := range notifiers {
|
||||||
notifier.UpdateComment(ctx, doer, c, oldContent)
|
notifier.UpdateComment(ctx, doer, c, oldContent)
|
||||||
}
|
}
|
||||||
@ -163,6 +182,10 @@ func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.C
|
|||||||
|
|
||||||
// DeleteComment notifies delete comment to notifiers
|
// DeleteComment notifies delete comment to notifiers
|
||||||
func DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) {
|
func DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) {
|
||||||
|
if !shouldSendCommentChangeNotification(ctx, c) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, notifier := range notifiers {
|
for _, notifier := range notifiers {
|
||||||
notifier.DeleteComment(ctx, doer, c)
|
notifier.DeleteComment(ctx, doer, c)
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,8 @@ type GetContentsOrListOptions struct {
|
|||||||
TreePath string
|
TreePath string
|
||||||
IncludeSingleFileContent bool // include the file's content when the tree path is a file
|
IncludeSingleFileContent bool // include the file's content when the tree path is a file
|
||||||
IncludeLfsMetadata bool
|
IncludeLfsMetadata bool
|
||||||
|
IncludeCommitMetadata bool
|
||||||
|
IncludeCommitMessage bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContentsOrList gets the metadata of a file's contents (*ContentsResponse) if treePath not a tree
|
// GetContentsOrList gets the metadata of a file's contents (*ContentsResponse) if treePath not a tree
|
||||||
@ -132,6 +134,19 @@ func getFileContentsByEntryInternal(_ context.Context, repo *repo_model.Reposito
|
|||||||
}
|
}
|
||||||
selfURLString := selfURL.String()
|
selfURLString := selfURL.String()
|
||||||
|
|
||||||
|
// All content types have these fields in populated
|
||||||
|
contentsResponse := &api.ContentsResponse{
|
||||||
|
Name: entry.Name(),
|
||||||
|
Path: opts.TreePath,
|
||||||
|
SHA: entry.ID.String(),
|
||||||
|
Size: entry.Size(),
|
||||||
|
URL: &selfURLString,
|
||||||
|
Links: &api.FileLinksResponse{
|
||||||
|
Self: &selfURLString,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.IncludeCommitMetadata || opts.IncludeCommitMessage {
|
||||||
err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(refCommit.InputRef, refType != git.RefTypeCommit), repo.FullName(), refCommit.CommitID)
|
err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(refCommit.InputRef, refType != git.RefTypeCommit), repo.FullName(), refCommit.CommitID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -142,29 +157,23 @@ func getFileContentsByEntryInternal(_ context.Context, repo *repo_model.Reposito
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// All content types have these fields in populated
|
if opts.IncludeCommitMetadata {
|
||||||
contentsResponse := &api.ContentsResponse{
|
contentsResponse.LastCommitSHA = util.ToPointer(lastCommit.ID.String())
|
||||||
Name: entry.Name(),
|
|
||||||
Path: opts.TreePath,
|
|
||||||
SHA: entry.ID.String(),
|
|
||||||
LastCommitSHA: lastCommit.ID.String(),
|
|
||||||
Size: entry.Size(),
|
|
||||||
URL: &selfURLString,
|
|
||||||
Links: &api.FileLinksResponse{
|
|
||||||
Self: &selfURLString,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// GitHub doesn't have these fields in the response, but we could follow other similar APIs to name them
|
// GitHub doesn't have these fields in the response, but we could follow other similar APIs to name them
|
||||||
// https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits
|
// https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits
|
||||||
if lastCommit.Committer != nil {
|
if lastCommit.Committer != nil {
|
||||||
contentsResponse.LastCommitterDate = lastCommit.Committer.When
|
contentsResponse.LastCommitterDate = util.ToPointer(lastCommit.Committer.When)
|
||||||
}
|
}
|
||||||
if lastCommit.Author != nil {
|
if lastCommit.Author != nil {
|
||||||
contentsResponse.LastAuthorDate = lastCommit.Author.When
|
contentsResponse.LastAuthorDate = util.ToPointer(lastCommit.Author.When)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.IncludeCommitMessage {
|
||||||
|
contentsResponse.LastCommitMessage = util.ToPointer(lastCommit.Message())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now populate the rest of the ContentsResponse based on entry type
|
// Now populate the rest of the ContentsResponse based on the entry type
|
||||||
if entry.IsRegular() || entry.IsExecutable() {
|
if entry.IsRegular() || entry.IsExecutable() {
|
||||||
contentsResponse.Type = string(ContentTypeRegular)
|
contentsResponse.Type = string(ContentTypeRegular)
|
||||||
// if it is listing the repo root dir, don't waste system resources on reading content
|
// if it is listing the repo root dir, don't waste system resources on reading content
|
||||||
|
@ -5,56 +5,21 @@ package files
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
|
||||||
"code.gitea.io/gitea/services/contexttest"
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
_ "code.gitea.io/gitea/models/actions"
|
_ "code.gitea.io/gitea/models/actions"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
unittest.MainTest(m)
|
unittest.MainTest(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExpectedReadmeContentsResponse() *api.ContentsResponse {
|
|
||||||
treePath := "README.md"
|
|
||||||
sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
|
|
||||||
encoding := "base64"
|
|
||||||
content := "IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"
|
|
||||||
selfURL := "https://try.gitea.io/api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
|
|
||||||
htmlURL := "https://try.gitea.io/user2/repo1/src/branch/master/" + treePath
|
|
||||||
gitURL := "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/" + sha
|
|
||||||
downloadURL := "https://try.gitea.io/user2/repo1/raw/branch/master/" + treePath
|
|
||||||
return &api.ContentsResponse{
|
|
||||||
Name: treePath,
|
|
||||||
Path: treePath,
|
|
||||||
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
|
|
||||||
LastCommitSHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
|
||||||
LastCommitterDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)),
|
|
||||||
LastAuthorDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)),
|
|
||||||
Type: "file",
|
|
||||||
Size: 30,
|
|
||||||
Encoding: &encoding,
|
|
||||||
Content: &content,
|
|
||||||
URL: &selfURL,
|
|
||||||
HTMLURL: &htmlURL,
|
|
||||||
GitURL: &gitURL,
|
|
||||||
DownloadURL: &downloadURL,
|
|
||||||
Links: &api.FileLinksResponse{
|
|
||||||
Self: &selfURL,
|
|
||||||
GitURL: &gitURL,
|
|
||||||
HTMLURL: &htmlURL,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetContents(t *testing.T) {
|
func TestGetContents(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
||||||
@ -63,45 +28,8 @@ func TestGetContents(t *testing.T) {
|
|||||||
contexttest.LoadRepoCommit(t, ctx)
|
contexttest.LoadRepoCommit(t, ctx)
|
||||||
contexttest.LoadUser(t, ctx, 2)
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
contexttest.LoadGitRepo(t, ctx)
|
contexttest.LoadGitRepo(t, ctx)
|
||||||
defer ctx.Repo.GitRepo.Close()
|
|
||||||
repo, gitRepo := ctx.Repo.Repository, ctx.Repo.GitRepo
|
|
||||||
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Run("GetContentsOrList(README.md)-MetaOnly", func(t *testing.T) {
|
// GetContentsOrList's behavior is fully tested in integration tests, so we don't need to test it here.
|
||||||
expectedContentsResponse := getExpectedReadmeContentsResponse()
|
|
||||||
expectedContentsResponse.Encoding = nil // because will be in a list, doesn't have encoding and content
|
|
||||||
expectedContentsResponse.Content = nil
|
|
||||||
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "README.md", IncludeSingleFileContent: false})
|
|
||||||
assert.Equal(t, expectedContentsResponse, extResp.FileContents)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("GetContentsOrList(README.md)", func(t *testing.T) {
|
|
||||||
expectedContentsResponse := getExpectedReadmeContentsResponse()
|
|
||||||
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "README.md", IncludeSingleFileContent: true})
|
|
||||||
assert.Equal(t, expectedContentsResponse, extResp.FileContents)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("GetContentsOrList(RootDir)", func(t *testing.T) {
|
|
||||||
readmeContentsResponse := getExpectedReadmeContentsResponse()
|
|
||||||
readmeContentsResponse.Encoding = nil // because will be in a list, doesn't have encoding and content
|
|
||||||
readmeContentsResponse.Content = nil
|
|
||||||
expectedContentsListResponse := []*api.ContentsResponse{readmeContentsResponse}
|
|
||||||
// even if IncludeFileContent is true, it has no effect for directory listing
|
|
||||||
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "", IncludeSingleFileContent: true})
|
|
||||||
assert.Equal(t, expectedContentsListResponse, extResp.DirContents)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("GetContentsOrList(NoSuchTreePath)", func(t *testing.T) {
|
|
||||||
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "no-such/file.md"})
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.EqualError(t, err, "object does not exist [id: , rel_path: no-such]")
|
|
||||||
assert.Nil(t, extResp.DirContents)
|
|
||||||
assert.Nil(t, extResp.FileContents)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("GetBlobBySHA", func(t *testing.T) {
|
t.Run("GetBlobBySHA", func(t *testing.T) {
|
||||||
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
|
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
|
||||||
|
@ -22,7 +22,12 @@ import (
|
|||||||
func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treePaths []string) (files []*api.ContentsResponse) {
|
func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treePaths []string) (files []*api.ContentsResponse) {
|
||||||
var size int64
|
var size int64
|
||||||
for _, treePath := range treePaths {
|
for _, treePath := range treePaths {
|
||||||
fileContents, _ := GetFileContents(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: treePath, IncludeSingleFileContent: true}) // ok if fails, then will be nil
|
// ok if fails, then will be nil
|
||||||
|
fileContents, _ := GetFileContents(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{
|
||||||
|
TreePath: treePath,
|
||||||
|
IncludeSingleFileContent: true,
|
||||||
|
IncludeCommitMetadata: true,
|
||||||
|
})
|
||||||
if fileContents != nil && fileContents.Content != nil && *fileContents.Content != "" {
|
if fileContents != nil && fileContents.Content != nil && *fileContents.Content != "" {
|
||||||
// if content isn't empty (e.g., due to the single blob being too large), add file size to response size
|
// if content isn't empty (e.g., due to the single blob being too large), add file size to response size
|
||||||
size += int64(len(*fileContents.Content))
|
size += int64(len(*fileContents.Content))
|
||||||
|
@ -5,28 +5,39 @@ package uinotification
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
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"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/graceful"
|
"code.gitea.io/gitea/modules/graceful"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/queue"
|
"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"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
notificationService struct {
|
notificationService struct {
|
||||||
notify_service.NullNotifier
|
notify_service.NullNotifier
|
||||||
issueQueue *queue.WorkerPoolQueue[issueNotificationOpts]
|
queue *queue.WorkerPoolQueue[notificationOpts]
|
||||||
}
|
}
|
||||||
|
|
||||||
issueNotificationOpts struct {
|
notificationOpts struct {
|
||||||
|
Source activities_model.NotificationSource
|
||||||
IssueID int64
|
IssueID int64
|
||||||
CommentID int64
|
CommentID int64
|
||||||
|
CommitID string // commit ID for commit notifications
|
||||||
|
RepoID int64
|
||||||
|
ReleaseID int64
|
||||||
NotificationAuthorID int64
|
NotificationAuthorID int64
|
||||||
ReceiverID int64 // 0 -- ALL Watcher
|
ReceiverID int64 // 0 -- ALL Watcher
|
||||||
}
|
}
|
||||||
@ -43,66 +54,79 @@ var _ notify_service.Notifier = ¬ificationService{}
|
|||||||
// NewNotifier create a new notificationService notifier
|
// NewNotifier create a new notificationService notifier
|
||||||
func NewNotifier() notify_service.Notifier {
|
func NewNotifier() notify_service.Notifier {
|
||||||
ns := ¬ificationService{}
|
ns := ¬ificationService{}
|
||||||
ns.issueQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "notification-service", handler)
|
ns.queue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "notification-service", handler)
|
||||||
if ns.issueQueue == nil {
|
if ns.queue == nil {
|
||||||
log.Fatal("Unable to create notification-service queue")
|
log.Fatal("Unable to create notification-service queue")
|
||||||
}
|
}
|
||||||
return ns
|
return ns
|
||||||
}
|
}
|
||||||
|
|
||||||
func handler(items ...issueNotificationOpts) []issueNotificationOpts {
|
func handler(items ...notificationOpts) []notificationOpts {
|
||||||
for _, opts := range items {
|
for _, opts := range items {
|
||||||
|
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 {
|
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)
|
log.Error("Was unable to create issue notification: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *notificationService) Run() {
|
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,
|
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,
|
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,
|
IssueID: issue.ID,
|
||||||
|
RepoID: issue.RepoID,
|
||||||
NotificationAuthorID: doer.ID,
|
NotificationAuthorID: doer.ID,
|
||||||
}
|
}
|
||||||
if comment != nil {
|
if comment != nil {
|
||||||
opts.CommentID = comment.ID
|
opts.CommentID = comment.ID
|
||||||
}
|
}
|
||||||
_ = ns.issueQueue.Push(opts)
|
_ = ns.queue.Push(opts)
|
||||||
for _, mention := range mentions {
|
for _, mention := range mentions {
|
||||||
opts := issueNotificationOpts{
|
opts.ReceiverID = mention.ID
|
||||||
IssueID: issue.ID,
|
_ = ns.queue.Push(opts)
|
||||||
NotificationAuthorID: doer.ID,
|
|
||||||
ReceiverID: mention.ID,
|
|
||||||
}
|
|
||||||
if comment != nil {
|
|
||||||
opts.CommentID = comment.ID
|
|
||||||
}
|
|
||||||
_ = ns.issueQueue.Push(opts)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *notificationService) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
|
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,
|
IssueID: issue.ID,
|
||||||
NotificationAuthorID: issue.Poster.ID,
|
NotificationAuthorID: issue.Poster.ID,
|
||||||
})
|
}
|
||||||
|
_ = ns.queue.Push(opts)
|
||||||
for _, mention := range mentions {
|
for _, mention := range mentions {
|
||||||
_ = ns.issueQueue.Push(issueNotificationOpts{
|
opts.ReceiverID = mention.ID
|
||||||
IssueID: issue.ID,
|
_ = ns.queue.Push(opts)
|
||||||
NotificationAuthorID: issue.Poster.ID,
|
|
||||||
ReceiverID: mention.ID,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *notificationService) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) {
|
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,
|
IssueID: issue.ID,
|
||||||
NotificationAuthorID: doer.ID,
|
NotificationAuthorID: doer.ID,
|
||||||
CommentID: actionComment.ID,
|
CommentID: actionComment.ID,
|
||||||
@ -115,7 +139,8 @@ func (ns *notificationService) IssueChangeTitle(ctx context.Context, doer *user_
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress(ctx) {
|
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,
|
IssueID: issue.ID,
|
||||||
NotificationAuthorID: doer.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) {
|
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,
|
IssueID: pr.Issue.ID,
|
||||||
NotificationAuthorID: doer.ID,
|
NotificationAuthorID: doer.ID,
|
||||||
})
|
})
|
||||||
@ -160,7 +186,8 @@ func (ns *notificationService) NewPullRequest(ctx context.Context, pr *issues_mo
|
|||||||
toNotify.Add(mention.ID)
|
toNotify.Add(mention.ID)
|
||||||
}
|
}
|
||||||
for receiverID := range toNotify {
|
for receiverID := range toNotify {
|
||||||
_ = ns.issueQueue.Push(issueNotificationOpts{
|
_ = ns.queue.Push(notificationOpts{
|
||||||
|
Source: activities_model.NotificationSourcePullRequest,
|
||||||
IssueID: pr.Issue.ID,
|
IssueID: pr.Issue.ID,
|
||||||
NotificationAuthorID: pr.Issue.PosterID,
|
NotificationAuthorID: pr.Issue.PosterID,
|
||||||
ReceiverID: receiverID,
|
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) {
|
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,
|
IssueID: pr.Issue.ID,
|
||||||
NotificationAuthorID: r.Reviewer.ID,
|
NotificationAuthorID: r.Reviewer.ID,
|
||||||
}
|
}
|
||||||
if c != nil {
|
if c != nil {
|
||||||
opts.CommentID = c.ID
|
opts.CommentID = c.ID
|
||||||
}
|
}
|
||||||
_ = ns.issueQueue.Push(opts)
|
_ = ns.queue.Push(opts)
|
||||||
for _, mention := range mentions {
|
for _, mention := range mentions {
|
||||||
opts := issueNotificationOpts{
|
opts.ReceiverID = mention.ID
|
||||||
IssueID: pr.Issue.ID,
|
_ = ns.queue.Push(opts)
|
||||||
NotificationAuthorID: r.Reviewer.ID,
|
|
||||||
ReceiverID: mention.ID,
|
|
||||||
}
|
|
||||||
if c != nil {
|
|
||||||
opts.CommentID = c.ID
|
|
||||||
}
|
|
||||||
_ = ns.issueQueue.Push(opts)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *notificationService) PullRequestCodeComment(ctx context.Context, pr *issues_model.PullRequest, c *issues_model.Comment, mentions []*user_model.User) {
|
func (ns *notificationService) PullRequestCodeComment(ctx context.Context, pr *issues_model.PullRequest, c *issues_model.Comment, mentions []*user_model.User) {
|
||||||
for _, mention := range mentions {
|
for _, mention := range mentions {
|
||||||
_ = ns.issueQueue.Push(issueNotificationOpts{
|
_ = ns.queue.Push(notificationOpts{
|
||||||
|
Source: activities_model.NotificationSourcePullRequest,
|
||||||
IssueID: pr.Issue.ID,
|
IssueID: pr.Issue.ID,
|
||||||
NotificationAuthorID: c.Poster.ID,
|
NotificationAuthorID: c.Poster.ID,
|
||||||
CommentID: c.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) {
|
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,
|
IssueID: pr.IssueID,
|
||||||
NotificationAuthorID: doer.ID,
|
NotificationAuthorID: doer.ID,
|
||||||
CommentID: comment.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) {
|
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,
|
IssueID: review.IssueID,
|
||||||
NotificationAuthorID: doer.ID,
|
NotificationAuthorID: doer.ID,
|
||||||
CommentID: comment.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) {
|
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 {
|
if !removed && doer.ID != assignee.ID {
|
||||||
opts := issueNotificationOpts{
|
opts := notificationOpts{
|
||||||
|
Source: activities_model.NotificationSourceIssue,
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
NotificationAuthorID: doer.ID,
|
NotificationAuthorID: doer.ID,
|
||||||
ReceiverID: assignee.ID,
|
ReceiverID: assignee.ID,
|
||||||
@ -231,13 +256,14 @@ func (ns *notificationService) IssueChangeAssignee(ctx context.Context, doer *us
|
|||||||
opts.CommentID = comment.ID
|
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) {
|
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 {
|
if isRequest {
|
||||||
opts := issueNotificationOpts{
|
opts := notificationOpts{
|
||||||
|
Source: activities_model.NotificationSourcePullRequest,
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
NotificationAuthorID: doer.ID,
|
NotificationAuthorID: doer.ID,
|
||||||
ReceiverID: reviewer.ID,
|
ReceiverID: reviewer.ID,
|
||||||
@ -247,15 +273,117 @@ func (ns *notificationService) PullRequestReviewRequest(ctx context.Context, doe
|
|||||||
opts.CommentID = comment.ID
|
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) {
|
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 {
|
opts := notificationOpts{
|
||||||
return activities_model.CreateRepoTransferNotification(ctx, doer, newOwner, repo)
|
Source: activities_model.NotificationSourceRepository,
|
||||||
})
|
RepoID: repo.ID,
|
||||||
|
NotificationAuthorID: doer.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if newOwner.IsOrganization() {
|
||||||
|
users, err := organization.GetUsersWhoCanCreateOrgRepo(ctx, newOwner.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("CreateRepoTransferNotification: %v", err)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ export default {
|
|||||||
important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles
|
important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles
|
||||||
content: [
|
content: [
|
||||||
isProduction && '!./templates/devtest/**/*',
|
isProduction && '!./templates/devtest/**/*',
|
||||||
isProduction && '!./web_src/js/standalone/devtest.js',
|
isProduction && '!./web_src/js/standalone/devtest.ts',
|
||||||
'!./templates/swagger/v1_json.tmpl',
|
'!./templates/swagger/v1_json.tmpl',
|
||||||
'!./templates/user/auth/oidc_wellknown.tmpl',
|
'!./templates/user/auth/oidc_wellknown.tmpl',
|
||||||
'!**/*_test.go',
|
'!**/*_test.go',
|
||||||
|
@ -187,7 +187,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{if .HasPullRequest}}
|
{{if .HasPullRequest}}
|
||||||
<div class="ui segment flex-text-block tw-gap-4">
|
<div class="ui segment flex-text-block tw-gap-4">
|
||||||
{{template "shared/issueicon" .}}
|
{{.IconHTML ctx}}
|
||||||
<div class="issue-title tw-break-anywhere">
|
<div class="issue-title tw-break-anywhere">
|
||||||
{{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title $.Repository}}
|
{{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title $.Repository}}
|
||||||
<span class="index">#{{.PullRequest.Issue.Index}}</span>
|
<span class="index">#{{.PullRequest.Issue.Index}}</span>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<div class="content tw-w-full">
|
<div class="content tw-w-full">
|
||||||
<div class="tw-flex tw-items-start tw-gap-[5px]">
|
<div class="tw-flex tw-items-start tw-gap-[5px]">
|
||||||
<div class="issue-card-icon">
|
<div class="issue-card-icon">
|
||||||
{{template "shared/issueicon" .}}
|
{{.IconHTML ctx}}
|
||||||
</div>
|
</div>
|
||||||
<a class="issue-card-title muted issue-title tw-break-anywhere" href="{{.Link}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
|
<a class="issue-card-title muted issue-title tw-break-anywhere" href="{{.Link}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
|
||||||
{{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}}
|
{{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}}
|
||||||
|
@ -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 -}}
|
|
@ -9,7 +9,7 @@
|
|||||||
{{if $.CanWriteIssuesOrPulls}}
|
{{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"}} "{{.Title}}"">
|
<input type="checkbox" autocomplete="off" class="issue-checkbox tw-mr-[14px]" data-issue-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} "{{.Title}}"">
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "shared/issueicon" .}}
|
{{.IconHTML ctx}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
14
templates/swagger/v1_json.tmpl
generated
14
templates/swagger/v1_json.tmpl
generated
@ -1523,7 +1523,8 @@
|
|||||||
"issue",
|
"issue",
|
||||||
"pull",
|
"pull",
|
||||||
"commit",
|
"commit",
|
||||||
"repository"
|
"repository",
|
||||||
|
"release"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -7547,7 +7548,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "path of the dir, file, symlink or submodule in the repo",
|
"description": "path of the dir, file, symlink or submodule in the repo. Swagger requires path parameter to be \"required\", you can leave it empty or pass a single dot (\".\") to get the root directory.",
|
||||||
"name": "filepath",
|
"name": "filepath",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
@ -7560,7 +7561,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "By default this API's response only contains file's metadata. Use comma-separated \"includes\" options to retrieve more fields. Option \"file_content\" will try to retrieve the file content, option \"lfs_metadata\" will try to retrieve LFS metadata.",
|
"description": "By default this API's response only contains file's metadata. Use comma-separated \"includes\" options to retrieve more fields. Option \"file_content\" will try to retrieve the file content, \"lfs_metadata\" will try to retrieve LFS metadata, \"commit_metadata\" will try to retrieve commit metadata, and \"commit_message\" will try to retrieve commit message.",
|
||||||
"name": "includes",
|
"name": "includes",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
@ -13305,7 +13306,8 @@
|
|||||||
"issue",
|
"issue",
|
||||||
"pull",
|
"pull",
|
||||||
"commit",
|
"commit",
|
||||||
"repository"
|
"repository",
|
||||||
|
"release"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -22368,6 +22370,10 @@
|
|||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"x-go-name": "LastAuthorDate"
|
"x-go-name": "LastAuthorDate"
|
||||||
},
|
},
|
||||||
|
"last_commit_message": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "LastCommitMessage"
|
||||||
|
},
|
||||||
"last_commit_sha": {
|
"last_commit_sha": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "LastCommitSHA"
|
"x-go-name": "LastCommitSHA"
|
||||||
|
@ -37,11 +37,7 @@
|
|||||||
{{range $notification := .Notifications}}
|
{{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-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">
|
<div class="notifications-icon tw-ml-2 tw-mr-1 tw-self-start tw-mt-1">
|
||||||
{{if .Issue}}
|
{{.IconHTML ctx}}
|
||||||
{{template "shared/issueicon" .Issue}}
|
|
||||||
{{else}}
|
|
||||||
{{svg "octicon-repo" 16 "text grey"}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
<a class="notifications-link tw-flex tw-flex-1 tw-flex-col silenced" href="{{.Link ctx}}">
|
<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">
|
<div class="notifications-top-row tw-text-13 tw-break-anywhere">
|
||||||
@ -54,6 +50,10 @@
|
|||||||
<span class="issue-title tw-break-anywhere">
|
<span class="issue-title tw-break-anywhere">
|
||||||
{{if .Issue}}
|
{{if .Issue}}
|
||||||
{{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}
|
{{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}
|
||||||
|
{{else if .Release}}
|
||||||
|
{{.Release.Title}}
|
||||||
|
{{else if .Commit}}
|
||||||
|
{{.Commit.Summary}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{.Repository.FullName}}
|
{{.Repository.FullName}}
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -63,6 +63,10 @@
|
|||||||
<div class="notifications-updated tw-items-center tw-mr-2">
|
<div class="notifications-updated tw-items-center tw-mr-2">
|
||||||
{{if .Issue}}
|
{{if .Issue}}
|
||||||
{{DateUtils.TimeSince .Issue.UpdatedUnix}}
|
{{DateUtils.TimeSince .Issue.UpdatedUnix}}
|
||||||
|
{{else if .Release}}
|
||||||
|
{{DateUtils.TimeSince .Release.CreatedUnix}}
|
||||||
|
{{else if .Commit}}
|
||||||
|
{{DateUtils.TimeSince .Commit.Committer.When}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{DateUtils.TimeSince .UpdatedUnix}}
|
{{DateUtils.TimeSince .UpdatedUnix}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -4,9 +4,12 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
@ -15,6 +18,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -213,3 +217,137 @@ func TestAPINotificationPUT(t *testing.T) {
|
|||||||
assert.True(t, apiNL[0].Unread)
|
assert.True(t, apiNL[0].Unread)
|
||||||
assert.False(t, apiNL[0].Pinned)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -52,8 +53,8 @@ func getCreateFileOptions() api.CreateFileOptions {
|
|||||||
func normalizeFileContentResponseCommitTime(c *api.ContentsResponse) {
|
func normalizeFileContentResponseCommitTime(c *api.ContentsResponse) {
|
||||||
// decoded JSON response may contain different timezone from the one parsed by git commit
|
// decoded JSON response may contain different timezone from the one parsed by git commit
|
||||||
// so we need to normalize the time to UTC to make "assert.Equal" pass
|
// so we need to normalize the time to UTC to make "assert.Equal" pass
|
||||||
c.LastCommitterDate = c.LastCommitterDate.UTC()
|
c.LastCommitterDate = util.ToPointer(c.LastCommitterDate.UTC())
|
||||||
c.LastAuthorDate = c.LastAuthorDate.UTC()
|
c.LastAuthorDate = util.ToPointer(c.LastAuthorDate.UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiFileResponseInfo struct {
|
type apiFileResponseInfo struct {
|
||||||
@ -74,9 +75,9 @@ func getExpectedFileResponseForCreate(info apiFileResponseInfo) *api.FileRespons
|
|||||||
Name: path.Base(info.treePath),
|
Name: path.Base(info.treePath),
|
||||||
Path: info.treePath,
|
Path: info.treePath,
|
||||||
SHA: sha,
|
SHA: sha,
|
||||||
LastCommitSHA: info.lastCommitSHA,
|
LastCommitSHA: util.ToPointer(info.lastCommitSHA),
|
||||||
LastCommitterDate: info.lastCommitterWhen,
|
LastCommitterDate: util.ToPointer(info.lastCommitterWhen),
|
||||||
LastAuthorDate: info.lastAuthorWhen,
|
LastAuthorDate: util.ToPointer(info.lastAuthorWhen),
|
||||||
Size: 16,
|
Size: 16,
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Encoding: &encoding,
|
Encoding: &encoding,
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -60,9 +61,9 @@ func getExpectedFileResponseForUpdate(info apiFileResponseInfo) *api.FileRespons
|
|||||||
Name: path.Base(info.treePath),
|
Name: path.Base(info.treePath),
|
||||||
Path: info.treePath,
|
Path: info.treePath,
|
||||||
SHA: sha,
|
SHA: sha,
|
||||||
LastCommitSHA: info.lastCommitSHA,
|
LastCommitSHA: util.ToPointer(info.lastCommitSHA),
|
||||||
LastCommitterDate: info.lastCommitterWhen,
|
LastCommitterDate: util.ToPointer(info.lastCommitterWhen),
|
||||||
LastAuthorDate: info.lastAuthorWhen,
|
LastAuthorDate: util.ToPointer(info.lastAuthorWhen),
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Size: 20,
|
Size: 20,
|
||||||
Encoding: &encoding,
|
Encoding: &encoding,
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -35,9 +36,9 @@ func getExpectedContentsListResponseForContents(ref, refType, lastCommitSHA stri
|
|||||||
Name: path.Base(treePath),
|
Name: path.Base(treePath),
|
||||||
Path: treePath,
|
Path: treePath,
|
||||||
SHA: sha,
|
SHA: sha,
|
||||||
LastCommitSHA: lastCommitSHA,
|
LastCommitSHA: util.ToPointer(lastCommitSHA),
|
||||||
LastCommitterDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)),
|
LastCommitterDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||||
LastAuthorDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)),
|
LastAuthorDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Size: 30,
|
Size: 30,
|
||||||
URL: &selfURL,
|
URL: &selfURL,
|
||||||
@ -65,7 +66,6 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
|
|||||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
|
||||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
|
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
|
||||||
repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
|
repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
|
||||||
treePath := "" // root dir
|
|
||||||
|
|
||||||
// Get user2's token
|
// Get user2's token
|
||||||
session := loginUser(t, user2.Name)
|
session := loginUser(t, user2.Name)
|
||||||
@ -94,7 +94,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
|
|||||||
// ref is default ref
|
// ref is default ref
|
||||||
ref := repo1.DefaultBranch
|
ref := repo1.DefaultBranch
|
||||||
refType := "branch"
|
refType := "branch"
|
||||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents?ref=%s", user2.Name, repo1.Name, ref)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
var contentsListResponse []*api.ContentsResponse
|
var contentsListResponse []*api.ContentsResponse
|
||||||
DecodeJSON(t, resp, &contentsListResponse)
|
DecodeJSON(t, resp, &contentsListResponse)
|
||||||
@ -106,7 +106,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
|
|||||||
|
|
||||||
// No ref
|
// No ref
|
||||||
refType = "branch"
|
refType = "branch"
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", user2.Name, repo1.Name)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &contentsListResponse)
|
DecodeJSON(t, resp, &contentsListResponse)
|
||||||
assert.NotNil(t, contentsListResponse)
|
assert.NotNil(t, contentsListResponse)
|
||||||
@ -117,7 +117,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
|
|||||||
// ref is the branch we created above in setup
|
// ref is the branch we created above in setup
|
||||||
ref = newBranch
|
ref = newBranch
|
||||||
refType = "branch"
|
refType = "branch"
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents?ref=%s", user2.Name, repo1.Name, ref)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &contentsListResponse)
|
DecodeJSON(t, resp, &contentsListResponse)
|
||||||
assert.NotNil(t, contentsListResponse)
|
assert.NotNil(t, contentsListResponse)
|
||||||
@ -131,7 +131,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
|
|||||||
// ref is the new tag we created above in setup
|
// ref is the new tag we created above in setup
|
||||||
ref = newTag
|
ref = newTag
|
||||||
refType = "tag"
|
refType = "tag"
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/?ref=%s", user2.Name, repo1.Name, ref)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &contentsListResponse)
|
DecodeJSON(t, resp, &contentsListResponse)
|
||||||
assert.NotNil(t, contentsListResponse)
|
assert.NotNil(t, contentsListResponse)
|
||||||
@ -145,7 +145,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
|
|||||||
// ref is a commit
|
// ref is a commit
|
||||||
ref = commitID
|
ref = commitID
|
||||||
refType = "commit"
|
refType = "commit"
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/?ref=%s", user2.Name, repo1.Name, ref)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &contentsListResponse)
|
DecodeJSON(t, resp, &contentsListResponse)
|
||||||
assert.NotNil(t, contentsListResponse)
|
assert.NotNil(t, contentsListResponse)
|
||||||
@ -154,21 +154,21 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
|
|||||||
|
|
||||||
// Test file contents a file with a bad ref
|
// Test file contents a file with a bad ref
|
||||||
ref = "badref"
|
ref = "badref"
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/?ref=%s", user2.Name, repo1.Name, ref)
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
// Test accessing private ref with user token that does not have access - should fail
|
// Test accessing private ref with user token that does not have access - should fail
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath).
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", user2.Name, repo16.Name).
|
||||||
AddTokenAuth(token4)
|
AddTokenAuth(token4)
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
// Test access private ref of owner of token
|
// Test access private ref of owner of token
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md", user2.Name, repo16.Name).
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", user2.Name, repo16.Name).
|
||||||
AddTokenAuth(token2)
|
AddTokenAuth(token2)
|
||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
// Test access of org org3 private repo file by owner user2
|
// Test access of org org3 private repo file by owner user2
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath).
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", org3.Name, repo3.Name).
|
||||||
AddTokenAuth(token2)
|
AddTokenAuth(token2)
|
||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
@ -35,9 +35,9 @@ func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string)
|
|||||||
Name: treePath,
|
Name: treePath,
|
||||||
Path: treePath,
|
Path: treePath,
|
||||||
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
|
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
|
||||||
LastCommitSHA: lastCommitSHA,
|
LastCommitSHA: util.ToPointer(lastCommitSHA),
|
||||||
LastCommitterDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)),
|
LastCommitterDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||||
LastAuthorDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)),
|
LastAuthorDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Size: 30,
|
Size: 30,
|
||||||
Encoding: util.ToPointer("base64"),
|
Encoding: util.ToPointer("base64"),
|
||||||
@ -97,11 +97,16 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
/*** END SETUP ***/
|
/*** END SETUP ***/
|
||||||
|
|
||||||
|
// not found
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/no-such/file.md", user2.Name, repo1.Name)
|
||||||
|
resp := MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
assert.Contains(t, resp.Body.String(), "object does not exist [id: , rel_path: no-such]")
|
||||||
|
|
||||||
// ref is default ref
|
// ref is default ref
|
||||||
ref := repo1.DefaultBranch
|
ref := repo1.DefaultBranch
|
||||||
refType := "branch"
|
refType := "branch"
|
||||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
var contentsResponse api.ContentsResponse
|
var contentsResponse api.ContentsResponse
|
||||||
DecodeJSON(t, resp, &contentsResponse)
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
lastCommit, _ := gitRepo.GetCommitByPath("README.md")
|
lastCommit, _ := gitRepo.GetCommitByPath("README.md")
|
||||||
@ -206,14 +211,30 @@ func testAPIGetContentsExt(t *testing.T) {
|
|||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
t.Run("DirContents", func(t *testing.T) {
|
t.Run("DirContents", func(t *testing.T) {
|
||||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs?ref=sub-home-md-img-check")
|
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext?ref=sub-home-md-img-check")
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
var contentsResponse api.ContentsExtResponse
|
var contentsResponse api.ContentsExtResponse
|
||||||
DecodeJSON(t, resp, &contentsResponse)
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
assert.Nil(t, contentsResponse.FileContents)
|
assert.Nil(t, contentsResponse.FileContents)
|
||||||
|
assert.NotNil(t, contentsResponse.DirContents)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/.?ref=sub-home-md-img-check")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
contentsResponse = api.ContentsExtResponse{}
|
||||||
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents)
|
||||||
|
assert.NotNil(t, contentsResponse.DirContents)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs?ref=sub-home-md-img-check")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
contentsResponse = api.ContentsExtResponse{}
|
||||||
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents)
|
||||||
assert.Equal(t, "README.md", contentsResponse.DirContents[0].Name)
|
assert.Equal(t, "README.md", contentsResponse.DirContents[0].Name)
|
||||||
assert.Nil(t, contentsResponse.DirContents[0].Encoding)
|
assert.Nil(t, contentsResponse.DirContents[0].Encoding)
|
||||||
assert.Nil(t, contentsResponse.DirContents[0].Content)
|
assert.Nil(t, contentsResponse.DirContents[0].Content)
|
||||||
|
assert.Nil(t, contentsResponse.DirContents[0].LastCommitSHA)
|
||||||
|
assert.Nil(t, contentsResponse.DirContents[0].LastCommitMessage)
|
||||||
|
|
||||||
// "includes=file_content" shouldn't affect directory listing
|
// "includes=file_content" shouldn't affect directory listing
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs?ref=sub-home-md-img-check&includes=file_content")
|
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs?ref=sub-home-md-img-check&includes=file_content")
|
||||||
@ -240,7 +261,7 @@ func testAPIGetContentsExt(t *testing.T) {
|
|||||||
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
|
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
|
||||||
})
|
})
|
||||||
t.Run("FileContents", func(t *testing.T) {
|
t.Run("FileContents", func(t *testing.T) {
|
||||||
// by default, no file content is returned
|
// by default, no file content or commit info is returned
|
||||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs/README.md?ref=sub-home-md-img-check")
|
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs/README.md?ref=sub-home-md-img-check")
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
var contentsResponse api.ContentsExtResponse
|
var contentsResponse api.ContentsExtResponse
|
||||||
@ -249,9 +270,11 @@ func testAPIGetContentsExt(t *testing.T) {
|
|||||||
assert.Equal(t, "README.md", contentsResponse.FileContents.Name)
|
assert.Equal(t, "README.md", contentsResponse.FileContents.Name)
|
||||||
assert.Nil(t, contentsResponse.FileContents.Encoding)
|
assert.Nil(t, contentsResponse.FileContents.Encoding)
|
||||||
assert.Nil(t, contentsResponse.FileContents.Content)
|
assert.Nil(t, contentsResponse.FileContents.Content)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents.LastCommitSHA)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents.LastCommitMessage)
|
||||||
|
|
||||||
// file content is only returned when `includes=file_content`
|
// file content is only returned when `includes=file_content`
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs/README.md?ref=sub-home-md-img-check&includes=file_content")
|
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs/README.md?ref=sub-home-md-img-check&includes=file_content,commit_metadata,commit_message")
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
contentsResponse = api.ContentsExtResponse{}
|
contentsResponse = api.ContentsExtResponse{}
|
||||||
DecodeJSON(t, resp, &contentsResponse)
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
@ -259,6 +282,8 @@ func testAPIGetContentsExt(t *testing.T) {
|
|||||||
assert.Equal(t, "README.md", contentsResponse.FileContents.Name)
|
assert.Equal(t, "README.md", contentsResponse.FileContents.Name)
|
||||||
assert.NotNil(t, contentsResponse.FileContents.Encoding)
|
assert.NotNil(t, contentsResponse.FileContents.Encoding)
|
||||||
assert.NotNil(t, contentsResponse.FileContents.Content)
|
assert.NotNil(t, contentsResponse.FileContents.Content)
|
||||||
|
assert.Equal(t, "4649299398e4d39a5c09eb4f534df6f1e1eb87cc", *contentsResponse.FileContents.LastCommitSHA)
|
||||||
|
assert.Equal(t, "Test how READMEs render images when found in a subfolder\n", *contentsResponse.FileContents.LastCommitMessage)
|
||||||
|
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/user2/lfs/contents-ext/jpeg.jpg?includes=file_content").AddTokenAuth(token2)
|
req = NewRequestf(t, "GET", "/api/v1/repos/user2/lfs/contents-ext/jpeg.jpg?includes=file_content").AddTokenAuth(token2)
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
@ -270,6 +295,8 @@ func testAPIGetContentsExt(t *testing.T) {
|
|||||||
assert.Equal(t, "jpeg.jpg", respFile.Name)
|
assert.Equal(t, "jpeg.jpg", respFile.Name)
|
||||||
assert.NotNil(t, respFile.Encoding)
|
assert.NotNil(t, respFile.Encoding)
|
||||||
assert.NotNil(t, respFile.Content)
|
assert.NotNil(t, respFile.Content)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents.LastCommitSHA)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents.LastCommitMessage)
|
||||||
assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize)
|
assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize)
|
||||||
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
|
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
|
||||||
})
|
})
|
||||||
|
@ -155,9 +155,9 @@ func getExpectedFileResponseForRepoFilesCreate(commitID string, lastCommit *git.
|
|||||||
Name: path.Base(treePath),
|
Name: path.Base(treePath),
|
||||||
Path: treePath,
|
Path: treePath,
|
||||||
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
||||||
LastCommitSHA: lastCommit.ID.String(),
|
LastCommitSHA: util.ToPointer(lastCommit.ID.String()),
|
||||||
LastCommitterDate: lastCommit.Committer.When,
|
LastCommitterDate: util.ToPointer(lastCommit.Committer.When),
|
||||||
LastAuthorDate: lastCommit.Author.When,
|
LastAuthorDate: util.ToPointer(lastCommit.Author.When),
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Size: 18,
|
Size: 18,
|
||||||
Encoding: &encoding,
|
Encoding: &encoding,
|
||||||
@ -198,7 +198,7 @@ func getExpectedFileResponseForRepoFilesCreate(commitID string, lastCommit *git.
|
|||||||
SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Message: "Updates README.md\n",
|
Message: "Creates new/file.txt\n",
|
||||||
Tree: &api.CommitMeta{
|
Tree: &api.CommitMeta{
|
||||||
URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
|
URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
|
||||||
SHA: "f93e3a1a1525fb5b91020git dda86e44810c87a2d7bc",
|
SHA: "f93e3a1a1525fb5b91020git dda86e44810c87a2d7bc",
|
||||||
@ -225,9 +225,9 @@ func getExpectedFileResponseForRepoFilesUpdate(commitID, filename, lastCommitSHA
|
|||||||
Name: filename,
|
Name: filename,
|
||||||
Path: filename,
|
Path: filename,
|
||||||
SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647",
|
SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647",
|
||||||
LastCommitSHA: lastCommitSHA,
|
LastCommitSHA: util.ToPointer(lastCommitSHA),
|
||||||
LastCommitterDate: lastCommitterWhen,
|
LastCommitterDate: util.ToPointer(lastCommitterWhen),
|
||||||
LastAuthorDate: lastAuthorWhen,
|
LastAuthorDate: util.ToPointer(lastAuthorWhen),
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Size: 43,
|
Size: 43,
|
||||||
Encoding: &encoding,
|
Encoding: &encoding,
|
||||||
@ -331,7 +331,7 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str
|
|||||||
Name: detail.filename,
|
Name: detail.filename,
|
||||||
Path: detail.filename,
|
Path: detail.filename,
|
||||||
SHA: detail.sha,
|
SHA: detail.sha,
|
||||||
LastCommitSHA: lastCommitSHA,
|
LastCommitSHA: util.ToPointer(lastCommitSHA),
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Size: detail.size,
|
Size: detail.size,
|
||||||
Encoding: util.ToPointer("base64"),
|
Encoding: util.ToPointer("base64"),
|
||||||
@ -537,7 +537,7 @@ func TestChangeRepoFilesForUpdateWithFileRename(t *testing.T) {
|
|||||||
lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
|
lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
|
||||||
expectedFileResponse := getExpectedFileResponseForRepoFilesUpdateRename(commit.ID.String(), lastCommit.ID.String())
|
expectedFileResponse := getExpectedFileResponseForRepoFilesUpdateRename(commit.ID.String(), lastCommit.ID.String())
|
||||||
for _, file := range filesResponse.Files {
|
for _, file := range filesResponse.Files {
|
||||||
file.LastCommitterDate, file.LastAuthorDate = time.Time{}, time.Time{} // there might be different time in one operation, so we ignore them
|
file.LastCommitterDate, file.LastAuthorDate = nil, nil // there might be different time in one operation, so we ignore them
|
||||||
}
|
}
|
||||||
assert.Len(t, filesResponse.Files, 4)
|
assert.Len(t, filesResponse.Files, 4)
|
||||||
assert.Equal(t, expectedFileResponse.Files, filesResponse.Files)
|
assert.Equal(t, expectedFileResponse.Files, filesResponse.Files)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type {Issue} from '../types.ts';
|
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) {
|
export function getIssueIcon(issue: Issue) {
|
||||||
if (issue.pull_request) {
|
if (issue.pull_request) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user