Skip to content

Add trailer property #1350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion git/objects/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
import datetime
import re
from subprocess import Popen
from gitdb import IStream
from git.util import (
Expand Down Expand Up @@ -39,7 +40,7 @@

# typing ------------------------------------------------------------------

from typing import Any, IO, Iterator, List, Sequence, Tuple, Union, TYPE_CHECKING, cast
from typing import Any, IO, Iterator, List, Sequence, Tuple, Union, TYPE_CHECKING, cast, Dict

from git.types import PathLike, Literal

Expand Down Expand Up @@ -315,6 +316,44 @@ def stats(self) -> Stats:
text = self.repo.git.diff(self.parents[0].hexsha, self.hexsha, '--', numstat=True)
return Stats._list_from_string(self.repo, text)

@property
def trailers(self) -> Dict:
"""Get the trailers of the message as dictionary

Git messages can contain trailer information that are similar to RFC 822
e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers).

The trailer is thereby the last paragraph (seperated by a empty line
from the subject/body). This trailer paragraph must contain a ``:`` as
seperator for key and value in every line.

Valid message with trailer:

.. code-block::

Subject line

some body information

another information

key1: value1
key2: value2

:return: Dictionary containing whitespace stripped trailer information
"""
d: Dict[str, str] = {}
match = re.search(r".+^\s*$\n([\w\n\s:]+?)\s*\Z", str(self.message), re.MULTILINE | re.DOTALL)
if match is None:
return d
last_paragraph = match.group(1)
if not all(':' in line for line in last_paragraph.split('\n')):
return d
for line in last_paragraph.split('\n'):
key, value = line.split(':', 1)
d[key.strip()] = value.strip()
return d

@ classmethod
def _iter_from_process_or_stream(cls, repo: 'Repo', proc_or_stream: Union[Popen, IO]) -> Iterator['Commit']:
"""Parse out commit information into a list of Commit objects
Expand Down
2 changes: 1 addition & 1 deletion test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ddt>=1.1.1
ddt>=1.1.1, !=1.4.3
mypy

flake8
Expand Down
46 changes: 46 additions & 0 deletions test/test_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
import copy
from datetime import datetime
from io import BytesIO
import re
Expand Down Expand Up @@ -429,3 +430,48 @@ def test_datetimes(self):
datetime(2009, 10, 8, 20, 22, 51, tzinfo=tzoffset(-7200)))
self.assertEqual(commit.committed_datetime,
datetime(2009, 10, 8, 18, 22, 51, tzinfo=utc), commit.committed_datetime)

def test_trailers(self):
KEY_1 = "Hello"
VALUE_1 = "World"
KEY_2 = "Key"
VALUE_2 = "Value"

# Check if KEY 1 & 2 with Value 1 & 2 is extracted from multiple msg variations
msgs = []
msgs.append(f"Subject\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n")
msgs.append(f"Subject\n \nSome body of a function\n \n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n")
msgs.append(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")
msgs.append(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")

for msg in msgs:
commit = self.rorepo.commit('master')
commit = copy.copy(commit)
commit.message = msg
assert KEY_1 in commit.trailers.keys()
assert KEY_2 in commit.trailers.keys()
assert commit.trailers[KEY_1] == VALUE_1
assert commit.trailers[KEY_2] == VALUE_2

# Check that trailer stays empty for multiple msg combinations
msgs = []
msgs.append(f"Subject\n")
msgs.append(f"Subject\n\nBody with some\nText\n")
msgs.append(f"Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n")
msgs.append(f"Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n")
msgs.append(f"Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n")
msgs.append(f"Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n")

for msg in msgs:
commit = self.rorepo.commit('master')
commit = copy.copy(commit)
commit.message = msg
assert len(commit.trailers.keys()) == 0

# check that only the last key value paragraph is evaluated
commit = self.rorepo.commit('master')
commit = copy.copy(commit)
commit.message = f"Subject\n\nMultiline\nBody\n\n{KEY_1}: {VALUE_1}\n\n{KEY_2}: {VALUE_2}\n"
assert KEY_1 not in commit.trailers.keys()
assert KEY_2 in commit.trailers.keys()
assert commit.trailers[KEY_2] == VALUE_2