Repository: ishepard/pydriller

Issue:
Cloned repositories are not deleted when in /tmp
Hello, I was working on a dataset of thousands of commits, involving about hundreds of different repositories. So I decided to use PyDriller.

**Describe the bug**
Before knowing this problem, I was creating an instance of `RepositoryMining`, giving it the list of all remote repositories URLs. I knew that the default clone location would be /tmp... and it was, but this quicky got filled up, slowing down everything.

So I decided to instantiate `RepositoryMining` in a loop, one repository per time.  
I hoped that after all objects needed by PyDriller were built, the cloned repository would be deleted, but it was not.
I also hoped that when another `RepositoryMining` object was instantiated, the previous repo would be deleted, but it was not.

I read on the documentation that they should deleted after each analysis:
>If you pass an URL, PyDriller will automatically create a temporary folder, clone the repository, run the study, and finally delete the temporary folder.

**To Reproduce**
Have /tmp and just give to RepositoryMining a huge number of remote repository such that your /tmp directory is filled up.

**How I fixed**
After having decided to instantiate `RepositoryMining` in a loop (one repository per time), I used `os` and `shutil` libraries to manually delete the cloned repository. In fact, knowing that the directory of the cloned repo has the same repo name, I checked for `/tmp/PROJECT_NAME`.  
I could get PROJECT_NAME from the remote repository URL. 

**OS Version:**
Manjaro Linux, 19.0.2

Discussion:

Patch:
diff --git a/pydriller/domain/commit.py b/pydriller/domain/commit.py
index 571ed67..4c4bf32 100644
--- a/pydriller/domain/commit.py
+++ b/pydriller/domain/commit.py
@@ -478,6 +478,15 @@ class Commit:
         """
         return Path(self._conf.get('path_to_repo')).name
 
+    @property
+    def project_path(self) -> str:
+        """
+        Return the project name.
+
+        :return: project name
+        """
+        return str(Path(self._conf.get('path_to_repo')))
+
     @property
     def author_date(self) -> datetime:
         """
diff --git a/pydriller/repository_mining.py b/pydriller/repository_mining.py
index 97d93e7..63d34a2 100644
--- a/pydriller/repository_mining.py
+++ b/pydriller/repository_mining.py
@@ -18,6 +18,8 @@ This module includes 1 class, RepositoryMining, main class of PyDriller.
 
 import logging
 import os
+import shutil
+import stat
 import tempfile
 from datetime import datetime
 from pathlib import Path
@@ -131,14 +133,16 @@ class RepositoryMining:
         }
         self._conf = Conf(options)
 
+        # If the user provides a directory where to clone the repositories,
+        # make sure we do not delete the directory after the study completes
+        self._cleanup = False if clone_repo_to is not None else True
+
     @staticmethod
     def _is_remote(repo: str) -> bool:
         return repo.startswith("git@") or repo.startswith("https://")
 
-    def _clone_remote_repos(self, tmp_folder: str, repo: str) -> str:
-
-        repo_folder = os.path.join(tmp_folder,
-                                   self._get_repo_name_from_url(repo))
+    def _clone_remote_repo(self, tmp_folder: str, repo: str) -> str:
+        repo_folder = os.path.join(tmp_folder, self._get_repo_name_from_url(repo))
         logger.info("Cloning %s in temporary folder %s", repo, repo_folder)
         Repo.clone_from(url=repo, to_path=repo_folder)
 
@@ -150,7 +154,9 @@ class RepositoryMining:
             if not os.path.isdir(clone_folder):
                 raise Exception("Not a directory: {0}".format(clone_folder))
         else:
-            clone_folder = tempfile.TemporaryDirectory().name
+            # Save the temporary directory so we can clean it up later
+            self._tmp_dir = tempfile.TemporaryDirectory()
+            clone_folder = self._tmp_dir.name
         return clone_folder
 
     def traverse_commits(self) -> Generator[Commit, None, None]:
@@ -159,16 +165,17 @@ class RepositoryMining:
         a generator of commits.
         """
         for path_repo in self._conf.get('path_to_repos'):
+            local_path_repo = path_repo
             if self._is_remote(path_repo):
-                path_repo = self._clone_remote_repos(self._clone_folder(), path_repo)
+                local_path_repo = self._clone_remote_repo(self._clone_folder(), path_repo)
 
-            git_repo = GitRepository(path_repo, self._conf)
+            git_repo = GitRepository(local_path_repo, self._conf)
             # saving the GitRepository object for further use
             self._conf.set_value("git_repo", git_repo)
 
             # when multiple repos are given in input, this variable will serve as a reminder
             # of which one we are currently analyzing
-            self._conf.set_value('path_to_repo', path_repo)
+            self._conf.set_value('path_to_repo', local_path_repo)
 
             # checking that the filters are set correctly
             self._conf.sanity_check_filters()
@@ -202,6 +209,22 @@ class RepositoryMining:
             self._conf.set_value("git_repo", None)
             git_repo.clear()
 
+            # delete the temporary directory if created
+            if self._is_remote(path_repo) and self._cleanup is True:
+                assert self._tmp_dir is not None
+                try:
+                    self._tmp_dir.cleanup()
+                except PermissionError:
+                    # on Windows, Python 3.5, 3.6, 3.7 are not able to delete
+                    # git directories because of read-only files. This is now fixed
+                    # in python 3.8. In this case, we need to use an
+                    # onerror callback to clear the read-only bit.
+                    # see https://docs.python.org/3/library/shutil.html?highlight=shutil#rmtree-example
+                    def remove_readonly(func, path, _):
+                        os.chmod(path, stat.S_IWRITE)
+                        func(path)
+                    shutil.rmtree(self._tmp_dir.name, onerror=remove_readonly)
+
     @staticmethod
     def _get_repo_name_from_url(url: str) -> str:
         last_slash_index = url.rfind("/")


Test patch:
diff --git a/tests/test_repository_mining.py b/tests/test_repository_mining.py
index 88b77ef..df1615f 100644
--- a/tests/test_repository_mining.py
+++ b/tests/test_repository_mining.py
@@ -1,6 +1,6 @@
 import logging
 from datetime import datetime
-
+import os
 import pytest
 
 from pydriller import RepositoryMining, GitRepository
@@ -228,6 +228,7 @@ def test_clone_repo_to(tmp_path):
         path_to_repo=url,
         to=dt2,
         clone_repo_to=str(tmp_path)).traverse_commits())) == 159
+    assert tmp_path.exists() is True
 
 
 def test_clone_repo_to_not_existing():
@@ -253,3 +254,16 @@ def test_projectname_multiple_repos_remote():
     ]
     for commit in RepositoryMining(path_to_repo=repos).traverse_commits():
         assert commit.project_name == 'pydriller'
+
+
+def test_deletion_remotes():
+    repos = [
+        'https://github.com/ishepard/pydriller',
+        'https://github.com/ishepard/pydriller'
+    ]
+    paths = set()
+    for commit in RepositoryMining(path_to_repo=repos).traverse_commits():
+        paths.add(commit.project_path)
+
+    for path in paths:
+        assert os.path.exists(path) is False