Git development
 help / color / mirror / Atom feed
* [PATCH] notes: do not drop notes written by a concurrent process
@ 2026-10-29  3:55 Farid Zakaria
  2026-10-29 11:05 ` Kristoffer Haugsbakk
  0 siblings, 1 reply; 6+ messages in thread
From: Farid Zakaria @ 2026-10-29  3:55 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Patrick Steinhardt, Farid Zakaria

"git notes add" and the other commands that modify notes read the
notes ref, update the notes tree in memory, and then commit it on top
of whatever the ref points to by then. If another process updated the
ref in the meantime, its notes are silently lost.

The fix is to remember the commit the notes were read from, and use
it as both the parent of the new notes commit and the expected old
value of the ref update. A concurrent update now makes the command
fail with "cannot lock ref" instead.

Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
---
I found this with Rewind, a deterministic VM I am building that replays
a program under many thread schedules until one fails. Two "git notes
add" commands run at once on different commits lost one of the two
notes in 15 of 32 schedules, while both exited 0. It is not specific to
Rewind: on a plain Linux machine 25 of 30 runs lost a note. How Rewind
narrowed it down to commit_notes() is written up at:

  https://fzakaria.com/2026/10/28/reading-the-code-is-the-hard-part

With this patch no notes are lost; when two commands race, one of them
fails with "cannot lock ref" and can be retried.
---
 notes-utils.c    | 15 +++++++++++++--
 notes.c          |  8 ++++++--
 notes.h          |  3 ++-
 t/t3301-notes.sh | 18 ++++++++++++++++++
 4 files changed, 39 insertions(+), 5 deletions(-)

diff --git a/notes-utils.c b/notes-utils.c
index 5c1c75d5b8..0ce430304f 100644
--- a/notes-utils.c
+++ b/notes-utils.c
@@ -46,6 +46,7 @@ void create_notes_commit(struct repository *r,
 void commit_notes(struct repository *r, struct notes_tree *t, const char *msg)
 {
 	struct strbuf buf = STRBUF_INIT;
+	struct commit_list *parents = NULL;
 	struct object_id commit_oid;
 
 	if (!t)
@@ -59,12 +60,22 @@ void commit_notes(struct repository *r, struct notes_tree *t, const char *msg)
 	strbuf_addstr(&buf, msg);
 	strbuf_complete_line(&buf);
 
-	create_notes_commit(r, t, NULL, buf.buf, buf.len, &commit_oid);
+	/* Build on the commit the notes were loaded from */
+	if (!is_null_oid(&t->base_oid)) {
+		struct commit *parent = lookup_commit(r, &t->base_oid);
+		if (repo_parse_commit(r, parent))
+			die("Failed to find/parse commit %s", t->ref);
+		commit_list_insert(parent, &parents);
+	}
+
+	create_notes_commit(r, t, parents, buf.buf, buf.len, &commit_oid);
 	strbuf_insertstr(&buf, 0, "notes: ");
 	refs_update_ref(get_main_ref_store(the_repository), buf.buf,
-			t->update_ref, &commit_oid, NULL, 0,
+			t->update_ref, &commit_oid, &t->base_oid, 0,
 			UPDATE_REFS_DIE_ON_ERR);
+	oidcpy(&t->base_oid, &commit_oid);
 
+	commit_list_free(parents);
 	strbuf_release(&buf);
 }
 
diff --git a/notes.c b/notes.c
index ec9c2cb150..d71b528f37 100644
--- a/notes.c
+++ b/notes.c
@@ -1039,12 +1039,16 @@ void init_notes(struct notes_tree *t, const char *notes_ref,
 	t->combine_notes = combine_notes;
 	t->initialized = 1;
 	t->dirty = 0;
+	oidclr(&t->base_oid, the_repository->hash_algo);
 
 	if (flags & NOTES_INIT_EMPTY ||
 	    repo_get_oid_treeish(the_repository, notes_ref, &object_oid))
 		goto out;
-	if (flags & NOTES_INIT_WRITABLE && refs_read_ref(get_main_ref_store(the_repository), notes_ref, &object_oid))
-		die("Cannot use notes ref %s", notes_ref);
+	if (flags & NOTES_INIT_WRITABLE) {
+		if (refs_read_ref(get_main_ref_store(the_repository), notes_ref, &object_oid))
+			die("Cannot use notes ref %s", notes_ref);
+		oidcpy(&t->base_oid, &object_oid);
+	}
 	if (get_tree_entry(the_repository, &object_oid, "", &oid, &mode))
 		die("Failed to read notes tree referenced by %s (%s)",
 		    notes_ref, oid_to_hex(&object_oid));
diff --git a/notes.h b/notes.h
index 6dc6d7b265..30f4ece5e9 100644
--- a/notes.h
+++ b/notes.h
@@ -1,9 +1,9 @@
 #ifndef NOTES_H
 #define NOTES_H
 
+#include "hash.h"
 #include "string-list.h"
 
-struct object_id;
 struct repository;
 struct strbuf;
 
@@ -54,6 +54,7 @@ extern struct notes_tree {
 	struct non_note *first_non_note, *prev_non_note;
 	char *ref;
 	char *update_ref;
+	struct object_id base_oid;
 	combine_notes_fn combine_notes;
 	int initialized;
 	int dirty;
diff --git a/t/t3301-notes.sh b/t/t3301-notes.sh
index 18a6000171..561f9ccc18 100755
--- a/t/t3301-notes.sh
+++ b/t/t3301-notes.sh
@@ -1629,4 +1629,22 @@ test_expect_success 'git notes append aborts when editor fails with -e' '
 	test_must_fail git notes show
 '
 
+test_expect_success 'git notes add does not drop notes added while it runs' '
+	test_commit 23rd &&
+	test_commit 24th &&
+
+	# The editor adds a note to another commit while the outer
+	# "git notes add" is waiting for it, as a concurrent process would.
+	write_script racing_editor <<-\EOF &&
+	git notes add -m "added meanwhile" 23rd &&
+	echo "edited" >"$1"
+	EOF
+	test_must_fail env GIT_EDITOR=./racing_editor git notes add 24th &&
+
+	echo "added meanwhile" >expect &&
+	git notes show 23rd >actual &&
+	test_cmp expect actual &&
+	test_must_fail git notes show 24th
+'
+
 test_done

---
base-commit: 8d1b215c01512b9658d4525c54ff9b9245f9a7e8
change-id: 20261028-fz-notes-concurrent-write-4ee590954ece


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

end of thread, other threads:[~2026-10-29 22:16 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-29  3:55 [PATCH] notes: do not drop notes written by a concurrent process Farid Zakaria
2026-10-29 11:05 ` Kristoffer Haugsbakk
2026-10-29 18:25   ` Junio C Hamano
2026-10-29 20:56     ` Farid Zakaria
2026-10-29 21:42       ` Junio C Hamano
2026-10-29 22:16         ` Farid Zakaria

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