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 <ulm@gentoo.org> Helped-by: Jeff King <peff@peff.net> Signed-off-by: Rene Scharfe <l.s.r@web.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

René Scharfe committed Jun 15, 2017 at 14:29 UTC c3fbf81a8534cf88ff948d12004eb94929ec1174
5 files changed +54 -8
Documentation/rev-list-options.txt
+2 -1
@@ -764,7 +764,8 @@ timezone value.
764 1970). As with `--raw`, this is always in UTC and therefore `-local`
765 has no effect.
766 +
767 -`--date=format:...` feeds the format `...` to your system `strftime`.
767 +`--date=format:...` feeds the format `...` to your system `strftime`,
768 +except for %z and %Z, which are handled internally.
769 Use `--date=format:%c` to show the date in your system locale's
770 preferred format. See the `strftime` manual for a complete list of
771 format placeholders. When using `-local`, the correct syntax is
date.c
+1 -1
@@ -233,7 +233,7 @@ const char *show_date(unsigned long time, int tz, const struct date_mode *mode)
233 month_names[tm->tm_mon], tm->tm_year + 1900,
234 tm->tm_hour, tm->tm_min, tm->tm_sec, tz);
235 else if (mode->type == DATE_STRFTIME)
236 - strbuf_addftime(&timebuf, mode->strftime_fmt, tm);
236 + strbuf_addftime(&timebuf, mode->strftime_fmt, tm, tz, "");
237 else
238 strbuf_addf(&timebuf, "%.3s %.3s %d %02d:%02d:%02d %d%c%+05d",
239 weekday_names[tm->tm_wday],
strbuf.c
+37 -4
@@ -785,14 +785,48 @@ char *xstrfmt(const char *fmt, ...)
785 return ret;
786 }
787
788 -void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm)
788 +void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm,
789 + int tz_offset, const char *tz_name)
790 {
791 + struct strbuf munged_fmt = STRBUF_INIT;
792 size_t hint = 128;
793 size_t len;
794
795 if (!*fmt)
796 return;
797
798 + /*
799 + * There is no portable way to pass timezone information to
800 + * strftime, so we handle %z and %Z here.
801 + */
802 + for (;;) {
803 + const char *percent = strchrnul(fmt, '%');
804 + strbuf_add(&munged_fmt, fmt, percent - fmt);
805 + if (!*percent)
806 + break;
807 + fmt = percent + 1;
808 + switch (*fmt) {
809 + case '%':
810 + strbuf_addstr(&munged_fmt, "%%");
811 + fmt++;
812 + break;
813 + case 'z':
814 + strbuf_addf(&munged_fmt, "%+05d", tz_offset);
815 + fmt++;
816 + break;
817 + case 'Z':
818 + if (tz_name) {
819 + strbuf_addstr(&munged_fmt, tz_name);
820 + fmt++;
821 + break;
822 + }
823 + /* FALLTHROUGH */
824 + default:
825 + strbuf_addch(&munged_fmt, '%');
826 + }
827 + }
828 + fmt = munged_fmt.buf;
829 +
830 strbuf_grow(sb, hint);
831 len = strftime(sb->buf + sb->len, sb->alloc - sb->len, fmt, tm);
832
@@ -804,17 +838,16 @@ void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm)
838 * output contains at least one character, and then drop the extra
839 * character before returning.
840 */
807 - struct strbuf munged_fmt = STRBUF_INIT;
808 - strbuf_addf(&munged_fmt, "%s ", fmt);
841 + strbuf_addch(&munged_fmt, ' ');
842 while (!len) {
843 hint *= 2;
844 strbuf_grow(sb, hint);
845 len = strftime(sb->buf + sb->len, sb->alloc - sb->len,
846 munged_fmt.buf, tm);
847 }
815 - strbuf_release(&munged_fmt);
848 len--; /* drop munged space */
849 }
850 + strbuf_release(&munged_fmt);
851 strbuf_setlen(sb, sb->len + len);
852 }
853
strbuf.h
+8 -2
@@ -340,8 +340,14 @@ extern void strbuf_vaddf(struct strbuf *sb, const char *fmt, va_list ap);
340
341 /**
342 * Add the time specified by `tm`, as formatted by `strftime`.
343 - */
344 -extern void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm);
343 + * `tz_name` is used to expand %Z internally unless it's NULL.
344 + * `tz_offset` is in decimal hhmm format, e.g. -600 means six hours west
345 + * of Greenwich, and it's used to expand %z internally. However, tokens
346 + * with modifiers (e.g. %Ez) are passed to `strftime`.
347 + */
348 +extern void strbuf_addftime(struct strbuf *sb, const char *fmt,
349 + const struct tm *tm, int tz_offset,
350 + const char *tz_name);
351
352 /**
353 * Read a given size of data from a FILE* pointer to the buffer.
t/t0006-date.sh
+6
@@ -51,6 +51,12 @@ check_show iso-local "$TIME" '2016-06-15 14:13:20 +0000'
51 check_show raw-local "$TIME" '1466000000 +0000'
52 check_show unix-local "$TIME" '1466000000'
53
54 +check_show 'format:%z' "$TIME" '+0200'
55 +check_show 'format-local:%z' "$TIME" '+0000'
56 +check_show 'format:%Z' "$TIME" ''
57 +check_show 'format:%%z' "$TIME" '%z'
58 +check_show 'format-local:%%z' "$TIME" '%z'
59 +
60 # arbitrary time absurdly far in the future
61 FUTURE="5758122296 -0400"
62 check_show iso "$FUTURE" "2152-06-19 18:24:56 -0400" LONG_IS_64BIT