Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f22a91f
feat(changelog): changelog tree generation from markdown
woile Jul 20, 2019
bf047e9
feat(cz/base): add default process_commit for processing commit message
Lee-W Jan 15, 2020
632a3e4
feat(cz/conventinal_commits): add changelog_map, changelog_pattern an…
Lee-W Jan 15, 2020
08e8238
feat(commands/changelog): generate changelog_tree from all past commits
Lee-W Jan 15, 2020
84ca569
feat(changelog): generate changelog based on git log
Lee-W Jan 17, 2020
c158042
style(all): blackify
Lee-W Jan 15, 2020
075b055
refactor(commands/changelog): use jinja2 template instead of string c…
Lee-W Jan 23, 2020
6215c29
fix(cli): add changelog arguments
Lee-W Jan 23, 2020
e52eda7
feat(commands/changlog): add --start-rev argument to `cz changelog`
Lee-W Jan 23, 2020
ceaf71b
style(cli): fix flake8 issue
Lee-W Jan 23, 2020
ae3c521
refactor(commands/changelog): remove redundant if statement
Lee-W Jan 23, 2020
529f5a2
refactor(tests/utils): move create_file_and_commit to tests/utils
Lee-W Jan 23, 2020
c12e44e
feat(commands/changelog): exit when there is no commit exists
Lee-W Jan 23, 2020
e871d16
fix(commands/changelog): remove --skip-merge argument
Lee-W Jan 23, 2020
273e736
fix(commitizen/cz): set changelog_map, changelog_pattern to none as d…
Lee-W Jan 23, 2020
8fb8c71
fix(changelog_template): fix list format
Lee-W Jan 23, 2020
aa877dc
test(commands/changelog): add test case for changelog command
Lee-W Jan 23, 2020
02d8a17
refactor(templates): move changelog_template from cz to templates
Lee-W Jan 23, 2020
32304b4
refactor(cli): reorder commands
Lee-W Jan 24, 2020
8916548
docs(README): add changelog command
Lee-W Jan 24, 2020
9b6fd57
refactor(templates): remove unneeded __init__ file
Lee-W Jan 24, 2020
4f0272f
style(tests/commands/changelog): blackify
Lee-W Jan 24, 2020
3278414
refactor(templates): rename as "keep_a_changelog_template.j2"
Lee-W Jan 24, 2020
780dbe1
feat(commands/changelog): make changelog_file an option in config
Lee-W Jan 24, 2020
5c152a4
docs(config): add changlog_file a config option
Lee-W Jan 24, 2020
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
15 changes: 9 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ Usage

$ cz --help
usage: cz [-h] [--debug] [-n NAME] [--version]
{ls,commit,c,example,info,schema,bump} ...
{init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
...

Commitizen is a cli tool to generate conventional commits.
For more information about the topic go to https://conventionalcommits.org/
Expand All @@ -161,18 +162,20 @@ Usage
--version get the version of the installed commitizen

commands:
{ls,commit,c,example,info,schema,bump,version,check,init}
ls show available commitizens
{init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
init init commitizen configuration
commit (c) create new commit
ls show available commitizens
example show commit example
info show information about the cz
schema show commit schema
bump bump semantic version based on the git log
version get the version of the installed commitizen or the
current project (default: installed commitizen)
changelog (ch) generate changelog (note that it will overwrite
existing file)
check validates that a commit message matches the commitizen
schema
init init commitizen configuration
version get the version of the installed commitizen or the
current project (default: installed commitizen)

Contributing
============
Expand Down
133 changes: 133 additions & 0 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""
# DESIGN

## Parse CHANGELOG.md

1. Get LATEST VERSION from CONFIG
1. Parse the file version to version
2. Build a dict (tree) of that particular version
3. Transform tree into markdown again

## Parse git log

1. get commits between versions
2. filter commits with the current cz rules
3. parse commit information
4. generate tree

Options:
- Generate full or partial changelog
"""
from typing import Generator, List, Dict, Iterable
import re

MD_VERSION_RE = r"^##\s(?P<version>[a-zA-Z0-9.+]+)\s?\(?(?P<date>[0-9-]+)?\)?"
MD_CATEGORY_RE = r"^###\s(?P<category>[a-zA-Z0-9.+\s]+)"
MD_MESSAGE_RE = r"^-\s(\*{2}(?P<scope>[a-zA-Z0-9]+)\*{2}:\s)?(?P<message>.+)"
md_version_c = re.compile(MD_VERSION_RE)
md_category_c = re.compile(MD_CATEGORY_RE)
md_message_c = re.compile(MD_MESSAGE_RE)


CATEGORIES = [
("fix", "fix"),
("breaking", "BREAKING CHANGES"),
("feat", "feat"),
("refactor", "refactor"),
("perf", "perf"),
("test", "test"),
("build", "build"),
("ci", "ci"),
("chore", "chore"),
]


def find_version_blocks(filepath: str) -> Generator:
"""
version block: contains all the information about a version.

E.g:
```
## 1.2.1 (2019-07-20)

## Bug fixes

- username validation not working

## Features

- new login system

```
"""
with open(filepath, "r") as f:
block: list = []
for line in f:
line = line.strip("\n")
if not line:
continue

if line.startswith("## "):
if len(block) > 0:
yield block
block = [line]
else:
block.append(line)
yield block


def parse_md_version(md_version: str) -> Dict:
m = md_version_c.match(md_version)
if not m:
return {}
return m.groupdict()


def parse_md_category(md_category: str) -> Dict:
m = md_category_c.match(md_category)
if not m:
return {}
return m.groupdict()


def parse_md_message(md_message: str) -> Dict:
m = md_message_c.match(md_message)
if not m:
return {}
return m.groupdict()


def transform_category(category: str) -> str:
_category_lower = category.lower()
for match_value, output in CATEGORIES:
if re.search(match_value, _category_lower):
return output
else:
raise ValueError(f"Could not match a category with {category}")


def generate_block_tree(block: List[str]) -> Dict:
tree: Dict = {"commits": []}
category = None
for line in block:
if line.startswith("## "):
category = None
tree = {**tree, **parse_md_version(line)}
elif line.startswith("### "):
result = parse_md_category(line)
if not result:
continue
category = transform_category(result.get("category", ""))

elif line.startswith("- "):
commit = parse_md_message(line)
commit["category"] = category
tree["commits"].append(commit)
else:
print("it's something else: ", line)
return tree


def generate_full_tree(blocks: Iterable) -> Iterable[Dict]:
for block in blocks:
yield generate_block_tree(block)
71 changes: 49 additions & 22 deletions commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
# "required": True,
"commands": [
{
"name": "ls",
"help": "show available commitizens",
"func": commands.ListCz,
"name": ["init"],
"help": "init commitizen configuration",
"func": commands.Init,
},
{
"name": ["commit", "c"],
Expand All @@ -55,6 +55,11 @@
},
],
},
{
"name": "ls",
"help": "show available commitizens",
"func": commands.ListCz,
},
{
"name": "example",
"help": "show commit example",
Expand Down Expand Up @@ -114,33 +119,29 @@
],
},
{
"name": ["version"],
"name": ["changelog", "ch"],
"help": (
"get the version of the installed commitizen or the current project"
" (default: installed commitizen)"
"generate changelog (note that it will overwrite existing file)"
),
"func": commands.Version,
"func": commands.Changelog,
"arguments": [
{
"name": ["-p", "--project"],
"help": "get the version of the current project",
"name": "--dry-run",
"action": "store_true",
"exclusive_group": "group1",
"default": False,
"help": "show changelog to stdout",
},
{
"name": ["-c", "--commitizen"],
"help": "get the version of the installed commitizen",
"action": "store_true",
"exclusive_group": "group1",
"name": "--file-name",
"help": "file name of changelog (default: 'CHANGELOG.md')",
},
{
"name": ["-v", "--verbose"],
"name": "--start-rev",
"default": None,
"help": (
"get the version of both the installed commitizen "
"and the current project"
"start rev of the changelog."
"If not set, it will generate changelog from the start"
),
"action": "store_true",
"exclusive_group": "group1",
},
],
},
Expand All @@ -161,9 +162,35 @@
],
},
{
"name": ["init"],
"help": "init commitizen configuration",
"func": commands.Init,
"name": ["version"],
"help": (
"get the version of the installed commitizen or the current project"
" (default: installed commitizen)"
),
"func": commands.Version,
"arguments": [
{
"name": ["-p", "--project"],
"help": "get the version of the current project",
"action": "store_true",
"exclusive_group": "group1",
},
{
"name": ["-c", "--commitizen"],
"help": "get the version of the installed commitizen",
"action": "store_true",
"exclusive_group": "group1",
},
{
"name": ["-v", "--verbose"],
"help": (
"get the version of both the installed commitizen "
"and the current project"
),
"action": "store_true",
"exclusive_group": "group1",
},
],
},
],
},
Expand Down
2 changes: 2 additions & 0 deletions commitizen/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
from .schema import Schema
from .version import Version
from .init import Init
from .changelog import Changelog


__all__ = (
"Bump",
"Check",
"Commit",
"Changelog",
"Example",
"Info",
"ListCz",
Expand Down
79 changes: 79 additions & 0 deletions commitizen/commands/changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import re
import pkg_resources
from collections import OrderedDict

from jinja2 import Template

from commitizen import factory, out, git
from commitizen.config import BaseConfig
from commitizen.error_codes import NO_COMMITS_FOUND, NO_PATTERN_MAP


class Changelog:
"""Generate a changelog based on the commit history."""

def __init__(self, config: BaseConfig, args):
self.config: BaseConfig = config
self.cz = factory.commiter_factory(self.config)

self.file_name = args["file_name"] or self.config.settings.get("changelog_file")
self.dry_run = args["dry_run"]
self.start_rev = args["start_rev"]

def __call__(self):
changelog_map = self.cz.changelog_map
changelog_pattern = self.cz.changelog_pattern
if not changelog_map or not changelog_pattern:
out.error(
f"'{self.config.settings['name']}' rule does not support changelog"
)
raise SystemExit(NO_PATTERN_MAP)

pat = re.compile(changelog_pattern)

commits = git.get_commits(start=self.start_rev)
if not commits:
out.error("No commits found")
raise SystemExit(NO_COMMITS_FOUND)

tag_map = {tag.rev: tag.name for tag in git.get_tags()}

entries = OrderedDict()
# The latest commit is not tagged
latest_commit = commits[0]
if latest_commit.rev not in tag_map:
current_key = "Unreleased"
entries[current_key] = OrderedDict(
{value: [] for value in changelog_map.values()}
)
else:
current_key = tag_map[latest_commit.rev]

for commit in commits:
if commit.rev in tag_map:
current_key = tag_map[commit.rev]
entries[current_key] = OrderedDict(
{value: [] for value in changelog_map.values()}
)

matches = pat.match(commit.message)
if not matches:
continue

processed_commit = self.cz.process_commit(commit.message)
for group_name, commit_type in changelog_map.items():
if matches.group(group_name):
entries[current_key][commit_type].append(processed_commit)
break

template_file = pkg_resources.resource_string(
__name__, "../templates/keep_a_changelog_template.j2"
).decode("utf-8")
jinja_template = Template(template_file)
changelog_str = jinja_template.render(entries=entries)
if self.dry_run:
out.write(changelog_str)
raise SystemExit(0)

with open(self.file_name, "w") as changelog_file:
changelog_file.write(changelog_str)
9 changes: 9 additions & 0 deletions commitizen/cz/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
class BaseCommitizen(metaclass=ABCMeta):
bump_pattern: Optional[str] = None
bump_map: Optional[dict] = None
changelog_pattern: Optional[str] = None
changelog_map: Optional[dict] = None
default_style_config: List[Tuple[str, str]] = [
("qmark", "fg:#ff9d00 bold"),
("question", "bold"),
Expand Down Expand Up @@ -57,3 +59,10 @@ def schema_pattern(self) -> str:
def info(self) -> str:
"""Information about the standardized commit message."""
raise NotImplementedError("Not Implemented yet")

def process_commit(self, commit: str) -> str:
"""Process commit for changelog.

If not overwritten, it returns the first line of commit.
"""
return commit.split("\n")[0]
Loading