From 40e99ea010b3b0be241be0e1365a70d33777bcf2 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Tue, 24 Dec 2019 03:33:52 +0100
Subject: [PATCH] [API] Extend contents with dates (#9464)

* extend CommitTree func

* make sure Date NOT nil

* spell corection

Co-Authored-By: zeripath <art27@cantab.net>

* add TEST

Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 integrations/api_repo_file_create_test.go | 11 ++++++++
 modules/repofiles/delete.go               |  8 +++++-
 modules/repofiles/temp_repo.go            | 10 +++++---
 modules/repofiles/update.go               | 15 ++++++++++-
 modules/structs/repo_commit.go            | 12 +++++++++
 modules/structs/repo_file.go              |  5 ++--
 routers/api/v1/repo/file.go               | 31 +++++++++++++++++++++++
 routers/api/v1/swagger/options.go         |  3 +++
 templates/swagger/v1_json.tmpl            | 26 +++++++++++++++++++
 9 files changed, 114 insertions(+), 7 deletions(-)

diff --git a/integrations/api_repo_file_create_test.go b/integrations/api_repo_file_create_test.go
index 53042b5d0a..3c8b50d5d1 100644
--- a/integrations/api_repo_file_create_test.go
+++ b/integrations/api_repo_file_create_test.go
@@ -11,6 +11,7 @@ import (
 	"net/url"
 	"path/filepath"
 	"testing"
+	"time"
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
@@ -37,6 +38,10 @@ func getCreateFileOptions() api.CreateFileOptions {
 				Name:  "John Doe",
 				Email: "johndoe@example.com",
 			},
+			Dates: api.CommitDateOptions{
+				Author:    time.Unix(946684810, 0),
+				Committer: time.Unix(978307190, 0),
+			},
 		},
 		Content: contentEncoded,
 	}
@@ -80,12 +85,14 @@ func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileRespon
 					Name:  "Anne Doe",
 					Email: "annedoe@example.com",
 				},
+				Date: "2000-01-01T00:00:10Z",
 			},
 			Committer: &api.CommitUser{
 				Identity: api.Identity{
 					Name:  "John Doe",
 					Email: "johndoe@example.com",
 				},
+				Date: "2000-12-31T23:59:50Z",
 			},
 			Message: "Updates README.md\n",
 		},
@@ -139,6 +146,10 @@ func TestAPICreateFile(t *testing.T) {
 			assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
 			assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
 			assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
+			assert.EqualValues(t, expectedFileResponse.Commit.Author.Date, fileResponse.Commit.Author.Date)
+			assert.EqualValues(t, expectedFileResponse.Commit.Committer.Email, fileResponse.Commit.Committer.Email)
+			assert.EqualValues(t, expectedFileResponse.Commit.Committer.Name, fileResponse.Commit.Committer.Name)
+			assert.EqualValues(t, expectedFileResponse.Commit.Committer.Date, fileResponse.Commit.Committer.Date)
 			gitRepo.Close()
 		}
 
diff --git a/modules/repofiles/delete.go b/modules/repofiles/delete.go
index 95b0804025..43937c49e1 100644
--- a/modules/repofiles/delete.go
+++ b/modules/repofiles/delete.go
@@ -23,6 +23,7 @@ type DeleteRepoFileOptions struct {
 	SHA          string
 	Author       *IdentityOptions
 	Committer    *IdentityOptions
+	Dates        *CommitDateOptions
 }
 
 // DeleteRepoFile deletes a file in the given repository
@@ -168,7 +169,12 @@ func DeleteRepoFile(repo *models.Repository, doer *models.User, opts *DeleteRepo
 	}
 
 	// Now commit the tree
-	commitHash, err := t.CommitTree(author, committer, treeHash, message)
+	var commitHash string
+	if opts.Dates != nil {
+		commitHash, err = t.CommitTreeWithDate(author, committer, treeHash, message, opts.Dates.Author, opts.Dates.Committer)
+	} else {
+		commitHash, err = t.CommitTree(author, committer, treeHash, message)
+	}
 	if err != nil {
 		return nil, err
 	}
diff --git a/modules/repofiles/temp_repo.go b/modules/repofiles/temp_repo.go
index 6bd775d9d2..f9ea4ba155 100644
--- a/modules/repofiles/temp_repo.go
+++ b/modules/repofiles/temp_repo.go
@@ -188,7 +188,11 @@ func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, erro
 
 // CommitTree creates a commit from a given tree for the user with provided message
 func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, treeHash string, message string) (string, error) {
-	commitTimeStr := time.Now().Format(time.RFC3339)
+	return t.CommitTreeWithDate(author, committer, treeHash, message, time.Now(), time.Now())
+}
+
+// CommitTreeWithDate creates a commit from a given tree for the user with provided message
+func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models.User, treeHash string, message string, authorDate, committerDate time.Time) (string, error) {
 	authorSig := author.NewGitSig()
 	committerSig := committer.NewGitSig()
 
@@ -201,10 +205,10 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t
 	env := append(os.Environ(),
 		"GIT_AUTHOR_NAME="+authorSig.Name,
 		"GIT_AUTHOR_EMAIL="+authorSig.Email,
-		"GIT_AUTHOR_DATE="+commitTimeStr,
+		"GIT_AUTHOR_DATE="+authorDate.Format(time.RFC3339),
 		"GIT_COMMITTER_NAME="+committerSig.Name,
 		"GIT_COMMITTER_EMAIL="+committerSig.Email,
-		"GIT_COMMITTER_DATE="+commitTimeStr,
+		"GIT_COMMITTER_DATE="+committerDate.Format(time.RFC3339),
 	)
 
 	messageBytes := new(bytes.Buffer)
diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go
index 4d2f1d5f04..8a95b4422c 100644
--- a/modules/repofiles/update.go
+++ b/modules/repofiles/update.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"path"
 	"strings"
+	"time"
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/cache"
@@ -31,6 +32,12 @@ type IdentityOptions struct {
 	Email string
 }
 
+// CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE
+type CommitDateOptions struct {
+	Author    time.Time
+	Committer time.Time
+}
+
 // UpdateRepoFileOptions holds the repository file update options
 type UpdateRepoFileOptions struct {
 	LastCommitID string
@@ -44,6 +51,7 @@ type UpdateRepoFileOptions struct {
 	IsNewFile    bool
 	Author       *IdentityOptions
 	Committer    *IdentityOptions
+	Dates        *CommitDateOptions
 }
 
 func detectEncodingAndBOM(entry *git.TreeEntry, repo *models.Repository) (string, bool) {
@@ -371,7 +379,12 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
 	}
 
 	// Now commit the tree
-	commitHash, err := t.CommitTree(author, committer, treeHash, message)
+	var commitHash string
+	if opts.Dates != nil {
+		commitHash, err = t.CommitTreeWithDate(author, committer, treeHash, message, opts.Dates.Author, opts.Dates.Committer)
+	} else {
+		commitHash, err = t.CommitTree(author, committer, treeHash, message)
+	}
 	if err != nil {
 		return nil, err
 	}
diff --git a/modules/structs/repo_commit.go b/modules/structs/repo_commit.go
index 9cde2873d4..088ccdf5af 100644
--- a/modules/structs/repo_commit.go
+++ b/modules/structs/repo_commit.go
@@ -5,6 +5,10 @@
 
 package structs
 
+import (
+	"time"
+)
+
 // Identity for a person's identity like an author or committer
 type Identity struct {
 	Name string `json:"name" binding:"MaxSize(100)"`
@@ -42,3 +46,11 @@ type Commit struct {
 	Committer  *User         `json:"committer"`
 	Parents    []*CommitMeta `json:"parents"`
 }
+
+// CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE
+type CommitDateOptions struct {
+	// swagger:strfmt date-time
+	Author time.Time `json:"author"`
+	// swagger:strfmt date-time
+	Committer time.Time `json:"committer"`
+}
diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go
index cb836e2e23..c34923e389 100644
--- a/modules/structs/repo_file.go
+++ b/modules/structs/repo_file.go
@@ -14,8 +14,9 @@ type FileOptions struct {
 	// new_branch (optional) will make a new branch from `branch` before creating the file
 	NewBranchName string `json:"new_branch" binding:"GitRefName;MaxSize(100)"`
 	// `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
-	Author    Identity `json:"author"`
-	Committer Identity `json:"committer"`
+	Author    Identity          `json:"author"`
+	Committer Identity          `json:"committer"`
+	Dates     CommitDateOptions `json:"dates"`
 }
 
 // CreateFileOptions options for creating files
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 8cfe039df5..14923984bd 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -8,6 +8,7 @@ package repo
 import (
 	"encoding/base64"
 	"net/http"
+	"time"
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
@@ -213,6 +214,16 @@ func CreateFile(ctx *context.APIContext, apiOpts api.CreateFileOptions) {
 			Name:  apiOpts.Author.Name,
 			Email: apiOpts.Author.Email,
 		},
+		Dates: &repofiles.CommitDateOptions{
+			Author:    apiOpts.Dates.Author,
+			Committer: apiOpts.Dates.Committer,
+		},
+	}
+	if opts.Dates.Author.IsZero() {
+		opts.Dates.Author = time.Now()
+	}
+	if opts.Dates.Committer.IsZero() {
+		opts.Dates.Committer = time.Now()
 	}
 
 	if opts.Message == "" {
@@ -277,6 +288,16 @@ func UpdateFile(ctx *context.APIContext, apiOpts api.UpdateFileOptions) {
 			Name:  apiOpts.Author.Name,
 			Email: apiOpts.Author.Email,
 		},
+		Dates: &repofiles.CommitDateOptions{
+			Author:    apiOpts.Dates.Author,
+			Committer: apiOpts.Dates.Committer,
+		},
+	}
+	if opts.Dates.Author.IsZero() {
+		opts.Dates.Author = time.Now()
+	}
+	if opts.Dates.Committer.IsZero() {
+		opts.Dates.Committer = time.Now()
 	}
 
 	if opts.Message == "" {
@@ -364,6 +385,16 @@ func DeleteFile(ctx *context.APIContext, apiOpts api.DeleteFileOptions) {
 			Name:  apiOpts.Author.Name,
 			Email: apiOpts.Author.Email,
 		},
+		Dates: &repofiles.CommitDateOptions{
+			Author:    apiOpts.Dates.Author,
+			Committer: apiOpts.Dates.Committer,
+		},
+	}
+	if opts.Dates.Author.IsZero() {
+		opts.Dates.Author = time.Now()
+	}
+	if opts.Dates.Committer.IsZero() {
+		opts.Dates.Committer = time.Now()
 	}
 
 	if opts.Message == "" {
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 80e4bf422a..74a475e275 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -118,6 +118,9 @@ type swaggerParameterBodies struct {
 	// in:body
 	DeleteFileOptions api.DeleteFileOptions
 
+	// in:body
+	CommitDateOptions api.CommitDateOptions
+
 	// in:body
 	RepoTopicOptions api.RepoTopicOptions
 }
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index dc9dd2395f..c4fa1f3112 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -8273,6 +8273,23 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "CommitDateOptions": {
+      "description": "CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE",
+      "type": "object",
+      "properties": {
+        "author": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "Author"
+        },
+        "committer": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "Committer"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "CommitMeta": {
       "type": "object",
       "title": "CommitMeta contains meta information of a commit in terms of API.",
@@ -8414,6 +8431,9 @@
           "type": "string",
           "x-go-name": "Content"
         },
+        "dates": {
+          "$ref": "#/definitions/CommitDateOptions"
+        },
         "message": {
           "description": "message (optional) for the commit of this file. if not supplied, a default message will be used",
           "type": "string",
@@ -8972,6 +8992,9 @@
         "committer": {
           "$ref": "#/definitions/Identity"
         },
+        "dates": {
+          "$ref": "#/definitions/CommitDateOptions"
+        },
         "message": {
           "description": "message (optional) for the commit of this file. if not supplied, a default message will be used",
           "type": "string",
@@ -11303,6 +11326,9 @@
           "type": "string",
           "x-go-name": "Content"
         },
+        "dates": {
+          "$ref": "#/definitions/CommitDateOptions"
+        },
         "from_path": {
           "description": "from_path (optional) is the path of the original file which will be moved/renamed to the path in the URL",
           "type": "string",