From 116eb3abfe4f79907f701317c2d905868941fff7 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 2 Feb 2012 13:41:42 -0800 Subject: [PATCH 1/2] parse_date(): allow ancient git-timestamp The date-time parser parses out a human-readble datestring piece by piece, so that it could even parse a string in a rather strange notation like 'noon november 11, 2005', but restricts itself from parsing strings in " " format only for reasonably new timestamps (like 1974 or newer) with 10 or more digits. This is to prevent a string like "20100917" from getting interpreted as seconds since epoch (we want to treat it as September 17, 2010 instead) while doing so. The same codepath is used to read back the timestamp that we have already recorded in the headers of commit and tag objects; because of this, such a commit with timestamp "0 +0000" cannot be rebased or amended very easily. Teach parse_date() codepath to special case a string of the form " +<4-digits>" to work this issue around, but require that there is no other cruft around the string when parsing a timestamp of this format for safety. Note that this has a slight backward incompatibility implications. If somebody writes "git commit --date='20100917 +0900'" and wants it to mean a timestamp in September 2010 in Japan, this change will break such a use case. Signed-off-by: Junio C Hamano --- date.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/date.c b/date.c index 896fbb4806..099ddbe7fc 100644 --- a/date.c +++ b/date.c @@ -585,6 +585,33 @@ static int date_string(unsigned long date, int offset, char *buf, int len) return snprintf(buf, len, "%lu %c%02d%02d", date, sign, offset / 60, offset % 60); } +/* + * Parse a string like "0 +0000" as ancient timestamp near epoch, but + * only when it appears not as part of any other string. + */ +static int match_object_header_date(const char *date, unsigned long *timestamp, int *offset) +{ + char *end; + unsigned long stamp; + int ofs; + + if (*date < '0' || '9' <= *date) + return -1; + stamp = strtoul(date, &end, 10); + if (*end != ' ' || stamp == ULONG_MAX || (end[1] != '+' && end[1] != '-')) + return -1; + date = end + 2; + ofs = strtol(date, &end, 10); + if ((*end != '\0' && (*end != '\n')) || end != date + 4) + return -1; + ofs = (ofs / 100) * 60 + (ofs % 100); + if (date[-1] == '-') + ofs = -ofs; + *timestamp = stamp; + *offset = ofs; + return 0; +} + /* Gr. strptime is crap for this; it doesn't have a way to require RFC2822 (i.e. English) day/month names, and it doesn't work correctly with %z. */ int parse_date_basic(const char *date, unsigned long *timestamp, int *offset) @@ -610,6 +637,8 @@ int parse_date_basic(const char *date, unsigned long *timestamp, int *offset) *offset = -1; tm_gmt = 0; + if (!match_object_header_date(date, timestamp, offset)) + return 0; /* success */ for (;;) { int match = 0; unsigned char c = *date; From 2c733fb24c10a9d7aacc51f956bf9b7881980870 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 2 Feb 2012 13:41:43 -0800 Subject: [PATCH 2/2] parse_date(): '@' prefix forces git-timestamp The only place that the issue this series addresses was observed where we read "cat-file commit" output and put it in GIT_AUTHOR_DATE in order to replay a commit with an ancient timestamp. With the previous patch alone, "git commit --date='20100917 +0900'" can be misinterpreted to mean an ancient timestamp, not September in year 2010. Guard this codepath by requring an extra '@' in front of the raw git timestamp on the parsing side. This of course needs to be compensated by updating get_author_ident_from_commit and the code for "git commit --amend" to prepend '@' to the string read from the existing commit in the GIT_AUTHOR_DATE environment variable. Signed-off-by: Junio C Hamano --- builtin/commit.c | 6 ++++++ date.c | 3 ++- git-sh-setup.sh | 2 +- t/t3400-rebase.sh | 23 +++++++++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index cbc9613ec6..bcb0db2db5 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -534,6 +534,7 @@ static void determine_author_info(struct strbuf *author_ident) if (author_message) { const char *a, *lb, *rb, *eol; + size_t len; a = strstr(author_message_buffer, "\nauthor "); if (!a) @@ -554,6 +555,11 @@ static void determine_author_info(struct strbuf *author_ident) (a + strlen("\nauthor ")))); email = xmemdupz(lb + strlen("<"), rb - (lb + strlen("<"))); date = xmemdupz(rb + strlen("> "), eol - (rb + strlen("> "))); + len = eol - (rb + strlen("> ")); + date = xmalloc(len + 2); + *date = '@'; + memcpy(date + 1, rb + strlen("> "), len); + date[len + 1] = '\0'; } if (force_author) { diff --git a/date.c b/date.c index 099ddbe7fc..bf8e088e6a 100644 --- a/date.c +++ b/date.c @@ -637,7 +637,8 @@ int parse_date_basic(const char *date, unsigned long *timestamp, int *offset) *offset = -1; tm_gmt = 0; - if (!match_object_header_date(date, timestamp, offset)) + if (*date == '@' && + !match_object_header_date(date + 1, timestamp, offset)) return 0; /* success */ for (;;) { int match = 0; diff --git a/git-sh-setup.sh b/git-sh-setup.sh index 8e427dab31..015fe6e336 100644 --- a/git-sh-setup.sh +++ b/git-sh-setup.sh @@ -200,7 +200,7 @@ get_author_ident_from_commit () { s/.*/GIT_AUTHOR_EMAIL='\''&'\''/p g - s/^author [^<]* <[^>]*> \(.*\)$/\1/ + s/^author [^<]* <[^>]*> \(.*\)$/@\1/ s/.*/GIT_AUTHOR_DATE='\''&'\''/p q diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh index 6eaecec906..e26e14dd53 100755 --- a/t/t3400-rebase.sh +++ b/t/t3400-rebase.sh @@ -218,4 +218,27 @@ test_expect_success 'rebase -m can copy notes' ' test "a note" = "$(git notes show HEAD)" ' +test_expect_success 'rebase commit with an ancient timestamp' ' + git reset --hard && + + >old.one && git add old.one && test_tick && + git commit --date="@12345 +0400" -m "Old one" && + >old.two && git add old.two && test_tick && + git commit --date="@23456 +0500" -m "Old two" && + >old.three && git add old.three && test_tick && + git commit --date="@34567 +0600" -m "Old three" && + + git cat-file commit HEAD^^ >actual && + grep "author .* 12345 +0400$" actual && + git cat-file commit HEAD^ >actual && + grep "author .* 23456 +0500$" actual && + git cat-file commit HEAD >actual && + grep "author .* 34567 +0600$" actual && + + git rebase --onto HEAD^^ HEAD^ && + + git cat-file commit HEAD >actual && + grep "author .* 34567 +0600$" actual +' + test_done