Repository: conan-io/conan

Issue:
SCM fails in Windows if remote URL is a local path with backslash
if the git clone of the repo is done with forward slash, all is ok, but if done with backslash, as would do automatically if from a python script, the SCM replacement in the conan recipe will have backslash and the conanfile.py will fail to parse



Discussion:

Resolving patch:
diff --git a/conans/client/cmd/export.py b/conans/client/cmd/export.py
--- a/conans/client/cmd/export.py
+++ b/conans/client/cmd/export.py
@@ -81,11 +81,10 @@ def cmd_export(app, conanfile_path, name, version, user, channel, keep_source,
         # FIXME: private access, will be improved when api collaborators are improved
         loader._python_requires._range_resolver.output  # invalidate previous version range output
         conanfile = loader.load_export(conanfile_path, conanfile.name, conanfile.version,
-                                       conanfile.user, conanfile.channel, python_requires)
+                                       ref.user, ref.channel, python_requires)
 
     check_casing_conflict(cache=cache, ref=ref)
     package_layout = cache.package_layout(ref, short_paths=conanfile.short_paths)
-
     if not export:
         metadata = package_layout.load_metadata()
         recipe_revision = metadata.recipe.revision
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
@@ -201,6 +201,8 @@ def get_remote_url(self, remote_name=None, remove_credentials=False):
                 url, _ = url.rsplit(None, 1)
                 if remove_credentials and not os.path.exists(url):  # only if not local
                     url = self._remove_credentials_url(url)
+                if os.path.exists(url):  # Windows local directory
+                    url = url.replace("\\", "/")
                 return url
         return None
 


Test patch:
diff --git a/conans/test/functional/graph_lock/graph_lock_test.py b/conans/test/functional/graph_lock/graph_lock_test.py
--- a/conans/test/functional/graph_lock/graph_lock_test.py
+++ b/conans/test/functional/graph_lock/graph_lock_test.py
@@ -100,7 +100,8 @@ def _check_lock(self, ref_b, rev_b=""):
         self.assertIn("PkgA/0.1@user/channel#fa090239f8ba41ad559f8e934494ee2a:"
                       "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9#0d561e10e25511b9bfa339d06360d7c1",
                       lock_file)
-        self.assertIn('"%s:%s%s"' % (repr(ConanFileReference.loads(ref_b)), self.pkg_b_id, rev_b), lock_file)
+        self.assertIn('"%s:%s%s"' % (repr(ConanFileReference.loads(ref_b)), self.pkg_b_id, rev_b),
+                      lock_file)
 
     def install_info_lock_test(self):
         # Normal install will use it (use install-folder to not change graph-info)
@@ -183,6 +184,125 @@ def export_pkg_test(self):
                          self.modified_pkg_b_package_revision)
 
 
+class GraphLockVersionRangeNoUserChannelTest(unittest.TestCase):
+    # This is exactly the same as above, but not using user/channel in packages
+    # https://github.com/conan-io/conan/issues/5873
+    consumer = GenConanfile().with_name("PkgB").with_version("0.1")\
+                             .with_require_plain("PkgA/[>=0.1]")
+    pkg_b_revision = "afa95143c0c11c46ad57670e1e0a0aa0"
+    pkg_b_id = "5bf1ba84b5ec8663764a406f08a7f9ae5d3d5fb5"
+    pkg_b_package_revision = "#f97ac3d1bee62d55a35085dd42fa847a"
+    modified_pkg_b_revision = "3bb0f77004b0afde1620c714630aa515"
+    modified_pkg_b_package_revision = "#7f92394bbc66cc7f9d403e764b88bac0"
+    graph_lock_command = "install ."
+
+    def setUp(self):
+        client = TestClient()
+        self.client = client
+        client.save({"conanfile.py": GenConanfile().with_name("PkgA").with_version("0.1")})
+        client.run("create .")
+
+        # Use a consumer with a version range
+        client.save({"conanfile.py": str(self.consumer)})
+        client.run(self.graph_lock_command)
+
+        self._check_lock("PkgB/0.1@")
+
+        # If we create a new PkgA version
+        client.save({"conanfile.py": GenConanfile().with_name("PkgA").with_version("0.2")})
+        client.run("create .")
+        client.save({"conanfile.py": str(self.consumer)})
+
+    def _check_lock(self, ref_b, rev_b=""):
+        lock_file = load(os.path.join(self.client.current_folder, LOCKFILE))
+        lock_file_json = json.loads(lock_file)
+        self.assertEqual(2, len(lock_file_json["graph_lock"]["nodes"]))
+        self.assertIn("PkgA/0.1#fa090239f8ba41ad559f8e934494ee2a:"
+                      "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9#0d561e10e25511b9bfa339d06360d7c1",
+                      lock_file)
+        self.assertIn('"%s:%s%s"' % (repr(ConanFileReference.loads(ref_b)), self.pkg_b_id, rev_b),
+                      lock_file)
+
+    def install_info_lock_test(self):
+        # Normal install will use it (use install-folder to not change graph-info)
+        client = self.client
+        client.run("install . -if=tmp")  # Output graph_info to temporary
+        self.assertIn("PkgA/0.2", client.out)
+        self.assertNotIn("PkgA/0.1", client.out)
+
+        # Locked install will use PkgA/0.1
+        # To use the stored graph_info.json, it has to be explicit in "--install-folder"
+        client.run("install . -g=cmake --lockfile")
+        self._check_lock("PkgB/0.1@")
+
+        self.assertIn("PkgA/0.1", client.out)
+        self.assertNotIn("PkgA/0.2", client.out)
+        cmake = client.load("conanbuildinfo.cmake")
+        self.assertIn("PkgA/0.1/_/_", cmake)
+        self.assertNotIn("PkgA/0.2/_/_", cmake)
+
+        # Info also works
+        client.run("info . --lockfile")
+        self.assertIn("PkgA/0.1", client.out)
+        self.assertNotIn("PkgA/0.2", client.out)
+
+    def install_ref_lock_test(self):
+        client = self.client
+        client.run("install PkgA/[>=0.1]@ -if=tmp")
+        self.assertIn("PkgA/0.2: Already installed!", client.out)
+        self.assertNotIn("PkgA/0.1", client.out)
+        # Explicit one
+        client.run("install PkgA/0.1@ --install-folder=.")
+        self.assertIn("PkgA/0.1: Already installed!", client.out)
+        self.assertNotIn("PkgA/0.2", client.out)
+        # Range locked one
+        client.run("install PkgA/[>=0.1]@ --lockfile")
+        self.assertIn("PkgA/0.1: Already installed!", client.out)
+        self.assertNotIn("PkgA/0.2", client.out)
+
+    def export_lock_test(self):
+        # locking a version range at export
+        self.client.run("export . --lockfile")
+        self._check_lock("PkgB/0.1#%s" % self.pkg_b_revision)
+
+    def create_lock_test(self):
+        # Create is also possible
+        client = self.client
+        client.run("create . PkgB/0.1@ --lockfile")
+        self.assertIn("PkgA/0.1", client.out)
+        self.assertNotIn("PkgA/0.2", client.out)
+        self._check_lock("PkgB/0.1#%s" % self.pkg_b_revision, self.pkg_b_package_revision)
+
+    def create_test_lock_test(self):
+        # Create is also possible
+        client = self.client
+        test_conanfile = textwrap.dedent("""
+            from conans import ConanFile
+            class Test(ConanFile):
+                def test(self):
+                    pass
+            """)
+        client.save({"conanfile.py": str(self.consumer),
+                     "test_package/conanfile.py": test_conanfile})
+        client.run("create . PkgB/0.1@ --lockfile")
+        self.assertIn("PkgA/0.1", client.out)
+        self.assertNotIn("PkgA/0.2", client.out)
+        self._check_lock("PkgB/0.1#%s" % self.pkg_b_revision,
+                         self.pkg_b_package_revision)
+
+    def export_pkg_test(self):
+        client = self.client
+        client.run("export-pkg . PkgB/0.1@ --lockfile")
+        self._check_lock("PkgB/0.1#%s" % self.pkg_b_revision,
+                         self.pkg_b_package_revision)
+
+        # Same, but modifying also PkgB Recipe
+        client.save({"conanfile.py": str(self.consumer) + "\n#comment"})
+        client.run("export-pkg . PkgB/0.1@ --lockfile --force")
+        self._check_lock("PkgB/0.1@#%s" % self.modified_pkg_b_revision,
+                         self.modified_pkg_b_package_revision)
+
+
 class GraphLockBuildRequireVersionRangeTest(GraphLockVersionRangeTest):
     consumer = GenConanfile().with_name("PkgB").with_version("0.1")\
                              .with_build_require_plain("PkgA/[>=0.1]@user/channel")
diff --git a/conans/test/functional/scm/scm_test.py b/conans/test/functional/scm/scm_test.py
--- a/conans/test/functional/scm/scm_test.py
+++ b/conans/test/functional/scm/scm_test.py
@@ -115,7 +115,7 @@ def test_auto_filesystem_remote_git(self):
             self.client.run_command('git config user.name "Your Name"')
             self.client.run_command("git add .")
             self.client.run_command('git commit -m  "commiting"')
-        self.client.run_command('git clone "%s" .' % repo.replace('\\', '/'))
+        self.client.run_command('git clone "%s" .' % repo)
         self.client.run("export . user/channel")
         self.assertIn("WARN: Repo origin looks like a local path", self.client.out)
         os.remove(self.client.cache.package_layout(self.ref).scm_folder())