Repository: pre-commit/pre-commit

Issue:
Invalid symmetric difference expression
```
$ git push
An unexpected error has occurred: CalledProcessError: Command: ('/nix/store/4c12r17gwha2hh6gvc6y2i0s6ckjc09y-git-2.16.2/libexec/git-core/git', 'diff', '--name-only', '--no-ext-diff', '-z', '192a93890ea5100a22a42a73249f7c1e571ebb0b...857ce8997c4f431c06ec52ef0ba1a64983ae945f')
Return code: 128
Expected return code: 0
Output: (none)
Errors:
    fatal: Invalid symmetric difference expression 192a93890ea5100a22a42a73249f7c1e571ebb0b...857ce8997c4f431c06ec52ef0ba1a64983ae945f


Check the log at /home/ielectric/.cache/pre-commit/pre-commit.log
```

I understand this happens if one rewrites git history, but pre-commit shouldn't fail in that case.

Discussion:

Patch:
diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl
index 2a9657e..d357585 100755
--- a/pre_commit/resources/hook-tmpl
+++ b/pre_commit/resources/hook-tmpl
@@ -105,6 +105,10 @@ def _exe():
     )
 
 
+def _rev_exists(rev):
+    return not subprocess.call(('git', 'rev-list', '--quiet', rev))
+
+
 def _pre_push(stdin):
     remote = sys.argv[1]
 
@@ -113,7 +117,7 @@ def _pre_push(stdin):
         _, local_sha, _, remote_sha = line.split()
         if local_sha == Z40:
             continue
-        elif remote_sha != Z40:
+        elif remote_sha != Z40 and _rev_exists(remote_sha):
             opts = ('--origin', local_sha, '--source', remote_sha)
         else:
             # First ancestor not found in remote


Test patch:
diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py
index 9f80569..6aa9c7f 100644
--- a/tests/commands/install_uninstall_test.py
+++ b/tests/commands/install_uninstall_test.py
@@ -495,13 +495,13 @@ def test_installed_from_venv(tempdir_factory, store):
         assert NORMAL_PRE_COMMIT_RUN.match(output)
 
 
-def _get_push_output(tempdir_factory):
+def _get_push_output(tempdir_factory, opts=()):
     return cmd_output_mocked_pre_commit_home(
-        'git', 'push', 'origin', 'HEAD:new_branch',
+        'git', 'push', 'origin', 'HEAD:new_branch', *opts,
         # git push puts pre-commit to stderr
         stderr=subprocess.STDOUT,
         tempdir_factory=tempdir_factory,
-        retcode=None,
+        retcode=None
     )[:2]
 
 
@@ -535,6 +535,26 @@ def test_pre_push_integration_accepted(tempdir_factory, store):
         assert 'Passed' in output
 
 
+def test_pre_push_force_push_without_fetch(tempdir_factory, store):
+    upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
+    path1 = tempdir_factory.get()
+    path2 = tempdir_factory.get()
+    cmd_output('git', 'clone', upstream, path1)
+    cmd_output('git', 'clone', upstream, path2)
+    with cwd(path1):
+        assert _get_commit_output(tempdir_factory)[0] == 0
+        assert _get_push_output(tempdir_factory)[0] == 0
+
+    with cwd(path2):
+        install(Runner(path2, C.CONFIG_FILE), store, hook_type='pre-push')
+        assert _get_commit_output(tempdir_factory, commit_msg='force!')[0] == 0
+
+        retc, output = _get_push_output(tempdir_factory, opts=('--force',))
+        assert retc == 0
+        assert 'Bash hook' in output
+        assert 'Passed' in output
+
+
 def test_pre_push_new_upstream(tempdir_factory, store):
     upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
     upstream2 = git_dir(tempdir_factory)