Skip to content

Commit

Permalink
feat: Add "template" command for generating templates from commits
Browse files Browse the repository at this point in the history
  • Loading branch information
multimac committed Jun 11, 2020
1 parent 72d54b0 commit d216714
Show file tree
Hide file tree
Showing 14 changed files with 548 additions and 74 deletions.
35 changes: 23 additions & 12 deletions conventional/commands/list_commits.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
import json
import logging
import os
from typing import List, TextIO
from typing import List, Optional, TextIO

import click
import confuse
import dateutil.tz

from .. import git, util
from ..parser.conventionalcommits.parser import ConventionalCommitParser
from ..main import pass_config
from . import parse_commit

logger = logging.getLogger(__name__)
Expand All @@ -24,26 +25,36 @@
default="-",
help="A file to write commits to. If `-`, commits will be written to stdout.",
)
@click.option("--from", "from_rev")
@click.option("--to", "to_rev", default="HEAD")
@click.option("--parse", is_flag=True, help="If set, commits will be parsed with `parse-commit`.")
@click.option(
"--include-unparsed",
"--include-unparsed/--no-include-unparsed",
is_flag=True,
default=None,
help="If set, commits which fail to be parsed will be returned. See `parse-commit`.",
)
def main(**kwargs):
asyncio.run(async_main(**kwargs))
@pass_config
def main(config: confuse.Configuration, **kwargs):
asyncio.run(async_main(config, **kwargs))


async def async_main(output: TextIO, **kwargs) -> None:
async def async_main(config: confuse.Configuration, *, output: TextIO, **kwargs) -> None:
try:
await async_main_impl(output=output, **kwargs)
await async_main_impl(config, output=output, **kwargs)
finally:
output.close()


async def async_main_impl(output: TextIO, parse: bool, include_unparsed: bool) -> None:
parser = ConventionalCommitParser()

async def async_main_impl(
config: confuse.Configuration,
*,
output: TextIO,
from_rev: Optional[str],
to_rev: str,
parse: bool,
include_unparsed: Optional[bool]
) -> None:
def _json_serial(obj):
"""JSON serializer for objects not serializable by default json code"""

Expand All @@ -61,7 +72,7 @@ def _json_serial(obj):
if parse:
parse_input, parse_output = util.create_pipe_streams()
parse_task = parse_commit.async_main(
parse_input, output, include_unparsed=include_unparsed
config, input=parse_input, output=output, include_unparsed=include_unparsed
)

logger.debug("Scheduling parse-commit task")
Expand All @@ -72,7 +83,7 @@ def _json_serial(obj):
try:
counter = 0

async for commit in git.get_commits():
async for commit in git.get_commits(start=from_rev, end=to_rev):
line = json.dumps(commit, default=_json_serial)
await loop.run_in_executor(None, output.writelines, [line, "\n"])
except:
Expand Down
50 changes: 38 additions & 12 deletions conventional/commands/parse_commit.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import asyncio
import datetime
import importlib
import json
import logging
from typing import Any, TextIO
from typing import Any, Optional, TextIO, TypedDict

import click
import confuse
import dateutil.tz

from .. import git
from ..parser.conventionalcommits.parser import ConventionalCommitParser
from ..main import pass_config
from ..parser.base import Parser

logger = logging.getLogger(__name__)


class ParsedCommit(TypedDict, total=False):
commit: git.Commit
parsed: Any


@click.command()
@click.option(
"--input",
Expand All @@ -27,25 +35,34 @@
help="A file to write parsed commits to. If `-`, parsed commits will be written to stdout.",
)
@click.option(
"--include-unparsed",
"--include-unparsed/--no-include-unparsed",
is_flag=True,
default=None,
help="If set, commits which fail to be parsed will be returned.",
)
def main(**kwargs):
asyncio.run(async_main(**kwargs))
@pass_config
def main(config: confuse.Configuration, **kwargs):
asyncio.run(async_main(config, **kwargs))


async def async_main(
config: confuse.Configuration,
*,
input: TextIO,
output: TextIO,
include_unparsed: Optional[bool],
) -> None:
if include_unparsed is not None:
config.set_args({"parser.include-unparsed": include_unparsed}, dots=True)

async def async_main(input: TextIO, output: TextIO, **kwargs) -> None:
try:
await async_main_impl(input=input, output=output, **kwargs)
await async_main_impl(config, input=input, output=output)
finally:
input.close()
output.close()


async def async_main_impl(input: TextIO, output: TextIO, include_unparsed: bool) -> None:
parser = ConventionalCommitParser()

async def async_main_impl(config: confuse.Configuration, *, input: TextIO, output: TextIO) -> None:
def _json_serial(obj):
"""JSON serializer for objects not serializable by default json code"""

Expand All @@ -54,6 +71,15 @@ def _json_serial(obj):

raise TypeError("Type %s not serializable" % type(obj))

def _load_parser(config: confuse.Configuration) -> Parser[Any]:
parser_config = config["parser"]
module = parser_config["module"].get(str)
name = parser_config["name"].get(str)
custom_config = parser_config["config"]

return getattr(importlib.import_module(module), name)(custom_config)

parser = _load_parser(config)
loop = asyncio.get_event_loop()

while True:
Expand All @@ -65,10 +91,10 @@ def _json_serial(obj):
commit: git.Commit = git.create_commit(**json.loads(input_line))
parsed: Any = parser.parse(commit)

if not include_unparsed and not parsed:
if not config["parser"]["include-unparsed"].get(bool) and not parsed:
continue

data = {"commit": commit}
data: ParsedCommit = {"commit": commit}
if parsed:
data["parsed"] = parsed

Expand Down
147 changes: 147 additions & 0 deletions conventional/commands/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import asyncio
import contextlib
import datetime
import fnmatch
import json
import logging
from typing import Any, AsyncIterable, Dict, List, Optional, TextIO, Tuple, cast

import click
import confuse
import dateutil.tz
import jinja2

from .. import git, util
from ..main import pass_config
from . import list_commits
from .parse_commit import ParsedCommit

logger = logging.getLogger(__name__)

DEFAULT = object()


@click.command()
@click.option("--input", type=click.File("r"), default=None)
@click.option("--output", type=click.File("w"), default="-")
@click.option("--tag-filter", type=str)
@click.option(
"--include-unparsed/--no-include-unparsed",
is_flag=True,
default=None,
help="If set, commits which fail to be parsed will be returned. See `parse-commit`.",
)
@pass_config
def main(config: confuse.Configuration, **kwargs):
asyncio.run(async_main(config, **kwargs))


async def async_main(
config: confuse.Configuration, *, input: Optional[TextIO], output: TextIO, **kwargs
) -> None:
try:
await async_main_impl(config, input=input, output=output, **kwargs)
finally:
if input is not None:
input.close()

output.close()


async def async_main_impl(
config: confuse.Configuration,
*,
input: Optional[TextIO],
output: TextIO,
include_unparsed: bool,
tag_filter: str,
) -> None:
def _read_config(view, default=DEFAULT, typ=str):
try:
return view.get(typ)
except confuse.NotFoundError:
if default is DEFAULT:
raise

return default

loop = asyncio.get_event_loop()

tasks: List[asyncio.Task] = []
if input is None:
list_input, list_output = util.create_pipe_streams()
list_task = list_commits.async_main(
config,
output=list_output,
include_unparsed=include_unparsed,
parse=True,
from_rev=None,
to_rev="HEAD",
)

logger.debug("Scheduling list-commits task")
tasks.append(asyncio.create_task(list_task))
input = list_input

gathered_tasks = asyncio.gather(*tasks)
try:
versioned_commits: List[
Tuple[Optional[git.Tag], Dict[Optional[str], List[ParsedCommit]]]
] = []

commits: Dict[Optional[str], List[ParsedCommit]] = {}
versioned_commits.append((None, commits))

while True:
input_line = await loop.run_in_executor(None, input.readline)

if not input_line:
break

data = json.loads(input_line)
commit = cast(ParsedCommit, data)

if commit["commit"]["tags"]:
tags = commit["commit"]["tags"]
if tag_filter:
tags = [tag for tag in tags if fnmatch.fnmatch(tag["name"], tag_filter)]

if tags:
tag = sorted(tags, key=lambda tag: tag["name"])[0]

logger.debug(f"Found version, {tag['name']}")
if commits:
logger.debug(f"({len(commits)} commits in previous version)")

commits = {}
versioned_commits.append((tag, commits))

typ = commit["parsed"]["subject"]["type"] if "parsed" in commit else None

if typ not in commits:
commits[typ] = []

commits[typ].append(commit)

logger.debug(f"{len(versioned_commits)} versions found")

environment = jinja2.Environment(
loader=jinja2.PackageLoader(config["template"]["package"].get(str)),
extensions=["jinja2.ext.loopcontrols"],
)

environment.filters["read_config"] = _read_config

template = environment.get_template(config["template"]["name"].get(str))
stream = template.stream(
versions=versioned_commits, config=config["template"]["config"], confuse=confuse
)
stream.dump(output)
except:
gathered_tasks.cancel()
raise
finally:
input.close()

with contextlib.suppress(asyncio.CancelledError):
await gathered_tasks
17 changes: 17 additions & 0 deletions conventional/config_default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
parser:
module: conventional.parser.conventional_commits
name: ConventionalCommitParser

include-unparsed: false

config:
types: [build, chore, ci, docs, feat, fix, perf, refactor, style, test]

template:
package: conventional
name: conventional-changelog.md

config:
types:
feat: Features
fix: Fixes
Loading

0 comments on commit d216714

Please sign in to comment.