Esempio n. 1
0
def migrate_repos(
    template_repo_urls: plug.types.SizedIterable[str], api: plug.PlatformAPI
) -> None:
    """Migrate a repository from an arbitrary URL to the target organization.
    The new repository is added to the master_repos team, which is created if
    it does not already exist.

    Args:
        template_repo_urls: Local urls to repos to migrate.
        api: An implementation of :py:class:`repobee_plug.PlatformAPI` used to
            interface with the platform (e.g. GitHub or GitLab) instance.
    """
    local_templates = [
        plug.TemplateRepo(name=urlutil.extract_repo_name(url), url=url)
        for url in template_repo_urls
    ]
    create_repo_it = plug.cli.io.progress_bar(
        (
            _create_or_fetch_repo(
                local.name, description="", private=True, api=api
            )
            for local in local_templates
        ),
        desc="Creating remote repos",
        total=len(template_repo_urls),
    )
    with tempfile.TemporaryDirectory() as tmpdir:
        workdir = pathlib.Path(tmpdir)
        _clone_all(local_templates, cwd=workdir, api=api)

        remote_templates = [
            plug.TemplateRepo(
                name=repo.name, url=repo.url, _path=workdir / repo.name
            )
            for _, repo in create_repo_it
        ]

        git.push(
            [
                PushSpec(
                    local_path=template_repo.path,
                    repo_url=api.insert_auth(template_repo.url),
                    branch=git.active_branch(template_repo.path),
                )
                for template_repo in remote_templates
            ]
        )

    plug.echo("Done!")
Esempio n. 2
0
    def test_tries_all_calls_when_repos_up_to_date(self, env_setup,
                                                   push_tuples, aio_subproc):
        aio_subproc.process.stderr = b"Everything up-to-date"

        expected_calls = [
            call(
                *f"git push {pt.repo_url}".split(),
                pt.branch,
                cwd=os.path.abspath(pt.local_path),
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            ) for pt in push_tuples
        ]

        git.push(push_tuples)

        aio_subproc.create_subprocess.assert_has_calls(expected_calls)
Esempio n. 3
0
def setup_student_repos(
    template_repo_urls: Iterable[str],
    teams: Iterable[plug.StudentTeam],
    api: plug.PlatformAPI,
) -> Mapping[str, List[plug.Result]]:
    """Setup student repositories based on master repo templates. Performs three
    primary tasks:

        1. Create the specified teams on the target platform and add the
        specified members to their teams. If a team already exists, it is left
        as-is. If a student is already in a team they are assigned to, nothing
        happens. If no account exists for some specified username, that
        particular student is ignored, but any associated teams are still
        created (even if a missing user is the only member of that team).

        2. For each master repository, create one student repo per team and add
        it to the corresponding student team. If a repository already exists,
        it is skipped.

        3. Push files from the master repos to the corresponding student repos.

    Args:
        template_repo_urls: URLs to master repos.
        teams: An iterable of student teams specifying the teams to be setup.
        api: An implementation of :py:class:`repobee_plug.PlatformAPI` used to
            interface with the platform (e.g. GitHub or GitLab) instance.
    """
    teams = list(teams)

    with tempfile.TemporaryDirectory() as tmpdir:
        workdir = pathlib.Path(tmpdir)
        template_repos = [
            plug.TemplateRepo(
                name=urlutil.extract_repo_name(url),
                url=url,
                _path=workdir / api.extract_repo_name(url),
            )
            for url in template_repo_urls
        ]

        plug.log.info("Cloning into master repos ...")
        _clone_all(template_repos, cwd=workdir, api=api)
        pre_setup_results = plugin.execute_setup_tasks(
            template_repos, api, cwd=pathlib.Path(tmpdir)
        )

        platform_teams = _create_platform_teams(teams, api)

        to_push, preexisting = _create_state_separated_push_tuples(
            platform_teams, template_repos, api
        )
        successful_pts, _ = git.push(push_tuples=to_push)

        post_setup_results = _execute_post_setup_hook(
            successful_pts, preexisting, api
        )

    return _combine_dicts(pre_setup_results, post_setup_results)
Esempio n. 4
0
def update_student_repos(
    template_repo_urls: plug.types.SizedIterable[str],
    teams: plug.types.SizedIterable[plug.StudentTeam],
    api: plug.PlatformAPI,
    issue: Optional[plug.Issue] = None,
) -> Mapping[str, List[plug.Result]]:
    """Attempt to update all student repos related to one of the master repos.

    Args:
        template_repo_urls: URLs to master repos. Must be in the organization
            that the api is set up for.
        teams: An iterable of student teams.
        api: An implementation of :py:class:`repobee_plug.PlatformAPI` used to
            interface with the platform (e.g. GitHub or GitLab) instance.
        issue: An optional issue to open in repos to which pushing fails.
    """
    if len(set(template_repo_urls)) != len(template_repo_urls):
        raise ValueError("template_repo_urls contains duplicates")

    with tempfile.TemporaryDirectory() as tmpdir:
        workdir = pathlib.Path(tmpdir)
        template_repos = [
            plug.TemplateRepo(
                name=urlutil.extract_repo_name(url),
                url=url,
                _path=workdir / api.extract_repo_name(url),
            )
            for url in template_repo_urls
        ]

        plug.log.info("Cloning into master repos ...")
        _clone_all(template_repos, cwd=workdir, api=api)
        hook_results = plugin.execute_setup_tasks(
            template_repos, api, cwd=pathlib.Path(tmpdir)
        )

        push_tuple_iter = _create_update_push_tuples(
            teams, template_repos, api
        )
        push_tuple_iter_progress = plug.cli.io.progress_bar(
            push_tuple_iter,
            desc="Setting up student repos",
            total=len(teams) * len(template_repos),
        )
        successful_pts, failed_pts = git.push(
            push_tuples=push_tuple_iter_progress
        )

    if failed_pts and issue:
        plug.echo("Opening issue in repos to which push failed")
        urls_without_auth = [
            re.sub("https://.*?@", "https://", pt.repo_url)
            for pt in failed_pts
        ]
        _open_issue_by_urls(urls_without_auth, issue, api)

    plug.log.info("Done!")
    return hook_results
Esempio n. 5
0
    def test_stops_retrying_when_failed_pushes_succeed(self, env_setup,
                                                       push_tuples, mocker):
        tried = False
        fail_pt = push_tuples[1]

        async def raise_once(pt):
            nonlocal tried
            if not tried and pt == fail_pt:
                tried = True
                raise exception.PushFailedError("Push failed", 128,
                                                b"some error", pt.repo_url)

        expected_num_calls = len(push_tuples) + 1  # one retry

        async def raise_(pt):
            raise exception.PushFailedError("Push failed", 128, b"some error",
                                            pt.repo_url)

        async_push_mock = mocker.patch("_repobee.git._push_async",
                                       side_effect=raise_once)

        git.push(push_tuples, tries=10)

        assert len(async_push_mock.call_args_list) == expected_num_calls
Esempio n. 6
0
    def test(self, env_setup, push_tuples, aio_subproc):
        """Test that push works as expected when no exceptions are thrown by
        tasks.
        """
        expected_calls = [
            call(
                *f"git push {url} {branch}".split(),
                cwd=os.path.abspath(local_repo),
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            ) for local_repo, url, branch in push_tuples
        ]

        successful_pts, failed_pts = git.push(push_tuples)

        assert not failed_pts
        assert successful_pts == push_tuples
        aio_subproc.create_subprocess.assert_has_calls(expected_calls)
Esempio n. 7
0
    def test_tries_all_calls_despite_exceptions(self, env_setup, push_tuples,
                                                mocker):
        """Test that push tries to push all push tuple values even if there
        are exceptions.
        """
        tries = 3
        expected_calls = [
            call(pt) for pt in sorted(push_tuples, key=lambda pt: pt.repo_url)
        ] * tries

        async def raise_(pt):
            raise exception.PushFailedError("Push failed", 128, b"some error",
                                            pt.repo_url)

        mocker.patch("_repobee.git._push_async", side_effect=raise_)

        successful_pts, failed_pts = git.push(push_tuples, tries=tries)

        assert not successful_pts
        assert failed_pts == push_tuples
        git._push_async.assert_has_calls(expected_calls, any_order=True)
Esempio n. 8
0
    def test_push_raises_when_tries_is_less_than_one(self, env_setup,
                                                     push_tuples, tries):
        with pytest.raises(ValueError) as exc_info:
            git.push(push_tuples, tries=tries)

        assert "tries must be larger than 0" in str(exc_info.value)