Repository: iterative/dvc

Issue:
Remove all experiments in the workspace
Have a `dvc exp remove --workspace` command or similar to drop all experiments in the workspace. Should be equivalent to `dvc exp list --names-only | xargs dvc exp remove`. This allows users to reset their experiments workspace if they have made mistakes or were playing around with some changes they ultimately want to roll back.

Some background:
* https://discord.com/channels/485586884165107732/485596304961962003/823901428652965898
* https://github.com/iterative/dvc/issues/5553#issuecomment-791717274
* https://github.com/iterative/dvc/issues/5437#issuecomment-776861639


Discussion:
This indeed would be a great functionality!

One possible (very likely) outcome of running experiment is that you find this path is not worth pursuing. In that case, would like to purge all of the experiments to be able to continue with the next idea

Also, the xargs solutions don't work that easy for me under windows ;)
`dvc exp remove -A/--all-commits` might also be useful.

@pmrowla What do you think about moving up the priority of this one? The workaround is not difficult, but it's not that easy for people to find in this thread, and it seems like it's a straightforward feature to implement.
I guess we just need to be clear that by workspace here we mean all experiments based on the current HEAD right?

Regarding priority, I think it's up to you. This is an issue that anyone should be able to fix - codebase-wise, it won't actually require touching anything on the experiments internals side of things.
> I guess we just need to be clear that by workspace here we mean all experiments based on the current HEAD right?

Right, it should match `dvc exp show`, which is described in https://dvc.org/doc/command-reference/exp/show#description:

> Only the experiments derived from the Git HEAD are shown by default


> This is an issue that anyone should be able to fix - codebase-wise, it won't actually require touching anything on the experiments internals side of things.

Yes, this could be a good opportunity for either one of the other devs or contributors.
I find the `dvc exp gc` flags a little confusing. They're mostly "remove all except" (exclusive) rather than "remove only" (inclusive). Ideally we should have both. Related #6006.

I'd prefer a new flag which inverts the meaning:

```
  -w, --workspace     Keep experiments derived from the current workspace.
...
  --queued            Keep queued experiments (experiments run queue will be
                      cleared by default).
  -R, --remove/--no-keep   Inverts meaning of the above options
                           (remove rather than keep).
```
Could we support `--keep` and `--remove` options, that can take
```
workspace
all-branches
all-tags
all-commits
<specific-experiment-id>
```
as parameters.

So `dvc exp gc --keep workspace` will be identical with the current `dvc exp gc --workspace` and `dvc exp gc --remove exp-abc123` will remove that. 

We can also use `dvc exp remove` for this. `gc` can just be a flag to `remove`, that basically says "run dvc gc after removing these to clean up the cache" So ınstead of `dvc exp gc --workspace && dvc gc --workspace`, the user can run `dvc exp remove --keep workspace --gc`.

@efiop @dberenbaum @pmrowla @karajan1001  
> I'd prefer a new flag which inverts the meaning:

Is this the same functionality as the proposed `dvc exp remove -w`? Is the point that you would prefer `dvc exp gc -w -R` to `dvc exp remove -w`? I agree that `gc` can be confusing, which is why I suggested to add this option to `remove` instead.

I'd say that introducing `gc -R` or `gc --keep/--remove` is a separate feature request since it does more than what's requested here, and `gc` applies to more than experiments. For example, would `dvc exp gc -R/--remove` ever make sense for all branches or tags? I think the ideas make sense, but I don't want them to block the simpler feature request here.

If the `--workspace` option seems awkward in `dvc exp remove`, we can simply implement `dvc exp remove --all` and save the distinctions between `--workspace` and similar options to another feature request.
> Is this the same functionality as the proposed `dvc exp remove -w`? Is the point that you would prefer `dvc exp gc -w -R` to `dvc exp remove -w`? I agree that `gc` can be confusing, which is why I suggested to add this option to `remove` instead.

IMHO, the flags should be explicit. Having a `--reverse` flag is confusing. The user shouldn't have to look up the help text about the meaning. What are we reversing to what?





> I'd say that introducing `gc -R` or `gc --keep/--remove` is a separate feature request since it does more than what's requested here, and `gc` applies to more than experiments. For example, would `dvc exp gc -R/--remove` ever make sense for all branches or tags? I think the ideas make sense, but I don't want them to block the simpler feature request here.

Implementation-wise, these are not that different. Basically, the command will iterate over all experiments and decide whether an experiment is within the group specified for deletion. If some of the groups are empty, so be it. 



> If the `--workspace` option seems awkward in `dvc exp remove`, we can simply implement `dvc exp remove --all` and save the distinctions between `--workspace` and similar options to another feature request.

`--all` is better than `--workspace`. `--workspace` is a smaller set than `--all`. 

BTW if `dvc exp remove` will be the primary removal command, having `--include` and `--exclude` options may be better semantically. `dvc exp remove --include all --exclude exp-123abc` says "delete all experiments except exp-123abc", but this is another feature as you say. 

@dberenbaum 

Resolving patch:
diff --git a/dvc/command/experiments.py b/dvc/command/experiments.py
--- a/dvc/command/experiments.py
+++ b/dvc/command/experiments.py
@@ -747,7 +747,9 @@ class CmdExperimentsRemove(CmdBase):
     def run(self):
 
         self.repo.experiments.remove(
-            exp_names=self.args.experiment, queue=self.args.queue
+            exp_names=self.args.experiment,
+            queue=self.args.queue,
+            clear_all=self.args.all,
         )
 
         return 0
@@ -1237,9 +1239,16 @@ def add_parser(subparsers, parent_parser):
         help=EXPERIMENTS_REMOVE_HELP,
         formatter_class=argparse.RawDescriptionHelpFormatter,
     )
-    experiments_remove_parser.add_argument(
+    remove_group = experiments_remove_parser.add_mutually_exclusive_group()
+    remove_group.add_argument(
         "--queue", action="store_true", help="Remove all queued experiments."
     )
+    remove_group.add_argument(
+        "-A",
+        "--all",
+        action="store_true",
+        help="Remove all committed experiments.",
+    )
     experiments_remove_parser.add_argument(
         "experiment",
         nargs="*",
diff --git a/dvc/repo/experiments/remove.py b/dvc/repo/experiments/remove.py
--- a/dvc/repo/experiments/remove.py
+++ b/dvc/repo/experiments/remove.py
@@ -7,21 +7,29 @@
 from dvc.scm.base import RevError
 
 from .base import EXPS_NAMESPACE, ExpRefInfo
-from .utils import exp_refs_by_name, remove_exp_refs
+from .utils import exp_refs, exp_refs_by_name, remove_exp_refs
 
 logger = logging.getLogger(__name__)
 
 
 @locked
 @scm_context
-def remove(repo, exp_names=None, queue=False, **kwargs):
-    if not exp_names and not queue:
+def remove(
+    repo,
+    exp_names=None,
+    queue=False,
+    clear_all=False,
+    **kwargs,
+):
+    if not any([exp_names, queue, clear_all]):
         return 0
 
     removed = 0
     if queue:
-        removed += len(repo.experiments.stash)
-        repo.experiments.stash.clear()
+        removed += _clear_stash(repo)
+    if clear_all:
+        removed += _clear_all(repo)
+
     if exp_names:
         remained = _remove_commited_exps(repo, exp_names)
         remained = _remove_queued_exps(repo, remained)
@@ -33,6 +41,18 @@ def remove(repo, exp_names=None, queue=False, **kwargs):
     return removed
 
 
+def _clear_stash(repo):
+    removed = len(repo.experiments.stash)
+    repo.experiments.stash.clear()
+    return removed
+
+
+def _clear_all(repo):
+    ref_infos = list(exp_refs(repo.scm))
+    remove_exp_refs(repo.scm, ref_infos)
+    return len(ref_infos)
+
+
 def _get_exp_stash_index(repo, ref_or_rev: str) -> Optional[int]:
     stash_revs = repo.experiments.stash_revs
     for _, ref_info in stash_revs.items():
@@ -53,9 +73,9 @@ def _get_exp_ref(repo, exp_name: str) -> Optional[ExpRefInfo]:
         if repo.scm.get_ref(exp_name):
             return ExpRefInfo.from_ref(exp_name)
     else:
-        exp_refs = list(exp_refs_by_name(repo.scm, exp_name))
-        if exp_refs:
-            return _get_ref(exp_refs, exp_name, cur_rev)
+        exp_ref_list = list(exp_refs_by_name(repo.scm, exp_name))
+        if exp_ref_list:
+            return _get_ref(exp_ref_list, exp_name, cur_rev)
     return None
 
 


Test patch:
diff --git a/tests/func/experiments/test_remove.py b/tests/func/experiments/test_remove.py
--- a/tests/func/experiments/test_remove.py
+++ b/tests/func/experiments/test_remove.py
@@ -72,3 +72,21 @@ def test_remove_special_queued_experiments(tmp_dir, scm, dvc, exp_stage):
     assert rev3 in dvc.experiments.stash_revs
     assert scm.get_ref(str(ref_info1)) is None
     assert scm.get_ref(str(ref_info2)) is not None
+
+
+def test_remove_all(tmp_dir, scm, dvc, exp_stage):
+    results = dvc.experiments.run(exp_stage.addressing, params=["foo=1"])
+    ref_info1 = first(exp_refs_by_rev(scm, first(results)))
+    dvc.experiments.run(exp_stage.addressing, params=["foo=2"], queue=True)
+    scm.add(["dvc.yaml", "dvc.lock", "copy.py", "params.yaml", "metrics.yaml"])
+    scm.commit("update baseline")
+
+    results = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
+    ref_info2 = first(exp_refs_by_rev(scm, first(results)))
+    dvc.experiments.run(exp_stage.addressing, params=["foo=4"], queue=True)
+
+    removed = dvc.experiments.remove(clear_all=True)
+    assert removed == 2
+    assert len(dvc.experiments.stash) == 2
+    assert scm.get_ref(str(ref_info2)) is None
+    assert scm.get_ref(str(ref_info1)) is None
diff --git a/tests/unit/command/test_experiments.py b/tests/unit/command/test_experiments.py
--- a/tests/unit/command/test_experiments.py
+++ b/tests/unit/command/test_experiments.py
@@ -252,13 +252,25 @@ def test_experiments_pull(dvc, scm, mocker):
     )
 
 
-def test_experiments_remove(dvc, scm, mocker):
-    cli_args = parse_args(["experiments", "remove", "--queue"])
+@pytest.mark.parametrize(
+    "queue,clear_all",
+    [(True, False), (False, True)],
+)
+def test_experiments_remove(dvc, scm, mocker, queue, clear_all):
+    if queue:
+        args = "--queue"
+    if clear_all:
+        args = "--all"
+    cli_args = parse_args(["experiments", "remove", args])
     assert cli_args.func == CmdExperimentsRemove
 
     cmd = cli_args.func(cli_args)
     m = mocker.patch("dvc.repo.experiments.remove.remove", return_value={})
 
     assert cmd.run() == 0
-
-    m.assert_called_once_with(cmd.repo, exp_names=[], queue=True)
+    m.assert_called_once_with(
+        cmd.repo,
+        exp_names=[],
+        queue=queue,
+        clear_all=clear_all,
+    )