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

Lines changed: 79 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,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

Lines changed: 47 additions & 36 deletions
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)
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