Repository: iterative/dvc

Issue:
dvc repro -e crashes when executed on detached head
## Bug Report

### Please provide information about your setup

**Output of `dvc version`:**

```console
$ dvc version
```
1.7.2
**Additional Information (if any):**
execute dvc repro -e on a detached HEAD you get an error that origin/HEAD was not found

Here is stack trace

If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.
```
2020-09-21 16:46:48,685 DEBUG: Check for update is enabled.
2020-09-21 16:46:48,686 DEBUG: assuming default target 'dvc.yaml'.
2020-09-21 16:46:48,687 DEBUG: fetched: [(3,)]                        
2020-09-21 16:46:48,720 DEBUG: fetched: [(68,)]
2020-09-21 16:46:48,728 ERROR: unexpected error - No item found with id 'origin/HEAD': 'IterableList' object has no attribute 'origin/HEAD'
------------------------------------------------------------
Traceback (most recent call last):
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/git/util.py", line 881, in __getitem__
    return getattr(self, index)
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/git/util.py", line 874, in __getattr__
    return list.__getattribute__(self, attr)
AttributeError: 'IterableList' object has no attribute 'origin/HEAD'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/main.py", line 76, in main
    ret = cmd.run()
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/command/repro.py", line 47, in run
    params=self.args.params,
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/repo/__init__.py", line 51, in wrapper
    return f(repo, *args, **kwargs)
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/repo/scm_context.py", line 4, in run
    result = method(repo, *args, **kw)
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/repo/reproduce.py", line 89, in reproduce
    **kwargs,
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/repo/reproduce.py", line 151, in _reproduce_experiments
    return repo.experiments.reproduce_one(**kwargs)
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/repo/experiments/__init__.py", line 274, in reproduce_one
    stash_rev = self.new(**kwargs)
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/repo/experiments/__init__.py", line 36, in wrapper
    return f(exp, *args, **kwargs)
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/repo/experiments/__init__.py", line 306, in new
    self._scm_checkout(rev)
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/repo/experiments/__init__.py", line 168, in _scm_checkout
    self._checkout_default_branch()
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/dvc/repo/experiments/__init__.py", line 178, in _checkout_default_branch
    ref = origin_refs["HEAD"].reference
  File "/home/roman/miniconda3/envs/tensorflow/lib/python3.6/site-packages/git/util.py", line 883, in __getitem__
    raise IndexError("No item found with id %r" % (self._prefix + index)) from e
IndexError: No item found with id 'origin/HEAD'
```


Discussion:
To clarify, this will occur if the initial experiments clone happens while the parent repo is on a detached head, since `.git/refs/heads` in the clone will end up empty in this scenario. For the case where `.dvc/experiments` has already been cloned and then the parent workspace is switched to a detached head afterwards, experiments repro appears to work correctly for me.

Resolving patch:
diff --git a/dvc/repo/experiments/__init__.py b/dvc/repo/experiments/__init__.py
--- a/dvc/repo/experiments/__init__.py
+++ b/dvc/repo/experiments/__init__.py
@@ -178,11 +178,26 @@ def _scm_checkout(self, rev):
         self.scm.checkout(rev)
 
     def _checkout_default_branch(self):
+        from git.refs.symbolic import SymbolicReference
+
         # switch to default branch
         git_repo = self.scm.repo
         origin_refs = git_repo.remotes["origin"].refs
-        ref = origin_refs["HEAD"].reference
+
+        # origin/HEAD will point to tip of the default branch unless we
+        # initially cloned a repo that was in a detached-HEAD state.
+        #
+        # If we are currently detached because we cloned a detached
+        # repo, we can't actually tell what branch should be considered
+        # default, so we just fall back to the first available reference.
+        if "HEAD" in origin_refs:
+            ref = origin_refs["HEAD"].reference
+        else:
+            ref = origin_refs[0]
+            if not isinstance(ref, SymbolicReference):
+                ref = ref.reference
         branch_name = ref.name.split("/")[-1]
+
         if branch_name in git_repo.heads:
             branch = git_repo.heads[branch_name]
         else:


Test patch:
diff --git a/tests/func/experiments/test_experiments.py b/tests/func/experiments/test_experiments.py
--- a/tests/func/experiments/test_experiments.py
+++ b/tests/func/experiments/test_experiments.py
@@ -136,3 +136,31 @@ def test_extend_branch(tmp_dir, scm, dvc):
     dvc.experiments.checkout(exp_b)
     assert (tmp_dir / "params.yaml").read_text().strip() == "foo: 3"
     assert (tmp_dir / "metrics.yaml").read_text().strip() == "foo: 3"
+
+
+def test_detached_parent(tmp_dir, scm, dvc, mocker):
+    tmp_dir.gen("copy.py", COPY_SCRIPT)
+    tmp_dir.gen("params.yaml", "foo: 1")
+    stage = dvc.run(
+        cmd="python copy.py params.yaml metrics.yaml",
+        metrics_no_cache=["metrics.yaml"],
+        params=["foo"],
+        name="copy-file",
+    )
+    scm.add(["dvc.yaml", "dvc.lock", "copy.py", "params.yaml", "metrics.yaml"])
+    scm.commit("v1")
+    detached_rev = scm.get_rev()
+
+    tmp_dir.gen("params.yaml", "foo: 2")
+    dvc.reproduce(stage.addressing)
+    scm.add(["dvc.yaml", "dvc.lock", "copy.py", "params.yaml", "metrics.yaml"])
+    scm.commit("v2")
+
+    scm.checkout(detached_rev)
+    assert scm.repo.head.is_detached
+    dvc.reproduce(stage.addressing, experiment=True, params=["foo=3"])
+
+    exp_rev = dvc.experiments.scm.get_rev()
+    assert dvc.experiments.get_baseline(exp_rev) == detached_rev
+    assert (tmp_dir / "params.yaml").read_text().strip() == "foo: 3"
+    assert (tmp_dir / "metrics.yaml").read_text().strip() == "foo: 3"