Esempio n. 1
0
def remove_and_report(config: Config, project: str) -> None:
    shutil.rmtree(path=config.projects_dir.joinpath(project), ignore_errors=True)
    printer.good(f"Deleted {project}")
Esempio n. 2
0
def diff(config: Config, limit: int) -> None:
    """
    Show the difference in local/remote projects.

    Show the projects that you own on GitHub but do not
    have locally.

    The "-l/--limit" flag can be used to limit the number of repos
    returned.

    Examples:

    $ pytoil show diff

    $ pytoil show diff --limit 10
    """
    console = Console()
    api = API(username=config.username, token=config.token)

    local_projects: set[str] = {
        f.name
        for f in config.projects_dir.iterdir()
        if f.is_dir() and not f.name.startswith(".")
    }

    try:
        remote_projects = api.get_repos()
    except httpx.HTTPStatusError as err:
        utils.handle_http_status_error(err)
    else:
        if not remote_projects:
            printer.error("You don't have any projects on GitHub yet!",
                          exits=1)
            return

        remote_names: set[str] = {repo["name"] for repo in remote_projects}
        diff = remote_names.difference(local_projects)

        diff_info: list[dict[str, Any]] = []
        for repo in remote_projects:
            if name := repo.get("name"):
                if name in diff:
                    diff_info.append(repo)

        if not diff:
            printer.good("Your local and remote projects are in sync!")
        else:
            table = Table(box=box.SIMPLE)
            table.add_column("Name", style="bold white")
            table.add_column("Size")
            table.add_column("Created")
            table.add_column("Modified")

            printer.title("Diff: Remote - Local", spaced=False)
            console.print(
                f"[bright_black italic]\nShowing {min(limit, len(diff_info))} out of"
                f" {len(diff_info)} projects [/]")

            for repo in diff_info[:limit]:
                table.add_row(
                    repo["name"],
                    humanize.naturalsize(int(repo["diskUsage"] * 1024)),
                    humanize.naturaltime(
                        datetime.strptime(repo["createdAt"],
                                          GITHUB_TIME_FORMAT),
                        when=datetime.utcnow(),
                    ),
                    humanize.naturaltime(
                        datetime.strptime(repo["pushedAt"],
                                          GITHUB_TIME_FORMAT),
                        when=datetime.utcnow(),
                    ),
                )

            console = Console()
            console.print(table)
Esempio n. 3
0
def pull(config: Config, projects: tuple[str, ...], force: bool,
         all_: bool) -> None:
    """
    Pull down your remote projects.

    The pull command provides easy methods for pulling down remote projects.

    It is effectively a nice wrapper around git clone but you don't have to
    worry about urls or what your cwd is, pull will grab your remote projects
    by name and clone them to your configured projects directory.

    You can also use pull to batch clone multiple repos, even all of them ("--all/-a")
    if you're into that sorta thing.

    If more than 1 repo is passed (or if "--all/-a" is used) pytoil will pull
    the repos concurrently, speeding up the process.

    Any remote project that already exists locally will be skipped and none of
    your local projects are changed in any way. pytoil will only pull down
    those projects that don't already exist locally.

    It's very possible to accidentally clone a lot of repos when using pull so
    you will be prompted for confirmation before pytoil does anything.

    The "--force/-f" flag can be used to override this confirmation prompt if
    desired.

    Examples:

    $ pytoil pull project1 project2 project3

    $ pytoil pull project1 project2 project3 --force

    $ pytoil pull --all

    $ pytoil pull --all --force
    """
    if not projects and not all_:
        printer.error(
            "If not using the '--all' flag, you must specify projects to pull.",
            exits=1)

    api = API(username=config.username, token=config.token)

    local_projects: set[str] = {
        f.name
        for f in config.projects_dir.iterdir()
        if f.is_dir() and not f.name.startswith(".")
    }

    try:
        remote_projects = api.get_repo_names()
    except httpx.HTTPStatusError as err:
        utils.handle_http_status_error(err)
    else:
        if not remote_projects:
            printer.error("You don't have any remote projects to pull.",
                          exits=1)

        specified_remotes = remote_projects if all_ else set(projects)

        # Check for typos
        for project in projects:
            if project not in remote_projects:
                printer.error(
                    f"{project!r} not found on GitHub. Was it a typo?",
                    exits=1)

        diff = specified_remotes.difference(local_projects)
        if not diff:
            printer.good("Your local and remote projects are in sync!",
                         exits=0)

        if not force:
            if len(diff) <= 3:
                message = f"This will pull down {', '.join(diff)}. Are you sure?"
            else:
                # Too many to show nicely
                message = f"This will pull down {len(diff)} projects. Are you sure?"

            confirmed: bool = questionary.confirm(message,
                                                  default=False,
                                                  auto_enter=False).ask()

            if not confirmed:
                printer.warn("Aborted", exits=1)

        # Now we're good to go
        to_clone = [
            Repo(
                owner=config.username,
                name=project,
                local_path=config.projects_dir.joinpath(project),
            ) for project in diff
        ]
        git = Git()
        with ThreadPoolExecutor() as executor:
            for repo in to_clone:
                executor.submit(clone_and_report,
                                repo=repo,
                                git=git,
                                config=config)
Esempio n. 4
0
def interactive_config() -> None:
    """
    Prompt the user with a series of questions
    to configure pytoil interactively.
    """
    printer.warn("No pytoil config file detected!")
    interactive: bool = questionary.confirm(
        "Interactively configure pytoil?", default=False, auto_enter=False
    ).ask()

    if not interactive:
        # User doesn't want to interactively walk through a config file
        # so just make a default and exit cleanly
        Config.helper().write()
        printer.good("I made a default file for you.")
        printer.note(
            f"It's here: {defaults.CONFIG_FILE}, you can edit it with `pytoil"
            " config edit``",
            exits=0,
        )
        return

    # If we get here, the user wants to interactively make the config
    projects_dir: str = questionary.path(
        "Where do you keep your projects?",
        default=str(defaults.PROJECTS_DIR),
        only_directories=True,
    ).ask()

    token: str = questionary.text("GitHub personal access token?").ask()

    username: str = questionary.text("What's your GitHub username?").ask()

    use_editor: bool = questionary.confirm(
        "Auto open projects in an editor?", default=False, auto_enter=False
    ).ask()

    if use_editor:
        editor: str = questionary.text("Name of the editor binary to use?").ask()
    else:
        editor = "None"

    git: bool = questionary.confirm(
        "Make git repos when creating new projects?", default=True, auto_enter=False
    ).ask()

    conda_bin: str = questionary.select(
        "Use conda or mamba for conda environments?",
        choices=("conda", "mamba"),
        default="conda",
    ).ask()

    config = Config(
        projects_dir=Path(projects_dir).resolve(),
        token=token,
        username=username,
        editor=editor,
        conda_bin=conda_bin,
        git=git,
    )

    config.write()

    printer.good("Config created")
    printer.note(f"It's available at {defaults.CONFIG_FILE}.", exits=0)
Esempio n. 5
0
def clone_and_report(repo: Repo, git: Git, config: Config) -> None:
    git.clone(url=repo.clone_url, cwd=config.projects_dir)
    printer.good(f"Cloned {repo.name!r}")