mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-04 04:24:49 +00:00
fix(api): normalize all API due dates to end of day (#38677)
Due dates set through the API were stored inconsistently. The create endpoints kept the raw timestamp, issue and pull request edits used end of day in the client's offset, and only the deadline and milestone edit endpoints normalized to end of day in the server's UI timezone like the web UI does. All API due dates now go through `ParseAPIDeadlineToEndOfDay`. Editing a pull request with `unset_due_date: false` and no `due_date` dereferenced a nil pointer and returned 500. It now shares the issue edit logic and returns 400. Related to https://github.com/go-gitea/gitea/issues/37620, which is about the web sidebar and isn't fixed here. --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -22,7 +22,6 @@ import (
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
@@ -568,25 +567,20 @@ func CreateIssue(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm[*api.CreateIssueOption](ctx)
|
||||
var deadlineUnix timeutil.TimeStamp
|
||||
if form.Deadline != nil && ctx.Repo.Permission.CanWrite(unit.TypeIssues) {
|
||||
deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
|
||||
}
|
||||
|
||||
issue := &issues_model.Issue{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Repo: ctx.Repo.Repository,
|
||||
Title: form.Title,
|
||||
PosterID: ctx.Doer.ID,
|
||||
Poster: ctx.Doer,
|
||||
Content: form.Body,
|
||||
Ref: form.Ref,
|
||||
DeadlineUnix: deadlineUnix,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Repo: ctx.Repo.Repository,
|
||||
Title: form.Title,
|
||||
PosterID: ctx.Doer.ID,
|
||||
Poster: ctx.Doer,
|
||||
Content: form.Body,
|
||||
Ref: form.Ref,
|
||||
}
|
||||
|
||||
assigneeIDs := make([]int64, 0)
|
||||
var err error
|
||||
if ctx.Repo.Permission.CanWrite(unit.TypeIssues) {
|
||||
issue.DeadlineUnix = common.ParseAPIDeadlineToEndOfDay(form.Deadline)
|
||||
issue.MilestoneID = form.Milestone
|
||||
assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
|
||||
if err != nil {
|
||||
@@ -760,26 +754,8 @@ func EditIssue(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// Update or remove the deadline, only if set and allowed
|
||||
if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
|
||||
var deadlineUnix timeutil.TimeStamp
|
||||
|
||||
if form.RemoveDeadline == nil || !*form.RemoveDeadline {
|
||||
if form.Deadline == nil {
|
||||
ctx.APIError(http.StatusBadRequest, "The due_date cannot be empty")
|
||||
return
|
||||
}
|
||||
if !form.Deadline.IsZero() {
|
||||
deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
|
||||
23, 59, 59, 0, form.Deadline.Location())
|
||||
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
|
||||
}
|
||||
}
|
||||
|
||||
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
issue.DeadlineUnix = deadlineUnix
|
||||
if canWrite && !editIssueDeadline(ctx, issue, form.Deadline, form.RemoveDeadline) {
|
||||
return
|
||||
}
|
||||
|
||||
// Add/delete assignees
|
||||
@@ -911,6 +887,25 @@ func DeleteIssue(ctx *context.APIContext) {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func editIssueDeadline(ctx *context.APIContext, issue *issues_model.Issue, deadline *time.Time, removeDeadline *bool) bool {
|
||||
if deadline == nil && removeDeadline == nil {
|
||||
return true
|
||||
}
|
||||
if removeDeadline != nil && *removeDeadline {
|
||||
deadline = nil
|
||||
} else if deadline == nil {
|
||||
ctx.APIError(http.StatusBadRequest, "The due_date cannot be empty")
|
||||
return false
|
||||
}
|
||||
deadlineUnix := common.ParseAPIDeadlineToEndOfDay(deadline)
|
||||
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return false
|
||||
}
|
||||
issue.DeadlineUnix = deadlineUnix
|
||||
return true
|
||||
}
|
||||
|
||||
// UpdateIssueDeadline updates an issue deadline
|
||||
func UpdateIssueDeadline(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline
|
||||
@@ -964,7 +959,7 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
deadlineUnix, _ := common.ParseAPIDeadlineToEndOfDay(form.Deadline)
|
||||
deadlineUnix := common.ParseAPIDeadlineToEndOfDay(form.Deadline)
|
||||
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
@@ -149,16 +149,11 @@ func CreateMilestone(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
form := web.GetForm[*api.CreateMilestoneOption](ctx)
|
||||
|
||||
var deadlineUnix int64
|
||||
if form.Deadline != nil {
|
||||
deadlineUnix = form.Deadline.Unix()
|
||||
}
|
||||
|
||||
milestone := &issues_model.Milestone{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: form.Title,
|
||||
Content: form.Description,
|
||||
DeadlineUnix: timeutil.TimeStamp(deadlineUnix),
|
||||
DeadlineUnix: common.ParseAPIDeadlineToEndOfDay(form.Deadline),
|
||||
}
|
||||
|
||||
if form.State == "closed" {
|
||||
@@ -219,7 +214,7 @@ func EditMilestone(ctx *context.APIContext) {
|
||||
if form.Description != nil {
|
||||
milestone.Content = *form.Description
|
||||
}
|
||||
milestone.DeadlineUnix, _ = common.ParseAPIDeadlineToEndOfDay(form.Deadline)
|
||||
milestone.DeadlineUnix = common.ParseAPIDeadlineToEndOfDay(form.Deadline)
|
||||
|
||||
oldIsClosed := milestone.IsClosed
|
||||
if form.State != nil {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
@@ -26,7 +25,6 @@ import (
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
@@ -491,11 +489,6 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||
milestoneID = milestone.ID
|
||||
}
|
||||
|
||||
var deadlineUnix timeutil.TimeStamp
|
||||
if form.Deadline != nil {
|
||||
deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
|
||||
}
|
||||
|
||||
unitPullRequest, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -510,7 +503,7 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||
MilestoneID: milestoneID,
|
||||
IsPull: true,
|
||||
Content: form.Body,
|
||||
DeadlineUnix: deadlineUnix,
|
||||
DeadlineUnix: common.ParseAPIDeadlineToEndOfDay(form.Deadline),
|
||||
}
|
||||
pr := &issues_model.PullRequest{
|
||||
HeadRepoID: compareResult.HeadRepo.ID,
|
||||
@@ -616,6 +609,8 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/PullRequest"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
@@ -690,19 +685,8 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// Update or remove deadline if set
|
||||
if form.Deadline != nil || form.RemoveDeadline != nil {
|
||||
var deadlineUnix timeutil.TimeStamp
|
||||
if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
|
||||
deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
|
||||
23, 59, 59, 0, form.Deadline.Location())
|
||||
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
|
||||
}
|
||||
|
||||
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
issue.DeadlineUnix = deadlineUnix
|
||||
if !editIssueDeadline(ctx, issue, form.Deadline, form.RemoveDeadline) {
|
||||
return
|
||||
}
|
||||
|
||||
// Add/delete assignees
|
||||
|
||||
@@ -22,10 +22,9 @@ func ParseDeadlineDateToEndOfDay(date string) (timeutil.TimeStamp, error) {
|
||||
return timeutil.TimeStamp(deadline.Unix()), nil
|
||||
}
|
||||
|
||||
func ParseAPIDeadlineToEndOfDay(t *time.Time) (timeutil.TimeStamp, error) {
|
||||
func ParseAPIDeadlineToEndOfDay(t *time.Time) timeutil.TimeStamp {
|
||||
if t == nil || t.IsZero() || t.Unix() == 0 {
|
||||
return 0, nil
|
||||
return 0
|
||||
}
|
||||
deadline := time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, setting.DefaultUILocation)
|
||||
return timeutil.TimeStamp(deadline.Unix()), nil
|
||||
return timeutil.TimeStamp(time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, setting.DefaultUILocation).Unix())
|
||||
}
|
||||
|
||||
+3
@@ -29212,6 +29212,9 @@
|
||||
"201": {
|
||||
"$ref": "#/components/responses/PullRequest"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/error"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
},
|
||||
|
||||
+3
@@ -16853,6 +16853,9 @@
|
||||
"201": {
|
||||
"$ref": "#/responses/PullRequest"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/error"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/responses/forbidden"
|
||||
},
|
||||
|
||||
@@ -126,11 +126,13 @@ func testAPICreateIssue(t *testing.T) {
|
||||
Body: body,
|
||||
Title: title,
|
||||
Assignee: owner.Name,
|
||||
Deadline: new(time.Date(2026, 5, 11, 12, 0, 0, 0, time.FixedZone("", -10*3600))),
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiIssue := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Equal(t, body, apiIssue.Body)
|
||||
assert.Equal(t, title, apiIssue.Title)
|
||||
assert.Equal(t, time.Date(2026, 5, 11, 23, 59, 59, 0, setting.DefaultUILocation).Unix(), apiIssue.Deadline.Unix())
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
||||
RepoID: repoBefore.ID,
|
||||
|
||||
@@ -449,13 +449,15 @@ func TestAPIEditPull(t *testing.T) {
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
title := "create a success pr"
|
||||
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
|
||||
Head: "develop",
|
||||
Base: "master",
|
||||
Title: title,
|
||||
Head: "develop",
|
||||
Base: "master",
|
||||
Title: title,
|
||||
Deadline: new(time.Date(2026, 5, 11, 12, 0, 0, 0, time.FixedZone("", -10*3600))),
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiPull := DecodeJSON(t, resp, &api.PullRequest{})
|
||||
assert.Equal(t, "master", apiPull.Base.Name)
|
||||
assert.Equal(t, time.Date(2026, 5, 11, 23, 59, 59, 0, setting.DefaultUILocation).Unix(), apiPull.Deadline.Unix())
|
||||
|
||||
newTitle := "edit a this pr"
|
||||
newBody := "edited body"
|
||||
@@ -480,6 +482,11 @@ func TestAPIEditPull(t *testing.T) {
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{
|
||||
RemoveDeadline: new(false),
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
t.Run("PullContentVersion", func(t *testing.T) {
|
||||
testAPIPullContentVersion(t, pull.ID)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user