Repository: mozilla/bugbug

Issue:
Also calculate experience of backed out patches
We are ignoring them to calculate experiences for authors and reviewers and previous commits for same files, directories and components.
Maybe we should have a new feature that says the number of previous backouts for the same author (probably doesn't make sense to have it for reviewers, files, directories and components too?).


Discussion:
I think it could be useful in case where a dev has a good experience on a component and even after years he continues to have backed out patches: it probably means that every change here is not trivial at all and if a "newbie" writes a patch here then the probability to have a regression is pretty high. 
Maybe it could help to identify risky component/files.
It could also help in calculating how likely a patch is to be backed out.

Resolving patch:
diff --git a/bugbug/commit_features.py b/bugbug/commit_features.py
--- a/bugbug/commit_features.py
+++ b/bugbug/commit_features.py
@@ -57,6 +57,12 @@ def get_exps(exp_type, commit):
         "avg": commit[f"touched_prev_total_{exp_type}_sum"] / items_num
         if items_num > 0
         else 0,
+        "sum_backout": commit[f"touched_prev_total_{exp_type}_backout_sum"],
+        "max_backout": commit[f"touched_prev_total_{exp_type}_backout_max"],
+        "min_backout": commit[f"touched_prev_total_{exp_type}_backout_min"],
+        "avg_backout": commit[f"touched_prev_total_{exp_type}_backout_sum"] / items_num
+        if items_num > 0
+        else 0,
         f"sum_{EXPERIENCE_TIMESPAN_TEXT}": commit[
             f"touched_prev_{EXPERIENCE_TIMESPAN_TEXT}_{exp_type}_sum"
         ],
@@ -72,6 +78,21 @@ def get_exps(exp_type, commit):
         / items_num
         if items_num > 0
         else 0,
+        f"sum_{EXPERIENCE_TIMESPAN_TEXT}_backout": commit[
+            f"touched_prev_{EXPERIENCE_TIMESPAN_TEXT}_{exp_type}_backout_sum"
+        ],
+        f"max_{EXPERIENCE_TIMESPAN_TEXT}_backout": commit[
+            f"touched_prev_{EXPERIENCE_TIMESPAN_TEXT}_{exp_type}_backout_max"
+        ],
+        f"min_{EXPERIENCE_TIMESPAN_TEXT}_backout": commit[
+            f"touched_prev_{EXPERIENCE_TIMESPAN_TEXT}_{exp_type}_backout_min"
+        ],
+        f"avg_{EXPERIENCE_TIMESPAN_TEXT}_backout": commit[
+            f"touched_prev_{EXPERIENCE_TIMESPAN_TEXT}_{exp_type}_backout_sum"
+        ]
+        / items_num
+        if items_num > 0
+        else 0,
     }
 
 
@@ -82,6 +103,10 @@ def __call__(self, commit, **kwargs):
             EXPERIENCE_TIMESPAN_TEXT: commit[
                 f"touched_prev_{EXPERIENCE_TIMESPAN_TEXT}_author_sum"
             ],
+            "total_backout": commit["touched_prev_total_author_backout_sum"],
+            f"{EXPERIENCE_TIMESPAN_TEXT}_backout": commit[
+                f"touched_prev_{EXPERIENCE_TIMESPAN_TEXT}_author_backout_sum"
+            ],
         }
 
 
diff --git a/bugbug/repository.py b/bugbug/repository.py
--- a/bugbug/repository.py
+++ b/bugbug/repository.py
@@ -60,8 +60,12 @@ def __init__(
         self.file_copies = file_copies
         self.reviewers = reviewers
 
-    def set_experience(self, exp_type, timespan, exp_sum, exp_max, exp_min):
+    def set_experience(
+        self, exp_type, commit_type, timespan, exp_sum, exp_max, exp_min
+    ):
         exp_str = f"touched_prev_{timespan}_{exp_type}_"
+        if commit_type:
+            exp_str += f"{commit_type}_"
         setattr(self, f"{exp_str}sum", exp_sum)
         if exp_type != "author":
             setattr(self, f"{exp_str}max", exp_max)
@@ -382,104 +386,138 @@ def calculate_experiences(commits):
     # for C should be 2 (A + B), and not 3 (A twice + B).
     experiences = {}
 
-    def get_experience(exp_type, item, day, default):
+    def get_experience(exp_type, commit_type, item, day, default):
         if exp_type not in experiences:
             experiences[exp_type] = {}
 
-        if item not in experiences[exp_type]:
-            experiences[exp_type][item] = exp_queue(
+        if commit_type not in experiences[exp_type]:
+            experiences[exp_type][commit_type] = {}
+
+        if item not in experiences[exp_type][commit_type]:
+            experiences[exp_type][commit_type][item] = exp_queue(
                 day, EXPERIENCE_TIMESPAN + 1, default
             )
 
-        return experiences[exp_type][item][day]
+        return experiences[exp_type][commit_type][item][day]
 
     def update_experiences(experience_type, day, items):
-        total_exps = [get_experience(experience_type, item, day, 0) for item in items]
-        timespan_exps = [
-            exp - get_experience(experience_type, item, day - EXPERIENCE_TIMESPAN, 0)
-            for exp, item in zip(total_exps, items)
-        ]
-
-        total_exps_sum = sum(total_exps)
-        timespan_exps_sum = sum(timespan_exps)
-
-        commit.set_experience(
-            experience_type,
-            "total",
-            total_exps_sum,
-            max(total_exps, default=0),
-            min(total_exps, default=0),
-        )
-        commit.set_experience(
-            experience_type,
-            EXPERIENCE_TIMESPAN_TEXT,
-            timespan_exps_sum,
-            max(timespan_exps, default=0),
-            min(timespan_exps, default=0),
-        )
+        for commit_type in ["", "backout"]:
+            total_exps = [
+                get_experience(experience_type, commit_type, item, day, 0)
+                for item in items
+            ]
+            timespan_exps = [
+                exp
+                - get_experience(
+                    experience_type, commit_type, item, day - EXPERIENCE_TIMESPAN, 0
+                )
+                for exp, item in zip(total_exps, items)
+            ]
+
+            total_exps_sum = sum(total_exps)
+            timespan_exps_sum = sum(timespan_exps)
+
+            commit.set_experience(
+                experience_type,
+                commit_type,
+                "total",
+                total_exps_sum,
+                max(total_exps, default=0),
+                min(total_exps, default=0),
+            )
+            commit.set_experience(
+                experience_type,
+                commit_type,
+                EXPERIENCE_TIMESPAN_TEXT,
+                timespan_exps_sum,
+                max(timespan_exps, default=0),
+                min(timespan_exps, default=0),
+            )
 
-        # We don't want to consider backed out commits when calculating experiences.
-        if not commit.backedoutby:
-            for i, item in enumerate(items):
-                experiences[experience_type][item][day] = total_exps[i] + 1
+            # We don't want to consider backed out commits when calculating normal experiences.
+            if (
+                commit_type == ""
+                and not commit.backedoutby
+                or commit_type == "backout"
+                and commit.backedoutby
+            ):
+                for i, item in enumerate(items):
+                    experiences[experience_type][commit_type][item][day] = (
+                        total_exps[i] + 1
+                    )
 
     def update_complex_experiences(experience_type, day, items):
-        all_commit_lists = [
-            get_experience(experience_type, item, day, tuple()) for item in items
-        ]
-        before_commit_lists = [
-            get_experience(experience_type, item, day - EXPERIENCE_TIMESPAN, tuple())
-            for item in items
-        ]
-        timespan_commit_lists = [
-            commit_list[len(before_commit_list) :]
-            for commit_list, before_commit_list in zip(
-                all_commit_lists, before_commit_lists
+        for commit_type in ["", "backout"]:
+            all_commit_lists = [
+                get_experience(experience_type, commit_type, item, day, tuple())
+                for item in items
+            ]
+            before_commit_lists = [
+                get_experience(
+                    experience_type,
+                    commit_type,
+                    item,
+                    day - EXPERIENCE_TIMESPAN,
+                    tuple(),
+                )
+                for item in items
+            ]
+            timespan_commit_lists = [
+                commit_list[len(before_commit_list) :]
+                for commit_list, before_commit_list in zip(
+                    all_commit_lists, before_commit_lists
+                )
+            ]
+
+            all_commits = set(sum(all_commit_lists, tuple()))
+            timespan_commits = set(sum(timespan_commit_lists, tuple()))
+
+            commit.set_experience(
+                experience_type,
+                commit_type,
+                "total",
+                len(all_commits),
+                max(
+                    (len(all_commit_list) for all_commit_list in all_commit_lists),
+                    default=0,
+                ),
+                min(
+                    (len(all_commit_list) for all_commit_list in all_commit_lists),
+                    default=0,
+                ),
             )
-        ]
-
-        all_commits = set(sum(all_commit_lists, tuple()))
-        timespan_commits = set(sum(timespan_commit_lists, tuple()))
-
-        commit.set_experience(
-            experience_type,
-            "total",
-            len(all_commits),
-            max(
-                (len(all_commit_list) for all_commit_list in all_commit_lists),
-                default=0,
-            ),
-            min(
-                (len(all_commit_list) for all_commit_list in all_commit_lists),
-                default=0,
-            ),
-        )
-        commit.set_experience(
-            experience_type,
-            EXPERIENCE_TIMESPAN_TEXT,
-            len(timespan_commits),
-            max(
-                (
-                    len(timespan_commit_list)
-                    for timespan_commit_list in timespan_commit_lists
+            commit.set_experience(
+                experience_type,
+                commit_type,
+                EXPERIENCE_TIMESPAN_TEXT,
+                len(timespan_commits),
+                max(
+                    (
+                        len(timespan_commit_list)
+                        for timespan_commit_list in timespan_commit_lists
+                    ),
+                    default=0,
                 ),
-                default=0,
-            ),
-            min(
-                (
-                    len(timespan_commit_list)
-                    for timespan_commit_list in timespan_commit_lists
+                min(
+                    (
+                        len(timespan_commit_list)
+                        for timespan_commit_list in timespan_commit_lists
+                    ),
+                    default=0,
                 ),
-                default=0,
-            ),
-        )
+            )
 
-        # We don't want to consider backed out commits when calculating experiences.
-        if not commit.backedoutby:
-            for i, item in enumerate(items):
-                experiences[experience_type][item][day] = all_commit_lists[i] + (
-                    commit.node,
-                )
+            # We don't want to consider backed out commits when calculating normal experiences.
+            if (
+                commit_type == ""
+                and not commit.backedoutby
+                or commit_type == "backout"
+                and commit.backedoutby
+            ):
+                for i, item in enumerate(items):
+                    experiences[experience_type][commit_type][item][
+                        day
+                    ] = all_commit_lists[i] + (commit.node,)
 
     for commit in tqdm(commits):
         day = (commit.pushdate - first_pushdate).days
@@ -493,22 +531,30 @@ def update_complex_experiences(experience_type, day, items):
             for orig, copied in commit.file_copies.items():
                 orig_directories = get_directories(orig)
                 copied_directories = get_directories(copied)
-                for orig_directory, copied_directory in zip(
-                    orig_directories, copied_directories
-                ):
-                    experiences["directory"][copied_directory] = copy.deepcopy(
-                        experiences["directory"][orig_directory]
-                    )
 
-                if orig in path_to_component and copied in path_to_component:
-                    orig_component = path_to_component[orig]
-                    copied_component = path_to_component[copied]
-                    experiences["component"][copied_component] = copy.deepcopy(
-                        experiences["component"][orig_component]
+                for commit_type in ["", "backout"]:
+                    for orig_directory, copied_directory in zip(
+                        orig_directories, copied_directories
+                    ):
+                        experiences["directory"][commit_type][
+                            copied_directory
+                        ] = copy.deepcopy(
+                            experiences["directory"][commit_type][orig_directory]
+                        )
+
+                    if orig in path_to_component and copied in path_to_component:
+                        orig_component = path_to_component[orig]
+                        copied_component = path_to_component[copied]
+                        experiences["component"][commit_type][
+                            copied_component
+                        ] = copy.deepcopy(
+                            experiences["component"][commit_type][orig_component]
+                        )
+
+                    experiences["file"][commit_type][copied] = copy.deepcopy(
+                        experiences["file"][commit_type][orig]
                     )
 
-                experiences["file"][copied] = copy.deepcopy(experiences["file"][orig])
-
         update_complex_experiences("file", day, commit.files)
 
         update_complex_experiences("directory", day, get_directories(commit.files))


Test patch:
diff --git a/tests/test_repository.py b/tests/test_repository.py
--- a/tests/test_repository.py
+++ b/tests/test_repository.py
@@ -167,8 +167,8 @@ def test_exp_queue():
 
 
 def test_calculate_experiences():
-    commits = [
-        repository.Commit(
+    commits = {
+        "commit1": repository.Commit(
             node="commit1",
             author="author1",
             desc="commit1",
@@ -181,7 +181,20 @@ def test_calculate_experiences():
             file_copies={},
             reviewers=("reviewer1", "reviewer2"),
         ),
-        repository.Commit(
+        "commitbackedout": repository.Commit(
+            node="commitbackedout",
+            author="author1",
+            desc="commitbackedout",
+            date=datetime(2019, 1, 1),
+            pushdate=datetime(2019, 1, 1),
+            bug="123",
+            backedoutby="commitbackout",
+            author_email="author1@mozilla.org",
+            files=["dom/file1.cpp", "apps/file1.jsm"],
+            file_copies={},
+            reviewers=("reviewer1", "reviewer2"),
+        ),
+        "commit2": repository.Commit(
             node="commit2",
             author="author2",
             desc="commit2",
@@ -194,7 +207,7 @@ def test_calculate_experiences():
             file_copies={},
             reviewers=("reviewer1",),
         ),
-        repository.Commit(
+        "commit3": repository.Commit(
             node="commit3",
             author="author1",
             desc="commit3",
@@ -207,7 +220,7 @@ def test_calculate_experiences():
             file_copies={},
             reviewers=("reviewer2",),
         ),
-        repository.Commit(
+        "commit4": repository.Commit(
             node="commit4",
             author="author2",
             desc="commit4",
@@ -220,7 +233,7 @@ def test_calculate_experiences():
             file_copies={},
             reviewers=("reviewer1", "reviewer2"),
         ),
-        repository.Commit(
+        "commit5": repository.Commit(
             node="commit5",
             author="author3",
             desc="commit5",
@@ -233,7 +246,7 @@ def test_calculate_experiences():
             file_copies={"dom/file1.cpp": "dom/file1copied.cpp"},
             reviewers=("reviewer3",),
         ),
-        repository.Commit(
+        "commit6": repository.Commit(
             node="commit6",
             author="author3",
             desc="commit6",
@@ -246,7 +259,7 @@ def test_calculate_experiences():
             file_copies={},
             reviewers=("reviewer3",),
         ),
-    ]
+    }
 
     repository.path_to_component = {
         "dom/file1.cpp": "Core::DOM",
@@ -256,177 +269,178 @@ def test_calculate_experiences():
         "apps/file2.jsm": "Firefox::Boh",
     }
 
-    repository.calculate_experiences(commits)
-
-    assert commits[0].seniority_author == 0
-    assert commits[1].seniority_author == 0
-    assert commits[2].seniority_author == 0
-    assert commits[3].seniority_author == 365
-    assert commits[4].seniority_author == 0
-    assert commits[5].seniority_author == 1
-
-    assert commits[0].touched_prev_total_author_sum == 0
-    assert commits[1].touched_prev_total_author_sum == 0
-    assert commits[2].touched_prev_total_author_sum == 1
-    assert commits[3].touched_prev_total_author_sum == 1
-    assert commits[4].touched_prev_total_author_sum == 0
-    assert commits[5].touched_prev_total_author_sum == 1
-
-    assert commits[0].touched_prev_90_days_author_sum == 0
-    assert commits[1].touched_prev_90_days_author_sum == 0
-    assert commits[2].touched_prev_90_days_author_sum == 1
-    assert commits[3].touched_prev_90_days_author_sum == 0
-    assert commits[4].touched_prev_90_days_author_sum == 0
-    assert commits[5].touched_prev_90_days_author_sum == 1
-
-    assert commits[0].touched_prev_total_reviewer_sum == 0
-    assert commits[0].touched_prev_total_reviewer_max == 0
-    assert commits[0].touched_prev_total_reviewer_min == 0
-    assert commits[1].touched_prev_total_reviewer_sum == 1
-    assert commits[1].touched_prev_total_reviewer_max == 1
-    assert commits[1].touched_prev_total_reviewer_min == 1
-    assert commits[2].touched_prev_total_reviewer_sum == 1
-    assert commits[2].touched_prev_total_reviewer_max == 1
-    assert commits[2].touched_prev_total_reviewer_min == 1
-    assert commits[3].touched_prev_total_reviewer_sum == 4
-    assert commits[3].touched_prev_total_reviewer_max == 2
-    assert commits[3].touched_prev_total_reviewer_min == 2
-    assert commits[4].touched_prev_total_reviewer_sum == 0
-    assert commits[4].touched_prev_total_reviewer_max == 0
-    assert commits[4].touched_prev_total_reviewer_min == 0
-    assert commits[5].touched_prev_total_reviewer_sum == 1
-    assert commits[5].touched_prev_total_reviewer_max == 1
-    assert commits[5].touched_prev_total_reviewer_min == 1
-
-    assert commits[0].touched_prev_90_days_reviewer_sum == 0
-    assert commits[0].touched_prev_90_days_reviewer_max == 0
-    assert commits[0].touched_prev_90_days_reviewer_min == 0
-    assert commits[1].touched_prev_90_days_reviewer_sum == 1
-    assert commits[1].touched_prev_90_days_reviewer_max == 1
-    assert commits[1].touched_prev_90_days_reviewer_min == 1
-    assert commits[2].touched_prev_90_days_reviewer_sum == 1
-    assert commits[2].touched_prev_90_days_reviewer_max == 1
-    assert commits[2].touched_prev_90_days_reviewer_min == 1
-    assert commits[3].touched_prev_90_days_reviewer_sum == 0
-    assert commits[3].touched_prev_90_days_reviewer_max == 0
-    assert commits[3].touched_prev_90_days_reviewer_min == 0
-    assert commits[4].touched_prev_90_days_reviewer_sum == 0
-    assert commits[4].touched_prev_90_days_reviewer_max == 0
-    assert commits[4].touched_prev_90_days_reviewer_min == 0
-    assert commits[5].touched_prev_90_days_reviewer_sum == 1
-    assert commits[5].touched_prev_90_days_reviewer_max == 1
-    assert commits[5].touched_prev_90_days_reviewer_min == 1
-
-    assert commits[0].touched_prev_total_file_sum == 0
-    assert commits[0].touched_prev_total_file_max == 0
-    assert commits[0].touched_prev_total_file_min == 0
-    assert commits[1].touched_prev_total_file_sum == 1
-    assert commits[1].touched_prev_total_file_max == 1
-    assert commits[1].touched_prev_total_file_min == 1
-    assert commits[2].touched_prev_total_file_sum == 1
-    assert commits[2].touched_prev_total_file_max == 1
-    assert commits[2].touched_prev_total_file_min == 0
-    assert commits[3].touched_prev_total_file_sum == 2
-    assert commits[3].touched_prev_total_file_max == 2
-    assert commits[3].touched_prev_total_file_min == 0
-    assert commits[4].touched_prev_total_file_sum == 3
-    assert commits[4].touched_prev_total_file_max == 3
-    assert commits[4].touched_prev_total_file_min == 3
-    assert commits[5].touched_prev_total_file_sum == 4
-    assert commits[5].touched_prev_total_file_max == 4
-    assert commits[5].touched_prev_total_file_min == 3
-
-    assert commits[0].touched_prev_90_days_file_sum == 0
-    assert commits[0].touched_prev_90_days_file_max == 0
-    assert commits[0].touched_prev_90_days_file_min == 0
-    assert commits[1].touched_prev_90_days_file_sum == 1
-    assert commits[1].touched_prev_90_days_file_max == 1
-    assert commits[1].touched_prev_90_days_file_min == 1
-    assert commits[2].touched_prev_90_days_file_sum == 1
-    assert commits[2].touched_prev_90_days_file_max == 1
-    assert commits[2].touched_prev_90_days_file_min == 0
-    assert commits[3].touched_prev_90_days_file_sum == 0
-    assert commits[3].touched_prev_90_days_file_max == 0
-    assert commits[3].touched_prev_90_days_file_min == 0
-    assert commits[4].touched_prev_90_days_file_sum == 1
-    assert commits[4].touched_prev_90_days_file_max == 1
-    assert commits[4].touched_prev_90_days_file_min == 1
-    assert commits[5].touched_prev_90_days_file_sum == 2
-    assert commits[5].touched_prev_90_days_file_max == 2
-    assert commits[5].touched_prev_90_days_file_min == 1
-
-    assert commits[0].touched_prev_total_directory_sum == 0
-    assert commits[0].touched_prev_total_directory_max == 0
-    assert commits[0].touched_prev_total_directory_min == 0
-    assert commits[1].touched_prev_total_directory_sum == 1
-    assert commits[1].touched_prev_total_directory_max == 1
-    assert commits[1].touched_prev_total_directory_min == 1
-    assert commits[2].touched_prev_total_directory_sum == 2
-    assert commits[2].touched_prev_total_directory_max == 2
-    assert commits[2].touched_prev_total_directory_min == 1
-    assert commits[3].touched_prev_total_directory_sum == 3
-    assert commits[3].touched_prev_total_directory_max == 3
-    assert commits[3].touched_prev_total_directory_min == 2
-    assert commits[4].touched_prev_total_directory_sum == 4
-    assert commits[4].touched_prev_total_directory_max == 4
-    assert commits[4].touched_prev_total_directory_min == 4
-    assert commits[5].touched_prev_total_directory_sum == 5
-    assert commits[5].touched_prev_total_directory_max == 5
-    assert commits[5].touched_prev_total_directory_min == 5
-
-    assert commits[0].touched_prev_90_days_directory_sum == 0
-    assert commits[0].touched_prev_90_days_directory_max == 0
-    assert commits[0].touched_prev_90_days_directory_min == 0
-    assert commits[1].touched_prev_90_days_directory_sum == 1
-    assert commits[1].touched_prev_90_days_directory_max == 1
-    assert commits[1].touched_prev_90_days_directory_min == 1
-    assert commits[2].touched_prev_90_days_directory_sum == 2
-    assert commits[2].touched_prev_90_days_directory_max == 2
-    assert commits[2].touched_prev_90_days_directory_min == 1
-    assert commits[3].touched_prev_90_days_directory_sum == 0
-    assert commits[3].touched_prev_90_days_directory_max == 0
-    assert commits[3].touched_prev_90_days_directory_min == 0
-    assert commits[4].touched_prev_90_days_directory_sum == 1
-    assert commits[4].touched_prev_90_days_directory_max == 1
-    assert commits[4].touched_prev_90_days_directory_min == 1
-    assert commits[5].touched_prev_90_days_directory_sum == 2
-    assert commits[5].touched_prev_90_days_directory_max == 2
-    assert commits[5].touched_prev_90_days_directory_min == 2
-
-    assert commits[0].touched_prev_total_component_sum == 0
-    assert commits[0].touched_prev_total_component_max == 0
-    assert commits[0].touched_prev_total_component_min == 0
-    assert commits[1].touched_prev_total_component_sum == 1
-    assert commits[1].touched_prev_total_component_max == 1
-    assert commits[1].touched_prev_total_component_min == 1
-    assert commits[2].touched_prev_total_component_sum == 1
-    assert commits[2].touched_prev_total_component_max == 1
-    assert commits[2].touched_prev_total_component_min == 0
-    assert commits[3].touched_prev_total_component_sum == 3
-    assert commits[3].touched_prev_total_component_max == 2
-    assert commits[3].touched_prev_total_component_min == 2
-    assert commits[4].touched_prev_total_component_sum == 3
-    assert commits[4].touched_prev_total_component_max == 3
-    assert commits[4].touched_prev_total_component_min == 3
-    assert commits[5].touched_prev_total_component_sum == 4
-    assert commits[5].touched_prev_total_component_max == 4
-    assert commits[5].touched_prev_total_component_min == 4
-
-    assert commits[0].touched_prev_90_days_component_sum == 0
-    assert commits[0].touched_prev_90_days_component_max == 0
-    assert commits[0].touched_prev_90_days_component_min == 0
-    assert commits[1].touched_prev_90_days_component_sum == 1
-    assert commits[1].touched_prev_90_days_component_max == 1
-    assert commits[1].touched_prev_90_days_component_min == 1
-    assert commits[2].touched_prev_90_days_component_sum == 1
-    assert commits[2].touched_prev_90_days_component_max == 1
-    assert commits[2].touched_prev_90_days_component_min == 0
-    assert commits[3].touched_prev_90_days_component_sum == 0
-    assert commits[3].touched_prev_90_days_component_max == 0
-    assert commits[3].touched_prev_90_days_component_min == 0
-    assert commits[4].touched_prev_90_days_component_sum == 1
-    assert commits[4].touched_prev_90_days_component_max == 1
-    assert commits[4].touched_prev_90_days_component_min == 1
-    assert commits[5].touched_prev_90_days_component_sum == 2
-    assert commits[5].touched_prev_90_days_component_max == 2
-    assert commits[5].touched_prev_90_days_component_min == 2
+    repository.calculate_experiences(list(commits.values()))
+
+    assert commits["commit1"].seniority_author == 0
+    assert commits["commitbackedout"].seniority_author == 0
+    assert commits["commit2"].seniority_author == 0
+    assert commits["commit3"].seniority_author == 0
+    assert commits["commit4"].seniority_author == 365
+    assert commits["commit5"].seniority_author == 0
+    assert commits["commit6"].seniority_author == 1
+
+    assert commits["commit1"].touched_prev_total_author_sum == 0
+    assert commits["commit2"].touched_prev_total_author_sum == 0
+    assert commits["commit3"].touched_prev_total_author_sum == 1
+    assert commits["commit4"].touched_prev_total_author_sum == 1
+    assert commits["commit5"].touched_prev_total_author_sum == 0
+    assert commits["commit6"].touched_prev_total_author_sum == 1
+
+    assert commits["commit1"].touched_prev_90_days_author_sum == 0
+    assert commits["commit2"].touched_prev_90_days_author_sum == 0
+    assert commits["commit3"].touched_prev_90_days_author_sum == 1
+    assert commits["commit4"].touched_prev_90_days_author_sum == 0
+    assert commits["commit5"].touched_prev_90_days_author_sum == 0
+    assert commits["commit6"].touched_prev_90_days_author_sum == 1
+
+    assert commits["commit1"].touched_prev_total_reviewer_sum == 0
+    assert commits["commit1"].touched_prev_total_reviewer_max == 0
+    assert commits["commit1"].touched_prev_total_reviewer_min == 0
+    assert commits["commit2"].touched_prev_total_reviewer_sum == 1
+    assert commits["commit2"].touched_prev_total_reviewer_max == 1
+    assert commits["commit2"].touched_prev_total_reviewer_min == 1
+    assert commits["commit3"].touched_prev_total_reviewer_sum == 1
+    assert commits["commit3"].touched_prev_total_reviewer_max == 1
+    assert commits["commit3"].touched_prev_total_reviewer_min == 1
+    assert commits["commit4"].touched_prev_total_reviewer_sum == 4
+    assert commits["commit4"].touched_prev_total_reviewer_max == 2
+    assert commits["commit4"].touched_prev_total_reviewer_min == 2
+    assert commits["commit5"].touched_prev_total_reviewer_sum == 0
+    assert commits["commit5"].touched_prev_total_reviewer_max == 0
+    assert commits["commit5"].touched_prev_total_reviewer_min == 0
+    assert commits["commit6"].touched_prev_total_reviewer_sum == 1
+    assert commits["commit6"].touched_prev_total_reviewer_max == 1
+    assert commits["commit6"].touched_prev_total_reviewer_min == 1
+
+    assert commits["commit1"].touched_prev_90_days_reviewer_sum == 0
+    assert commits["commit1"].touched_prev_90_days_reviewer_max == 0
+    assert commits["commit1"].touched_prev_90_days_reviewer_min == 0
+    assert commits["commit2"].touched_prev_90_days_reviewer_sum == 1
+    assert commits["commit2"].touched_prev_90_days_reviewer_max == 1
+    assert commits["commit2"].touched_prev_90_days_reviewer_min == 1
+    assert commits["commit3"].touched_prev_90_days_reviewer_sum == 1
+    assert commits["commit3"].touched_prev_90_days_reviewer_max == 1
+    assert commits["commit3"].touched_prev_90_days_reviewer_min == 1
+    assert commits["commit4"].touched_prev_90_days_reviewer_sum == 0
+    assert commits["commit4"].touched_prev_90_days_reviewer_max == 0
+    assert commits["commit4"].touched_prev_90_days_reviewer_min == 0
+    assert commits["commit5"].touched_prev_90_days_reviewer_sum == 0
+    assert commits["commit5"].touched_prev_90_days_reviewer_max == 0
+    assert commits["commit5"].touched_prev_90_days_reviewer_min == 0
+    assert commits["commit6"].touched_prev_90_days_reviewer_sum == 1
+    assert commits["commit6"].touched_prev_90_days_reviewer_max == 1
+    assert commits["commit6"].touched_prev_90_days_reviewer_min == 1
+
+    assert commits["commit1"].touched_prev_total_file_sum == 0
+    assert commits["commit1"].touched_prev_total_file_max == 0
+    assert commits["commit1"].touched_prev_total_file_min == 0
+    assert commits["commit2"].touched_prev_total_file_sum == 1
+    assert commits["commit2"].touched_prev_total_file_max == 1
+    assert commits["commit2"].touched_prev_total_file_min == 1
+    assert commits["commit3"].touched_prev_total_file_sum == 1
+    assert commits["commit3"].touched_prev_total_file_max == 1
+    assert commits["commit3"].touched_prev_total_file_min == 0
+    assert commits["commit4"].touched_prev_total_file_sum == 2
+    assert commits["commit4"].touched_prev_total_file_max == 2
+    assert commits["commit4"].touched_prev_total_file_min == 0
+    assert commits["commit5"].touched_prev_total_file_sum == 3
+    assert commits["commit5"].touched_prev_total_file_max == 3
+    assert commits["commit5"].touched_prev_total_file_min == 3
+    assert commits["commit6"].touched_prev_total_file_sum == 4
+    assert commits["commit6"].touched_prev_total_file_max == 4
+    assert commits["commit6"].touched_prev_total_file_min == 3
+
+    assert commits["commit1"].touched_prev_90_days_file_sum == 0
+    assert commits["commit1"].touched_prev_90_days_file_max == 0
+    assert commits["commit1"].touched_prev_90_days_file_min == 0
+    assert commits["commit2"].touched_prev_90_days_file_sum == 1
+    assert commits["commit2"].touched_prev_90_days_file_max == 1
+    assert commits["commit2"].touched_prev_90_days_file_min == 1
+    assert commits["commit3"].touched_prev_90_days_file_sum == 1
+    assert commits["commit3"].touched_prev_90_days_file_max == 1
+    assert commits["commit3"].touched_prev_90_days_file_min == 0
+    assert commits["commit4"].touched_prev_90_days_file_sum == 0
+    assert commits["commit4"].touched_prev_90_days_file_max == 0
+    assert commits["commit4"].touched_prev_90_days_file_min == 0
+    assert commits["commit5"].touched_prev_90_days_file_sum == 1
+    assert commits["commit5"].touched_prev_90_days_file_max == 1
+    assert commits["commit5"].touched_prev_90_days_file_min == 1
+    assert commits["commit6"].touched_prev_90_days_file_sum == 2
+    assert commits["commit6"].touched_prev_90_days_file_max == 2
+    assert commits["commit6"].touched_prev_90_days_file_min == 1
+
+    assert commits["commit1"].touched_prev_total_directory_sum == 0
+    assert commits["commit1"].touched_prev_total_directory_max == 0
+    assert commits["commit1"].touched_prev_total_directory_min == 0
+    assert commits["commit2"].touched_prev_total_directory_sum == 1
+    assert commits["commit2"].touched_prev_total_directory_max == 1
+    assert commits["commit2"].touched_prev_total_directory_min == 1
+    assert commits["commit3"].touched_prev_total_directory_sum == 2
+    assert commits["commit3"].touched_prev_total_directory_max == 2
+    assert commits["commit3"].touched_prev_total_directory_min == 1
+    assert commits["commit4"].touched_prev_total_directory_sum == 3
+    assert commits["commit4"].touched_prev_total_directory_max == 3
+    assert commits["commit4"].touched_prev_total_directory_min == 2
+    assert commits["commit5"].touched_prev_total_directory_sum == 4
+    assert commits["commit5"].touched_prev_total_directory_max == 4
+    assert commits["commit5"].touched_prev_total_directory_min == 4
+    assert commits["commit6"].touched_prev_total_directory_sum == 5
+    assert commits["commit6"].touched_prev_total_directory_max == 5
+    assert commits["commit6"].touched_prev_total_directory_min == 5
+
+    assert commits["commit1"].touched_prev_90_days_directory_sum == 0
+    assert commits["commit1"].touched_prev_90_days_directory_max == 0
+    assert commits["commit1"].touched_prev_90_days_directory_min == 0
+    assert commits["commit2"].touched_prev_90_days_directory_sum == 1
+    assert commits["commit2"].touched_prev_90_days_directory_max == 1
+    assert commits["commit2"].touched_prev_90_days_directory_min == 1
+    assert commits["commit3"].touched_prev_90_days_directory_sum == 2
+    assert commits["commit3"].touched_prev_90_days_directory_max == 2
+    assert commits["commit3"].touched_prev_90_days_directory_min == 1
+    assert commits["commit4"].touched_prev_90_days_directory_sum == 0
+    assert commits["commit4"].touched_prev_90_days_directory_max == 0
+    assert commits["commit4"].touched_prev_90_days_directory_min == 0
+    assert commits["commit5"].touched_prev_90_days_directory_sum == 1
+    assert commits["commit5"].touched_prev_90_days_directory_max == 1
+    assert commits["commit5"].touched_prev_90_days_directory_min == 1
+    assert commits["commit6"].touched_prev_90_days_directory_sum == 2
+    assert commits["commit6"].touched_prev_90_days_directory_max == 2
+    assert commits["commit6"].touched_prev_90_days_directory_min == 2
+
+    assert commits["commit1"].touched_prev_total_component_sum == 0
+    assert commits["commit1"].touched_prev_total_component_max == 0
+    assert commits["commit1"].touched_prev_total_component_min == 0
+    assert commits["commit2"].touched_prev_total_component_sum == 1
+    assert commits["commit2"].touched_prev_total_component_max == 1
+    assert commits["commit2"].touched_prev_total_component_min == 1
+    assert commits["commit3"].touched_prev_total_component_sum == 1
+    assert commits["commit3"].touched_prev_total_component_max == 1
+    assert commits["commit3"].touched_prev_total_component_min == 0
+    assert commits["commit4"].touched_prev_total_component_sum == 3
+    assert commits["commit4"].touched_prev_total_component_max == 2
+    assert commits["commit4"].touched_prev_total_component_min == 2
+    assert commits["commit5"].touched_prev_total_component_sum == 3
+    assert commits["commit5"].touched_prev_total_component_max == 3
+    assert commits["commit5"].touched_prev_total_component_min == 3
+    assert commits["commit6"].touched_prev_total_component_sum == 4
+    assert commits["commit6"].touched_prev_total_component_max == 4
+    assert commits["commit6"].touched_prev_total_component_min == 4
+
+    assert commits["commit1"].touched_prev_90_days_component_sum == 0
+    assert commits["commit1"].touched_prev_90_days_component_max == 0
+    assert commits["commit1"].touched_prev_90_days_component_min == 0
+    assert commits["commit2"].touched_prev_90_days_component_sum == 1
+    assert commits["commit2"].touched_prev_90_days_component_max == 1
+    assert commits["commit2"].touched_prev_90_days_component_min == 1
+    assert commits["commit3"].touched_prev_90_days_component_sum == 1
+    assert commits["commit3"].touched_prev_90_days_component_max == 1
+    assert commits["commit3"].touched_prev_90_days_component_min == 0
+    assert commits["commit4"].touched_prev_90_days_component_sum == 0
+    assert commits["commit4"].touched_prev_90_days_component_max == 0
+    assert commits["commit4"].touched_prev_90_days_component_min == 0
+    assert commits["commit5"].touched_prev_90_days_component_sum == 1
+    assert commits["commit5"].touched_prev_90_days_component_max == 1
+    assert commits["commit5"].touched_prev_90_days_component_min == 1
+    assert commits["commit6"].touched_prev_90_days_component_sum == 2
+    assert commits["commit6"].touched_prev_90_days_component_max == 2
+    assert commits["commit6"].touched_prev_90_days_component_min == 2