From 609c91665e5e4d6da50af0b2168d6cb46f9d6273 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ren=C3=A9=20Schaar?= <rene@schaar.priv.at>
Date: Tue, 15 Feb 2022 17:50:10 +0100
Subject: [PATCH] Fix display time of milestones (#18753)

* Fix display time of milestones

* Move the SecToTime function

From the models/issue_stopwatch.go file to the modules/util package

* Rename the sec_to_time file

* Updated formatting

* Include copyright notice in sec_to_time.go

* Apply PR review suggestions

- Update copyright notice dates to 2022
- Change `1 day 3h 5min 7s` to `1d 3h 5m 7s`

* Rename hrs var and combine conditions

* Update unit tests to match new time pattern

Changed `1min` to `1m`

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/issue_stopwatch.go           | 34 ++--------------------
 models/issue_tracked_time.go        |  9 +++---
 models/issue_tracked_time_test.go   |  6 ++--
 modules/templates/helper.go         |  4 +--
 modules/util/sec_to_time.go         | 44 +++++++++++++++++++++++++++++
 modules/util/sec_to_time_test.go    | 20 +++++++++++++
 routers/web/repo/issue_timetrack.go |  3 +-
 7 files changed, 79 insertions(+), 41 deletions(-)
 create mode 100644 modules/util/sec_to_time.go
 create mode 100644 modules/util/sec_to_time_test.go

diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go
index 530a524218..3be9ad4e3f 100644
--- a/models/issue_stopwatch.go
+++ b/models/issue_stopwatch.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
 )
 
 // ErrIssueStopwatchNotExist represents an error that stopwatch is not exist
@@ -53,7 +54,7 @@ func (s Stopwatch) Seconds() int64 {
 
 // Duration returns a human-readable duration string based on local server time
 func (s Stopwatch) Duration() string {
-	return SecToTime(s.Seconds())
+	return util.SecToTime(s.Seconds())
 }
 
 func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
@@ -164,7 +165,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
 		Doer:    user,
 		Issue:   issue,
 		Repo:    issue.Repo,
-		Content: SecToTime(timediff),
+		Content: util.SecToTime(timediff),
 		Type:    CommentTypeStopTracking,
 		TimeID:  tt.ID,
 	}); err != nil {
@@ -263,32 +264,3 @@ func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) e
 	}
 	return nil
 }
-
-// SecToTime converts an amount of seconds to a human-readable string (example: 66s -> 1min 6s)
-func SecToTime(duration int64) string {
-	seconds := duration % 60
-	minutes := (duration / (60)) % 60
-	hours := duration / (60 * 60)
-
-	var hrs string
-
-	if hours > 0 {
-		hrs = fmt.Sprintf("%dh", hours)
-	}
-	if minutes > 0 {
-		if hours == 0 {
-			hrs = fmt.Sprintf("%dmin", minutes)
-		} else {
-			hrs = fmt.Sprintf("%s %dmin", hrs, minutes)
-		}
-	}
-	if seconds > 0 {
-		if hours == 0 && minutes == 0 {
-			hrs = fmt.Sprintf("%ds", seconds)
-		} else {
-			hrs = fmt.Sprintf("%s %ds", hrs, seconds)
-		}
-	}
-
-	return hrs
-}
diff --git a/models/issue_tracked_time.go b/models/issue_tracked_time.go
index c887baae15..2d7bef19e1 100644
--- a/models/issue_tracked_time.go
+++ b/models/issue_tracked_time.go
@@ -11,6 +11,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
 )
@@ -177,7 +178,7 @@ func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Tim
 		Issue:   issue,
 		Repo:    issue.Repo,
 		Doer:    user,
-		Content: SecToTime(amount),
+		Content: util.SecToTime(amount),
 		Type:    CommentTypeAddTimeManual,
 		TimeID:  t.ID,
 	}); err != nil {
@@ -226,7 +227,7 @@ func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string,
 			}
 			return nil, err
 		}
-		totalTimes[user] = SecToTime(total)
+		totalTimes[user] = util.SecToTime(total)
 	}
 	return totalTimes, nil
 }
@@ -260,7 +261,7 @@ func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error {
 		Issue:   issue,
 		Repo:    issue.Repo,
 		Doer:    user,
-		Content: "- " + SecToTime(removedTime),
+		Content: "- " + util.SecToTime(removedTime),
 		Type:    CommentTypeDeleteTimeManual,
 	}); err != nil {
 		return err
@@ -289,7 +290,7 @@ func DeleteTime(t *TrackedTime) error {
 		Issue:   t.Issue,
 		Repo:    t.Issue.Repo,
 		Doer:    t.User,
-		Content: "- " + SecToTime(t.Time),
+		Content: "- " + util.SecToTime(t.Time),
 		Type:    CommentTypeDeleteTimeManual,
 	}); err != nil {
 		return err
diff --git a/models/issue_tracked_time_test.go b/models/issue_tracked_time_test.go
index 97efd8bb72..e6c9caf900 100644
--- a/models/issue_tracked_time_test.go
+++ b/models/issue_tracked_time_test.go
@@ -34,7 +34,7 @@ func TestAddTime(t *testing.T) {
 	assert.Equal(t, int64(3661), tt.Time)
 
 	comment := unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*Comment)
-	assert.Equal(t, comment.Content, "1h 1min 1s")
+	assert.Equal(t, comment.Content, "1h 1m 1s")
 }
 
 func TestGetTrackedTimes(t *testing.T) {
@@ -86,7 +86,7 @@ func TestTotalTimes(t *testing.T) {
 	assert.Len(t, total, 1)
 	for user, time := range total {
 		assert.Equal(t, int64(1), user.ID)
-		assert.Equal(t, "6min 40s", time)
+		assert.Equal(t, "6m 40s", time)
 	}
 
 	total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 2})
@@ -94,7 +94,7 @@ func TestTotalTimes(t *testing.T) {
 	assert.Len(t, total, 2)
 	for user, time := range total {
 		if user.ID == 2 {
-			assert.Equal(t, "1h 1min 2s", time)
+			assert.Equal(t, "1h 1m 2s", time)
 		} else if user.ID == 1 {
 			assert.Equal(t, "20s", time)
 		} else {
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 255866e2ed..63c165bc8b 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -256,7 +256,7 @@ func NewFuncMap() []template.FuncMap {
 		},
 		"Printf":   fmt.Sprintf,
 		"Escape":   Escape,
-		"Sec2Time": models.SecToTime,
+		"Sec2Time": util.SecToTime,
 		"ParseDeadline": func(deadline string) []string {
 			return strings.Split(deadline, "|")
 		},
@@ -447,7 +447,7 @@ func NewTextFuncMap() []texttmpl.FuncMap {
 		},
 		"Printf":   fmt.Sprintf,
 		"Escape":   Escape,
-		"Sec2Time": models.SecToTime,
+		"Sec2Time": util.SecToTime,
 		"ParseDeadline": func(deadline string) []string {
 			return strings.Split(deadline, "|")
 		},
diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go
new file mode 100644
index 0000000000..657b30cddf
--- /dev/null
+++ b/modules/util/sec_to_time.go
@@ -0,0 +1,44 @@
+// Copyright 2022 Gitea. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package util
+
+import "fmt"
+
+// SecToTime converts an amount of seconds to a human-readable string (example: 66s -> 1min 6s)
+func SecToTime(duration int64) string {
+	seconds := duration % 60
+	minutes := (duration / (60)) % 60
+	hours := duration / (60 * 60) % 24
+	days := duration / (60 * 60) / 24
+
+	var formattedTime string
+
+	if days > 0 {
+		formattedTime = fmt.Sprintf("%dd", days)
+	}
+	if hours > 0 {
+		if formattedTime == "" {
+			formattedTime = fmt.Sprintf("%dh", hours)
+		} else {
+			formattedTime = fmt.Sprintf("%s %dh", formattedTime, hours)
+		}
+	}
+	if minutes > 0 {
+		if formattedTime == "" {
+			formattedTime = fmt.Sprintf("%dm", minutes)
+		} else {
+			formattedTime = fmt.Sprintf("%s %dm", formattedTime, minutes)
+		}
+	}
+	if seconds > 0 {
+		if formattedTime == "" {
+			formattedTime = fmt.Sprintf("%ds", seconds)
+		} else {
+			formattedTime = fmt.Sprintf("%s %ds", formattedTime, seconds)
+		}
+	}
+
+	return formattedTime
+}
diff --git a/modules/util/sec_to_time_test.go b/modules/util/sec_to_time_test.go
new file mode 100644
index 0000000000..915dcbf727
--- /dev/null
+++ b/modules/util/sec_to_time_test.go
@@ -0,0 +1,20 @@
+// Copyright 2022 Gitea. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package util
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSecToTime(t *testing.T) {
+	assert.Equal(t, SecToTime(10), "10s")
+	assert.Equal(t, SecToTime(100), "1m 40s")
+	assert.Equal(t, SecToTime(1000), "16m 40s")
+	assert.Equal(t, SecToTime(10000), "2h 46m 40s")
+	assert.Equal(t, SecToTime(100000), "1d 3h 46m 40s")
+	assert.Equal(t, SecToTime(1000000), "11d 13h 46m 40s")
+}
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
index 3770cd7b4e..ec6bb6142d 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -10,6 +10,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/forms"
 )
@@ -81,6 +82,6 @@ func DeleteTime(c *context.Context) {
 		return
 	}
 
-	c.Flash.Success(c.Tr("repo.issues.del_time_history", models.SecToTime(t.Time)))
+	c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time)))
 	c.Redirect(issue.HTMLURL())
 }