summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Turner <novalis@novalis.org>2021-04-14 21:52:36 -0400
committerDavid Turner <novalis@novalis.org>2021-04-20 09:24:25 -0400
commit95b7a6398fe499fb37e49a528292c9ab2e1e8fc9 (patch)
treee821a5c9ea3b4d5d1688571572d84f97e211c321
parentac77d306f539938aa599ba81faee52854c262328 (diff)
downloadlibgit2-95b7a6398fe499fb37e49a528292c9ab2e1e8fc9.tar.gz
git_reference_create_matching: Treat all-zero OID as "must be absent"
This is pretty useful in avoiding races: I want to create a ref only if it doesn't already exist. I can't check first because of TOCTOU -- by the time I finish the check, someone else might have already created the ref. And I can't take a lock because then I can't do the create, since the create expects to take the lock. The semantics are inspired by git update-ref, which allows an all-zero old value to mean that the ref must not exist.
-rw-r--r--include/git2/refs.h3
-rw-r--r--src/refdb_fs.c5
-rw-r--r--tests/refs/races.c13
3 files changed, 20 insertions, 1 deletions
diff --git a/include/git2/refs.h b/include/git2/refs.h
index 8bc99e15e..7ebb209b2 100644
--- a/include/git2/refs.h
+++ b/include/git2/refs.h
@@ -97,6 +97,9 @@ GIT_EXTERN(int) git_reference_dwim(git_reference **out, git_repository *repo, co
* of updating does not match the one passed through `current_value`
* (i.e. if the ref has changed since the user read it).
*
+ * If `current_value` is all zeros, this function will return GIT_EMODIFIED
+ * if the ref already exists.
+ *
* @param out Pointer to the newly created reference
* @param repo Repository where that reference will live
* @param name The name of the reference
diff --git a/src/refdb_fs.c b/src/refdb_fs.c
index a5a6b3c0e..971dc021d 100644
--- a/src/refdb_fs.c
+++ b/src/refdb_fs.c
@@ -1145,8 +1145,11 @@ static int cmp_old_ref(int *cmp, git_refdb_backend *backend, const char *name,
if (!old_id && !old_target)
return 0;
- if ((error = refdb_fs_backend__lookup(&old_ref, backend, name)) < 0)
+ if ((error = refdb_fs_backend__lookup(&old_ref, backend, name)) < 0) {
+ if (error == GIT_ENOTFOUND && old_id && git_oid_is_zero(old_id))
+ return 0;
goto out;
+ }
/* If the types don't match, there's no way the values do */
if (old_id && old_ref->type != GIT_REFERENCE_DIRECT) {
diff --git a/tests/refs/races.c b/tests/refs/races.c
index 04a1bc17b..988072794 100644
--- a/tests/refs/races.c
+++ b/tests/refs/races.c
@@ -22,6 +22,19 @@ void test_refs_races__cleanup(void)
cl_git_sandbox_cleanup();
}
+void test_refs_races__create_matching_zero_old(void)
+{
+ git_reference *ref;
+ git_oid id, zero_id;
+
+ git_oid_fromstr(&id, commit_id);
+ git_oid_fromstr(&zero_id, "0000000000000000000000000000000000000000");
+
+ cl_git_pass(git_reference_create_matching(&ref, g_repo, other_refname, &id, 1, &zero_id, NULL));
+
+ git_reference_free(ref);
+}
+
void test_refs_races__create_matching(void)
{
git_reference *ref, *ref2, *ref3;