--- /dev/null
+/*
+ * Copyright (C) 2012, Christian Halstrick and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util;
+
+import static org.junit.Assert.assertThrows;
+
+import java.text.ParseException;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests which assert that unparseable Strings lead to ParseExceptions
+ */
+@RunWith(Theories.class)
+public class GitTimeParserBadlyFormattedTest {
+ private String dateStr;
+
+ @Before
+ public void setUp() {
+ MockSystemReader mockSystemReader = new MockSystemReader();
+ SystemReader.setInstance(mockSystemReader);
+ }
+
+ @After
+ public void tearDown() {
+ SystemReader.setInstance(null);
+ }
+
+ public GitTimeParserBadlyFormattedTest(String dateStr) {
+ this.dateStr = dateStr;
+ }
+
+ @DataPoints
+ public static String[] getDataPoints() {
+ return new String[] { "", "1970", "3000.3000.3000", "3 yesterday ago",
+ "now yesterday ago", "yesterdays", "3.day. 2.week.ago",
+ "day ago", "Gra Feb 21 15:35:00 2007 +0100",
+ "Sun Feb 21 15:35:00 2007 +0100",
+ "Wed Feb 21 15:35:00 Grand +0100" };
+ }
+
+ @Theory
+ public void badlyFormattedWithoutRef() {
+ assertThrows(
+ "The expected ParseException while parsing '" + dateStr
+ + "' did not occur.",
+ ParseException.class, () -> GitTimeParser.parse(dateStr));
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2024, Christian Halstrick and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Period;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GitTimeParserTest {
+ MockSystemReader mockSystemReader;
+
+ @Before
+ public void setUp() {
+ mockSystemReader = new MockSystemReader();
+ SystemReader.setInstance(mockSystemReader);
+ }
+
+ @After
+ public void tearDown() {
+ SystemReader.setInstance(null);
+ }
+
+ @Test
+ public void yesterday() throws ParseException {
+ LocalDateTime parse = GitTimeParser.parse("yesterday");
+
+ LocalDateTime now = SystemReader.getInstance().civilNow();
+ assertEquals(Period.between(parse.toLocalDate(), now.toLocalDate()),
+ Period.ofDays(1));
+ }
+
+ @Test
+ public void never() throws ParseException {
+ LocalDateTime parse = GitTimeParser.parse("never");
+ assertEquals(LocalDateTime.MAX, parse);
+ }
+
+ @Test
+ public void now_pointInTime() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+ LocalDateTime parsedNow = GitTimeParser.parse("now", aTime);
+
+ assertEquals(aTime, parsedNow);
+ }
+
+ @Test
+ public void now_systemTime() throws ParseException {
+ LocalDateTime firstNow = GitTimeParser.parse("now");
+ assertEquals(SystemReader.getInstance().civilNow(), firstNow);
+ mockSystemReader.tick(10);
+ LocalDateTime secondNow = GitTimeParser.parse("now");
+ assertTrue(secondNow.isAfter(firstNow));
+ }
+
+ @Test
+ public void weeksAgo() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+ LocalDateTime parse = GitTimeParser.parse("2 weeks ago", aTime);
+ assertEquals(asLocalDateTime("2007-02-07 15:35:00 +0100"), parse);
+ }
+
+ @Test
+ public void daysAndWeeksAgo() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+ LocalDateTime twoWeeksAgoActual = GitTimeParser.parse("2 weeks ago",
+ aTime);
+
+ LocalDateTime twoWeeksAgoExpected = asLocalDateTime(
+ "2007-02-07 15:35:00 +0100");
+ assertEquals(twoWeeksAgoExpected, twoWeeksAgoActual);
+
+ LocalDateTime combinedWhitespace = GitTimeParser
+ .parse("3 days 2 weeks ago", aTime);
+ LocalDateTime combinedWhitespaceExpected = asLocalDateTime(
+ "2007-02-04 15:35:00 +0100");
+ assertEquals(combinedWhitespaceExpected, combinedWhitespace);
+
+ LocalDateTime combinedDots = GitTimeParser.parse("3.day.2.week.ago",
+ aTime);
+ LocalDateTime combinedDotsExpected = asLocalDateTime(
+ "2007-02-04 15:35:00 +0100");
+ assertEquals(combinedDotsExpected, combinedDots);
+ }
+
+ @Test
+ public void hoursAgo() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:00 +0100");
+
+ LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago",
+ aTime);
+
+ LocalDateTime twoHoursAgoExpected = asLocalDateTime(
+ "2007-02-21 15:35:00 +0100");
+ assertEquals(twoHoursAgoExpected, twoHoursAgoActual);
+ }
+
+ @Test
+ public void hoursAgo_acrossDay() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:00 +0100");
+
+ LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago",
+ aTime);
+
+ LocalDateTime twoHoursAgoExpected = asLocalDateTime(
+ "2007-02-20 22:35:00 +0100");
+ assertEquals(twoHoursAgoExpected, twoHoursAgoActual);
+ }
+
+ @Test
+ public void minutesHoursAgoCombined() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-04 15:35:00 +0100");
+
+ LocalDateTime combinedWhitespace = GitTimeParser
+ .parse("3 hours 2 minutes ago", aTime);
+ LocalDateTime combinedWhitespaceExpected = asLocalDateTime(
+ "2007-02-04 12:33:00 +0100");
+ assertEquals(combinedWhitespaceExpected, combinedWhitespace);
+
+ LocalDateTime combinedDots = GitTimeParser
+ .parse("3.hours.2.minutes.ago", aTime);
+ LocalDateTime combinedDotsExpected = asLocalDateTime(
+ "2007-02-04 12:33:00 +0100");
+ assertEquals(combinedDotsExpected, combinedDots);
+ }
+
+ @Test
+ public void minutesAgo() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:10 +0100");
+
+ LocalDateTime twoMinutesAgo = GitTimeParser.parse("2 minutes ago",
+ aTime);
+
+ LocalDateTime twoMinutesAgoExpected = asLocalDateTime(
+ "2007-02-21 17:33:10 +0100");
+ assertEquals(twoMinutesAgoExpected, twoMinutesAgo);
+ }
+
+ @Test
+ public void minutesAgo_acrossDay() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:10 +0100");
+
+ LocalDateTime minutesAgoActual = GitTimeParser.parse("40 minutes ago",
+ aTime);
+
+ LocalDateTime minutesAgoExpected = asLocalDateTime(
+ "2007-02-20 23:55:10 +0100");
+ assertEquals(minutesAgoExpected, minutesAgoActual);
+ }
+
+ @Test
+ public void iso() throws ParseException {
+ String dateStr = "2007-02-21 15:35:00 +0100";
+
+ LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+ LocalDateTime expected = asLocalDateTime(dateStr);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void rfc() throws ParseException {
+ String dateStr = "Wed, 21 Feb 2007 15:35:00 +0100";
+
+ LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+ LocalDateTime expected = asLocalDateTime(dateStr,
+ "EEE, dd MMM yyyy HH:mm:ss Z");
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void shortFmt() throws ParseException {
+ assertParsing("2007-02-21", "yyyy-MM-dd");
+ }
+
+ @Test
+ public void shortWithDots() throws ParseException {
+ assertParsing("2007.02.21", "yyyy.MM.dd");
+ }
+
+ @Test
+ public void shortWithSlash() throws ParseException {
+ assertParsing("02/21/2007", "MM/dd/yyyy");
+ }
+
+ @Test
+ public void shortWithDotsReverse() throws ParseException {
+ assertParsing("21.02.2007", "dd.MM.yyyy");
+ }
+
+ @Test
+ public void defaultFmt() throws ParseException {
+ assertParsing("Wed Feb 21 15:35:00 2007 +0100",
+ "EEE MMM dd HH:mm:ss yyyy Z");
+ }
+
+ @Test
+ public void local() throws ParseException {
+ assertParsing("Wed Feb 21 15:35:00 2007", "EEE MMM dd HH:mm:ss yyyy");
+ }
+
+ private static void assertParsing(String dateStr, String format)
+ throws ParseException {
+ LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+ LocalDateTime expected = asLocalDateTime(dateStr, format);
+ assertEquals(expected, actual);
+ }
+
+ private static LocalDateTime asLocalDateTime(String dateStr) {
+ return asLocalDateTime(dateStr, "yyyy-MM-dd HH:mm:ss Z");
+ }
+
+ private static LocalDateTime asLocalDateTime(String dateStr,
+ String pattern) {
+ DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern);
+ TemporalAccessor ta = fmt
+ .withZone(SystemReader.getInstance().getTimeZoneId())
+ .withLocale(SystemReader.getInstance().getLocale())
+ .parse(dateStr);
+ return ta.isSupported(ChronoField.HOUR_OF_DAY) ? LocalDateTime.from(ta)
+ : LocalDate.from(ta).atStartOfDay();
+ }
+}
* used. One example is the parsing of the config parameter gc.pruneexpire. The
* parser can handle only subset of what native gits approxidate parser
* understands.
+ *
+ * @deprecated Use {@link GitTimeParser} instead.
*/
+@Deprecated(since = "7.1")
public class GitDateParser {
/**
* The Date representing never. Though this is a concrete value, most
--- /dev/null
+/*
+ * Copyright (C) 2024 Christian Halstrick and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util;
+
+import java.text.MessageFormat;
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.JGitText;
+
+/**
+ * Parses strings with time and date specifications into
+ * {@link java.time.Instant}.
+ *
+ * When git needs to parse strings specified by the user this parser can be
+ * used. One example is the parsing of the config parameter gc.pruneexpire. The
+ * parser can handle only subset of what native gits approxidate parser
+ * understands.
+ *
+ * @since 7.1
+ */
+public class GitTimeParser {
+
+ private static final Map<ParseableSimpleDateFormat, DateTimeFormatter> formatCache = new HashMap<>();
+
+ // An enum of all those formats which this parser can parse with the help of
+ // a DateTimeFormatter. There are other formats (e.g. the relative formats
+ // like "yesterday" or "1 week ago") which this parser can parse but which
+ // are not listed here because they are parsed without the help of a
+ // DateTimeFormatter.
+ enum ParseableSimpleDateFormat {
+ ISO("yyyy-MM-dd HH:mm:ss Z"), // //$NON-NLS-1$
+ RFC("EEE, dd MMM yyyy HH:mm:ss Z"), // //$NON-NLS-1$
+ SHORT("yyyy-MM-dd"), // //$NON-NLS-1$
+ SHORT_WITH_DOTS_REVERSE("dd.MM.yyyy"), // //$NON-NLS-1$
+ SHORT_WITH_DOTS("yyyy.MM.dd"), // //$NON-NLS-1$
+ SHORT_WITH_SLASH("MM/dd/yyyy"), // //$NON-NLS-1$
+ DEFAULT("EEE MMM dd HH:mm:ss yyyy Z"), // //$NON-NLS-1$
+ LOCAL("EEE MMM dd HH:mm:ss yyyy"); //$NON-NLS-1$
+
+ private final String formatStr;
+
+ ParseableSimpleDateFormat(String formatStr) {
+ this.formatStr = formatStr;
+ }
+ }
+
+ /**
+ * Parses a string into a {@link java.time.LocalDateTime} using the default
+ * locale. Since this parser also supports relative formats (e.g.
+ * "yesterday") the caller can specify the reference date. These types of
+ * strings can be parsed:
+ * <ul>
+ * <li>"never"</li>
+ * <li>"now"</li>
+ * <li>"yesterday"</li>
+ * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br>
+ * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of '
+ * ' one can use '.' to separate the words</li>
+ * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li>
+ * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li>
+ * <li>"yyyy-MM-dd"</li>
+ * <li>"yyyy.MM.dd"</li>
+ * <li>"MM/dd/yyyy",</li>
+ * <li>"dd.MM.yyyy"</li>
+ * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li>
+ * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li>
+ * </ul>
+ *
+ * @param dateStr
+ * the string to be parsed
+ * @return the parsed {@link java.time.LocalDateTime}
+ * @throws java.text.ParseException
+ * if the given dateStr was not recognized
+ */
+ public static LocalDateTime parse(String dateStr) throws ParseException {
+ return parse(dateStr, SystemReader.getInstance().civilNow());
+ }
+
+ // Only tests seem to use this method
+ static LocalDateTime parse(String dateStr, LocalDateTime now)
+ throws ParseException {
+ dateStr = dateStr.trim();
+ LocalDateTime ret;
+
+ if ("never".equalsIgnoreCase(dateStr)) //$NON-NLS-1$
+ return LocalDateTime.MAX;
+ ret = parse_relative(dateStr, now);
+ if (ret != null)
+ return ret;
+ for (ParseableSimpleDateFormat f : ParseableSimpleDateFormat.values()) {
+ try {
+ return parse_simple(dateStr, f);
+ } catch (DateTimeParseException e) {
+ // simply proceed with the next parser
+ }
+ }
+ ParseableSimpleDateFormat[] values = ParseableSimpleDateFormat.values();
+ StringBuilder allFormats = new StringBuilder("\"") //$NON-NLS-1$
+ .append(values[0].formatStr);
+ for (int i = 1; i < values.length; i++)
+ allFormats.append("\", \"").append(values[i].formatStr); //$NON-NLS-1$
+ allFormats.append("\""); //$NON-NLS-1$
+ throw new ParseException(
+ MessageFormat.format(JGitText.get().cannotParseDate, dateStr,
+ allFormats.toString()),
+ 0);
+ }
+
+ // tries to parse a string with the formats supported by DateTimeFormatter
+ private static LocalDateTime parse_simple(String dateStr,
+ ParseableSimpleDateFormat f) throws DateTimeParseException {
+ DateTimeFormatter dateFormat = formatCache.computeIfAbsent(f,
+ format -> DateTimeFormatter.ofPattern(f.formatStr)
+ .withLocale(SystemReader.getInstance().getLocale()));
+ TemporalAccessor parsed = dateFormat.parse(dateStr);
+ return parsed.isSupported(ChronoField.HOUR_OF_DAY)
+ ? LocalDateTime.from(parsed)
+ : LocalDate.from(parsed).atStartOfDay();
+ }
+
+ // tries to parse a string with a relative time specification
+ @SuppressWarnings("nls")
+ private static LocalDateTime parse_relative(String dateStr,
+ LocalDateTime now) {
+ // check for the static words "yesterday" or "now"
+ if ("now".equals(dateStr)) {
+ return now;
+ }
+
+ if ("yesterday".equals(dateStr)) {
+ return now.minusDays(1);
+ }
+
+ // parse constructs like "3 days ago", "5.week.2.day.ago"
+ String[] parts = dateStr.split("\\.| ");
+ int partsLength = parts.length;
+ // check we have an odd number of parts (at least 3) and that the last
+ // part is "ago"
+ if (partsLength < 3 || (partsLength & 1) == 0
+ || !"ago".equals(parts[parts.length - 1]))
+ return null;
+ int number;
+ for (int i = 0; i < parts.length - 2; i += 2) {
+ try {
+ number = Integer.parseInt(parts[i]);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ if (parts[i + 1] == null) {
+ return null;
+ }
+ switch (parts[i + 1]) {
+ case "year":
+ case "years":
+ now = now.minusYears(number);
+ break;
+ case "month":
+ case "months":
+ now = now.minusMonths(number);
+ break;
+ case "week":
+ case "weeks":
+ now = now.minusWeeks(number);
+ break;
+ case "day":
+ case "days":
+ now = now.minusDays(number);
+ break;
+ case "hour":
+ case "hours":
+ now = now.minusHours(number);
+ break;
+ case "minute":
+ case "minutes":
+ now = now.minusMinutes(number);
+ break;
+ case "second":
+ case "seconds":
+ now = now.minusSeconds(number);
+ break;
+ default:
+ return null;
+ }
+ }
+ return now;
+ }
+}