exp run: fix failure when dependencies are from inside submodules (#10831)

This commit is contained in:
skshetry
2025-08-06 21:07:07 +05:45
committed by GitHub
parent 396e71f30c
commit e619b6dccd
6 changed files with 62 additions and 9 deletions
+6 -2
View File
@@ -298,7 +298,9 @@ class BaseExecutor(ABC):
stages = dvc.commit([], recursive=recursive, force=True, relink=False) stages = dvc.commit([], recursive=recursive, force=True, relink=False)
exp_hash = cls.hash_exp(stages) exp_hash = cls.hash_exp(stages)
if include_untracked: if include_untracked:
dvc.scm.add(include_untracked, force=True) # type: ignore[call-arg] from dvc.scm import add_no_submodules
add_no_submodules(dvc.scm, include_untracked, force=True) # type: ignore[call-arg]
with cls.auto_push(dvc): with cls.auto_push(dvc):
cls.commit( cls.commit(
@@ -513,7 +515,9 @@ class BaseExecutor(ABC):
stages = dvc.reproduce(*args, **kwargs) stages = dvc.reproduce(*args, **kwargs)
if paths := cls._get_top_level_paths(dvc): if paths := cls._get_top_level_paths(dvc):
logger.debug("Staging top-level files: %s", paths) logger.debug("Staging top-level files: %s", paths)
dvc.scm_context.add(paths) from dvc.scm import add_no_submodules
add_no_submodules(dvc.scm, paths)
exp_hash = cls.hash_exp(stages) exp_hash = cls.hash_exp(stages)
if not repro_dry: if not repro_dry:
+3 -1
View File
@@ -41,8 +41,10 @@ class SCMContext:
def add(self, paths: Union[str, Iterable[str]]) -> None: def add(self, paths: Union[str, Iterable[str]]) -> None:
from scmrepo.exceptions import UnsupportedIndexFormat from scmrepo.exceptions import UnsupportedIndexFormat
from dvc.scm import add_no_submodules
try: try:
return self.scm.add(paths) add_no_submodules(self.scm, paths)
except UnsupportedIndexFormat: except UnsupportedIndexFormat:
link = "https://github.com/iterative/dvc/issues/610" link = "https://github.com/iterative/dvc/issues/610"
add_cmd = self._make_git_add_cmd([relpath(path) for path in paths]) add_cmd = self._make_git_add_cmd([relpath(path) for path in paths])
+34 -2
View File
@@ -1,17 +1,18 @@
"""Manages source control systems (e.g. Git).""" """Manages source control systems (e.g. Git)."""
import os import os
from collections.abc import Iterator, Mapping from collections.abc import Iterable, Iterator, Mapping
from contextlib import contextmanager from contextlib import contextmanager
from functools import partial from functools import partial
from typing import TYPE_CHECKING, Literal, Optional, Union, overload from typing import TYPE_CHECKING, Literal, Optional, Union, overload
from funcy import group_by from funcy import group_by
from scmrepo.base import Base # noqa: F401 from scmrepo.base import Base # noqa: TC002
from scmrepo.git import Git from scmrepo.git import Git
from scmrepo.noscm import NoSCM from scmrepo.noscm import NoSCM
from dvc.exceptions import DvcException from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.progress import Tqdm from dvc.progress import Tqdm
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -19,6 +20,8 @@ if TYPE_CHECKING:
from dvc.fs import FileSystem from dvc.fs import FileSystem
logger = logger.getChild(__name__)
class SCMError(DvcException): class SCMError(DvcException):
"""Base class for source control management errors.""" """Base class for source control management errors."""
@@ -283,3 +286,32 @@ def lfs_prefetch(fs: "FileSystem", paths: list[str]):
include=[(path if path.startswith("/") else f"/{path}") for path in paths], include=[(path if path.startswith("/") else f"/{path}") for path in paths],
progress=pbar.update_git, progress=pbar.update_git,
) )
def add_no_submodules(
scm: "Base",
paths: Union[str, Iterable[str]],
**kwargs,
) -> None:
"""Stage paths to Git, excluding those inside submodules."""
if isinstance(paths, str):
paths = [paths]
submodule_roots = {os.path.join(scm.root_dir, sub) for sub in scm.list_submodules()}
repo_paths: list[str] = []
skipped_paths: list[str] = []
for p in paths:
abs_path = os.path.abspath(p)
if abs_path in submodule_roots or abs_path.startswith(tuple(submodule_roots)):
skipped_paths.append(p)
else:
repo_paths.append(p)
if skipped_paths:
msg = "Skipping staging for path(s) inside submodules: %s"
logger.debug(msg, ", ".join(skipped_paths))
scm.add(repo_paths, **kwargs)
+1 -1
View File
@@ -67,7 +67,7 @@ dependencies = [
"requests>=2.22", "requests>=2.22",
"rich>=12", "rich>=12",
"ruamel.yaml>=0.17.11", "ruamel.yaml>=0.17.11",
"scmrepo>=3.3.8,<4", "scmrepo>=3.5.2,<4",
"shortuuid>=0.5", "shortuuid>=0.5",
"shtab<2,>=1.3.4", "shtab<2,>=1.3.4",
"tabulate>=0.8.7", "tabulate>=0.8.7",
@@ -793,3 +793,18 @@ def test_custom_commit_message(tmp_dir, scm, dvc, tmp):
) )
) )
assert scm.resolve_commit(exp).message == "custom commit message" assert scm.resolve_commit(exp).message == "custom commit message"
@pytest.mark.parametrize("dep", ["submodule", "submodule/file"])
def test_experiments_run_with_submodule_dependencies(dvc, scm, make_tmp_dir, dep):
external_repo = make_tmp_dir("external_repo", scm=True)
external_repo.scm_gen("file", "content", commit="add file")
submodules = scm.pygit2.repo.submodules
submodules.add(os.fspath(external_repo), "submodule")
submodules.update(init=True)
scm.add_commit([".gitmodules"], message="add submodule")
dvc.stage.add(cmd="echo foo", deps=[dep], name="foo")
assert dvc.experiments.run()
+3 -3
View File
@@ -39,7 +39,7 @@ def test_scm_track_changed_files(scm_context):
scm_context.track_file("foo") scm_context.track_file("foo")
scm_context.track_changed_files() scm_context.track_changed_files()
scm_context.scm.add.assert_called_once_with({"foo"}) scm_context.scm.add.assert_called_once_with(["foo"])
def test_ignore(scm_context): def test_ignore(scm_context):
@@ -73,7 +73,7 @@ def test_scm_context_autostage_changed_files(scm_context):
assert not scm_context.files_to_track assert not scm_context.files_to_track
assert not scm_context.ignored_paths assert not scm_context.ignored_paths
scm_context.scm.add.assert_called_once_with({"foo"}) scm_context.scm.add.assert_called_once_with(["foo"])
def test_scm_context_clears_ignores_on_error(scm_context): def test_scm_context_clears_ignores_on_error(scm_context):
@@ -141,4 +141,4 @@ def test_scm_context_decorator(scm_context, mocker):
method = mocker.MagicMock(wraps=test_method) method = mocker.MagicMock(wraps=test_method)
decorator(method, autostage=True)(repo, "arg", kw=1) decorator(method, autostage=True)(repo, "arg", kw=1)
method.assert_called_once_with(repo, "arg", kw=1) method.assert_called_once_with(repo, "arg", kw=1)
scm_context.scm.add.assert_called_once_with({"foo"}) scm_context.scm.add.assert_called_once_with(["foo"])