Repository: gitguardian/ggshield
Issue: Bug: scan first commit in GitHub action fails

**GitGuardian Shield Version**

- [x] I can reproduce this bug in the latest version (bug occured with gg-shield-action@master)

**Command executed**

I use the GitGuardian/gg-shield-action@master in my GitHub CI.

**Describe the bug**

When pushing the first commit of a repo, the action fails.

**Expected behavior**

I expect this commit to be scanned and the action to finish.

**Traceback (if available)**

```
Error: Unable to get commit range. Please submit an issue with the following info:
  Repository URL: <Fill if public>
github_push_before_sha: 0000000000000000000000000000000000000000
github_push_base_sha: 
github_pull_base_sha: github_default_branch: main
github_head_sha: REDACTED
```

** Action configuration**
```yaml
 ggshield:
    name: Check for secrets with ggshield
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0 # fetch all history so multiple commits can be scanned

      - name: GitGuardian scan
        uses: GitGuardian/gg-shield-action@master
        env:
          GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }}
          GITHUB_PUSH_BASE_SHA: ${{ github.event.base }}
          GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }}
          GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
          GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}
```


Changed files:
tests/core/test_git_shell.py, ggshield/cmd/secret/scan/ci.py, ggshield/core/git_shell.py

diff --git a/ggshield/cmd/secret/scan/ci.py b/ggshield/cmd/secret/scan/ci.py
index b7999c0..627212e 100644
--- a/ggshield/cmd/secret/scan/ci.py
+++ b/ggshield/cmd/secret/scan/ci.py
@@ -206,7 +206,7 @@ def github_actions_range(verbose: bool) -> List[str]:  # pragma: no cover
         "  Repository URL: <Fill if public>\\n"
         f"github_push_before_sha: {push_before_sha}\\n"
         f"github_push_base_sha: {push_base_sha}\\n"
-        f"github_pull_base_sha: {pull_req_base_sha}"
+        f"github_pull_base_sha: {pull_req_base_sha}\\n"
         f"github_default_branch: {default_branch}\\n"
         f"github_head_sha: {head_sha}"
     )
diff --git a/ggshield/core/git_shell.py b/ggshield/core/git_shell.py
index a99d74e..7d710c2 100644
--- a/ggshield/core/git_shell.py
+++ b/ggshield/core/git_shell.py
@@ -73,7 +73,11 @@ def check_git_installed() -> None:
             raise click.ClickException("Git is not installed.")
 
 
-def shell(command: List[str], timeout: int = COMMAND_TIMEOUT) -> str:
+def shell(
+    command: List[str],
+    timeout: int = COMMAND_TIMEOUT,
+    check: bool = False,
+) -> str:
     """Execute a command in a subprocess."""
     env = os.environ.copy()
     env["LANG"] = "C"
@@ -82,23 +86,15 @@ def shell(command: List[str], timeout: int = COMMAND_TIMEOUT) -> str:
         logger.debug("command=%s", command)
         result = subprocess.run(
             command,
-            check=True,
+            check=check,
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
             timeout=timeout,
             env=env,
         )
         return result.stdout.decode("utf-8", errors="ignore").rstrip()
-    except subprocess.CalledProcessError:
-        pass
     except subprocess.TimeoutExpired:
         raise click.Abort('Command "{}" timed out'.format(" ".join(command)))
-    except Exception as exc:
-        raise click.ClickException(
-            f"Unhandled exception: {' '.join(command)}\\n\\t{str(exc)}"
-        )
-
-    return ""
 
 
 def shell_split(command: List[str], **kwargs: Any) -> List[str]:
@@ -115,15 +111,39 @@ def git_ls(wd: Optional[str] = None) -> List[str]:
     return shell_split(cmd, timeout=600)
 
 
+def is_valid_git_commit_ref(ref: str) -> bool:
+    """
+    Check if a reference is valid and can be resolved to a commit
+    """
+    ref += "^{commit}"
+    cmd = [GIT_PATH, "cat-file", "-e", ref]
+
+    try:
+        shell(cmd, check=True)
+    except subprocess.CalledProcessError:
+        return False
+
+    return True
+
+
 def get_list_commit_SHA(commit_range: str) -> List[str]:
     """
     Retrieve the list of commit SHA from a range.
     :param commit_range: A range of commits (ORIGIN...HEAD)
     """
+    try:
+        commit_list = shell_split(
+            [GIT_PATH, "rev-list", "--reverse", *commit_range.split(), "--"],
+            check=True,
+        )
+    except subprocess.CalledProcessError as e:
+        if b"bad revision" in e.stderr and commit_range.endswith("~1..."):
+            # Handle the case where commit_ref has no parent
+            commit_ref = commit_range[:-5]
+            if is_valid_git_commit_ref(commit_ref):
+                return [commit_ref] + get_list_commit_SHA(f"{commit_ref}...")
+        return []
 
-    commit_list = shell_split(
-        [GIT_PATH, "rev-list", "--reverse", *commit_range.split()]
-    )
     if "" in commit_list:
         commit_list.remove("")
         # only happens when git rev-list doesn't error
diff --git a/tests/core/test_git_shell.py b/tests/core/test_git_shell.py
index 3f4b441..778753a 100644
--- a/tests/core/test_git_shell.py
+++ b/tests/core/test_git_shell.py
@@ -3,7 +3,13 @@ import os
 import pytest
 from click import ClickException
 
-from ggshield.core.git_shell import GIT_PATH, check_git_dir, is_git_dir, shell
+from ggshield.core.git_shell import (
+    GIT_PATH,
+    check_git_dir,
+    is_git_dir,
+    is_valid_git_commit_ref,
+    shell,
+)
 from ggshield.scan.repo import cd
 
 
@@ -16,6 +22,11 @@ def test_is_git_dir(tmp_path):
     assert not is_git_dir(str(tmp_path))
 
 
+def test_is_valid_git_commit_ref():
+    assert is_valid_git_commit_ref("HEAD")
+    assert not is_valid_git_commit_ref("invalid_ref")
+
+
 def test_check_git_dir(tmp_path):
     """
     GIVEN a git checkout