Repository: ishepard/pydriller
Pull request #98 (2020-03-06 10:57:39)

Added process metrics for Code churns
Metrics from paper [A Comparative Analysis of the Efficiency of Change Metrics and Static Code Attributes for Defect Prediction](https://dl.acm.org/doi/pdf/10.1145/1368088.1368114).

Added:

* ```count()``` - Sum of (added lines of code – deleted lines of code) over all revisions. In our case, revisions are the commits in the period ```[from_commit, to_commit]```

* ```max()``` - maximum code churn for all revisions. In our case, revisions are the commits in the period ```[from_commit, to_commit]```

* ```avg()``` - average code churn over all revisions. In our case, revisions are the commits in the period ```[from_commit, to_commit]```


Patch:
diff --git a/docs/index.rst b/docs/index.rst
index f1175fd..372716f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -15,10 +15,10 @@ PyDriller documentation!
    commit
    modifications
    gitrepository
+   processmetrics
    reference
 
 
-
 Indices and tables
 ==================
 
diff --git a/docs/processmetrics.rst b/docs/processmetrics.rst
new file mode 100644
index 0000000..fcec1b8
--- /dev/null
+++ b/docs/processmetrics.rst
@@ -0,0 +1,117 @@
+.. highlight:: python
+
+==================
+Process Metrics
+==================
+
+Process metrics capture aspects of the development process rather than aspects about the code itself.
+From release 1.11 PyDriller can calculate ``code churn``, ``commits count``, ``contributors count``, ``contributors experience``, ``history complexity``, ``hunks count``, ``lines count`` and ``minor contributors``. Everything in just one line!
+
+Below an example of how call the metrics.
+
+
+Code Churn
+===============
+
+This metric measures the number of commits made to a file.
+For example::
+
+    from pydriller.metrics.process.code_churn import CodeChurn
+    metric = CodeChurn(path_to_repo='path/to/the/repo',
+                       from_commit='from commit hash',
+                       to_commit='to commit hash')
+    files_count = metric.count()
+    files_max = metric.max()
+    files_avg = metric.avg()
+    print('Total code churn for each file: {}'.format(files_count))
+    print('Maximum code churn for each file: {}'.format(files_max))
+    print('Average code churn for each file: {}'.format(files_avg))
+
+will print the total, maximum and average number of code churn for each modified file in the evolution period ``[from_commit, to_commit]``. 
+
+
+Commits Count
+===============
+
+This metric measures the number of commits made to a file.
+For example::
+
+    from pydriller.metrics.process.commits_count import CommitsCount
+    metric = CommitsCount(path_to_repo='path/to/the/repo',
+                          from_commit='from commit hash',
+                          to_commit='to commit hash')
+    files = metric.count()
+    print('Files: {}'.format(files))
+
+will print the number of commits for each modified file in the evolution period ``[from_commit, to_commit]``. 
+
+
+
+Contributors Count
+===============
+
+This metric measures the number of developers that contributed to a file.
+For example::
+
+    from pydriller.metrics.process.contributors_count import ContributorsCount
+    metric = ContributorsCount(path_to_repo='path/to/the/repo',
+                               from_commit='from commit hash',
+                               to_commit='to commit hash')
+    files = metric.count()
+    print('Files: {}'.format(files))
+
+will print the number of developers that contributed to each of the modified file in the evolution period ``[from_commit, to_commit]``. 
+
+
+
+Contributors Experience
+===============
+
+This metric measures the percetage of the lines authored by the highest contributor of a file.
+For example::
+
+    from pydriller.metrics.process.contributors_experience import ContributorsExperience
+    metric = ContributorsExperience(path_to_repo='path/to/the/repo',
+                          	    from_commit='from commit hash',
+                                    to_commit='to commit hash')
+    files = metric.count()
+    print('Files: {}'.format(files))
+
+will print the percentage of the lines authored by the highest contributor for each of the modified file in the evolution period ``[from_commit, to_commit]``. 
+
+
+
+Hunks Count
+===============
+
+This metric measures the number of hunks made to a file.
+As a hunk is a continuous block of changes in a ``diff``, this number assesses how fragmented the commit file is (i.e. lots of changes all over the file versus one big change).
+For example::
+
+    from pydriller.metrics.process.hunks_count import HunksCount
+    metric = HunksCount(path_to_repo='path/to/the/repo',
+                        from_commit='from commit hash',
+                        to_commit='to commit hash')
+    files = metric.count()
+    print('Files: {}'.format(files))
+
+will print the median number of hunks for each of the modified file in the evolution period ``[from_commit, to_commit]``. 
+
+
+
+Lines Count
+===============
+
+This metric measures the number of added and removed lines in a file.
+For example::
+
+    from pydriller.metrics.process.lines_count import LinesCount
+    metric = LinesCount(path_to_repo='path/to/the/repo',
+                        from_commit='from commit hash',
+                        to_commit='to commit hash')
+    files = metric.count()
+    print('Files: {}'.format(files))
+
+will print a dictionary ``{'added': int, 'removed': int}`` of added and removed lines for each of the modified file in the evolution period ``[from_commit, to_commit]``. 
+
+
diff --git a/pydriller/metrics/process/code_churn.py b/pydriller/metrics/process/code_churn.py
new file mode 100644
index 0000000..2708faf
--- /dev/null
+++ b/pydriller/metrics/process/code_churn.py
@@ -0,0 +1,80 @@
+"""
+Module that calculates the number of hunks made to a commit file.
+"""
+import statistics
+
+from pydriller import ModificationType
+from pydriller.metrics.process.process_metric import ProcessMetric
+
+class CodeChurn(ProcessMetric):
+    """
+    This class is responsible to implement the Code Churn metric for a
+    file.
+    A code churn is the sum of (added lines - removed lines) across the
+    analyzed commits. 
+    It allows to count for the:
+    * total number of code churns - count();
+    * maximum code churn for all commits - max();
+    * average code churn per commit.
+    """
+
+    def __init__(self, path_to_repo: str,
+                 from_commit: str,
+                 to_commit: str):
+        super().__init__(path_to_repo, from_commit, to_commit)
+        self.__initialize()
+
+
+    def __initialize(self):
+
+        renamed_files = {}
+        self.files = {}
+
+        for commit in self.repo_miner.traverse_commits():
+
+            for modified_file in commit.modifications:
+
+                filepath = renamed_files.get(modified_file.new_path,
+                                                modified_file.new_path)
+
+                if modified_file.change_type == ModificationType.RENAME:
+                    renamed_files[modified_file.old_path] = filepath
+
+                churn = modified_file.added - modified_file.removed
+                self.files.setdefault(filepath, []).append(churn)
+
+    def count(self):
+        """
+        Return the total number of code churns for each modified file.
+
+        :return: int number of churns
+        """
+        count = dict()
+        for path, churns in self.files.items():
+            count[path] = sum(churns)
+
+        return count
+
+    def max(self):
+        """
+        Return the maximum code churn for each modified file.
+
+        :return: int max number of churns
+        """
+        max_count = dict()
+        for path, churns in self.files.items():
+            max_count[path] = max(churns)
+
+        return max_count
+
+    def avg(self):
+        """
+        Return the average number of code churns for each modified file.
+
+        :return: int avg number of churns rounded off to the nearest integer
+        """
+        avg_count = dict()
+        for path, churns in self.files.items():
+            avg_count[path] = round(statistics.mean(churns))
+
+        return avg_count


Test patch:
diff --git a/tests/metrics/process/test_code_churn.py b/tests/metrics/process/test_code_churn.py
new file mode 100644
index 0000000..9010c78
--- /dev/null
+++ b/tests/metrics/process/test_code_churn.py
@@ -0,0 +1,23 @@
+import pytest
+from pathlib import Path
+from pydriller.metrics.process.code_churn import CodeChurn
+
+TEST_DATA = [
+    ('test-repos/pydriller', 'domain/commit.py', 'ab36bf45859a210b0eae14e17683f31d19eea041', '71e053f61fc5d31b3e31eccd9c79df27c31279bf', 47, 34, 16)
+]
+
+@pytest.mark.parametrize('path_to_repo, filepath, from_commit, to_commit, expected_count, expected_max, expected_avg', TEST_DATA)
+def test(path_to_repo, filepath, from_commit, to_commit, expected_count, expected_max, expected_avg):
+    metric = CodeChurn(path_to_repo=path_to_repo,
+                       from_commit=from_commit,
+                       to_commit=to_commit)
+
+    actual_count = metric.count()
+    actual_max = metric.max()
+    actual_avg = metric.avg()
+
+    filepath = str(Path(filepath))
+
+    assert actual_count[filepath] == expected_count
+    assert actual_max[filepath] == expected_max
+    assert actual_avg[filepath] == expected_avg