Git development
 help / color / mirror / Atom feed
* [PATCH] date: add --date=elapsed and %aX/%cX placeholders
@ 2026-07-30  7:01 Israel Roldan via GitGitGadget
  0 siblings, 0 replies; only message in thread
From: Israel Roldan via GitGitGadget @ 2026-07-30  7:01 UTC (permalink / raw)
  To: git; +Cc: Israel Roldan, Israel Roldan

From: Israel Roldan <israel.alberto.rv@gmail.com>

The --date=human format and the %cr/%ch placeholders both produce
a localized English phrase that hides the precise amount of time
that has passed. A commit shown as "2 days ago" may in reality be
1 day 3 hours, or 1 day 23 hours. Switching to %ct and a calculator
solves the problem but is not glance-friendly.

Add an uptime(1)-style elapsed-time format:
  --date=elapsed          --date=elapsed-pad
  39s                     39s
  8h 7m 48s               08h 07m 48s
  1y 11M 5d 13h 5m 13s    01y 11M 05d 13h 05m 13s

The placeholder letter is X because the obvious choice E is already
used for the mailmap-respecting email variants (%aE, %cE, %gE).

Signed-off-by: Israel Roldan <israel.alberto.rv@gmail.com>
---
    [RFC PATCH] date: add --date=elapsed and %aX/%cX placeholders
    
    
    Motivation
    ==========
    
    The --date=human format and the %cr/%ch placeholders both produce a
    localized English phrase that hides the precise amount of time that has
    passed. A commit shown as "2 days ago" may in reality be 1 day 3 hours,
    or 1 day 23 hours. Switching to %ct and a calculator solves the problem
    but is not glance-friendly.
    
    The closest external tool that solves the same problem is
    git-log-compact by Kyle J. McKay (2016), which the maintainer declined
    to merge into Git core because it was a heavyweight change to the
    oneline output rather than a date format.
    
    
    What this patch adds
    ====================
    
    A new date format --date=elapsed and --date=elapsed-pad modelled on
    uptime(1):
    
      seconds   --date=elapsed          --date=elapsed-pad
          39    "39s"                   "39s"
         984    "16m 24s"               "16m 24s"
       29268    "8h 7m 48s"             "08h 07m 48s"
     3024000    "1M 5d 0s"              "01M 05d 00h 00m 00s"
    60527113    "1y 11M 5d 13h 5m 13s"  "01y 11M 05d 13h 05m 13s"
    
    
    The padded variant zero-pads every emitted unit to at least two digits.
    
    A commit timestamp that is in the future is reported as "0s", not the
    localized phrase "in the future" that show_date_relative() emits.
    
    Months are fixed at 30 days and years at 365 days, matching the
    uptime(1) convention. They are consistent, and the same approximations
    are already in use by Git's own show_date_relative() for the
    relative-time format.
    
    Two new pretty-format placeholders, %aX (author) and %cX (committer):
    
    git log --format='%h (%aX) %s'
    13c7afec21 (1d 2h 9s) The 8th batch
    8c7817c361 (1y 3d 19h 5m 10s) Merge branch 'rj/meson-libexecdir-fix'
    0e521a41b5 (11y 17d 21h 8m 40s) Merge branch 'et/http-proxyauth'
    
    
    The placeholders mirror %ch and %cr, which also ignore the caller's
    --date= and emit a fixed shape.
    
    
    Open questions
    ==============
    
    Q1. The placeholder letter. The natural mnemonic is E (Elapsed), but
    %aE, %cE and %gE are already taken (mailmap-respecting email variants).
    This patch uses X as a free letter. Would the maintainer prefer a
    different letter? I am not attached to X.
    
    Q2. Should --date=elapsed-pad be its own format name, or a separate
    --date=elapsed plus a flag? Which mirrors --date=human /
    --date=human-local / --date=iso / --date=iso-local.
    
    Q3. The "future timestamp" behaviour. This patch clamps a future
    timestamp to "0s". An alternative would be to keep the relative format's
    "in the future" wording.
    
    Q4. %aX/%cX are hard-coded compact; padded output requires %ad/%cd
    together with --date=elapsed-pad. Should a paired %aY/%cY for padded
    also exist, or is the asymmetry acceptable?
    
    
    Patch
    =====
    
    The patch modifies the existing date_mode_type enum, adds a new
    show_date_elapsed() helper in date.c, wires it through show_date() and
    parse_date_format(), exposes two placeholders via format_person_part()
    in pretty.c, handles the new format in builtin/blame.c for column width,
    updates the documentation, and adds tests for both the date helper and
    the pretty-format placeholder equivalence.
    
    The full patch is attached below.
    
    
    Testing
    =======
    
    $ make DEVELOPER=1 && cd t &&
    prove --no-color ./t0006-date.sh ./t4205-log-pretty-formats.sh
    ./t0006-date.sh ................ ok ./t4205-log-pretty-formats.sh .. ok
    All tests successful.
    
    The full test suite (1054 test files, 33832 tests) also passes on this
    machine.
    
    Thanks for your time. I am happy to iterate on any of the open questions
    before posting a non-RFC version.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2368%2Fairvzxf%2Ffeature%2Fdate-elapsed-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2368/airvzxf/feature/date-elapsed-v1
Pull-Request: https://github.com/git/git/pull/2368

 Documentation/pretty-formats.adoc      |  4 ++
 Documentation/rev-list-options.adoc    | 13 ++++
 builtin/blame.c                        |  8 +++
 contrib/completion/git-completion.bash |  2 +-
 date.c                                 | 88 +++++++++++++++++++++++++-
 date.h                                 |  4 +-
 pretty.c                               |  3 +
 t/helper/test-date.c                   | 19 +++++-
 t/t0006-date.sh                        | 43 +++++++++++++
 t/t4205-log-pretty-formats.sh          |  6 ++
 10 files changed, 186 insertions(+), 4 deletions(-)

diff --git a/Documentation/pretty-formats.adoc b/Documentation/pretty-formats.adoc
index 2ae0eb11a9..9ae882b160 100644
--- a/Documentation/pretty-formats.adoc
+++ b/Documentation/pretty-formats.adoc
@@ -233,6 +233,8 @@ The placeholders are:
 +%as+:: author date, short format (`YYYY-MM-DD`)
 +%ah+:: author date, human style (like the `--date=human` option of
 	linkgit:git-rev-list[1])
++%aX+:: author date, elapsed (like the `--date=elapsed` option of
+	linkgit:git-rev-list[1])
 +%cn+:: committer name
 +%cN+:: committer name (respecting .mailmap, see
 	linkgit:git-shortlog[1] or linkgit:git-blame[1])
@@ -251,6 +253,8 @@ The placeholders are:
 +%cs+:: committer date, short format (`YYYY-MM-DD`)
 +%ch+:: committer date, human style (like the `--date=human` option of
 	linkgit:git-rev-list[1])
++%cX+:: committer date, elapsed (like the `--date=elapsed` option of
+	linkgit:git-rev-list[1])
 +%d+:: ref names, like the --decorate option of linkgit:git-log[1]
 +%D+:: ref names without the " (", ")" wrapping.
 +%(count)+:: the number of a patch within a patch series. Used only in
diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc
index fd831f0ec6..937c6096c5 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -1174,6 +1174,19 @@ omitted.
 1970).  As with `--raw`, this is always in UTC and therefore `-local`
 has no effect.
 
+`--date=elapsed` shows the elapsed time between the commit and now,
+in a compact, script-friendly, `uptime(1)`-style format. Only the
+non-zero leading units are emitted, separated by single spaces. For
+example "39s", "16m 24s", "8h 7m 48s", "12M 30d 13h 25m 43s",
+"1y 11M 5d 13h 5m 13s". A commit timestamp that is in the future
+is reported as "0s". Months are fixed at 30 days and years at 365
+days, matching the `uptime(1)` convention.
+
+`--date=elapsed-pad` is like `--date=elapsed`, but every emitted
+unit is at least two digits wide, zero-padded on the left, so
+that columns line up across rows.  For example "16m 24s",
+"08h 07m 48s", "01M 05d 00h 00m 00s", "01y 11M 05d 13h 05m 13s".
+
 `--date=format:<format>` feeds the _<format>_ to your system `strftime`,
 except for `%s`, `%z`, and `%Z`, which are handled internally.
 Use `--date=format:%c` to show the date in your system locale's
diff --git a/builtin/blame.c b/builtin/blame.c
index 48d5251c6d..f3fd866e82 100644
--- a/builtin/blame.c
+++ b/builtin/blame.c
@@ -1123,6 +1123,14 @@ parse_done:
 		/* If the year is shown, no time is shown */
 		blame_date_width = sizeof("Thu Oct 19 16:00");
 		break;
+	case DATE_ELAPSED:
+	case DATE_ELAPSED_PAD:
+		/*
+		 * Maximum display width of the elapsed time format,
+		 * e.g. "99y 99M 99d 99h 99m 99s" -- a safe upper bound.
+		 */
+		blame_date_width = sizeof("99y 99M 99d 99h 99m 99s");
+		break;
 	case DATE_NORMAL:
 		blame_date_width = sizeof("Thu Oct 19 16:00:04 2006 -0700");
 		break;
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index e875787710..4db014c7c0 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -2231,7 +2231,7 @@ __git_log_show_options="
 __git_diff_merges_opts="off none on first-parent 1 separate m combined c dense-combined cc remerge r"
 
 __git_log_pretty_formats="oneline short medium full fuller reference email raw format: tformat: mboxrd"
-__git_log_date_formats="relative iso8601 iso8601-strict rfc2822 short local default human raw unix auto: format:"
+__git_log_date_formats="relative iso8601 iso8601-strict rfc2822 short local default human raw unix elapsed auto: format:"
 
 # Complete porcelain (i.e. not git-rev-list) options and at least some
 # option arguments accepted by git-log.  Note that this same set of options
diff --git a/date.c b/date.c
index 014065b419..566da2fd21 100644
--- a/date.c
+++ b/date.c
@@ -209,6 +209,79 @@ void show_date_relative(timestamp_t time, struct strbuf *timebuf)
 		 (diff + 183) / 365);
 }
 
+/*
+ * Print the elapsed time between `time` and now in a compact, script-friendly
+ * "uptime(1)"-style format: only non-zero units are emitted, left to right,
+ * separated by single spaces.  With `pad` set, every emitted unit is at least
+ * two digits wide, zero-padded on the left.
+ *
+ *   diff < 60s          -> "39s"
+ *   diff < 60m          -> "16m 24s"
+ *   diff < 24h          -> "08h 07m 48s" (pad) / "8h 7m 48s" (compact)
+ *   diff < 1y           -> "12M 30d 13h 25m 43s"
+ *   diff >= 1y          -> "01y 11M 05d 13h 05m 13s" (pad) / "1y 11M 5d ..." (compact)
+ *
+ * A negative diff (commit timestamp is in the future) clamps to "0s" so the
+ * output stays script-parseable instead of producing a localized phrase like
+ * "in the future" as `show_date_relative()` does.
+ */
+static void show_date_elapsed(timestamp_t time, struct strbuf *timebuf, int pad)
+{
+	struct timeval now;
+	timestamp_t diff, y, M, d, h, m, s;
+	const char *sp = " ";
+
+	get_time(&now);
+	if (now.tv_sec < time)
+		diff = 0;
+	else
+		diff = now.tv_sec - time;
+
+#define ELAPSED_SECS_PER_MIN  (60L)
+#define ELAPSED_SECS_PER_HOUR (60L * 60L)
+#define ELAPSED_SECS_PER_DAY  (24L * 60L * 60L)
+#define ELAPSED_SECS_PER_MON  (30L * 24L * 60L * 60L)        /* uptime(1) convention */
+#define ELAPSED_SECS_PER_YEAR (365L * 24L * 60L * 60L)       /* uptime(1) convention */
+
+	y  = diff / ELAPSED_SECS_PER_YEAR;
+	diff %= ELAPSED_SECS_PER_YEAR;
+	M  = diff / ELAPSED_SECS_PER_MON;
+	diff %= ELAPSED_SECS_PER_MON;
+	d  = diff / ELAPSED_SECS_PER_DAY;
+	diff %= ELAPSED_SECS_PER_DAY;
+	h  = diff / ELAPSED_SECS_PER_HOUR;
+	diff %= ELAPSED_SECS_PER_HOUR;
+	m  = diff / ELAPSED_SECS_PER_MIN;
+	s  = diff % ELAPSED_SECS_PER_MIN;
+
+#undef ELAPSED_SECS_PER_MIN
+#undef ELAPSED_SECS_PER_HOUR
+#undef ELAPSED_SECS_PER_DAY
+#undef ELAPSED_SECS_PER_MON
+#undef ELAPSED_SECS_PER_YEAR
+
+	if (!pad && !y && !M && !d && !h && !m) {
+		strbuf_addf(timebuf, "%"PRItime"s", s);
+		return;
+	}
+
+	if (pad) {
+		if (y) strbuf_addf(timebuf, "%02"PRItime"y%c", y, *sp);
+		if (y || M) strbuf_addf(timebuf, "%02"PRItime"M%c", M, *sp);
+		if (y || M || d) strbuf_addf(timebuf, "%02"PRItime"d%c", d, *sp);
+		if (y || M || d || h) strbuf_addf(timebuf, "%02"PRItime"h%c", h, *sp);
+		if (y || M || d || h || m) strbuf_addf(timebuf, "%02"PRItime"m%c", m, *sp);
+		strbuf_addf(timebuf, "%02"PRItime"s", s);
+	} else {
+		if (y) strbuf_addf(timebuf, "%"PRItime"y%c", y, *sp);
+		if (M) strbuf_addf(timebuf, "%"PRItime"M%c", M, *sp);
+		if (d) strbuf_addf(timebuf, "%"PRItime"d%c", d, *sp);
+		if (h) strbuf_addf(timebuf, "%"PRItime"h%c", h, *sp);
+		if (m) strbuf_addf(timebuf, "%"PRItime"m%c", m, *sp);
+		strbuf_addf(timebuf, "%"PRItime"s", s);
+	}
+}
+
 struct date_mode date_mode_from_type(enum date_mode_type type)
 {
 	struct date_mode mode = DATE_MODE_INIT;
@@ -323,6 +396,12 @@ const char *show_date(timestamp_t time, int tz, struct date_mode mode)
 		return timebuf.buf;
 	}
 
+	if (mode.type == DATE_ELAPSED || mode.type == DATE_ELAPSED_PAD) {
+		strbuf_reset(&timebuf);
+		show_date_elapsed(time, &timebuf, mode.type == DATE_ELAPSED_PAD);
+		return timebuf.buf;
+	}
+
 	if (mode.local)
 		tm = time_to_tm_local(time, &tmbuf);
 	else
@@ -1009,6 +1088,8 @@ static enum date_mode_type parse_date_type(const char *format, const char **end)
 		return DATE_RAW;
 	if (skip_prefix(format, "unix", end))
 		return DATE_UNIX;
+	if (skip_prefix(format, "elapsed", end))
+		return DATE_ELAPSED;
 	if (skip_prefix(format, "format", end))
 		return DATE_STRFTIME;
 	/*
@@ -1038,7 +1119,12 @@ void parse_date_format(const char *format, struct date_mode *mode)
 	mode->type = parse_date_type(format, &p);
 	mode->local = 0;
 
-	if (skip_prefix(p, "-local", &p))
+	if (mode->type == DATE_ELAPSED) {
+		if (skip_prefix(p, "-pad", &p))
+			mode->type = DATE_ELAPSED_PAD;
+		if (*p)
+			die("unknown date format %s", format);
+	} else if (skip_prefix(p, "-local", &p))
 		mode->local = 1;
 
 	if (mode->type == DATE_STRFTIME) {
diff --git a/date.h b/date.h
index 0747864fd7..eb7f1fb6ad 100644
--- a/date.h
+++ b/date.h
@@ -17,7 +17,9 @@ enum date_mode_type {
 	DATE_RFC2822,
 	DATE_STRFTIME,
 	DATE_RAW,
-	DATE_UNIX
+	DATE_UNIX,
+	DATE_ELAPSED,
+	DATE_ELAPSED_PAD
 };
 
 struct date_mode {
diff --git a/pretty.c b/pretty.c
index d8a9f370f6..9ba274c64a 100644
--- a/pretty.c
+++ b/pretty.c
@@ -848,6 +848,9 @@ static size_t format_person_part(struct strbuf *sb, char part,
 	case 'h':	/* date, human */
 		strbuf_addstr(sb, show_ident_date(&s, DATE_MODE(HUMAN)));
 		return placeholder_len;
+	case 'X':	/* date, elapsed */
+		strbuf_addstr(sb, show_ident_date(&s, DATE_MODE(ELAPSED)));
+		return placeholder_len;
 	case 's':
 		strbuf_addstr(sb, show_ident_date(&s, DATE_MODE(SHORT)));
 		return placeholder_len;
diff --git a/t/helper/test-date.c b/t/helper/test-date.c
index 87d2ad6fca..13101201d0 100644
--- a/t/helper/test-date.c
+++ b/t/helper/test-date.c
@@ -11,7 +11,9 @@ static const char *const usage_msg = "\n"
 "  test-tool date timestamp [date]...\n"
 "  test-tool date getnanos [start-nanos]\n"
 "  test-tool date is64bit\n"
-"  test-tool date time_t-is64bit\n";
+"  test-tool date time_t-is64bit\n"
+"  test-tool date elapsed [time_t]...\n"
+"  test-tool date elapsed-pad [time_t]...\n";
 
 static void show_relative_dates(const char **argv)
 {
@@ -33,6 +35,17 @@ static void show_human_dates(const char **argv)
 	}
 }
 
+static void show_elapsed_dates(const char **argv, int pad)
+{
+	for (; *argv; argv++) {
+		time_t t = atoi(*argv);
+		struct date_mode mode = DATE_MODE(ELAPSED);
+		if (pad)
+			mode = DATE_MODE(ELAPSED_PAD);
+		printf("%s -> %s\n", *argv, show_date(t, 0, mode));
+	}
+}
+
 static void show_dates(const char **argv, const char *format)
 {
 	struct date_mode mode = DATE_MODE_INIT;
@@ -115,6 +128,10 @@ int cmd__date(int argc UNUSED, const char **argv)
 		show_relative_dates(argv+1);
 	else if (!strcmp(*argv, "human"))
 		show_human_dates(argv+1);
+	else if (!strcmp(*argv, "elapsed"))
+		show_elapsed_dates(argv+1, 0);
+	else if (!strcmp(*argv, "elapsed-pad"))
+		show_elapsed_dates(argv+1, 1);
 	else if (skip_prefix(*argv, "show:", &x))
 		show_dates(argv+1, x);
 	else if (!strcmp(*argv, "parse"))
diff --git a/t/t0006-date.sh b/t/t0006-date.sh
index 6e63130ea2..7b2bc2178b 100755
--- a/t/t0006-date.sh
+++ b/t/t0006-date.sh
@@ -266,4 +266,47 @@ check_date_format_human 37500000 "Jun 22 2008" # 1 year, 2 months ago
 check_date_format_human 55188000 "Dec 1 2007" # 1 year, 9 months ago
 check_date_format_human 630000000 "Sep 13 1989" # 20 years ago
 
+check_date_format_elapsed() {
+	t=$(($GIT_TEST_DATE_NOW - $1))
+	pad=$2
+	expect=$3
+	if test -n "$pad"
+	then
+		desc="elapsed-pad $t"
+		cmd="elapsed-pad"
+	else
+		desc="elapsed $t"
+		cmd="elapsed"
+	fi
+	echo "$t -> $expect" >expect
+	test_expect_success "$desc" '
+		GIT_TEST_DATE_NOW=$GIT_TEST_DATE_NOW test-tool date '"$cmd"' $t >actual &&
+		test_cmp expect actual
+	'
+}
+
+# seconds-only
+check_date_format_elapsed       5  '' "5s"
+check_date_format_elapsed      39  '' "39s"
+# minutes + seconds
+check_date_format_elapsed      60  '' "1m 0s"
+check_date_format_elapsed     984  '' "16m 24s"
+# hours + minutes + seconds
+check_date_format_elapsed    29268 '' "8h 7m 48s"
+# months + days + hours + minutes + seconds (no year)
+check_date_format_elapsed  3024000 '' "1M 5d 0s"
+# years + everything
+check_date_format_elapsed 37890313 '' "1y 2M 13d 13h 5m 13s"
+check_date_format_elapsed 60527113 '' "1y 11M 5d 13h 5m 13s"
+
+# padded variant: same units but zero-padded to at least 2 digits
+check_date_format_elapsed       5 1 "05s"
+check_date_format_elapsed      39 1 "39s"
+check_date_format_elapsed      60 1 "01m 00s"
+check_date_format_elapsed     984 1 "16m 24s"
+check_date_format_elapsed    29268 1 "08h 07m 48s"
+check_date_format_elapsed  3024000 1 "01M 05d 00h 00m 00s"
+check_date_format_elapsed 37890313 1 "01y 02M 13d 13h 05m 13s"
+check_date_format_elapsed 60527113 1 "01y 11M 05d 13h 05m 13s"
+
 test_done
diff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh
index 4be5c51489..307be52b8f 100755
--- a/t/t4205-log-pretty-formats.sh
+++ b/t/t4205-log-pretty-formats.sh
@@ -557,6 +557,12 @@ test_expect_success '--date=human %ad%cd is the same as %ah%ch' '
 	test_cmp expected actual
 '
 
+test_expect_success '--date=elapsed %ad%cd is the same as %aX%cX' '
+	git log --format=%ad%n%cd --date=elapsed >expected &&
+	git log --format=%aX%n%cX >actual &&
+	test_cmp expected actual
+'
+
 # get new digests (with no abbreviations)
 test_expect_success 'set up log decoration tests' '
 	head1=$(git rev-parse --verify HEAD~0) &&

base-commit: 13c7afec212fc97ce257d15601659314c6673d6c
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-07-30  7:01 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-30  7:01 [PATCH] date: add --date=elapsed and %aX/%cX placeholders Israel Roldan via GitGitGadget

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox