Repository: iterative/dvc

Issue:
exp push --rev broken
# Bug Report

## Description

`dvc exp push --rev` is broken since https://github.com/iterative/dvc/pull/9391.

### Reproduce

Run `dvc exp push --rev HEAD` from any repo and you will get:

```
$ dvc exp push -vv --rev HEAD origin 
2023-05-17 15:34:26,148 DEBUG: v2.55.0, CPython 3.10.10 on macOS-13.3.1-arm64-arm-64bit
2023-05-17 15:34:26,148 DEBUG: command: /Users/dave/miniforge3/envs/dvc/bin/dvc exp push -vv --rev HEAD origin
2023-05-17 15:34:26,148 TRACE: Namespace(cprofile=False, yappi=False, yappi_separate_threads=False, viztracer=False, viztracer_depth=None, viztracer_async=False, cprofile_dump=None, pdb=False, instrument=False, instrument_open=False, show_stack=False, quiet=0, verbose=2, cd='.', cmd='push', all_commits=False, rev=['HEAD'], num=1, force=False, push_cache=True, dvc_remote=None, jobs=None, run_cache=False, git_remote='origin', experiment=[], func=<class 'dvc.commands.experiments.push.CmdExperimentsPush'>, parser=DvcParser(prog='dvc', usage=None, description='Data Version Control', formatter_class=<class 'argparse.RawTextHelpFormatter'>, conflict_handler='error', add_help=False))
2023-05-17 15:34:26,370 ERROR: unexpected error - expected string or bytes-like object
Traceback (most recent call last):
  File "/Users/dave/Code/dvc/dvc/cli/__init__.py", line 210, in main
    ret = cmd.do_run()
  File "/Users/dave/Code/dvc/dvc/cli/command.py", line 26, in do_run
    return self.run()
  File "/Users/dave/Code/dvc/dvc/commands/experiments/push.py", line 65, in run
    result = self.repo.experiments.push(
  File "/Users/dave/Code/dvc/dvc/repo/experiments/__init__.py", line 494, in push
    return push(self.repo, *args, **kwargs)
  File "/Users/dave/Code/dvc/dvc/repo/__init__.py", line 65, in wrapper
    return f(repo, *args, **kwargs)
  File "/Users/dave/Code/dvc/dvc/repo/scm_context.py", line 151, in run
    return method(repo, *args, **kw)
  File "/Users/dave/Code/dvc/dvc/repo/experiments/push.py", line 115, in push
    exp_ref_set.update(exp_refs_from_rev(repo.scm, rev, num=num))
  File "/Users/dave/Code/dvc/dvc/repo/experiments/push.py", line 87, in exp_refs_from_rev
    rev_dict = iter_revs(scm, [rev], num)
  File "/Users/dave/Code/dvc/dvc/scm.py", line 266, in iter_revs
    return group_by(rev_resolver, results)
  File "/Users/dave/miniforge3/envs/dvc/lib/python3.10/site-packages/funcy/seqs.py", line 313, in group_by
    result[f(item)].append(item)
  File "/Users/dave/Code/dvc/dvc/scm.py", line 174, in resolve_rev
    return scm.resolve_rev(fix_exp_head(scm, rev))
  File "/Users/dave/Code/dvc/dvc/repo/experiments/utils.py", line 239, in fix_exp_head
    name, tail = Git.split_ref_pattern(ref)
  File "/Users/dave/miniforge3/envs/dvc/lib/python3.10/site-packages/scmrepo/git/__init__.py", line 157, in split_ref_pattern
    name = cls.BAD_REF_CHARS_RE.split(ref, maxsplit=1)[0]
TypeError: expected string or bytes-like object

2023-05-17 15:34:26,393 DEBUG: Removing '/Users/dave/Code/.CLFaTE3ctW8Fy6qXaisSEw.tmp'
2023-05-17 15:34:26,394 DEBUG: Removing '/Users/dave/Code/.CLFaTE3ctW8Fy6qXaisSEw.tmp'
2023-05-17 15:34:26,394 DEBUG: Removing '/Users/dave/Code/.CLFaTE3ctW8Fy6qXaisSEw.tmp'
2023-05-17 15:34:26,394 DEBUG: Removing '/Users/dave/Code/dvclive-exp-tracking/.dvc/cache/.YMaAXVC63teQCaVoXiTvho.tmp'
2023-05-17 15:34:26,395 DEBUG: Version info for developers:
DVC version: 2.55.0
-------------------
Platform: Python 3.10.10 on macOS-13.3.1-arm64-arm-64bit
Subprojects:
        dvc_data = 0.47.5
        dvc_objects = 0.22.0
        dvc_render = 0.3.1
        dvc_task = 0.2.1
        scmrepo = 1.0.3
Supports:
        azure (adlfs = 2023.1.0, knack = 0.10.1, azure-identity = 1.12.0),
        gdrive (pydrive2 = 1.15.3),
        gs (gcsfs = 2022.11.0),
        hdfs (fsspec = 2022.11.0, pyarrow = 11.0.0),
        http (aiohttp = 3.8.4, aiohttp-retry = 2.8.3),
        https (aiohttp = 3.8.4, aiohttp-retry = 2.8.3),
        oss (ossfs = 2021.8.0),
        s3 (s3fs = 2022.11.0, boto3 = 1.24.59),
        ssh (sshfs = 2023.4.1),
        webdav (webdav4 = 0.9.8),
        webdavs (webdav4 = 0.9.8),
        webhdfs (fsspec = 2022.11.0)
Config:
        Global: /Users/dave/Library/Application Support/dvc
        System: /Library/Application Support/dvc
Cache types: reflink, hardlink, symlink
Cache directory: apfs on /dev/disk3s1s1
Caches: local
Remotes: None
Workspace directory: apfs on /dev/disk3s1s1
Repo: dvc, git
Repo.site_cache_dir: /Library/Caches/dvc/repo/49450ff302024812849858c0b20a2692
```



Discussion:
@daavoo Are you looking into it? I was going to take a look but don't want to duplicate the effort. We could either extend multiple rev support to all exp commands (looks like it impacts `push`, `pull`, `remove`, and `list`) or restrict this to `exp show`.
I have a fix, sending a patch

Resolving patch:
diff --git a/dvc/repo/experiments/push.py b/dvc/repo/experiments/push.py
--- a/dvc/repo/experiments/push.py
+++ b/dvc/repo/experiments/push.py
@@ -82,9 +82,9 @@ def exp_refs_from_names(scm: "Git", exp_names: List[str]) -> Set["ExpRefInfo"]:
     return exp_ref_set
 
 
-def exp_refs_from_rev(scm: "Git", rev: str, num: int = 1) -> Set["ExpRefInfo"]:
+def exp_refs_from_rev(scm: "Git", rev: List[str], num: int = 1) -> Set["ExpRefInfo"]:
     exp_ref_set = set()
-    rev_dict = iter_revs(scm, [rev], num)
+    rev_dict = iter_revs(scm, rev, num)
     rev_set = set(rev_dict.keys())
     ref_info_dict = exp_refs_by_baseline(scm, rev_set)
     for _, ref_info_list in ref_info_dict.items():
@@ -99,7 +99,7 @@ def push(
     git_remote: str,
     exp_names: Union[List[str], str],
     all_commits: bool = False,
-    rev: Optional[str] = None,
+    rev: Optional[Union[List[str], str]] = None,
     num: int = 1,
     force: bool = False,
     push_cache: bool = False,
@@ -112,6 +112,8 @@ def push(
     if exp_names:
         exp_ref_set.update(exp_refs_from_names(repo.scm, ensure_list(exp_names)))
     if rev:
+        if isinstance(rev, str):
+            rev = [rev]
         exp_ref_set.update(exp_refs_from_rev(repo.scm, rev, num=num))
 
     push_result = _push(repo, git_remote, exp_ref_set, force)


Test patch:
diff --git a/tests/func/experiments/test_remote.py b/tests/func/experiments/test_remote.py
--- a/tests/func/experiments/test_remote.py
+++ b/tests/func/experiments/test_remote.py
@@ -66,6 +66,29 @@ def test_push_args(tmp_dir, scm, dvc, git_upstream, exp_stage, all_, rev, result
     assert git_upstream.tmp_dir.scm.get_ref(str(ref_info3)) == result3
 
 
+def test_push_multi_rev(tmp_dir, scm, dvc, git_upstream, exp_stage):
+    remote = git_upstream.url
+    baseline = scm.get_rev()
+
+    results = dvc.experiments.run(exp_stage.addressing, params=["foo=1"])
+    exp1 = first(results)
+    ref_info1 = first(exp_refs_by_rev(scm, exp1))
+    results = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
+    exp2 = first(results)
+    ref_info2 = first(exp_refs_by_rev(scm, exp2))
+
+    scm.commit("new_baseline")
+
+    results = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
+    exp3 = first(results)
+    ref_info3 = first(exp_refs_by_rev(scm, exp3))
+
+    dvc.experiments.push(remote, [], rev=[baseline, scm.get_rev()])
+    assert git_upstream.tmp_dir.scm.get_ref(str(ref_info1)) == exp1
+    assert git_upstream.tmp_dir.scm.get_ref(str(ref_info2)) == exp2
+    assert git_upstream.tmp_dir.scm.get_ref(str(ref_info3)) == exp3
+
+
 def test_push_diverged(tmp_dir, scm, dvc, git_upstream, exp_stage):
     git_upstream.tmp_dir.scm_gen("foo", "foo", commit="init")
     remote_rev = git_upstream.tmp_dir.scm.get_rev()