diff --git a/dvc/repo/experiments/__init__.py b/dvc/repo/experiments/__init__.py index 2a5e18f42..4822ca645 100644 --- a/dvc/repo/experiments/__init__.py +++ b/dvc/repo/experiments/__init__.py @@ -178,11 +178,26 @@ class Experiments: 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: diff --git a/tests/func/experiments/test_experiments.py b/tests/func/experiments/test_experiments.py index edff2f0aa..88a24086a 100644 --- a/tests/func/experiments/test_experiments.py +++ b/tests/func/experiments/test_experiments.py @@ -208,3 +208,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"