Repository: conan-io/conan
Pull request: Git.clone change folder to target one
Changelog: Fix: Allow spaces in path in ``Git`` helper.
Docs: https://github.com/conan-io/docs/pull/3271

Close https://github.com/conan-io/conan/issues/14058




Problem statement:
[bug] Git.checkout() with a target folder doesn't work with the remaining Git functions
### Environment details

* Operating System+version: n/a
* Compiler+version: n/a
* Conan version: 2.0.6
* Python version: 3.11


### Steps to reproduce

The `conan.tools.scm.Git` interface seems lacking. If I specify a `target` folder in the `clone()` command, there seems to be no straight-forward way to correctly use the other `Git` functions like `checkout()`. They're all going to be executed in the wrong directory.

```python
    def source(self):
        git = Git(self)
        git.clone(url=self.scm["url"], target=os.path.join(self.source_folder, "src"))
        git.checkout(commit=self.scm["revision"])
```

### Logs

Giving this error:
```
        git.checkout(commit=self.scm["revision"])
        ConanException: Command 'git checkout 42353e2' failed with errorcode '128'
b'fatal: not a git repository (or any parent up to mount point /)\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\n'
```

Hints from issue discussion:
Hi @fschoenm 

Thanks for your report.

The ``Git()`` constructor itself is nothing but a caching of the target folder to use, for convenience, and the folder is a public attribute. So this works:

````python
 def source(self):
        git = Git(self)
        target = os.path.join(self.source_folder, "target")
        git.clone(url="{url}", target=target)
        git.folder = target
        git.checkout(commit="{commit}")
```

I'd say this is not a bug, but intended. I will submit a PR to propose to make this automatic, but probably hiding the detail is not that great, and having explicit the fact that the current folder for "git" commands has changed is better, lets see what the team thinks.
> The `Git()` constructor itself is nothing but a caching of the target folder to use, for convenience, and the folder is a public attribute. So this works:

Actually, I didn't really understand why there are two folders involved, a target folder and a working directory, and what is their relationship. Do you mean it might work to just set the _folder_ and all commands (including `clone()` and `checkout()`) should work? Then I really don't understand what the _target_ is for.
The folder that is defined is the ``cwd`` where the ``git`` command will be executed, no more no less. But when ``git clone`` is called, there can be 2 folders, the ``cwd`` and the ``target`` folder. If more than 1 repo is cloned (not typical, but sometimes happen) there will be 3 folders, one ``cwd`` and 2 ``target`` folders.

For the use case at and, it is also possible (and probably what makes sense) to do:
```python
 def source(self):
      git = Git(self, "target")
      git.clone(url="{url}", target=".")
      git.checkout(commit="{commit}")
```
Note the ``target="."`` clones in the current folder, not creating another extra subfolder

Commit message: not automatic cd folder

Patch to review:
diff --git a/conan/tools/scm/git.py b/conan/tools/scm/git.py
index 60441e09461..a47caa0b905 100644
--- a/conan/tools/scm/git.py
+++ b/conan/tools/scm/git.py
@@ -162,7 +162,8 @@ def clone(self, url, target="", args=None):
             url = url.replace("\\", "/")  # Windows local directory
         mkdir(self.folder)
         self._conanfile.output.info("Cloning git repo")
-        self.run('clone "{}" {} {}'.format(url, " ".join(args), target))
+        target_path = f'"{target}"' if target else ""  # quote in case there are spaces in path
+        self.run('clone "{}" {} {}'.format(url, " ".join(args), target_path))
 
     def fetch_commit(self, url, commit):
         """
diff --git a/conans/test/functional/tools/scm/test_git.py b/conans/test/functional/tools/scm/test_git.py
index a8e5343742f..6ce16a32fe7 100644
--- a/conans/test/functional/tools/scm/test_git.py
+++ b/conans/test/functional/tools/scm/test_git.py
@@ -210,6 +210,49 @@ def test_clone_checkout(self):
         assert c.load("source/src/myfile.h") == "myheader!"
         assert c.load("source/CMakeLists.txt") == "mycmake"
 
+    def test_clone_target(self):
+        # Clone to a different target folder
+        # https://github.com/conan-io/conan/issues/14058
+        conanfile = textwrap.dedent("""
+            import os
+            from conan import ConanFile
+            from conan.tools.scm import Git
+            from conan.tools.files import load
+
+            class Pkg(ConanFile):
+                name = "pkg"
+                version = "0.1"
+
+                def layout(self):
+                    self.folders.source = "source"
+
+                def source(self):
+                    # Alternative, first defining the folder
+                    # git = Git(self, "target")
+                    # git.clone(url="{url}", target=".")
+                    # git.checkout(commit="{commit}")
+
+                    git = Git(self)
+                    git.clone(url="{url}", target="target") # git clone url target
+                    git.folder = "target"                   # cd target
+                    git.checkout(commit="{commit}")         # git checkout commit
+
+                    self.output.info("MYCMAKE: {{}}".format(load(self, "target/CMakeLists.txt")))
+                    self.output.info("MYFILE: {{}}".format(load(self, "target/src/myfile.h")))
+                """)
+        folder = os.path.join(temp_folder(), "myrepo")
+        url, commit = create_local_git_repo(files={"src/myfile.h": "myheader!",
+                                                   "CMakeLists.txt": "mycmake"}, folder=folder)
+        # This second commit will NOT be used, as I will use the above commit in the conanfile
+        save_files(path=folder, files={"src/myfile.h": "my2header2!"})
+        git_add_changes_commit(folder=folder)
+
+        c = TestClient()
+        c.save({"conanfile.py": conanfile.format(url=url, commit=commit)})
+        c.run("create .")
+        assert "pkg/0.1: MYCMAKE: mycmake" in c.out
+        assert "pkg/0.1: MYFILE: myheader!" in c.out
+
 
 @pytest.mark.tool("git")
 class TestGitShallowClone:


Code review comments:
diff_hunk: @@ -210,6 +210,49 @@ def test_clone_checkout(self):
         assert c.load("source/src/myfile.h") == "myheader!"
         assert c.load("source/CMakeLists.txt") == "mycmake"
 
+    def test_clone_target(self):
+        # Clone to a different target folder
+        # https://github.com/conan-io/conan/issues/14058
+        conanfile = textwrap.dedent("""
+            import os
+            from conan import ConanFile
+            from conan.tools.scm import Git
+            from conan.tools.files import load
+
+            class Pkg(ConanFile):
+                name = "pkg"
+                version = "0.1"
+
+                def layout(self):
+                    self.folders.source = "source"
+
+                def source(self):
+                    # Alternative, first defining the folder
+                    # git = Git(self, "target")
+                    # git.clone(url="{url}", target=".")
+                    # git.checkout(commit="{commit}")
+
+                    git = Git(self)
+                    git.clone(url="{url}", target="target") # git clone url target
path: conans/test/functional/tools/scm/test_git.py
text: @user1:
```suggestion
                    git.clone(url="{url}", target="target with spaces") # git clone url target
```
So that the quotes behaviour is also checked for, maybe?

@author:
Great idea, changed it!