import re import subprocess from pathlib import Path from typing import Optional import typer from wasabi import msg from .. import about from ..util import ensure_path, get_git_version, git_checkout, git_repo_branch_exists from .main import COMMAND, PROJECT_FILE, Arg, Opt, _get_parent_command, app DEFAULT_REPO = about.__projects__ DEFAULT_PROJECTS_BRANCH = about.__projects_branch__ DEFAULT_BRANCHES = ["main", "master"] @app.command("clone") def project_clone_cli( # fmt: off ctx: typer.Context, # This is only used to read the parent command name: str = Arg(..., help="The name of the template to clone"), dest: Optional[Path] = Arg(None, help="Where to clone the project. Defaults to current working directory", exists=False), repo: str = Opt(DEFAULT_REPO, "--repo", "-r", help="The repository to clone from"), branch: Optional[str] = Opt(None, "--branch", "-b", help=f"The branch to clone from. If not provided, will attempt {', '.join(DEFAULT_BRANCHES)}"), sparse_checkout: bool = Opt(False, "--sparse", "-S", help="Use sparse Git checkout to only check out and clone the files needed. Requires Git v22.2+."), # fmt: on ): """Clone a project template from a repository. Calls into "git" and will only download the files from the given subdirectory. The GitHub repo defaults to the official Weasel template repo, but can be customized (including using a private repo). DOCS: https://github.com/explosion/weasel/tree/main/docs/cli.md#clipboard-clone """ if dest is None: dest = Path.cwd() / Path(name).parts[-1] if repo == DEFAULT_REPO and branch is None: branch = DEFAULT_PROJECTS_BRANCH if branch is None: for default_branch in DEFAULT_BRANCHES: if git_repo_branch_exists(repo, default_branch): branch = default_branch break if branch is None: default_branches_msg = ", ".join(f"'{b}'" for b in DEFAULT_BRANCHES) msg.fail( "No branch provided and attempted default " f"branches {default_branches_msg} do not exist.", exits=1, ) else: if not git_repo_branch_exists(repo, branch): msg.fail(f"repo: {repo} (branch: {branch}) does not exist.", exits=1) assert isinstance(branch, str) parent_command = _get_parent_command(ctx) project_clone( name, dest, repo=repo, branch=branch, sparse_checkout=sparse_checkout, parent_command=parent_command, ) def project_clone( name: str, dest: Path, *, repo: str = about.__projects__, branch: str = about.__projects_branch__, sparse_checkout: bool = False, parent_command: str = COMMAND, ) -> None: """Clone a project template from a repository. name (str): Name of subdirectory to clone. dest (Path): Destination path of cloned project. repo (str): URL of Git repo containing project templates. branch (str): The branch to clone from """ dest = ensure_path(dest) check_clone(name, dest, repo) project_dir = dest.resolve() repo_name = re.sub(r"(http(s?)):\/\/github.com/", "", repo) try: git_checkout(repo, name, dest, branch=branch, sparse=sparse_checkout) except subprocess.CalledProcessError: err = f"Could not clone '{name}' from repo '{repo_name}' (branch '{branch}')" msg.fail(err, exits=1) msg.good(f"Cloned '{name}' from '{repo_name}' (branch '{branch}')", project_dir) if not (project_dir / PROJECT_FILE).exists(): msg.warn(f"No {PROJECT_FILE} found in directory") else: msg.good("Your project is now ready!") print(f"To fetch the assets, run:\n{parent_command} assets {dest}") def check_clone(name: str, dest: Path, repo: str) -> None: """Check and validate that the destination path can be used to clone. Will check that Git is available and that the destination path is suitable. name (str): Name of the directory to clone from the repo. dest (Path): Local destination of cloned directory. repo (str): URL of the repo to clone from. """ git_err = ( f"Cloning Weasel project templates requires Git and the 'git' command. " f"To clone a project without Git, copy the files from the '{name}' " f"directory in the {repo} to {dest} manually." ) get_git_version(error=git_err) if not dest: msg.fail(f"Not a valid directory to clone project: {dest}", exits=1) if dest.exists(): # Directory already exists (not allowed, clone needs to create it) msg.fail(f"Can't clone project, directory already exists: {dest}", exits=1) if not dest.parent.exists(): # We're not creating parents, parent dir should exist msg.fail( f"Can't clone project, parent directory doesn't exist: {dest.parent}. " f"Create the necessary folder(s) first before continuing.", exits=1, )