Skip to content

Commit e3bc5d1

Browse files
authored
Merge pull request #1576 from itsluketwist/fix-trailers
Fix up the commit trailers functionality
2 parents 61ed7ec + 9ef07a7 commit e3bc5d1

File tree

2 files changed

+121
-58
lines changed

2 files changed

+121
-58
lines changed

git/objects/commit.py

Lines changed: 80 additions & 22 deletions
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,72 @@ 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+
:note: This property is deprecated, please use either ``Commit.trailers_list`` or ``Commit.trailers_dict``.
343+
344+
:return:
345+
Dictionary containing whitespace stripped trailer information.
346+
Only contains the latest instance of each trailer key.
347+
"""
348+
return {
349+
k: v[0] for k, v in self.trailers_dict.items()
350+
}
351+
352+
@property
353+
def trailers_list(self) -> List[Tuple[str, str]]:
354+
"""Get the trailers of the message as a list
355+
356+
Git messages can contain trailer information that are similar to RFC 822
357+
e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers).
358+
359+
This functions calls ``git interpret-trailers --parse`` onto the message
360+
to extract the trailer information, returns the raw trailer data as a list.
361+
362+
Valid message with trailer::
363+
364+
Subject line
365+
366+
some body information
367+
368+
another information
369+
370+
key1: value1.1
371+
key1: value1.2
372+
key2 : value 2 with inner spaces
373+
374+
375+
Returned list will look like this::
376+
377+
[
378+
("key1", "value1.1"),
379+
("key1", "value1.2"),
380+
("key2", "value 2 with inner spaces"),
381+
]
382+
383+
384+
:return:
385+
List containing key-value tuples of whitespace stripped trailer information.
386+
"""
387+
cmd = ["git", "interpret-trailers", "--parse"]
388+
proc: Git.AutoInterrupt = self.repo.git.execute(cmd, as_process=True, istream=PIPE) # type: ignore
389+
trailer: str = proc.communicate(str(self.message).encode())[0].decode("utf8")
390+
trailer = trailer.strip()
391+
392+
if not trailer:
393+
return []
394+
395+
trailer_list = []
396+
for t in trailer.split("\n"):
397+
key, val = t.split(":", 1)
398+
trailer_list.append((key.strip(), val.strip()))
399+
400+
return trailer_list
401+
402+
@property
403+
def trailers_dict(self) -> Dict[str, List[str]]:
404+
"""Get the trailers of the message as a dictionary
340405
341406
Git messages can contain trailer information that are similar to RFC 822
342407
e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers).
@@ -345,42 +410,35 @@ def trailers(self) -> Dict:
345410
to extract the trailer information. The key value pairs are stripped of
346411
leading and trailing whitespaces before they get saved into a dictionary.
347412
348-
Valid message with trailer:
349-
350-
.. code-block::
413+
Valid message with trailer::
351414
352415
Subject line
353416
354417
some body information
355418
356419
another information
357420
358-
key1: value1
421+
key1: value1.1
422+
key1: value1.2
359423
key2 : value 2 with inner spaces
360424
361-
dictionary will look like this:
362425
363-
.. code-block::
426+
Returned dictionary will look like this::
364427
365428
{
366-
"key1": "value1",
367-
"key2": "value 2 with inner spaces"
429+
"key1": ["value1.1", "value1.2"],
430+
"key2": ["value 2 with inner spaces"],
368431
}
369432
370-
:return: Dictionary containing whitespace stripped trailer information
371433
434+
:return:
435+
Dictionary containing whitespace stripped trailer information.
436+
Mapping trailer keys to a list of their corresponding values.
372437
"""
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
438+
d = defaultdict(list)
439+
for key, val in self.trailers_list:
440+
d[key].append(val)
441+
return dict(d)
384442

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

test/test_commit.py

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -494,52 +494,57 @@ 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+
(KEY_1, VALUE_1_1),
519+
(KEY_2, VALUE_2),
520+
(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+
527+
# check that trailer stays empty for multiple msg combinations
528+
msgs = [
529+
f"Subject\n",
530+
f"Subject\n\nBody with some\nText\n",
531+
f"Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n",
532+
f"Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n",
533+
f"Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n",
534+
f"Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n",
535+
]
529536

530537
for msg in msgs:
531-
commit = self.rorepo.commit("master")
532-
commit = copy.copy(commit)
538+
commit = copy.copy(self.rorepo.commit("master"))
533539
commit.message = msg
534-
assert len(commit.trailers.keys()) == 0
540+
assert commit.trailers_list == []
541+
assert commit.trailers_dict == {}
535542

536543
# 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
544+
commit = copy.copy(self.rorepo.commit("master"))
545+
commit.message = f"Subject\n\nMultiline\nBody\n\n{KEY_1}: {VALUE_1_1}\n\n{KEY_2}: {VALUE_2}\n"
546+
assert commit.trailers_list == [(KEY_2, VALUE_2)]
547+
assert commit.trailers_dict == {KEY_2: [VALUE_2]}
543548

544549
def test_commit_co_authors(self):
545550
commit = copy.copy(self.rorepo.commit("4251bd5"))

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy