Repository: pre-commit/pre-commit

Issue:
autoupdate: flip-flop between shallow-fetched tags
```yaml
repos:
  - repo: https://github.com/asottile/pyupgrade
    rev: v1.20.1
    hooks:
      - id: pyupgrade
```

```console
$ pre-commit autoupdate
Updating https://github.com/asottile/pyupgrade...updating v1.20.1 -> v1.20.0.
$ pre-commit autoupdate
Updating https://github.com/asottile/pyupgrade...updating v1.20.0 -> v1.20.1.
$ pre-commit autoupdate
Updating https://github.com/asottile/pyupgrade...updating v1.20.1 -> v1.20.0.
$ pre-commit autoupdate
Updating https://github.com/asottile/pyupgrade...updating v1.20.0 -> v1.20.1.
```

Seems to be related to shallow clones as I can only reproduce there.

CC @mxr


Discussion:
Here's my entry on the mailing list: https://marc.info/?l=git&m=156256221122204&w=2

Resolving patch:
diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py
--- a/pre_commit/commands/autoupdate.py
+++ b/pre_commit/commands/autoupdate.py
@@ -10,6 +10,7 @@
 from cfgv import remove_defaults
 
 import pre_commit.constants as C
+from pre_commit import git
 from pre_commit import output
 from pre_commit.clientlib import CONFIG_SCHEMA
 from pre_commit.clientlib import InvalidManifestError
@@ -20,6 +21,7 @@
 from pre_commit.commands.migrate_config import migrate_config
 from pre_commit.util import CalledProcessError
 from pre_commit.util import cmd_output
+from pre_commit.util import tmpdir
 
 
 class RepositoryCannotBeUpdatedError(RuntimeError):
@@ -34,19 +36,20 @@ def _update_repo(repo_config, store, tags_only):
     Args:
         repo_config - A config for a repository
     """
-    repo_path = store.clone(repo_config['repo'], repo_config['rev'])
-
-    cmd_output('git', 'fetch', cwd=repo_path)
-    tag_cmd = ('git', 'describe', 'origin/master', '--tags')
-    if tags_only:
-        tag_cmd += ('--abbrev=0',)
-    else:
-        tag_cmd += ('--exact',)
-    try:
-        rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip()
-    except CalledProcessError:
-        tag_cmd = ('git', 'rev-parse', 'origin/master')
-        rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip()
+    with tmpdir() as repo_path:
+        git.init_repo(repo_path, repo_config['repo'])
+        cmd_output('git', 'fetch', cwd=repo_path)
+
+        tag_cmd = ('git', 'describe', 'origin/master', '--tags')
+        if tags_only:
+            tag_cmd += ('--abbrev=0',)
+        else:
+            tag_cmd += ('--exact',)
+        try:
+            rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip()
+        except CalledProcessError:
+            tag_cmd = ('git', 'rev-parse', 'origin/master')
+            rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip()
 
     # Don't bother trying to update if our rev is the same
     if rev == repo_config['rev']:
diff --git a/pre_commit/git.py b/pre_commit/git.py
--- a/pre_commit/git.py
+++ b/pre_commit/git.py
@@ -143,6 +143,15 @@ def has_diff(*args, **kwargs):
     return cmd_output(*cmd, cwd=repo, retcode=None)[0]
 
 
+def init_repo(path, remote):
+    if os.path.isdir(remote):
+        remote = os.path.abspath(remote)
+
+    env = no_git_env()
+    cmd_output('git', 'init', path, env=env)
+    cmd_output('git', 'remote', 'add', 'origin', remote, cwd=path, env=env)
+
+
 def commit(repo='.'):
     env = no_git_env()
     name, email = 'pre-commit', 'asottile+pre-commit@umich.edu'
diff --git a/pre_commit/store.py b/pre_commit/store.py
--- a/pre_commit/store.py
+++ b/pre_commit/store.py
@@ -156,18 +156,13 @@ def _shallow_clone(self, ref, git_cmd):
     def clone(self, repo, ref, deps=()):
         """Clone the given url and checkout the specific ref."""
 
-        if os.path.isdir(repo):
-            repo = os.path.abspath(repo)
-
         def clone_strategy(directory):
+            git.init_repo(directory, repo)
             env = git.no_git_env()
 
             def _git_cmd(*args):
                 cmd_output('git', *args, cwd=directory, env=env)
 
-            _git_cmd('init', '.')
-            _git_cmd('remote', 'add', 'origin', repo)
-
             try:
                 self._shallow_clone(ref, _git_cmd)
             except CalledProcessError:
@@ -193,8 +188,7 @@ def make_local_strategy(directory):
             def _git_cmd(*args):
                 cmd_output('git', *args, cwd=directory, env=env)
 
-            _git_cmd('init', '.')
-            _git_cmd('config', 'remote.origin.url', '<<unknown>>')
+            git.init_repo(directory, '<<unknown>>')
             _git_cmd('add', '.')
             git.commit(repo=directory)
 


Test patch:
diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py
--- a/tests/commands/gc_test.py
+++ b/tests/commands/gc_test.py
@@ -5,6 +5,7 @@
 from pre_commit.clientlib import load_config
 from pre_commit.commands.autoupdate import autoupdate
 from pre_commit.commands.gc import gc
+from pre_commit.commands.install_uninstall import install_hooks
 from pre_commit.repository import all_hooks
 from testing.fixtures import make_config_from_repo
 from testing.fixtures import make_repo
@@ -40,6 +41,7 @@ def test_gc(tempdir_factory, store, in_git_dir, cap_out):
     store.mark_config_used(C.CONFIG_FILE)
 
     # update will clone both the old and new repo, making the old one gc-able
+    install_hooks(C.CONFIG_FILE, store)
     assert not autoupdate(C.CONFIG_FILE, store, tags_only=False)
 
     assert _config_count(store) == 1
@@ -145,7 +147,7 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out):
     store.mark_config_used(C.CONFIG_FILE)
 
     # trigger a clone
-    assert not autoupdate(C.CONFIG_FILE, store, tags_only=False)
+    install_hooks(C.CONFIG_FILE, store)
 
     # we'll "break" the manifest to simulate an old version clone
     (_, _, path), = store.select_all_repos()