Repository: conan-io/conan

Issue:
[bug] SCM username value is ignored
Conan ignores `username` SCM attribute.
For file:
```python
from conans import ConanFile

class TestRecipe(ConanFile):
    name = 'test-recipe'
    version = '0.1'
    scm = {
        'type': 'git',
        'url': 'ssh://github.com/conan-io/conan.git',
        'username': 'test-user',
        }
```
When cloning git repository, `test-user` is not used, but current logged-in user is used instead:
```bash
test-recipe/0.1: SCM: Getting sources from url: 'ssh://github.com/conan-io/conan.git'
ERROR: Command 'git -c http.sslVerify=true clone "ssh://github.com/conan-io/conan.git" .   ' returned non-zero exit status 128.
Cloning into '.'...
seva@github.com: Permission denied (publickey).
```
(conan.git repository is used just as example).

### Environment Details
Conan version: 1.31.0
Python version: 3.8.4, 3.7.3

### Steps to reproduce
- Create conanfile recipe (example is attached).
- run 'conan create .' in it's directory.

Conanfile and example output:
[username-bug.zip](https://github.com/conan-io/conan/files/5492773/username-bug.zip)



Discussion:
According to `conans/tools/scm.py` it looks like `username` is not used if `password` is not specified. Not sure if it's OK because somebody would use a username and an SSH key to authenticate.
@kdsx is right and the username value is only considered if the password one is added to scm too. I took a look at the code and maybe it can be changed to support username and password independently.

Could you verify that a command like `git clone ssh://test-user@github.com/conan-io/conan.git` (following the example above) works from your side?
@danimtb I added this command only as example. In my recipe I use another repository. And it works when I explicitly add username to it, as in your example. And this is what in the recipe right now.
However username passed via URL is captured to `conandata.yml` during `conan export`, and that is not nice.

Resolving patch:
diff --git a/conans/client/tools/scm.py b/conans/client/tools/scm.py
--- a/conans/client/tools/scm.py
+++ b/conans/client/tools/scm.py
@@ -61,15 +61,59 @@ def run(self, command):
                     else:
                         return self._runner(command)
 
+    def _handle_scp_pattern(self, user, domain, url):
+        if self._password:
+            self._output.warn("SCM password cannot be set for scp url, ignoring parameter")
+        if self._username:
+            self._output.warn("SCM username got from URL, ignoring 'username' parameter")
+        return "{user}@{domain}:{url}".format(user=user, domain=domain, url=url)
+
+    def _handle_url_pattern(self, scheme, url, user=None, password=None):
+        if scheme == "file":
+            if self._username:
+                self._output.warn("SCM username cannot be set for file url, ignoring parameter")
+            if self._password:
+                self._output.warn("SCM password cannot be set for file url, ignoring parameter")
+            return "{scheme}://{url}".format(scheme=scheme, url=url)
+        elif scheme == "ssh" and self._password:
+            self._output.warn("SCM password cannot be set for ssh url, ignoring parameter")
+
+        if user and self._username:
+            self._output.warn("SCM username got from URL, ignoring 'username' parameter")
+        if password and self._password:
+            self._output.warn("SCM password got from URL, ignoring 'password' parameter")
+
+        the_user = user or self._username
+        the_password = password or self._password
+        if the_password and the_user and scheme != "ssh":
+            return "{scheme}://{user}:{password}@{url}".format(scheme=scheme, user=the_user,
+                                                               password=the_password, url=url)
+        elif the_user:
+            return "{scheme}://{user}@{url}".format(scheme=scheme, user=the_user, url=url)
+        else:
+            return "{scheme}://{url}".format(scheme=scheme, url=url)
+
     def get_url_with_credentials(self, url):
-        if not self._username or not self._password:
-            return url
-        if urlparse(url).password:
+        if not self._username and not self._password:
             return url
 
-        user_enc = quote_plus(self._username)
-        pwd_enc = quote_plus(self._password)
-        url = url.replace("://", "://" + user_enc + ":" + pwd_enc + "@", 1)
+        scp_regex = re.compile("^(?P<user>[a-zA-Z0-9_]+)@(?P<domain>[a-zA-Z0-9._-]+):(?P<url>.*)$")
+        url_user_pass_regex = re.compile("^(?P<scheme>file|http|https|git):\/\/(?P<user>\S+):(?P<password>\S+)@(?P<url>.*)$")
+        url_user_regex = re.compile("^(?P<scheme>file|http|https|git|ssh):\/\/(?P<user>\S+)@(?P<url>.*)$")
+        url_basic_regex = re.compile("^(?P<scheme>file|http|https|git|ssh):\/\/(?P<url>.*)$")
+
+        url_patterns = [
+            (scp_regex, self._handle_scp_pattern),
+            (url_user_pass_regex, self._handle_url_pattern),
+            (url_user_regex, self._handle_url_pattern),
+            (url_basic_regex, self._handle_url_pattern)
+        ]
+
+        for regex, handler in url_patterns:
+            match = regex.match(url)
+            if match:
+                return handler(**match.groupdict())
+        self._output.warn("URL type not supported, ignoring 'username' and 'password' parameters")
         return url
 
     @classmethod


Test patch:
diff --git a/conans/test/unittests/client/tools/scm/test_scm_base.py b/conans/test/unittests/client/tools/scm/test_scm_base.py
--- a/conans/test/unittests/client/tools/scm/test_scm_base.py
+++ b/conans/test/unittests/client/tools/scm/test_scm_base.py
@@ -3,6 +3,7 @@
 import unittest
 
 from conans.client.tools.scm import SCMBase
+from conans.errors import ConanException
 
 
 class RemoveCredentialsTest(unittest.TestCase):
@@ -33,6 +34,9 @@ def test_ssh(self):
         self.assertEqual('ssh://git@github.com:2222/conan-io/conan.git',
                          SCMBase._remove_credentials_url(
                              'ssh://git:password@github.com:2222/conan-io/conan.git'))
+        self.assertEqual('ssh://github.com:2222/conan-io/conan.git',
+                         SCMBase._remove_credentials_url(
+                             'ssh://github.com:2222/conan-io/conan.git'))
         # scp-like syntax
         self.assertEqual('git@github.com:conan-io/conan.git',
                          SCMBase._remove_credentials_url(
@@ -53,4 +57,222 @@ def test_local_windows(self):
     def test_svn_ssh(self):
         self.assertEqual('svn+ssh://10.106.191.164/home/svn/shproject',
                          SCMBase._remove_credentials_url(
-                             'svn+ssh://username:password@10.106.191.164/home/svn/shproject'))
\ No newline at end of file
+                             'svn+ssh://username:password@10.106.191.164/home/svn/shproject'))
+
+
+class OutputMock(object):
+
+    def __init__(self):
+        self.out = list()
+
+    def warn(self, text):
+        self.out.append("WARN: " + text)
+
+
+class GetUrlWithCredentialsTest(unittest.TestCase):
+
+    def test_url(self):
+        scm = SCMBase()
+        self.assertEqual('http://github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("http://github.com/conan-io/conan.git"))
+
+    def test_url_username(self):
+        scm = SCMBase()
+        self.assertEqual('http://user@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("http://user@github.com/conan-io/conan.git"))
+
+    def test_url_password(self):
+        scm = SCMBase()
+        self.assertEqual('http://user:pass@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials(
+                             "http://user:pass@github.com/conan-io/conan.git"))
+
+    def test_url_with_user_param(self):
+        scm = SCMBase(username="user")
+        self.assertEqual('https://user@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("https://github.com/conan-io/conan.git"))
+
+    def test_url_with_password_param(self):
+        scm = SCMBase(password="pass")
+        self.assertEqual('https://github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("https://github.com/conan-io/conan.git"))
+
+    def test_url_with_user_password_param(self):
+        scm = SCMBase(username="user", password="pass")
+        self.assertEqual('https://user:pass@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("https://github.com/conan-io/conan.git"))
+
+    def test_url_user_with_user_param(self):
+        output = OutputMock()
+        scm = SCMBase(username="user", output=output)
+        self.assertEqual('https://dani@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("https://dani@github.com/conan-io/conan.git"))
+        self.assertEqual(1, len(output.out))
+        self.assertIn("WARN: SCM username got from URL, ignoring 'username' parameter", output.out)
+
+    def test_url_user_with_password_param(self):
+        scm = SCMBase(password="pass")
+        self.assertEqual('https://dani:pass@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("https://dani@github.com/conan-io/conan.git"))
+
+    def test_url_user_with_user_password_param(self):
+        output = OutputMock()
+        scm = SCMBase(username="user", password="pass", output=output)
+        self.assertEqual('https://dani:pass@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("https://dani@github.com/conan-io/conan.git"))
+        self.assertEqual(1, len(output.out))
+        self.assertIn("WARN: SCM username got from URL, ignoring 'username' parameter", output.out)
+
+    def test_url_user_pass_with_user_param(self):
+        output = OutputMock()
+        scm = SCMBase(username="user", output=output)
+        self.assertEqual('http://dani:pass@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials(
+                             "http://dani:pass@github.com/conan-io/conan.git"))
+        self.assertEqual(1, len(output.out))
+        self.assertIn("WARN: SCM username got from URL, ignoring 'username' parameter", output.out)
+
+    def test_url_user_pass_with_password_param(self):
+        output = OutputMock()
+        scm = SCMBase(password="pass", output=output)
+        self.assertEqual('http://dani:secret@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials(
+                             "http://dani:secret@github.com/conan-io/conan.git"))
+        self.assertEqual(1, len(output.out))
+        self.assertIn("WARN: SCM password got from URL, ignoring 'password' parameter", output.out)
+
+    def test_url_user_pass_with_user_password_param(self):
+        output = OutputMock()
+        scm = SCMBase(username="user", password="pass", output=output)
+        self.assertEqual('http://dani:secret@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials(
+                             "http://dani:secret@github.com/conan-io/conan.git"))
+        self.assertEqual(2, len(output.out))
+        self.assertIn("WARN: SCM username got from URL, ignoring 'username' parameter", output.out)
+        self.assertIn("WARN: SCM password got from URL, ignoring 'password' parameter", output.out)
+
+    def test_ssh(self):
+        scm = SCMBase()
+        self.assertEqual('ssh://github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("ssh://github.com/conan-io/conan.git"))
+
+    def test_ssh_username_password(self):
+        output = OutputMock()
+        scm = SCMBase(username="dani", password="pass", output=output)
+        self.assertEqual('ssh://dani@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("ssh://github.com/conan-io/conan.git"))
+        self.assertEqual(1, len(output.out))
+        self.assertIn("WARN: SCM password cannot be set for ssh url, ignoring parameter", output.out)
+
+    def test_ssh_username(self):
+        scm = SCMBase(username="dani")
+        self.assertEqual('ssh://dani@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("ssh://github.com/conan-io/conan.git"))
+
+    def test_ssh_password(self):
+        output = OutputMock()
+        scm = SCMBase(password="pass", output=output)
+        self.assertEqual('ssh://github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("ssh://github.com/conan-io/conan.git"))
+        self.assertEqual(1, len(output.out))
+        self.assertIn("WARN: SCM password cannot be set for ssh url, ignoring parameter", output.out)
+
+    def test_ssh_url_with_username_only_password(self):
+        output = OutputMock()
+        scm = SCMBase(password="pass", output=output)
+        self.assertEqual('ssh://dani@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("ssh://dani@github.com/conan-io/conan.git"))
+        self.assertEqual(1, len(output.out))
+        self.assertIn("WARN: SCM password cannot be set for ssh url, ignoring parameter", output.out)
+
+    def test_ssh_url_with_username_only_username(self):
+        output = OutputMock()
+        scm = SCMBase(username="dani", output=output)
+        self.assertEqual('ssh://git@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("ssh://git@github.com/conan-io/conan.git"))
+        self.assertIn("WARN: SCM username got from URL, ignoring 'username' parameter", output.out)
+
+    def test_ssh_url_with_username_and_username_password(self):
+        output = OutputMock()
+        scm = SCMBase(password="pass", username="dani", output=output)
+        self.assertEqual('ssh://git@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("ssh://git@github.com/conan-io/conan.git"))
+        self.assertEqual(2, len(output.out))
+        self.assertIn("WARN: SCM password cannot be set for ssh url, ignoring parameter", output.out)
+        self.assertIn("WARN: SCM username got from URL, ignoring 'username' parameter", output.out)
+
+    def test_ssh_url_with_username_password_and_only_password(self):
+        output = OutputMock()
+        scm = SCMBase(password="password", output=output)
+        self.assertEqual('ssh://git:pass@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("ssh://git:pass@github.com/conan-io/conan.git"))
+        self.assertEqual(1, len(output.out))
+        self.assertIn("WARN: SCM password cannot be set for ssh url, ignoring parameter", output.out)
+
+    def test_ssh_url_with_username_password_and_only_username(self):
+        output = OutputMock()
+        scm = SCMBase(username="dani", output=output)
+        self.assertEqual('ssh://git:pass@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("ssh://git:pass@github.com/conan-io/conan.git"))
+        self.assertEqual(1, len(output.out))
+        self.assertIn("WARN: SCM username got from URL, ignoring 'username' parameter",
+                      output.out)
+
+    def test_ssh_url_with_username_password_and_username_password(self):
+        output = OutputMock()
+        scm = SCMBase(password="password", username="dani", output=output)
+        self.assertEqual("ssh://git:pass@github.com/conan-io/conan.git",
+                         scm.get_url_with_credentials("ssh://git:pass@github.com/conan-io/conan.git"))
+        self.assertEqual(2, len(output.out))
+        self.assertIn("WARN: SCM password cannot be set for ssh url, ignoring parameter", output.out)
+        self.assertIn("WARN: SCM username got from URL, ignoring 'username' parameter", output.out)
+
+    def test_scp(self):
+        scm = SCMBase()
+        self.assertEqual('git@github.com/conan-io/conan.git',
+                         scm.get_url_with_credentials("git@github.com/conan-io/conan.git"))
+
+    def test_scp_only_password(self):
+        output = OutputMock()
+        scm = SCMBase(password="pass", output=output)
+        self.assertEqual("git@github.com:conan-io/conan.git",
+                         scm.get_url_with_credentials("git@github.com:conan-io/conan.git"))
+        self.assertIn("WARN: SCM password cannot be set for scp url, ignoring parameter", output.out)
+
+    def test_scp_only_username(self):
+        output = OutputMock()
+        scm = SCMBase(username="dani", output=output)
+        self.assertEqual('git@github.com:conan-io/conan.git',
+                         scm.get_url_with_credentials("git@github.com:conan-io/conan.git"))
+        self.assertIn("WARN: SCM username got from URL, ignoring 'username' parameter", output.out)
+
+    def test_scp_username_password(self):
+        output = OutputMock()
+        scm = SCMBase(password="pass", username="dani", output=output)
+        self.assertEqual("git@github.com:conan-io/conan.git",
+                         scm.get_url_with_credentials("git@github.com:conan-io/conan.git"))
+        self.assertEqual(2, len(output.out))
+        self.assertIn("WARN: SCM password cannot be set for scp url, ignoring parameter", output.out)
+        self.assertIn("WARN: SCM username got from URL, ignoring 'username' parameter", output.out)
+
+    def test_scp_url_username_password(self):
+        output = OutputMock()
+        scm = SCMBase(password="password", output=output)
+        self.assertEqual('git:pass@github.com:conan-io/conan.git',
+                         scm.get_url_with_credentials("git:pass@github.com:conan-io/conan.git"))
+        self.assertIn("WARN: URL type not supported, ignoring 'username' and 'password' "
+                      "parameters", output.out)
+
+    def test_file_url(self):
+        scm = SCMBase()
+        self.assertEqual("file://path/to/.git", scm.get_url_with_credentials("file://path/to/.git"))
+
+    def test_file_url_with_username_password_params(self):
+        output = OutputMock()
+        scm = SCMBase(username="user", password="pass", output=output)
+        self.assertEqual('file://path/to/.git', scm.get_url_with_credentials("file://path/to/.git"))
+        self.assertEqual(2, len(output.out))
+        self.assertIn("WARN: SCM username cannot be set for file url, ignoring parameter",
+                      output.out)
+        self.assertIn("WARN: SCM password cannot be set for file url, ignoring parameter",
+                      output.out)