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