Repository: iterative/dvc

Issue:
`dvc studio login`: setup auto pushing experiments
See ~#5029~ (edit: https://github.com/iterative/dvc.org/issues/5029) and the related issues linked there for background.

Rather than document the environment variables to auto push experiments, we could make this part of the studio login workflow since auto-pushing experiments is mostly useful when using studio rather than keeping experiments local. We would need to:

1. Make config options like `exp.auto_push` and `exp.git_remote`
2. During studio login, ask to set these options. The UI could look something like this: 

```sh
$ dvc studio login
...
Authentication successful. The token will be available as risen-geum in Studio profile.
Do you want to push experiments automatically when they are completed [Y\n]?
Enter the Git remote to use [origin]:
```


Discussion:
Since this is a `studio login`, I'd rather not have any prompts and enable everything by default. But we should notify users that they are enabled and provide hints to disable (and support arg to disable this behaviour).
cc @iterative/vs-code since this should also impact the vs code flow
@skshetry Do you mean to enable them during `studio login` or some other time? Auto-pushing is pretty connected to the Studio workflow since it's where the pushed experiments appear, and I don't think it's worthwhile to auto-push them without Studio.

Regardless, I think we can do the first step of adding config options. Having to set environment variables every time to auto push doesn't make much sense.
> Do you mean to enable them during studio login or some other time?

Enabling them automatically during `studio login` (unless it's not disabled already by other means).
Not that strong an opinion, but `gh auth login` has prompts. While they can be clumsy, in this case there is already some interaction needed, so I didn't think prompts would be bad UX. What's your concern?
From a new user perspective, it might be confusing and unclear what to choose. "Do you want to push experiments?"  - maybe, maybe not, idk. What's experiments? etc.

It'll definitely lead to choice paralysis to me if I was using it for the first time. 😅

It's better to make a choice for them here. But the message should be clear that we are doing that.
We want to have less interactions as possible, less decisions for user to make as possible. 




We also need a way to auto push on `exp save` for dvclive-only experiments. `DVC_EXP_AUTO_PUSH` does not do this now.
Thoughts on this approach?

- Once you login to studio, everything will be pushed automatically unless you set it to offline, and we can make clear during login how to toggle offline mode
- We can show a notification before starting the push making clear that if you don't want to wait, it's safe to cancel and you can always upload later with exp push
Not a requirement but nice to have would be to incorporate #8843 when doing this. If we can push the dvc-tracked data at the end of each stage, and include the run cache, it can help in scenarios like recovery from failed runners but also break up the pushes during the experiment run so the final push may not feel so painful.
Tasks for this issue:

- [x] Confirm `DVC_EXP_AUTO_PUSH` works as expected
- [x] Make `DVC_EXP_AUTO_PUSH` default to use git remote `origin` (currently requires `DVC_EXP_GIT_REMOTE`)
- [x] Make `DVC_EXP_AUTO_PUSH` work on `dvc exp save`
- [x] Add config options for `dvc config exp.auto_push` and `dvc config exp.git_remote`
- [x] Handle errors if no dvc or git remote
- [x] Enable during `dvc studio login` with instructions or option to opt out
- [x] During push, show useful messages in case it's slow (it's safe to cancel, how to upload later, how to disable push)
- [x] Handle case where [remote doesn't exist](https://github.com/iterative/dvc/pull/10331#discussion_r1511565116)
- [ ] Simplify ways to [set git remote url](https://github.com/iterative/dvc.org/pull/5165#discussion_r1511814833)
- [x] Make auto push work with queue

Out of scope:
- [ ] #8843

@skshetry I updated the checklist above for what's left to do here.
@dberenbaum, any thoughts on how to simplify?
> We could also make `studio.repo_url` an alias for `exp.git_remote` and deprecate it, so you can specify either a URL or a git remote name.

_Originally posted by @skshetry in https://github.com/iterative/dvc.org/pull/5165#discussion_r1512677465_
            
@skshetry This suggestion makes sense to me.
Added `Make auto push work with queue`. Currently, queued experiments fail because `origin` is not set in the queued repo:

```
$ dvc exp run --run-all
Following logs for all queued experiments. Use Ctrl+C to stop following logs (experiment execution will continue).

Reproducing experiment 'sober-daze'
Running stage 'train':
> python src/stages/train.py --config=params.yaml
WARNING: Failed to validate remotes. Disabling auto push: 'origin' is not a valid Git remote or URL

Ran experiment(s):
To apply the results of an experiment to your workspace run:

        dvc exp apply <exp>
```

Resolving patch:
diff --git a/dvc/repo/experiments/executor/base.py b/dvc/repo/experiments/executor/base.py
--- a/dvc/repo/experiments/executor/base.py
+++ b/dvc/repo/experiments/executor/base.py
@@ -25,6 +25,7 @@
 from dvc.utils.fs import remove
 from dvc.utils.studio import (
     env_to_config,
+    get_repo_url,
     get_subrepo_relpath,
 )
 
@@ -611,6 +612,8 @@ def _repro_dvc(
             # inferring repo url will fail if not set here
             run_env_config = env_to_config(kwargs.get("run_env", {}))
             dvc_studio_config = run_env_config | dvc_studio_config
+            # override studio repo url if exp git remote set
+            repo_url = get_repo_url(dvc)
             try:
                 post_live_metrics(
                     "start",
@@ -621,6 +624,7 @@ def _repro_dvc(
                     dvc_studio_config=dvc_studio_config,
                     message=message,
                     subdir=get_subrepo_relpath(dvc),
+                    studio_repo_url=repo_url,
                 )
                 logger.debug("Running repro in '%s'", os.getcwd())
                 yield dvc
@@ -646,6 +650,7 @@ def _repro_dvc(
                     experiment_rev=dvc.experiments.scm.get_ref(EXEC_BRANCH),
                     metrics=_gather_metrics(dvc, on_error="return"),
                     dvc_studio_config=dvc_studio_config,
+                    studio_repo_url=repo_url,
                 )
 
                 if infofile is not None:
diff --git a/dvc/utils/studio.py b/dvc/utils/studio.py
--- a/dvc/utils/studio.py
+++ b/dvc/utils/studio.py
@@ -1,3 +1,4 @@
+import os
 from typing import TYPE_CHECKING, Any, Optional
 from urllib.parse import urljoin
 
@@ -125,3 +126,19 @@ def get_subrepo_relpath(repo: "Repo") -> str:
     relpath = as_posix(repo.fs.relpath(repo.root_dir, scm_root_dir))
 
     return "" if relpath == "." else relpath
+
+
+def get_repo_url(repo: "Repo") -> str:
+    from dulwich.porcelain import get_remote_repo
+
+    from dvc.env import DVC_EXP_GIT_REMOTE
+
+    repo_url = os.getenv(
+        DVC_EXP_GIT_REMOTE, repo.config.get("exp", {}).get("git_remote")
+    )
+    if repo_url:
+        try:
+            _, repo_url = get_remote_repo(repo.scm.dulwich.repo, repo_url)
+        except IndexError:
+            pass
+    return repo_url


Test patch:
diff --git a/tests/integration/test_studio_live_experiments.py b/tests/integration/test_studio_live_experiments.py
--- a/tests/integration/test_studio_live_experiments.py
+++ b/tests/integration/test_studio_live_experiments.py
@@ -3,6 +3,7 @@
 from funcy import first
 
 from dvc.env import (
+    DVC_EXP_GIT_REMOTE,
     DVC_STUDIO_OFFLINE,
     DVC_STUDIO_REPO_URL,
     DVC_STUDIO_TOKEN,
@@ -16,8 +17,9 @@
 @pytest.mark.studio
 @pytest.mark.parametrize("tmp", [True, False])
 @pytest.mark.parametrize("offline", [True, False])
+@pytest.mark.parametrize("dvc_exp_git_remote", [None, "DVC_EXP_GIT_REMOTE"])
 def test_post_to_studio(
-    tmp_dir, dvc, scm, exp_stage, mocker, monkeypatch, tmp, offline
+    tmp_dir, dvc, scm, exp_stage, mocker, monkeypatch, tmp, offline, dvc_exp_git_remote
 ):
     valid_response = mocker.MagicMock()
     valid_response.status_code = 200
@@ -28,6 +30,8 @@ def test_post_to_studio(
     monkeypatch.setenv(DVC_STUDIO_TOKEN, "STUDIO_TOKEN")
     monkeypatch.setenv(DVC_STUDIO_URL, "https://0.0.0.0")
     monkeypatch.setenv(DVC_STUDIO_OFFLINE, offline)
+    if dvc_exp_git_remote:
+        monkeypatch.setenv(DVC_EXP_GIT_REMOTE, dvc_exp_git_remote)
 
     baseline_sha = scm.get_rev()
     exp_rev = first(
@@ -52,7 +56,7 @@ def test_post_to_studio(
 
         assert start_call.kwargs["json"] == {
             "type": "start",
-            "repo_url": "STUDIO_REPO_URL",
+            "repo_url": dvc_exp_git_remote or "STUDIO_REPO_URL",
             "baseline_sha": baseline_sha,
             "name": name,
             "params": {"params.yaml": {"foo": 1}},
@@ -61,7 +65,7 @@ def test_post_to_studio(
 
         assert done_call.kwargs["json"] == {
             "type": "done",
-            "repo_url": "STUDIO_REPO_URL",
+            "repo_url": dvc_exp_git_remote or "STUDIO_REPO_URL",
             "baseline_sha": baseline_sha,
             "name": name,
             "client": "dvc",
diff --git a/tests/unit/utils/test_studio.py b/tests/unit/utils/test_studio.py
--- a/tests/unit/utils/test_studio.py
+++ b/tests/unit/utils/test_studio.py
@@ -4,12 +4,19 @@
 from requests import Response
 
 from dvc.env import (
+    DVC_EXP_GIT_REMOTE,
     DVC_STUDIO_OFFLINE,
     DVC_STUDIO_REPO_URL,
     DVC_STUDIO_TOKEN,
     DVC_STUDIO_URL,
 )
-from dvc.utils.studio import STUDIO_URL, config_to_env, env_to_config, notify_refs
+from dvc.utils.studio import (
+    STUDIO_URL,
+    config_to_env,
+    env_to_config,
+    get_repo_url,
+    notify_refs,
+)
 
 CONFIG = {"offline": True, "repo_url": "repo_url", "token": "token", "url": "url"}
 
@@ -67,3 +74,18 @@ def test_config_to_env():
 
 def test_env_to_config():
     assert env_to_config(ENV) == CONFIG
+
+
+@pytest.mark.parametrize(
+    "exp_git_remote, repo_url",
+    [
+        (None, None),
+        ("origin", "git@url"),
+        ("http://url", "http://url"),
+    ],
+)
+def test_get_repo_url(dvc, scm, monkeypatch, exp_git_remote, repo_url):
+    scm.gitpython.repo.create_remote("origin", "git@url")
+    if exp_git_remote:
+        monkeypatch.setenv(DVC_EXP_GIT_REMOTE, exp_git_remote)
+    assert get_repo_url(dvc) == repo_url