From c3fbf81a8534cf88ff948d12004eb94929ec1174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Thu, 15 Jun 2017 14:29:53 +0200 Subject: [PATCH 1/3] strbuf: let strbuf_addftime handle %z and %Z itself There is no portable way to pass timezone information to strftime. Add parameters for timezone offset and name to strbuf_addftime and let it handle the timezone-related format specifiers %z and %Z internally. Callers can opt out for %Z by passing NULL as timezone name. %z is always handled internally -- this helps on Windows, where strftime would expand it to a timezone name (same as %Z), in violation of POSIX. Modifiers are not handled, e.g. %Ez is still passed to strftime. Use an empty string as timezone name in show_date (the only current caller) for now because we only have the timezone offset in non-local mode. POSIX allows %Z to resolve to an empty string in case of missing information. Helped-by: Ulrich Mueller Helped-by: Jeff King Signed-off-by: Rene Scharfe Signed-off-by: Junio C Hamano --- Documentation/rev-list-options.txt | 3 ++- date.c | 2 +- strbuf.c | 41 +++++++++++++++++++++++++++--- strbuf.h | 8 +++++- t/t0006-date.sh | 6 +++++ 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt index a02f7324c0..3921066b7f 100644 --- a/Documentation/rev-list-options.txt +++ b/Documentation/rev-list-options.txt @@ -764,7 +764,8 @@ timezone value. 1970). As with `--raw`, this is always in UTC and therefore `-local` has no effect. + -`--date=format:...` feeds the format `...` to your system `strftime`. +`--date=format:...` feeds the format `...` to your system `strftime`, +except for %z and %Z, which are handled internally. Use `--date=format:%c` to show the date in your system locale's preferred format. See the `strftime` manual for a complete list of format placeholders. When using `-local`, the correct syntax is diff --git a/date.c b/date.c index a996331f5b..6eccbd8546 100644 --- a/date.c +++ b/date.c @@ -233,7 +233,7 @@ const char *show_date(unsigned long time, int tz, const struct date_mode *mode) month_names[tm->tm_mon], tm->tm_year + 1900, tm->tm_hour, tm->tm_min, tm->tm_sec, tz); else if (mode->type == DATE_STRFTIME) - strbuf_addftime(&timebuf, mode->strftime_fmt, tm); + strbuf_addftime(&timebuf, mode->strftime_fmt, tm, tz, ""); else strbuf_addf(&timebuf, "%.3s %.3s %d %02d:%02d:%02d %d%c%+05d", weekday_names[tm->tm_wday], diff --git a/strbuf.c b/strbuf.c index 00457940cf..be3b9e37b1 100644 --- a/strbuf.c +++ b/strbuf.c @@ -785,14 +785,48 @@ char *xstrfmt(const char *fmt, ...) return ret; } -void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm) +void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm, + int tz_offset, const char *tz_name) { + struct strbuf munged_fmt = STRBUF_INIT; size_t hint = 128; size_t len; if (!*fmt) return; + /* + * There is no portable way to pass timezone information to + * strftime, so we handle %z and %Z here. + */ + for (;;) { + const char *percent = strchrnul(fmt, '%'); + strbuf_add(&munged_fmt, fmt, percent - fmt); + if (!*percent) + break; + fmt = percent + 1; + switch (*fmt) { + case '%': + strbuf_addstr(&munged_fmt, "%%"); + fmt++; + break; + case 'z': + strbuf_addf(&munged_fmt, "%+05d", tz_offset); + fmt++; + break; + case 'Z': + if (tz_name) { + strbuf_addstr(&munged_fmt, tz_name); + fmt++; + break; + } + /* FALLTHROUGH */ + default: + strbuf_addch(&munged_fmt, '%'); + } + } + fmt = munged_fmt.buf; + strbuf_grow(sb, hint); len = strftime(sb->buf + sb->len, sb->alloc - sb->len, fmt, tm); @@ -804,17 +838,16 @@ void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm) * output contains at least one character, and then drop the extra * character before returning. */ - struct strbuf munged_fmt = STRBUF_INIT; - strbuf_addf(&munged_fmt, "%s ", fmt); + strbuf_addch(&munged_fmt, ' '); while (!len) { hint *= 2; strbuf_grow(sb, hint); len = strftime(sb->buf + sb->len, sb->alloc - sb->len, munged_fmt.buf, tm); } - strbuf_release(&munged_fmt); len--; /* drop munged space */ } + strbuf_release(&munged_fmt); strbuf_setlen(sb, sb->len + len); } diff --git a/strbuf.h b/strbuf.h index 80047b1bb7..4559035c47 100644 --- a/strbuf.h +++ b/strbuf.h @@ -340,8 +340,14 @@ extern void strbuf_vaddf(struct strbuf *sb, const char *fmt, va_list ap); /** * Add the time specified by `tm`, as formatted by `strftime`. + * `tz_name` is used to expand %Z internally unless it's NULL. + * `tz_offset` is in decimal hhmm format, e.g. -600 means six hours west + * of Greenwich, and it's used to expand %z internally. However, tokens + * with modifiers (e.g. %Ez) are passed to `strftime`. */ -extern void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm); +extern void strbuf_addftime(struct strbuf *sb, const char *fmt, + const struct tm *tm, int tz_offset, + const char *tz_name); /** * Read a given size of data from a FILE* pointer to the buffer. diff --git a/t/t0006-date.sh b/t/t0006-date.sh index c0c910867d..fded9dbd68 100755 --- a/t/t0006-date.sh +++ b/t/t0006-date.sh @@ -51,6 +51,12 @@ check_show iso-local "$TIME" '2016-06-15 14:13:20 +0000' check_show raw-local "$TIME" '1466000000 +0000' check_show unix-local "$TIME" '1466000000' +check_show 'format:%z' "$TIME" '+0200' +check_show 'format-local:%z' "$TIME" '+0000' +check_show 'format:%Z' "$TIME" '' +check_show 'format:%%z' "$TIME" '%z' +check_show 'format-local:%%z' "$TIME" '%z' + # arbitrary time absurdly far in the future FUTURE="5758122296 -0400" check_show iso "$FUTURE" "2152-06-19 18:24:56 -0400" LONG_IS_64BIT From 22280d7ee7a4286542e14dd0784cb29703ebb389 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 15 Jun 2017 09:51:22 -0400 Subject: [PATCH 2/3] t0006: check --date=format zone offsets We already test that "%z" and "%Z" show the right thing, but we don't actually check that the time we display is the correct one. Let's add two new tests: 1. Test that "format:" shows the time in the author's timezone, just like the other time formats. 2. Test that "format-local:" shows time in the local timezone. We don't want to use our normal UTC for this, because its offset is zero (so the result would be "correct" even if the code forgot to apply the offset or applied it in the wrong direction). We'll use the EST5 zone, which is already used elsewhere in the script (and so is assumed to be available everywhere). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- t/t0006-date.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/t/t0006-date.sh b/t/t0006-date.sh index fded9dbd68..4b789c00f1 100755 --- a/t/t0006-date.sh +++ b/t/t0006-date.sh @@ -31,9 +31,11 @@ check_show () { format=$1 time=$2 expect=$3 - test_expect_success $4 "show date ($format:$time)" ' + prereqs=$4 + zone=$5 + test_expect_success $prereqs "show date ($format:$time)" ' echo "$time -> $expect" >expect && - test-date show:$format "$time" >actual && + TZ=${zone:-$TZ} test-date show:"$format" "$time" >actual && test_cmp expect actual ' } @@ -57,6 +59,9 @@ check_show 'format:%Z' "$TIME" '' check_show 'format:%%z' "$TIME" '%z' check_show 'format-local:%%z' "$TIME" '%z' +check_show 'format:%Y-%m-%d %H:%M:%S' "$TIME" '2016-06-15 16:13:20' +check_show 'format-local:%Y-%m-%d %H:%M:%S' "$TIME" '2016-06-15 09:13:20' '' EST5 + # arbitrary time absurdly far in the future FUTURE="5758122296 -0400" check_show iso "$FUTURE" "2152-06-19 18:24:56 -0400" LONG_IS_64BIT From 6eced3ec5e5d7fbe61de2791e2627b1acf1246b3 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 15 Jun 2017 09:52:17 -0400 Subject: [PATCH 3/3] date: use localtime() for "-local" time formats When we convert seconds-since-epochs timestamps into a broken-down "struct tm", we do so by adjusting the timestamp according to the known offset and then using gmtime() to break down the result. This means that the resulting struct "knows" that it's in GMT, even though the time it represents is adjusted for a different zone. The fields where it stores this data are not portably accessible, so we have no way to override them to tell them the real zone info. For the most part, this works. Our date-formatting routines don't pay attention to these inaccessible fields, and use the same tz info we provided for adjustment. The one exception is when we call strftime(), whose %Z format reveals this hidden timezone data. We solved that by always showing the empty string for %Z. This is allowed by POSIX, but not very helpful to the user. We can't make this work in the general case, as there's no portable function for setting an arbitrary timezone (and anyway, we don't have the zone name for the author zones, only their offsets). But for the special case of the "-local" formats, we can just skip the adjustment and use localtime() instead of gmtime(). This makes --date=format-local:%Z work correctly, showing the local timezone instead of an empty string. The new test checks the result for "UTC", our default test-lib value for $TZ. Using something like EST5 might be more interesting, but the actual zone string is system-dependent (for instance, on my system it expands to just EST). Hopefully "UTC" is vanilla enough that every system treats it the same. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- date.c | 14 ++++++++++++-- t/t0006-date.sh | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/date.c b/date.c index 6eccbd8546..5580c40ac0 100644 --- a/date.c +++ b/date.c @@ -60,6 +60,12 @@ static struct tm *time_to_tm(unsigned long time, int tz) return gmtime(&t); } +static struct tm *time_to_tm_local(unsigned long time) +{ + time_t t = time; + return localtime(&t); +} + /* * What value of "tz" was in effect back then at "time" in the * local timezone? @@ -201,7 +207,10 @@ const char *show_date(unsigned long time, int tz, const struct date_mode *mode) return timebuf.buf; } - tm = time_to_tm(time, tz); + if (mode->local) + tm = time_to_tm_local(time); + else + tm = time_to_tm(time, tz); if (!tm) { tm = time_to_tm(0, 0); tz = 0; @@ -233,7 +242,8 @@ const char *show_date(unsigned long time, int tz, const struct date_mode *mode) month_names[tm->tm_mon], tm->tm_year + 1900, tm->tm_hour, tm->tm_min, tm->tm_sec, tz); else if (mode->type == DATE_STRFTIME) - strbuf_addftime(&timebuf, mode->strftime_fmt, tm, tz, ""); + strbuf_addftime(&timebuf, mode->strftime_fmt, tm, tz, + mode->local ? NULL : ""); else strbuf_addf(&timebuf, "%.3s %.3s %d %02d:%02d:%02d %d%c%+05d", weekday_names[tm->tm_wday], diff --git a/t/t0006-date.sh b/t/t0006-date.sh index 4b789c00f1..8e219b7c51 100755 --- a/t/t0006-date.sh +++ b/t/t0006-date.sh @@ -56,6 +56,7 @@ check_show unix-local "$TIME" '1466000000' check_show 'format:%z' "$TIME" '+0200' check_show 'format-local:%z' "$TIME" '+0000' check_show 'format:%Z' "$TIME" '' +check_show 'format-local:%Z' "$TIME" 'UTC' check_show 'format:%%z' "$TIME" '%z' check_show 'format-local:%%z' "$TIME" '%z'