diff --git a/dvc/commands/experiments/save.py b/dvc/commands/experiments/save.py index 9045dc5c2..a285fd994 100644 --- a/dvc/commands/experiments/save.py +++ b/dvc/commands/experiments/save.py @@ -16,6 +16,7 @@ class CmdExperimentsSave(CmdBase): name=self.args.name, force=self.args.force, include_untracked=self.args.include_untracked, + message=self.args.message, ) except DvcException: logger.exception("failed to save experiment") @@ -75,4 +76,11 @@ def add_parser(experiments_subparsers, parent_parser): help="List of untracked paths to include in the experiment.", metavar="", ) + save_parser.add_argument( + "-M", + "--message", + type=str, + default=None, + help="Custom commit message to use when committing the experiment.", + ) save_parser.set_defaults(func=CmdExperimentsSave) diff --git a/dvc/repo/experiments/executor/base.py b/dvc/repo/experiments/executor/base.py index 9570a9e83..c932026c1 100644 --- a/dvc/repo/experiments/executor/base.py +++ b/dvc/repo/experiments/executor/base.py @@ -277,6 +277,7 @@ class BaseExecutor(ABC): info: "ExecutorInfo", force: bool = False, include_untracked: Optional[List[str]] = None, + message: Optional[str] = None, ) -> ExecutorResult: from dvc.dvcfile import LOCK_FILE from dvc.repo import Repo @@ -308,6 +309,7 @@ class BaseExecutor(ABC): exp_hash, exp_name=info.name, force=force, + message=message, ) ref: Optional[str] = dvc.scm.get_ref(EXEC_BRANCH, follow=False) exp_ref = ExpRefInfo.from_ref(ref) if ref else None diff --git a/dvc/repo/experiments/save.py b/dvc/repo/experiments/save.py index 99d4420bf..818234679 100644 --- a/dvc/repo/experiments/save.py +++ b/dvc/repo/experiments/save.py @@ -16,6 +16,7 @@ def save( name: Optional[str] = None, force: bool = False, include_untracked: Optional[List[str]] = None, + message: Optional[str] = None, ) -> Optional[str]: """Save the current workspace status as an experiment. @@ -29,7 +30,10 @@ def save( try: save_result = executor.save( - executor.info, force=force, include_untracked=include_untracked + executor.info, + force=force, + include_untracked=include_untracked, + message=message, ) result = queue.collect_executor(repo.experiments, executor, save_result) finally: diff --git a/tests/func/experiments/test_save.py b/tests/func/experiments/test_save.py index 976c30c3d..fb127c243 100644 --- a/tests/func/experiments/test_save.py +++ b/tests/func/experiments/test_save.py @@ -153,3 +153,10 @@ def test_untracked_dvclock_is_included_in_exp(tmp_dir, scm, dvc): exp = dvc.experiments.save() fs = scm.get_fs(exp) assert fs.exists("dvc.lock") + + +def test_exp_save_custom_message(tmp_dir, dvc, scm): + setup_stage(tmp_dir, dvc, scm) + + exp = dvc.experiments.save(message="custom commit message") + assert scm.gitpython.repo.commit(exp).message == "custom commit message" diff --git a/tests/unit/command/test_experiments.py b/tests/unit/command/test_experiments.py index e527640f2..925bea3db 100644 --- a/tests/unit/command/test_experiments.py +++ b/tests/unit/command/test_experiments.py @@ -628,7 +628,7 @@ def test_experiments_save(dvc, scm, mocker): assert cmd.run() == 0 m.assert_called_once_with( - cmd.repo, name="exp-name", force=True, include_untracked=[] + cmd.repo, name="exp-name", force=True, include_untracked=[], message=None )