From c524d33402c76bc4cccea2806f289e08a009baae Mon Sep 17 00:00:00 2001 From: fluzz Date: Tue, 30 May 2023 18:42:58 +0200 Subject: [PATCH] WIP: Add an 'updated_at' field to the EditIssueOption struct This field adds the possibility to set the update date when modifying an issue through the API. A 'NoAutoDate' in-memory field is added in the Issue struct. If the update_at field is set, NoAutoDate is set to true and the Issue's UpdatedUnix field is filled. That information is passed down to the functions that actually updates the database, which have been modified to not auto update dates if requested. A guard is added to the 'EditIssue' API call, to checks that the udpate_at date is between the issue's creation date and the current date (to avoid 'malicious' changes). It also limits the new feature to project's owners and admins. --- models/issues/comment.go | 5 +++++ models/issues/issue.go | 1 + models/issues/issue_update.go | 36 +++++++++++++++++++++++++--------- models/issues/issue_xref.go | 4 ++++ models/issues/milestone.go | 22 ++++++++++++++++----- modules/structs/issue.go | 2 ++ routers/api/v1/repo/issue.go | 27 +++++++++++++++++++++++++ services/issue/milestone.go | 20 +++++++++++++++---- templates/swagger/v1_json.tmpl | 5 +++++ 9 files changed, 104 insertions(+), 18 deletions(-) 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"