From 4653d4f97b1973dcd116d12983cd9da6178f0897 Mon Sep 17 00:00:00 2001 From: Christopher Bohn <bohn@unl.edu> Date: Thu, 30 Jul 2020 20:30:23 -0500 Subject: [PATCH] Team contribution grading can now consider all branches --- api/gitlab_classes.py | 1 - commit_format_info.py | 2 +- grade_team_contribution.py | 71 ++++++++++++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/api/gitlab_classes.py b/api/gitlab_classes.py index 418c0a9..e77d3d2 100644 --- a/api/gitlab_classes.py +++ b/api/gitlab_classes.py @@ -667,7 +667,6 @@ class GitlabProject: branch_names: List[str] = list(map(lambda b: b.name, branches)) return branch_names - def get_labels(self) -> Set[str]: """ :return: set of label names diff --git a/commit_format_info.py b/commit_format_info.py index 4a5f38f..312cfbb 100644 --- a/commit_format_info.py +++ b/commit_format_info.py @@ -31,7 +31,7 @@ if __name__ == '__main__': projects.sort(key=lambda p: p.get_name()) for project in projects: print(f'\n\n\t>>>> {project} <<<<') - master_branch_commits = project.get_commits() + master_branch_commits = list(sorted(project.get_commits(), key=lambda c: c.get_timestamp())) print(f'{len(master_branch_commits)} commits on the master branch') all_branches_commits: Set[GitlabCommit] = set() for branch in project.get_branch_names(): diff --git a/grade_team_contribution.py b/grade_team_contribution.py index 4b29ba6..805a047 100644 --- a/grade_team_contribution.py +++ b/grade_team_contribution.py @@ -105,24 +105,32 @@ def _combine_typed_contributions(typed_sizes: Tuple[Dict[str, int]]) -> Dict[str return combined_typed_size -def display_git_contributions(project: GitlabProject): +def display_git_contributions(project: GitlabProject, all_branches: bool = False): # TODO: recognize that this only works for projects in namespace; will need to ask whether to retrieve project. - binary_like_text_files = {"css", "csv", "fxml", "html"} # TODO soft-code this + binary_like_text_files = {"css", "csv", "fxml", "html"} # TODO soft-code this # noinspection SpellCheckingInspection binary_files = {"docx", "gif", "jpg", "jpeg", "pdf", "png", "pptx", "svg", "xlsx"} # TODO this, too + project_branches = project.get_branch_names() if all_branches else ['master'] # noinspection PyUnusedLocal - project_commits: List[GitlabCommit] + master_commits: List[GitlabCommit] + project_commits: Set[GitlabCommit] = set() if assignment_start_date == '': - project_commits = project.get_commits() + master_commits = project.get_commits() else: - project_commits = project.get_commits(after_date=assignment_start_date) - contributions: Dict[str, int] = {} # TODO: also broaden to multiple branches? + master_commits = project.get_commits(after_date=assignment_start_date) + for branch in project_branches: + if assignment_start_date == '': + project_commits = project_commits.union(set(project.get_commits(branch_name=branch))) + else: + project_commits = project_commits.union(set(project.get_commits(branch_name=branch, + after_date=assignment_start_date))) + contributions: Dict[str, int] = {} typed_contributions: Dict[str, Dict[str, int]] = {} timestamps: Dict[str, List[datetime]] = {} contributors: Set[Tuple[str, str]] = set() # noinspection PyShadowingNames - for commit in project_commits: - if not commit.is_merge(): + for commit in master_commits: + if not commit.is_merge() and not commit.is_revert(): author = commit.get_author() contributors.add((author['name'], author['email'])) email = author['email'] # TODO: manage aliases @@ -137,7 +145,7 @@ def display_git_contributions(project: GitlabProject): typed_sizes: Tuple[Dict[str, int]] = (typed_contributions[email], typed_size) typed_contributions[email] = _combine_typed_contributions(typed_sizes) timestamps[email].append(commit.get_timestamp()) - print(f'Contributions by each partner to {project} :') + print(f'Contributions by each partner to {project} on the master branch:') for contribution in contributions: contributor = list(filter(lambda c: c[1] == contribution, contributors))[0] email = contributor[1] @@ -154,6 +162,43 @@ def display_git_contributions(project: GitlabProject): print(f'\t\tFirst commit: {timestamps[email][0]}') print(f'\t\tMedian commit: {timestamps[email][floor(number_of_commits/2)]}') print(f'\t\tLast commit: {timestamps[email][-1]}') + if all_branches: + # noinspection PyShadowingNames + for commit in project_commits: + if commit not in master_commits: + if not commit.is_merge() and not commit.is_revert(): + author = commit.get_author() + contributors.add((author['name'], author['email'])) + email = author['email'] # TODO: manage aliases + size = commit.get_diff_size() + typed_size = commit.get_diff_size_by_filetype() + if email != 'bohn@unl.edu': # TODO: un-hard-code this -- may not be necessary with a starting date + if email not in contributions: + contributions[email] = 0 + typed_contributions[email] = {} + timestamps[email] = [] + contributions[email] += size + typed_sizes: Tuple[Dict[str, int]] = (typed_contributions[email], typed_size) + typed_contributions[email] = _combine_typed_contributions(typed_sizes) + timestamps[email].append(commit.get_timestamp()) + print(f'Contributions by each partner to {project} on {len(project_branches)} branches:') + for contribution in contributions: + contributor = list(filter(lambda c: c[1] == contribution, contributors))[0] + email = contributor[1] + typed_contribution = typed_contributions[email] + timestamps[email].sort() + number_of_commits = len(timestamps[email]) + print(f'\t{contributor}') + print( + f'\t{str(contributions[contribution]).rjust(5)} total line changes in ' + f'{str(number_of_commits).rjust(3)} commits') + for filetype in sorted(typed_contribution.keys()): + change_type = 'file' if filetype in binary_files.union(binary_like_text_files) else 'line' + print(f'\t\t\t{filetype} {str(typed_contribution[filetype]).rjust(10-len(filetype))} ' + f'{change_type} changes') + print(f'\t\tFirst commit: {timestamps[email][0]}') + print(f'\t\tMedian commit: {timestamps[email][floor(number_of_commits/2)]}') + print(f'\t\tLast commit: {timestamps[email][-1]}') # noinspection PyUnusedLocal @@ -195,6 +240,9 @@ if __name__ == '__main__': if option is options[1]: print('Which group?') student_groups = [select_from_list(student_groups, 'student group')] + print('Are you examining the master branch only, or are you examining all branches?') + options = ['master branch only', 'all branches'] + examine_all_branches: bool = select_from_list(options, 'option') is options[1] zero_padding: int = floor(log10(len(projects))) + 1 assignment_start_date = get_assignment_start() # TODO: only need this if grading git histories for student_group in student_groups: # TODO: Skip past graded groups @@ -214,10 +262,11 @@ if __name__ == '__main__': print(f'Could not location repository {custom_project}; please confirm path.') custom_project = None if len(custom_project) > 0: - display_git_contributions(repository) + display_git_contributions(repository, examine_all_branches) else: project_name = f'{project_prefix}{student_group.get_name().split()[1]}'.zfill(zero_padding) - display_git_contributions(list(filter(lambda p: p.get_name() == project_name, projects))[0]) + display_git_contributions(list(filter(lambda p: p.get_name() == project_name, projects))[0], + examine_all_branches) if grading_peer_reviews: display_peer_reviews(peer_review_assignment, student_group.get_students()) # TODO: Ask if you want to grade (keep track of groups that you don't grade) -- GitLab