Skip to content

Commit e285ea5

Browse files
committed
Refactored commit processing to its own module.
- All commit processing moved from the templating module to the commits module.
1 parent 81c54de commit e285ea5

3 files changed

Lines changed: 195 additions & 166 deletions

File tree

generate_changelog/commits.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""Filter and process commits into contexts."""
2+
from typing import List, Optional
3+
4+
import collections
5+
import re
6+
7+
from git import Actor, Repo
8+
9+
from generate_changelog import git_ops
10+
from generate_changelog.actions.metadata import MetadataCollector
11+
from generate_changelog.configuration import Configuration
12+
from generate_changelog.context import CommitContext, GroupingContext, VersionContext
13+
from generate_changelog.git_ops import GitTag
14+
from generate_changelog.pipeline import Action, pipeline_factory
15+
from generate_changelog.utilities import resolve_name
16+
17+
18+
def get_context_from_tags(
19+
repository: Repo, config: Configuration, starting_tag: Optional[str] = None
20+
) -> List[VersionContext]:
21+
"""
22+
Generate the template context from git tags.
23+
24+
Args:
25+
repository: The git repository to evaluate.
26+
config: The current configuration object.
27+
starting_tag: Optional starting tag for generating incremental changelogs.
28+
29+
Returns:
30+
A list of VersionContext objects.
31+
"""
32+
tags = git_ops.get_commits_by_tags(repository, config.tag_pattern, starting_tag)
33+
output = []
34+
35+
for tag in tags:
36+
version_context = create_version_context(config, tag)
37+
38+
if output:
39+
output[-1].previous_tag = version_context.tag
40+
41+
output.append(version_context)
42+
43+
if starting_tag and output and output[-1].previous_tag is None:
44+
output[-1].previous_tag = starting_tag
45+
46+
return output
47+
48+
49+
def create_version_context(config: Configuration, tag: GitTag) -> VersionContext:
50+
"""
51+
Generate a :class:`VersionContext` from a tag dictionary.
52+
53+
Args:
54+
config: The current configuration object.
55+
tag: A GitTag used as the basis for a VersionContext
56+
57+
Returns:
58+
The finished version context.
59+
"""
60+
version_metadata_func = MetadataCollector()
61+
version_commit_groups = collections.defaultdict(list)
62+
63+
for commit in tag.commits:
64+
if any(re.search(ignore_pat, commit.summary) is not None for ignore_pat in config.ignore_patterns):
65+
continue
66+
67+
commit_ctx = generate_commit_context(commit, config, version_metadata_func)
68+
version_commit_groups[commit_ctx.grouping].append(commit_ctx)
69+
70+
tag_label = tag.tag_name if tag.tag_name != "HEAD" else config.unreleased_label
71+
72+
if tag.tag_info:
73+
tag_name = tag.tag_info.name
74+
tag_datetime = tag.tag_info.tagged_datetime
75+
if isinstance(tag.tag_info.tagger, Actor):
76+
tagger = f"{tag.tag_info.tagger.name} <{tag.tag_info.tagger.email}>"
77+
else:
78+
tagger = str(tag.tag_info.tagger)
79+
else:
80+
tag_name = None
81+
tag_datetime = None
82+
tagger = None
83+
84+
version_commits = sort_group_commits(version_commit_groups)
85+
86+
return VersionContext(
87+
label=tag_label,
88+
date_time=tag_datetime,
89+
tag=tag_name,
90+
tagger=tagger,
91+
grouped_commits=version_commits,
92+
metadata=version_metadata_func.metadata,
93+
)
94+
95+
96+
def generate_commit_context(commit, config, version_metadata_func) -> CommitContext:
97+
"""
98+
Create the renderable context for this commit.
99+
100+
The summary and body are processed through their pipelines, and a category is assigned.
101+
102+
Args:
103+
commit: The original commit data
104+
config: The configuration to use
105+
version_metadata_func: An optional callable to set version metadata while processing
106+
107+
Returns:
108+
The render-able commit context
109+
"""
110+
commit_metadata_func = MetadataCollector()
111+
summary_pipeline = pipeline_factory(
112+
action_list=config.summary_pipeline,
113+
commit_metadata_func=commit_metadata_func,
114+
version_metadata_func=version_metadata_func,
115+
)
116+
summary = summary_pipeline.run(commit.summary)
117+
body_pipeline = pipeline_factory(
118+
action_list=config.body_pipeline,
119+
commit_metadata_func=commit_metadata_func,
120+
version_metadata_func=version_metadata_func,
121+
)
122+
body_text = "\n".join(commit.message.splitlines()[1:])
123+
body = body_pipeline.run(body_text)
124+
125+
commit_ctx = CommitContext(
126+
sha=commit.hexsha,
127+
commit_datetime=commit.committed_datetime,
128+
committer=f"{commit.committer.name} <{commit.committer.email}>",
129+
summary=summary,
130+
body=body,
131+
grouping=(),
132+
metadata=commit_metadata_func.metadata.copy(),
133+
files=set(commit.stats.files.keys()),
134+
)
135+
category = first_matching(config.commit_classifiers, commit_ctx)
136+
commit_ctx.metadata["category"] = category
137+
138+
# The grouping is a tuple of the appropriate values according to the group_by configuration
139+
# We can sort commits later and grouped by this.
140+
grouping = tuple(resolve_name(commit_ctx, group) for group in config.group_by)
141+
commit_ctx.grouping = grouping
142+
return commit_ctx
143+
144+
145+
def sort_group_commits(commit_groups: dict) -> list:
146+
"""
147+
Sort the commit groups and convert the `dict` into a list of `GroupedCommit` objects.
148+
149+
Args:
150+
commit_groups: A dict where the keys are grouping values.
151+
152+
Returns:
153+
A list
154+
"""
155+
# Props to this sorting method goes to:
156+
# https://scipython.com/book2/chapter-4-the-core-python-language-ii/questions/sorting-a-list-containing-none/
157+
158+
def key_func(input_value) -> tuple:
159+
"""Generate the sortable key for tuples that may contain None."""
160+
return tuple((i is not None, i) for i in input_value[0])
161+
162+
sorted_groups = sorted(commit_groups.items(), key=key_func)
163+
return [GroupingContext(*item) for item in sorted_groups]
164+
165+
166+
def first_matching(actions: list, commit: CommitContext) -> str:
167+
"""
168+
Return the first section that matches the given commit summary.
169+
170+
Args:
171+
actions: A mapping of section names to a list of regular expressions for matching.
172+
commit: The commit context to evaluate
173+
174+
Returns:
175+
The name of the section.
176+
"""
177+
for action in actions:
178+
if action.get("action", None) is None:
179+
return action.get("category", None)
180+
181+
act = Action(
182+
action=action["action"],
183+
id_=action.get("id"),
184+
args=action.get("args"),
185+
kwargs=action.get("kwargs"),
186+
)
187+
if act.run(context={}, input_value=commit):
188+
return action.get("category", None)

generate_changelog/context.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ class CommitContext:
3535
metadata: dict = field(default_factory=dict)
3636
"""Metadata for this commit parsed from the commit message."""
3737

38+
files: set = field(default_factory=set)
39+
"""The files modified by this commit."""
40+
3841
_authors: Optional[list] = field(init=False) # list of dicts with name and email keys
3942
_author_names: Optional[list] = field(init=False) # list of just the names
4043

generate_changelog/templating.py

Lines changed: 4 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
"""Templating functions."""
2-
from typing import List, Optional
2+
from typing import Optional
33

4-
import collections
5-
import re
6-
7-
from git import Actor, Repo
4+
from git import Repo
85
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PackageLoader, select_autoescape
96

10-
from generate_changelog import git_ops
11-
from generate_changelog.actions.metadata import MetadataCollector
127
from generate_changelog.configuration import Configuration, get_config
13-
from generate_changelog.context import ChangelogContext, CommitContext, GroupingContext, VersionContext
14-
from generate_changelog.pipeline import Action, pipeline_factory
8+
from generate_changelog.context import ChangelogContext
159

16-
from .utilities import resolve_name
10+
from .commits import get_context_from_tags
1711

1812

1913
def get_default_env(config: Optional[Configuration] = None):
@@ -39,137 +33,6 @@ def get_pipeline_env(config: Optional[Configuration] = None):
3933
)
4034

4135

42-
def get_context_from_tags(
43-
repository: Repo, config: Configuration, starting_tag: Optional[str] = None
44-
) -> List[VersionContext]:
45-
"""
46-
Generate the template context from git tags.
47-
48-
Args:
49-
repository: The git repository to evaluate.
50-
config: The current configuration object.
51-
starting_tag: Optional starting tag for generating incremental changelogs.
52-
53-
Returns:
54-
A list of VersionContext objects.
55-
"""
56-
tags = git_ops.get_commits_by_tags(repository, config.tag_pattern, starting_tag)
57-
output = []
58-
version_metadata_func = MetadataCollector()
59-
60-
for tag in tags:
61-
version_commit_groups = collections.defaultdict(list)
62-
for commit in tag["commits"]:
63-
if any(re.search(pattern, commit.summary) is not None for pattern in config.ignore_patterns):
64-
continue
65-
66-
commit_ctx = generate_commit_context(commit, config, version_metadata_func)
67-
version_commit_groups[commit_ctx.grouping].append(commit_ctx)
68-
69-
tag_label = tag["tag_name"] if tag["tag_name"] != "HEAD" else config.unreleased_label
70-
if tag["tag_info"]:
71-
tag_name = tag["tag_info"].name
72-
tag_datetime = tag["tag_info"].tagged_datetime
73-
if isinstance(tag["tag_info"].tagger, Actor):
74-
tagger = f'{tag["tag_info"].tagger.name} <{tag["tag_info"].tagger.email}>'
75-
else:
76-
tagger = str(tag["tag_info"].tagger)
77-
else:
78-
tag_name = None
79-
tag_datetime = None
80-
tagger = None
81-
82-
version_commits = sort_group_commits(version_commit_groups)
83-
84-
if output:
85-
output[-1].previous_tag = tag_name
86-
87-
output.append(
88-
VersionContext(
89-
label=tag_label,
90-
date_time=tag_datetime,
91-
tag=tag_name,
92-
tagger=tagger,
93-
grouped_commits=version_commits,
94-
metadata=version_metadata_func.metadata,
95-
)
96-
)
97-
98-
if starting_tag and output and output[-1].previous_tag is None:
99-
output[-1].previous_tag = starting_tag
100-
101-
return output
102-
103-
104-
def generate_commit_context(commit, config, version_metadata_func) -> CommitContext:
105-
"""
106-
Create the renderable context for this commit.
107-
108-
The summary and body are processed through their pipelines, and a category is assigned.
109-
110-
Args:
111-
commit: The original commit data
112-
config: The configuration to use
113-
version_metadata_func: An optional callable to set version metadata while processing
114-
115-
Returns:
116-
The render-able commit context
117-
"""
118-
commit_metadata_func = MetadataCollector()
119-
summary_pipeline = pipeline_factory(
120-
action_list=config.summary_pipeline,
121-
commit_metadata_func=commit_metadata_func,
122-
version_metadata_func=version_metadata_func,
123-
)
124-
summary = summary_pipeline.run(commit.summary)
125-
body_pipeline = pipeline_factory(
126-
action_list=config.body_pipeline,
127-
commit_metadata_func=commit_metadata_func,
128-
version_metadata_func=version_metadata_func,
129-
)
130-
body_text = "\n".join(commit.message.splitlines()[1:])
131-
body = body_pipeline.run(body_text)
132-
133-
commit_ctx = CommitContext(
134-
sha=commit.hexsha,
135-
commit_datetime=commit.committed_datetime,
136-
committer=f"{commit.committer.name} <{commit.committer.email}>",
137-
summary=summary,
138-
body=body,
139-
grouping=(),
140-
metadata=commit_metadata_func.metadata.copy(),
141-
)
142-
category = first_matching(config.commit_classifiers, commit_ctx)
143-
commit_ctx.metadata["category"] = category
144-
145-
# The grouping is a tuple of the appropriate values according to the group_by configuration
146-
# We can sort commits later and grouped by this.
147-
grouping = tuple(resolve_name(commit_ctx, group) for group in config.group_by)
148-
commit_ctx.grouping = grouping
149-
return commit_ctx
150-
151-
152-
def sort_group_commits(commit_groups: dict) -> list:
153-
"""
154-
Sort the commit groups and convert the `dict` into a list of `GroupedCommit` objects.
155-
156-
Args:
157-
commit_groups: A dict where the keys are grouping values.
158-
159-
Returns:
160-
A list
161-
"""
162-
# Props to this sorting method goes to:
163-
# https://scipython.com/book2/chapter-4-the-core-python-language-ii/questions/sorting-a-list-containing-none/
164-
165-
def key_func(input_value) -> tuple:
166-
"""Generate the sortable key for tuples that may contain None."""
167-
return tuple((i is not None, i) for i in input_value[0])
168-
169-
sorted_groups = sorted(commit_groups.items(), key=key_func)
170-
return [GroupingContext(*item) for item in sorted_groups]
171-
172-
17336
def render(repository: Repo, config: Configuration, starting_tag: Optional[str] = None) -> str:
17437
"""
17538
Render the full or incremental changelog for the repository to a string.
@@ -190,28 +53,3 @@ def render(repository: Repo, config: Configuration, starting_tag: Optional[str]
19053
return "\n".join([heading_str, versions_str])
19154

19255
return get_default_env(config).get_template("base.md.jinja").render(context.as_dict())
193-
194-
195-
def first_matching(actions: list, commit: CommitContext) -> str:
196-
"""
197-
Return the first section that matches the given commit summary.
198-
199-
Args:
200-
actions: A mapping of section names to a list of regular expressions for matching.
201-
commit: The commit context to evaluate
202-
203-
Returns:
204-
The name of the section.
205-
"""
206-
for action in actions:
207-
if action.get("action", None) is None:
208-
return action.get("category", None)
209-
210-
act = Action(
211-
action=action["action"],
212-
id_=action.get("id"),
213-
args=action.get("args"),
214-
kwargs=action.get("kwargs"),
215-
)
216-
if act.run(context={}, input_value=commit):
217-
return action.get("category", None)

0 commit comments

Comments
 (0)