diff --git a/commitizen/bump.py b/commitizen/bump.py index 64ae2e469e..bfda1c4541 100644 --- a/commitizen/bump.py +++ b/commitizen/bump.py @@ -6,6 +6,7 @@ from packaging.version import Version +from commitizen.git import GitCommit from commitizen.defaults import ( MAJOR, MINOR, @@ -17,7 +18,7 @@ def find_increment( - messages: List[str], regex: str = bump_pattern, increments_map: dict = bump_map + commits: List[GitCommit], regex: str = bump_pattern, increments_map: dict = bump_map ) -> Optional[str]: # Most important cases are major and minor. @@ -26,18 +27,19 @@ def find_increment( pattern = re.compile(regex) increment = None - for message in messages: - result = pattern.search(message) - if not result: - continue - found_keyword = result.group(0) - new_increment = increments_map_default[found_keyword] - if new_increment == "MAJOR": + for commit in commits: + for message in commit.message.split("\n"): + result = pattern.search(message) + if not result: + continue + found_keyword = result.group(0) + new_increment = increments_map_default[found_keyword] + if new_increment == "MAJOR": + increment = new_increment + break + elif increment == "MINOR" and new_increment == "PATCH": + continue increment = new_increment - break - elif increment == "MINOR" and new_increment == "PATCH": - continue - increment = new_increment return increment diff --git a/commitizen/cmd.py b/commitizen/cmd.py index 17c81ed7af..67c7cf6bd0 100644 --- a/commitizen/cmd.py +++ b/commitizen/cmd.py @@ -10,9 +10,12 @@ class Command(NamedTuple): def run(cmd: str) -> Command: - cmd.split() process = subprocess.Popen( - cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, ) stdout, stderr = process.communicate() return Command(stdout.decode(), stderr.decode(), stdout, stderr) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index aa658cbebc..605fde9878 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, List import questionary from packaging.version import Version @@ -20,17 +20,11 @@ class Bump: def __init__(self, config: BaseConfig, arguments: dict): self.config: BaseConfig = config self.arguments: dict = arguments - self.parameters: dict = { + self.bump_settings: dict = { **config.settings, **{ key: arguments[key] - for key in [ - "dry_run", - "tag_format", - "prerelease", - "increment", - "bump_message", - ] + for key in ["tag_format", "prerelease", "increment", "bump_message"] if arguments[key] is not None }, } @@ -54,7 +48,7 @@ def is_initial_tag(self, current_tag_version: str, is_yes: bool = False) -> bool is_initial = questionary.confirm("Is this the first tag created?").ask() return is_initial - def find_increment(self, commits: list) -> Optional[str]: + def find_increment(self, commits: List[git.GitCommit]) -> Optional[str]: bump_pattern = self.cz.bump_pattern bump_map = self.cz.bump_map if not bump_map or not bump_pattern: @@ -65,10 +59,10 @@ def find_increment(self, commits: list) -> Optional[str]: ) return increment - def __call__(self): + def __call__(self): # noqa: C901 """Steps executed to bump.""" try: - current_version_instance: Version = Version(self.parameters["version"]) + current_version_instance: Version = Version(self.bump_settings["version"]) except TypeError: out.error( "[NO_VERSION_SPECIFIED]\n" @@ -79,21 +73,26 @@ def __call__(self): # Initialize values from sources (conf) current_version: str = self.config.settings["version"] - tag_format: str = self.parameters["tag_format"] - bump_commit_message: str = self.parameters["bump_message"] - current_tag_version: str = bump.create_tag( - current_version, tag_format=tag_format - ) - version_files: list = self.parameters["version_files"] - dry_run: bool = self.parameters["dry_run"] + tag_format: str = self.bump_settings["tag_format"] + bump_commit_message: str = self.bump_settings["bump_message"] + version_files: list = self.bump_settings["version_files"] + + dry_run: bool = self.arguments["dry_run"] is_yes: bool = self.arguments["yes"] - prerelease: str = self.arguments["prerelease"] increment: Optional[str] = self.arguments["increment"] + prerelease: str = self.arguments["prerelease"] is_files_only: Optional[bool] = self.arguments["files_only"] + current_tag_version: str = bump.create_tag( + current_version, tag_format=tag_format + ) + is_initial = self.is_initial_tag(current_tag_version, is_yes) - commits = git.get_commits(current_tag_version, from_beginning=is_initial) + if is_initial: + commits = git.get_commits() + else: + commits = git.get_commits(current_tag_version) # No commits, there is no need to create an empty tag. # Unless we previously had a prerelease. @@ -118,9 +117,11 @@ def __call__(self): ) # Report found information - out.write(message) - out.write(f"tag to create: {new_tag_version}") - out.write(f"increment detected: {increment}") + out.write( + f"message\n" + f"tag to create: {new_tag_version}\n" + f"increment detected: {increment}\n" + ) # Do not perform operations over files or git. if dry_run: diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index 7fc39cdd01..6a06abb560 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -5,7 +5,7 @@ from commitizen import factory, out from commitizen.cz import registry from commitizen.config import BaseConfig, TomlConfig, IniConfig -from commitizen.git import get_latest_tag, get_all_tags +from commitizen.git import get_latest_tag_name, get_tag_names from commitizen.defaults import config_files @@ -57,7 +57,7 @@ def _ask_name(self) -> str: return name def _ask_tag(self) -> str: - latest_tag = get_latest_tag() + latest_tag = get_latest_tag_name() if not latest_tag: out.error("No Existing Tag. Set tag to v0.0.1") return "0.0.1" @@ -66,14 +66,14 @@ def _ask_tag(self) -> str: f"Is {latest_tag} the latest tag?", style=self.cz.style, default=False ).ask() if not is_correct_tag: - tags = get_all_tags() + tags = get_tag_names() if not tags: out.error("No Existing Tag. Set tag to v0.0.1") return "0.0.1" latest_tag = questionary.select( "Please choose the latest tag: ", - choices=get_all_tags(), + choices=get_tag_names(), style=self.cz.style, ).ask() diff --git a/commitizen/git.py b/commitizen/git.py index 9f831e5b54..0853d76005 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -6,6 +6,37 @@ from commitizen import cmd +class GitObject: + def __eq__(self, other): + if not isinstance(other, GitObject): + return False + return self.rev == other.rev + + +class GitCommit(GitObject): + def __init__(self, rev, title, body=""): + self.rev = rev.strip() + self.title = title.strip() + self.body = body.strip() + + @property + def message(self): + return f"{self.title}\n\n{self.body}" + + def __repr__(self): + return f"{self.title} ({self.rev})" + + +class GitTag(GitObject): + def __init__(self, name, rev, date): + self.rev = rev.strip() + self.name = name.strip() + self.date = date.strip() + + def __repr__(self): + return f"{self.name} ({self.rev})" + + def tag(tag: str): c = cmd.run(f"git tag {tag}") return c @@ -20,16 +51,54 @@ def commit(message: str, args=""): return c -def get_commits(start: str, end: str = "HEAD", from_beginning: bool = False) -> list: +def get_commits( + start: Optional[str] = None, + end: str = "HEAD", + *, + log_format: str = "%H%n%s%n%b", + delimiter: str = "----------commit-delimiter----------", +) -> List[GitCommit]: + """ + Get the commits betweeen start and end + """ + git_log_cmd = f"git log --pretty={log_format}{delimiter}" - c = cmd.run(f"git log --pretty=format:%s%n%b {start}...{end}") - - if from_beginning: - c = cmd.run(f"git log --pretty=format:%s%n%b {end}") + if start: + c = cmd.run(f"{git_log_cmd} {start}...{end}") + else: + c = cmd.run(f"{git_log_cmd} {end}") if not c.out: return [] - return c.out.split("\n") + + git_commits = [] + for rev_and_commit in c.out.split(delimiter): + rev_and_commit = rev_and_commit.strip() + if not rev_and_commit: + continue + rev, title, *body_list = rev_and_commit.split("\n") + + if rev_and_commit: + git_commit = GitCommit( + rev=rev.strip(), title=title.strip(), body="\n".join(body_list).strip() + ) + git_commits.append(git_commit) + return git_commits + + +def get_tags(dateformat: str = "%Y-%m-%d") -> List[GitTag]: + inner_delimiter = "---inner_delimiter---" + formatter = ( + f"'%(refname:lstrip=2){inner_delimiter}" + f"%(objectname){inner_delimiter}" + f"%(committerdate:format:{dateformat})'" + ) + c = cmd.run(f"git tag --format={formatter} --sort=-committerdate") + if c.err or not c.out: + return [] + + git_tags = [GitTag(*line.split(inner_delimiter)) for line in c.out.split("\n")[:-1]] + return git_tags def tag_exist(tag: str) -> bool: @@ -37,21 +106,14 @@ def tag_exist(tag: str) -> bool: return tag in c.out -def is_staging_clean() -> bool: - """Check if staing is clean""" - c = cmd.run("git diff --no-ext-diff --name-only") - c_cached = cmd.run("git diff --no-ext-diff --cached --name-only") - return not (bool(c.out) or bool(c_cached.out)) - - -def get_latest_tag() -> Optional[str]: +def get_latest_tag_name() -> Optional[str]: c = cmd.run("git describe --abbrev=0 --tags") if c.err: return None return c.out.strip() -def get_all_tags() -> Optional[List[str]]: +def get_tag_names() -> Optional[List[str]]: c = cmd.run("git tag --list") if c.err: return [] @@ -63,3 +125,10 @@ def find_git_project_root() -> Optional[Path]: if not c.err: return Path(c.out.strip()) return None + + +def is_staging_clean() -> bool: + """Check if staing is clean""" + c = cmd.run("git diff --no-ext-diff --name-only") + c_cached = cmd.run("git diff --no-ext-diff --cached --name-only") + return not (bool(c.out) or bool(c_cached.out)) diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index 48ebeebfeb..e5ef886f63 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -8,17 +8,6 @@ from commitizen import cli, cmd, git -@pytest.fixture(scope="function") -def tmp_git_project(tmpdir): - with tmpdir.as_cwd(): - with open("pyproject.toml", "w") as f: - f.write("[tool.commitizen]\n" 'version="0.1.0"') - - cmd.run("git init") - - yield - - def create_file_and_commit(message: str, filename: Optional[str] = None): if not filename: filename = str(uuid.uuid4()) @@ -28,7 +17,7 @@ def create_file_and_commit(message: str, filename: Optional[str] = None): git.commit(message) -@pytest.mark.usefixtures("tmp_git_project") +@pytest.mark.usefixtures("tmp_commitizen_project") def test_bump_command(mocker): # MINOR create_file_and_commit("feat: new file") @@ -101,12 +90,12 @@ def test_bump_when_bumpping_is_not_support(mocker, capsys, tmpdir): assert "'cz_jira' rule does not support bump" in err -def test_bump_is_not_specify(mocker, capsys, tmpdir): +@pytest.mark.usefixtures("tmp_git_project") +def test_bump_is_not_specify(mocker, capsys): mocker.patch.object(sys, "argv", ["cz", "bump"]) with pytest.raises(SystemExit): - with tmpdir.as_cwd(): - cli.main() + cli.main() expected_error_message = ( "[NO_VERSION_SPECIFIED]\n" @@ -118,7 +107,8 @@ def test_bump_is_not_specify(mocker, capsys, tmpdir): assert expected_error_message in err -def test_bump_when_not_new_commit(mocker, capsys, tmp_git_project): +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_when_not_new_commit(mocker, capsys): testargs = ["cz", "bump", "--yes"] mocker.patch.object(sys, "argv", testargs) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..5d1acdcdfd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,18 @@ +import pytest + +from commitizen import cmd + + +@pytest.fixture(scope="function") +def tmp_git_project(tmpdir): + with tmpdir.as_cwd(): + cmd.run("git init") + + yield + + +@pytest.fixture(scope="function") +@pytest.mark.usefixtures("tmp_git_project") +def tmp_commitizen_project(tmp_git_project): + with open("pyproject.toml", "w") as f: + f.write("[tool.commitizen]\n" 'version="0.1.0"') diff --git a/tests/test_bump_find_increment.py b/tests/test_bump_find_increment.py index 160457c870..405410712a 100644 --- a/tests/test_bump_find_increment.py +++ b/tests/test_bump_find_increment.py @@ -3,6 +3,7 @@ SVE: Semantic version at the end """ from commitizen import bump +from commitizen.git import GitCommit NONE_INCREMENT_CC = ["docs(README): motivation", "ci: added travis"] @@ -45,49 +46,56 @@ def test_find_increment_type_patch(): messages = PATCH_INCREMENTS_CC - increment_type = bump.find_increment(messages) + commits = [GitCommit(rev="test", title=message) for message in messages] + increment_type = bump.find_increment(commits) assert increment_type == "PATCH" def test_find_increment_type_minor(): messages = MINOR_INCREMENTS_CC - increment_type = bump.find_increment(messages) + commits = [GitCommit(rev="test", title=message) for message in messages] + increment_type = bump.find_increment(commits) assert increment_type == "MINOR" def test_find_increment_type_major(): messages = MAJOR_INCREMENTS_CC - increment_type = bump.find_increment(messages) + commits = [GitCommit(rev="test", title=message) for message in messages] + increment_type = bump.find_increment(commits) assert increment_type == "MAJOR" def test_find_increment_type_patch_sve(): messages = PATCH_INCREMENTS_SVE + commits = [GitCommit(rev="test", title=message) for message in messages] increment_type = bump.find_increment( - messages, regex=semantic_version_pattern, increments_map=semantic_version_map + commits, regex=semantic_version_pattern, increments_map=semantic_version_map ) assert increment_type == "PATCH" def test_find_increment_type_minor_sve(): messages = MINOR_INCREMENTS_SVE + commits = [GitCommit(rev="test", title=message) for message in messages] increment_type = bump.find_increment( - messages, regex=semantic_version_pattern, increments_map=semantic_version_map + commits, regex=semantic_version_pattern, increments_map=semantic_version_map ) assert increment_type == "MINOR" def test_find_increment_type_major_sve(): messages = MAJOR_INCREMENTS_SVE + commits = [GitCommit(rev="test", title=message) for message in messages] increment_type = bump.find_increment( - messages, regex=semantic_version_pattern, increments_map=semantic_version_map + commits, regex=semantic_version_pattern, increments_map=semantic_version_map ) assert increment_type == "MAJOR" def test_find_increment_type_none(): messages = NONE_INCREMENT_CC + commits = [GitCommit(rev="test", title=message) for message in messages] increment_type = bump.find_increment( - messages, regex=semantic_version_pattern, increments_map=semantic_version_map + commits, regex=semantic_version_pattern, increments_map=semantic_version_map ) assert increment_type is None diff --git a/tests/test_cli.py b/tests/test_cli.py index 51638655b3..e136096f37 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -35,14 +35,14 @@ def test_name(mocker, capsys): assert out.startswith("JRA") -def test_name_default_value(tmpdir, mocker, capsys): - with tmpdir.as_cwd() as _: - testargs = ["cz", "example"] - mocker.patch.object(sys, "argv", testargs) +@pytest.mark.usefixtures("tmp_git_project") +def test_name_default_value(mocker, capsys): + testargs = ["cz", "example"] + mocker.patch.object(sys, "argv", testargs) - cli.main() - out, _ = capsys.readouterr() - assert out.startswith("fix: correct minor typos in code") + cli.main() + out, _ = capsys.readouterr() + assert out.startswith("fix: correct minor typos in code") def test_ls(mocker, capsys): diff --git a/tests/test_git.py b/tests/test_git.py new file mode 100644 index 0000000000..ef7cb2cdc9 --- /dev/null +++ b/tests/test_git.py @@ -0,0 +1,49 @@ +from commitizen import git + + +class FakeCommand: + def __init__(self, out=None, err=None): + self.out = out + self.err = err + + +def test_git_object_eq(): + git_commit = git.GitCommit( + rev="sha1-code", title="this is title", body="this is body" + ) + git_tag = git.GitTag(rev="sha1-code", name="0.0.1", date="2020-01-21") + + assert git_commit == git_tag + assert git_commit != "sha1-code" + + +def test_get_tags(mocker): + tag_str = ( + "v1.0.0---inner_delimiter---333---inner_delimiter---2020-01-20\n" + "v0.5.0---inner_delimiter---222---inner_delimiter---2020-01-17\n" + "v0.0.1---inner_delimiter---111---inner_delimiter---2020-01-17\n" + ) + mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=tag_str)) + + git_tags = git.get_tags() + latest_git_tag = git_tags[0] + assert latest_git_tag.rev == "333" + assert latest_git_tag.name == "v1.0.0" + assert latest_git_tag.date == "2020-01-20" + + mocker.patch( + "commitizen.cmd.run", return_value=FakeCommand(out="", err="No tag available") + ) + assert git.get_tags() == [] + + +def test_get_tag_names(mocker): + tag_str = "v1.0.0\n" "v0.5.0\n" "v0.0.1\n" + mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=tag_str)) + + assert git.get_tag_names() == ["v1.0.0", "v0.5.0", "v0.0.1"] + + mocker.patch( + "commitizen.cmd.run", return_value=FakeCommand(out="", err="No tag available") + ) + assert git.get_tag_names() == []