Repository: ishepard/pydriller
Pull request #31 (2019-01-14 05:17:54)

add param to traverse file history
For #30 


Patch:
diff --git a/pydriller/git_repository.py b/pydriller/git_repository.py
index 4433c61..fdd7a7d 100644
--- a/pydriller/git_repository.py
+++ b/pydriller/git_repository.py
@@ -265,3 +265,19 @@ class GitRepository:
         # this covers comments in Java and Python, as well as empty lines. More have to be added!
         return not line or line.startswith('//') or line.startswith('#') or line.startswith("/*") or \
                line.startswith("'''") or line.startswith('"""') or line.startswith("*")
+
+    def get_commits_modified_file(self, filepath: str) -> List[str]:
+        all_commits = self.get_list_commits()
+
+        dict_commits = {}
+        for commit in all_commits:
+            dict_commits[commit.hash] = commit
+
+        path = str(Path(filepath).absolute())
+        commits = []
+        try:
+            commits = self.git.log("--follow", "--format=%H", path).split('\n')
+        except GitCommandError:
+            logger.debug("Could not find information of file %s", path)
+
+        return commits
diff --git a/pydriller/repository_mining.py b/pydriller/repository_mining.py
index e8e514c..b4cd154 100644
--- a/pydriller/repository_mining.py
+++ b/pydriller/repository_mining.py
@@ -38,7 +38,8 @@ class RepositoryMining:
                  only_modifications_with_file_types: List[str] = None,
                  only_no_merge: bool = False,
                  only_authors: List[str] = None,
-                 only_commits: List[str] = None):
+                 only_commits: List[str] = None,
+                 filepath: str = None):
         """
         Init a repository mining. The only required parameter is "path_to_repo": to analyze a single repo, pass the absolute
         path to the repo; if you need to analyze more repos, pass a list of absolute paths.
@@ -79,6 +80,8 @@ class RepositoryMining:
         self._only_no_merge = only_no_merge
         self._only_authors = only_authors
         self._only_commits = only_commits
+        self._filepath = filepath
+        self._filepath_commits = None
 
     def _sanity_check_repos(self, path_to_repo):
         if not isinstance(path_to_repo, str) and not isinstance(path_to_repo, list):
@@ -148,6 +151,9 @@ class RepositoryMining:
 
             logger.info('Analyzing git repository in {}'.format(git_repo.path))
 
+            if self._filepath is not None:
+                self._filepath_commits = git_repo.get_commits_modified_file(self._filepath)
+
             for commit in git_repo.get_list_commits(self._only_in_branch, not self._reversed_order):
                 logger.info('Commit #{} in {} from {}'.format(commit.hash, commit.committer_date, commit.author.name))
 
@@ -178,6 +184,11 @@ class RepositoryMining:
         if self._only_commits is not None and commit.hash not in self._only_commits:
             logger.debug("Commit filtered because it is not one of the specified commits")
             return True
+        if self._filepath is not None:
+            if self._filepath_commits is not None:
+                if commit.hash not in self._filepath_commits:
+                    return True
+
         return False
 
     def _has_modification_with_file_type(self, commit):


Test patch:
diff --git a/tests/test_git_repository.py b/tests/test_git_repository.py
index 7a00c38..1b26d93 100644
--- a/tests/test_git_repository.py
+++ b/tests/test_git_repository.py
@@ -397,3 +397,22 @@ def test_get_commits_last_modified_lines_with_more_modification():
     buggy_commits = gr.get_commits_last_modified_lines(gr.get_commit('c7002fb321a8ba32a28fac200538f7c2ba76f175'))
     assert len(buggy_commits) == 1
     assert '5cb9e9ae44a0949ec91d06a955975289be766f34' in buggy_commits
+
+
+def test_get_commits_modified_file():
+    gr = GitRepository('test-repos/test1/')
+
+    commits = gr.get_commits_modified_file('test-repos/test1/file2.java')
+
+    assert len(commits) == 3
+    assert '09f6182cef737db02a085e1d018963c7a29bde5a' in commits
+    assert '6411e3096dd2070438a17b225f44475136e54e3a' in commits
+    assert 'a88c84ddf42066611e76e6cb690144e5357d132c' in commits
+
+
+def test_get_commits_modified_file_missing_file():
+    gr = GitRepository('test-repos/test1/')
+
+    commits = gr.get_commits_modified_file('test-repos/test1/non-existing-file.java')
+
+    assert len(commits) == 0
diff --git a/tests/test_repository_mining.py b/tests/test_repository_mining.py
index f7a9abd..3318519 100644
--- a/tests/test_repository_mining.py
+++ b/tests/test_repository_mining.py
@@ -67,3 +67,24 @@ def test_badly_formatted_url():
 
     with pytest.raises(Exception):
         list(RepositoryMining(path_to_repo='test').traverse_commits())
+
+
+def test_filepath():
+    dt = datetime(2018, 6, 6)
+    assert 4 == len(list(RepositoryMining(
+        path_to_repo='test-repos/test5',
+        filepath='test-repos/test5/A.java',
+        to=dt).traverse_commits()))
+
+def test_filepath_with_rename():
+    dt = datetime(2018, 6, 6)
+    commits = list(RepositoryMining(
+        path_to_repo='test-repos/test1',
+        filepath='test-repos/test1/file4.java',
+        to=dt).traverse_commits())
+    assert 2 == len(commits)
+
+    commit_hashes = [commit.hash for commit in commits]
+
+    assert 'da39b1326dbc2edfe518b90672734a08f3c13458' in commit_hashes
+    assert 'a88c84ddf42066611e76e6cb690144e5357d132c' in commit_hashes