Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
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
Ejemplo n.º 5
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)
Ejemplo n.º 6
0
	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}")
Ejemplo n.º 7
0
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))
Ejemplo n.º 8
0
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!
""")
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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