def version(quiet: bool = False) -> None: """ Show the repository version. """ # 3rd party from domdf_python_tools.paths import PathPlus from southwark import get_tags from southwark.repo import Repo # this package from repo_helper.core import RepoHelper rh = RepoHelper(PathPlus.cwd()) rh.load_settings(allow_unknown_keys=True) version = rh.templates.globals["version"] if quiet: click.echo(f"v{version}") else: click.echo(f"Current version: v{version}") repo = Repo(rh.target_repo) for sha, tag in get_tags(repo).items(): if tag == f"v{version}": walker = repo.get_walker() for idx, entry in enumerate(walker): commit_id = entry.commit.id.decode("UTF-8") if commit_id == sha: click.echo( f"{idx} commit{'s' if idx > 1 else ''} since that release." ) break break
def temp_empty_repo(tmp_pathplus, monkeypatch) -> Repo: """ Pytest fixture to return an empty git repository in a temporary location. :data:`repo_helper.utils.today` is monkeypatched to return 25th July 2020. """ # Monkeypatch dulwich so it doesn't try to use the global config. monkeypatch.setattr(StackedConfig, "default_backends", lambda *args: [], raising=True) monkeypatch.setenv("GIT_COMMITTER_NAME", "Guido") monkeypatch.setenv("GIT_COMMITTER_EMAIL", "*****@*****.**") monkeypatch.setenv("GIT_AUTHOR_NAME", "Guido") monkeypatch.setenv("GIT_AUTHOR_EMAIL", "*****@*****.**") monkeypatch.setattr(repo_helper.utils, "today", FAKE_DATE) repo_dir = tmp_pathplus / secrets.token_hex(8) if sys.platform == "linux": repo_dir /= "%%tmp" repo_dir.maybe_make(parents=True) repo: Repo = Repo.init(repo_dir) return repo
def log(entries: Optional[int] = None, reverse: bool = False, from_date: Optional[datetime] = None, from_tag: Optional[str] = None, colour: Optional[bool] = None, no_pager: bool = False) -> int: """ Show git commit log. """ # 3rd party from consolekit.terminal_colours import resolve_color_default from consolekit.utils import abort from domdf_python_tools.paths import PathPlus from southwark.log import Log from southwark.repo import Repo repo = Repo(PathPlus.cwd()) try: commit_log = Log(repo).log(max_entries=entries, reverse=reverse, from_date=from_date, from_tag=from_tag) except ValueError as e: raise abort(f"ERROR: {e}") if no_pager: click.echo(commit_log, color=resolve_color_default(colour)) else: click.echo_via_pager(commit_log, color=resolve_color_default(colour)) return 0
def new(self, org: bool = False) -> int: """ Create a new GitHub repository for this project. :param org: Whether the repository should be created for the organization set as ``username``, or for the authenticated user (default). :rtype: .. versionchanged:: 0.3.0 * Removed the ``verbose`` option. Provide it to the class constructor instead. * Added the ``org`` argument. """ with self.echo_rate_limit(): user = self.get_org_or_user(org) repo_name = self.templates.globals["repo_name"] repo: Optional[repos.Repository] if org: repo = user.create_repository(repo_name, **self.get_repo_kwargs()) else: repo = self.github.create_repository(repo_name, **self.get_repo_kwargs()) if repo is None: raise ErrorCreatingRepository(user.login, repo_name, org=org) self.update_topics(repo) click.echo(f"Success! View the repository online at {repo.html_url}") try: dulwich_repo = Repo(self.target_repo) except NotGitRepository: return 0 config = dulwich_repo.get_config() config.set(("remote", "origin"), "url", repo.ssh_url.encode("UTF-8")) config.set(("remote", "origin"), "fetch", b"+refs/heads/*:refs/remotes/origin/*") config.write_to_path() fetch(dulwich_repo, remote_location="origin") return 0
def clone(url: str, dest: PathLike) -> Repo: """ Clones the given URL and returns the :class:`southwark.repo.Repo` object representing it. :param url: :param dest: """ process = Popen(["git", "clone", url, dest]) process.communicate() process.wait() return Repo(dest)
def bump(self, new_version: Version, commit: Optional[bool], message: str): """ Bump to the given version. :param new_version: :param commit: Whether to commit automatically (:py:obj:`True`) or ask first (:py:obj:`None`). :param message: The commit message. .. versionchanged:: 2021.8.11 Now takes a :class:`packaging.version.Version` rather than a :class:`domdf_python_tools.versions.Version`. """ new_version_str = str(new_version) dulwich_repo = Repo(self.repo.target_repo) if f"v{new_version_str}".encode("UTF-8") in dulwich_repo.refs.as_dict(b"refs/tags"): raise abort(f"The tag 'v{new_version_str}' already exists!") bumpversion_config = self.get_bumpversion_config(str(self.current_version), new_version_str) changed_files = [self.bumpversion_file.relative_to(self.repo.target_repo).as_posix()] for filename in bumpversion_config.keys(): if not os.path.isfile(filename): raise FileNotFoundError(filename) for filename, config in bumpversion_config.items(): self.bump_version_for_file(filename, config) changed_files.append(filename) # Update number in .bumpversion.cfg bv = ConfigUpdater() bv.read(self.bumpversion_file) bv["bumpversion"]["current_version"] = new_version_str self.bumpversion_file.write_clean(str(bv)) commit_message = message.format(current_version=self.current_version, new_version=new_version) click.echo(commit_message) if commit_changed_files( self.repo.target_repo, managed_files=changed_files, commit=commit, message=commit_message.encode("UTF-8"), enable_pre_commit=False, ): tag_create(dulwich_repo, f"v{new_version_str}")
def changelog( entries: Optional[int] = None, reverse: bool = False, colour: Optional[bool] = None, no_pager: bool = False, ): """ Show commits since the last version tag. """ # 3rd party from consolekit.terminal_colours import resolve_color_default from consolekit.utils import abort from domdf_python_tools.paths import PathPlus from southwark.log import Log from southwark.repo import Repo # this package from repo_helper.core import RepoHelper rh = RepoHelper(PathPlus.cwd()) rh.load_settings(allow_unknown_keys=True) repo = Repo(rh.target_repo) try: commit_log = Log(repo).log( max_entries=entries, reverse=reverse, from_tag=f"v{rh.templates.globals['version']}", ) except ValueError as e: raise abort(f"ERROR: {e}") if no_pager: click.echo(commit_log, color=resolve_color_default(colour)) else: click.echo_via_pager(commit_log, color=resolve_color_default(colour))
def wizard() -> None: """ Run the wizard 🧙 to create a 'repo_helper.yml' file. """ # stdlib import datetime import getpass import os import socket # 3rd party from apeye.email_validator import EmailSyntaxError, validate_email from consolekit.terminal_colours import Fore from domdf_python_tools.paths import PathPlus from dulwich.errors import NotGitRepository from ruamel.yaml import scalarstring from southwark.repo import Repo # this package from repo_helper.utils import _round_trip_dump, license_lookup path = PathPlus.cwd() config_file = path / "repo_helper.yml" try: r = Repo(path) except NotGitRepository: with Fore.RED: click.echo(f"The directory {path} is not a git repository.") click.echo( "You may need to run 'git init' in that directory first.") raise click.Abort # ---------- intro ---------- click.echo( "This wizard 🧙will guide you through creating a 'repo_helper.yml' configuration file." ) click.echo(f"This will be created in '{config_file}'.") if not confirm("Do you want to continue?"): raise click.Abort() # ---------- file exists warning ---------- if config_file.is_file(): click.echo( f"\nWoah! That file already exists. It will be overwritten if you continue!" ) if not confirm("Are you sure you want to continue?"): raise click.Abort() click.echo("\nDefault options are indicated in [square brackets].") # ---------- modname ---------- click.echo("\nThe name of the library/project.") modname = prompt("Name") # ---------- name ---------- click.echo("\nThe name of the author.") click.echo("The author is usually the person who wrote the library.") git_config = r.get_config_stack() try: default_author = git_config.get(("user", ), "name").decode("UTF-8") except KeyError: try: getpass_user = getpass.getuser() default_author = os.getenv( "GIT_AUTHOR_NAME", default=os.getenv("GIT_COMMITTER_NAME", default=getpass_user), ) except ImportError: # Usually USERNAME is not set when trying getpass.getuser() default_author = '' author = prompt("Name", default=default_author) # ---------- email ---------- try: default_email = git_config.get(("user", ), "email").decode("UTF-8") except KeyError: default_email = os.getenv( "GIT_AUTHOR_EMAIL", default=os.getenv("GIT_COMMITTER_EMAIL", default=f"{author}@{socket.gethostname()}")) click.echo( "\nThe email address of the author. This will be shown on PyPI, amongst other places." ) while True: try: email = validate_email(prompt("Email", default=default_email)).email break except EmailSyntaxError: click.echo("That is not a valid email address.") # ---------- username ---------- click.echo("\nThe username of the author.") click.echo( "(repo_helper naïvely assumes that you use the same username on GitHub as on other sites.)" ) username = prompt("Username", default=author) # TODO: validate username # ---------- version ---------- click.echo("\nThe version number of the library, in semver format.") version = prompt("Version number", default="0.0.0") # ---------- copyright_years ---------- click.echo("\nThe copyright years for the library.") copyright_years = prompt("Copyright years", default=str(datetime.datetime.today().year), type=str) # ---------- license_ ---------- click.echo(""" The SPDX identifier for the license this library is distributed under. Not all SPDX identifiers are allowed as not all map to PyPI Trove classifiers.""" ) while True: license_ = prompt("License") if license_ in license_lookup: break else: click.echo("That is not a valid identifier.") # ---------- short_desc ---------- click.echo("\nEnter a short, one-line description for the project.") short_desc = prompt("Description") # ---------- writeout ---------- data = { "modname": modname, "copyright_years": copyright_years, "author": author, "email": email, "username": username, "version": str(version), "license": license_, "short_desc": short_desc, } data = { k: scalarstring.SingleQuotedScalarString(v) for k, v in data.items() } config_file.write_lines([ "# Configuration for 'repo_helper' (https://github.com/repo-helper/repo_helper)", "---", _round_trip_dump(data), "enable_conda: false", ]) click.echo(f""" The options you provided have been written to the file {config_file}. You can configure additional options in that file. The schema for the Yaml file can be found at: https://github.com/repo-helper/repo_helper/blob/master/repo_helper/repo_helper_schema.json You may be able to configure your code editor to validate your configuration file against that schema. repo_helper can now be run with the 'repo_helper' command in the repository root. Be seeing you! """)
def commit_changed_files( repo_path: PathLike, managed_files: Iterable[PathLike], commit: Optional[bool] = None, message: bytes = b"Updated files with 'repo_helper'.", enable_pre_commit: bool = True, ) -> bool: """ Stage and commit any files that have been updated, added or removed. :param repo_path: The path to the repository root. :param managed_files: List of files managed by ``repo_helper``. :param commit: Whether to commit the changes automatically. :py:obj:`None` (default) indicates the user should be asked. :param message: The commit message to use. Default ``"Updated files with 'repo_helper'."`` :param enable_pre_commit: Whether to install and configure pre-commit. Default :py:obj`True`. :returns: :py:obj:`True` if the changes were committed. :py:obj:`False` otherwise. """ # this package from repo_helper.utils import commit_changes, sort_paths, stage_changes repo_path = PathPlus(repo_path).absolute() r = Repo(str(repo_path)) staged_files = stage_changes(r.path, managed_files) # Ensure pre-commit hooks are installed if enable_pre_commit and platform.system() == "Linux": with in_directory(repo_path), suppress(ImportError): # 3rd party import pre_commit.main # type: ignore pre_commit.main.main(["install"]) if staged_files: click.echo("\nThe following files will be committed:") # Sort staged_files and put directories first for staged_filename in sort_paths(*staged_files): click.echo(f" {staged_filename.as_posix()!s}") click.echo() if commit is None: commit = confirm("Commit?", default=True) if commit: if enable_pre_commit or "pre-commit" in r.hooks: # Ensure the working directory for pre-commit is correct r.hooks["pre-commit"].cwd = str( repo_path.absolute()) # type: ignore try: commit_id = commit_changes(r, message.decode("UTF-8")) click.echo(f"Committed as {commit_id}") return True except CommitError as e: click.echo(f"Unable to commit: {e}", err=True) else: click.echo("Changed files were staged but not committed.") else: click.echo("Nothing to commit") return False
def run_repo_helper( path, force: bool, initialise: bool, commit: Optional[bool], message: str, enable_pre_commit: bool = True, ) -> int: """ Run repo_helper. :param path: The repository path. :param force: Whether to force the operation if the repository is not clean. :param initialise: Whether to initialise the repository. :param commit: Whether to commit unchanged files. :param message: The commit message. :param enable_pre_commit: Whether to install and configure pre-commit. Default :py:obj`True`. """ # this package from repo_helper.cli.commands.init import init_repo from repo_helper.core import RepoHelper from repo_helper.utils import easter_egg try: rh = RepoHelper(path) rh.load_settings() except FileNotFoundError as e: error_block = textwrap.indent(str(e), '\t') raise abort( f"Unable to run 'repo_helper'.\nThe error was:\n{error_block}") if not assert_clean(rh.target_repo, allow_config=("repo_helper.yml", "git_helper.yml")): if force: click.echo(Fore.RED("Proceeding anyway"), err=True) else: return 1 if initialise: r = Repo(rh.target_repo) for filename in init_repo(rh.target_repo, rh.templates): r.stage(os.path.normpath(filename)) managed_files = rh.run() try: commit_changed_files( repo_path=rh.target_repo, managed_files=managed_files, commit=commit, message=message.encode("UTF-8"), enable_pre_commit=enable_pre_commit, ) except CommitError as e: indented_error = '\n'.join(f"\t{line}" for line in textwrap.wrap(str(e))) click.echo( f"Unable to commit changes. The error was:\n\n{indented_error}", err=True) return 1 easter_egg() return 0