Repository: ishepard/pydriller
Pull request #176 (2021-05-17 18:59:08)

Choose calculation variant to calculate the code churn metric
Hi @ishepard,

as propsed in #175 I have implemented another variant of the churn metric calculation, that can be configured in the initialization of the `CodeChurn` class:

```python
def __init__(self, path_to_repo: str,
                 since=None,
                 to=None,
                 from_commit: str = None,
                 to_commit: str = None,
                 ignore_added_files=False,
                 add_deleted_lines_to_churn=False):
```

To ensure that this new calculation will not break anything, `add_deleted_lines_to_churn ` is set to `False` per default.

The calculation variant switch is trivial:

```python
if self.add_deleted_lines_to_churn:
        churn = modified_file.added_lines + modified_file.deleted_lines
else:
        churn = modified_file.added_lines - modified_file.deleted_lines
```

I've added the corresponding unit test `test_with_add_deleted_lines_flag()` in `test_code_churn.py` that proves the correct behavior of the new variant.

I've also added/modified a part of the documentation (`processmetrics.rst`) to include the new variant and the parameter.

I hope I haven't missed anything else.


Patch:
diff --git a/docs/processmetrics.rst b/docs/processmetrics.rst
index d0f0b0f..60c99ca 100644
--- a/docs/processmetrics.rst
+++ b/docs/processmetrics.rst
@@ -59,7 +59,13 @@ Code Churn
 ==========
 
 This metric measures the code churns of a file.
-A code churn is the sum of (added lines - removed lines) across the analyzed commits.
+
+Depending on the parametrization, a code churn is the sum of either 
+    
+    (a) (added lines - removed lines) or 
+    (b) (added lines + removed lines)
+    
+across the analyzed commits.
 
 The class ``CodeChurn`` has three methods:
 
@@ -82,6 +88,10 @@ For example::
 
 will print the total, maximum and average number of code churn for each modified file in the evolution period ``[from_commit, to_commit]``. 
 
+The calculation variant (a) or (b) can be configured by setting the ``CodeChurn`` init parameter:
+
+* ``add_deleted_lines_to_churn``
+
 
 Commits Count
 =============
diff --git a/pydriller/metrics/process/code_churn.py b/pydriller/metrics/process/code_churn.py
index f350d9d..52ef056 100644
--- a/pydriller/metrics/process/code_churn.py
+++ b/pydriller/metrics/process/code_churn.py
@@ -9,11 +9,11 @@ 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:
+    This class is responsible to implement the Code Churn metric for a file.
+    Depending on the parametrization of this class, a code churn is the sum of either
+    (added lines - removed lines) or
+    (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.
@@ -24,13 +24,16 @@ class CodeChurn(ProcessMetric):
                  to=None,
                  from_commit: str = None,
                  to_commit: str = None,
-                 ignore_added_files=False):
+                 ignore_added_files=False,
+                 add_deleted_lines_to_churn=False):
         """
         :ignore_added_files: if True, do not count churns for files when created
+        :add_deleted_lines_to_churn: if True, also add deleted lines to churn calculation
         """
 
         super().__init__(path_to_repo, since=since, to=to, from_commit=from_commit, to_commit=to_commit)
         self.ignore_added_files = ignore_added_files
+        self.add_deleted_lines_to_churn = add_deleted_lines_to_churn
         self._initialize()
 
     def _initialize(self):
@@ -50,7 +53,11 @@ class CodeChurn(ProcessMetric):
                 if self.ignore_added_files and modified_file.change_type == ModificationType.ADD:
                     continue
 
-                churn = modified_file.added_lines - modified_file.deleted_lines
+                if self.add_deleted_lines_to_churn:
+                    churn = modified_file.added_lines + modified_file.deleted_lines
+                else:
+                    churn = modified_file.added_lines - modified_file.deleted_lines
+
                 self.files.setdefault(filepath, []).append(churn)
 
     def count(self):


Test patch:
diff --git a/tests/metrics/process/test_code_churn.py b/tests/metrics/process/test_code_churn.py
index 6bff44b..f21642b 100644
--- a/tests/metrics/process/test_code_churn.py
+++ b/tests/metrics/process/test_code_churn.py
@@ -71,3 +71,17 @@ def test_with_flag():
     assert len(code_churns) == 7
     assert str(Path('domain/__init__.py')) not in code_churns
     assert code_churns[str(Path('domain/commit.py'))] == 0
+
+
+def test_with_add_deleted_lines_flag():
+    metric = CodeChurn(path_to_repo='test-repos/pydriller',
+                       from_commit='ab36bf45859a210b0eae14e17683f31d19eea041',
+                       to_commit='fdf671856b260aca058e6595a96a7a0fba05454b',
+                       ignore_added_files=False,
+                       add_deleted_lines_to_churn=True)
+
+    code_churns = metric.count()
+
+    assert len(code_churns) == 18
+    assert str(Path('domain/__init__.py')) in code_churns
+    assert code_churns[str(Path('domain/commit.py'))] == 40