Git development
 help / color / mirror / Atom feed
* [PATCH 0/2] commit: preserve commit hash on a no-op amend
@ 2026-06-13  9:16 Harald Nordgren via GitGitGadget
  2026-06-13  9:16 ` [PATCH 1/2] commit: extract commit_index_files_or_die() helper Harald Nordgren via GitGitGadget
                   ` (3 more replies)
  0 siblings, 4 replies; 7+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-06-13  9:16 UTC (permalink / raw)
  To: git; +Cc: Harald Nordgren

git commit --amend --no-edit rewrote the commit and moved the branch tip
even when nothing changed, because the committer date was reset to "now".
Reuse the existing committer date so a no-op amend keeps the commit hash and
leaves the branch untouched.

A real change (tree, message, author, committer, or signing) still rewrites
as before.

Harald Nordgren (2):
  commit: extract commit_index_files_or_die() helper
  commit: keep the commit on a no-op amend

 Documentation/git-commit.adoc         |   6 ++
 builtin/commit.c                      |  69 ++++++++++++++-
 t/t7501-commit-basic-functionality.sh | 119 ++++++++++++++++++++++++++
 3 files changed, 190 insertions(+), 4 deletions(-)


base-commit: ea97ad8d017de0c9037451a78008a0fd60abea0c
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2334%2FHaraldNordgren%2Famend-noop-keeps-commit-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2334/HaraldNordgren/amend-noop-keeps-commit-v1
Pull-Request: https://github.com/git/git/pull/2334
-- 
gitgitgadget

^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH 1/2] commit: extract commit_index_files_or_die() helper
  2026-06-13  9:16 [PATCH 0/2] commit: preserve commit hash on a no-op amend Harald Nordgren via GitGitGadget
@ 2026-06-13  9:16 ` Harald Nordgren via GitGitGadget
  2026-06-13  9:16 ` [PATCH 2/2] commit: keep the commit on a no-op amend Harald Nordgren via GitGitGadget
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 7+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-06-13  9:16 UTC (permalink / raw)
  To: git; +Cc: Harald Nordgren, Harald Nordgren

From: Harald Nordgren <haraldnordgren@gmail.com>

A later change adds a second caller that commits the index lock and dies
on failure, so wrap that into a helper to avoid duplicating its message.

No functional change intended.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 builtin/commit.c | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/builtin/commit.c b/builtin/commit.c
index 28f6174503..1a51450660 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -248,6 +248,14 @@ static int commit_index_files(void)
 	return err;
 }
 
+static void commit_index_files_or_die(void)
+{
+	if (commit_index_files())
+		die(_("repository has been updated, but unable to write\n"
+		      "new index file. Check that disk is not full and quota is\n"
+		      "not exceeded, and then \"git restore --staged :/\" to recover."));
+}
+
 /*
  * Take a union of paths in the index and the named tree (typically, "HEAD"),
  * and return the paths that match the given pattern in list.
@@ -1954,10 +1962,7 @@ int cmd_commit(int argc,
 	unlink(git_path_merge_mode(the_repository));
 	unlink(git_path_squash_msg(the_repository));
 
-	if (commit_index_files())
-		die(_("repository has been updated, but unable to write\n"
-		      "new index file. Check that disk is not full and quota is\n"
-		      "not exceeded, and then \"git restore --staged :/\" to recover."));
+	commit_index_files_or_die();
 
 	git_test_write_commit_graph_or_die(the_repository->objects->sources);
 
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH 2/2] commit: keep the commit on a no-op amend
  2026-06-13  9:16 [PATCH 0/2] commit: preserve commit hash on a no-op amend Harald Nordgren via GitGitGadget
  2026-06-13  9:16 ` [PATCH 1/2] commit: extract commit_index_files_or_die() helper Harald Nordgren via GitGitGadget
@ 2026-06-13  9:16 ` Harald Nordgren via GitGitGadget
  2026-06-13  9:59 ` [PATCH 0/2] commit: preserve commit hash " Johannes Sixt
  2026-06-13 15:44 ` Junio C Hamano
  3 siblings, 0 replies; 7+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-06-13  9:16 UTC (permalink / raw)
  To: git; +Cc: Harald Nordgren, Harald Nordgren

From: Harald Nordgren <haraldnordgren@gmail.com>

"git commit --amend --no-edit" reset the committer date to "now" and
rewrote the commit even when nothing else changed, moving the branch tip
to a new hash for an effective no-op.

Build the amended commit reusing the existing committer date: if that
reproduces the current commit, leave the branch alone, report "nothing
to amend", and skip the reflog entry and the post-commit and post-rewrite
hooks.

Signing always rewrites the commit, since its signature cannot reproduce
the original, so it skips this detection.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/git-commit.adoc         |   6 ++
 builtin/commit.c                      |  56 ++++++++++++
 t/t7501-commit-basic-functionality.sh | 119 ++++++++++++++++++++++++++
 3 files changed, 181 insertions(+)

diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
index 8329c1034b..c433c60929 100644
--- a/Documentation/git-commit.adoc
+++ b/Documentation/git-commit.adoc
@@ -282,6 +282,12 @@ variable (see linkgit:git-config[1]).
 	parents and author as the current one (the `--reset-author`
 	option can countermand this).
 +
+If the amended commit would be identical to the original (its tree,
+message, author, parents, and committer are all unchanged), the original
+committer date is kept so that the commit, and thus the branch tip, is
+left untouched. A commit that is being signed (`-S`, or `commit.gpgsign`)
+is always rewritten, since its signature cannot reproduce the original.
++
 --
 It is a rough equivalent for:
 
diff --git a/builtin/commit.c b/builtin/commit.c
index 1a51450660..e330a53d5c 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -17,6 +17,7 @@
 #include "dir.h"
 #include "editor.h"
 #include "environment.h"
+#include "ident.h"
 #include "diff.h"
 #include "commit.h"
 #include "add-interactive.h"
@@ -760,6 +761,49 @@ static void prepare_amend_commit(struct commit *commit, struct strbuf *sb,
 	repo_unuse_commit_buffer(the_repository, commit, buffer);
 }
 
+/*
+ * Rebuild the amended commit reusing the existing committer date and report
+ * whether it reproduces the current commit. Because the committer date is the
+ * only field that an amend would otherwise replace with "now", an exact match
+ * means everything else (tree, message, author, parents, committer identity)
+ * is unchanged too.
+ */
+static int amend_is_noop(struct commit *current_head,
+				 const struct strbuf *message,
+				 const struct commit_list *parents,
+				 const char *author,
+				 const struct commit_extra_header *extra,
+				 struct object_id *oid)
+{
+	const char *buffer, *committer_line;
+	size_t len;
+	struct ident_split ident;
+	struct strbuf date = STRBUF_INIT;
+	int unchanged = 0;
+
+	buffer = repo_get_commit_buffer(the_repository, current_head, NULL);
+	committer_line = find_commit_header(buffer, "committer", &len);
+	if (committer_line && !split_ident_line(&ident, committer_line, len) &&
+	    ident.date_begin) {
+		const char *committer;
+
+		strbuf_add(&date, ident.date_begin,
+			   ident.tz_end - ident.date_begin);
+		committer = fmt_ident(getenv("GIT_COMMITTER_NAME"),
+				      getenv("GIT_COMMITTER_EMAIL"),
+				      WANT_COMMITTER_IDENT, date.buf,
+				      IDENT_STRICT);
+		if (!commit_tree_extended(message->buf, message->len,
+					  &the_repository->index->cache_tree->oid,
+					  parents, oid, author, committer, NULL,
+					  extra))
+			unchanged = oideq(oid, &current_head->object.oid);
+	}
+	repo_unuse_commit_buffer(the_repository, current_head, buffer);
+	strbuf_release(&date);
+	return unchanged;
+}
+
 static void change_data_free(void *util, const char *str UNUSED)
 {
 	struct wt_status_change_data *d = util;
@@ -1943,6 +1987,18 @@ int cmd_commit(int argc,
 		append_merge_tag_headers(parents, &tail);
 	}
 
+	if (amend && current_head && !sign_commit &&
+	    amend_is_noop(current_head, &sb, parents, author_ident.buf,
+			  extra, &oid)) {
+		commit_index_files_or_die();
+		if (!quiet)
+			fprintf(stderr,
+				_("nothing to amend; %s left unchanged\n"),
+				repo_find_unique_abbrev(the_repository, &oid,
+							DEFAULT_ABBREV));
+		goto cleanup;
+	}
+
 	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/t7501-commit-basic-functionality.sh b/t/t7501-commit-basic-functionality.sh
index a37509f004..160edb9c0a 100755
--- a/t/t7501-commit-basic-functionality.sh
+++ b/t/t7501-commit-basic-functionality.sh
@@ -11,6 +11,7 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 
 . ./test-lib.sh
 . "$TEST_DIRECTORY/lib-diff.sh"
+. "$TEST_DIRECTORY/lib-gpg.sh"
 
 author='The Real Author <someguy@his.email.org>'
 
@@ -654,6 +655,124 @@ test_expect_success 'amend commit to fix author' '
 
 '
 
+test_expect_success 'amend --no-edit that changes nothing keeps the commit' '
+	git reset --hard &&
+	old=$(git rev-parse HEAD) &&
+	test_tick &&
+	git commit --amend --no-edit 2>err &&
+	test_cmp_rev $old HEAD &&
+	test_grep "nothing to amend" err
+'
+
+test_expect_success 'amend --no-edit keeps the commit out of the reflog' '
+	git reset --hard &&
+	git rev-parse HEAD@{0} >before &&
+	test_tick &&
+	git commit --amend --no-edit &&
+	git rev-parse HEAD@{0} >after &&
+	test_cmp before after
+'
+
+test_expect_success 'amend --signoff is idempotent once signed off' '
+	git reset --hard &&
+	test_tick &&
+	git commit --amend --no-edit --signoff &&
+	signed=$(git rev-parse HEAD) &&
+	git log -1 --format=%B | grep "^Signed-off-by:" &&
+	test_tick &&
+	git commit --amend --no-edit --signoff &&
+	test_cmp_rev $signed HEAD
+'
+
+test_expect_success 'amend that changes the tree still rewrites the commit' '
+	git reset --hard &&
+	old=$(git rev-parse HEAD) &&
+	echo changed >>file &&
+	git add file &&
+	test_tick &&
+	git commit --amend --no-edit &&
+	test_cmp_rev ! $old HEAD
+'
+
+test_expect_success 'amend that changes the committer still rewrites the commit' '
+	git reset --hard &&
+	old=$(git rev-parse HEAD) &&
+	test_tick &&
+	GIT_COMMITTER_EMAIL=other@example.com \
+		git commit --amend --no-edit &&
+	test_cmp_rev ! $old HEAD
+'
+
+test_expect_success 'amend that changes only the message still rewrites the commit' '
+	git reset --hard &&
+	old=$(git rev-parse HEAD) &&
+	test_tick &&
+	git commit --amend -m "new message" &&
+	test_cmp_rev ! $old HEAD &&
+	echo "new message" >expect &&
+	git log -1 --format=%s >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'amend --allow-empty of an empty commit that changes nothing keeps it' '
+	test_when_finished "git reset --hard parent && git tag -d parent" &&
+	git tag parent &&
+	git commit --allow-empty -m "empty" &&
+	old=$(git rev-parse HEAD) &&
+	test_tick &&
+	git commit --amend --no-edit --allow-empty 2>err &&
+	test_cmp_rev $old HEAD &&
+	test_grep "nothing to amend" err
+'
+
+test_expect_success GPG 'amend --no-edit of a signed commit is not a no-op' '
+	git reset --hard &&
+	test_tick &&
+	git commit --amend --no-edit -S &&
+	signed=$(git rev-parse HEAD) &&
+	git verify-commit HEAD &&
+	test_tick &&
+	git commit --amend --no-edit -S &&
+	test_cmp_rev ! $signed HEAD &&
+	git verify-commit HEAD
+'
+
+test_expect_success GPG 'amend --no-edit with commit.gpgsign is not a no-op' '
+	git reset --hard &&
+	test_tick &&
+	old=$(git rev-parse HEAD) &&
+	git -c commit.gpgsign=true commit --amend --no-edit &&
+	test_cmp_rev ! $old HEAD &&
+	git verify-commit HEAD
+'
+
+test_expect_success 'amend --reset-author rewrites the commit' '
+	git reset --hard &&
+	old=$(git rev-parse HEAD) &&
+	test_tick &&
+	git commit --amend --no-edit --reset-author &&
+	test_cmp_rev ! $old HEAD
+'
+
+test_expect_success 'amend --date rewrites the commit' '
+	git reset --hard &&
+	old=$(git rev-parse HEAD) &&
+	test_tick &&
+	git commit --amend --no-edit --date="@1234567890 +0000" &&
+	test_cmp_rev ! $old HEAD
+'
+
+test_expect_success 'amend that changes nothing skips the post-commit hook' '
+	test_when_finished "rm -f post-commit.ran" &&
+	test_hook post-commit <<-\EOF &&
+	>post-commit.ran
+	EOF
+	git reset --hard &&
+	test_tick &&
+	git commit --amend --no-edit &&
+	test_path_is_missing post-commit.ran
+'
+
 test_expect_success 'git commit <file> with dirty index' '
 	echo tacocat >elif &&
 	echo tehlulz >chz &&
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] 7+ messages in thread

* Re: [PATCH 0/2] commit: preserve commit hash on a no-op amend
  2026-06-13  9:16 [PATCH 0/2] commit: preserve commit hash on a no-op amend Harald Nordgren via GitGitGadget
  2026-06-13  9:16 ` [PATCH 1/2] commit: extract commit_index_files_or_die() helper Harald Nordgren via GitGitGadget
  2026-06-13  9:16 ` [PATCH 2/2] commit: keep the commit on a no-op amend Harald Nordgren via GitGitGadget
@ 2026-06-13  9:59 ` Johannes Sixt
  2026-06-13 14:07   ` Ben Knoble
  2026-06-13 15:44 ` Junio C Hamano
  3 siblings, 1 reply; 7+ messages in thread
From: Johannes Sixt @ 2026-06-13  9:59 UTC (permalink / raw)
  To: Harald Nordgren; +Cc: git, Harald Nordgren via GitGitGadget

Am 13.06.26 um 11:16 schrieb Harald Nordgren via GitGitGadget:
> git commit --amend --no-edit rewrote the commit and moved the branch tip
> even when nothing changed, because the committer date was reset to "now".
> Reuse the existing committer date so a no-op amend keeps the commit hash and
> leaves the branch untouched.

`git commit --amend --no-edit` is a way to set the committer timestamp
to the current time without changing other aspects of the commit. This
takes away this ability, doesn't it?

Is this keyed to --no-edit? Why is this mode special? Wouldn't it be an
identical case when the commit message is passed to the editor, but
comes back unchanged?

An invocation of `git commit` asks to "please make a new commit". But in
the suggested mode, no new commit is created. Shouldn't this then be
regarded as failure?

What happens with the current branch? Is it left unchanged (no ref
update occurs) or is it changed (a ref update occurs, but it happens to
be a no-op)? And does this then generate a reflog entry?

The updated documentation says about signed commits (note: I am totally
clueless about commit signing procedures):

> A commit that is being signed (`-S`, or `commit.gpgsign`)
> is always rewritten, since its signature cannot reproduce the original.

But if the commit doesn't change in any way, why should the signature be
invalidated, rewritten, or updated?

-- Hannes


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH 0/2] commit: preserve commit hash on a no-op amend
  2026-06-13  9:59 ` [PATCH 0/2] commit: preserve commit hash " Johannes Sixt
@ 2026-06-13 14:07   ` Ben Knoble
  0 siblings, 0 replies; 7+ messages in thread
From: Ben Knoble @ 2026-06-13 14:07 UTC (permalink / raw)
  To: Johannes Sixt; +Cc: Harald Nordgren, git, Harald Nordgren via GitGitGadget

> Le 13 juin 2026 à 05:59, Johannes Sixt <j6t@kdbg.org> a écrit :
> 
> Am 13.06.26 um 11:16 schrieb Harald Nordgren via GitGitGadget:
>> git commit --amend --no-edit rewrote the commit and moved the branch tip
>> even when nothing changed, because the committer date was reset to "now".
>> Reuse the existing committer date so a no-op amend keeps the commit hash and
>> leaves the branch untouched.
> 
> `git commit --amend --no-edit` is a way to set the committer timestamp
> to the current time without changing other aspects of the commit. This
> takes away this ability, doesn't it?

Indeed. This is a convenient formula to force CI re-runs in certain environments, and so on.

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH 0/2] commit: preserve commit hash on a no-op amend
  2026-06-13  9:16 [PATCH 0/2] commit: preserve commit hash on a no-op amend Harald Nordgren via GitGitGadget
                   ` (2 preceding siblings ...)
  2026-06-13  9:59 ` [PATCH 0/2] commit: preserve commit hash " Johannes Sixt
@ 2026-06-13 15:44 ` Junio C Hamano
  2026-06-13 16:15   ` Harald Nordgren
  3 siblings, 1 reply; 7+ messages in thread
From: Junio C Hamano @ 2026-06-13 15:44 UTC (permalink / raw)
  To: Harald Nordgren via GitGitGadget; +Cc: git, Harald Nordgren

"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:

> git commit --amend --no-edit rewrote the commit and moved the branch tip
> even when nothing changed, because the committer date was reset to "now".
> Reuse the existing committer date so a no-op amend keeps the commit hash and
> leaves the branch untouched.
>
> A real change (tree, message, author, committer, or signing) still rewrites
> as before.

I think this change brings nothing but regression.

Isn't it obvious that "commit --amend --no-edit" without updating
any tree contents would record exactly the same contents as before,
without a "real change" (as you said above), to any and all users,
expert and casual alike?

The end-user who runs such a command must have a reason to do so.
The *ONLY* valid reason anybody might want to such an amend is to
make sure the result is a new object, even if it records otherwise
the same content.

Why would they want to do so?  Perhaps it is so that future merges
of the topic branch that contains the commit will work more smoothly
into an integration branch that had earlier merged the topic branch,
and then that earlier merge was reverted.  This change will rob an
effective way to ensure a successful final merge in a workflow to
(1) merge a topic, (2) revert the topic, (3) update near the tip of
the topic while keeping earlier topic intact, and then (4) merge the
result again.

So, no.  I do not think this is a good change.

Let's digress and imagine an alternate universe where rebase/commit
--amend/history were "smart" from day one.  These command in such a
hypothetical world may not be capable of refreshing an existing
commit without making any "real change".

Making a change to these commands to _optionally_ allow them to
recreate an otherwise unchanged commit, so that it will get a new
object name, would be a welcome change that would allow users who
would use "commit --amend --no-edit" with today's system for such a
use case.  

And that would have been a logical evolution of the system in such a
hypothetical world.

But the thing is, we do not live in such a world.

If we still think that alternate hypothetical world is a better
place, we'd need to actively move things around, carefully designing
the transition to avoid harming existing users along the way, to get
there.  Changing the behaviour all of a sudden and breaking existing
workflows is not something we do around here.

One way to get to such a world might be:

 * Introduce an "committer timestamp is a trashable information"
   option, and teach commands like "commit --amend", "rebase", and
   "history" to cheat and yield the existing commit without
   refreshing when they are asked to recreate an existing commit
   while the option is in effect.  Give people the opposite
   "committer timestamp is not trashable information" option, so an
   earlier "is trashable" option on the command line can be
   countermanded by giving it later on the command line.

 * Have users discuss if "is trashable" is a better default, and
   gain consensus to make it the default in a future version of Git.
   Advertise the fact that we achieved consensus LOUDLY, while
   telling dissidents that "is not trashable" option will forever be
   available for them.

 * At a big version boundary, switch the default.

And I do not think I in principle would object to the first step of
such a three step process.

Thanks.

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH 0/2] commit: preserve commit hash on a no-op amend
  2026-06-13 15:44 ` Junio C Hamano
@ 2026-06-13 16:15   ` Harald Nordgren
  0 siblings, 0 replies; 7+ messages in thread
From: Harald Nordgren @ 2026-06-13 16:15 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Harald Nordgren via GitGitGadget, git

Interesting discussions! This sounds like showstopper, seems more
reasonable to leave this topic for now.

I just want to share that I've been running this for years to
re-trigger CI (because up until a few days ago I didn't realize that
the hash did indeed change even when nothing had changed), I had the
wrong mental model for commit hashes:

    git commit --amend --no-edit --date="now"


Harald

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-06-13 16:16 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-13  9:16 [PATCH 0/2] commit: preserve commit hash on a no-op amend Harald Nordgren via GitGitGadget
2026-06-13  9:16 ` [PATCH 1/2] commit: extract commit_index_files_or_die() helper Harald Nordgren via GitGitGadget
2026-06-13  9:16 ` [PATCH 2/2] commit: keep the commit on a no-op amend Harald Nordgren via GitGitGadget
2026-06-13  9:59 ` [PATCH 0/2] commit: preserve commit hash " Johannes Sixt
2026-06-13 14:07   ` Ben Knoble
2026-06-13 15:44 ` Junio C Hamano
2026-06-13 16:15   ` Harald Nordgren

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