blame: show "previous" information in --porcelain/--incremental format
[gitweb.git] / date.c
diff --git a/date.c b/date.c
index 365dc3b14e47826dd7becf19a76e29bfaa6b2eb5..950b88fdcf74f550a582684f1702ffb58c62c7f9 100644 (file)
--- a/date.c
+++ b/date.c
@@ -4,12 +4,12 @@
  * Copyright (C) Linus Torvalds, 2005
  */
 
-#include <time.h>
-#include <sys/time.h>
-
 #include "cache.h"
 
-static time_t my_mktime(struct tm *tm)
+/*
+ * This is like mktime, but without normalization of tm_wday and tm_yday.
+ */
+time_t tm_to_time_t(const struct tm *tm)
 {
        static const int mdays[] = {
            0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
@@ -37,6 +37,16 @@ static const char *weekday_names[] = {
        "Sundays", "Mondays", "Tuesdays", "Wednesdays", "Thursdays", "Fridays", "Saturdays"
 };
 
+static time_t gm_time_t(unsigned long time, int tz)
+{
+       int minutes;
+
+       minutes = tz < 0 ? -tz : tz;
+       minutes = (minutes / 100)*60 + (minutes % 100);
+       minutes = tz < 0 ? -minutes : minutes;
+       return time + minutes * 60;
+}
+
 /*
  * The "tz" thing is passed in as this strange "decimal parse of tz"
  * thing, which means that tz -0100 is passed in as the integer -100,
@@ -44,45 +54,113 @@ static const char *weekday_names[] = {
  */
 static struct tm *time_to_tm(unsigned long time, int tz)
 {
-       time_t t;
-       int minutes;
-
-       minutes = tz < 0 ? -tz : tz;
-       minutes = (minutes / 100)*60 + (minutes % 100);
-       minutes = tz < 0 ? -minutes : minutes;
-       t = time + minutes * 60;
+       time_t t = gm_time_t(time, tz);
        return gmtime(&t);
 }
 
-const char *show_date(unsigned long time, int tz)
+/*
+ * What value of "tz" was in effect back then at "time" in the
+ * local timezone?
+ */
+static int local_tzoffset(unsigned long time)
 {
-       struct tm *tm;
-       static char timebuf[200];
-
-       tm = time_to_tm(time, tz);
-       if (!tm)
-               return NULL;
-       sprintf(timebuf, "%.3s %.3s %d %02d:%02d:%02d %d %+05d",
-               weekday_names[tm->tm_wday],
-               month_names[tm->tm_mon],
-               tm->tm_mday,
-               tm->tm_hour, tm->tm_min, tm->tm_sec,
-               tm->tm_year + 1900, tz);
-       return timebuf;
+       time_t t, t_local;
+       struct tm tm;
+       int offset, eastwest;
+
+       t = time;
+       localtime_r(&t, &tm);
+       t_local = tm_to_time_t(&tm);
+
+       if (t_local < t) {
+               eastwest = -1;
+               offset = t - t_local;
+       } else {
+               eastwest = 1;
+               offset = t_local - t;
+       }
+       offset /= 60; /* in minutes */
+       offset = (offset % 60) + ((offset / 60) * 100);
+       return offset * eastwest;
 }
 
-const char *show_rfc2822_date(unsigned long time, int tz)
+const char *show_date(unsigned long time, int tz, enum date_mode mode)
 {
        struct tm *tm;
        static char timebuf[200];
 
+       if (mode == DATE_RELATIVE) {
+               unsigned long diff;
+               struct timeval now;
+               gettimeofday(&now, NULL);
+               if (now.tv_sec < time)
+                       return "in the future";
+               diff = now.tv_sec - time;
+               if (diff < 90) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu seconds ago", diff);
+                       return timebuf;
+               }
+               /* Turn it into minutes */
+               diff = (diff + 30) / 60;
+               if (diff < 90) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu minutes ago", diff);
+                       return timebuf;
+               }
+               /* Turn it into hours */
+               diff = (diff + 30) / 60;
+               if (diff < 36) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu hours ago", diff);
+                       return timebuf;
+               }
+               /* We deal with number of days from here on */
+               diff = (diff + 12) / 24;
+               if (diff < 14) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu days ago", diff);
+                       return timebuf;
+               }
+               /* Say weeks for the past 10 weeks or so */
+               if (diff < 70) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu weeks ago", (diff + 3) / 7);
+                       return timebuf;
+               }
+               /* Say months for the past 12 months or so */
+               if (diff < 360) {
+                       snprintf(timebuf, sizeof(timebuf), "%lu months ago", (diff + 15) / 30);
+                       return timebuf;
+               }
+               /* Else fall back on absolute format.. */
+       }
+
+       if (mode == DATE_LOCAL)
+               tz = local_tzoffset(time);
+
        tm = time_to_tm(time, tz);
        if (!tm)
                return NULL;
-       sprintf(timebuf, "%.3s, %d %.3s %d %02d:%02d:%02d %+05d",
-               weekday_names[tm->tm_wday], tm->tm_mday,
-               month_names[tm->tm_mon], tm->tm_year + 1900,
-               tm->tm_hour, tm->tm_min, tm->tm_sec, tz);
+       if (mode == DATE_SHORT)
+               sprintf(timebuf, "%04d-%02d-%02d", tm->tm_year + 1900,
+                               tm->tm_mon + 1, tm->tm_mday);
+       else if (mode == DATE_ISO8601)
+               sprintf(timebuf, "%04d-%02d-%02d %02d:%02d:%02d %+05d",
+                               tm->tm_year + 1900,
+                               tm->tm_mon + 1,
+                               tm->tm_mday,
+                               tm->tm_hour, tm->tm_min, tm->tm_sec,
+                               tz);
+       else if (mode == DATE_RFC2822)
+               sprintf(timebuf, "%.3s, %d %.3s %d %02d:%02d:%02d %+05d",
+                       weekday_names[tm->tm_wday], tm->tm_mday,
+                       month_names[tm->tm_mon], tm->tm_year + 1900,
+                       tm->tm_hour, tm->tm_min, tm->tm_sec, tz);
+       else
+               sprintf(timebuf, "%.3s %.3s %d %02d:%02d:%02d %d%c%+05d",
+                               weekday_names[tm->tm_wday],
+                               month_names[tm->tm_mon],
+                               tm->tm_mday,
+                               tm->tm_hour, tm->tm_min, tm->tm_sec,
+                               tm->tm_year + 1900,
+                               (mode == DATE_LOCAL) ? 0 : ' ',
+                               tz);
        return timebuf;
 }
 
@@ -138,9 +216,9 @@ static const struct {
        { "EAST", +10, 0, },    /* Eastern Australian Standard */
        { "EADT", +10, 1, },    /* Eastern Australian Daylight */
        { "GST",  +10, 0, },    /* Guam Standard, USSR Zone 9 */
-       { "NZT",  +11, 0, },    /* New Zealand */
-       { "NZST", +11, 0, },    /* New Zealand Standard */
-       { "NZDT", +11, 1, },    /* New Zealand Daylight */
+       { "NZT",  +12, 0, },    /* New Zealand */
+       { "NZST", +12, 0, },    /* New Zealand Standard */
+       { "NZDT", +12, 1, },    /* New Zealand Daylight */
        { "IDLE", +12, 0, },    /* International Date Line East */
 };
 
@@ -209,8 +287,12 @@ static int match_alpha(const char *date, struct tm *tm, int *offset)
        }
 
        if (match_string(date, "PM") == 2) {
-               if (tm->tm_hour > 0 && tm->tm_hour < 12)
-                       tm->tm_hour += 12;
+               tm->tm_hour = (tm->tm_hour % 12) + 12;
+               return 2;
+       }
+
+       if (match_string(date, "AM") == 2) {
+               tm->tm_hour = (tm->tm_hour % 12) + 0;
                return 2;
        }
 
@@ -243,7 +325,7 @@ static int is_date(int year, int month, int day, struct tm *now_tm, time_t now,
                if (!now_tm)
                        return 1;
 
-               specified = my_mktime(r);
+               specified = tm_to_time_t(r);
 
                /* Be it commit time or author time, it does not make
                 * sense to specify timestamp way into the future.  Make
@@ -320,8 +402,17 @@ static int match_multi_number(unsigned long num, char c, const char *date, char
        return end - date;
 }
 
+/* Have we filled in any part of the time/date yet? */
+static inline int nodate(struct tm *tm)
+{
+       return tm->tm_year < 0 &&
+               tm->tm_mon < 0 &&
+               tm->tm_mday < 0 &&
+               !(tm->tm_hour | tm->tm_min | tm->tm_sec);
+}
+
 /*
- * We've seen a digit. Time? Year? Date? 
+ * We've seen a digit. Time? Year? Date?
  */
 static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt)
 {
@@ -332,9 +423,11 @@ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt
        num = strtoul(date, &end, 10);
 
        /*
-        * Seconds since 1970? We trigger on that for anything after Jan 1, 2000
+        * Seconds since 1970? We trigger on that for any numbers with
+        * more than 8 digits. This is because we don't want to rule out
+        * numbers like 20070606 as a YYYYMMDD date.
         */
-       if (num > 946684800) {
+       if (num >= 100000000 && nodate(tm)) {
                time_t time = num;
                if (gmtime_r(&time, tm)) {
                        *tm_gmt = 1;
@@ -369,7 +462,7 @@ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt
 
        /* Four-digit year or a timezone? */
        if (n == 4) {
-               if (num <= 1200 && *offset == -1) {
+               if (num <= 1400 && *offset == -1) {
                        unsigned int minutes = num % 100;
                        unsigned int hours = num / 100;
                        *offset = hours*60 + minutes;
@@ -378,6 +471,13 @@ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt
                return n;
        }
 
+       /*
+        * Ignore lots of numerals. We took care of 4-digit years above.
+        * Days or months must be one or two digits.
+        */
+       if (n > 2)
+               return n;
+
        /*
         * NOTE! We will give precedence to day-of-month over month or
         * year numbers in the 1-12 range. So 05 is always "mday 5",
@@ -404,14 +504,10 @@ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt
 
        if (num > 0 && num < 32) {
                tm->tm_mday = num;
-       } else if (num > 1900) {
-               tm->tm_year = num - 1900;
-       } else if (num > 70) {
-               tm->tm_year = num;
        } else if (num > 0 && num < 13) {
                tm->tm_mon = num-1;
        }
-               
+
        return n;
 }
 
@@ -485,13 +581,13 @@ int parse_date(const char *date, char *result, int maxlen)
                if (!match) {
                        /* BAD CRAP */
                        match = 1;
-               }       
+               }
 
                date += match;
        }
 
        /* mktime uses local timezone */
-       then = my_mktime(&tm); 
+       then = tm_to_time_t(&tm);
        if (offset == -1)
                offset = (then - mktime(&tm)) / 60;
 
@@ -503,6 +599,26 @@ int parse_date(const char *date, char *result, int maxlen)
        return date_string(then, offset, result, maxlen);
 }
 
+enum date_mode parse_date_format(const char *format)
+{
+       if (!strcmp(format, "relative"))
+               return DATE_RELATIVE;
+       else if (!strcmp(format, "iso8601") ||
+                !strcmp(format, "iso"))
+               return DATE_ISO8601;
+       else if (!strcmp(format, "rfc2822") ||
+                !strcmp(format, "rfc"))
+               return DATE_RFC2822;
+       else if (!strcmp(format, "short"))
+               return DATE_SHORT;
+       else if (!strcmp(format, "local"))
+               return DATE_LOCAL;
+       else if (!strcmp(format, "default"))
+               return DATE_NORMAL;
+       else
+               die("unknown date format %s", format);
+}
+
 void datestamp(char *buf, int bufsize)
 {
        time_t now;
@@ -510,7 +626,7 @@ void datestamp(char *buf, int bufsize)
 
        time(&now);
 
-       offset = my_mktime(localtime(&now)) - now;
+       offset = tm_to_time_t(localtime(&now)) - now;
        offset /= 60;
 
        date_string(now, offset, buf, bufsize);
@@ -551,6 +667,40 @@ static void date_tea(struct tm *tm, int *num)
        date_time(tm, 17);
 }
 
+static void date_pm(struct tm *tm, int *num)
+{
+       int hour, n = *num;
+       *num = 0;
+
+       hour = tm->tm_hour;
+       if (n) {
+               hour = n;
+               tm->tm_min = 0;
+               tm->tm_sec = 0;
+       }
+       tm->tm_hour = (hour % 12) + 12;
+}
+
+static void date_am(struct tm *tm, int *num)
+{
+       int hour, n = *num;
+       *num = 0;
+
+       hour = tm->tm_hour;
+       if (n) {
+               hour = n;
+               tm->tm_min = 0;
+               tm->tm_sec = 0;
+       }
+       tm->tm_hour = (hour % 12);
+}
+
+static void date_never(struct tm *tm, int *num)
+{
+       time_t n = 0;
+       localtime_r(&n, tm);
+}
+
 static const struct special {
        const char *name;
        void (*fn)(struct tm *, int *);
@@ -559,6 +709,9 @@ static const struct special {
        { "noon", date_noon },
        { "midnight", date_midnight },
        { "tea", date_tea },
+       { "PM", date_pm },
+       { "AM", date_am },
+       { "never", date_never },
        { NULL }
 };
 
@@ -577,17 +730,17 @@ static const struct typelen {
        { "days", 24*60*60 },
        { "weeks", 7*24*60*60 },
        { NULL }
-};     
+};
 
 static const char *approxidate_alpha(const char *date, struct tm *tm, int *num)
 {
        const struct typelen *tl;
        const struct special *s;
        const char *end = date;
-       int n = 1, i;
+       int i;
 
-       while (isalpha(*++end))
-               n++;
+       while (isalpha(*++end));
+               ;
 
        for (i = 0; i < 12; i++) {
                int match = match_string(date, month_names[i]);
@@ -665,6 +818,29 @@ static const char *approxidate_alpha(const char *date, struct tm *tm, int *num)
        return end;
 }
 
+static const char *approxidate_digit(const char *date, struct tm *tm, int *num)
+{
+       char *end;
+       unsigned long number = strtoul(date, &end, 10);
+
+       switch (*end) {
+       case ':':
+       case '.':
+       case '/':
+       case '-':
+               if (isdigit(end[1])) {
+                       int match = match_multi_number(number, *end, date, end, tm);
+                       if (match)
+                               return date + match;
+               }
+       }
+
+       /* Accept zero-padding only for small numbers ("Dec 02", never "Dec 0002") */
+       if (date[0] != '0' || end - date <= 2)
+               *num = number;
+       return end;
+}
+
 unsigned long approxidate(const char *date)
 {
        int number = 0;
@@ -684,9 +860,7 @@ unsigned long approxidate(const char *date)
                        break;
                date++;
                if (isdigit(c)) {
-                       char *end;
-                       number = strtoul(date-1, &end, 10);
-                       date = end;
+                       date = approxidate_digit(date-1, &tm, &number);
                        continue;
                }
                if (isalpha(c))