Repository: conan-io/conan

Issue:
Rename a remote
- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.

conan 1.1.0

Currently, the only way to rename a remote is to remove it, and then add it with a new name. 
- This breaks existing reference-remote-associations.
- The old name must be known.

Solutions could be 
- `conan remote rename old_name new_name`
- `conan remote add remote_name remote_url --rename`
- `conan remote add remote_name remote_url --force`

I would prefer the last solution. It always works, no matter if there is an existing remote.

See also https://github.com/conan-io/conan-package-tools/issues/156#issuecomment-373639997


Discussion:
Agree, this is a valid feature.

Yes, maybe the last one is the most practical, but we can have also the ``rename``, it is not incompatible, I vote for both.

Support for renaming has been implemented, will be in 1.3

Regarding ``add --force``, I am not sure, as it is not well defined regarding the ordering:
- If the remote exists, will the add replace just the URL, but keep ordering? Or will it add at the end, as defined by the ``add`` semantics?
- What does it mean regarding existing URL? If the URL already exists, then it is the equivalent of the rename? Will it keep the order of the renamed remote or added to the end of the list?

Resolving patch:
diff --git a/conans/client/command.py b/conans/client/command.py
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -901,6 +901,8 @@ def remote(self, *args):
                                 help='Verify SSL certificated. Default True')
         parser_add.add_argument("-i", "--insert", nargs="?", const=0, type=int, action=OnceArgument,
                                 help="insert remote at specific index")
+        parser_add.add_argument("-f", "--force", default=False, action='store_true',
+                                help="Force addition, will update if existing")
         parser_rm = subparsers.add_parser('remove', help='Remove a remote')
         parser_rm.add_argument('remote', help='Name of the remote')
         parser_upd = subparsers.add_parser('update', help='Update the remote url')
@@ -942,7 +944,7 @@ def remote(self, *args):
             remotes = self._conan.remote_list()
             self._outputer.remote_list(remotes)
         elif args.subcommand == "add":
-            return self._conan.remote_add(remote, url, verify_ssl, args.insert)
+            return self._conan.remote_add(remote, url, verify_ssl, args.insert, args.force)
         elif args.subcommand == "remove":
             return self._conan.remote_remove(remote)
         elif args.subcommand == "rename":
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -697,8 +697,8 @@ def remote_list(self):
         return self._registry.remotes
 
     @api_method
-    def remote_add(self, remote, url, verify_ssl=True, insert=None):
-        return self._registry.add(remote, url, verify_ssl, insert)
+    def remote_add(self, remote, url, verify_ssl=True, insert=None, force=None):
+        return self._registry.add(remote, url, verify_ssl, insert, force)
 
     @api_method
     def remote_remove(self, remote):
diff --git a/conans/client/remote_registry.py b/conans/client/remote_registry.py
--- a/conans/client/remote_registry.py
+++ b/conans/client/remote_registry.py
@@ -1,8 +1,10 @@
 import os
+import fasteners
+
+from collections import OrderedDict, namedtuple
+
 from conans.errors import ConanException, NoRemoteAvailable
 from conans.util.files import load, save
-from collections import OrderedDict, namedtuple
-import fasteners
 from conans.util.config_parser import get_bool_from_text_value
 from conans.util.log import logger
 
@@ -155,12 +157,46 @@ def update_ref(self, conan_reference, remote):
             refs[conan_reference] = remote
             self._save(remotes, refs)
 
-    def add(self, remote_name, remote, verify_ssl=True, insert=None):
+    def _upsert(self, remote_name, url, verify_ssl, insert):
+        self._remotes = None  # invalidate cached remotes
+        with fasteners.InterProcessLock(self._filename + ".lock", logger=logger):
+            remotes, refs = self._load()
+            # Remove duplicates
+            remotes.pop(remote_name, None)
+            remotes_list = []
+            renamed = None
+            for name, r in remotes.items():
+                if r[0] != url:
+                    remotes_list.append((name, r))
+                else:
+                    renamed = name
+
+            if insert is not None:
+                try:
+                    insert_index = int(insert)
+                except ValueError:
+                    raise ConanException("insert argument must be an integer")
+                remotes_list.insert(insert_index, (remote_name, (url, verify_ssl)))
+                remotes = OrderedDict(remotes_list)
+            else:
+                remotes = OrderedDict(remotes_list)
+                remotes[remote_name] = (url, verify_ssl)
+
+            if renamed:
+                for k, v in refs.items():
+                    if v == renamed:
+                        refs[k] = remote_name
+            self._save(remotes, refs)
+
+    def add(self, remote_name, url, verify_ssl=True, insert=None, force=None):
+        if force:
+            return self._upsert(remote_name, url, verify_ssl, insert)
+
         def exists_function(remotes):
             if remote_name in remotes:
                 raise ConanException("Remote '%s' already exists in remotes (use update to modify)"
                                      % remote_name)
-        self._add_update(remote_name, remote, verify_ssl, exists_function, insert)
+        self._add_update(remote_name, url, verify_ssl, exists_function, insert)
 
     def remove(self, remote_name):
         self._remotes = None  # invalidate cached remotes
@@ -172,11 +208,11 @@ def remove(self, remote_name):
             refs = {k: v for k, v in refs.items() if v != remote_name}
             self._save(remotes, refs)
 
-    def update(self, remote_name, remote, verify_ssl=True, insert=None):
+    def update(self, remote_name, url, verify_ssl=True, insert=None):
         def exists_function(remotes):
             if remote_name not in remotes:
                 raise ConanException("Remote '%s' not found in remotes" % remote_name)
-        self._add_update(remote_name, remote, verify_ssl, exists_function, insert)
+        self._add_update(remote_name, url, verify_ssl, exists_function, insert)
 
     def rename(self, remote_name, new_remote_name):
         self._remotes = None  # invalidate cached remotes
@@ -204,14 +240,14 @@ def define_remotes(self, remotes):
             refs = {k: v for k, v in refs.items() if v in new_remotes}
             self._save(new_remotes, refs)
 
-    def _add_update(self, remote_name, remote, verify_ssl, exists_function, insert=None):
+    def _add_update(self, remote_name, url, verify_ssl, exists_function, insert=None):
         self._remotes = None  # invalidate cached remotes
         with fasteners.InterProcessLock(self._filename + ".lock", logger=logger):
             remotes, refs = self._load()
             exists_function(remotes)
             urls = {r[0]: name for name, r in remotes.items() if name != remote_name}
-            if remote in urls:
-                raise ConanException("Remote '%s' already exists with same URL" % urls[remote])
+            if url in urls:
+                raise ConanException("Remote '%s' already exists with same URL" % urls[url])
             if insert is not None:
                 try:
                     insert_index = int(insert)
@@ -219,8 +255,8 @@ def _add_update(self, remote_name, remote, verify_ssl, exists_function, insert=N
                     raise ConanException("insert argument must be an integer")
                 remotes.pop(remote_name, None)  # Remove if exists (update)
                 remotes_list = list(remotes.items())
-                remotes_list.insert(insert_index, (remote_name, (remote, verify_ssl)))
+                remotes_list.insert(insert_index, (remote_name, (url, verify_ssl)))
                 remotes = OrderedDict(remotes_list)
             else:
-                remotes[remote_name] = (remote, verify_ssl)
+                remotes[remote_name] = (url, verify_ssl)
             self._save(remotes, refs)


Test patch:
diff --git a/conans/test/command/remote_test.py b/conans/test/command/remote_test.py
--- a/conans/test/command/remote_test.py
+++ b/conans/test/command/remote_test.py
@@ -41,6 +41,49 @@ def basic_test(self):
         output = str(self.client.user_io.out)
         self.assertIn("remote1: http://", output.splitlines()[0])
 
+    def add_force_test(self):
+        client = TestClient()
+        client.run("remote add r1 https://r1")
+        client.run("remote add r2 https://r2")
+        client.run("remote add r3 https://r3")
+        client.run("remote add_ref Hello/0.1@user/testing r2")
+        client.run("remote add_ref Hello2/0.1@user/testing r1")
+
+        client.run("remote add r4 https://r4 -f")
+        client.run("remote list")
+        lines = str(client.user_io.out).splitlines()
+        self.assertIn("r1: https://r1", lines[0])
+        self.assertIn("r2: https://r2", lines[1])
+        self.assertIn("r3: https://r3", lines[2])
+        self.assertIn("r4: https://r4", lines[3])
+
+        client.run("remote add r2 https://newr2 -f")
+        client.run("remote list")
+        lines = str(client.user_io.out).splitlines()
+        self.assertIn("r1: https://r1", lines[0])
+        self.assertIn("r3: https://r3", lines[1])
+        self.assertIn("r4: https://r4", lines[2])
+        self.assertIn("r2: https://newr2", lines[3])
+
+        client.run("remote add newr1 https://r1 -f")
+        client.run("remote list")
+        lines = str(client.user_io.out).splitlines()
+        self.assertIn("r3: https://r3", lines[0])
+        self.assertIn("r4: https://r4", lines[1])
+        self.assertIn("r2: https://newr2", lines[2])
+        self.assertIn("newr1: https://r1", lines[3])
+        client.run("remote list_ref")
+        self.assertIn("Hello2/0.1@user/testing: newr1", client.out)
+        self.assertIn("Hello/0.1@user/testing: r2", client.out)
+
+        client.run("remote add newr1 https://newr1 -f -i")
+        client.run("remote list")
+        lines = str(client.user_io.out).splitlines()
+        self.assertIn("newr1: https://newr1", lines[0])
+        self.assertIn("r3: https://r3", lines[1])
+        self.assertIn("r4: https://r4", lines[2])
+        self.assertIn("r2: https://newr2", lines[3])
+
     def rename_test(self):
         client = TestClient()
         client.run("remote add r1 https://r1")