Skip to content

Commit d2f7284

Browse files
committed
Add trailers_list and trailers_list methods to fix the commit trailers functionality. Update trailers tests.
1 parent 61ed7ec commit d2f7284

File tree

2 files changed

+126
-58
lines changed

2 files changed

+126
-58
lines changed

‎git/objects/commit.py

+79-22
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import os
2727
from io import BytesIO
2828
import logging
29+
from collections import defaultdict
2930

3031

3132
# typing ------------------------------------------------------------------
@@ -335,8 +336,70 @@ def stats(self) -> Stats:
335336
return Stats._list_from_string(self.repo, text)
336337

337338
@property
338-
def trailers(self) -> Dict:
339-
"""Get the trailers of the message as dictionary
339+
def trailers(self) -> Dict[str, str]:
340+
"""Get the trailers of the message as a dictionary
341+
342+
Git messages can contain trailer information that are similar to RFC 822
343+
e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers).
344+
345+
WARNING: This function only returns the latest instance of each trailer key
346+
and will be deprecated soon. Please see either ``Commit.trailers_list`` or ``Commit.trailers_dict``.
347+
348+
:return:
349+
Dictionary containing whitespace stripped trailer information.
350+
Only the latest instance of each trailer key.
351+
"""
352+
return {
353+
k: v[0] for k, v in self.trailers_dict.items()
354+
}
355+
356+
@property
357+
def trailers_list(self) -> List[str]:
358+
"""Get the trailers of the message as a list
359+
360+
Git messages can contain trailer information that are similar to RFC 822
361+
e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers).
362+
363+
This functions calls ``git interpret-trailers --parse`` onto the message
364+
to extract the trailer information, returns the raw trailer data as a list.
365+
366+
Valid message with trailer::
367+
368+
Subject line
369+
370+
some body information
371+
372+
another information
373+
374+
key1: value1.1
375+
key1: value1.2
376+
key2 : value 2 with inner spaces
377+
378+
379+
Returned list will look like this::
380+
381+
[
382+
"key1: value1.1",
383+
"key1: value1.2",
384+
"key2 : value 2 with inner spaces",
385+
]
386+
387+
388+
:return:
389+
List containing whitespace stripped trailer information.
390+
"""
391+
cmd = ["git", "interpret-trailers", "--parse"]
392+
proc: Git.AutoInterrupt = self.repo.git.execute(cmd, as_process=True, istream=PIPE) # type: ignore
393+
trailer: str = proc.communicate(str(self.message).encode())[0].decode()
394+
trailer = trailer.strip()
395+
if trailer:
396+
return [t.strip() for t in trailer.split("\n")]
397+
398+
return []
399+
400+
@property
401+
def trailers_dict(self) -> Dict[str, List[str]]:
402+
"""Get the trailers of the message as a dictionary
340403
341404
Git messages can contain trailer information that are similar to RFC 822
342405
e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers).
@@ -345,42 +408,36 @@ def trailers(self) -> Dict:
345408
to extract the trailer information. The key value pairs are stripped of
346409
leading and trailing whitespaces before they get saved into a dictionary.
347410
348-
Valid message with trailer:
349-
350-
.. code-block::
411+
Valid message with trailer::
351412
352413
Subject line
353414
354415
some body information
355416
356417
another information
357418
358-
key1: value1
419+
key1: value1.1
420+
key1: value1.2
359421
key2 : value 2 with inner spaces
360422
361-
dictionary will look like this:
362423
363-
.. code-block::
424+
Returned dictionary will look like this::
364425
365426
{
366-
"key1": "value1",
367-
"key2": "value 2 with inner spaces"
427+
"key1": ["value1.1", "value1.2"],
428+
"key2": ["value 2 with inner spaces"],
368429
}
369430
370-
:return: Dictionary containing whitespace stripped trailer information
371431
432+
:return:
433+
Dictionary containing whitespace stripped trailer information.
434+
Mapping trailer keys to a list of their corresponding values.
372435
"""
373-
d = {}
374-
cmd = ["git", "interpret-trailers", "--parse"]
375-
proc: Git.AutoInterrupt = self.repo.git.execute(cmd, as_process=True, istream=PIPE) # type: ignore
376-
trailer: str = proc.communicate(str(self.message).encode())[0].decode()
377-
if trailer.endswith("\n"):
378-
trailer = trailer[0:-1]
379-
if trailer != "":
380-
for line in trailer.split("\n"):
381-
key, value = line.split(":", 1)
382-
d[key.strip()] = value.strip()
383-
return d
436+
d = defaultdict(list)
437+
for trailer in self.trailers_list:
438+
key, value = trailer.split(":", 1)
439+
d[key.strip()].append(value.strip())
440+
return dict(d)
384441

385442
@classmethod
386443
def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, IO]) -> Iterator["Commit"]:

‎test/test_commit.py

+47-36
Original file line numberDiff line numberDiff line change
@@ -494,52 +494,63 @@ def test_datetimes(self):
494494

495495
def test_trailers(self):
496496
KEY_1 = "Hello"
497-
VALUE_1 = "World"
497+
VALUE_1_1 = "World"
498+
VALUE_1_2 = "Another-World"
498499
KEY_2 = "Key"
499500
VALUE_2 = "Value with inner spaces"
500501

501-
# Check if KEY 1 & 2 with Value 1 & 2 is extracted from multiple msg variations
502-
msgs = []
503-
msgs.append(f"Subject\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n")
504-
msgs.append(f"Subject\n \nSome body of a function\n \n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n")
505-
msgs.append(
506-
f"Subject\n \nSome body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n"
507-
)
508-
msgs.append(
509-
f"Subject\n \nSome multiline\n body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2} : {VALUE_2}\n"
510-
)
511-
502+
# Check the following trailer example is extracted from multiple msg variations
503+
TRAILER = f"{KEY_1}: {VALUE_1_1}\n{KEY_2}: {VALUE_2}\n{KEY_1}: {VALUE_1_2}"
504+
msgs = [
505+
f"Subject\n\n{TRAILER}\n",
506+
f"Subject\n \nSome body of a function\n \n{TRAILER}\n",
507+
f"Subject\n \nSome body of a function\n\nnon-key: non-value\n\n{TRAILER}\n",
508+
(
509+
# check when trailer has inconsistent whitespace
510+
f"Subject\n \nSome multiline\n body of a function\n\nnon-key: non-value\n\n"
511+
f"{KEY_1}:{VALUE_1_1}\n{KEY_2} : {VALUE_2}\n{KEY_1}: {VALUE_1_2}\n"
512+
),
513+
]
512514
for msg in msgs:
513-
commit = self.rorepo.commit("master")
514-
commit = copy.copy(commit)
515+
commit = copy.copy(self.rorepo.commit("master"))
515516
commit.message = msg
516-
assert KEY_1 in commit.trailers.keys()
517-
assert KEY_2 in commit.trailers.keys()
518-
assert commit.trailers[KEY_1] == VALUE_1
519-
assert commit.trailers[KEY_2] == VALUE_2
520-
521-
# Check that trailer stays empty for multiple msg combinations
522-
msgs = []
523-
msgs.append(f"Subject\n")
524-
msgs.append(f"Subject\n\nBody with some\nText\n")
525-
msgs.append(f"Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n")
526-
msgs.append(f"Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n")
527-
msgs.append(f"Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n")
528-
msgs.append(f"Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n")
517+
assert commit.trailers_list == [
518+
f"{KEY_1}: {VALUE_1_1}",
519+
f"{KEY_2}: {VALUE_2}",
520+
f"{KEY_1}: {VALUE_1_2}",
521+
]
522+
assert commit.trailers_dict == {
523+
KEY_1: [VALUE_1_1, VALUE_1_2],
524+
KEY_2: [VALUE_2],
525+
}
526+
assert commit.trailers == {
527+
KEY_1: VALUE_1_1,
528+
KEY_2: VALUE_2,
529+
}
530+
531+
# check that trailer stays empty for multiple msg combinations
532+
msgs = [
533+
f"Subject\n",
534+
f"Subject\n\nBody with some\nText\n",
535+
f"Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n",
536+
f"Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n",
537+
f"Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n",
538+
f"Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n",
539+
]
529540

530541
for msg in msgs:
531-
commit = self.rorepo.commit("master")
532-
commit = copy.copy(commit)
542+
commit = copy.copy(self.rorepo.commit("master"))
533543
commit.message = msg
534-
assert len(commit.trailers.keys()) == 0
544+
assert commit.trailers_list == []
545+
assert commit.trailers_dict == {}
546+
assert commit.trailers == {}
535547

536548
# check that only the last key value paragraph is evaluated
537-
commit = self.rorepo.commit("master")
538-
commit = copy.copy(commit)
539-
commit.message = f"Subject\n\nMultiline\nBody\n\n{KEY_1}: {VALUE_1}\n\n{KEY_2}: {VALUE_2}\n"
540-
assert KEY_1 not in commit.trailers.keys()
541-
assert KEY_2 in commit.trailers.keys()
542-
assert commit.trailers[KEY_2] == VALUE_2
549+
commit = copy.copy(self.rorepo.commit("master"))
550+
commit.message = f"Subject\n\nMultiline\nBody\n\n{KEY_1}: {VALUE_1_1}\n\n{KEY_2}: {VALUE_2}\n"
551+
assert commit.trailers_list == [f"{KEY_2}: {VALUE_2}"]
552+
assert commit.trailers_dict == {KEY_2: [VALUE_2]}
553+
assert commit.trailers == {KEY_2: VALUE_2}
543554

544555
def test_commit_co_authors(self):
545556
commit = copy.copy(self.rorepo.commit("4251bd5"))

0 commit comments

Comments
 (0)