]> git.ipfire.org Git - thirdparty/git.git/commitdiff
strbuf_addftime(): handle "%s" manually
authorJeff King <peff@peff.net>
Tue, 2 Nov 2021 11:35:34 +0000 (07:35 -0400)
committerJunio C Hamano <gitster@pobox.com>
Thu, 4 Nov 2021 19:38:09 +0000 (12:38 -0700)
The strftime() function has a non-standard "%s" extension, which prints
the number of seconds since the epoch. But the "struct tm" we get has
already been adjusted for a particular time zone; going back to an epoch
time requires knowing that zone offset. Since strftime() doesn't take
such an argument, round-tripping to a "struct tm" and back to the "%s"
format may produce the wrong value (off by tz_offset seconds).

Since we're already passing in the zone offset courtesy of c3fbf81a85
(strbuf: let strbuf_addftime handle %z and %Z itself, 2017-06-15), we
can use that same value to adjust our epoch seconds accordingly.

Note that the description above makes it sound like strftime()'s "%s" is
useless (and really, the issue is shared by mktime(), which is what
strftime() would use under the hood). But it gets the two cases for
which it's designed correct:

  - the result of gmtime() will have a zero offset, so no adjustment is
    necessary

  - the result of localtime() will be offset by the local zone offset,
    and mktime() and strftime() are defined to assume this offset when
    converting back (there's actually some magic here; some
    implementations record this in the "struct tm", but we can't
    portably access or manipulate it. But they somehow "know" whether a
    "struct tm" is from gmtime() or localtime()).

This latter point means that "format-local:%s" actually works correctly
already, because in that case we rely on the system routines due to
6eced3ec5e (date: use localtime() for "-local" time formats,
2017-06-15). Our problem comes when trying to show times in the author's
zone, as the system routines provide no mechanism for converting in
non-local zones. So in those cases we have a "struct tm" that came from
gmtime(), but has been manipulated according to our offset.

The tests cover the broken round-trip by formatting "%s" for a time in a
non-system timezone. We use the made-up "+1234" here, which has two
advantages. One, we know it won't ever be the real system zone (and so
we're actually testing a case that would break). And two, since it has a
minute component, we're testing the full decoding of the +HHMM zone into
a number of seconds. Likewise, we test the "-1234" variant to make sure
there aren't any sign mistakes.

There's one final test, which covers "format-local:%s". As noted, this
already passes, but it's important to check that we didn't regress this
case. In particular, the caller in show_date() is relying on localtime()
to have done the zone adjustment, independent of any tz_offset we
compute ourselves. These should match up, since our local_tzoffset() is
likewise built around localtime(). But it would be easy for a caller to
forget to pass in a correct tz_offset to strbuf_addftime(). Fortunately
show_date() does this correctly (it has to because of the existing
handling of %z), and the test continues to pass. So this one is just
future-proofing against a change in our assumptions.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/rev-list-options.txt
cache.h
date.c
strbuf.c
t/t0006-date.sh

index b7bd27e1713be969b9e2fdc5a7c88cf1d1a5ebe8..2ead371c899b7b0321b4ace653fdf3ee44a7a16e 100644 (file)
@@ -1053,7 +1053,7 @@ omitted.
 has no effect.
 
 `--date=format:...` feeds the format `...` to your system `strftime`,
-except for %z and %Z, which are handled internally.
+except for %s, %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/cache.h b/cache.h
index eba12487b99caae71cbd4367e609e156ea9867ff..aa6f380d100b8a9ce541b59eb3605be3eb54f012 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -1588,6 +1588,7 @@ timestamp_t approxidate_careful(const char *, int *);
 timestamp_t approxidate_relative(const char *date);
 void parse_date_format(const char *format, struct date_mode *mode);
 int date_overflows(timestamp_t date);
+time_t tm_to_time_t(const struct tm *tm);
 
 #define IDENT_STRICT          1
 #define IDENT_NO_DATE         2
diff --git a/date.c b/date.c
index c55ea47e96ade22462c4b02f1e9954d13ef72837..84bb4451c1ae167cfd88ae4a4b936a69b1c9296c 100644 (file)
--- a/date.c
+++ b/date.c
@@ -9,7 +9,7 @@
 /*
  * This is like mktime, but without normalization of tm_wday and tm_yday.
  */
-static time_t tm_to_time_t(const struct tm *tm)
+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
index b22e9816559cabc4f3d94f7f42cde5a50f32495a..613fee8c82e0f1682e99b5facee338c5e14cd28a 100644 (file)
--- a/strbuf.c
+++ b/strbuf.c
@@ -1006,7 +1006,12 @@ void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm,
 
        /*
         * There is no portable way to pass timezone information to
-        * strftime, so we handle %z and %Z here.
+        * strftime, so we handle %z and %Z here. Likewise '%s', because
+        * going back to an epoch time requires knowing the zone.
+        *
+        * Note that tz_offset is in the "[-+]HHMM" decimal form; this is what
+        * we want for %z, but the computation for %s has to convert to number
+        * of seconds.
         */
        for (;;) {
                const char *percent = strchrnul(fmt, '%');
@@ -1019,6 +1024,13 @@ void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm,
                        strbuf_addstr(&munged_fmt, "%%");
                        fmt++;
                        break;
+               case 's':
+                       strbuf_addf(&munged_fmt, "%"PRItime,
+                                   (timestamp_t)tm_to_time_t(tm) -
+                                   3600 * (tz_offset / 100) -
+                                   60 * (tz_offset % 100));
+                       fmt++;
+                       break;
                case 'z':
                        strbuf_addf(&munged_fmt, "%+05d", tz_offset);
                        fmt++;
index 6b757d7169252b467ed18c4f209c0428e286bba3..794186961eebcc0edc8442b8d730287a5ff2c67a 100755 (executable)
@@ -63,6 +63,10 @@ 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
 
+check_show 'format:%s' '123456789 +1234' 123456789
+check_show 'format:%s' '123456789 -1234' 123456789
+check_show 'format-local:%s' '123456789 -1234' 123456789
+
 # arbitrary time absurdly far in the future
 FUTURE="5758122296 -0400"
 check_show iso       "$FUTURE" "2152-06-19 18:24:56 -0400" TIME_IS_64BIT,TIME_T_IS_64BIT