コード例 #1
0
def _delete_anonymous_repos(
    assignment_names: Iterable[str],
    student_teams: Iterable[plug.StudentTeam],
    double_blind_key: str,
    api: plug.PlatformAPI,
):
    """Delete any anonymous repos created for these students and
    assignments.
    """
    anon_repo_names = [
        _hash_if_key(
            plug.generate_repo_name(student_team, assignment_name),
            key=double_blind_key,
        ) for student_team, assignment_name in itertools.product(
            student_teams, assignment_names)
    ]
    anon_repo_urls = api.get_repo_urls(anon_repo_names)
    anon_repos = api.get_repos(anon_repo_urls)
    anon_repos_progress = plug.cli.io.progress_bar(
        anon_repos,
        desc="Deleting anonymous repo copies",
        total=len(anon_repo_names),
    )
    for repo in anon_repos_progress:
        api.delete_repo(repo)
    progresswrappers.end_progress(anon_repos_progress)
コード例 #2
0
    def test_assign_to_nonexisting_students(
        self, with_student_repos, extra_args
    ):
        """If you try to assign reviews where one or more of the allocated
        student repos don't exist, there should be an error.
        """
        assignment_name = assignment_names[1]
        non_existing_group = "non-existing-group"
        student_team_names = STUDENT_TEAM_NAMES + [non_existing_group]

        command = " ".join(
            [
                REPOBEE_GITLAB,
                *repobee_plug.cli.CoreCommand.reviews.assign.as_name_tuple(),
                *BASE_ARGS_NO_TB,
                "-a",
                assignment_name,
                "-s",
                *student_team_names,
                "-n",
                "1",
            ]
        )

        result = run_in_docker_with_coverage(command, extra_args=extra_args)
        output = result.stdout.decode("utf-8")

        assert (
            "[ERROR] NotFoundError: Can't find repos: {}".format(
                plug.generate_repo_name(non_existing_group, assignment_name)
            )
            in output
        )
        assert result.returncode == 1
        assert_num_issues(STUDENT_TEAMS, [assignment_name], 0)
コード例 #3
0
ファイル: test_repos.py プロジェクト: repobee/repobee
def assert_cloned_student_repos_match_templates(
    student_teams: List[plug.StudentTeam],
    template_repo_names: List[str],
    workdir: pathlib.Path,
):
    repos_dict = {
        plug.generate_repo_name(team.name, template_repo_name): workdir /
        team.name / plug.generate_repo_name(team.name, template_repo_name)
        for team in student_teams for template_repo_name in template_repo_names
    }
    _assert_repos_match_templates(
        student_teams,
        template_repo_names,
        funcs.template_repo_hashes(),
        repos_dict,
    )
コード例 #4
0
ファイル: repos.py プロジェクト: repobee/repobee
def _create_push_tuples(
    teams: List[plug.Team],
    template_repos: Iterable[plug.TemplateRepo],
    api: plug.PlatformAPI,
) -> Iterable[Tuple[bool, PushSpec]]:
    """Create push tuples for newly created repos. Repos that already exist are
    ignored.

    Args:
        teams: An iterable of teams.
        template_repos: Template repositories.
        api: A platform API instance.
    Returns:
        A list of tuples (created, push_tuple) for all student repo urls
        that relate to any of the master repo urls. ``created`` indicates
        whether or not the student repo was created in this invocation.
    """
    for team, template_repo in itertools.product(teams, template_repos):
        repo_name = plug.generate_repo_name(team, template_repo.name)
        created, repo = _create_or_fetch_repo(
            name=repo_name,
            description=f"{repo_name} created for {team.name}",
            private=True,
            team=team,
            api=api,
        )

        yield created, PushSpec(
            local_path=template_repo.path,
            repo_url=api.insert_auth(repo.url),
            branch=git.active_branch(template_repo.path),
            metadata=dict(repo=repo, team=team),
        )
コード例 #5
0
    def test_get_student_repo_urls(self, assignment_names):
        """When supplied with the students argument, the generated urls should
        go to the student repos related to the supplied master repos.
        """
        # arrange
        api = _repobee.ext.gitlab.GitLabAPI(BASE_URL, TOKEN, TARGET_GROUP)
        expected_urls = [
            api._insert_auth("{}/{}/{}/{}.git".format(
                BASE_URL,
                TARGET_GROUP,
                str(student_group),
                plug.generate_repo_name(str(student_group), mn),
            )) for student_group in constants.STUDENTS
            for mn in assignment_names
        ]
        assert (
            expected_urls
        ), "there must be at least some urls for this test to make sense"

        # act
        actual_urls = api.get_repo_urls(
            assignment_names,
            team_names=[t.name for t in constants.STUDENTS],
            insert_auth=True,
        )

        # assert
        assert sorted(actual_urls) == sorted(expected_urls)
コード例 #6
0
ファイル: test_gitlab_system.py プロジェクト: repobee/repobee
    def test_opens_issue_if_update_rejected(self, tmpdir, with_student_repos):
        master_repo = assignment_names[0]
        conflict_repo = plug.generate_repo_name(STUDENT_TEAMS[0], master_repo)
        filename = "superfile.super"
        text = "some epic content\nfor this file!"
        # update the master repo
        update_repo(master_repo, filename, text)
        # conflicting update in the student repo
        update_repo(conflict_repo, "somefile.txt", "some other content")

        issue = plug.Issue(title="Oops, push was rejected!", body="")
        issue_file = pathlib.Path(str(tmpdir)) / "issue.md"
        issue_file.write_text(issue.title)

        command = " ".join([
            *repobee_plug.cli.CoreCommand.repos.update.as_name_tuple(),
            *TEMPLATE_ORG_ARG,
            *BASE_ARGS,
            "-a",
            master_repo,
            *STUDENTS_ARG,
            "--issue",
            issue_file.name,
        ])

        run_repobee(command, workdir=tmpdir, plugins=[_repobee.ext.gitlab])

        assert_repos_contain(STUDENT_TEAMS[1:], [master_repo], filename, text)
        assert_issues_exist(STUDENT_TEAMS[0:1], [master_repo], issue)
コード例 #7
0
ファイル: test_gitlab_system.py プロジェクト: repobee/repobee
    def test_assign_to_nonexisting_students(self, with_student_repos, tmpdir):
        """If you try to assign reviews where one or more of the allocated
        student repos don't exist, there should be an error.
        """
        assignment_name = assignment_names[1]
        non_existing_group = "non-existing-group"
        student_team_names = STUDENT_TEAM_NAMES + [non_existing_group]

        command = " ".join([
            *repobee_plug.cli.CoreCommand.reviews.assign.as_name_tuple(),
            *BASE_ARGS_NO_TB,
            "-a",
            assignment_name,
            "-s",
            *student_team_names,
            "-n",
            "1",
        ])

        with pytest.raises(plug.NotFoundError) as exc_info:
            run_repobee(command, workdir=tmpdir, plugins=[_repobee.ext.gitlab])

        non_existing_repo_name = plug.generate_repo_name(
            non_existing_group, assignment_name)
        assert f"Can't find repos: {non_existing_repo_name}" in str(
            exc_info.value)
        assert_num_issues(STUDENT_TEAMS, [assignment_name], 0)
コード例 #8
0
    def test_setup_once(self):
        """Test that running setup once results in the expected repos."""

        command = re.sub(
            r"\s+",
            " ",
            f"""
repos setup --bu https://localhost:3000/api/v1
    --token {giteamanager.TEACHER_TOKEN}
    --user {giteamanager.TEACHER_USER}
    --org-name {giteamanager.TARGET_ORG_NAME}
    --template-org-name {giteamanager.TEMPLATE_ORG_NAME}
    --students {' '.join([t.members[0] for t in giteamanager.STUDENT_TEAMS])}
    --assignments {' '.join(template_helpers.TEMPLATE_REPO_NAMES)}
    --tb
""",
        )

        repobee.run(shlex.split(command), plugins=[gitea])

        for team, template_name in itertools.product(
                giteamanager.STUDENT_TEAMS,
                template_helpers.TEMPLATE_REPO_NAMES):
            repo_name = plug.generate_repo_name(team, template_name)
            sha = git.Repo(giteamanager.REPOSITORIES_ROOT /
                           giteamanager.TARGET_ORG_NAME /
                           (repo_name + ".git")).head.commit.tree.hexsha
            assert template_helpers.TASK_CONTENTS_SHAS[template_name] == sha
コード例 #9
0
def callback(args: argparse.Namespace, api: plug.PlatformAPI) -> None:
    repo_name_to_team: Mapping[str, plug.StudentTeam] = {
        plug.generate_repo_name(student_team.name, assignment_name):
        student_team
        for student_team in args.students
        for assignment_name in args.assignments
    }
    repo_names = list(repo_name_to_team.keys())

    if "multi_issues_file" in args and args.multi_issues_file is not None:
        issues_file = pathlib.Path(args.multi_issues_file).resolve()
        all_issues = _parse_multi_issues_file(issues_file)
    else:
        issues_dir = pathlib.Path(args.issues_dir).resolve()
        all_issues = _collect_issues(repo_names, issues_dir)

    issues = _extract_expected_issues(all_issues, repo_names,
                                      args.allow_missing)
    for repo_name, issue in issues:
        open_issue = args.batch_mode or _ask_for_open(issue, repo_name,
                                                      args.truncation_length)
        if open_issue:
            repo = api.get_repo(repo_name, repo_name_to_team[repo_name].name)
            api.create_issue(issue.title, issue.body, repo)
        else:
            plug.echo("Skipping {}".format(repo_name))
コード例 #10
0
ファイル: test_repos.py プロジェクト: tohanss/repobee
    def test_empty_student_repos_dont_cause_errors(
        self, with_student_repos, platform_url, capsys, tmp_path_factory
    ):
        """No error messages should be displayed when empty repos are
        cloned, and the empty repos should be on disk.
        """
        # arrange
        workdir = tmp_path_factory.mktemp("workdir")
        task_name = self._setup_empty_task(platform_url)

        # act
        funcs.run_repobee(
            f"repos clone -a {task_name} --base-url {platform_url} ",
            workdir=workdir,
        )

        # assert
        assert not capsys.readouterr().err
        for student_team in STUDENT_TEAMS:
            repo = (
                workdir
                / student_team.name
                / plug.generate_repo_name(student_team.name, task_name)
            )
            assert repo.is_dir()
            assert [f.name for f in repo.iterdir()] == [".git"]
コード例 #11
0
ファイル: parsing.py プロジェクト: tohanss/repobee
def _repo_tuple_generator(
    assignment_names: List[str],
    teams: List[plug.StudentTeam],
    api: plug.PlatformAPI,
) -> Iterable[plug.StudentRepo]:
    for assignment_name in assignment_names:
        for team in teams:
            url, *_ = api.get_repo_urls([assignment_name],
                                        team_names=[team.name])
            name = plug.generate_repo_name(team, assignment_name)
            yield plug.StudentRepo(name=name, url=url, team=team)
コード例 #12
0
ファイル: test_repos.py プロジェクト: repobee/repobee
    def _setup_empty_task(self, platform_url: str) -> str:
        task_name = "empty-task"
        api = funcs.get_api(platform_url)
        for team in api.get_teams([t.name for t in STUDENT_TEAMS]):
            repo_name = plug.generate_repo_name(team.name, task_name)
            api.create_repo(
                name=repo_name,
                description="An empty task",
                private=True,
                team=team,
            )

        return task_name
コード例 #13
0
ファイル: test_repos.py プロジェクト: repobee/repobee
    def test_clone_local_gitconfig(self, platform_url, with_student_repos,
                                   tmp_path):
        funcs.run_repobee(
            f"repos clone --assignments {TEMPLATE_REPOS_ARG} "
            f"--base-url {platform_url}",
            workdir=tmp_path,
        )

        # do a spot check on a single repo
        team = STUDENT_TEAMS[0]
        assignment_name = TEMPLATE_REPO_NAMES[0]
        repo = git.Repo(tmp_path / str(team) /
                        plug.generate_repo_name(team, assignment_name))
        assert "pull.ff=only" in repo.git.config("--local", "--list")
コード例 #14
0
    def test_aborts_if_issue_is_missing(self, with_issues,
                                        parsed_args_issues_dir, api_mock,
                                        tmp_path):
        """Test that the callback exits with a plug.PlugError if any of the
        expected issues is not found.
        """
        repo_without_issue = plug.generate_repo_name(STUDENT_TEAM_NAMES[-1],
                                                     ASSIGNMENT_NAMES[0])
        missing_file = tmp_path / "{}.md".format(repo_without_issue)
        missing_file.unlink()

        with pytest.raises(plug.PlugError) as exc_info:
            feedback.callback(args=parsed_args_issues_dir, api=api_mock)

        assert repo_without_issue in str(exc_info.value)
        assert not api_mock.create_issue.called
コード例 #15
0
    def test_ignores_missing_issue_if_allow_missing(self, with_issues,
                                                    parsed_args_issues_dir,
                                                    api_mock, tmp_path):
        """Test that missing issues are ignored if --allow-mising is set."""
        repo_without_issue = plug.generate_repo_name(STUDENT_TEAM_NAMES[-1],
                                                     ASSIGNMENT_NAMES[0])
        (tmp_path / "{}.md".format(repo_without_issue)).unlink()
        expected_calls = [
            mock.call(issue.title, issue.body, mock.ANY)
            for repo_name, issue in with_issues
            if repo_name != repo_without_issue
        ]
        args_dict = vars(parsed_args_issues_dir)
        args_dict["allow_missing"] = True
        args = argparse.Namespace(**args_dict)

        feedback.callback(args=args, api=api_mock)

        api_mock.create_issue.assert_has_calls(expected_calls, any_order=True)
コード例 #16
0
ファイル: test_gitlab.py プロジェクト: repobee/repobee
def repo_names(api, assignment_names):
    """Setup repo tuples along with groups for the repos to be created in."""
    target_group_id = api._gitlab.tests_only_target_group_id
    groups = [
        api._gitlab.groups.create(
            dict(name=str(team), path=str(team), parent_id=target_group_id))
        for team in constants.STUDENTS
    ]

    repo_names = []
    for group, assignment in itertools.product(groups, assignment_names):
        repo_name = plug.generate_repo_name(group.name, assignment)
        api._gitlab.projects.create(
            dict(
                name=repo_name,
                path=repo_name,
                description="Some description",
                visibility="private",
                namespace_id=group.id,
            ))
        repo_names.append(repo_name)
    return repo_names
コード例 #17
0
ファイル: test_repos.py プロジェクト: repobee/repobee
    def test_raises_on_path_clash_with_non_git_directory(
            self, platform_url, tmp_path, with_student_repos):
        """Test that an error is raised if there is a path clash between a
        student repository and a non-git directory.
        """
        # arrange
        self._clone_all_student_repos(platform_url, tmp_path)
        non_git_dir = (
            tmp_path / str(STUDENT_TEAMS[0]) /
            plug.generate_repo_name(STUDENT_TEAMS[0], TEMPLATE_REPO_NAMES[0]))
        shutil.rmtree(non_git_dir / ".git")

        # act/assert
        with pytest.raises(exception.RepoBeeException) as exc_info:
            funcs.run_repobee(
                f"repos clone -a {const.TEMPLATE_REPOS_ARG} "
                f"--base-url {platform_url}",
                workdir=tmp_path,
            )

        assert (f"name clash with directory that is not a Git repository: "
                f"'{non_git_dir}'" in str(exc_info))
コード例 #18
0
ファイル: test_cli.py プロジェクト: repobee/repobee
    user=USER,
    template_repo_urls=REPO_URLS,
    assignments=ASSIGNMENT_NAMES,
    students=list(STUDENTS),
    issue=ISSUE,
    title_regex="some regex",
    traceback=False,
    state=plug.IssueState.OPEN,
    show_body=True,
    author=None,
    token=TOKEN,
    num_reviews=1,
    hook_results_file=None,
    repos=[
        plug.StudentRepo(
            name=plug.generate_repo_name(team, master_name),
            team=team,
            url=generate_repo_url(
                plug.generate_repo_name(team, master_name), ORG_NAME
            ),
        )
        for team, master_name in itertools.product(STUDENTS, ASSIGNMENT_NAMES)
    ],
    secrets=False,
    update_local=False,
    double_blind_key=None,
    directory_layout=fileutil.DirectoryLayout.BY_TEAM,
)


@pytest.fixture(autouse=True)
コード例 #19
0
def assign_peer_reviews(
    assignment_names: Iterable[str],
    teams: Iterable[plug.StudentTeam],
    num_reviews: int,
    issue: Optional[plug.Issue],
    api: plug.PlatformAPI,
) -> None:
    """Assign peer reviewers among the students to each student repo. Each
    student is assigned to review num_reviews repos, and consequently, each
    repo gets reviewed by num_reviews reviewers.

    In practice, each student repo has a review team generated (called
    <student-repo-name>-review), to which num_reviews _other_ students are
    assigned. The team itself is given pull-access to the student repo, so
    that reviewers can view code and open issues, but cannot modify the
    contents of the repo.

    Args:
        assignment_names: Names of assginments.
        teams: Team objects specifying student groups.
        num_reviews: Amount of reviews each student should perform
            (consequently, the amount of reviews of each repo)
        issue: An issue with review instructions to be opened in the considered
            repos.
        api: An implementation of :py:class:`repobee_plug.PlatformAPI` used to
            interface with the platform (e.g. GitHub or GitLab) instance.
    """
    issue = issue or DEFAULT_REVIEW_ISSUE
    expected_repo_names = plug.generate_repo_names(teams, assignment_names)
    fetched_teams = progresswrappers.get_teams(teams,
                                               api,
                                               desc="Fetching teams and repos")
    fetched_repos = list(
        itertools.chain.from_iterable(map(api.get_team_repos, fetched_teams)))
    fetched_repo_dict = {r.name: r for r in fetched_repos}

    missing = set(expected_repo_names) - set(fetched_repo_dict.keys())
    if missing:
        raise plug.NotFoundError(f"Can't find repos: {', '.join(missing)}")

    for assignment_name in assignment_names:
        plug.echo("Allocating reviews")
        allocations = plug.manager.hook.generate_review_allocations(
            teams=teams, num_reviews=num_reviews)
        # adjust names of review teams
        review_team_specs, reviewed_team_names = list(
            zip(*[(
                plug.StudentTeam(
                    members=alloc.review_team.members,
                    name=plug.generate_review_team_name(
                        str(alloc.reviewed_team), assignment_name),
                ),
                alloc.reviewed_team,
            ) for alloc in allocations]))

        review_teams = _repobee.command.teams.create_teams(
            review_team_specs, plug.TeamPermission.PULL, api)
        review_teams_progress = plug.cli.io.progress_bar(
            review_teams,
            desc="Creating review teams",
            total=len(review_team_specs),
        )

        for review_team, reviewed_team_name in zip(review_teams_progress,
                                                   reviewed_team_names):
            reviewed_repo = fetched_repo_dict[plug.generate_repo_name(
                reviewed_team_name, assignment_name)]
            review_teams_progress.write(  # type: ignore
                f"Assigning {' and '.join(review_team.members)} "
                f"to review {reviewed_repo.name}")
            api.assign_repo(review_team, reviewed_repo,
                            plug.TeamPermission.PULL)
            api.create_issue(
                issue.title,
                issue.body,
                reviewed_repo,
                assignees=review_team.members,
            )
コード例 #20
0
 def command(self):
     nonlocal actual_repo_name
     actual_repo_name = plug.generate_repo_name(
         team_name=student, assignment_name=assignment)
コード例 #21
0
def assign_peer_reviews(
    assignment_names: Iterable[str],
    teams: Iterable[plug.StudentTeam],
    num_reviews: int,
    issue: Optional[plug.Issue],
    double_blind_key: Optional[str],
    api: plug.PlatformAPI,
) -> None:
    """Assign peer reviewers among the students to each student repo. Each
    student is assigned to review num_reviews repos, and consequently, each
    repo gets reviewed by num_reviews reviewers.

    In practice, each student repo has a review team generated (called
    <student-repo-name>-review), to which num_reviews _other_ students are
    assigned. The team itself is given pull-access to the student repo, so
    that reviewers can view code and open issues, but cannot modify the
    contents of the repo.

    Args:
        assignment_names: Names of assginments.
        teams: Team objects specifying student groups.
        num_reviews: Amount of reviews each student should perform
            (consequently, the amount of reviews of each repo)
        issue: An issue with review instructions to be opened in the considered
            repos.
        double_blind_key: If provided, use key to make double-blind review
            allocation.
        api: An implementation of :py:class:`repobee_plug.PlatformAPI` used to
            interface with the platform (e.g. GitHub or GitLab) instance.
    """
    issue = issue or DEFAULT_REVIEW_ISSUE
    expected_repo_names = set(plug.generate_repo_names(teams,
                                                       assignment_names))
    fetched_teams = progresswrappers.get_teams(teams,
                                               api,
                                               desc="Fetching teams and repos")
    team_repo_tuples = [(team, list(api.get_team_repos(team)))
                        for team in fetched_teams]
    fetched_repos = list(
        itertools.chain.from_iterable(repos for _, repos in team_repo_tuples))
    fetched_repo_dict = {r.name: r for r in fetched_repos}

    missing = expected_repo_names - fetched_repo_dict.keys()
    if missing:
        raise plug.NotFoundError(f"Can't find repos: {', '.join(missing)}")

    if double_blind_key:
        plug.log.info(f"Creating anonymous repos with key: {double_blind_key}")
        fetched_repo_dict = _create_anonymized_repos(
            [(team, _only_expected_repos(repos, expected_repo_names))
             for team, repos in team_repo_tuples],
            double_blind_key,
            api,
        )

    allocations_for_output = []
    for assignment_name in assignment_names:
        plug.echo("Allocating reviews")
        allocations = plug.manager.hook.generate_review_allocations(
            teams=teams, num_reviews=num_reviews)
        # adjust names of review teams
        review_team_specs, reviewed_team_names = list(
            zip(*[(
                plug.StudentTeam(
                    members=alloc.review_team.members,
                    name=_review_team_name(
                        alloc.reviewed_team,
                        assignment_name,
                        key=double_blind_key,
                    ),
                ),
                alloc.reviewed_team,
            ) for alloc in allocations]))

        review_teams = _repobee.command.teams.create_teams(
            review_team_specs, plug.TeamPermission.PULL, api)
        review_teams_progress = plug.cli.io.progress_bar(
            review_teams,
            desc="Creating review teams",
            total=len(review_team_specs),
        )

        for review_team, reviewed_team_name in zip(review_teams_progress,
                                                   reviewed_team_names):
            reviewed_repo = fetched_repo_dict[plug.generate_repo_name(
                reviewed_team_name, assignment_name)]

            review_teams_progress.write(  # type: ignore
                f"Assigning {' and '.join(review_team.members)} "
                f"to review {reviewed_repo.name}")
            api.assign_repo(review_team, reviewed_repo,
                            plug.TeamPermission.PULL)
            api.create_issue(
                issue.title,
                issue.body,
                reviewed_repo,
                # It's not possible to assign users with read-access in Gitea
                # FIXME redesign so Gitea does not require special handling
                assignees=review_team.members
                if not isinstance(api, _repobee.ext.gitea.GiteaAPI) else None,
            )

            allocations_for_output.append({
                "reviewed_repo": {
                    "name": reviewed_repo.name,
                    "url": reviewed_repo.url,
                },
                "review_team": {
                    "name": review_team.name,
                    "members": review_team.members,
                },
            })

        if featflags.is_feature_enabled(
                featflags.FeatureFlag.REPOBEE_4_REVIEW_COMMANDS):
            output = dict(allocations=allocations_for_output,
                          num_reviews=num_reviews)
            pathlib.Path("review_allocations.json").write_text(
                json.dumps(output, indent=4),
                encoding=sys.getdefaultencoding(),
            )