def create_file_and_commit(message: str, filename: Optional[str] = None): if not filename: filename = str(uuid.uuid4()) Path(f"./{filename}").touch() cmd.run("git add .") git.commit(message)
def get_commits( start: Optional[str] = None, end: str = "HEAD", *, log_format: str = "%H%n%s%n%an%n%ae%n%b", delimiter: str = "----------commit-delimiter----------", args: str = "", ) -> List[GitCommit]: """Get the commits between start and end.""" git_log_cmd = f"git log --pretty={log_format}{delimiter} {args}" 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 [] git_commits = [] for rev_and_commit in c.out.split(f"{delimiter}\n"): if not rev_and_commit: continue rev, title, author, author_email, *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(), author=author, author_email=author_email, ) git_commits.append(git_commit) return git_commits
def test_get_latest_tag_name(tmp_commitizen_project): with tmp_commitizen_project.as_cwd(): tag_name = git.get_latest_tag_name() assert tag_name is None create_file_and_commit("feat(test): test") cmd.run("git tag 1.0") tag_name = git.get_latest_tag_name() assert tag_name == "1.0"
def test_bump_command(mocker, create_project): with open("./pyproject.toml", "w") as f: f.write("[tool.commitizen]\n" 'version="0.1.0"') cmd.run("git init") # MINOR create_file_and_commit("feat: new file") testargs = ["cz", "bump", "--yes"] mocker.patch.object(sys, "argv", testargs) cli.main() tag_exists = git.tag_exist("0.2.0") assert tag_exists is True # PATCH create_file_and_commit("fix: username exception") testargs = ["cz", "bump"] mocker.patch.object(sys, "argv", testargs) cli.main() tag_exists = git.tag_exist("0.2.1") assert tag_exists is True # PRERELEASE create_file_and_commit("feat: location") testargs = ["cz", "bump", "--prerelease", "alpha"] mocker.patch.object(sys, "argv", testargs) cli.main() tag_exists = git.tag_exist("0.3.0a0") assert tag_exists is True # PRERELEASE BUMP CREATES VERSION WITHOUT PRERELEASE testargs = ["cz", "bump"] mocker.patch.object(sys, "argv", testargs) cli.main() tag_exists = git.tag_exist("0.3.0") assert tag_exists is True # MAJOR create_file_and_commit( "feat: new user interface\n\nBREAKING CHANGE: age is no longer supported" ) testargs = ["cz", "bump"] mocker.patch.object(sys, "argv", testargs) cli.main() tag_exists = git.tag_exist("1.0.0") assert tag_exists is True
def test_is_staging_clean_when_adding_file(tmp_commitizen_project): with tmp_commitizen_project.as_cwd(): assert git.is_staging_clean() is True cmd.run("touch test_file") assert git.is_staging_clean() is True cmd.run("git add test_file") assert git.is_staging_clean() is False
def get_commits(start: str, end: str = "HEAD", from_beginning: bool = False) -> list: 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 not c.out: return [] return c.out.split("\n")
def test_is_staging_clean_when_updating_file(tmp_commitizen_project): with tmp_commitizen_project.as_cwd(): assert git.is_staging_clean() is True cmd.run("touch test_file") cmd.run("git add test_file") cmd.run("git commit -m 'add test_file'") cmd.run("echo 'test' > test_file") assert git.is_staging_clean() is True cmd.run("git add test_file") assert git.is_staging_clean() is False
def test_bump_on_git_with_hooks_no_verify_enabled(mocker): cmd.run("mkdir .git/hooks") with open(".git/hooks/pre-commit", "w") as f: f.write("#!/usr/bin/env bash\n" 'echo "0.1.0"') cmd.run("chmod +x .git/hooks/pre-commit") # MINOR create_file_and_commit("feat: new file") testargs = ["cz", "bump", "--yes", "--no-verify"] mocker.patch.object(sys, "argv", testargs) cli.main() tag_exists = git.tag_exist("0.2.0") assert tag_exists is True
def commit(message: str, args: str = ""): f = NamedTemporaryFile("wb", delete=False) f.write(message.encode("utf-8")) f.close() c = cmd.run(f"git commit {args} -F {f.name}") os.unlink(f.name) return c
def test_bump_on_git_with_hooks_no_verify_disabled(mocker): cmd.run("mkdir .git/hooks") with open(".git/hooks/pre-commit", "w") as f: f.write("#!/usr/bin/env bash\n" 'echo "0.1.0"') cmd.run("chmod +x .git/hooks/pre-commit") # MINOR create_file_and_commit("feat: new file") testargs = ["cz", "bump", "--yes"] mocker.patch.object(sys, "argv", testargs) with pytest.raises(BumpCommitFailedError) as excinfo: cli.main() assert 'git.commit error: "0.1.0"' in str(excinfo.value)
def test_bump_tag_exists_raises_exception(mocker): cmd.run("mkdir .git/hooks") with open(".git/hooks/post-commit", "w") as f: f.write("#!/usr/bin/env bash\n" "exit 9") cmd.run("chmod +x .git/hooks/post-commit") # MINOR create_file_and_commit("feat: new file") git.tag("0.2.0") testargs = ["cz", "bump", "--yes"] mocker.patch.object(sys, "argv", testargs) with pytest.raises(BumpTagFailedError) as excinfo: cli.main() assert "0.2.0" in str(excinfo.value) # This should be a fatal error
def test_bump_on_git_with_hooks_no_verify_disabled(mocker, capsys): cmd.run("mkdir .git/hooks") with open(".git/hooks/pre-commit", "w") as f: f.write("#!/usr/bin/env bash\n" 'echo "0.1.0"') cmd.run("chmod +x .git/hooks/pre-commit") # MINOR create_file_and_commit("feat: new file") testargs = ["cz", "bump", "--yes"] mocker.patch.object(sys, "argv", testargs) with pytest.raises(SystemExit): cli.main() _, err = capsys.readouterr() assert 'git.commit errror: "0.1.0"' in err
def test_bump_minor_increment_annotated(commit_msg, mocker): create_file_and_commit(commit_msg) testargs = ["cz", "bump", "--yes", "--annotated-tag"] mocker.patch.object(sys, "argv", testargs) cli.main() tag_exists = git.tag_exist("0.2.0") cmd_res = cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"') assert tag_exists is True and "tag:refs/tags/0.2.0\n" in cmd_res.out
def test_bump_when_bumpping_is_not_support(mocker, capsys, 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") create_file_and_commit( "feat: new user interface\n\nBREAKING CHANGE: age is no longer supported" ) testargs = ["cz", "-n", "cz_jira", "bump", "--yes"] mocker.patch.object(sys, "argv", testargs) with pytest.raises(SystemExit): cli.main() _, err = capsys.readouterr() assert "'cz_jira' rule does not support bump" in err
def test_get_commits_with_signature(): config_file = ".git/config" config_backup = ".git/config.bak" shutil.copy(config_file, config_backup) try: # temporarily turn on --show-signature cmd.run("git config log.showsignature true") # retrieve a commit that we know has a signature commit = git.get_commits( start="bec20ebf433f2281c70f1eb4b0b6a1d0ed83e9b2", end="9eae518235d051f145807ddf971ceb79ad49953a", )[0] assert commit.title.startswith("fix") finally: # restore the repo's original config shutil.move(config_backup, config_file)
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 test_bump_minor_increment_annotated_config_file(commit_msg, mocker, tmp_commitizen_project): tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") tmp_commitizen_cfg_file.write(f"{tmp_commitizen_cfg_file.read()}\n" f"annotated_tag = 1") create_file_and_commit(commit_msg) testargs = ["cz", "bump", "--yes"] mocker.patch.object(sys, "argv", testargs) cli.main() tag_exists = git.tag_exist("0.2.0") cmd_res = cmd.run( 'git for-each-ref refs/tags --format "%(objecttype):%(refname)"') assert tag_exists is True and "tag:refs/tags/0.2.0\n" in cmd_res.out
def _install_pre_commit_hook(self): pre_commit_config_filename = ".pre-commit-config.yaml" cz_hook_config = { "repo": "https://github.com/commitizen-tools/commitizen", "rev": f"v{__version__}", "hooks": [{ "id": "commitizen", "stages": ["commit-msg"] }], } config_data = {} if not os.path.isfile(pre_commit_config_filename): # .pre-commit-config does not exist config_data["repos"] = [cz_hook_config] else: # breakpoint() with open(pre_commit_config_filename) as config_file: yaml_data = yaml.safe_load(config_file) if yaml_data: config_data = yaml_data if "repos" in config_data: for pre_commit_hook in config_data["repos"]: if "commitizen" in pre_commit_hook["repo"]: out.write("commitizen already in pre-commit config") break else: config_data["repos"].append(cz_hook_config) else: # .pre-commit-config exists but there's no "repos" key config_data["repos"] = [cz_hook_config] with open(pre_commit_config_filename, "w") as config_file: yaml.safe_dump(config_data, stream=config_file) c = cmd.run("pre-commit install --hook-type commit-msg") if c.return_code == 127: out.error( "pre-commit is not installed in current environement.\n" "Run 'pre-commit install --hook-type commit-msg' again after it's installed" ) elif c.return_code != 0: out.error(c.err) else: out.write( "commitizen pre-commit hook is now installed in your '.git'\n")
def test_bump_pre_commit_changelog_fails_always(tmp_commitizen_project, mocker, freezer, retry): freezer.move_to("2022-04-01") testargs = ["cz", "bump", "--changelog", "--yes"] if retry: testargs.append("--retry") mocker.patch.object(sys, "argv", testargs) with tmp_commitizen_project.as_cwd(): Path(".pre-commit-config.yaml").write_text(""" repos: - repo: local hooks: - id: forbid-changelog name: changelogs are forbidden entry: changelogs are forbidden language: fail files: CHANGELOG.md """) cmd.run("git add -A") cmd.run("git commit -m 'feat: forbid changelogs'") cmd.run("pre-commit install") with pytest.raises(exceptions.BumpCommitFailedError): cli.main()
def test_bump_pre_commit_changelog(tmp_commitizen_project, mocker, freezer, retry): freezer.move_to("2022-04-01") testargs = ["cz", "bump", "--changelog", "--yes"] if retry: testargs.append("--retry") else: pytest.xfail( "it will fail because pre-commit will reformat CHANGELOG.md") mocker.patch.object(sys, "argv", testargs) with tmp_commitizen_project.as_cwd(): # Configure prettier as a pre-commit hook Path(".pre-commit-config.yaml").write_text(""" repos: - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.6.2 hooks: - id: prettier stages: [commit] """) # Prettier inherits editorconfig Path(".editorconfig").write_text(""" [*] indent_size = 4 """) cmd.run("git add -A") cmd.run("git commit -m 'fix: _test'") cmd.run("pre-commit install") cli.main() # Pre-commit fixed last line adding extra indent and "\" char assert Path("CHANGELOG.md").read_text() == dedent("""\ ## 0.1.1 (2022-04-01) ### Fix - \\_test """)
def __call__(self): # noqa: C901 """Steps executed to bump.""" try: current_version_instance: Version = Version(self.bump_settings["version"]) except TypeError: raise NoVersionSpecifiedError() # Initialize values from sources (conf) current_version: str = self.config.settings["version"] tag_format: str = self.bump_settings["tag_format"] bump_commit_message: str = self.bump_settings["bump_message"] version_files: List[str] = self.bump_settings["version_files"] dry_run: bool = self.arguments["dry_run"] is_yes: bool = self.arguments["yes"] increment: Optional[str] = self.arguments["increment"] prerelease: str = self.arguments["prerelease"] is_files_only: Optional[bool] = self.arguments["files_only"] is_local_version: Optional[bool] = self.arguments["local_version"] current_tag_version: str = bump.create_tag( current_version, tag_format=tag_format ) is_initial = self.is_initial_tag(current_tag_version, is_yes) 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. if not commits and not current_version_instance.is_prerelease: raise NoCommitsFoundError("[NO_COMMITS_FOUND]\n" "No new commits found.") if increment is None: increment = self.find_increment(commits) # Increment is removed when current and next version # are expected to be prereleases. if prerelease and current_version_instance.is_prerelease: increment = None new_version = bump.generate_version( current_version, increment, prerelease=prerelease, is_local_version=is_local_version, ) new_tag_version = bump.create_tag(new_version, tag_format=tag_format) message = bump.create_commit_message( current_version, new_version, bump_commit_message ) # Report found information out.write( f"{message}\n" f"tag to create: {new_tag_version}\n" f"increment detected: {increment}\n" ) if increment is None and new_tag_version == current_tag_version: raise NoneIncrementExit() # Do not perform operations over files or git. if dry_run: raise DryRunExit() bump.update_version_in_files( current_version, str(new_version), version_files, check_consistency=self.check_consistency, ) if self.changelog: changelog_cmd = Changelog( self.config, { "unreleased_version": new_tag_version, "incremental": True, "dry_run": dry_run, }, ) changelog_cmd() c = cmd.run(f"git add {changelog_cmd.file_name}") self.config.set_key("version", str(new_version)) if is_files_only: raise ExpectedExit() c = git.commit(message, args=self._get_commit_args()) if c.return_code != 0: raise BumpCommitFailedError(f'git.commit error: "{c.err.strip()}"') c = git.tag(new_tag_version) if c.return_code != 0: raise BumpTagFailedError(c.err) out.success("Done!")
def tag(tag: str, annotated: bool = False): c = cmd.run( f"git tag -a {tag} -m {tag}" if annotated else f"git tag {tag}") return c
def is_git_project() -> bool: c = cmd.run("git rev-parse --is-inside-work-tree") if c.out.strip() == "true": return True return False
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 find_git_project_root() -> Optional[Path]: c = cmd.run("git rev-parse --show-toplevel") if not c.err: return Path(c.out.strip()) return None
def get_tag_names() -> List[Optional[str]]: c = cmd.run("git tag --list") if c.err: return [] return [tag.strip() for tag in c.out.split("\n") if tag.strip()]
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 tag_exist(tag: str) -> bool: c = cmd.run(f"git tag --list {tag}") return tag in c.out
def tmp_git_project(tmpdir): with tmpdir.as_cwd(): cmd.run("git init") yield tmpdir
def tag(tag: str): c = cmd.run(f"git tag {tag}") return c