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 <matthias.sohn@sap.com>
This commit is contained in:
Matthias Sohn
2013-03-27 10:44:28 -04:00
parent 7044ba5f43
commit e1e95ef496
15 changed files with 308 additions and 3 deletions

View File

@@ -1,3 +1,4 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/test/java=UTF-8
encoding/<project>=UTF-8

View File

@@ -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));

View File

@@ -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;
}
}

View File

@@ -33,6 +33,7 @@ public interface AccountConstants extends Constants {
String reversePatchSetOrder();
String showUsernameInReviewCategory();
String buttonSaveChanges();
String showRelativeDateInChangeTable();
String tabAccountSummary();
String tabPreferences();

View File

@@ -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

View File

@@ -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);

View File

@@ -171,4 +171,10 @@ public interface ChangeConstants extends Constants {
String diffAllSideBySide();
String diffAllUnified();
String inTheFuture();
String month();
String months();
String year();
String years();
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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<ChangeInfo> {
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();

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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<Schema_77> C = Schema_77.class;
public static final Class<Schema_78> C = Schema_78.class;
public static class Module extends AbstractModule {
@Override

View File

@@ -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<Schema_77> prior) {
super(prior);
}
}