Repository: ishepard/pydriller
Pull request #299 (2024-09-20 16:37:24)

FEAT(CodeChurn): Add method to return lines added and removed per file
This PR introduces a new method, `get_added_and_removed_lines()`, in the `CodeChurn` class that provides the number of lines added and removed per file across commits in a repository. The new method returns a dictionary where each key is the file path, and the value is a tuple `(added_lines, removed_lines)` representing the total added and removed lines for that file.

#### Changes made:
- **New method `get_added_and_removed_lines()`**:
  - Added a method to return a dictionary with file paths as keys and a tuple `(added_lines, removed_lines)` as values.
  - Refactored the `_initialize()` method to store both added and removed lines for each modified file.
- **Updated documentation**:
  - Updated the Code Churn documentation to include the new feature.
- **Added unit tests**:
  - Introduced tests to verify the accuracy of the `get_added_and_removed_lines()` method, ensuring correct results across multiple commits.

#### Impact:
This change enhances the `CodeChurn` metric class by allowing users to directly retrieve detailed information on code churn, specifically the number of lines added and removed for each file.

#### Linked Issue:
This PR is related to and resolves [Issue #298](https://github.com/ishepard/pydriller/issues/298), which requested a feature to provide a way to access the number of lines added and removed per file in a commit.



Patch:
diff --git a/docs/processmetrics.rst b/docs/processmetrics.rst
index 60c99ca..ea36a99 100644
--- a/docs/processmetrics.rst
+++ b/docs/processmetrics.rst
@@ -67,11 +67,12 @@ Depending on the parametrization, a code churn is the sum of either
     
 across the analyzed commits.
 
-The class ``CodeChurn`` has three methods:
+The class ``CodeChurn`` has four methods:
 
 * ``count()`` to count the *total* size of code churns of a file;
 * ``max()`` to count the *maximum* size of a code churn of a file;
-* ``avg()`` to count the *average* size of a code churn of a file. **Note:** The average value is rounded off to the nearest integer.
+* ``avg()`` to count the *average* size of a code churn of a file. **Note:** The average value is rounded off to the nearest integer;
+* ``get_added_and_removed_lines()`` to retrieve the *exact* number of lines added and removed for each file as a tuple (added_lines, removed_lines).
 
 For example::
 
@@ -82,16 +83,21 @@ For example::
     files_count = metric.count()
     files_max = metric.max()
     files_avg = metric.avg()
+    added_removed_lines = metric.get_added_and_removed_lines()
+    
     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))
+    print('Lines added and removed for each file: {}'.format(added_removed_lines))
 
-will print the total, maximum and average number of code churn for each modified file in the evolution period ``[from_commit, to_commit]``. 
+will print the total, maximum, and average number of code churns for each modified file, along with the number of lines added and removed, 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``
 
+To retrieve the added and removed lines for each file directly, the ``get_added_and_removed_lines()`` method can be used, which returns a dictionary with file paths as keys and a tuple (added_lines, removed_lines) as values.
+
 
 Commits Count
 =============
diff --git a/pydriller/metrics/process/code_churn.py b/pydriller/metrics/process/code_churn.py
index 543ff3c..eca255f 100644
--- a/pydriller/metrics/process/code_churn.py
+++ b/pydriller/metrics/process/code_churn.py
@@ -2,7 +2,7 @@
 Module that calculates the number of hunks made to a commit file.
 """
 import statistics
-from typing import Optional
+from typing import Optional, Dict, Tuple
 
 from pydriller import ModificationType
 from pydriller.metrics.process.process_metric import ProcessMetric
@@ -35,10 +35,10 @@ class CodeChurn(ProcessMetric):
         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.added_removed_lines: Dict[str, Tuple[int, int]] = {}
         self._initialize()
 
     def _initialize(self):
-
         renamed_files = {}
         self.files = {}
 
@@ -54,13 +54,25 @@ class CodeChurn(ProcessMetric):
                 if self.ignore_added_files and modified_file.change_type == ModificationType.ADD:
                     continue
 
+                added_lines = modified_file.added_lines
+                deleted_lines = modified_file.deleted_lines
+                self.added_removed_lines[filepath] = (added_lines, deleted_lines)
+
                 if self.add_deleted_lines_to_churn:
-                    churn = modified_file.added_lines + modified_file.deleted_lines
+                    churn = added_lines + deleted_lines
                 else:
-                    churn = modified_file.added_lines - modified_file.deleted_lines
+                    churn = added_lines - deleted_lines
 
                 self.files.setdefault(filepath, []).append(churn)
 
+    def get_added_and_removed_lines(self) -> Dict[str, Tuple[int, int]]:
+        """
+        Returns a dictionary with file paths as keys and a tuple of added and removed lines as values.
+
+        :return: A dictionary where the key is the file path, and the value is a tuple (added_lines, removed_lines).
+        """
+        return self.added_removed_lines
+
     def count(self):
         """
         Return the total number of code churns for each modified file.


Test patch:
diff --git a/tests/metrics/process/test_code_churn.py b/tests/metrics/process/test_code_churn.py
index f21642b..dd92ada 100644
--- a/tests/metrics/process/test_code_churn.py
+++ b/tests/metrics/process/test_code_churn.py
@@ -85,3 +85,14 @@ def test_with_add_deleted_lines_flag():
     assert len(code_churns) == 18
     assert str(Path('domain/__init__.py')) in code_churns
     assert code_churns[str(Path('domain/commit.py'))] == 40
+
+
+def test_get_added_and_removed_lines():
+    metric = CodeChurn(path_to_repo='test-repos/pydriller',
+                       from_commit='ab36bf45859a210b0eae14e17683f31d19eea041',
+                       to_commit='fdf671856b260aca058e6595a96a7a0fba05454b')
+
+    added_removed_lines = metric.get_added_and_removed_lines()
+
+    assert isinstance(added_removed_lines, dict)
+    assert all(isinstance(value, tuple) and len(value) == 2 for value in added_removed_lines.values())