Repository: iterative/dvc

Issue:
remote local config does not see existing remote
## Bug Report

### Please provide information about your setup

**Output of `dvc version`:**

```console
DVC version: 1.1.10+1c9588
Python version: 3.8.4
Platform: macOS-10.15.4-x86_64-i386-64bit
Binary: False
Package: None
Supported remotes: azure, gdrive, gs, hdfs, http, https, s3, ssh, oss
Repo: dvc, git
Filesystem type (workspace): ('apfs', '/dev/disk1s1')
```

**Additional Information (if any):**

```
dvc remote add -d storage s3://storage
dvc remote modify storage region us-east-2 --local
```

complains:

```
ERROR: configuration error - config file error: remote 'storage' doesn't exists.
```

For example, it breaks the use case of using `dvc remote modify` to store credentials in the local config only.


Discussion:

Patch:
diff --git a/dvc/command/remote.py b/dvc/command/remote.py
index db622a378..a3484a7be 100644
--- a/dvc/command/remote.py
+++ b/dvc/command/remote.py
@@ -3,7 +3,7 @@ import logging
 
 from dvc.command.base import append_doc_link, fix_subparsers
 from dvc.command.config import CmdConfig
-from dvc.config import ConfigError
+from dvc.config import ConfigError, merge
 from dvc.utils import format_link
 
 logger = logging.getLogger(__name__)
@@ -61,7 +61,12 @@ class CmdRemoteRemove(CmdRemote):
 class CmdRemoteModify(CmdRemote):
     def run(self):
         with self.config.edit(self.args.level) as conf:
-            self._check_exists(conf)
+            merged = self.config.load_config_to_level(self.args.level)
+            merge(merged, conf)
+            self._check_exists(merged)
+
+            if self.args.name not in conf["remote"]:
+                conf["remote"][self.args.name] = {}
             section = conf["remote"][self.args.name]
             if self.args.unset:
                 section.pop(self.args.option, None)
diff --git a/dvc/config.py b/dvc/config.py
index 68cda1bec..26181e4d6 100644
--- a/dvc/config.py
+++ b/dvc/config.py
@@ -400,7 +400,7 @@ class Config(dict):
             if merge_level == level:
                 break
             if merge_level in self.files:
-                _merge(merged_conf, self.load_one(merge_level))
+                merge(merged_conf, self.load_one(merge_level))
         return merged_conf
 
     @contextmanager
@@ -414,7 +414,7 @@ class Config(dict):
         conf = self._save_paths(conf, self.files[level])
 
         merged_conf = self.load_config_to_level(level)
-        _merge(merged_conf, conf)
+        merge(merged_conf, conf)
         self.validate(merged_conf)
 
         self._save_config(level, conf)
@@ -453,11 +453,11 @@ def _pack_remotes(conf):
     return result
 
 
-def _merge(into, update):
+def merge(into, update):
     """Merges second dict into first recursively"""
     for key, val in update.items():
         if isinstance(into.get(key), dict) and isinstance(val, dict):
-            _merge(into[key], val)
+            merge(into[key], val)
         else:
             into[key] = val
 


Test patch:
diff --git a/tests/func/test_remote.py b/tests/func/test_remote.py
index 35ce310f0..38196ffbd 100644
--- a/tests/func/test_remote.py
+++ b/tests/func/test_remote.py
@@ -232,6 +232,24 @@ def test_modify_missing_remote(tmp_dir, dvc):
     assert main(["remote", "modify", "myremote", "user", "xxx"]) == 251
 
 
+def test_remote_modify_local_on_repo_config(tmp_dir, dvc):
+    assert main(["remote", "add", "myremote", "http://example.com/path"]) == 0
+    assert (
+        main(["remote", "modify", "myremote", "user", "xxx", "--local"]) == 0
+    )
+    assert dvc.config.load_one("local")["remote"]["myremote"] == {
+        "user": "xxx"
+    }
+    assert dvc.config.load_one("repo")["remote"]["myremote"] == {
+        "url": "http://example.com/path"
+    }
+    dvc.config.load()
+    assert dvc.config["remote"]["myremote"] == {
+        "url": "http://example.com/path",
+        "user": "xxx",
+    }
+
+
 def test_external_dir_resource_on_no_cache(tmp_dir, dvc, tmp_path_factory):
     # https://github.com/iterative/dvc/issues/2647, is some situations
     # (external dir dependency) cache is required to calculate dir md5