Repository: ishepard/pydriller (python)

Issue:
to_commit and from_commit no longer support HEAD
There have been some recent changes to traverse_commits options that made some of my code break. I've tried quickly fiddling with the options, but it didn't help.

In the end, after debugging pydriller's code, I found out that the problem is that passing HEAD to either to_commit or from_commit breaks a lot of pydriller's assumptions.

I think HEAD (and similar references) should be supported, or clear errors should be spit out.

Pull request description:
now HEAD can be used in filters
Fix #90.

Now we can use:

```
for commit in RepositoryMining('.', single="HEAD").traverse_commits():
```
or
```
for commit in RepositoryMining('.', from_commit="HEAD").traverse_commits():
```
or
```
for commit in RepositoryMining('.', to_commit="HEAD").traverse_commits():
```

Patch:
diff --git a/.gitignore b/.gitignore
index 7162a6a..e22df51 100644
--- a/.gitignore
+++ b/.gitignore
@@ -220,4 +220,5 @@ _templates
 # Ignore all local history of files
 .history
 
-# End of https://www.gitignore.io/api/visualstudiocode
\ No newline at end of file
+# End of https://www.gitignore.io/api/visualstudiocode
+.vscode
diff --git a/docs/configuration.rst b/docs/configuration.rst
index d648c0a..7606519 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -38,18 +38,22 @@ By default, PyDriller analyzes all the commits in the repository. However, filte
 *FROM*:
 
 * **since** *(datetime)*: only commits after this date will be analyzed
-* **from_commit** *(str)*: only commits after this commit hash will be analyzed
-* **from_tag** *(str)*: only commits after this commit tag will be analyzed
+* **from\_commit** *(str)*: only commits after this commit hash will be analyzed
+* **from\_tag** *(str)*: only commits after this commit tag will be analyzed
 
 *TO*:
 
 * **to** *(datetime)*: only commits up to this date will be analyzed
-* **to_commit** *(str)*: only commits up to this commit hash will be analyzed
-* **to_tag** *(str)*: only commits up to this commit tag will be analyzed
+* **to\_commit** *(str)*: only commits up to this commit hash will be analyzed
+* **to\_tag** *(str)*: only commits up to this commit tag will be analyzed
 
 *ORDER*:
 
-* **reversed\_order** *(bool)*: by default PyDriller returns the commits in chronological order (from the oldest to the newest, the contrary of `git log`). If you need viceversa instead, put this field to **True**. 
+* **reversed\_order** *(bool)*: by default PyDriller returns the commits in chronological order (from the oldest to the newest, the contrary of `git log`). If you need viceversa instead, put this field to **True**.
+ Note: If you use *reversed\_order* with *from\_commit* or *to\_commit*, be sure to double check their order! For example, if we 
+ have 4 commits: A -> B -> C -> D, and you apply the filter *from\_commit=B*, you will obtain: B -> C -> D. 
+ With *reverse\_order=True*, normally you would obtain D -> C -> B -> A, while with the filter *from\_commit=B*, 
+ you will only obtain B -> A. Two completely different results! 
 
 Examples::
 
diff --git a/pydriller/utils/conf.py b/pydriller/utils/conf.py
index 64bec22..f8442dd 100644
--- a/pydriller/utils/conf.py
+++ b/pydriller/utils/conf.py
@@ -6,6 +6,7 @@ import logging
 from datetime import datetime
 
 import pytz
+from gitdb.exc import BadName
 
 from pydriller.domain.commit import Commit
 
@@ -19,6 +20,7 @@ class Conf:
     It's also responsible for checking whether the filters are correct (i.e.,
     the user did not specify 2 starting commits).
     """
+
     def __init__(self, options):
         # insert all the configurations in a local dictionary
         self._options = {}
@@ -92,6 +94,13 @@ class Conf:
                     self.get('to_tag')]):
                 raise Exception('You can not specify a single commit with '
                                 'other filters')
+            try:
+                self.set_value('single', self.get("git_repo").get_commit(
+                    self.get('single')).hash)
+            except BadName:
+                raise Exception("The commit {} defined in "
+                                "the 'single' filtered does "
+                                "not exist".format(self.get('single')))
 
     def _check_correct_filters_order(self):
         """
@@ -133,6 +142,14 @@ class Conf:
         if self.get('from_tag') is not None:
             self.set_value('from_commit', self.get(
                 "git_repo").get_commit_from_tag(self.get('from_tag')).hash)
+        if self.get('from_commit'):
+            try:
+                self.set_value('from_commit', self.get("git_repo").get_commit(
+                    self.get('from_commit')).hash)
+            except BadName:
+                raise Exception("The commit {} defined in the 'from_tag' "
+                                "or 'from_commit' filter does "
+                                "not exist".format(self.get('single')))
 
     def check_ending_commit(self):
         """
@@ -146,6 +163,14 @@ class Conf:
         if self.get('to_tag') is not None:
             self.set_value('to_commit', self.get(
                 "git_repo").get_commit_from_tag(self.get('to_tag')).hash)
+        if self.get('to_commit'):
+            try:
+                self.set_value('to_commit', self.get("git_repo").get_commit(
+                    self.get('to_commit')).hash)
+            except BadName as e:
+                raise Exception("The commit {} defined in the 'to_tag' "
+                                "or 'to_commit' filter does "
+                                "not exist".format(self.get('single')))
 
     @staticmethod
     def only_one_filter(arr):
@@ -172,7 +197,7 @@ class Conf:
                          'the defined in single')
             return True
         if (self.get('since') is not None and
-                commit.committer_date < self.get('since')) or \
+            commit.committer_date < self.get('since')) or \
                 (self.get('to') is not None and
                  commit.committer_date > self.get('to')):
             return True


Test patch:
diff --git a/tests/integration/test_commit_filters.py b/tests/integration/test_commit_filters.py
index 1152fb4..b5dfcc4 100644
--- a/tests/integration/test_commit_filters.py
+++ b/tests/integration/test_commit_filters.py
@@ -185,10 +185,40 @@ def test_single_commit():
     assert lc[0].hash == "ffccf1e7497eb8136fd66ed5e42bef29677c4b71"
 
 
-def test_single_commit_not_existing():
+def test_single_commit_head():
     lc = list(RepositoryMining('test-repos/complex_repo',
-                               single="ffccf1e7497eb813ASD66ed5e42bef29677c4b71").traverse_commits())
-    assert len(lc) == 0
+                               single="e7d13b0511f8a176284ce4f92ed8c6e8d09c77f2").traverse_commits())
+    assert len(lc) == 1
+
+    lc_head = list(RepositoryMining('test-repos/complex_repo',
+                               single="HEAD").traverse_commits())
+    assert len(lc_head) == 1
+    assert lc[0].hash == lc_head[0].hash
+
+
+def test_single_commit_not_existing_single_commit():
+    with pytest.raises(Exception):
+        list(RepositoryMining('test-repos/complex_repo',
+                              single="ASD").traverse_commits())
+
+
+def test_single_commit_not_existing_from_commit():
+    with pytest.raises(Exception):
+        list(RepositoryMining('test-repos/complex_repo',
+                              from_commit="ASD").traverse_commits())
+
+
+def test_single_commit_not_existing_to_commit():
+    with pytest.raises(Exception):
+        list(RepositoryMining('test-repos/complex_repo',
+                              to_commit="ASD").traverse_commits())
+
+
+def test_single_commit_not_existing_from_and_to_commit():
+    with pytest.raises(Exception):
+        list(RepositoryMining('test-repos/complex_repo',
+                              from_commit="ASD",
+                              to_commit="ASD").traverse_commits())
 
 
 def test_filepath_with_to():
diff --git a/tests/integration/test_ranges.py b/tests/integration/test_ranges.py
index c557855..c3197d8 100644
--- a/tests/integration/test_ranges.py
+++ b/tests/integration/test_ranges.py
@@ -109,14 +109,20 @@ def test_since_and_to_filters(repository_mining_st, expected_commits):
 
 # FROM AND TO COMMIT
 @pytest.mark.parametrize('to_commit,expected_commits', [
+    ('6411e3096dd2070438a17b225f44475136e54e3a', 2),
     ('09f6182cef737db02a085e1d018963c7a29bde5a', 3),
+    ('1f99848edadfffa903b8ba1286a935f1b92b2845', 4),
+    ('HEAD', 5),
 ])
-def test_to_commit_filter_new(repository_mining_cc, expected_commits):
+def test_to_commit_filter(repository_mining_cc, expected_commits):
     assert len(repository_mining_cc) == expected_commits
 
 
 @pytest.mark.parametrize('from_commit,expected_commits', [
     ('6411e3096dd2070438a17b225f44475136e54e3a', 4),
+    ('09f6182cef737db02a085e1d018963c7a29bde5a', 3),
+    ('1f99848edadfffa903b8ba1286a935f1b92b2845', 2),
+    ('HEAD', 1)
 ])
 def test_from_commit_filter(repository_mining_cc, expected_commits):
     assert len(repository_mining_cc) == expected_commits
@@ -124,13 +130,9 @@ def test_from_commit_filter(repository_mining_cc, expected_commits):
 
 @pytest.mark.parametrize('from_commit,to_commit,expected_commits', [
     ('6411e3096dd2070438a17b225f44475136e54e3a', '09f6182cef737db02a085e1d018963c7a29bde5a', 2),
-])
-def test_from_and_to_commit(repository_mining_cc, expected_commits):
-    assert len(repository_mining_cc) == expected_commits
-
-
-@pytest.mark.parametrize('from_commit,to_commit,expected_commits', [
     ('09f6182cef737db02a085e1d018963c7a29bde5a', '6411e3096dd2070438a17b225f44475136e54e3a', 2),
+    ('6411e3096dd2070438a17b225f44475136e54e3a', 'HEAD', 4),
+    ('09f6182cef737db02a085e1d018963c7a29bde5a', 'HEAD', 3),
 ])
 def test_from_and_to_commit(repository_mining_cc, expected_commits):
     assert len(repository_mining_cc) == expected_commits