Compare commits

...

7 Commits

Author SHA1 Message Date
6543
77af0a23c4 Changelog v1.12.3 (#12356)
* Changelog v1.12.3

* better description for 12351

* @techknowlogick suggestions
2020-07-28 16:41:36 -04:00
Richard Mahn
87bfe02b5b Backport to v1.12 for #12341 - Release date fix (#12351)
* Backport for Issue #12341 PR #12343 - Release date fix

* Adds sleep for comparing times

* Fixes imports

* Fixes tests
2020-07-28 14:10:50 -04:00
techknowlogick
9bac656b7d Show 404 page when release not found (#12328) (#12332)
Signed-off-by: a1012112796 <1012112796@qq.com>

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>

Co-authored-by: 赵智超 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2020-07-27 00:39:44 +03:00
silverwind
ad68c9ccb2 Backport emoji fixes to 1.12 (#12327)
* Fix emoji detection in certain cases (#12320)

* Fix emoji detection certain cases

Previous tests weren't complicated enough so there were some situations where emojis were't detected properly. Find the earliest occurance in addition to checking for the longest combination.

Fixes #12312

* ok spell bot

Co-authored-by: Lauris BH <lauris@nix.lv>

* Reduce emoji size (#12317)

* Reduce emoji size

Rendering should now pretty much match GitHub with 1.25em. I verified
that emojis don't increase the line height and removed unecessary size
overrides because now all emojis should appear similar in relation to
the font size.

* fix reaction hover

Co-authored-by: mrsdizzie <info@mrsdizzie.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2020-07-25 12:50:57 -04:00
techknowlogick
8d1cd4d252 Fix double-indirection bug in logging IDs (#12294) (#12308)
This PR fixes a bug in log.NewColoredIDValue() which led to a double
indirection and incorrect IDs being printed out.

Signed-off-by: Andrew Thornton <art27@cantab.net>

Co-authored-by: zeripath <art27@cantab.net>
2020-07-24 02:24:22 +03:00
techknowlogick
64eaa2a942 [ui] Link to pr list page on sidebar when view pr (#12256) (#12263)
Fix #12254

Signed-off-by: a1012112796 <1012112796@qq.com>

Co-authored-by: 赵智超 <1012112796@qq.com>
2020-07-16 11:56:09 -04:00
zeripath
489e9162fc Extend Notifications API and return pinned notifications by default (#12164) (#12232)
Backport #12164

This PR extends the notifications API to allow specific notification statuses to be searched for and to allow setting of notifications to statuses other than read.

By default unread and pinned statuses will be returned when querying for notifications - however pinned statuses will not be marked as read.

Close #12152

Signed-off-by: Andrew Thornton art27@cantab.net
2020-07-13 21:52:05 +01:00
17 changed files with 404 additions and 50 deletions

View File

@@ -4,6 +4,17 @@ This changelog goes through all the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.io). been added to each release, please refer to the [blog](https://blog.gitea.io).
## [1.12.3](https://github.com/go-gitea/gitea/releases/tag/v1.12.3) - 2020-07-28
* BUGFIXES
* Don't change creation date when updating Release (#12343) (#12351)
* Show 404 page when release not found (#12328) (#12332)
* Fix emoji detection in certain cases (#12320) (#12327)
* Reduce emoji size (#12317) (#12327)
* Fix double-indirection bug in logging IDs (#12294) (#12308)
* Link to pull list page on sidebar when view pr (#12256) (#12263)
* Extend Notifications API and return pinned notifications by default (#12164) (#12232)
## [1.12.2](https://github.com/go-gitea/gitea/releases/tag/v1.12.2) - 2020-07-11 ## [1.12.2](https://github.com/go-gitea/gitea/releases/tag/v1.12.2) - 2020-07-11
* BUGFIXES * BUGFIXES

View File

@@ -55,7 +55,7 @@ func TestAPINotification(t *testing.T) {
assert.EqualValues(t, false, apiNL[2].Pinned) assert.EqualValues(t, false, apiNL[2].Pinned)
// -- GET /repos/{owner}/{repo}/notifications -- // -- GET /repos/{owner}/{repo}/notifications --
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?token=%s", user2.Name, repo1.Name, token)) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?status-types=unread&token=%s", user2.Name, repo1.Name, token))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiNL) DecodeJSON(t, resp, &apiNL)
@@ -92,7 +92,7 @@ func TestAPINotification(t *testing.T) {
assert.True(t, new.New > 0) assert.True(t, new.New > 0)
// -- mark notifications as read -- // -- mark notifications as read --
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?status-types=unread&token=%s", token))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiNL) DecodeJSON(t, resp, &apiNL)
assert.Len(t, apiNL, 2) assert.Len(t, apiNL, 2)
@@ -101,7 +101,7 @@ func TestAPINotification(t *testing.T) {
req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token)) req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token))
resp = session.MakeRequest(t, req, http.StatusResetContent) resp = session.MakeRequest(t, req, http.StatusResetContent)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?status-types=unread&token=%s", token))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiNL) DecodeJSON(t, resp, &apiNL)
assert.Len(t, apiNL, 1) assert.Len(t, apiNL, 1)

View File

@@ -59,7 +59,7 @@ func TestEventSourceManagerRun(t *testing.T) {
var apiNL []api.NotificationThread var apiNL []api.NotificationThread
// -- mark notifications as read -- // -- mark notifications as read --
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?status-types=unread&token=%s", token))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiNL) DecodeJSON(t, resp, &apiNL)
@@ -69,7 +69,7 @@ func TestEventSourceManagerRun(t *testing.T) {
req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token)) req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token))
resp = session.MakeRequest(t, req, http.StatusResetContent) resp = session.MakeRequest(t, req, http.StatusResetContent)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s&status-types=unread", token))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiNL) DecodeJSON(t, resp, &apiNL)
assert.Len(t, apiNL, 1) assert.Len(t, apiNL, 1)

View File

@@ -72,7 +72,7 @@ type FindNotificationOptions struct {
UserID int64 UserID int64
RepoID int64 RepoID int64
IssueID int64 IssueID int64
Status NotificationStatus Status []NotificationStatus
UpdatedAfterUnix int64 UpdatedAfterUnix int64
UpdatedBeforeUnix int64 UpdatedBeforeUnix int64
} }
@@ -89,8 +89,8 @@ func (opts *FindNotificationOptions) ToCond() 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.Status != 0 { if len(opts.Status) > 0 {
cond = cond.And(builder.Eq{"notification.status": opts.Status}) cond = cond.And(builder.In("notification.status", opts.Status))
} }
if opts.UpdatedAfterUnix != 0 { if opts.UpdatedAfterUnix != 0 {
cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix}) cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})

View File

@@ -130,6 +130,8 @@ func ReplaceAliases(s string) string {
// FindEmojiSubmatchIndex returns index pair of longest emoji in a string // FindEmojiSubmatchIndex returns index pair of longest emoji in a string
func FindEmojiSubmatchIndex(s string) []int { func FindEmojiSubmatchIndex(s string) []int {
loadMap() loadMap()
found := make(map[int]int)
keys := make([]int, 0)
//see if there are any emoji in string before looking for position of specific ones //see if there are any emoji in string before looking for position of specific ones
//no performance difference when there is a match but 10x faster when there are not //no performance difference when there is a match but 10x faster when there are not
@@ -137,11 +139,26 @@ func FindEmojiSubmatchIndex(s string) []int {
return nil return nil
} }
// get index of first emoji occurrence while also checking for longest combination
for j := range GemojiData { for j := range GemojiData {
i := strings.Index(s, GemojiData[j].Emoji) i := strings.Index(s, GemojiData[j].Emoji)
if i != -1 { if i != -1 {
return []int{i, i + len(GemojiData[j].Emoji)} if _, ok := found[i]; !ok {
if len(keys) == 0 || i < keys[0] {
found[i] = j
keys = []int{i}
}
if i == 0 {
break
} }
} }
}
}
if len(keys) > 0 {
index := keys[0]
return []int{index, index + len(GemojiData[found[index]].Emoji)}
}
return nil return nil
} }

View File

@@ -355,7 +355,7 @@ func NewColoredValueBytes(value interface{}, colorBytes *[]byte) *ColoredValue {
// The Value will be colored with FgCyan // The Value will be colored with FgCyan
// If a ColoredValue is provided it is not changed // If a ColoredValue is provided it is not changed
func NewColoredIDValue(value interface{}) *ColoredValue { func NewColoredIDValue(value interface{}) *ColoredValue {
return NewColoredValueBytes(&value, &fgCyanBytes) return NewColoredValueBytes(value, &fgCyanBytes)
} }
// Format will format the provided value and protect against ANSI color spoofing within the value // Format will format the provided value and protect against ANSI color spoofing within the value

View File

@@ -266,6 +266,10 @@ func TestRender_emoji(t *testing.T) {
test( test(
"Some text with 😄😄 2 emoji next to each other", "Some text with 😄😄 2 emoji next to each other",
`<p>Some text with <span class="emoji" aria-label="grinning face with smiling eyes">😄</span><span class="emoji" aria-label="grinning face with smiling eyes">😄</span> 2 emoji next to each other</p>`) `<p>Some text with <span class="emoji" aria-label="grinning face with smiling eyes">😄</span><span class="emoji" aria-label="grinning face with smiling eyes">😄</span> 2 emoji next to each other</p>`)
test(
"😎🤪🔐🤑❓",
`<p><span class="emoji" aria-label="smiling face with sunglasses">😎</span><span class="emoji" aria-label="zany face">🤪</span><span class="emoji" aria-label="locked with key">🔐</span><span class="emoji" aria-label="money-mouth face">🤑</span><span class="emoji" aria-label="question mark">❓</span></p>`)
// should match nothing // should match nothing
test( test(
"2001:0db8:85a3:0000:0000:8a2e:0370:7334", "2001:0db8:85a3:0000:0000:8a2e:0370:7334",

View File

@@ -11,9 +11,37 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
) )
func statusStringToNotificationStatus(status string) models.NotificationStatus {
switch strings.ToLower(strings.TrimSpace(status)) {
case "unread":
return models.NotificationStatusUnread
case "read":
return models.NotificationStatusRead
case "pinned":
return models.NotificationStatusPinned
default:
return 0
}
}
func statusStringsToNotificationStatuses(statuses []string, defaultStatuses []string) []models.NotificationStatus {
if len(statuses) == 0 {
statuses = defaultStatuses
}
results := make([]models.NotificationStatus, 0, len(statuses))
for _, status := range statuses {
notificationStatus := statusStringToNotificationStatus(status)
if notificationStatus > 0 {
results = append(results, notificationStatus)
}
}
return results
}
// ListRepoNotifications list users's notification threads on a specific repo // ListRepoNotifications list users's notification threads on a specific repo
func ListRepoNotifications(ctx *context.APIContext) { func ListRepoNotifications(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/notifications notification notifyGetRepoList // swagger:operation GET /repos/{owner}/{repo}/notifications notification notifyGetRepoList
@@ -39,6 +67,14 @@ func ListRepoNotifications(ctx *context.APIContext) {
// description: If true, show notifications marked as read. Default value is false // description: If true, show notifications marked as read. Default value is false
// type: string // type: string
// required: false // required: false
// - name: status-types
// in: query
// description: "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread & pinned"
// type: array
// collectionFormat: multi
// items:
// type: string
// required: false
// - 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
@@ -75,9 +111,10 @@ func ListRepoNotifications(ctx *context.APIContext) {
UpdatedBeforeUnix: before, UpdatedBeforeUnix: before,
UpdatedAfterUnix: since, UpdatedAfterUnix: since,
} }
qAll := strings.Trim(ctx.Query("all"), " ")
if qAll != "true" { if !ctx.QueryBool("all") {
opts.Status = models.NotificationStatusUnread statuses := ctx.QueryStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread", "pinned"})
} }
nl, err := models.GetNotifications(opts) nl, err := models.GetNotifications(opts)
if err != nil { if err != nil {
@@ -97,7 +134,7 @@ func ListRepoNotifications(ctx *context.APIContext) {
func ReadRepoNotifications(ctx *context.APIContext) { func ReadRepoNotifications(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/notifications notification notifyReadRepoList // swagger:operation PUT /repos/{owner}/{repo}/notifications notification notifyReadRepoList
// --- // ---
// summary: Mark notification threads as read on a specific repo // summary: Mark notification threads as read, pinned or unread on a specific repo
// consumes: // consumes:
// - application/json // - application/json
// produces: // produces:
@@ -113,6 +150,24 @@ func ReadRepoNotifications(ctx *context.APIContext) {
// description: name of the repo // description: name of the repo
// type: string // type: string
// required: true // required: true
// - name: all
// in: query
// description: If true, mark all notifications on this repo. Default value is false
// type: string
// required: false
// - name: status-types
// in: query
// description: "Mark notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread."
// type: array
// collectionFormat: multi
// items:
// type: string
// required: false
// - name: to-status
// in: query
// description: Status to mark notifications as. Defaults to read.
// type: string
// required: false
// - name: last_read_at // - name: last_read_at
// in: query // in: query
// description: Describes the last point that notifications were checked. Anything updated since this time will not be updated. // description: Describes the last point that notifications were checked. Anything updated since this time will not be updated.
@@ -135,11 +190,17 @@ func ReadRepoNotifications(ctx *context.APIContext) {
lastRead = tmpLastRead.Unix() lastRead = tmpLastRead.Unix()
} }
} }
opts := models.FindNotificationOptions{ opts := models.FindNotificationOptions{
UserID: ctx.User.ID, UserID: ctx.User.ID,
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
UpdatedBeforeUnix: lastRead, UpdatedBeforeUnix: lastRead,
Status: models.NotificationStatusUnread, }
if !ctx.QueryBool("all") {
statuses := ctx.QueryStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
log.Error("%v", opts.Status)
} }
nl, err := models.GetNotifications(opts) nl, err := models.GetNotifications(opts)
if err != nil { if err != nil {
@@ -147,8 +208,13 @@ func ReadRepoNotifications(ctx *context.APIContext) {
return return
} }
targetStatus := statusStringToNotificationStatus(ctx.Query("to-status"))
if targetStatus == 0 {
targetStatus = models.NotificationStatusRead
}
for _, n := range nl { for _, n := range nl {
err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) err := models.SetNotificationStatus(n.ID, ctx.User, targetStatus)
if err != nil { if err != nil {
ctx.InternalServerError(err) ctx.InternalServerError(err)
return return

View File

@@ -62,6 +62,12 @@ func ReadThread(ctx *context.APIContext) {
// description: id of notification thread // description: id of notification thread
// type: string // type: string
// required: true // required: true
// - name: to-status
// in: query
// description: Status to mark notifications as
// type: string
// default: read
// required: false
// responses: // responses:
// "205": // "205":
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"
@@ -75,7 +81,12 @@ func ReadThread(ctx *context.APIContext) {
return return
} }
err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) targetStatus := statusStringToNotificationStatus(ctx.Query("to-status"))
if targetStatus == 0 {
targetStatus = models.NotificationStatusRead
}
err := models.SetNotificationStatus(n.ID, ctx.User, targetStatus)
if err != nil { if err != nil {
ctx.InternalServerError(err) ctx.InternalServerError(err)
return return

View File

@@ -29,6 +29,14 @@ func ListNotifications(ctx *context.APIContext) {
// description: If true, show notifications marked as read. Default value is false // description: If true, show notifications marked as read. Default value is false
// type: string // type: string
// required: false // required: false
// - name: status-types
// in: query
// description: "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread & pinned."
// type: array
// collectionFormat: multi
// items:
// type: string
// required: false
// - 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
@@ -64,9 +72,9 @@ func ListNotifications(ctx *context.APIContext) {
UpdatedBeforeUnix: before, UpdatedBeforeUnix: before,
UpdatedAfterUnix: since, UpdatedAfterUnix: since,
} }
qAll := strings.Trim(ctx.Query("all"), " ") if !ctx.QueryBool("all") {
if qAll != "true" { statuses := ctx.QueryStrings("status-types")
opts.Status = models.NotificationStatusUnread opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread", "pinned"})
} }
nl, err := models.GetNotifications(opts) nl, err := models.GetNotifications(opts)
if err != nil { if err != nil {
@@ -82,11 +90,11 @@ func ListNotifications(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, nl.APIFormat()) ctx.JSON(http.StatusOK, nl.APIFormat())
} }
// ReadNotifications mark notification threads as read // ReadNotifications mark notification threads as read, unread, or pinned
func ReadNotifications(ctx *context.APIContext) { func ReadNotifications(ctx *context.APIContext) {
// swagger:operation PUT /notifications notification notifyReadList // swagger:operation PUT /notifications notification notifyReadList
// --- // ---
// summary: Mark notification threads as read // summary: Mark notification threads as read, pinned or unread
// consumes: // consumes:
// - application/json // - application/json
// produces: // produces:
@@ -98,6 +106,24 @@ func ReadNotifications(ctx *context.APIContext) {
// type: string // type: string
// format: date-time // format: date-time
// required: false // required: false
// - name: all
// in: query
// description: If true, mark all notifications on this repo. Default value is false
// type: string
// required: false
// - name: status-types
// in: query
// description: "Mark notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread."
// type: array
// collectionFormat: multi
// items:
// type: string
// required: false
// - name: to-status
// in: query
// description: Status to mark notifications as, Defaults to read.
// type: string
// required: false
// responses: // responses:
// "205": // "205":
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"
@@ -117,7 +143,10 @@ func ReadNotifications(ctx *context.APIContext) {
opts := models.FindNotificationOptions{ opts := models.FindNotificationOptions{
UserID: ctx.User.ID, UserID: ctx.User.ID,
UpdatedBeforeUnix: lastRead, UpdatedBeforeUnix: lastRead,
Status: models.NotificationStatusUnread, }
if !ctx.QueryBool("all") {
statuses := ctx.QueryStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
} }
nl, err := models.GetNotifications(opts) nl, err := models.GetNotifications(opts)
if err != nil { if err != nil {
@@ -125,8 +154,13 @@ func ReadNotifications(ctx *context.APIContext) {
return return
} }
targetStatus := statusStringToNotificationStatus(ctx.Query("to-status"))
if targetStatus == 0 {
targetStatus = models.NotificationStatusRead
}
for _, n := range nl { for _, n := range nl {
err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) err := models.SetNotificationStatus(n.ID, ctx.User, targetStatus)
if err != nil { if err != nil {
ctx.InternalServerError(err) ctx.InternalServerError(err)
return return

View File

@@ -134,6 +134,10 @@ func SingleRelease(ctx *context.Context) {
release, err := models.GetRelease(ctx.Repo.Repository.ID, ctx.Params("tag")) release, err := models.GetRelease(ctx.Repo.Repository.ID, ctx.Params("tag"))
if err != nil { if err != nil {
if models.IsErrReleaseNotExist(err) {
ctx.NotFound("GetRelease", err)
return
}
ctx.ServerError("GetReleasesByRepoID", err) ctx.ServerError("GetReleasesByRepoID", err)
return return
} }

View File

@@ -46,6 +46,7 @@ func createTag(gitRepo *git.Repository, rel *models.Release) error {
rel.Publisher, rel.Repo, git.TagPrefix+rel.TagName, rel.Publisher, rel.Repo, git.TagPrefix+rel.TagName,
git.EmptySHA, commit.ID.String(), repository.NewPushCommits()) git.EmptySHA, commit.ID.String(), repository.NewPushCommits())
notification.NotifyCreateRef(rel.Publisher, rel.Repo, "tag", git.TagPrefix+rel.TagName) notification.NotifyCreateRef(rel.Publisher, rel.Repo, "tag", git.TagPrefix+rel.TagName)
rel.CreatedUnix = timeutil.TimeStampNow()
} }
commit, err := gitRepo.GetTagCommit(rel.TagName) commit, err := gitRepo.GetTagCommit(rel.TagName)
if err != nil { if err != nil {
@@ -53,7 +54,6 @@ func createTag(gitRepo *git.Repository, rel *models.Release) error {
} }
rel.Sha1 = commit.ID.String() rel.Sha1 = commit.ID.String()
rel.CreatedUnix = timeutil.TimeStampNow()
rel.NumCommits, err = commit.CommitsCount() rel.NumCommits, err = commit.CommitsCount()
if err != nil { if err != nil {
return fmt.Errorf("CommitsCount: %v", err) return fmt.Errorf("CommitsCount: %v", err)

View File

@@ -7,6 +7,7 @@ package release
import ( import (
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@@ -101,3 +102,153 @@ func TestRelease_Create(t *testing.T) {
IsTag: true, IsTag: true,
}, nil)) }, nil))
} }
func TestRelease_Update(t *testing.T) {
assert.NoError(t, models.PrepareTestDatabase())
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
repoPath := models.RepoPath(user.Name, repo.Name)
gitRepo, err := git.OpenRepository(repoPath)
assert.NoError(t, err)
defer gitRepo.Close()
// Test a changed release
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
PublisherID: user.ID,
TagName: "v1.1.1",
Target: "master",
Title: "v1.1.1 is released",
Note: "v1.1.1 is released",
IsDraft: false,
IsPrerelease: false,
IsTag: false,
}, nil))
release, err := models.GetRelease(repo.ID, "v1.1.1")
assert.NoError(t, err)
releaseCreatedUnix := release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Note = "Changed note"
assert.NoError(t, UpdateRelease(user, gitRepo, release, nil))
release, err = models.GetReleaseByID(release.ID)
assert.NoError(t, err)
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed draft
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
PublisherID: user.ID,
TagName: "v1.2.1",
Target: "65f1bf2",
Title: "v1.2.1 is draft",
Note: "v1.2.1 is draft",
IsDraft: true,
IsPrerelease: false,
IsTag: false,
}, nil))
release, err = models.GetRelease(repo.ID, "v1.2.1")
assert.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
assert.NoError(t, UpdateRelease(user, gitRepo, release, nil))
release, err = models.GetReleaseByID(release.ID)
assert.NoError(t, err)
assert.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed pre-release
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
PublisherID: user.ID,
TagName: "v1.3.1",
Target: "65f1bf2",
Title: "v1.3.1 is pre-released",
Note: "v1.3.1 is pre-released",
IsDraft: false,
IsPrerelease: true,
IsTag: false,
}, nil))
release, err = models.GetRelease(repo.ID, "v1.3.1")
assert.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
release.Note = "Changed note"
assert.NoError(t, UpdateRelease(user, gitRepo, release, nil))
release, err = models.GetReleaseByID(release.ID)
assert.NoError(t, err)
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
}
func TestRelease_createTag(t *testing.T) {
assert.NoError(t, models.PrepareTestDatabase())
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
repoPath := models.RepoPath(user.Name, repo.Name)
gitRepo, err := git.OpenRepository(repoPath)
assert.NoError(t, err)
defer gitRepo.Close()
// Test a changed release
release := &models.Release{
RepoID: repo.ID,
PublisherID: user.ID,
TagName: "v2.1.1",
Target: "master",
Title: "v2.1.1 is released",
Note: "v2.1.1 is released",
IsDraft: false,
IsPrerelease: false,
IsTag: false,
}
assert.NoError(t, createTag(gitRepo, release))
assert.NotEmpty(t, release.CreatedUnix)
releaseCreatedUnix := release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Note = "Changed note"
assert.NoError(t, createTag(gitRepo, release))
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed draft
release = &models.Release{
RepoID: repo.ID,
PublisherID: user.ID,
TagName: "v2.2.1",
Target: "65f1bf2",
Title: "v2.2.1 is draft",
Note: "v2.2.1 is draft",
IsDraft: true,
IsPrerelease: false,
IsTag: false,
}
assert.NoError(t, createTag(gitRepo, release))
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
assert.NoError(t, createTag(gitRepo, release))
assert.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed pre-release
release = &models.Release{
RepoID: repo.ID,
PublisherID: user.ID,
TagName: "v2.3.1",
Target: "65f1bf2",
Title: "v2.3.1 is pre-released",
Note: "v2.3.1 is pre-released",
IsDraft: false,
IsPrerelease: true,
IsTag: false,
}
assert.NoError(t, createTag(gitRepo, release))
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
release.Note = "Changed note"
assert.NoError(t, createTag(gitRepo, release))
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
}

View File

@@ -128,12 +128,12 @@
<span class="no-select item {{if .HasSelectedLabel}}hide{{end}}">{{.i18n.Tr "repo.issues.new.no_label"}}</span> <span class="no-select item {{if .HasSelectedLabel}}hide{{end}}">{{.i18n.Tr "repo.issues.new.no_label"}}</span>
{{range .Labels}} {{range .Labels}}
<div class="item"> <div class="item">
<a class="ui label {{if not .IsChecked}}hide{{end}}" id="label_{{.ID}}" href="{{$.RepoLink}}/issues?labels={{.ID}}" style="color: {{.ForegroundColor}}; background-color: {{.Color}}" title="{{.Description | RenderEmojiPlain}}">{{.Name | RenderEmoji}}</a> <a class="ui label {{if not .IsChecked}}hide{{end}}" id="label_{{.ID}}" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.ID}}" style="color: {{.ForegroundColor}}; background-color: {{.Color}}" title="{{.Description | RenderEmojiPlain}}">{{.Name | RenderEmoji}}</a>
</div> </div>
{{end}} {{end}}
{{range .OrgLabels}} {{range .OrgLabels}}
<div class="item"> <div class="item">
<a class="ui label {{if not .IsChecked}}hide{{end}}" id="label_{{.ID}}" href="{{$.RepoLink}}/issues?labels={{.ID}}" style="color: {{.ForegroundColor}}; background-color: {{.Color}}" title="{{.Description | RenderEmojiPlain}}">{{.Name | RenderEmoji}}</a> <a class="ui label {{if not .IsChecked}}hide{{end}}" id="label_{{.ID}}" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.ID}}" style="color: {{.ForegroundColor}}; background-color: {{.Color}}" title="{{.Description | RenderEmojiPlain}}">{{.Name | RenderEmoji}}</a>
</div> </div>
{{end}} {{end}}
@@ -238,7 +238,7 @@
<div class="selected"> <div class="selected">
{{range .Issue.Assignees}} {{range .Issue.Assignees}}
<div class="item" style="margin-bottom: 10px;"> <div class="item" style="margin-bottom: 10px;">
<a href="{{$.RepoLink}}/issues?assignee={{.ID}}"><img class="ui avatar image" src="{{.RelAvatarLink}}">&nbsp;{{.GetDisplayName}}</a> <a href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?assignee={{.ID}}"><img class="ui avatar image" src="{{.RelAvatarLink}}">&nbsp;{{.GetDisplayName}}</a>
</div> </div>
{{end}} {{end}}
</div> </div>

View File

@@ -459,6 +459,16 @@
"name": "all", "name": "all",
"in": "query" "in": "query"
}, },
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread \u0026 pinned.",
"name": "status-types",
"in": "query"
},
{ {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
@@ -502,7 +512,7 @@
"tags": [ "tags": [
"notification" "notification"
], ],
"summary": "Mark notification threads as read", "summary": "Mark notification threads as read, pinned or unread",
"operationId": "notifyReadList", "operationId": "notifyReadList",
"parameters": [ "parameters": [
{ {
@@ -511,6 +521,28 @@
"description": "Describes the last point that notifications were checked. Anything updated since this time will not be updated.", "description": "Describes the last point that notifications were checked. Anything updated since this time will not be updated.",
"name": "last_read_at", "name": "last_read_at",
"in": "query" "in": "query"
},
{
"type": "string",
"description": "If true, mark all notifications on this repo. Default value is false",
"name": "all",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "Mark notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread.",
"name": "status-types",
"in": "query"
},
{
"type": "string",
"description": "Status to mark notifications as, Defaults to read.",
"name": "to-status",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -587,6 +619,13 @@
"name": "id", "name": "id",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "string",
"default": "read",
"description": "Status to mark notifications as",
"name": "to-status",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -6290,6 +6329,16 @@
"name": "all", "name": "all",
"in": "query" "in": "query"
}, },
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread \u0026 pinned",
"name": "status-types",
"in": "query"
},
{ {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
@@ -6333,7 +6382,7 @@
"tags": [ "tags": [
"notification" "notification"
], ],
"summary": "Mark notification threads as read on a specific repo", "summary": "Mark notification threads as read, pinned or unread on a specific repo",
"operationId": "notifyReadRepoList", "operationId": "notifyReadRepoList",
"parameters": [ "parameters": [
{ {
@@ -6350,6 +6399,28 @@
"in": "path", "in": "path",
"required": true "required": true
}, },
{
"type": "string",
"description": "If true, mark all notifications on this repo. Default value is false",
"name": "all",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "Mark notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread.",
"name": "status-types",
"in": "query"
},
{
"type": "string",
"description": "Status to mark notifications as. Defaults to read.",
"name": "to-status",
"in": "query"
},
{ {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",

View File

@@ -1271,33 +1271,18 @@ i.icon.centerlock {
.emoji, .emoji,
.reaction { .reaction {
font-size: 1.5em; font-size: 1.25em;
line-height: 1.2; line-height: 1;
font-style: normal !important; font-style: normal !important;
font-weight: normal !important; font-weight: normal !important;
vertical-align: middle; vertical-align: -.075em;
} }
#issue-title > .emoji {
font-size: 1em;
}
.commit-summary > .emoji {
font-size: 1em;
}
.label > .emoji {
font-size: 1em;
}
.dropdown .emoji {
font-size: 1em;
}
.emoji img, .emoji img,
.reaction img { .reaction img {
border-width: 0 !important; border-width: 0 !important;
margin: 0 !important; margin: 0 !important;
width: 1em !important; width: 1em !important;
height: 1em !important; height: 1em !important;
vertical-align: middle !important; vertical-align: -.15em;
} }

View File

@@ -2246,7 +2246,7 @@
.select-reaction { .select-reaction {
display: flex; display: flex;
align-items: center; align-items: center;
padding: .5rem; padding: 0 .5rem;
&:not(.active) a { &:not(.active) a {
display: none; display: none;