Skip to content

Commit 053e021

Browse files
ysai258claude
andcommitted
commit: warn when a new commit is dated before its parent
Git writes whatever the clock says into the commit object and validates nothing: a commit dated years in the future, or earlier than its own parent, is accepted silently. "git fsck --strict" does not object either, since fsck's badDate and badDateOverflow checks are purely syntactic. That would be harmless if history traversal did not assume commit dates are non-decreasing, but it does. "git log --since" stops walking at the first commit older than the cutoff, so a single out-of-order date hides every commit behind it: $ git log --pretty='%cd %s' --date=short 2026-09-25 C3 - inside the window 2026-09-01 C2 - outside the window 2026-09-20 C1 - inside the window $ git log --pretty='%cd %s' --date=short --since=2026-09-13 2026-09-25 C3 - inside the window C1 is inside the window and silently missing. This is understood -- 9669778 (revision: add "--since-as-filter" option, 2022-07-19) added an opt-in traversal mode for it -- but nothing tells the person whose clock caused it, at the moment they could still fix it cheaply. Warn at commit time when the new commit's date precedes a parent's, gated on a new advice.clockSkew setting. Warning rather than refusing is deliberate: only the committer can tell whether their clock or the parent's is the wrong one. Once the commit is published the date is part of its object name, and correcting it means rewriting every descendant, so the warning is worth little later and quite a lot now. The check looks at the commit being created and its parents and nothing else. Skew between different machines is ordinary in a distributed system and is not something to complain about; this fires only when one repository's own history steps backwards. It is limited to git commit -- merges and replayed history go through other paths, where non-monotonic dates are often legitimate. A warning along these lines has been suggested more than once without landing; see for instance the discussion around clock skew in <CA+55aFw_XjWm+4XwsN6CRJnsrcEu5YEChOHSHN51UUBN6PynWw@mail.gmail.com>. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yashwanth Sai <ysaimuppineni789@gmail.com>
1 parent 47ce805 commit 053e021

5 files changed

Lines changed: 97 additions & 0 deletions

File tree

Documentation/config/advice.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ all advice messages.
3838
configuration variable for how to set a given remote
3939
to be used by default in some situations where this
4040
advice would be printed.
41+
clockSkew::
42+
Shown by linkgit:git-commit[1] when the commit being
43+
created is dated earlier than one of its parents, which
44+
usually means the system clock is wrong. History
45+
traversal assumes commit dates do not decrease, so such
46+
a commit can cause commands like `git log --since` to
47+
skip the commits behind it.
4148
commitBeforeMerge::
4249
Shown when linkgit:git-merge[1] refuses to
4350
merge to avoid overwriting local changes.

advice.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ static struct {
5050
[ADVICE_AMBIGUOUS_FETCH_REFSPEC] = { "ambiguousFetchRefspec" },
5151
[ADVICE_AM_WORK_DIR] = { "amWorkDir" },
5252
[ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME] = { "checkoutAmbiguousRemoteBranchName" },
53+
[ADVICE_CLOCK_SKEW] = { "clockSkew" },
5354
[ADVICE_COMMIT_BEFORE_MERGE] = { "commitBeforeMerge" },
5455
[ADVICE_DEFAULT_BRANCH_NAME] = { "defaultBranchName" },
5556
[ADVICE_DETACHED_HEAD] = { "detachedHead" },

advice.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ enum advice_type {
1717
ADVICE_AMBIGUOUS_FETCH_REFSPEC,
1818
ADVICE_AM_WORK_DIR,
1919
ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME,
20+
ADVICE_CLOCK_SKEW,
2021
ADVICE_COMMIT_BEFORE_MERGE,
2122
ADVICE_DEFAULT_BRANCH_NAME, /* To be retired sometime after Git 3.0 */
2223
ADVICE_DETACHED_HEAD,

builtin/commit.c

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "builtin.h"
1212
#include "advice.h"
1313
#include "config.h"
14+
#include "date.h"
1415
#include "lockfile.h"
1516
#include "cache-tree.h"
1617
#include "color.h"
@@ -21,6 +22,7 @@
2122
#include "commit.h"
2223
#include "add-interactive.h"
2324
#include "gettext.h"
25+
#include "ident.h"
2426
#include "revision.h"
2527
#include "wt-status.h"
2628
#include "run-command.h"
@@ -1666,6 +1668,63 @@ struct repository *repo UNUSED)
16661668
return 0;
16671669
}
16681670

1671+
/*
1672+
* Warn when the commit we are about to write is dated earlier than a parent.
1673+
*
1674+
* Git stores whatever the clock says, and history traversal assumes commit
1675+
* dates do not decrease: "git log --since", for one, stops walking at the
1676+
* first commit older than the cutoff, so an out-of-order date silently hides
1677+
* the commits behind it. Only the person committing can tell whether their
1678+
* clock or the parent's is the wrong one, so warn rather than refuse.
1679+
*
1680+
* This deliberately looks at nothing but the commit being created and its
1681+
* parents. Skew between machines is normal in a distributed system and is not
1682+
* something to complain about at commit time.
1683+
*/
1684+
static void warn_if_dated_before_parents(struct commit_list *parents)
1685+
{
1686+
struct ident_split committer;
1687+
struct strbuf ours = STRBUF_INIT;
1688+
const char *info;
1689+
timestamp_t date, newest = 0;
1690+
1691+
if (!advice_enabled(ADVICE_CLOCK_SKEW))
1692+
return;
1693+
1694+
info = git_committer_info(IDENT_STRICT);
1695+
if (split_ident_line(&committer, info, strlen(info)) ||
1696+
!committer.date_begin)
1697+
return;
1698+
date = parse_timestamp(committer.date_begin, NULL, 10);
1699+
1700+
for (; parents; parents = parents->next) {
1701+
struct commit *parent = parents->item;
1702+
1703+
if (repo_parse_commit(the_repository, parent))
1704+
continue;
1705+
if (parent->date > newest)
1706+
newest = parent->date;
1707+
}
1708+
1709+
if (!newest || date >= newest)
1710+
return;
1711+
1712+
/* show_date() reuses one buffer, so keep a copy of the first result. */
1713+
strbuf_addstr(&ours, show_date(date, atoi(committer.date_end + 1),
1714+
DATE_MODE(ISO8601)));
1715+
1716+
advise_if_enabled(ADVICE_CLOCK_SKEW,
1717+
_("the new commit is dated %s,\n"
1718+
"which is earlier than its parent, dated %s.\n"
1719+
"This usually means the system clock is wrong.\n"
1720+
"Commands that walk history in date order, such as\n"
1721+
"\"git log --since\", may skip commits as a result."),
1722+
ours.buf,
1723+
/* A parsed commit keeps no timezone, so show UTC. */
1724+
show_date(newest, 0, DATE_MODE(ISO8601)));
1725+
strbuf_release(&ours);
1726+
}
1727+
16691728
static int git_commit_config(const char *k, const char *v,
16701729
const struct config_context *ctx, void *cb)
16711730
{
@@ -1935,6 +1994,8 @@ int cmd_commit(int argc,
19351994
append_merge_tag_headers(parents, &tail);
19361995
}
19371996

1997+
warn_if_dated_before_parents(parents);
1998+
19381999
if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
19392000
parents, &oid, author_ident.buf, NULL,
19402001
sign_commit, extra)) {

t/t7502-commit-porcelain.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,4 +1003,31 @@ test_expect_success WITH_BREAKING_CHANGES 'core.commentChar=auto is rejected' '
10031003
test_cmp expect actual
10041004
'
10051005

1006+
test_expect_success 'warn when a commit is dated before its parent' '
1007+
test_when_finished "git checkout main 2>/dev/null || git checkout master" &&
1008+
git checkout -b clock-skew &&
1009+
test_commit --date "2026-09-25T10:00:00+0000" skew-parent &&
1010+
echo skew >skew-child &&
1011+
git add skew-child &&
1012+
GIT_COMMITTER_DATE="2026-09-13T06:00:00+0000" \
1013+
git commit -m "behind its parent" 2>actual &&
1014+
test_grep "earlier than its parent" actual
1015+
'
1016+
1017+
test_expect_success 'no warning when commit dates increase' '
1018+
echo forward >skew-forward &&
1019+
git add skew-forward &&
1020+
GIT_COMMITTER_DATE="2026-09-26T06:00:00+0000" \
1021+
git commit -m "after its parent" 2>actual &&
1022+
test_grep ! "earlier than its parent" actual
1023+
'
1024+
1025+
test_expect_success 'advice.clockSkew silences the warning' '
1026+
echo quiet >skew-quiet &&
1027+
git add skew-quiet &&
1028+
GIT_COMMITTER_DATE="2026-09-14T06:00:00+0000" \
1029+
git -c advice.clockSkew=false commit -m quiet 2>actual &&
1030+
test_grep ! "earlier than its parent" actual
1031+
'
1032+
10061033
test_done

0 commit comments

Comments
 (0)