diff --git a/models/issues/comment.go b/models/issues/comment.go index e781931261..643956670d 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -823,6 +823,11 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, IsForcePush: opts.IsForcePush, Invalidated: opts.Invalidated, } + if opts.Issue.NoAutoTime { + comment.CreatedUnix = opts.Issue.UpdatedUnix + comment.UpdatedUnix = opts.Issue.UpdatedUnix + e.NoAutoTime() + } if _, err = e.Insert(comment); err != nil { return nil, err } diff --git a/models/issues/issue.go b/models/issues/issue.go index f000f4c660..acd6eb15f9 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -125,6 +125,7 @@ type Issue struct { CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` ClosedUnix timeutil.TimeStamp `xorm:"INDEX"` + NoAutoTime bool `xorm:"-"` Attachments []*repo_model.Attachment `xorm:"-"` Comments CommentList `xorm:"-"` diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 9607b21a67..a9a79d3b19 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -27,7 +27,12 @@ import ( // UpdateIssueCols updates cols of issue func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { - if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil { + sess := db.GetEngine(ctx).ID(issue.ID) + if issue.NoAutoTime { + cols = append(cols, []string{"updated_unix"}...) + sess.NoAutoTime() + } + if _, err := sess.Cols(cols...).Update(issue); err != nil { return err } return nil @@ -71,7 +76,11 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use } if issue.IsClosed { - issue.ClosedUnix = timeutil.TimeStampNow() + if issue.NoAutoTime { + issue.ClosedUnix = issue.UpdatedUnix + } else { + issue.ClosedUnix = timeutil.TimeStampNow() + } } else { issue.ClosedUnix = 0 } @@ -92,8 +101,14 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use // Update issue count of milestone if issue.MilestoneID > 0 { - if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { - return nil, err + if issue.NoAutoTime { + if err := UpdateMilestoneCountersWithDate(ctx, issue.MilestoneID, issue.UpdatedUnix); err != nil { + return nil, err + } + } else { + if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { + return nil, err + } } } @@ -449,10 +464,13 @@ func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment return nil, false, err } - if _, err := db.GetEngine(ctx).ID(issue.ID).Cols( - "name", "content", "milestone_id", "priority", - "deadline_unix", "updated_unix", "is_locked"). - Update(issue); err != nil { + sess := db.GetEngine(ctx).ID(issue.ID) + cols := []string{"name", "content", "milestone_id", "priority", "deadline_unix", "is_locked"} + if issue.NoAutoTime { + cols = append(cols, "updated_unix") + sess.NoAutoTime() + } + if _, err := sess.Cols(cols...).Update(issue); err != nil { return nil, false, err } @@ -498,7 +516,7 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us defer committer.Close() // Update the deadline - if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { + if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix, NoAutoTime: issue.NoAutoTime, UpdatedUnix: issue.UpdatedUnix}, "deadline_unix"); err != nil { return err } diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go index a1086f9e81..d580c75fb8 100644 --- a/models/issues/issue_xref.go +++ b/models/issues/issue_xref.go @@ -110,6 +110,10 @@ func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossRefe if ctx.OrigComment != nil { refCommentID = ctx.OrigComment.ID } + if ctx.OrigIssue.NoAutoTime { + xref.Issue.NoAutoTime = true + xref.Issue.UpdatedUnix = ctx.OrigIssue.UpdatedUnix + } opts := &CreateCommentOptions{ Type: ctx.Type, Doer: ctx.Doer, diff --git a/models/issues/milestone.go b/models/issues/milestone.go index 1418e0869d..9d30a0f871 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -188,10 +188,9 @@ func updateMilestone(ctx context.Context, m *Milestone) error { return UpdateMilestoneCounters(ctx, m.ID) } -// UpdateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness -func UpdateMilestoneCounters(ctx context.Context, id int64) error { +func updateMilestoneCounters(ctx context.Context, id int64, noAutoTime bool, updatedUnix timeutil.TimeStamp) error { e := db.GetEngine(ctx) - _, err := e.ID(id). + sess := e.ID(id). SetExpr("num_issues", builder.Select("count(*)").From("issue").Where( builder.Eq{"milestone_id": id}, )). @@ -200,8 +199,11 @@ func UpdateMilestoneCounters(ctx context.Context, id int64) error { "milestone_id": id, "is_closed": true, }, - )). - Update(&Milestone{}) + )) + if noAutoTime { + sess.SetExpr("updated_unix", updatedUnix).NoAutoTime() + } + _, err := sess.Update(&Milestone{}) if err != nil { return err } @@ -211,6 +213,16 @@ func UpdateMilestoneCounters(ctx context.Context, id int64) error { return err } +// UpdateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness +func UpdateMilestoneCounters(ctx context.Context, id int64) error { + return updateMilestoneCounters(ctx, id, false, 0) +} + +// UpdateMilestoneCountersWithDate calculates NumIssues, NumClosesIssues and Completeness and set the UpdatedUnix date +func UpdateMilestoneCountersWithDate(ctx context.Context, id int64, updatedUnix timeutil.TimeStamp) error { + return updateMilestoneCounters(ctx, id, true, updatedUnix) +} + // ChangeMilestoneStatusByRepoIDAndID changes a milestone open/closed status if the milestone ID is in the repo. func ChangeMilestoneStatusByRepoIDAndID(repoID, milestoneID int64, isClosed bool) error { ctx, committer, err := db.TxContext(db.DefaultContext) diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 1aec5cc6b8..552496e652 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -110,6 +110,8 @@ type EditIssueOption struct { // swagger:strfmt date-time Deadline *time.Time `json:"due_date"` RemoveDeadline *bool `json:"unset_due_date"` + // swagger:strfmt date-time + Updated *time.Time `json:"updated_at"` } // EditDeadlineOption options for creating a deadline diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index a08fdf5940..bd756a3f11 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -774,6 +774,33 @@ func EditIssue(ctx *context.APIContext) { return } + // In order to be set a specific update time, the DB will be updated + // with NoAutoTime. The 'noAutoTime' bool will be propagated down to the + // DB update calls to apply autoupdate or not. + issue.NoAutoTime = false + if form.Updated != nil { + // Check if the poster is allowed to set an update date + perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, ctx.Doer) + if err != nil { + ctx.Status(http.StatusForbidden) + return + } + if !perm.IsAdmin() && !perm.IsOwner() { + ctx.Error(http.StatusUnauthorized, "EditIssue", "user needs to have admin or owner right") + return + } + + // A simple guard against potential inconsistent calls + updatedUnix := timeutil.TimeStamp(form.Updated.Unix()) + if updatedUnix < issue.CreatedUnix || updatedUnix > timeutil.TimeStampNow() { + ctx.Error(http.StatusForbidden, "EditIssue", "unallowed update date") + return + } + + issue.UpdatedUnix = updatedUnix + issue.NoAutoTime = true + } + oldTitle := issue.Title if len(form.Title) > 0 { issue.Title = form.Title diff --git a/services/issue/milestone.go b/services/issue/milestone.go index a9be8bd887..6de3ce2d8b 100644 --- a/services/issue/milestone.go +++ b/services/issue/milestone.go @@ -30,14 +30,26 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *is } if oldMilestoneID > 0 { - if err := issues_model.UpdateMilestoneCounters(ctx, oldMilestoneID); err != nil { - return err + if issue.NoAutoTime { + if err := issues_model.UpdateMilestoneCountersWithDate(ctx, oldMilestoneID, issue.UpdatedUnix); err != nil { + return err + } + } else { + if err := issues_model.UpdateMilestoneCounters(ctx, oldMilestoneID); err != nil { + return err + } } } if issue.MilestoneID > 0 { - if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { - return err + if issue.NoAutoTime { + if err := issues_model.UpdateMilestoneCountersWithDate(ctx, issue.MilestoneID, issue.UpdatedUnix); err != nil { + return err + } + } else { + if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { + return err + } } } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 5e75f6f8b4..f60efa5172 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -18086,6 +18086,11 @@ "unset_due_date": { "type": "boolean", "x-go-name": "RemoveDeadline" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" } }, "x-go-package": "code.gitea.io/gitea/modules/structs"