diff options
Diffstat (limited to 'modules')
-rw-r--r-- | modules/auth/auth.go | 6 | ||||
-rw-r--r-- | modules/base/tool.go | 151 | ||||
-rw-r--r-- | modules/base/tool_test.go | 149 | ||||
-rw-r--r-- | modules/migrations/gitea.go | 26 | ||||
-rw-r--r-- | modules/pull/merge.go | 4 | ||||
-rw-r--r-- | modules/setting/setting.go | 71 | ||||
-rw-r--r-- | modules/templates/helper.go | 10 | ||||
-rw-r--r-- | modules/timeutil/language.go | 36 | ||||
-rw-r--r-- | modules/timeutil/since.go | 164 | ||||
-rw-r--r-- | modules/timeutil/since_test.go | 163 | ||||
-rw-r--r-- | modules/timeutil/timestamp.go (renamed from modules/util/time_stamp.go) | 27 |
11 files changed, 449 insertions, 358 deletions
diff --git a/modules/auth/auth.go b/modules/auth/auth.go index 2a2ee40492..1013628073 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -20,7 +20,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/validation" ) @@ -68,7 +68,7 @@ func SignedInID(ctx *macaron.Context, sess session.Store) int64 { } return 0 } - t.UpdatedUnix = util.TimeStampNow() + t.UpdatedUnix = timeutil.TimeStampNow() if err = models.UpdateAccessToken(t); err != nil { log.Error("UpdateAccessToken: %v", err) } @@ -210,7 +210,7 @@ func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) return nil, false } } - token.UpdatedUnix = util.TimeStampNow() + token.UpdatedUnix = timeutil.TimeStampNow() if err = models.UpdateAccessToken(token); err != nil { log.Error("UpdateAccessToken: %v", err) } diff --git a/modules/base/tool.go b/modules/base/tool.go index 2bb39dbfea..43678979a5 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -12,7 +12,6 @@ import ( "encoding/base64" "encoding/hex" "fmt" - "html/template" "io" "math" "net/http" @@ -29,10 +28,8 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "github.com/Unknwon/com" - "github.com/Unknwon/i18n" ) // EncodeMD5 encodes string to md5 hex value. @@ -217,154 +214,6 @@ func AvatarLink(email string) string { return SizedAvatarLink(email, DefaultAvatarSize) } -// Seconds-based time units -const ( - Minute = 60 - Hour = 60 * Minute - Day = 24 * Hour - Week = 7 * Day - Month = 30 * Day - Year = 12 * Month -) - -func computeTimeDiff(diff int64, lang string) (int64, string) { - diffStr := "" - switch { - case diff <= 0: - diff = 0 - diffStr = i18n.Tr(lang, "tool.now") - case diff < 2: - diff = 0 - diffStr = i18n.Tr(lang, "tool.1s") - case diff < 1*Minute: - diffStr = i18n.Tr(lang, "tool.seconds", diff) - diff = 0 - - case diff < 2*Minute: - diff -= 1 * Minute - diffStr = i18n.Tr(lang, "tool.1m") - case diff < 1*Hour: - diffStr = i18n.Tr(lang, "tool.minutes", diff/Minute) - diff -= diff / Minute * Minute - - case diff < 2*Hour: - diff -= 1 * Hour - diffStr = i18n.Tr(lang, "tool.1h") - case diff < 1*Day: - diffStr = i18n.Tr(lang, "tool.hours", diff/Hour) - diff -= diff / Hour * Hour - - case diff < 2*Day: - diff -= 1 * Day - diffStr = i18n.Tr(lang, "tool.1d") - case diff < 1*Week: - diffStr = i18n.Tr(lang, "tool.days", diff/Day) - diff -= diff / Day * Day - - case diff < 2*Week: - diff -= 1 * Week - diffStr = i18n.Tr(lang, "tool.1w") - case diff < 1*Month: - diffStr = i18n.Tr(lang, "tool.weeks", diff/Week) - diff -= diff / Week * Week - - case diff < 2*Month: - diff -= 1 * Month - diffStr = i18n.Tr(lang, "tool.1mon") - case diff < 1*Year: - diffStr = i18n.Tr(lang, "tool.months", diff/Month) - diff -= diff / Month * Month - - case diff < 2*Year: - diff -= 1 * Year - diffStr = i18n.Tr(lang, "tool.1y") - default: - diffStr = i18n.Tr(lang, "tool.years", diff/Year) - diff -= (diff / Year) * Year - } - return diff, diffStr -} - -// MinutesToFriendly returns a user friendly string with number of minutes -// converted to hours and minutes. -func MinutesToFriendly(minutes int, lang string) string { - duration := time.Duration(minutes) * time.Minute - return TimeSincePro(time.Now().Add(-duration), lang) -} - -// TimeSincePro calculates the time interval and generate full user-friendly string. -func TimeSincePro(then time.Time, lang string) string { - return timeSincePro(then, time.Now(), lang) -} - -func timeSincePro(then, now time.Time, lang string) string { - diff := now.Unix() - then.Unix() - - if then.After(now) { - return i18n.Tr(lang, "tool.future") - } - if diff == 0 { - return i18n.Tr(lang, "tool.now") - } - - var timeStr, diffStr string - for { - if diff == 0 { - break - } - - diff, diffStr = computeTimeDiff(diff, lang) - timeStr += ", " + diffStr - } - return strings.TrimPrefix(timeStr, ", ") -} - -func timeSince(then, now time.Time, lang string) string { - return timeSinceUnix(then.Unix(), now.Unix(), lang) -} - -func timeSinceUnix(then, now int64, lang string) string { - lbl := "tool.ago" - diff := now - then - if then > now { - lbl = "tool.from_now" - diff = then - now - } - if diff <= 0 { - return i18n.Tr(lang, "tool.now") - } - - _, diffStr := computeTimeDiff(diff, lang) - return i18n.Tr(lang, lbl, diffStr) -} - -// RawTimeSince retrieves i18n key of time since t -func RawTimeSince(t time.Time, lang string) string { - return timeSince(t, time.Now(), lang) -} - -// TimeSince calculates the time interval and generate user-friendly string. -func TimeSince(then time.Time, lang string) template.HTML { - return htmlTimeSince(then, time.Now(), lang) -} - -func htmlTimeSince(then, now time.Time, lang string) template.HTML { - return template.HTML(fmt.Sprintf(`<span class="time-since" title="%s">%s</span>`, - then.Format(setting.TimeFormat), - timeSince(then, now, lang))) -} - -// TimeSinceUnix calculates the time interval and generate user-friendly string. -func TimeSinceUnix(then util.TimeStamp, lang string) template.HTML { - return htmlTimeSinceUnix(then, util.TimeStamp(time.Now().Unix()), lang) -} - -func htmlTimeSinceUnix(then, now util.TimeStamp, lang string) template.HTML { - return template.HTML(fmt.Sprintf(`<span class="time-since" title="%s">%s</span>`, - then.Format(setting.TimeFormat), - timeSinceUnix(int64(then), int64(now), lang))) -} - // Storage space size types const ( Byte = 1 diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index 3f1eebfc4b..3aa86c5cbf 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -2,43 +2,13 @@ package base import ( "net/url" - "os" "testing" - "time" "code.gitea.io/gitea/modules/setting" - "github.com/Unknwon/i18n" - macaroni18n "github.com/go-macaron/i18n" "github.com/stretchr/testify/assert" ) -var BaseDate time.Time - -// time durations -const ( - DayDur = 24 * time.Hour - WeekDur = 7 * DayDur - MonthDur = 30 * DayDur - YearDur = 12 * MonthDur -) - -func TestMain(m *testing.M) { - // setup - macaroni18n.I18n(macaroni18n.Options{ - Directory: "../../options/locale/", - DefaultLang: "en-US", - Langs: []string{"en-US"}, - Names: []string{"english"}, - }) - BaseDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) - - // run the tests - retVal := m.Run() - - os.Exit(retVal) -} - func TestEncodeMD5(t *testing.T) { assert.Equal(t, "3858f62230ac3c915f300c664312c63f", @@ -131,125 +101,6 @@ func TestAvatarLink(t *testing.T) { ) } -func TestComputeTimeDiff(t *testing.T) { - // test that for each offset in offsets, - // computeTimeDiff(base + offset) == (offset, str) - test := func(base int64, str string, offsets ...int64) { - for _, offset := range offsets { - diff, diffStr := computeTimeDiff(base+offset, "en") - assert.Equal(t, offset, diff) - assert.Equal(t, str, diffStr) - } - } - test(0, "now", 0) - test(1, "1 second", 0) - test(2, "2 seconds", 0) - test(Minute, "1 minute", 0, 1, 30, Minute-1) - test(2*Minute, "2 minutes", 0, Minute-1) - test(Hour, "1 hour", 0, 1, Hour-1) - test(5*Hour, "5 hours", 0, Hour-1) - test(Day, "1 day", 0, 1, Day-1) - test(5*Day, "5 days", 0, Day-1) - test(Week, "1 week", 0, 1, Week-1) - test(3*Week, "3 weeks", 0, 4*Day+25000) - test(Month, "1 month", 0, 1, Month-1) - test(10*Month, "10 months", 0, Month-1) - test(Year, "1 year", 0, Year-1) - test(3*Year, "3 years", 0, Year-1) -} - -func TestMinutesToFriendly(t *testing.T) { - // test that a number of minutes yields the expected string - test := func(expected string, minutes int) { - actual := MinutesToFriendly(minutes, "en") - assert.Equal(t, expected, actual) - } - test("1 minute", 1) - test("2 minutes", 2) - test("1 hour", 60) - test("1 hour, 1 minute", 61) - test("1 hour, 2 minutes", 62) - test("2 hours", 120) -} - -func TestTimeSince(t *testing.T) { - assert.Equal(t, "now", timeSince(BaseDate, BaseDate, "en")) - - // test that each diff in `diffs` yields the expected string - test := func(expected string, diffs ...time.Duration) { - for _, diff := range diffs { - actual := timeSince(BaseDate, BaseDate.Add(diff), "en") - assert.Equal(t, i18n.Tr("en", "tool.ago", expected), actual) - actual = timeSince(BaseDate.Add(diff), BaseDate, "en") - assert.Equal(t, i18n.Tr("en", "tool.from_now", expected), actual) - } - } - test("1 second", time.Second, time.Second+50*time.Millisecond) - test("2 seconds", 2*time.Second, 2*time.Second+50*time.Millisecond) - test("1 minute", time.Minute, time.Minute+30*time.Second) - test("2 minutes", 2*time.Minute, 2*time.Minute+30*time.Second) - test("1 hour", time.Hour, time.Hour+30*time.Minute) - test("2 hours", 2*time.Hour, 2*time.Hour+30*time.Minute) - test("1 day", DayDur, DayDur+12*time.Hour) - test("2 days", 2*DayDur, 2*DayDur+12*time.Hour) - test("1 week", WeekDur, WeekDur+3*DayDur) - test("2 weeks", 2*WeekDur, 2*WeekDur+3*DayDur) - test("1 month", MonthDur, MonthDur+15*DayDur) - test("2 months", 2*MonthDur, 2*MonthDur+15*DayDur) - test("1 year", YearDur, YearDur+6*MonthDur) - test("2 years", 2*YearDur, 2*YearDur+6*MonthDur) -} - -func TestTimeSincePro(t *testing.T) { - assert.Equal(t, "now", timeSincePro(BaseDate, BaseDate, "en")) - - // test that a difference of `diff` yields the expected string - test := func(expected string, diff time.Duration) { - actual := timeSincePro(BaseDate, BaseDate.Add(diff), "en") - assert.Equal(t, expected, actual) - assert.Equal(t, "future", timeSincePro(BaseDate.Add(diff), BaseDate, "en")) - } - test("1 second", time.Second) - test("2 seconds", 2*time.Second) - test("1 minute", time.Minute) - test("1 minute, 1 second", time.Minute+time.Second) - test("1 minute, 59 seconds", time.Minute+59*time.Second) - test("2 minutes", 2*time.Minute) - test("1 hour", time.Hour) - test("1 hour, 1 second", time.Hour+time.Second) - test("1 hour, 59 minutes, 59 seconds", time.Hour+59*time.Minute+59*time.Second) - test("2 hours", 2*time.Hour) - test("1 day", DayDur) - test("1 day, 23 hours, 59 minutes, 59 seconds", - DayDur+23*time.Hour+59*time.Minute+59*time.Second) - test("2 days", 2*DayDur) - test("1 week", WeekDur) - test("2 weeks", 2*WeekDur) - test("1 month", MonthDur) - test("3 months", 3*MonthDur) - test("1 year", YearDur) - test("2 years, 3 months, 1 week, 2 days, 4 hours, 12 minutes, 17 seconds", - 2*YearDur+3*MonthDur+WeekDur+2*DayDur+4*time.Hour+ - 12*time.Minute+17*time.Second) -} - -func TestHtmlTimeSince(t *testing.T) { - setting.TimeFormat = time.UnixDate - // test that `diff` yields a result containing `expected` - test := func(expected string, diff time.Duration) { - actual := htmlTimeSince(BaseDate, BaseDate.Add(diff), "en") - assert.Contains(t, actual, `title="Sat Jan 1 00:00:00 UTC 2000"`) - assert.Contains(t, actual, expected) - } - test("1 second", time.Second) - test("3 minutes", 3*time.Minute+5*time.Second) - test("1 day", DayDur+18*time.Hour) - test("1 week", WeekDur+6*DayDur) - test("3 months", 3*MonthDur+3*WeekDur) - test("2 years", 2*YearDur) - test("3 years", 3*YearDur+11*MonthDur+4*WeekDur) -} - func TestFileSize(t *testing.T) { var size int64 = 512 assert.Equal(t, "512B", FileSize(size)) diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go index 08244e5ab2..ddfc2ca271 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -21,7 +21,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/timeutil" gouuid "github.com/satori/go.uuid" ) @@ -106,12 +106,12 @@ func (g *GiteaLocalUploader) CreateTopics(topics ...string) error { func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) error { var mss = make([]*models.Milestone, 0, len(milestones)) for _, milestone := range milestones { - var deadline util.TimeStamp + var deadline timeutil.TimeStamp if milestone.Deadline != nil { - deadline = util.TimeStamp(milestone.Deadline.Unix()) + deadline = timeutil.TimeStamp(milestone.Deadline.Unix()) } if deadline == 0 { - deadline = util.TimeStamp(time.Date(9999, 1, 1, 0, 0, 0, 0, setting.UILocation).Unix()) + deadline = timeutil.TimeStamp(time.Date(9999, 1, 1, 0, 0, 0, 0, setting.DefaultUILocation).Unix()) } var ms = models.Milestone{ RepoID: g.repo.ID, @@ -121,7 +121,7 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err DeadlineUnix: deadline, } if ms.IsClosed && milestone.Closed != nil { - ms.ClosedDateUnix = util.TimeStamp(milestone.Closed.Unix()) + ms.ClosedDateUnix = timeutil.TimeStamp(milestone.Closed.Unix()) } mss = append(mss, &ms) } @@ -175,7 +175,7 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { IsDraft: release.Draft, IsPrerelease: release.Prerelease, IsTag: false, - CreatedUnix: util.TimeStamp(release.Created.Unix()), + CreatedUnix: timeutil.TimeStamp(release.Created.Unix()), } // calc NumCommits @@ -194,7 +194,7 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { Name: asset.Name, DownloadCount: int64(*asset.DownloadCount), Size: int64(*asset.Size), - CreatedUnix: util.TimeStamp(asset.Created.Unix()), + CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()), } // download attachment @@ -265,10 +265,10 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { IsLocked: issue.IsLocked, MilestoneID: milestoneID, Labels: labels, - CreatedUnix: util.TimeStamp(issue.Created.Unix()), + CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()), } if issue.Closed != nil { - is.ClosedUnix = util.TimeStamp(issue.Closed.Unix()) + is.ClosedUnix = timeutil.TimeStamp(issue.Closed.Unix()) } // TODO: add reactions iss = append(iss, &is) @@ -307,7 +307,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { OriginalAuthor: comment.PosterName, OriginalAuthorID: comment.PosterID, Content: comment.Content, - CreatedUnix: util.TimeStamp(comment.Created.Unix()), + CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()), }) // TODO: Reactions @@ -453,15 +453,15 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR IsClosed: pr.State == "closed", IsLocked: pr.IsLocked, Labels: labels, - CreatedUnix: util.TimeStamp(pr.Created.Unix()), + CreatedUnix: timeutil.TimeStamp(pr.Created.Unix()), }, } if pullRequest.Issue.IsClosed && pr.Closed != nil { - pullRequest.Issue.ClosedUnix = util.TimeStamp(pr.Closed.Unix()) + pullRequest.Issue.ClosedUnix = timeutil.TimeStamp(pr.Closed.Unix()) } if pullRequest.HasMerged && pr.MergedTime != nil { - pullRequest.MergedUnix = util.TimeStamp(pr.MergedTime.Unix()) + pullRequest.MergedUnix = timeutil.TimeStamp(pr.MergedTime.Unix()) pullRequest.MergedCommitID = pr.MergeCommitSHA pullRequest.MergerID = g.doer.ID } diff --git a/modules/pull/merge.go b/modules/pull/merge.go index cf2fb7fc4f..ef154128c7 100644 --- a/modules/pull/merge.go +++ b/modules/pull/merge.go @@ -21,7 +21,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/timeutil" ) // Merge merges pull request to base repository. @@ -258,7 +258,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor return fmt.Errorf("GetBranchCommit: %v", err) } - pr.MergedUnix = util.TimeStampNow() + pr.MergedUnix = timeutil.TimeStampNow() pr.Merger = doer pr.MergerID = doer.ID diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 97bdc03cc9..97db7eaf9e 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -278,6 +278,8 @@ var ( // Time settings TimeFormat string + // UILocation is the location on the UI, so that we can display the time on UI. + DefaultUILocation = time.Local CSRFCookieName = "_csrf" CSRFCookieHTTPOnly = true @@ -356,10 +358,6 @@ var ( HasRobotsTxt bool InternalToken string // internal access token IterateBufferSize int - - // UILocation is the location on the UI, so that we can display the time on UI. - // Currently only show the default time.Local, it could be added to app.ini after UI is ready - UILocation = time.Local ) // DateLang transforms standard language locale name to corresponding value in datetime plugin. @@ -792,32 +790,47 @@ func NewContext() { AttachmentMaxFiles = sec.Key("MAX_FILES").MustInt(5) AttachmentEnabled = sec.Key("ENABLED").MustBool(true) - TimeFormatKey := Cfg.Section("time").Key("FORMAT").MustString("RFC1123") - TimeFormat = map[string]string{ - "ANSIC": time.ANSIC, - "UnixDate": time.UnixDate, - "RubyDate": time.RubyDate, - "RFC822": time.RFC822, - "RFC822Z": time.RFC822Z, - "RFC850": time.RFC850, - "RFC1123": time.RFC1123, - "RFC1123Z": time.RFC1123Z, - "RFC3339": time.RFC3339, - "RFC3339Nano": time.RFC3339Nano, - "Kitchen": time.Kitchen, - "Stamp": time.Stamp, - "StampMilli": time.StampMilli, - "StampMicro": time.StampMicro, - "StampNano": time.StampNano, - }[TimeFormatKey] - // When the TimeFormatKey does not exist in the previous map e.g.'2006-01-02 15:04:05' - if len(TimeFormat) == 0 { - TimeFormat = TimeFormatKey - TestTimeFormat, _ := time.Parse(TimeFormat, TimeFormat) - if TestTimeFormat.Format(time.RFC3339) != "2006-01-02T15:04:05Z" { - log.Fatal("Can't create time properly, please check your time format has 2006, 01, 02, 15, 04 and 05") + timeFormatKey := Cfg.Section("time").Key("FORMAT").MustString("") + if timeFormatKey != "" { + TimeFormat = map[string]string{ + "ANSIC": time.ANSIC, + "UnixDate": time.UnixDate, + "RubyDate": time.RubyDate, + "RFC822": time.RFC822, + "RFC822Z": time.RFC822Z, + "RFC850": time.RFC850, + "RFC1123": time.RFC1123, + "RFC1123Z": time.RFC1123Z, + "RFC3339": time.RFC3339, + "RFC3339Nano": time.RFC3339Nano, + "Kitchen": time.Kitchen, + "Stamp": time.Stamp, + "StampMilli": time.StampMilli, + "StampMicro": time.StampMicro, + "StampNano": time.StampNano, + }[timeFormatKey] + // When the TimeFormatKey does not exist in the previous map e.g.'2006-01-02 15:04:05' + if len(TimeFormat) == 0 { + TimeFormat = timeFormatKey + TestTimeFormat, _ := time.Parse(TimeFormat, TimeFormat) + if TestTimeFormat.Format(time.RFC3339) != "2006-01-02T15:04:05Z" { + log.Fatal("Can't create time properly, please check your time format has 2006, 01, 02, 15, 04 and 05") + } + log.Trace("Custom TimeFormat: %s", TimeFormat) + } + } + + zone := Cfg.Section("time").Key("DEFAULT_UI_LOCATION").String() + if zone != "" { + DefaultUILocation, err = time.LoadLocation(zone) + if err != nil { + log.Fatal("Load time zone failed: %v", err) + } else { + log.Info("Default UI Location is %v", zone) } - log.Trace("Custom TimeFormat: %s", TimeFormat) + } + if DefaultUILocation == nil { + DefaultUILocation = time.Local } RunUser = Cfg.Section("").Key("RUN_USER").MustString(user.CurrentUsername()) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 17df375a80..e0285808f8 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -20,13 +20,13 @@ import ( "strings" "time" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "gopkg.in/editorconfig/editorconfig-core-go.v1" ) @@ -74,9 +74,9 @@ func NewFuncMap() []template.FuncMap { "Safe": Safe, "SafeJS": SafeJS, "Str2html": Str2html, - "TimeSince": base.TimeSince, - "TimeSinceUnix": base.TimeSinceUnix, - "RawTimeSince": base.RawTimeSince, + "TimeSince": timeutil.TimeSince, + "TimeSinceUnix": timeutil.TimeSinceUnix, + "RawTimeSince": timeutil.RawTimeSince, "FileSize": base.FileSize, "Subtract": base.Subtract, "EntryIcon": base.EntryIcon, diff --git a/modules/timeutil/language.go b/modules/timeutil/language.go new file mode 100644 index 0000000000..121b50f277 --- /dev/null +++ b/modules/timeutil/language.go @@ -0,0 +1,36 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package timeutil + +import ( + "time" + + "code.gitea.io/gitea/modules/setting" +) + +var ( + langTimeFormats = map[string]string{ + "zh-CN": "2006年01月02日 15时04分05秒", + "en-US": time.RFC1123, + "lv-LV": "02.01.2006. 15:04:05", + } +) + +// GetLangTimeFormat represents the default time format for the language +func GetLangTimeFormat(lang string) string { + return langTimeFormats[lang] +} + +// GetTimeFormat represents the +func GetTimeFormat(lang string) string { + if setting.TimeFormat == "" { + format := GetLangTimeFormat(lang) + if format == "" { + format = time.RFC1123 + } + return format + } + return setting.TimeFormat +} diff --git a/modules/timeutil/since.go b/modules/timeutil/since.go new file mode 100644 index 0000000000..9ba111e54e --- /dev/null +++ b/modules/timeutil/since.go @@ -0,0 +1,164 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package timeutil + +import ( + "fmt" + "html/template" + "strings" + "time" + + "code.gitea.io/gitea/modules/setting" + + "github.com/Unknwon/i18n" +) + +// Seconds-based time units +const ( + Minute = 60 + Hour = 60 * Minute + Day = 24 * Hour + Week = 7 * Day + Month = 30 * Day + Year = 12 * Month +) + +func computeTimeDiff(diff int64, lang string) (int64, string) { + diffStr := "" + switch { + case diff <= 0: + diff = 0 + diffStr = i18n.Tr(lang, "tool.now") + case diff < 2: + diff = 0 + diffStr = i18n.Tr(lang, "tool.1s") + case diff < 1*Minute: + diffStr = i18n.Tr(lang, "tool.seconds", diff) + diff = 0 + + case diff < 2*Minute: + diff -= 1 * Minute + diffStr = i18n.Tr(lang, "tool.1m") + case diff < 1*Hour: + diffStr = i18n.Tr(lang, "tool.minutes", diff/Minute) + diff -= diff / Minute * Minute + + case diff < 2*Hour: + diff -= 1 * Hour + diffStr = i18n.Tr(lang, "tool.1h") + case diff < 1*Day: + diffStr = i18n.Tr(lang, "tool.hours", diff/Hour) + diff -= diff / Hour * Hour + + case diff < 2*Day: + diff -= 1 * Day + diffStr = i18n.Tr(lang, "tool.1d") + case diff < 1*Week: + diffStr = i18n.Tr(lang, "tool.days", diff/Day) + diff -= diff / Day * Day + + case diff < 2*Week: + diff -= 1 * Week + diffStr = i18n.Tr(lang, "tool.1w") + case diff < 1*Month: + diffStr = i18n.Tr(lang, "tool.weeks", diff/Week) + diff -= diff / Week * Week + + case diff < 2*Month: + diff -= 1 * Month + diffStr = i18n.Tr(lang, "tool.1mon") + case diff < 1*Year: + diffStr = i18n.Tr(lang, "tool.months", diff/Month) + diff -= diff / Month * Month + + case diff < 2*Year: + diff -= 1 * Year + diffStr = i18n.Tr(lang, "tool.1y") + default: + diffStr = i18n.Tr(lang, "tool.years", diff/Year) + diff -= (diff / Year) * Year + } + return diff, diffStr +} + +// MinutesToFriendly returns a user friendly string with number of minutes +// converted to hours and minutes. +func MinutesToFriendly(minutes int, lang string) string { + duration := time.Duration(minutes) * time.Minute + return TimeSincePro(time.Now().Add(-duration), lang) +} + +// TimeSincePro calculates the time interval and generate full user-friendly string. +func TimeSincePro(then time.Time, lang string) string { + return timeSincePro(then, time.Now(), lang) +} + +func timeSincePro(then, now time.Time, lang string) string { + diff := now.Unix() - then.Unix() + + if then.After(now) { + return i18n.Tr(lang, "tool.future") + } + if diff == 0 { + return i18n.Tr(lang, "tool.now") + } + + var timeStr, diffStr string + for { + if diff == 0 { + break + } + + diff, diffStr = computeTimeDiff(diff, lang) + timeStr += ", " + diffStr + } + return strings.TrimPrefix(timeStr, ", ") +} + +func timeSince(then, now time.Time, lang string) string { + return timeSinceUnix(then.Unix(), now.Unix(), lang) +} + +func timeSinceUnix(then, now int64, lang string) string { + lbl := "tool.ago" + diff := now - then + if then > now { + lbl = "tool.from_now" + diff = then - now + } + if diff <= 0 { + return i18n.Tr(lang, "tool.now") + } + + _, diffStr := computeTimeDiff(diff, lang) + return i18n.Tr(lang, lbl, diffStr) +} + +// RawTimeSince retrieves i18n key of time since t +func RawTimeSince(t time.Time, lang string) string { + return timeSince(t, time.Now(), lang) +} + +// TimeSince calculates the time interval and generate user-friendly string. +func TimeSince(then time.Time, lang string) template.HTML { + return htmlTimeSince(then, time.Now(), lang) +} + +func htmlTimeSince(then, now time.Time, lang string) template.HTML { + return template.HTML(fmt.Sprintf(`<span class="time-since" title="%s">%s</span>`, + then.In(setting.DefaultUILocation).Format(GetTimeFormat(lang)), + timeSince(then, now, lang))) +} + +// TimeSinceUnix calculates the time interval and generate user-friendly string. +func TimeSinceUnix(then TimeStamp, lang string) template.HTML { + return htmlTimeSinceUnix(then, TimeStamp(time.Now().Unix()), lang) +} + +func htmlTimeSinceUnix(then, now TimeStamp, lang string) template.HTML { + return template.HTML(fmt.Sprintf(`<span class="time-since" title="%s">%s</span>`, + then.FormatInLocation(GetTimeFormat(lang), setting.DefaultUILocation), + timeSinceUnix(int64(then), int64(now), lang))) +} diff --git a/modules/timeutil/since_test.go b/modules/timeutil/since_test.go new file mode 100644 index 0000000000..c016e4ac65 --- /dev/null +++ b/modules/timeutil/since_test.go @@ -0,0 +1,163 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package timeutil + +import ( + "os" + "testing" + "time" + + "code.gitea.io/gitea/modules/setting" + + "github.com/Unknwon/i18n" + macaroni18n "github.com/go-macaron/i18n" + "github.com/stretchr/testify/assert" +) + +var BaseDate time.Time + +// time durations +const ( + DayDur = 24 * time.Hour + WeekDur = 7 * DayDur + MonthDur = 30 * DayDur + YearDur = 12 * MonthDur +) + +func TestMain(m *testing.M) { + // setup + macaroni18n.I18n(macaroni18n.Options{ + Directory: "../../options/locale/", + DefaultLang: "en-US", + Langs: []string{"en-US"}, + Names: []string{"english"}, + }) + BaseDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) + + // run the tests + retVal := m.Run() + + os.Exit(retVal) +} + +func TestTimeSince(t *testing.T) { + assert.Equal(t, "now", timeSince(BaseDate, BaseDate, "en")) + + // test that each diff in `diffs` yields the expected string + test := func(expected string, diffs ...time.Duration) { + for _, diff := range diffs { + actual := timeSince(BaseDate, BaseDate.Add(diff), "en") + assert.Equal(t, i18n.Tr("en", "tool.ago", expected), actual) + actual = timeSince(BaseDate.Add(diff), BaseDate, "en") + assert.Equal(t, i18n.Tr("en", "tool.from_now", expected), actual) + } + } + test("1 second", time.Second, time.Second+50*time.Millisecond) + test("2 seconds", 2*time.Second, 2*time.Second+50*time.Millisecond) + test("1 minute", time.Minute, time.Minute+30*time.Second) + test("2 minutes", 2*time.Minute, 2*time.Minute+30*time.Second) + test("1 hour", time.Hour, time.Hour+30*time.Minute) + test("2 hours", 2*time.Hour, 2*time.Hour+30*time.Minute) + test("1 day", DayDur, DayDur+12*time.Hour) + test("2 days", 2*DayDur, 2*DayDur+12*time.Hour) + test("1 week", WeekDur, WeekDur+3*DayDur) + test("2 weeks", 2*WeekDur, 2*WeekDur+3*DayDur) + test("1 month", MonthDur, MonthDur+15*DayDur) + test("2 months", 2*MonthDur, 2*MonthDur+15*DayDur) + test("1 year", YearDur, YearDur+6*MonthDur) + test("2 years", 2*YearDur, 2*YearDur+6*MonthDur) +} + +func TestTimeSincePro(t *testing.T) { + assert.Equal(t, "now", timeSincePro(BaseDate, BaseDate, "en")) + + // test that a difference of `diff` yields the expected string + test := func(expected string, diff time.Duration) { + actual := timeSincePro(BaseDate, BaseDate.Add(diff), "en") + assert.Equal(t, expected, actual) + assert.Equal(t, "future", timeSincePro(BaseDate.Add(diff), BaseDate, "en")) + } + test("1 second", time.Second) + test("2 seconds", 2*time.Second) + test("1 minute", time.Minute) + test("1 minute, 1 second", time.Minute+time.Second) + test("1 minute, 59 seconds", time.Minute+59*time.Second) + test("2 minutes", 2*time.Minute) + test("1 hour", time.Hour) + test("1 hour, 1 second", time.Hour+time.Second) + test("1 hour, 59 minutes, 59 seconds", time.Hour+59*time.Minute+59*time.Second) + test("2 hours", 2*time.Hour) + test("1 day", DayDur) + test("1 day, 23 hours, 59 minutes, 59 seconds", + DayDur+23*time.Hour+59*time.Minute+59*time.Second) + test("2 days", 2*DayDur) + test("1 week", WeekDur) + test("2 weeks", 2*WeekDur) + test("1 month", MonthDur) + test("3 months", 3*MonthDur) + test("1 year", YearDur) + test("2 years, 3 months, 1 week, 2 days, 4 hours, 12 minutes, 17 seconds", + 2*YearDur+3*MonthDur+WeekDur+2*DayDur+4*time.Hour+ + 12*time.Minute+17*time.Second) +} + +func TestHtmlTimeSince(t *testing.T) { + setting.TimeFormat = time.UnixDate + setting.DefaultUILocation = time.UTC + // test that `diff` yields a result containing `expected` + test := func(expected string, diff time.Duration) { + actual := htmlTimeSince(BaseDate, BaseDate.Add(diff), "en") + assert.Contains(t, actual, `title="Sat Jan 1 00:00:00 UTC 2000"`) + assert.Contains(t, actual, expected) + } + test("1 second", time.Second) + test("3 minutes", 3*time.Minute+5*time.Second) + test("1 day", DayDur+18*time.Hour) + test("1 week", WeekDur+6*DayDur) + test("3 months", 3*MonthDur+3*WeekDur) + test("2 years", 2*YearDur) + test("3 years", 3*YearDur+11*MonthDur+4*WeekDur) +} + +func TestComputeTimeDiff(t *testing.T) { + // test that for each offset in offsets, + // computeTimeDiff(base + offset) == (offset, str) + test := func(base int64, str string, offsets ...int64) { + for _, offset := range offsets { + diff, diffStr := computeTimeDiff(base+offset, "en") + assert.Equal(t, offset, diff) + assert.Equal(t, str, diffStr) + } + } + test(0, "now", 0) + test(1, "1 second", 0) + test(2, "2 seconds", 0) + test(Minute, "1 minute", 0, 1, 30, Minute-1) + test(2*Minute, "2 minutes", 0, Minute-1) + test(Hour, "1 hour", 0, 1, Hour-1) + test(5*Hour, "5 hours", 0, Hour-1) + test(Day, "1 day", 0, 1, Day-1) + test(5*Day, "5 days", 0, Day-1) + test(Week, "1 week", 0, 1, Week-1) + test(3*Week, "3 weeks", 0, 4*Day+25000) + test(Month, "1 month", 0, 1, Month-1) + test(10*Month, "10 months", 0, Month-1) + test(Year, "1 year", 0, Year-1) + test(3*Year, "3 years", 0, Year-1) +} + +func TestMinutesToFriendly(t *testing.T) { + // test that a number of minutes yields the expected string + test := func(expected string, minutes int) { + actual := MinutesToFriendly(minutes, "en") + assert.Equal(t, expected, actual) + } + test("1 minute", 1) + test("2 minutes", 2) + test("1 hour", 60) + test("1 hour, 1 minute", 61) + test("1 hour, 2 minutes", 62) + test("2 hours", 120) +} diff --git a/modules/util/time_stamp.go b/modules/timeutil/timestamp.go index 56f41882f5..f70da9db74 100644 --- a/modules/util/time_stamp.go +++ b/modules/timeutil/timestamp.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package util +package timeutil import ( "time" @@ -35,19 +35,34 @@ func (ts TimeStamp) Year() int { // AsTime convert timestamp as time.Time in Local locale func (ts TimeStamp) AsTime() (tm time.Time) { - tm = time.Unix(int64(ts), 0).In(setting.UILocation) + return ts.AsTimeInLocation(setting.DefaultUILocation) +} + +// AsTimeInLocation convert timestamp as time.Time in Local locale +func (ts TimeStamp) AsTimeInLocation(loc *time.Location) (tm time.Time) { + tm = time.Unix(int64(ts), 0).In(loc) return } // AsTimePtr convert timestamp as *time.Time in Local locale func (ts TimeStamp) AsTimePtr() *time.Time { - tm := time.Unix(int64(ts), 0).In(setting.UILocation) + return ts.AsTimePtrInLocation(setting.DefaultUILocation) +} + +// AsTimePtrInLocation convert timestamp as *time.Time in customize location +func (ts TimeStamp) AsTimePtrInLocation(loc *time.Location) *time.Time { + tm := time.Unix(int64(ts), 0).In(loc) return &tm } -// Format formats timestamp as +// Format formats timestamp as given format func (ts TimeStamp) Format(f string) string { - return ts.AsTime().Format(f) + return ts.FormatInLocation(f, setting.DefaultUILocation) +} + +// FormatInLocation formats timestamp as given format with spiecific location +func (ts TimeStamp) FormatInLocation(f string, loc *time.Location) string { + return ts.AsTimeInLocation(loc).Format(f) } // FormatLong formats as RFC1123Z @@ -62,5 +77,5 @@ func (ts TimeStamp) FormatShort() string { // IsZero is zero time func (ts TimeStamp) IsZero() bool { - return ts.AsTime().IsZero() + return ts.AsTimeInLocation(time.Local).IsZero() } |