Repository: conan-io/conan

Issue:
conan config from a specific branch/tag from a git repository
To help us debug your issue please explain:

- [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 version 1.7.4
Git version 1.8.3.1
Centos 7.1.503

I want to install the settings from a git repository, but from a specific branch or tag. I tried: 

```bash
conan config install "git@gitlab.gitlabserv.com:chreniuc/conan_config.git" --args "-b tag"
```

But  I get an error:

```bash
Trying to clone repo  git@gitlab.gitlabserv.com:chreniuc/conan_config.git
Unknown option: -b
usage: git [--version] [--help] [-c name=value]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p|--paginate|--no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           <command> [<args>]
ERROR: config install error. Can't clone repo: Command 'git -c http.sslVerify=True -b tag clone "git@gitlab.gitlabserv.com:chreniuc/conan_config.git" config' returned non-zero exit status 129
```

I also tried:

```bash
conan config install "git@gitlab.gitlabserver.com:chreniuc/conan_config.git -b master"
```

But I got this error:

```bash
ERROR: I don't know how to process git@gitlab.gitlabserver.com:chreniuc/conan_config.git -b master
```

Does conan support this feature? Am I missing something? I tried to look for an example, but I couldn't find any or any documentation related to this.

The use case for this is: we want to have different versions of the config for conan. We have a git repository and tags for each version. Can this be achieved?

Also, another thing to notice is that, when you run:
```bash
 conan config install --help
```
For **--args** you get:
```bash
String with extra arguments for "git clone"
```

But if you take a look at the command that was generated by conan, you can see that those args are passed to the `git` command not to the `git clone`:

```bash
Command 'git -c http.sslVerify=True -b tag clone "git@gitlab.gitlabserv.com:chreniuc/conan_config.git" config' returned non-zero exit status 129
```



Discussion:
Setting this as high priority as it could be useful to clone recusrively submodules for hooks in the conan config folder

Resolving patch:
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
@@ -556,14 +556,11 @@ def config_rm(self, item):
         self._client_cache.invalidate()
 
     @api_method
-    def config_install(self, item, verify_ssl, config_type=None, args=None):
-        # _make_abs_path, but could be not a path at all
-        if item is not None and os.path.exists(item) and not os.path.isabs(item):
-            item = os.path.abspath(item)
+    def config_install(self, path_or_url, verify_ssl, config_type=None, args=None):
 
         from conans.client.conf.config_installer import configuration_install
         requester = self._remote_manager._auth_manager._rest_client.requester,  # FIXME: Look out!
-        return configuration_install(item, self._client_cache, self._user_io.out, verify_ssl,
+        return configuration_install(path_or_url, self._client_cache, self._user_io.out, verify_ssl,
                                      requester=requester, config_type=config_type, args=args)
 
     def _info_args(self, reference, install_folder, profile_name, settings, options, env):
diff --git a/conans/client/conf/config_installer.py b/conans/client/conf/config_installer.py
--- a/conans/client/conf/config_installer.py
+++ b/conans/client/conf/config_installer.py
@@ -7,6 +7,7 @@
 from conans import load
 from conans.client import tools
 from conans.client.remote_registry import load_registry_txt
+from conans.client.tools import Git
 from conans.client.tools.files import unzip
 from conans.errors import ConanException
 from conans.util.files import mkdir, rmdir, walk
@@ -38,24 +39,21 @@ def _handle_profiles(source_folder, target_folder, output):
             relative_path = ""
         for f in files:
             profile = os.path.join(relative_path, f)
-            output.info("    Installing profile %s" % profile)
+            output.info("  - %s" % profile)
             shutil.copy(os.path.join(root, f), os.path.join(target_folder, profile))
 
 
 def _process_git_repo(repo_url, client_cache, output, tmp_folder, verify_ssl, args=None):
-    output.info("Trying to clone repo  %s" % repo_url)
+    output.info("Trying to clone repo: %s" % repo_url)
 
     with tools.chdir(tmp_folder):
         try:
             args = args or ""
-            subprocess.check_output('git -c http.sslVerify=%s %s clone "%s" config'
-                                    % (verify_ssl, args, repo_url),
-                                    shell=True)
-            output.info("Repo cloned")
+            git = Git(verify_ssl=verify_ssl, output=output)
+            git.clone(repo_url, args=args)
+            output.info("Repo cloned!")
         except Exception as e:
-            raise ConanException("config install error. Can't clone repo: %s" % str(e))
-
-    tmp_folder = os.path.join(tmp_folder, "config")
+            raise ConanException("Can't clone repo: %s" % str(e))
     _process_folder(tmp_folder, client_cache, output)
 
 
@@ -84,7 +82,7 @@ def _process_folder(folder, client_cache, output):
                 conan_conf = client_cache.conan_config
                 _handle_conan_conf(conan_conf, os.path.join(root, f))
             elif f == "remotes.txt":
-                output.info("Defining remotes")
+                output.info("Defining remotes from remotes.txt")
                 _handle_remotes(client_cache, os.path.join(root, f))
             else:
                 relpath = os.path.relpath(root, folder)
@@ -94,7 +92,7 @@ def _process_folder(folder, client_cache, output):
                 shutil.copy(os.path.join(root, f), target_folder)
         for d in dirs:
             if d == "profiles":
-                output.info("Installing profiles")
+                output.info("Installing profiles:")
                 profiles_path = client_cache.profiles_path
                 _handle_profiles(os.path.join(root, d), profiles_path, output)
                 break
@@ -111,32 +109,76 @@ def _process_download(item, client_cache, output, tmp_folder, verify_ssl, reques
         raise ConanException("Error while installing config from %s\n%s" % (item, str(e)))
 
 
-def configuration_install(item, client_cache, output, verify_ssl, requester,
+def configuration_install(path_or_url, client_cache, output, verify_ssl, requester,
                           config_type=None, args=None):
+    if path_or_url is None:
+        try:
+            item = client_cache.conan_config.get_item("general.config_install")
+            _config_type, path_or_url, _args = _process_config_install_item(item)
+        except ConanException:
+            raise ConanException("Called config install without arguments and "
+                                 "'general.config_install' not defined in conan.conf")
+    else:
+        _config_type, path_or_url, _args = _process_config_install_item(path_or_url)
+
+    config_type = config_type or _config_type
+    args = args or _args
+
+    if os.path.exists(path_or_url):
+        path_or_url = os.path.abspath(path_or_url)
+
     tmp_folder = os.path.join(client_cache.conan_folder, "tmp_config_install")
     # necessary for Mac OSX, where the temp folders in /var/ are symlinks to /private/var/
     tmp_folder = os.path.realpath(tmp_folder)
     mkdir(tmp_folder)
     try:
-        if item is None:
-            try:
-                item = client_cache.conan_config.get_item("general.config_install")
-            except ConanException:
-                raise ConanException("Called config install without arguments and "
-                                     "'general.config_install' not defined in conan.conf")
-
-        if item.endswith(".git") or config_type == "git":
-            _process_git_repo(item, client_cache, output, tmp_folder, verify_ssl, args)
-        elif os.path.isdir(item):
-            _process_folder(item, client_cache, output)
-        elif os.path.isfile(item):
-            _process_zip_file(item, client_cache, output, tmp_folder)
-        elif item.startswith("http"):
-            _process_download(item, client_cache, output, tmp_folder, verify_ssl,
+
+        if config_type == "git":
+            _process_git_repo(path_or_url, client_cache, output, tmp_folder, verify_ssl, args)
+        elif config_type == "dir":
+            args = None
+            _process_folder(path_or_url, client_cache, output)
+        elif config_type == "file":
+            args = None
+            _process_zip_file(path_or_url, client_cache, output, tmp_folder)
+        elif config_type == "url":
+            args = None
+            _process_download(path_or_url, client_cache, output, tmp_folder, verify_ssl,
                               requester=requester)
         else:
-            raise ConanException("I don't know how to process %s" % item)
+            raise ConanException("Unable to process config install: %s" % path_or_url)
     finally:
-        if item:
-            client_cache.conan_config.set_item("general.config_install", item)
+        if config_type is not None and path_or_url is not None:
+            value = "%s:[%s, %s]" % (config_type, path_or_url, args)
+            client_cache.conan_config.set_item("general.config_install", value)
         rmdir(tmp_folder)
+
+
+def _process_config_install_item(item):
+    """
+    Processes a config_install item and outputs a tuple with the configuration type, the path/url
+    and additional args
+
+    :param item: config_install item value from conan.conf with a simple path/url or a string
+    following this pattern: "<config_type>:[<path_or_url>, <args>]"
+    :return: configuration source type (git, url, dir or file), path to file/dir or git/http url and
+    additional arguments
+    """
+    config_type, path_or_url, args = None, None, None
+    if not item.startswith(("git:", "dir:", "url:", "file:")):
+        path_or_url = item
+        if path_or_url.endswith(".git"):
+            config_type = "git"
+        elif os.path.isdir(path_or_url):
+            config_type = "dir"
+        elif os.path.isfile(path_or_url):
+            config_type = "file"
+        elif path_or_url.startswith("http"):
+            config_type = "url"
+        else:
+            raise ConanException("Unable to process config install: %s" % path_or_url)
+    else:
+        config_type, rest = item.split(":", 1)
+        path_or_url, args = [item.strip() for item in rest[1:-1].rstrip().split(",", 1)]
+        args = None if "none" in args.lower() else args
+    return config_type, path_or_url, args
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
@@ -58,7 +58,7 @@ def _configure_ssl_verify(self):
         # TODO: This should be a context manager
         return self.run("config http.sslVerify %s" % ("true" if self._verify_ssl else "false"))
 
-    def clone(self, url, branch=None):
+    def clone(self, url, branch=None, args=""):
         url = self.get_url_with_credentials(url)
         if os.path.exists(url):
             url = url.replace("\\", "/")  # Windows local directory
@@ -75,7 +75,7 @@ def clone(self, url, branch=None):
             output += self.run("checkout -t origin/%s" % branch)
         else:
             branch_cmd = "--branch %s" % branch if branch else ""
-            output = self.run('clone "%s" . %s' % (url, branch_cmd))
+            output = self.run('clone "%s" . %s %s' % (url, branch_cmd, args))
             output += self._configure_ssl_verify()
 
         return output


Test patch:
diff --git a/conans/test/functional/command/config_install_test.py b/conans/test/functional/command/config_install_test.py
--- a/conans/test/functional/command/config_install_test.py
+++ b/conans/test/functional/command/config_install_test.py
@@ -180,7 +180,7 @@ def install_file_test(self):
         """
         zippath = self._create_zip()
         self.client.run('config install "%s"' % zippath)
-        self._check(zippath)
+        self._check("file:[%s, None]" % zippath)
         self.assertTrue(os.path.exists(zippath))
 
     def install_dir_test(self):
@@ -189,7 +189,7 @@ def install_dir_test(self):
         folder = self._create_profile_folder()
         self.assertTrue(os.path.isdir(folder))
         self.client.run('config install "%s"' % folder)
-        self._check(folder)
+        self._check("dir:[%s, None]" % folder)
 
     def test_without_profile_folder(self):
         shutil.rmtree(self.client.client_cache.profiles_path)
@@ -209,17 +209,17 @@ def my_download(obj, url, filename, **kwargs):  # @UnusedVariable
 
         with patch.object(Downloader, 'download', new=my_download):
             self.client.run("config install http://myfakeurl.com/myconf.zip")
-            self._check("http://myfakeurl.com/myconf.zip")
+            self._check("url:[http://myfakeurl.com/myconf.zip, None]")
 
             # repeat the process to check
             self.client.run("config install http://myfakeurl.com/myconf.zip")
-            self._check("http://myfakeurl.com/myconf.zip")
+            self._check("url:[http://myfakeurl.com/myconf.zip, None]")
 
     def failed_install_repo_test(self):
         """ should install from a git repo
         """
         self.client.run('config install notexistingrepo.git', assert_error=True)
-        self.assertIn("ERROR: config install error. Can't clone repo", self.client.out)
+        self.assertIn("ERROR: Can't clone repo", self.client.out)
 
     def failed_install_http_test(self):
         """ should install from a http zip
@@ -241,7 +241,8 @@ def install_repo_test(self):
             self.client.runner('git commit -m "mymsg"')
 
         self.client.run('config install "%s/.git"' % folder)
-        self._check("%s/.git" % folder)
+        check_path = os.path.join(folder, ".git")
+        self._check("git:[%s, None]" % check_path)
 
     def install_repo_relative_test(self):
         relative_folder = "./config"
@@ -256,7 +257,7 @@ def install_repo_relative_test(self):
             self.client.runner('git commit -m "mymsg"')
 
         self.client.run('config install "%s/.git"' % relative_folder)
-        self._check(os.path.join("%s" % folder, ".git"))
+        self._check("git:[%s, None]" % os.path.join("%s" % folder, ".git"))
 
     def install_custom_args_test(self):
         """ should install from a git repo
@@ -270,8 +271,9 @@ def install_custom_args_test(self):
             self.client.runner('git config user.email myname@mycompany.com')
             self.client.runner('git commit -m "mymsg"')
 
-        self.client.run('config install "%s/.git" --args="-c init.templateDir=\"\""' % folder)
-        self._check("%s/.git" % folder)
+        self.client.run('config install "%s/.git" --args="-c init.templateDir=value"' % folder)
+        check_path = os.path.join(folder, ".git")
+        self._check("git:[%s, -c init.templateDir=value]" % check_path)
 
     def force_git_type_test(self):
         client = TestClient()
@@ -284,7 +286,7 @@ def reinstall_test(self):
         zippath = self._create_zip()
         self.client.run('config set general.config_install="%s"' % zippath)
         self.client.run("config install")
-        self._check(zippath)
+        self._check("file:[%s, None]" % zippath)
 
     def reinstall_error_test(self):
         """ should use configured URL in conan.conf
@@ -341,7 +343,7 @@ def my_download(obj, url, filename, **kwargs):  # @UnusedVariable
             self.assertIn(fake_url_hidden_password, self.client.out)
 
             # Check credentials still stored in configuration
-            self._check(fake_url_with_credentials)
+            self._check("url:[%s, None]" % fake_url_with_credentials)
 
     def ssl_verify_test(self):
         fake_url = "https://fakeurl.com/myconf.zip"
@@ -359,3 +361,49 @@ def download_verify_true(obj, url, filename, **kwargs):  # @UnusedVariable
 
         with patch.object(Downloader, 'download', new=download_verify_true):
             self.client.run("config install %s --verify-ssl=True" % fake_url)
+
+    def test_git_checkout_is_possible(self):
+        folder = self._create_profile_folder()
+        with tools.chdir(folder):
+            self.client.runner('git init .')
+            self.client.runner('git add .')
+            self.client.runner('git config user.name myname')
+            self.client.runner('git config user.email myname@mycompany.com')
+            self.client.runner('git commit -m "mymsg"')
+            self.client.runner('git checkout -b other_branch')
+            save(os.path.join(folder, "hooks", "cust", "cust.py"), "")
+            self.client.runner('git add .')
+            self.client.runner('git commit -m "my file"')
+            self.client.runner('git tag 0.0.1')
+            self.client.runner('git checkout master')
+
+        # Without checkout
+        self.client.run('config install "%s/.git"' % folder)
+        check_path = os.path.join(folder, ".git")
+        self._check("git:[%s, None]" % check_path)
+        file_path = os.path.join(self.client.client_cache.conan_folder, "hooks", "cust", "cust.py")
+        self.assertFalse(os.path.exists(file_path))
+        # With checkout tag and reuse url
+        self.client.run('config install --args="-b 0.0.1"')
+        check_path = os.path.join(folder, ".git")
+        self._check("git:[%s, -b 0.0.1]" % check_path)
+        self.assertTrue(os.path.exists(file_path))
+        # With checkout branch and reuse url
+        self.client.run('config install --args="-b other_branch"')
+        check_path = os.path.join(folder, ".git")
+        self._check("git:[%s, -b other_branch]" % check_path)
+        self.assertTrue(os.path.exists(file_path))
+        # Add changes to that branch and update
+        with tools.chdir(folder):
+            self.client.runner('git checkout other_branch')
+            save(os.path.join(folder, "hooks", "other", "other.py"), "")
+            self.client.runner('git add .')
+            self.client.runner('git commit -m "my other file"')
+            self.client.runner('git checkout master')
+        other_path = os.path.join(self.client.client_cache.conan_folder, "hooks", "other",
+                                  "other.py")
+        self.assertFalse(os.path.exists(other_path))
+        self.client.run('config install')
+        check_path = os.path.join(folder, ".git")
+        self._check("git:[%s, -b other_branch]" % check_path)
+        self.assertTrue(os.path.exists(other_path))
diff --git a/conans/test/unittests/client/conf/__init__.py b/conans/test/unittests/client/conf/__init__.py
new file mode 100644
diff --git a/conans/test/unittests/client/conf/config_installer.py b/conans/test/unittests/client/conf/config_installer.py
new file mode 100644
--- /dev/null
+++ b/conans/test/unittests/client/conf/config_installer.py
@@ -0,0 +1,57 @@
+import os
+import unittest
+from parameterized import parameterized
+
+from conans.client.conf.config_installer import _process_config_install_item
+from conans.errors import ConanException
+from conans.test.utils.test_files import temp_folder
+from conans.util.files import save
+
+
+class ConfigInstallerTests(unittest.TestCase):
+
+    def process_config_install_item_test(self):
+        config_type, url_or_path, args = _process_config_install_item(
+                "git:[whaterver.url.com/repo.git, --recusrive --other -b 0.3.4]")
+        self.assertEqual("git", config_type)
+        self.assertEqual("whaterver.url.com/repo.git", url_or_path)
+        self.assertEqual("--recusrive --other -b 0.3.4", args)
+
+        config_type, url_or_path, args = _process_config_install_item("whaterver.url.com/repo.git")
+        self.assertEqual("git", config_type)
+        self.assertEqual("whaterver.url.com/repo.git", url_or_path)
+        self.assertIsNone(args)
+
+        dir_path = temp_folder()
+        for dir_item in ["dir:[%s, None]" % dir_path, dir_path]:
+            config_type, url_or_path, args = _process_config_install_item(dir_item)
+            self.assertEqual("dir", config_type)
+            self.assertEqual(dir_path, url_or_path)
+            self.assertIsNone(args)
+
+        file_path = os.path.join(dir_path, "file.zip")
+        save(file_path, "")
+        for file_item in ["file:[%s, None]" % file_path, file_path]:
+            config_type, url_or_path, args = _process_config_install_item(file_item)
+            self.assertEqual("file", config_type)
+            self.assertEqual(file_path, url_or_path)
+            self.assertIsNone(args)
+
+        for url_item in ["url:[http://is/an/absloute/path with spaces/here/file.zip, None]",
+                         "http://is/an/absloute/path with spaces/here/file.zip"]:
+            config_type, url_or_path, args = _process_config_install_item(url_item)
+            self.assertEqual("url", config_type)
+            self.assertEqual("http://is/an/absloute/path with spaces/here/file.zip", url_or_path)
+            self.assertIsNone(args)
+
+        config_type, url, args = _process_config_install_item(
+                "url:[http://is/an/absloute/path with spaces/here/file.zip, --option]")
+        self.assertEqual("url", config_type)
+        self.assertEqual("http://is/an/absloute/path with spaces/here/file.zip", url)
+        self.assertEqual("--option", args)
+
+        # Test wrong input
+        for item in ["git@github.com:conan-io/conan.git, None"
+                     "file/not/exists.zip"]:
+            with self.assertRaisesRegex(ConanException, "Unable to process config install"):
+                _, _, _ = _process_config_install_item(item)