diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java new file mode 100644 index 000000000..4b45209ea --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2011, Matthias Sohn + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +import static org.junit.Assert.assertEquals; +import static org.eclipse.jgit.util.RelativeDateFormatter.YEAR_IN_MILLIS; +import static org.eclipse.jgit.util.RelativeDateFormatter.SECOND_IN_MILLIS; +import static org.eclipse.jgit.util.RelativeDateFormatter.MINUTE_IN_MILLIS; +import static org.eclipse.jgit.util.RelativeDateFormatter.HOUR_IN_MILLIS; +import static org.eclipse.jgit.util.RelativeDateFormatter.DAY_IN_MILLIS; + +import java.util.Date; + +import org.eclipse.jgit.util.RelativeDateFormatter; +import org.junit.Test; + +public class RelativeDateFormatterTest { + + private 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, 0 month 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, 0 month 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/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties index a19c9c41d..1f4e9a137 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties @@ -136,6 +136,7 @@ createNewFileFailed=Could not create new file {0} credentialPassword=Password credentialUsername=Username daemonAlreadyRunning=Daemon already running +daysAgo={0} days ago deleteBranchUnexpectedResult=Delete branch returned unexpected result {0} deleteFileFailed=Could not delete file {0} deletingNotSupported=Deleting {0} not supported. @@ -203,6 +204,7 @@ flagIsDisposed={0} is disposed. flagNotFromThis={0} not from this. flagsAlreadyCreated={0} flags already created. funnyRefname=funny refname +hoursAgo={0} hours ago hugeIndexesAreNotSupportedByJgitYet=Huge indexes are not supported by jgit, yet hunkBelongsToAnotherFile=Hunk belongs to another file hunkDisconnectedFromFile=Hunk disconnected from file @@ -221,6 +223,7 @@ indexWriteException=Modified index could not be written integerValueOutOfRange=Integer value {0}.{1} out of range internalRevisionError=internal revision error interruptedWriting=Interrupted writing {0} +inTheFuture=in the future invalidAdvertisementOf=invalid advertisement of {0} invalidAncestryLength=Invalid ancestry length invalidBooleanValue=Invalid boolean value: {0}.{1}={2} @@ -268,6 +271,7 @@ mergeConflictOnNonNoteEntries=Merge conflict on non-note entries: base = {0}, ou mergeStrategyAlreadyExistsAsDefault=Merge strategy "{0}" already exists as a default strategy mergeStrategyDoesNotSupportHeads=merge strategy {0} does not support {1} heads to be merged into HEAD mergeUsingStrategyResultedInDescription=Merge of revisions {0} with base {1} using strategy {2} resulted in: {3}. {4} +minutesAgo={0} minutes ago missingAccesskey=Missing accesskey. missingConfigurationForKey=No value for key {0} found in configuration missingDeltaBase=delta base @@ -279,6 +283,9 @@ missingSecretkey=Missing secretkey. mixedStagesNotAllowed=Mixed stages not allowed mkDirFailed=Creating directory {0} failed mkDirsFailed=Creating directories for {0} failed +month=month +months=months +monthsAgo={0} months ago multipleMergeBasesFor=Multiple merge bases for:\n {0}\n {1} found:\n {2}\n {3} need2Arguments=Need 2 arguments needPackOut=need packOut @@ -384,6 +391,7 @@ resultLengthIncorrect=result length incorrect rewinding=Rewinding to commit {0} searchForReuse=Finding sources searchForSizes=Getting sizes +secondsAgo={0} seconds ago sequenceTooLargeForDiffAlgorithm=Sequence too large for difference algorithm. serviceNotEnabledNoName=Service not enabled serviceNotPermitted={0} not permitted @@ -465,6 +473,7 @@ uriNotFound={0} not found userConfigFileInvalid=User config file {0} invalid {1} walkFailure=Walk failure. wantNotValid=want {0} not valid +weeksAgo={0} weeks ago windowSizeMustBeLesserThanLimit=Window size must be < limit windowSizeMustBePowerOf2=Window size must be power of 2 writeTimedOut=Write timed out @@ -474,3 +483,7 @@ writingNotSupported=Writing {0} not supported. writingObjects=Writing objects wrongDecompressedLength=wrong decompressed length wrongRepositoryState=Wrong Repository State: {0} +year=year +years=years +yearsAgo={0} years ago +yearsMonthsAgo={0} {1}, {2} {3} ago diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java index f86a8e544..845608440 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java @@ -196,6 +196,7 @@ public class JGitText extends TranslationBundle { /***/ public String credentialPassword; /***/ public String credentialUsername; /***/ public String daemonAlreadyRunning; + /***/ public String daysAgo; /***/ public String deleteBranchUnexpectedResult; /***/ public String deleteFileFailed; /***/ public String deletingNotSupported; @@ -263,6 +264,7 @@ public class JGitText extends TranslationBundle { /***/ public String flagNotFromThis; /***/ public String flagsAlreadyCreated; /***/ public String funnyRefname; + /***/ public String hoursAgo; /***/ public String hugeIndexesAreNotSupportedByJgitYet; /***/ public String hunkBelongsToAnotherFile; /***/ public String hunkDisconnectedFromFile; @@ -281,6 +283,7 @@ public class JGitText extends TranslationBundle { /***/ public String integerValueOutOfRange; /***/ public String internalRevisionError; /***/ public String interruptedWriting; + /***/ public String inTheFuture; /***/ public String invalidAdvertisementOf; /***/ public String invalidAncestryLength; /***/ public String invalidBooleanValue; @@ -328,6 +331,7 @@ public class JGitText extends TranslationBundle { /***/ public String mergeStrategyAlreadyExistsAsDefault; /***/ public String mergeStrategyDoesNotSupportHeads; /***/ public String mergeUsingStrategyResultedInDescription; + /***/ public String minutesAgo; /***/ public String missingAccesskey; /***/ public String missingConfigurationForKey; /***/ public String missingDeltaBase; @@ -339,6 +343,9 @@ public class JGitText extends TranslationBundle { /***/ public String mixedStagesNotAllowed; /***/ public String mkDirFailed; /***/ public String mkDirsFailed; + /***/ public String month; + /***/ public String months; + /***/ public String monthsAgo; /***/ public String multipleMergeBasesFor; /***/ public String need2Arguments; /***/ public String needPackOut; @@ -444,6 +451,7 @@ public class JGitText extends TranslationBundle { /***/ public String rewinding; /***/ public String searchForReuse; /***/ public String searchForSizes; + /***/ public String secondsAgo; /***/ public String sequenceTooLargeForDiffAlgorithm; /***/ public String serviceNotEnabledNoName; /***/ public String serviceNotPermitted; @@ -525,6 +533,7 @@ public class JGitText extends TranslationBundle { /***/ public String userConfigFileInvalid; /***/ public String walkFailure; /***/ public String wantNotValid; + /***/ public String weeksAgo; /***/ public String windowSizeMustBeLesserThanLimit; /***/ public String windowSizeMustBePowerOf2; /***/ public String writeTimedOut; @@ -534,4 +543,8 @@ public class JGitText extends TranslationBundle { /***/ public String writingObjects; /***/ public String wrongDecompressedLength; /***/ public String wrongRepositoryState; + /***/ public String year; + /***/ public String years; + /***/ public String yearsAgo; + /***/ public String yearsMonthsAgo; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java new file mode 100644 index 000000000..bcbcd808d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2011, Matthias Sohn + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +import java.text.MessageFormat; +import java.util.Date; + +import org.eclipse.jgit.JGitText; + +/** + * 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 = (System.currentTimeMillis() - when.getTime()); + + // shouldn't happen in a perfect world + if (ageMillis < 0) + return JGitText.get().inTheFuture; + + // seconds + if (ageMillis < upperLimit(MINUTE_IN_MILLIS)) + return MessageFormat.format(JGitText.get().secondsAgo, + round(ageMillis, SECOND_IN_MILLIS)); + + // minutes + if (ageMillis < upperLimit(HOUR_IN_MILLIS)) + return MessageFormat.format(JGitText.get().minutesAgo, + round(ageMillis, MINUTE_IN_MILLIS)); + + // hours + if (ageMillis < upperLimit(DAY_IN_MILLIS)) + return MessageFormat.format(JGitText.get().hoursAgo, + round(ageMillis, HOUR_IN_MILLIS)); + + // up to 14 days use days + if (ageMillis < 14 * DAY_IN_MILLIS) + return MessageFormat.format(JGitText.get().daysAgo, + round(ageMillis, DAY_IN_MILLIS)); + + // up to 10 weeks use weeks + if (ageMillis < 10 * WEEK_IN_MILLIS) + return MessageFormat.format(JGitText.get().weeksAgo, + round(ageMillis, WEEK_IN_MILLIS)); + + // months + if (ageMillis < YEAR_IN_MILLIS) + return MessageFormat.format(JGitText.get().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) ? JGitText.get().years : // + JGitText.get().year; + long months = round(ageMillis % YEAR_IN_MILLIS, MONTH_IN_MILLIS); + String monthLabel = (months > 1) ? JGitText.get().months : // + JGitText.get().month; + return MessageFormat.format(JGitText.get().yearsMonthsAgo, + new Object[] { years, yearLabel, months, monthLabel }); + } + + // years + return MessageFormat.format(JGitText.get().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; + } +} \ No newline at end of file