diff --git a/models/issues/comment.go b/models/issues/comment.go
index 901958dc5d..1f0b28a050 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -817,6 +817,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
 	}
@@ -1095,9 +1100,17 @@ func UpdateComment(ctx context.Context, c *Comment, doer *user_model.User) error
 		return err
 	}
 	defer committer.Close()
-	sess := db.GetEngine(ctx)
 
-	if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil {
+	sess := db.GetEngine(ctx).ID(c.ID).AllCols()
+	if c.Issue.NoAutoTime {
+		// update the DataBase
+		sess = sess.NoAutoTime().SetExpr("updated_unix", c.Issue.UpdatedUnix)
+		// the UpdatedUnix value of the Comment also has to be set,
+		// to return the adequate valuè
+		// see https://codeberg.org/forgejo/forgejo/pulls/764#issuecomment-1023801
+		c.UpdatedUnix = c.Issue.UpdatedUnix
+	}
+	if _, err := sess.Update(c); err != nil {
 		return err
 	}
 	if err := c.LoadIssue(ctx); err != nil {
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 6d9c8727b3..ee78906b82 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -126,6 +126,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 b258dc882c..78f4657c44 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
+			}
 		}
 	}
 
@@ -259,8 +274,12 @@ func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User
 		return fmt.Errorf("UpdateIssueCols: %w", err)
 	}
 
+	historyDate := timeutil.TimeStampNow()
+	if issue.NoAutoTime {
+		historyDate = issue.UpdatedUnix
+	}
 	if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0,
-		timeutil.TimeStampNow(), issue.Content, false); err != nil {
+		historyDate, issue.Content, false); err != nil {
 		return fmt.Errorf("SaveIssueContentHistory: %w", err)
 	}
 
@@ -449,10 +468,13 @@ func UpdateIssueByAPI(ctx context.Context, issue *Issue, doer *user_model.User)
 		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 +520,7 @@ func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeuti
 	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 77ef53a013..5a39d045cd 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 ad1d5d0453..24dca7e08c 100644
--- a/models/issues/milestone.go
+++ b/models/issues/milestone.go
@@ -187,10 +187,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},
 		)).
@@ -199,8 +198,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
 	}
@@ -210,6 +212,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(ctx context.Context, repoID, milestoneID int64, isClosed bool) error {
 	ctx, committer, err := db.TxContext(ctx)
diff --git a/models/repo/attachment.go b/models/repo/attachment.go
index 1a588398c1..64df6b166e 100644
--- a/models/repo/attachment.go
+++ b/models/repo/attachment.go
@@ -28,6 +28,7 @@ type Attachment struct {
 	Name              string
 	DownloadCount     int64              `xorm:"DEFAULT 0"`
 	Size              int64              `xorm:"DEFAULT 0"`
+	NoAutoTime        bool               `xorm:"-"`
 	CreatedUnix       timeutil.TimeStamp `xorm:"created"`
 	CustomDownloadURL string             `xorm:"-"`
 }
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/modules/structs/issue_comment.go b/modules/structs/issue_comment.go
index 9e8f5c4bf3..9ecb4a1789 100644
--- a/modules/structs/issue_comment.go
+++ b/modules/structs/issue_comment.go
@@ -28,12 +28,16 @@ type Comment struct {
 type CreateIssueCommentOption struct {
 	// required:true
 	Body string `json:"body" binding:"Required"`
+	// swagger:strfmt date-time
+	Updated *time.Time `json:"updated_at"`
 }
 
 // EditIssueCommentOption options for editing a comment
 type EditIssueCommentOption struct {
 	// required: true
 	Body string `json:"body" binding:"Required"`
+	// swagger:strfmt date-time
+	Updated *time.Time `json:"updated_at"`
 }
 
 // TimelineComment represents a timeline comment (comment of any type) on a commit or issue
diff --git a/modules/structs/issue_label.go b/modules/structs/issue_label.go
index bf68726d79..b64e375961 100644
--- a/modules/structs/issue_label.go
+++ b/modules/structs/issue_label.go
@@ -4,6 +4,10 @@
 
 package structs
 
+import (
+	"time"
+)
+
 // Label a label to an issue or a pr
 // swagger:model
 type Label struct {
@@ -45,10 +49,18 @@ type EditLabelOption struct {
 	IsArchived *bool `json:"is_archived"`
 }
 
+// DeleteLabelOption options for deleting a label
+type DeleteLabelsOption struct {
+	// swagger:strfmt date-time
+	Updated *time.Time `json:"updated_at"`
+}
+
 // IssueLabelsOption a collection of labels
 type IssueLabelsOption struct {
 	// list of label IDs
 	Labels []int64 `json:"labels"`
+	// swagger:strfmt date-time
+	Updated *time.Time `json:"updated_at"`
 }
 
 // LabelTemplate info of a Label template
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 8d8373f0aa..575d0ee119 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1334,8 +1334,8 @@ func Routes() *web.Route {
 							m.Combo("").Get(repo.ListIssueLabels).
 								Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
 								Put(reqToken(), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
-								Delete(reqToken(), repo.ClearIssueLabels)
-							m.Delete("/{id}", reqToken(), repo.DeleteIssueLabel)
+								Delete(reqToken(), bind(api.DeleteLabelsOption{}), repo.ClearIssueLabels)
+							m.Delete("/{id}", reqToken(), bind(api.DeleteLabelsOption{}), repo.DeleteIssueLabel)
 						})
 						m.Group("/times", func() {
 							m.Combo("").
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 74e6361f6c..b58f3a6fa7 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -780,6 +780,12 @@ func EditIssue(ctx *context.APIContext) {
 		return
 	}
 
+	err = issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer)
+	if err != nil {
+		ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
+		return
+	}
+
 	oldTitle := issue.Title
 	if len(form.Title) > 0 {
 		issue.Title = form.Title
diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go
index 11d19b21ff..eaaaeaa9f9 100644
--- a/routers/api/v1/repo/issue_attachment.go
+++ b/routers/api/v1/repo/issue_attachment.go
@@ -5,6 +5,7 @@ package repo
 
 import (
 	"net/http"
+	"time"
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -141,6 +142,11 @@ func CreateIssueAttachment(ctx *context.APIContext) {
 	//   description: name of the attachment
 	//   type: string
 	//   required: false
+	// - name: updated_at
+	//   in: query
+	//   description: time of the attachment's creation. This is a timestamp in RFC 3339 format
+	//   type: string
+	//   format: date-time
 	// - name: attachment
 	//   in: formData
 	//   description: attachment to upload
@@ -165,6 +171,20 @@ func CreateIssueAttachment(ctx *context.APIContext) {
 		return
 	}
 
+	updatedAt := ctx.Req.FormValue("updated_at")
+	if len(updatedAt) != 0 {
+		updated, err := time.Parse(time.RFC3339, updatedAt)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "time.Parse", err)
+			return
+		}
+		err = issue_service.SetIssueUpdateDate(ctx, issue, &updated, ctx.Doer)
+		if err != nil {
+			ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
+			return
+		}
+	}
+
 	// Get uploaded file from request
 	file, header, err := ctx.Req.FormFile("attachment")
 	if err != nil {
@@ -179,10 +199,12 @@ func CreateIssueAttachment(ctx *context.APIContext) {
 	}
 
 	attachment, err := attachment.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
-		Name:       filename,
-		UploaderID: ctx.Doer.ID,
-		RepoID:     ctx.Repo.Repository.ID,
-		IssueID:    issue.ID,
+		Name:        filename,
+		UploaderID:  ctx.Doer.ID,
+		RepoID:      ctx.Repo.Repository.ID,
+		IssueID:     issue.ID,
+		NoAutoTime:  issue.NoAutoTime,
+		CreatedUnix: issue.UpdatedUnix,
 	})
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index c718424f7e..a24ef75ae6 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -372,6 +372,12 @@ func CreateIssueComment(ctx *context.APIContext) {
 		return
 	}
 
+	err = issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer)
+	if err != nil {
+		ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
+		return
+	}
+
 	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
@@ -565,6 +571,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
 		return
 	}
 
+	err = comment.LoadIssue(ctx)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
+		return
+	}
+	err = issue_service.SetIssueUpdateDate(ctx, comment.Issue, form.Updated, ctx.Doer)
+	if err != nil {
+		ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
+		return
+	}
+
 	oldContent := comment.Content
 	comment.Content = form.Body
 	if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go
index c327c54d10..0730122904 100644
--- a/routers/api/v1/repo/issue_comment_attachment.go
+++ b/routers/api/v1/repo/issue_comment_attachment.go
@@ -5,6 +5,7 @@ package repo
 
 import (
 	"net/http"
+	"time"
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -144,6 +145,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
 	//   description: name of the attachment
 	//   type: string
 	//   required: false
+	// - name: updated_at
+	//   in: query
+	//   description: time of the attachment's creation. This is a timestamp in RFC 3339 format
+	//   type: string
+	//   format: date-time
 	// - name: attachment
 	//   in: formData
 	//   description: attachment to upload
@@ -169,6 +175,25 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
 		return
 	}
 
+	updatedAt := ctx.Req.FormValue("updated_at")
+	if len(updatedAt) != 0 {
+		updated, err := time.Parse(time.RFC3339, updatedAt)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "time.Parse", err)
+			return
+		}
+		err = comment.LoadIssue(ctx)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
+			return
+		}
+		err = issue_service.SetIssueUpdateDate(ctx, comment.Issue, &updated, ctx.Doer)
+		if err != nil {
+			ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
+			return
+		}
+	}
+
 	// Get uploaded file from request
 	file, header, err := ctx.Req.FormFile("attachment")
 	if err != nil {
@@ -183,11 +208,13 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
 	}
 
 	attachment, err := attachment.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
-		Name:       filename,
-		UploaderID: ctx.Doer.ID,
-		RepoID:     ctx.Repo.Repository.ID,
-		IssueID:    comment.IssueID,
-		CommentID:  comment.ID,
+		Name:        filename,
+		UploaderID:  ctx.Doer.ID,
+		RepoID:      ctx.Repo.Repository.ID,
+		IssueID:     comment.IssueID,
+		CommentID:   comment.ID,
+		NoAutoTime:  comment.Issue.NoAutoTime,
+		CreatedUnix: comment.Issue.UpdatedUnix,
 	})
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go
index c2f530956e..008ff5c699 100644
--- a/routers/api/v1/repo/issue_label.go
+++ b/routers/api/v1/repo/issue_label.go
@@ -151,6 +151,10 @@ func DeleteIssueLabel(ctx *context.APIContext) {
 	//   type: integer
 	//   format: int64
 	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/DeleteLabelsOption"
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
@@ -160,6 +164,7 @@ func DeleteIssueLabel(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 	//   "422":
 	//     "$ref": "#/responses/validationError"
+	form := web.GetForm(ctx).(*api.DeleteLabelsOption)
 
 	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
@@ -176,6 +181,11 @@ func DeleteIssueLabel(ctx *context.APIContext) {
 		return
 	}
 
+	if err := issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer); err != nil {
+		ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
+		return
+	}
+
 	label, err := issues_model.GetLabelByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
 		if issues_model.IsErrLabelNotExist(err) {
@@ -275,6 +285,10 @@ func ClearIssueLabels(ctx *context.APIContext) {
 	//   type: integer
 	//   format: int64
 	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/DeleteLabelsOption"
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
@@ -282,6 +296,7 @@ func ClearIssueLabels(ctx *context.APIContext) {
 	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
+	form := web.GetForm(ctx).(*api.DeleteLabelsOption)
 
 	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
@@ -298,6 +313,11 @@ func ClearIssueLabels(ctx *context.APIContext) {
 		return
 	}
 
+	if err := issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer); err != nil {
+		ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
+		return
+	}
+
 	if err := issue_service.ClearLabels(ctx, issue, ctx.Doer); err != nil {
 		ctx.Error(http.StatusInternalServerError, "ClearLabels", err)
 		return
@@ -328,5 +348,11 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
 		return nil, nil, nil
 	}
 
+	err = issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer)
+	if err != nil {
+		ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
+		return nil, nil, err
+	}
+
 	return issue, labels, err
 }
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 6f7859df62..b5efbe916d 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -47,6 +47,9 @@ type swaggerParameterBodies struct {
 	// in:body
 	IssueLabelsOption api.IssueLabelsOption
 
+	// in:body
+	DeleteLabelsOption api.DeleteLabelsOption
+
 	// in:body
 	CreateKeyOption api.CreateKeyOption
 
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index 967332fd98..1bcd460e3c 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -32,7 +32,12 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R
 		}
 		attach.Size = size
 
-		return db.Insert(ctx, attach)
+		eng := db.GetEngine(ctx)
+		if attach.NoAutoTime {
+			eng.NoAutoTime()
+		}
+		_, err = eng.Insert(attach)
+		return err
 	})
 
 	return attach, err
diff --git a/services/issue/comments.go b/services/issue/comments.go
index 8d8c575c14..8de085026e 100644
--- a/services/issue/comments.go
+++ b/services/issue/comments.go
@@ -89,7 +89,11 @@ func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_mode
 	}
 
 	if needsContentHistory {
-		err := issues_model.SaveIssueContentHistory(ctx, doer.ID, c.IssueID, c.ID, timeutil.TimeStampNow(), c.Content, false)
+		historyDate := timeutil.TimeStampNow()
+		if c.Issue.NoAutoTime {
+			historyDate = c.Issue.UpdatedUnix
+		}
+		err := issues_model.SaveIssueContentHistory(ctx, doer.ID, c.IssueID, c.ID, historyDate, c.Content, false)
 		if err != nil {
 			return err
 		}
diff --git a/services/issue/issue.go b/services/issue/issue.go
index b1f418c32e..b577fa189c 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -6,6 +6,7 @@ package issue
 import (
 	"context"
 	"fmt"
+	"time"
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
@@ -17,6 +18,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/storage"
+	"code.gitea.io/gitea/modules/timeutil"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
@@ -290,3 +292,40 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
 
 	return committer.Commit()
 }
+
+// Set the UpdatedUnix date and the NoAutoTime field of an Issue if a non
+// nil 'updated' time is provided
+//
+// In order to set a specific update time, the DB will be updated with
+// NoAutoTime(). A 'NoAutoTime' boolean field in the Issue struct is used to
+// propagate down to the DB update calls the will to apply autoupdate or not.
+func SetIssueUpdateDate(ctx context.Context, issue *issues_model.Issue, updated *time.Time, doer *user_model.User) error {
+	issue.NoAutoTime = false
+	if updated == nil {
+		return nil
+	}
+
+	if err := issue.LoadRepo(ctx); err != nil {
+		return err
+	}
+
+	// Check if the poster is allowed to set an update date
+	perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
+	if err != nil {
+		return err
+	}
+	if !perm.IsAdmin() && !perm.IsOwner() {
+		return fmt.Errorf("user needs to have admin or owner right")
+	}
+
+	// A simple guard against potential inconsistent calls
+	updatedUnix := timeutil.TimeStamp(updated.Unix())
+	if updatedUnix < issue.CreatedUnix || updatedUnix > timeutil.TimeStampNow() {
+		return fmt.Errorf("unallowed update date")
+	}
+
+	issue.UpdatedUnix = updatedUnix
+	issue.NoAutoTime = true
+
+	return nil
+}
diff --git a/services/issue/milestone.go b/services/issue/milestone.go
index ff645744a7..31490c7b03 100644
--- a/services/issue/milestone.go
+++ b/services/issue/milestone.go
@@ -13,6 +13,32 @@ import (
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
+func updateMilestoneCounters(ctx context.Context, issue *issues_model.Issue, id int64) error {
+	if issue.NoAutoTime {
+		// We set the milestone's update date to the max of the
+		// milestone and issue update dates.
+		// Note: we can not call UpdateMilestoneCounters() if the
+		// milestone's update date is to be kept, because that function
+		// auto-updates the dates.
+		milestone, err := issues_model.GetMilestoneByRepoID(ctx, issue.RepoID, id)
+		if err != nil {
+			return fmt.Errorf("GetMilestoneByRepoID: %w", err)
+		}
+		updatedUnix := milestone.UpdatedUnix
+		if issue.UpdatedUnix > updatedUnix {
+			updatedUnix = issue.UpdatedUnix
+		}
+		if err := issues_model.UpdateMilestoneCountersWithDate(ctx, id, updatedUnix); err != nil {
+			return err
+		}
+	} else {
+		if err := issues_model.UpdateMilestoneCounters(ctx, id); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) error {
 	// Only check if milestone exists if we don't remove it.
 	if issue.MilestoneID > 0 {
@@ -30,13 +56,13 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *is
 	}
 
 	if oldMilestoneID > 0 {
-		if err := issues_model.UpdateMilestoneCounters(ctx, oldMilestoneID); err != nil {
+		if err := updateMilestoneCounters(ctx, issue, oldMilestoneID); err != nil {
 			return err
 		}
 	}
 
 	if issue.MilestoneID > 0 {
-		if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
+		if err := updateMilestoneCounters(ctx, issue, issue.MilestoneID); err != nil {
 			return err
 		}
 	}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 2389ec3bee..7e1aef315d 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -6485,6 +6485,13 @@
             "name": "name",
             "in": "query"
           },
+          {
+            "type": "string",
+            "format": "date-time",
+            "description": "time of the attachment's creation. This is a timestamp in RFC 3339 format",
+            "name": "updated_at",
+            "in": "query"
+          },
           {
             "type": "file",
             "description": "attachment to upload",
@@ -7104,6 +7111,13 @@
             "name": "name",
             "in": "query"
           },
+          {
+            "type": "string",
+            "format": "date-time",
+            "description": "time of the attachment's creation. This is a timestamp in RFC 3339 format",
+            "name": "updated_at",
+            "in": "query"
+          },
           {
             "type": "file",
             "description": "attachment to upload",
@@ -8079,6 +8093,13 @@
             "name": "index",
             "in": "path",
             "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/DeleteLabelsOption"
+            }
           }
         ],
         "responses": {
@@ -8134,6 +8155,13 @@
             "name": "id",
             "in": "path",
             "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/DeleteLabelsOption"
+            }
           }
         ],
         "responses": {
@@ -17826,6 +17854,11 @@
         "body": {
           "type": "string",
           "x-go-name": "Body"
+        },
+        "updated_at": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "Updated"
         }
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
@@ -18578,6 +18611,18 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "DeleteLabelsOption": {
+      "description": "DeleteLabelOption options for deleting a label",
+      "type": "object",
+      "properties": {
+        "updated_at": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "Updated"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "DeployKey": {
       "description": "DeployKey a deploy key",
       "type": "object",
@@ -18831,6 +18876,11 @@
         "body": {
           "type": "string",
           "x-go-name": "Body"
+        },
+        "updated_at": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "Updated"
         }
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
@@ -18880,6 +18930,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"
@@ -20294,6 +20349,11 @@
             "format": "int64"
           },
           "x-go-name": "Labels"
+        },
+        "updated_at": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "Updated"
         }
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
diff --git a/tests/integration/api_comment_attachment_test.go b/tests/integration/api_comment_attachment_test.go
index e211376c3c..9761e06987 100644
--- a/tests/integration/api_comment_attachment_test.go
+++ b/tests/integration/api_comment_attachment_test.go
@@ -11,6 +11,7 @@ import (
 	"mime/multipart"
 	"net/http"
 	"testing"
+	"time"
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
@@ -111,6 +112,82 @@ func TestAPICreateCommentAttachment(t *testing.T) {
 	unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID})
 }
 
+func TestAPICreateCommentAttachmentAutoDate(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+	session := loginUser(t, repoOwner.Name)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets?token=%s",
+		repoOwner.Name, repo.Name, comment.ID, token)
+
+	filename := "image.png"
+	buff := generateImg()
+	body := &bytes.Buffer{}
+
+	t.Run("WithAutoDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		// Setup multi-part
+		writer := multipart.NewWriter(body)
+		part, err := writer.CreateFormFile("attachment", filename)
+		assert.NoError(t, err)
+		_, err = io.Copy(part, &buff)
+		assert.NoError(t, err)
+		err = writer.Close()
+		assert.NoError(t, err)
+
+		req := NewRequestWithBody(t, "POST", urlStr, body)
+		req.Header.Add("Content-Type", writer.FormDataContentType())
+		resp := session.MakeRequest(t, req, http.StatusCreated)
+		apiAttachment := new(api.Attachment)
+		DecodeJSON(t, resp, &apiAttachment)
+
+		unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID})
+		// the execution of the API call supposedly lasted less than one minute
+		updatedSince := time.Since(apiAttachment.Created)
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+
+		commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID})
+		updatedSince = time.Since(commentAfter.UpdatedUnix.AsTime())
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+	})
+
+	t.Run("WithUpdateDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
+		urlStr += fmt.Sprintf("&updated_at=%s", updatedAt.UTC().Format(time.RFC3339))
+
+		// Setup multi-part
+		writer := multipart.NewWriter(body)
+		part, err := writer.CreateFormFile("attachment", filename)
+		assert.NoError(t, err)
+		_, err = io.Copy(part, &buff)
+		assert.NoError(t, err)
+		err = writer.Close()
+		assert.NoError(t, err)
+
+		req := NewRequestWithBody(t, "POST", urlStr, body)
+		req.Header.Add("Content-Type", writer.FormDataContentType())
+		resp := session.MakeRequest(t, req, http.StatusCreated)
+		apiAttachment := new(api.Attachment)
+		DecodeJSON(t, resp, &apiAttachment)
+
+		// dates will be converted into the same tz, in order to compare them
+		utcTZ, _ := time.LoadLocation("UTC")
+		unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID})
+		assert.Equal(t, updatedAt.In(utcTZ), apiAttachment.Created.In(utcTZ))
+
+		commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID})
+		assert.Equal(t, updatedAt.In(utcTZ), commentAfter.UpdatedUnix.AsTime().In(utcTZ))
+	})
+}
+
 func TestAPIEditCommentAttachment(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
diff --git a/tests/integration/api_comment_test.go b/tests/integration/api_comment_test.go
index ee648210e5..339ffdbe0e 100644
--- a/tests/integration/api_comment_test.go
+++ b/tests/integration/api_comment_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"net/url"
 	"testing"
+	"time"
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
@@ -110,6 +111,58 @@ func TestAPICreateComment(t *testing.T) {
 	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
 }
 
+func TestAPICreateCommentAutoDate(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{})
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
+
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments?token=%s",
+		repoOwner.Name, repo.Name, issue.Index, token)
+	const commentBody = "Comment body"
+
+	t.Run("WithAutoDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
+			"body": commentBody,
+		})
+		resp := MakeRequest(t, req, http.StatusCreated)
+		var updatedComment api.Comment
+		DecodeJSON(t, resp, &updatedComment)
+
+		// the execution of the API call supposedly lasted less than one minute
+		updatedSince := time.Since(updatedComment.Updated)
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+
+		commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
+		updatedSince = time.Since(commentAfter.UpdatedUnix.AsTime())
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+	})
+
+	t.Run("WithUpdateDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
+
+		req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueCommentOption{
+			Body:    commentBody,
+			Updated: &updatedAt,
+		})
+		resp := MakeRequest(t, req, http.StatusCreated)
+		var updatedComment api.Comment
+		DecodeJSON(t, resp, &updatedComment)
+
+		// dates will be converted into the same tz, in order to compare them
+		utcTZ, _ := time.LoadLocation("UTC")
+		assert.Equal(t, updatedAt.In(utcTZ), updatedComment.Updated.In(utcTZ))
+		commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
+		assert.Equal(t, updatedAt.In(utcTZ), commentAfter.UpdatedUnix.AsTime().In(utcTZ))
+	})
+}
+
 func TestAPIGetComment(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
@@ -161,6 +214,60 @@ func TestAPIEditComment(t *testing.T) {
 	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
 }
 
+func TestAPIEditCommentWithDate(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
+		unittest.Cond("type = ?", issues_model.CommentTypeComment))
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
+
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
+		repoOwner.Name, repo.Name, comment.ID, token)
+	const newCommentBody = "This is the new comment body"
+
+	t.Run("WithAutoDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
+			"body": newCommentBody,
+		})
+		resp := MakeRequest(t, req, http.StatusOK)
+		var updatedComment api.Comment
+		DecodeJSON(t, resp, &updatedComment)
+
+		// the execution of the API call supposedly lasted less than one minute
+		updatedSince := time.Since(updatedComment.Updated)
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+
+		commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
+		updatedSince = time.Since(commentAfter.UpdatedUnix.AsTime())
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+	})
+
+	t.Run("WithUpdateDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
+
+		req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditIssueCommentOption{
+			Body:    newCommentBody,
+			Updated: &updatedAt,
+		})
+		resp := MakeRequest(t, req, http.StatusOK)
+		var updatedComment api.Comment
+		DecodeJSON(t, resp, &updatedComment)
+
+		// dates will be converted into the same tz, in order to compare them
+		utcTZ, _ := time.LoadLocation("UTC")
+		assert.Equal(t, updatedAt.In(utcTZ), updatedComment.Updated.In(utcTZ))
+		commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
+		assert.Equal(t, updatedAt.In(utcTZ), commentAfter.UpdatedUnix.AsTime().In(utcTZ))
+	})
+}
+
 func TestAPIDeleteComment(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
diff --git a/tests/integration/api_issue_attachment_test.go b/tests/integration/api_issue_attachment_test.go
index 3b43ba2c41..2250646354 100644
--- a/tests/integration/api_issue_attachment_test.go
+++ b/tests/integration/api_issue_attachment_test.go
@@ -11,6 +11,7 @@ import (
 	"mime/multipart"
 	"net/http"
 	"testing"
+	"time"
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	issues_model "code.gitea.io/gitea/models/issues"
@@ -100,6 +101,82 @@ func TestAPICreateIssueAttachment(t *testing.T) {
 	unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
 }
 
+func TestAPICreateIssueAttachmentAutoDate(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
+	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+	session := loginUser(t, repoOwner.Name)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
+		repoOwner.Name, repo.Name, issue.Index, token)
+
+	filename := "image.png"
+	buff := generateImg()
+	body := &bytes.Buffer{}
+
+	t.Run("WithAutoDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		// Setup multi-part
+		writer := multipart.NewWriter(body)
+		part, err := writer.CreateFormFile("attachment", filename)
+		assert.NoError(t, err)
+		_, err = io.Copy(part, &buff)
+		assert.NoError(t, err)
+		err = writer.Close()
+		assert.NoError(t, err)
+
+		req := NewRequestWithBody(t, "POST", urlStr, body)
+		req.Header.Add("Content-Type", writer.FormDataContentType())
+		resp := session.MakeRequest(t, req, http.StatusCreated)
+
+		apiAttachment := new(api.Attachment)
+		DecodeJSON(t, resp, &apiAttachment)
+
+		unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
+		// the execution of the API call supposedly lasted less than one minute
+		updatedSince := time.Since(apiAttachment.Created)
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+
+		issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.Index})
+		updatedSince = time.Since(issueAfter.UpdatedUnix.AsTime())
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+	})
+
+	t.Run("WithUpdateDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
+		urlStr += fmt.Sprintf("&updated_at=%s", updatedAt.UTC().Format(time.RFC3339))
+
+		// Setup multi-part
+		writer := multipart.NewWriter(body)
+		part, err := writer.CreateFormFile("attachment", filename)
+		assert.NoError(t, err)
+		_, err = io.Copy(part, &buff)
+		assert.NoError(t, err)
+		err = writer.Close()
+		assert.NoError(t, err)
+
+		req := NewRequestWithBody(t, "POST", urlStr, body)
+		req.Header.Add("Content-Type", writer.FormDataContentType())
+		resp := session.MakeRequest(t, req, http.StatusCreated)
+
+		apiAttachment := new(api.Attachment)
+		DecodeJSON(t, resp, &apiAttachment)
+
+		// dates will be converted into the same tz, in order to compare them
+		utcTZ, _ := time.LoadLocation("UTC")
+		unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
+		assert.Equal(t, updatedAt.In(utcTZ), apiAttachment.Created.In(utcTZ))
+		issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
+		assert.Equal(t, updatedAt.In(utcTZ), issueAfter.UpdatedUnix.AsTime().In(utcTZ))
+	})
+}
+
 func TestAPIEditIssueAttachment(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
diff --git a/tests/integration/api_issue_label_test.go b/tests/integration/api_issue_label_test.go
index d2d8af102b..a29c75727f 100644
--- a/tests/integration/api_issue_label_test.go
+++ b/tests/integration/api_issue_label_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"strings"
 	"testing"
+	"time"
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	issues_model "code.gitea.io/gitea/models/issues"
@@ -15,6 +16,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -111,6 +113,49 @@ func TestAPIAddIssueLabels(t *testing.T) {
 	unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: 2})
 }
 
+func TestAPIAddIssueLabelsAutoDate(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
+	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+	session := loginUser(t, owner.Name)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels?token=%s",
+		owner.Name, repo.Name, issueBefore.Index, token)
+
+	t.Run("WithAutoDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
+			Labels: []int64{1},
+		})
+		MakeRequest(t, req, http.StatusOK)
+
+		issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
+		// the execution of the API call supposedly lasted less than one minute
+		updatedSince := time.Since(issueAfter.UpdatedUnix.AsTime())
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+	})
+
+	t.Run("WithUpdatedDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
+		req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
+			Labels:  []int64{2},
+			Updated: &updatedAt,
+		})
+		MakeRequest(t, req, http.StatusOK)
+
+		// dates will be converted into the same tz, in order to compare them
+		utcTZ, _ := time.LoadLocation("UTC")
+		issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
+		assert.Equal(t, updatedAt.In(utcTZ), issueAfter.UpdatedUnix.AsTime().In(utcTZ))
+	})
+}
+
 func TestAPIReplaceIssueLabels(t *testing.T) {
 	assert.NoError(t, unittest.LoadFixtures())
 
diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go
index 29f09fa09e..e2aa98ad41 100644
--- a/tests/integration/api_issue_test.go
+++ b/tests/integration/api_issue_test.go
@@ -213,6 +213,157 @@ func TestAPIEditIssue(t *testing.T) {
 	assert.Equal(t, title, issueAfter.Title)
 }
 
+func TestAPIEditIssueAutoDate(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 13})
+	repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
+	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
+	assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
+
+	t.Run("WithAutoDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		// User2 is not owner, but can update the 'public' issue with auto date
+		session := loginUser(t, "user2")
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
+		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
+
+		body := "new content!"
+		req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
+			Body: &body,
+		})
+		resp := MakeRequest(t, req, http.StatusCreated)
+		var apiIssue api.Issue
+		DecodeJSON(t, resp, &apiIssue)
+
+		// the execution of the API call supposedly lasted less than one minute
+		updatedSince := time.Since(apiIssue.Updated)
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+
+		issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
+		updatedSince = time.Since(issueAfter.UpdatedUnix.AsTime())
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+	})
+
+	t.Run("WithUpdateDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		// User1 is admin, and so can update the issue without auto date
+		session := loginUser(t, "user1")
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
+		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
+
+		body := "new content, with updated time"
+		updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
+		req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
+			Body:    &body,
+			Updated: &updatedAt,
+		})
+		resp := MakeRequest(t, req, http.StatusCreated)
+		var apiIssue api.Issue
+		DecodeJSON(t, resp, &apiIssue)
+
+		// dates are converted into the same tz, in order to compare them
+		utcTZ, _ := time.LoadLocation("UTC")
+		assert.Equal(t, updatedAt.In(utcTZ), apiIssue.Updated.In(utcTZ))
+
+		issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
+		assert.Equal(t, updatedAt.In(utcTZ), issueAfter.UpdatedUnix.AsTime().In(utcTZ))
+	})
+
+	t.Run("WithoutPermission", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		// User2 is not owner nor admin, and so can't update the issue without auto date
+		session := loginUser(t, "user2")
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
+		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
+
+		body := "new content, with updated time"
+		updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
+		req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
+			Body:    &body,
+			Updated: &updatedAt,
+		})
+		resp := MakeRequest(t, req, http.StatusForbidden)
+		var apiError api.APIError
+		DecodeJSON(t, resp, &apiError)
+
+		assert.Equal(t, "user needs to have admin or owner right", apiError.Message)
+	})
+}
+
+func TestAPIEditIssueMilestoneAutoDate(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+	repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
+
+	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
+	assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
+
+	session := loginUser(t, owner.Name)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
+
+	t.Run("WithAutoDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		milestone := int64(1)
+		req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
+			Milestone: &milestone,
+		})
+		MakeRequest(t, req, http.StatusCreated)
+
+		// the execution of the API call supposedly lasted less than one minute
+		milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
+		updatedSince := time.Since(milestoneAfter.UpdatedUnix.AsTime())
+		assert.LessOrEqual(t, updatedSince, time.Minute)
+	})
+
+	t.Run("WithPostUpdateDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		// Note: the updated_unix field of the test Milestones is set to NULL
+		// Hence, any date is higher than the Milestone's updated date
+		updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
+		milestone := int64(2)
+		req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
+			Milestone: &milestone,
+			Updated:   &updatedAt,
+		})
+		MakeRequest(t, req, http.StatusCreated)
+
+		// the milestone date should be set to 'updatedAt'
+		// dates are converted into the same tz, in order to compare them
+		utcTZ, _ := time.LoadLocation("UTC")
+		milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
+		assert.Equal(t, updatedAt.In(utcTZ), milestoneAfter.UpdatedUnix.AsTime().In(utcTZ))
+	})
+
+	t.Run("WithPastUpdateDate", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		// Note: This Milestone's updated_unix has been set to Now() by the first subtest
+		milestone := int64(1)
+		milestoneBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
+
+		updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
+		req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
+			Milestone: &milestone,
+			Updated:   &updatedAt,
+		})
+		MakeRequest(t, req, http.StatusCreated)
+
+		// the milestone date should not change
+		// dates are converted into the same tz, in order to compare them
+		utcTZ, _ := time.LoadLocation("UTC")
+		milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
+		assert.Equal(t, milestoneAfter.UpdatedUnix.AsTime().In(utcTZ), milestoneBefore.UpdatedUnix.AsTime().In(utcTZ))
+	})
+}
+
 func TestAPISearchIssues(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()