Skip to content

Commit 3a7a211

Browse files
committed
feat: Wrap libgit2 git_message_trailers
git-log has a formatting function to extract trailers from the commit message field. The libgit2 function `git_message_trailers` implements this logic and seems to catch a lot of corner cases. Rather than ask users to extract trailers from commit messages by fiddling with the `message` attribute of the `Commit` object, wrap the libgit2 implementation.
1 parent 464836c commit 3a7a211

File tree

5 files changed

+105
-0
lines changed

5 files changed

+105
-0
lines changed

docs/recipes/git-log.rst

+15
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ Traverse commit history
3434
>>> for commit in repo.walk(last.id, pygit2.GIT_SORT_TIME):
3535
>>> print(commit.message) # or some other operation
3636
37+
======================================================================
38+
Show trailers from the last commit
39+
======================================================================
40+
41+
.. code-block:: bash
42+
43+
$ git log --format='%(trailers:key=Bug)'
44+
45+
.. code-block:: python
46+
47+
>>> last = repo[repo.head.target]
48+
>>> for commit in repo.walk(last.id, pygit2.GIT_SORT_TIME):
49+
>>> print(commit.message_trailers.get('Bug'))
50+
51+
3752
----------------------------------------------------------------------
3853
References
3954
----------------------------------------------------------------------

src/commit.c

+41
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,46 @@ Commit_gpg_signature__get__(Commit *self)
9898
}
9999

100100

101+
PyDoc_STRVAR(Commit_message_trailers__doc__,
102+
"Returns commit message trailers (e.g., Bug: 1234) as a dictionary."
103+
);
104+
105+
PyObject *
106+
Commit_message_trailers__get__(Commit *self)
107+
{
108+
git_message_trailer_array gmt_arr;
109+
int i, trailer_count, err;
110+
PyObject *dict;
111+
PyObject *py_val;
112+
const char *message = git_commit_message(self->commit);
113+
const char *encoding = git_commit_message_encoding(self->commit);
114+
115+
err = git_message_trailers(&gmt_arr, message);
116+
if (err < 0)
117+
return Error_set(err);
118+
119+
dict = PyDict_New();
120+
if (dict == NULL)
121+
return NULL;
122+
123+
trailer_count = gmt_arr.count;
124+
for (i=0; i < trailer_count; i++) {
125+
py_val = to_unicode(gmt_arr.trailers[i].value, encoding, NULL);
126+
err = PyDict_SetItemString(dict, gmt_arr.trailers[i].key, py_val);
127+
if (err < 0)
128+
goto error;
129+
130+
}
131+
132+
git_message_trailer_array_free(&gmt_arr);
133+
return dict;
134+
135+
error:
136+
git_message_trailer_array_free(&gmt_arr);
137+
Py_CLEAR(dict);
138+
return NULL;
139+
}
140+
101141
PyDoc_STRVAR(Commit_raw_message__doc__, "Message (bytes).");
102142

103143
PyObject *
@@ -270,6 +310,7 @@ PyGetSetDef Commit_getseters[] = {
270310
GETTER(Commit, tree_id),
271311
GETTER(Commit, parents),
272312
GETTER(Commit, parent_ids),
313+
GETTER(Commit, message_trailers),
273314
{NULL}
274315
};
275316

src/commit.h

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
PyObject* Commit_get_message_encoding(Commit *commit);
3636
PyObject* Commit_get_message(Commit *commit);
3737
PyObject* Commit_get_raw_message(Commit *commit);
38+
PyObject* Commit_get_message_trailers(Commit *commit);
3839
PyObject* Commit_get_commit_time(Commit *commit);
3940
PyObject* Commit_get_commit_time_offset(Commit *commit);
4041
PyObject* Commit_get_committer(Commit *self);

test/data/trailerrepo.tar

50 KB
Binary file not shown.

test/test_commit_trailer.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright 2010-2021 The pygit2 contributors
2+
#
3+
# This file is free software; you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License, version 2,
5+
# as published by the Free Software Foundation.
6+
#
7+
# In addition to the permissions in the GNU General Public License,
8+
# the authors give you unlimited permission to link the compiled
9+
# version of this file into combinations with other programs,
10+
# and to distribute those combinations without any restriction
11+
# coming from the use of this file. (The General Public License
12+
# restrictions do apply in other respects; for example, they cover
13+
# modification of the file, and distribution when not linked into
14+
# a combined executable.)
15+
#
16+
# This file is distributed in the hope that it will be useful, but
17+
# WITHOUT ANY WARRANTY; without even the implied warranty of
18+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19+
# General Public License for more details.
20+
#
21+
# You should have received a copy of the GNU General Public License
22+
# along with this program; see the file COPYING. If not, write to
23+
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
24+
# Boston, MA 02110-1301, USA.
25+
26+
import pygit2
27+
import pytest
28+
29+
from . import utils
30+
31+
32+
@pytest.fixture
33+
def repo(tmp_path):
34+
with utils.TemporaryRepository('trailerrepo.tar', tmp_path) as path:
35+
yield pygit2.Repository(path)
36+
37+
38+
def test_get_trailers_array(repo):
39+
commit_hash = '010231b2fdaee6b21da4f06058cf6c6a3392dd12'
40+
expected_trailers = {
41+
'Bug': '1234',
42+
'Signed-off-by': 'Tyler Cipriani <[email protected]>',
43+
}
44+
commit = repo.get(commit_hash)
45+
trailers = commit.message_trailers
46+
47+
assert trailers['Bug'] == expected_trailers['Bug']
48+
assert trailers['Signed-off-by'] == expected_trailers['Signed-off-by']

0 commit comments

Comments
 (0)