Skip to content

WIP: Fix/issue 251 #279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
17 changes: 17 additions & 0 deletions commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,23 @@
},
],
},
{
"name": "undo",
"help": "revert bump or commit",
"func": commands.Undo,
"arguments": [
{
"name": ["--bump", "-b"],
"action": "store_true",
"help": "revert bump",
},
{
"name": ["--commit", "-c"],
"action": "store_true",
"help": "revert latest commit, equal to git reset HEAD~",
},
],
},
{
"name": ["changelog", "ch"],
"help": (
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,6 +7,7 @@
from .init import Init
from .list_cz import ListCz
from .schema import Schema
from .undo import Undo
from .version import Version

__all__ = (
Expand All @@ -18,6 +19,7 @@
"Info",
"ListCz",
"Schema",
"Undo",
"Version",
"Init",
)
4 changes: 2 additions & 2 deletions commitizen/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def __call__(self):
def _ask_config_path(self) -> str:
name = questionary.select(
"Please choose a supported config file: (default: pyproject.toml)",
choices=config_files,
choices=config_files, # type: ignore
default="pyproject.toml",
style=self.cz.style,
).ask()
Expand Down Expand Up @@ -79,7 +79,7 @@ def _ask_tag(self) -> str:

latest_tag = questionary.select(
"Please choose the latest tag: ",
choices=get_tag_names(),
choices=get_tag_names(), # type: ignore
style=self.cz.style,
).ask()

Expand Down
61 changes: 61 additions & 0 deletions commitizen/commands/undo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from commitizen import cmd, factory, git, out
from commitizen.config import BaseConfig
from commitizen.exceptions import InvalidCommandArgumentError


class Undo:
"""Reset the latest git commit or git tag."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we use various terms for the same meaning (i.e., reset / revert / undo). I'll suggest unifying them.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could use "undo". 👌


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

def _get_bump_command(self):
created_tag = git.get_latest_tag()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this one named as created_tag instead of latest_tag?

Copy link
Author

@Sirius207 Sirius207 Oct 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The created_tag is original name and get_latest_tag comes later, I would use latest_tag in next commit. 👍

commits = git.get_commits()

if created_tag and commits:
created_commit = commits[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as the question above

else:
raise InvalidCommandArgumentError("There is no tag or commit to undo")

if created_tag.rev != created_commit.rev:
raise InvalidCommandArgumentError(
"The revision of the latest tag is not equal to the latest commit, use git undo --commit instead\n\n"
f"Latest Tag: {created_tag.name}, {created_tag.rev}, {created_tag.date}\n"
f"Latest Commit: {created_commit.title}, {created_commit.rev}"
)

command = f"git tag --delete {created_tag.name} && git reset HEAD~ && git reset --hard HEAD"

out.info("Reverting version bump, running:")
out.info(f"{command}")
out.info(
f"The tag can be removed from a remote by running `git push origin :{created_tag.name}`"
)

return command

def __call__(self):
bump: bool = self.arguments.get("bump")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump doesn't seem to be clear enough to me. How about something like reverting_bump?

commit: bool = self.arguments.get("commit")

if bump:
command = self._get_bump_command()
elif commit:
command = "git reset HEAD~"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes we'll encounter the situation that we don't have HEAD. Will it be possible for us to solve that situation?

else:
raise InvalidCommandArgumentError(
(
"One and only one argument is required for check command! "
"See 'cz undo -h' for more information"
)
)

c = cmd.run(command)
if c.err:
out.error(c.err)

out.write(c.out)
out.success("Undo successful!")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Succeeded on undoning! or Undo successfully!?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the latter 👍

17 changes: 16 additions & 1 deletion commitizen/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,22 @@ def get_latest_tag_name() -> Optional[str]:
return c.out.strip()


def get_tag_names() -> Optional[List[str]]:
def get_latest_tag() -> Optional[GitTag]:
tags = get_tags()
latest_tag_name = get_latest_tag_name()

if not tags or not latest_tag_name:
return None

if tags[0].name == latest_tag_name:
return tags[0]

tag_names = [tag.name for tag in tags]
latest_tag_index = tag_names.index(latest_tag_name)
return tags[latest_tag_index]


def get_tag_names() -> List[Optional[str]]:
c = cmd.run("git tag --list")
if c.err:
return []
Expand Down
7 changes: 4 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ versions](https://img.shields.io/pypi/pyversions/commitizen.svg?style=flat-squar

---

**Documentation**: https://commitizen-tools.github.io/
**Documentation**: https://commitizen-tools.github.io/commitizen/

---

Expand Down Expand Up @@ -105,7 +105,7 @@ Read more about the `check` command [here](https://commitizen-tools.github.io/co
```bash
$ cz --help
usage: cz [-h] [--debug] [-n NAME] [--version]
{init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
{init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version,undo}
...

Commitizen is a cli tool to generate conventional commits.
Expand All @@ -119,14 +119,15 @@ optional arguments:
--version get the version of the installed commitizen

commands:
{init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
{init,commit,c,ls,example,info,schema,bump,undo,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
undo revert the latest bump or commit
changelog (ch) generate changelog (note that it will overwrite
existing file)
check validates that a commit message matches the commitizen
Expand Down
68 changes: 68 additions & 0 deletions tests/commands/test_undo_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import sys

import pytest

from commitizen import cli, git
from tests.utils import create_file_and_commit


@pytest.mark.usefixtures("tmp_commitizen_project")
def test_undo_commit(config, mocker):
create_file_and_commit("feat: new file")
# We can not revert the first commit, thus we commit twice.
create_file_and_commit("feat: extra file")

testargs = ["cz", "undo", "--commit"]
mocker.patch.object(sys, "argv", testargs)
cli.main()

commits = git.get_commits()

assert len(commits) == 1


def _execute_command(mocker, testargs):
mocker.patch.object(sys, "argv", testargs)
cli.main()


def _undo_bump(mocker, tag_num: int = 0):
testargs = ["cz", "undo", "--bump"]
_execute_command(mocker, testargs)

tags = git.get_tags()
assert len(tags) == tag_num


@pytest.mark.usefixtures("tmp_commitizen_project")
def test_undo_bump(config, mocker):
# MINOR
create_file_and_commit("feat: new file")
_execute_command(mocker, ["cz", "bump", "--yes"])
_undo_bump(mocker)

# PATCH
create_file_and_commit("feat: new file")
_execute_command(mocker, ["cz", "bump", "--yes"])

create_file_and_commit("fix: username exception")
_execute_command(mocker, ["cz", "bump"])
_undo_bump(mocker, 1)

# PRERELEASE
create_file_and_commit("feat: location")
_execute_command(mocker, ["cz", "bump", "--prerelease", "alpha"])
_undo_bump(mocker, 1)

# PRERELEASE BUMP CREATES VERSION WITHOUT PRERELEASE
create_file_and_commit("feat: location")
_execute_command(mocker, ["cz", "bump", "--prerelease", "alpha"])
_execute_command(mocker, ["cz", "bump"])
_undo_bump(mocker, 2)

# MAJOR
create_file_and_commit(
"feat: new user interface\n\nBREAKING CHANGE: age is no longer supported"
)
_execute_command(mocker, ["cz", "bump"])
_undo_bump(mocker, 2)