Skip to content

Commit

Permalink
Add github/gitlab regexes
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Jun 22, 2018
1 parent 27a60e8 commit 584fd73
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 76 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
Pipfile.lock
example/
example2/
output.md
example3/
output.md
__pycache__/
223 changes: 162 additions & 61 deletions script.py → gitolog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,95 +8,178 @@
env = Environment(loader=FileSystemLoader('templates'))


def bump(version, part='patch'):
major, minor, patch = version.split('.', 2)
patch = patch.split('-', 1)
pre = ''
if len(patch) > 1:
patch, pre = patch
if part == 'major':
major = str(int(major) + 1)
minor = patch = '0'
elif part == 'minor':
minor = str(int(minor) + 1)
patch = '0'
elif part == 'patch' and not pre:
patch = str(int(patch) + 1)
return '.'.join((major, minor, patch))


class GitHub:
ISSUE_REF = re.compile(
r'(?P<keyword>(close[sd]?|fix(e[sd])?|resolve[sd]?):? +)?'
r'(?P<ref>([-\w]+/[-\w]+)?#[1-9]\d*)', re.IGNORECASE)

@classmethod
def parse_issues_refs(cls, s):
return [m.groupdict() for m in cls.ISSUE_REF.finditer(s)]


class GitLab(GitHub):
ISSUE_REF = re.compile(
r'(?P<keyword>' # start keyword group
r'clos(?:e[sd]?|ing)|'
r'fix(?:e[sd]|ing)?|'
r'resolv(?:e[sd]?|ing)|'
r'implement(?:s|ed|ing)?'
r')' # end keyword group
r':? +' # optional : and spaces
r'(?:' # start repetition of issues sep by "," or "and"
r'(?:issues? +)?' # optional issue[s] word
r'(?P<ref>' # start ref group
r'(?:(?:[-\w]+/)?[-\w]+)' # [namespace/]project
r'?#[1-9]\d*' # number
r')' # end ref group
r'(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+)'
r')+', # end repetition of issues
re.IGNORECASE)


class Issue:
def __init__(self, number='', url=''):
def __init__(self, number='', url='', closed=False):
self.number = number
self.url = url
self.closed = closed


class Commit:
subject_regex = re.compile(
r'^(?P<type>((add(ed|s)?)|(change[ds]?)|(fix(es|ed)?)|(remove[sd]?)|(merged?)))',
re.IGNORECASE)
body_regex = re.compile(r'(?P<issues>#\d+)')
break_regex = re.compile(r'^break(s|ing changes)?[ :].+$')
types = {
'add': 'Added',
'fix': 'Fixed',
'change': 'Changed',
'remove': 'Removed',
'merge': 'Merged'
}

def __init__(
self, hash, author_name='', author_email='', author_date='',
committer_name='', committer_email='', committer_date='',
tag='', subject='', body=None, url=''):
tag='', subject='', body=None, url='', style=None):
self.hash = hash
self.author_name = author_name
self.author_email = author_email
self.author_date = datetime.utcfromtimestamp(float(author_date))
self.committer_name = committer_name
self.committer_email = committer_email
self.committer_date = datetime.utcfromtimestamp(float(committer_date))
self.tag = self.version = tag.replace('tag: ', '')
self.subject = subject
self.body = body or []
self.type = ''
self.url = url
self.issues = []

self.has_breaking_change = None
self.adds_feature = None
if tag.startswith('tag: '):
tag = tag.replace('tag: ', '')
elif tag:
tag = ''
self.tag = self.version = tag

self.parse_message()
self.fix_type()
self.type = ''
self.issues = []

def parse_message(self):
subject_match = self.subject_regex.match(self.subject)
if subject_match is not None:
for group, value in subject_match.groupdict().items():
setattr(self, group, value)
body_match = self.body_regex.match('\n'.join(self.body))
if body_match is not None:
for group, value in body_match.groupdict().items():
setattr(self, group, value)
if style:
self.extra = style.parse_commit(self)

def fix_type(self):
for k, v in self.types.items():
if self.type.lower().startswith(k):
self.type = v

def build_issues(self, url):
self.issues = [Issue(number=issue, url=url + issue)
if isinstance(issue, str) else issue
for issue in self.issues]
class Style:
def parse_commit(self, commit):
raise NotImplementedError

@property
def is_patch(self):
return not any((self.is_minor, self.is_major))

@property
def is_minor(self):
return self.type.lower() in ('add', 'adds', 'added') and not self.is_major
class DefaultStyle(Style):
TYPES = {
'add': 'Added',
'fix': 'Fixed',
'change': 'Changed',
'remove': 'Removed',
'merge': 'Merged',
'doc': 'Documented'
}

@property
def is_major(self):
return bool(self.break_regex.match('\n'.join(self.body)))
TYPE_REGEX = re.compile(r'^(?P<type>(%s))' % '|'.join(TYPES.keys()), re.IGNORECASE)
CLOSED = ('close', 'fix')
ISSUE_REGEX = re.compile(r'(?P<issues>((%s)\w* )?(#\d+,? ?)+)' % '|'.join(CLOSED))
BREAK_REGEX = re.compile(r'^break(s|ing changes)?[ :].+$')

def __init__(self, issue_url):
self.issue_url = issue_url

class AngularCommit(Commit):
subject_regex = re.compile(
def parse_commit(self, commit):
commit_type = self.parse_type(commit.subject)
message = '\n'.join([commit.subject] + commit.body)
is_major = self.is_major(message)
is_minor = not is_major or self.is_minor(commit_type)
is_patch = not any((is_major, is_minor))
issues = self.parse_issues(message)

info = dict(
type=commit_type,
issues=issues,
related_issues=[],
closed_issues=[],
is_major=is_major,
is_minor=is_minor,
is_patch=is_patch
)

for issue in issues:
{True: info['closed_issues'], # on-the-fly dict.gets are fun
False: info['related_issues']}.get(issue.closed).append(issue)

def parse_type(self, commit_subject):
type_match = self.TYPE_REGEX.match(commit_subject)
if type_match is not None:
return self.TYPES.get(type_match.groupdict().get('type').lower())

def parse_issues(self, commit_message):
issues = []
issue_match = self.ISSUE_REGEX.match(commit_message)
if issue_match is not None:
issues_found = issue_match.groupdict().get('issues')
for issue in issues_found:
closed = False
numbers = issue
for close_word in self.CLOSED:
if issue.lower().startswith(close_word):
closed = True
numbers = issue.split(' ', 1)[1]
break
numbers = numbers.replace(',', '').replace(' ', '').lstrip('#').split('#')
for number in numbers:
url = self.issue_url.format(number)
issue = Issue(number=number, url=url, closed=closed)
issues.append(issue)
return issues

def is_minor(self, commit_type):
return commit_type == self.TYPES['add']

def is_major(self, commit_message):
return bool(self.BREAK_REGEX.match(commit_message))


class AngularStyle(Style):
SUBJECT_REGEX = re.compile(
r'^(?P<type>(feat|fix|docs|style|refactor|test|chore))'
r'(?P<scope>\(.+\))?: (?P<subject>.+)$')

@property
def is_minor(self):
return self.type.lower() == 'feat' and not self.is_major

def fix_type(self):
def parse_commit(self, commit):
pass

@staticmethod
def is_minor(commit_type):
return commit_type == 'feat'


class Gitolog:
MARKER = '--GITOLOG MARKER--'
Expand All @@ -114,16 +197,14 @@ class Gitolog:
)

COMMAND = ['git', 'log', '--date=unix', '--format=' + FORMAT]
DEFAULT_STYLE = 'basic'
STYLE = {
'basic': Commit,
'angular': AngularCommit
'angular': AngularStyle
}

def __init__(
self, repository,
project_url='', commit_url='', issue_url='', compare_url='',
style=DEFAULT_STYLE):
project_url='', commit_url='', issue_url='', compare_url='', version_url='',
style=DefaultStyle):
self.repository = repository
self.project_url = project_url if project_url else self.get_url()

Expand All @@ -133,10 +214,13 @@ def __init__(
issue_url = self.project_url + '/issues/'
if not compare_url:
compare_url = self.project_url + '/compare/'
if not version_url:
version_url = self.project_url + '/releases/tag/'

self.commit_url = commit_url
self.issue_url = issue_url
self.compare_url = compare_url
self.version_url = version_url

self.raw_log = self.get_log()
self.commits = self.parse_commits()
Expand All @@ -146,6 +230,23 @@ def __init__(
self.versions_list = versions['as_list']
self.versions_dict = versions['as_dict']

if not self.versions_list[0].tag and len(self.versions_list) > 1:
last_tag = self.versions_list[1].tag
major = minor = False
for commit in self.versions_list[0].commits:
if commit.is_major:
major = True
break
elif commit.is_minor:
minor = True
if major:
planned_tag = bump(last_tag, 'major')
elif minor:
planned_tag = bump(last_tag, 'minor')
else:
planned_tag = bump(last_tag, 'patch')
self.versions_list[0].planned_tag = planned_tag

if isinstance(style, str):
try:
style = self.STYLE[style]
Expand Down Expand Up @@ -198,7 +299,7 @@ def parse_commits(self):
return commits

def apply_versions_to_commits(self):
versions_dates = {}
versions_dates = {'': None}
version = None
for commit in self.commits:
if commit.version:
Expand Down
4 changes: 0 additions & 4 deletions templates/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,3 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
{% for version in gitolog.versions_list -%}
{% include 'version.md' with context %}
{% endfor -%}

{% for version in gitolog.versions_list -%}
{% include 'version_link.md' with context %}
{% endfor -%}
2 changes: 1 addition & 1 deletion templates/commit.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
- {{ commit.subject }} ([{{ commit.hash }}]({{ commit.url }}))
- {{ commit.subject }} ([{{ commit.hash|truncate(7, True, '') }}]({{ commit.url }}))
2 changes: 1 addition & 1 deletion templates/section.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
### {{ section.type or "Misc" }}
{% for commit in section.commits -%}
{% for commit in section.commits|sort(attribute='subject') -%}
{% include 'commit.md' with context %}
{% endfor %}
8 changes: 7 additions & 1 deletion templates/version.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
## [{{ version.tag }}]{% if version.date %} - {{ version.date }}{% endif %}
## [{{ version.tag or version.planned_tag or "Unrealeased" }}]({{ gitolog.compare_url }}
{%- if version.previous_version -%}
{{- version.previous_version.tag -}}
{%- else -%}
{{- (version.commits|last).hash -}}
{%- endif -%}
...{{ version.tag or "HEAD" }}){% if version.date %} - {{ version.date }}{% endif %}

{% for type, section in version.sections_dict|dictsort -%}
{%- if type and type != 'Merged' -%}
Expand Down
7 changes: 0 additions & 7 deletions templates/version_link.md

This file was deleted.

29 changes: 29 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import gitolog

message = """
This is the subject #1
This is the body. Related: #2. Also mentions #3 and #4.
Closes #5. closed #6, #7. FIX: #89 and #10.
Resolve #1111.
Also support other projects references like shellm-org/shellm-data#19!!
Or fix pawamoy/gitolog#1.
Don't match this one: #01.
"""


def test_github_issue_parsing():
matches = gitolog.GitHub.parse_issues_refs(message)
for match in matches:
print(match)


def test_gitlab_issue_parsing():
matches = gitolog.GitLab.parse_issues_refs(message)
for match in matches:
print(match)


if __name__ == '__main__':
# test_github_issue_parsing()
test_gitlab_issue_parsing()

0 comments on commit 584fd73

Please sign in to comment.