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 all 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
52 changes: 50 additions & 2 deletions git/objects/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +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
from subprocess import Popen
from subprocess import Popen, PIPE
from gitdb import IStream
from git.util import (
hex_to_bin,
Expand All @@ -13,6 +13,7 @@
finalize_process
)
from git.diff import Diffable
from git.cmd import Git

from .tree import Tree
from . import base
Expand All @@ -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,53 @@ 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).

This funcions calls ``git interpret-trailers --parse`` onto the message
to extract the trailer information. The key value pairs are stripped of
leading and trailing whitespaces before they get saved into a dictionary.

Valid message with trailer:

.. code-block::

Subject line

some body information

another information

key1: value1
key2 : value 2 with inner spaces

dictionary will look like this:
.. code-block::

{
"key1": "value1",
"key2": "value 2 with inner spaces"
}

:return: Dictionary containing whitespace stripped trailer information

"""
d = {}
cmd = ['git', 'interpret-trailers', '--parse']
proc: Git.AutoInterrupt = self.repo.git.execute(cmd, as_process=True, istream=PIPE) # type: ignore
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can someone help with this?
I don't know how to get mypy to accept subprocess.PIPE as argument

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe @Yobmod could help here.

Copy link
Contributor

@Yobmod Yobmod Oct 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the istream type annotation is too narrow in cmd.Git.execute().
It should be int | IO | None if we accept any args that Popen does.

But execute() needs a lot more annotations anyway to clear up the remaining str | bytes problems from py2 and all the overloapping arg type @overloads.
So you can update the type annotation, or leave the type: ignore for now.
Once annotations updated, mypy should then warn that the type: ignore is no longer needed.

trailer: str = proc.communicate(str(self.message).encode())[0].decode()
if trailer.endswith('\n'):
trailer = trailer[0:-1]
if trailer != '':
for line in trailer.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 with inner spaces"

# 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