From e1e95ef49690004851bb01c6895bdd2a3482808f Mon Sep 17 00:00:00 2001 From: Matthias Sohn Date: Wed, 27 Mar 2013 10:44:28 -0400 Subject: [PATCH] Option to show relative times in changes table Derived from the original implementation in JGit's RelativeDateFormatter and adapted to the GWT environment. A new preference setting allows the user to decide if he wants absolute or relative dates. Myself as the original author and SAP agree to relicense the code borrowed from JGit under Apache 2 license for use in Gerrit Code Review. Change-Id: Id130f76a5937ad0d4f2e9a1b5f9f805f301b782c Signed-off-by: Matthias Sohn --- .../org.eclipse.core.resources.prefs | 1 + .../com/google/gerrit/client/FormatUtil.java | 5 + .../gerrit/client/RelativeDateFormatter.java | 109 ++++++++++++++++++ .../client/account/AccountConstants.java | 1 + .../account/AccountConstants.properties | 1 + .../client/account/MyPreferencesScreen.java | 14 ++- .../client/changes/ChangeConstants.java | 6 + .../client/changes/ChangeConstants.properties | 6 + .../gerrit/client/changes/ChangeMessages.java | 11 ++ .../client/changes/ChangeMessages.properties | 10 ++ .../gerrit/client/changes/ChangeTable2.java | 9 +- .../client/RelativeDateFormatterTest.java | 98 ++++++++++++++++ .../client/AccountGeneralPreferences.java | 12 ++ .../gerrit/server/schema/SchemaVersion.java | 2 +- .../gerrit/server/schema/Schema_78.java | 26 +++++ 15 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java create mode 100644 gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java diff --git a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs index e9441bb123..f9fe34593f 100644 --- a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs +++ b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs @@ -1,3 +1,4 @@ eclipse.preferences.version=1 encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 encoding/=UTF-8 diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java index bc4009782b..ee9fa4f7c1 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java @@ -119,6 +119,11 @@ public class FormatUtil { } } + /** Format a date using git log's relative date format. */ + public static String relativeFormat(Date dt) { + return RelativeDateFormatter.format(dt); + } + @Deprecated public static String nameEmail(com.google.gerrit.common.data.AccountInfo acct) { return nameEmail(asInfo(acct)); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java new file mode 100644 index 0000000000..3298a06ca8 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java @@ -0,0 +1,109 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.client; + +import com.google.gerrit.client.changes.Util; + +import java.util.Date; + +/** + * Formatter to format timestamps relative to the current time using time units + * in the format defined by {@code git log --relative-date}. + */ +public class RelativeDateFormatter { + final static long SECOND_IN_MILLIS = 1000; + + final static long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS; + + final static long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS; + + final static long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS; + + final static long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS; + + final static long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS; + + final static long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS; + + /** + * @param when {@link Date} to format + * @return age of given {@link Date} compared to now formatted in the same + * relative format as returned by {@code git log --relative-date} + */ + @SuppressWarnings("boxing") + public static String format(Date when) { + long ageMillis = (new Date()).getTime() - when.getTime(); + + // shouldn't happen in a perfect world + if (ageMillis < 0) return Util.C.inTheFuture(); + + // seconds + if (ageMillis < upperLimit(MINUTE_IN_MILLIS)) { + return Util.M.secondsAgo(round(ageMillis, SECOND_IN_MILLIS)); + } + + // minutes + if (ageMillis < upperLimit(HOUR_IN_MILLIS)) { + return Util.M.minutesAgo(round(ageMillis, MINUTE_IN_MILLIS)); + } + + // hours + if (ageMillis < upperLimit(DAY_IN_MILLIS)) { + return Util.M.hoursAgo(round(ageMillis, HOUR_IN_MILLIS)); + } + + // up to 14 days use days + if (ageMillis < 14 * DAY_IN_MILLIS) { + return Util.M.daysAgo(round(ageMillis, DAY_IN_MILLIS)); + } + + // up to 10 weeks use weeks + if (ageMillis < 10 * WEEK_IN_MILLIS) { + return Util.M.weeksAgo(round(ageMillis, WEEK_IN_MILLIS)); + } + + // months + if (ageMillis < YEAR_IN_MILLIS) { + return Util.M.monthsAgo(round(ageMillis, MONTH_IN_MILLIS)); + } + + // up to 5 years use "year, months" rounded to months + if (ageMillis < 5 * YEAR_IN_MILLIS) { + long years = ageMillis / YEAR_IN_MILLIS; + String yearLabel = (years > 1) ? Util.C.years() : Util.C.year(); + long months = round(ageMillis % YEAR_IN_MILLIS, MONTH_IN_MILLIS); + String monthLabel = + (months > 1) ? Util.C.months() : (months == 1 ? Util.C.month() : ""); + if (months == 0) { + return Util.M.years0MonthsAgo(years, yearLabel); + } else { + return Util.M.yearsMonthsAgo(years, yearLabel, months, monthLabel); + } + } + + // years + return Util.M.yearsAgo(round(ageMillis, YEAR_IN_MILLIS)); + } + + private static long upperLimit(long unit) { + long limit = unit + unit / 2; + return limit; + } + + private static long round(long n, long unit) { + long rounded = (n + unit / 2) / unit; + return rounded; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java index fa2c5fdaa7..06f0a4cf0a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java @@ -33,6 +33,7 @@ public interface AccountConstants extends Constants { String reversePatchSetOrder(); String showUsernameInReviewCategory(); String buttonSaveChanges(); + String showRelativeDateInChangeTable(); String tabAccountSummary(); String tabPreferences(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties index fd54363387..b5d42c7749 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties @@ -14,6 +14,7 @@ maximumPageSizeFieldLabel = Maximum Page Size: dateFormatLabel = Date/Time Format: contextWholeFile = Whole File buttonSaveChanges = Save Changes +showRelativeDateInChangeTable = Show Relative Dates in Changes Table tabAccountSummary = Profile tabPreferences = Preferences diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java index c17a0aac2e..b7452979b2 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java @@ -42,6 +42,7 @@ public class MyPreferencesScreen extends SettingsScreen { private CheckBox copySelfOnEmails; private CheckBox reversePatchSetOrder; private CheckBox showUsernameInReviewCategory; + private CheckBox relativeDateInChangeTable; private ListBox maximumPageSize; private ListBox dateFormat; private ListBox timeFormat; @@ -94,7 +95,10 @@ public class MyPreferencesScreen extends SettingsScreen { dateTimePanel.add(dateFormat); dateTimePanel.add(timeFormat); } - final Grid formGrid = new Grid(7, 2); + + relativeDateInChangeTable = new CheckBox(Util.C.showRelativeDateInChangeTable()); + + final Grid formGrid = new Grid(8, 2); int row = 0; formGrid.setText(row, labelIdx, ""); @@ -125,6 +129,10 @@ public class MyPreferencesScreen extends SettingsScreen { formGrid.setWidget(row, fieldIdx, dateTimePanel); row++; + formGrid.setText(row, labelIdx, ""); + formGrid.setWidget(row, fieldIdx, relativeDateInChangeTable); + row++; + add(formGrid); save = new Button(Util.C.buttonSaveChanges()); @@ -146,6 +154,7 @@ public class MyPreferencesScreen extends SettingsScreen { e.listenTo(maximumPageSize); e.listenTo(dateFormat); e.listenTo(timeFormat); + e.listenTo(relativeDateInChangeTable); } @Override @@ -167,6 +176,7 @@ public class MyPreferencesScreen extends SettingsScreen { maximumPageSize.setEnabled(on); dateFormat.setEnabled(on); timeFormat.setEnabled(on); + relativeDateInChangeTable.setEnabled(on); } private void display(final AccountGeneralPreferences p) { @@ -180,6 +190,7 @@ public class MyPreferencesScreen extends SettingsScreen { p.getDateFormat()); setListBox(timeFormat, AccountGeneralPreferences.TimeFormat.HHMM_12, // p.getTimeFormat()); + relativeDateInChangeTable.setValue(p.isRelativeDateInChangeTable()); } private void setListBox(final ListBox f, final short defaultValue, @@ -243,6 +254,7 @@ public class MyPreferencesScreen extends SettingsScreen { p.setTimeFormat(getListBox(timeFormat, AccountGeneralPreferences.TimeFormat.HHMM_12, AccountGeneralPreferences.TimeFormat.values())); + p.setRelativeDateInChangeTable(relativeDateInChangeTable.getValue()); enable(false); save.setEnabled(false); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java index 65b06343c6..b4ec733e17 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java @@ -171,4 +171,10 @@ public interface ChangeConstants extends Constants { String diffAllSideBySide(); String diffAllUnified(); + + String inTheFuture(); + String month(); + String months(); + String year(); + String years(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties index a3719d3bab..5d9284461d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties @@ -152,3 +152,9 @@ buttonClose = Close diffAllSideBySide = All Side-by-Side diffAllUnified = All Unified + +inTheFuture = in the future +month = month +months = months +years = years +year = year diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java index ba46702ebf..098fe0782d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java @@ -56,4 +56,15 @@ public interface ChangeMessages extends Messages { String groupIsNotAllowed(String group); String groupHasTooManyMembers(String group); String groupManyMembersConfirmation(String group, int memberCount); + + String secondsAgo(long seconds); + String minutesAgo(long minutes); + String hoursAgo(long hours); + String daysAgo(long days); + String weeksAgo(long weeks); + String monthsAgo(long months); + String yearsAgo(long years); + String years0MonthsAgo(long years, String yearLabel); + String yearsMonthsAgo(long years, String yearLabel, long months, + String monthLabel); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties index ffa749df84..6c99081ba0 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties @@ -39,3 +39,13 @@ groupIsEmpty = The group {0} does not have any members to add as reviewers. groupIsNotAllowed = The group {0} cannot be added as reviewer. groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers. groupManyMembersConfirmation = The group {0} has {1} members. Do you want to add them all as reviewers? + +secondsAgo = {0} seconds ago +minutesAgo = {0} minutes ago +hoursAgo = {0} hours ago +daysAgo = {0} days ago +weeksAgo = {0} weeks ago +monthsAgo = {0} months ago +years0MonthsAgo = {0} {1} ago +yearsMonthsAgo = {0} {1}, {2} {3} ago +yearsAgo = {0} years ago diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java index 03cc11d5de..07f0c114db 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java @@ -14,6 +14,7 @@ package com.google.gerrit.client.changes; +import static com.google.gerrit.client.FormatUtil.relativeFormat; import static com.google.gerrit.client.FormatUtil.shortFormat; import com.google.gerrit.client.Gerrit; @@ -206,7 +207,13 @@ public class ChangeTable2 extends NavigationTable { row, C_PROJECT, new ProjectLink(c.project_name_key(), c.status())); table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c .status(), c.branch(), c.topic())); - table.setText(row, C_LAST_UPDATE, shortFormat(c.updated())); + if (Gerrit.isSignedIn() + && Gerrit.getUserAccount().getGeneralPreferences() + .isRelativeDateInChangeTable()) { + table.setText(row, C_LAST_UPDATE, relativeFormat(c.updated())); + } else { + table.setText(row, C_LAST_UPDATE, shortFormat(c.updated())); + } boolean displayName = Gerrit.isSignedIn() && Gerrit.getUserAccount() .getGeneralPreferences().isShowUsernameInReviewCategory(); diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java new file mode 100644 index 0000000000..5be029cadd --- /dev/null +++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java @@ -0,0 +1,98 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.client; + +import static org.junit.Assert.assertEquals; +import static com.google.gerrit.client.RelativeDateFormatter.YEAR_IN_MILLIS; +import static com.google.gerrit.client.RelativeDateFormatter.SECOND_IN_MILLIS; +import static com.google.gerrit.client.RelativeDateFormatter.MINUTE_IN_MILLIS; +import static com.google.gerrit.client.RelativeDateFormatter.HOUR_IN_MILLIS; +import static com.google.gerrit.client.RelativeDateFormatter.DAY_IN_MILLIS; + +import java.util.Date; + +import org.eclipse.jgit.util.RelativeDateFormatter; +import org.junit.Test; + +public class RelativeDateFormatterTest { + + private static void assertFormat(long ageFromNow, long timeUnit, + String expectedFormat) { + Date d = new Date(System.currentTimeMillis() - ageFromNow * timeUnit); + String s = RelativeDateFormatter.format(d); + assertEquals(expectedFormat, s); + } + + @Test + public void testFuture() { + assertFormat(-100, YEAR_IN_MILLIS, "in the future"); + assertFormat(-1, SECOND_IN_MILLIS, "in the future"); + } + + @Test + public void testFormatSeconds() { + assertFormat(1, SECOND_IN_MILLIS, "1 seconds ago"); + assertFormat(89, SECOND_IN_MILLIS, "89 seconds ago"); + } + + @Test + public void testFormatMinutes() { + assertFormat(90, SECOND_IN_MILLIS, "2 minutes ago"); + assertFormat(3, MINUTE_IN_MILLIS, "3 minutes ago"); + assertFormat(60, MINUTE_IN_MILLIS, "60 minutes ago"); + assertFormat(89, MINUTE_IN_MILLIS, "89 minutes ago"); + } + + @Test + public void testFormatHours() { + assertFormat(90, MINUTE_IN_MILLIS, "2 hours ago"); + assertFormat(149, MINUTE_IN_MILLIS, "2 hours ago"); + assertFormat(35, HOUR_IN_MILLIS, "35 hours ago"); + } + + @Test + public void testFormatDays() { + assertFormat(36, HOUR_IN_MILLIS, "2 days ago"); + assertFormat(13, DAY_IN_MILLIS, "13 days ago"); + } + + @Test + public void testFormatWeeks() { + assertFormat(14, DAY_IN_MILLIS, "2 weeks ago"); + assertFormat(69, DAY_IN_MILLIS, "10 weeks ago"); + } + + @Test + public void testFormatMonths() { + assertFormat(70, DAY_IN_MILLIS, "2 months ago"); + assertFormat(75, DAY_IN_MILLIS, "3 months ago"); + assertFormat(364, DAY_IN_MILLIS, "12 months ago"); + } + + @Test + public void testFormatYearsMonths() { + assertFormat(366, DAY_IN_MILLIS, "1 year ago"); + assertFormat(380, DAY_IN_MILLIS, "1 year, 1 month ago"); + assertFormat(410, DAY_IN_MILLIS, "1 year, 2 months ago"); + assertFormat(2, YEAR_IN_MILLIS, "2 years ago"); + assertFormat(1824, DAY_IN_MILLIS, "4 years, 12 months ago"); + } + + @Test + public void testFormatYears() { + assertFormat(5, YEAR_IN_MILLIS, "5 years ago"); + assertFormat(60, YEAR_IN_MILLIS, "60 years ago"); + } +} diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java index 6f121ee03a..23664bcce4 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java @@ -123,6 +123,9 @@ public final class AccountGeneralPreferences { @Column(id = 11) protected boolean showUsernameInReviewCategory; + @Column(id = 12) + protected boolean relativeDateInChangeTable; + public AccountGeneralPreferences() { } @@ -226,6 +229,14 @@ public final class AccountGeneralPreferences { timeFormat = fmt.name(); } + public boolean isRelativeDateInChangeTable() { + return relativeDateInChangeTable; + } + + public void setRelativeDateInChangeTable(final boolean relativeDateInChangeTable) { + this.relativeDateInChangeTable = relativeDateInChangeTable; + } + public void resetToDefaults() { maximumPageSize = DEFAULT_PAGESIZE; showSiteHeader = true; @@ -237,5 +248,6 @@ public final class AccountGeneralPreferences { downloadCommand = null; dateFormat = null; timeFormat = null; + relativeDateInChangeTable = false; } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java index 4de38887f2..ea9245ccc3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java @@ -32,7 +32,7 @@ import java.util.List; /** A version of the database schema. */ public abstract class SchemaVersion { /** The current schema version. */ - public static final Class C = Schema_77.class; + public static final Class C = Schema_78.class; public static class Module extends AbstractModule { @Override diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java new file mode 100644 index 0000000000..18ae8b4ff6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java @@ -0,0 +1,26 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_78 extends SchemaVersion { + + @Inject + Schema_78(Provider prior) { + super(prior); + } +}