diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc index 81f80a92745123..59b643d9d0c626 100644 --- a/Documentation/config/advice.adoc +++ b/Documentation/config/advice.adoc @@ -38,6 +38,13 @@ all advice messages. configuration variable for how to set a given remote to be used by default in some situations where this advice would be printed. + clockSkew:: + Shown by linkgit:git-commit[1] when the commit being + created is dated earlier than one of its parents, which + usually means the system clock is wrong. History + traversal assumes commit dates do not decrease, so such + a commit can cause commands like `git log --since` to + skip the commits behind it. commitBeforeMerge:: Shown when linkgit:git-merge[1] refuses to merge to avoid overwriting local changes. diff --git a/advice.c b/advice.c index 63bf8b0c5f0481..3e14859de475b1 100644 --- a/advice.c +++ b/advice.c @@ -50,6 +50,7 @@ static struct { [ADVICE_AMBIGUOUS_FETCH_REFSPEC] = { "ambiguousFetchRefspec" }, [ADVICE_AM_WORK_DIR] = { "amWorkDir" }, [ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME] = { "checkoutAmbiguousRemoteBranchName" }, + [ADVICE_CLOCK_SKEW] = { "clockSkew" }, [ADVICE_COMMIT_BEFORE_MERGE] = { "commitBeforeMerge" }, [ADVICE_DEFAULT_BRANCH_NAME] = { "defaultBranchName" }, [ADVICE_DETACHED_HEAD] = { "detachedHead" }, diff --git a/advice.h b/advice.h index 66f6cd6a772d8c..43de2b19a2fa73 100644 --- a/advice.h +++ b/advice.h @@ -17,6 +17,7 @@ enum advice_type { ADVICE_AMBIGUOUS_FETCH_REFSPEC, ADVICE_AM_WORK_DIR, ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME, + ADVICE_CLOCK_SKEW, ADVICE_COMMIT_BEFORE_MERGE, ADVICE_DEFAULT_BRANCH_NAME, /* To be retired sometime after Git 3.0 */ ADVICE_DETACHED_HEAD, diff --git a/builtin/commit.c b/builtin/commit.c index 28f61745034506..70b9aa5ec5a3de 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -11,6 +11,7 @@ #include "builtin.h" #include "advice.h" #include "config.h" +#include "date.h" #include "lockfile.h" #include "cache-tree.h" #include "color.h" @@ -21,6 +22,7 @@ #include "commit.h" #include "add-interactive.h" #include "gettext.h" +#include "ident.h" #include "revision.h" #include "wt-status.h" #include "run-command.h" @@ -1666,6 +1668,63 @@ struct repository *repo UNUSED) return 0; } +/* + * Warn when the commit we are about to write is dated earlier than a parent. + * + * Git stores whatever the clock says, and history traversal assumes commit + * dates do not decrease: "git log --since", for one, stops walking at the + * first commit older than the cutoff, so an out-of-order date silently hides + * the commits behind it. Only the person committing can tell whether their + * clock or the parent's is the wrong one, so warn rather than refuse. + * + * This deliberately looks at nothing but the commit being created and its + * parents. Skew between machines is normal in a distributed system and is not + * something to complain about at commit time. + */ +static void warn_if_dated_before_parents(struct commit_list *parents) +{ + struct ident_split committer; + struct strbuf ours = STRBUF_INIT; + const char *info; + timestamp_t date, newest = 0; + + if (!advice_enabled(ADVICE_CLOCK_SKEW)) + return; + + info = git_committer_info(IDENT_STRICT); + if (split_ident_line(&committer, info, strlen(info)) || + !committer.date_begin) + return; + date = parse_timestamp(committer.date_begin, NULL, 10); + + for (; parents; parents = parents->next) { + struct commit *parent = parents->item; + + if (repo_parse_commit(the_repository, parent)) + continue; + if (parent->date > newest) + newest = parent->date; + } + + if (!newest || date >= newest) + return; + + /* show_date() reuses one buffer, so keep a copy of the first result. */ + strbuf_addstr(&ours, show_date(date, atoi(committer.date_end + 1), + DATE_MODE(ISO8601))); + + advise_if_enabled(ADVICE_CLOCK_SKEW, + _("the new commit is dated %s,\n" + "which is earlier than its parent, dated %s.\n" + "This usually means the system clock is wrong.\n" + "Commands that walk history in date order, such as\n" + "\"git log --since\", may skip commits as a result."), + ours.buf, + /* A parsed commit keeps no timezone, so show UTC. */ + show_date(newest, 0, DATE_MODE(ISO8601))); + strbuf_release(&ours); +} + static int git_commit_config(const char *k, const char *v, const struct config_context *ctx, void *cb) { @@ -1935,6 +1994,8 @@ int cmd_commit(int argc, append_merge_tag_headers(parents, &tail); } + warn_if_dated_before_parents(parents); + if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid, parents, &oid, author_ident.buf, NULL, sign_commit, extra)) { diff --git a/t/t7502-commit-porcelain.sh b/t/t7502-commit-porcelain.sh index 2adfe70b3d1c25..fb611b191bc7e0 100755 --- a/t/t7502-commit-porcelain.sh +++ b/t/t7502-commit-porcelain.sh @@ -1003,4 +1003,31 @@ test_expect_success WITH_BREAKING_CHANGES 'core.commentChar=auto is rejected' ' test_cmp expect actual ' +test_expect_success 'warn when a commit is dated before its parent' ' + test_when_finished "git checkout main 2>/dev/null || git checkout master" && + git checkout -b clock-skew && + test_commit --date "2026-09-25T10:00:00+0000" skew-parent && + echo skew >skew-child && + git add skew-child && + GIT_COMMITTER_DATE="2026-09-13T06:00:00+0000" \ + git commit -m "behind its parent" 2>actual && + test_grep "earlier than its parent" actual +' + +test_expect_success 'no warning when commit dates increase' ' + echo forward >skew-forward && + git add skew-forward && + GIT_COMMITTER_DATE="2026-09-26T06:00:00+0000" \ + git commit -m "after its parent" 2>actual && + test_grep ! "earlier than its parent" actual +' + +test_expect_success 'advice.clockSkew silences the warning' ' + echo quiet >skew-quiet && + git add skew-quiet && + GIT_COMMITTER_DATE="2026-09-14T06:00:00+0000" \ + git -c advice.clockSkew=false commit -m quiet 2>actual && + test_grep ! "earlier than its parent" actual +' + test_done