Skip to content

Commit 997c65b

Browse files
committed
Fix the refdb backend rename callback
pygit2_refdb_backend_rename() had an inverted success check on build_signature(): it returned an error precisely when the signature was built successfully, and fell through with a NULL signature when it failed, so the callback could never work. It also decref'd the signature object twice (it is stolen into the argument tuple by Py_BuildValue()'s N format), passed a borrowed reference to the Py_True/Py_False singleton with N, and leaked the callback result when it was not a Reference. Two more bugs kept the function from working once the check was fixed. The Python Signature takes ownership of the git_signature it wraps, but _who belongs to libgit2 and is freed again on return; pass a git_signature_dup() copy instead. And git_reference_dup() cannot be used on the returned reference, because its db field is only set by git_refdb_rename() after the callback returns; transfer the pointer directly, as pygit2_refdb_backend_lookup() already does. Also accept None for the message argument of RefdbBackend.rename(): libgit2 passes NULL when no log message is given (pygit2's Reference.rename() always does), Py_BuildValue() maps that to None, and the Python-side method must round-trip it back to NULL. This matches the z format already used by write() and delete(). Add test_rename_callback, the first test to drive a custom refdb backend through libgit2: assigning repo.backend only sets a Python attribute, so the existing tests never exercised the C callbacks. Fixes #1474 Assisted-by: Kimi Code
1 parent eb512d2 commit 997c65b

3 files changed

Lines changed: 59 additions & 14 deletions

File tree

pygit2/_pygit2.pyi

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,12 @@ class RefdbBackend:
599599
def has_log(self, ref_name: str, /) -> bool: ...
600600
def lookup(self, refname: str, /) -> Reference: ...
601601
def rename(
602-
self, old_name: str, new_name: str, force: bool, who: Signature, message: str
602+
self,
603+
old_name: str,
604+
new_name: str,
605+
force: bool,
606+
who: Signature,
607+
message: str | None,
603608
) -> Reference: ...
604609
def write(
605610
self,

src/refdb_backend.c

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -249,31 +249,47 @@ pygit2_refdb_backend_rename(git_reference **out, git_refdb_backend *_be,
249249
const char *old_name, const char *new_name, int force,
250250
const git_signature *_who, const char *message)
251251
{
252-
int err;
253-
PyObject *args, *who;
254252
struct pygit2_refdb_backend *be = (struct pygit2_refdb_backend *)_be;
255253

256-
if ((who = build_signature(NULL, _who, "utf-8")) != NULL)
254+
// The Python object takes ownership of the signature, so pass it a copy;
255+
// _who belongs to the caller (libgit2).
256+
git_signature *signature;
257+
int err = git_signature_dup(&signature, _who);
258+
if (err != 0) {
259+
return err;
260+
}
261+
262+
PyObject *who = build_signature(NULL, signature, "utf-8");
263+
if (who == NULL) {
257264
return GIT_EUSER;
258-
if ((args = Py_BuildValue("(ssNNs)", old_name, new_name,
259-
force ? Py_True : Py_False, who, message)) == NULL) {
260-
Py_DECREF(who);
265+
}
266+
267+
// Py_BuildValue takes ownership of who (N format), even on failure, so
268+
// it must not be decref'd past this point.
269+
PyObject *args = Py_BuildValue("(ssNNs)", old_name, new_name,
270+
PyBool_FromLong(force), who, message);
271+
if (args == NULL) {
261272
return GIT_EUSER;
262273
}
274+
263275
Reference *ref = (Reference *)PyObject_CallObject(be->rename, args);
264-
Py_DECREF(who);
265276
Py_DECREF(args);
266277

267-
if ((err = git_error_for_exc()) != 0)
278+
err = git_error_for_exc();
279+
if (err != 0) {
268280
return err;
281+
}
269282

270283
if (!PyObject_IsInstance((PyObject *)ref, (PyObject *)&ReferenceType)) {
271284
PyErr_SetString(PyExc_TypeError, "Expected object of type pygit2.Reference");
285+
Py_DECREF(ref);
272286
return GIT_EUSER;
273287
}
274288

275-
git_reference_dup(out, ref->reference);
276-
Py_DECREF(ref);
289+
// Ownership of the underlying git_reference is transferred to libgit2,
290+
// which sets its db field itself once the callback returns, so the Python
291+
// object must not be decref'd; same as in pygit2_refdb_backend_lookup.
292+
*out = ref->reference;
277293
return 0;
278294
}
279295

@@ -625,7 +641,7 @@ RefdbBackend_write(RefdbBackend *self, PyObject *args)
625641
}
626642

627643
PyDoc_STRVAR(RefdbBackend_rename__doc__,
628-
"rename(old_name: str, new_name: str, force: bool, who: Signature, message: str) -> Reference\n"
644+
"rename(old_name: str, new_name: str, force: bool, who: Signature, message: str | None) -> Reference\n"
629645
"\n"
630646
"Renames a reference.");
631647

@@ -643,7 +659,7 @@ RefdbBackend_rename(RefdbBackend *self, PyObject *args)
643659
return Py_NotImplemented;
644660
}
645661

646-
if (!PyArg_ParseTuple(args, "sspO!s", &old_name, &new_name,
662+
if (!PyArg_ParseTuple(args, "sspO!z", &old_name, &new_name,
647663
&force, &SignatureType, &who, &message))
648664
return NULL;
649665

test/test_refdb_backend.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,15 @@
4141
# design.
4242
class ProxyRefdbBackend(pygit2.RefdbBackend):
4343
def __init__(self, source: pygit2.RefdbBackend) -> None:
44+
super().__init__()
4445
self.source = source
4546

47+
def __iter__(self) -> 'ProxyRefdbBackend':
48+
return self
49+
50+
def __next__(self) -> Reference:
51+
raise StopIteration
52+
4653
def exists(self, ref: str) -> bool:
4754
return self.source.exists(ref)
4855

@@ -61,7 +68,12 @@ def write(
6168
return self.source.write(ref, force, who, message, old, old_target)
6269

6370
def rename(
64-
self, old_name: str, new_name: str, force: bool, who: Signature, message: str
71+
self,
72+
old_name: str,
73+
new_name: str,
74+
force: bool,
75+
who: Signature,
76+
message: str | None,
6577
) -> Reference:
6678
return self.source.rename(old_name, new_name, force, who, message)
6779

@@ -112,6 +124,18 @@ def test_rename(repo: Repository) -> None:
112124
assert repo.backend.lookup('refs/heads/intl').target == target.id
113125

114126

127+
def test_rename_callback(repo: Repository) -> None:
128+
# Exercise the custom backend's rename callback through libgit2's
129+
# git_reference_rename; calling repo.backend.rename() directly bypasses it.
130+
refdb = pygit2.Refdb.new(repo)
131+
refdb.set_backend(repo.backend)
132+
repo.set_refdb(refdb)
133+
ref = repo.references['refs/heads/i18n']
134+
target = ref.target
135+
ref.rename('refs/heads/intl')
136+
assert repo.references['refs/heads/intl'].target == target
137+
138+
115139
def test_delete(repo: Repository) -> None:
116140
old = repo.backend.lookup('refs/heads/i18n')
117141
repo.backend.delete('refs/heads/i18n', old.target, None)

0 commit comments

Comments
 (0)