Repository: conan-io/conan
Pull request #11407 (2022-06-06 21:06:39)

Add relative path support to Git SCM tool
Changelog: Feature: Add relative path support to Git SCM tool (feature #11408)

When trying to setup Conan packaging with the target source in a submodule relative to the recipe, I was receiving
empty commit ids. Upon further investigation, it seems that the current assumption is that the folder path to Git()
will be absolute. A minor adjustment allows relative paths to be used as well: since the git command always `chdir`s to the
target git folder, we can perform the `rev-parse` relative to the current directory and receive the expected behavior.

- Test cases have been added.
- Documentation does not seem to warrant updates since this behavior is implied by the currently document default value for `folder`

<!--
- [x] Explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
-->
<sup>**Note:** By default this PR will skip the slower tests and will use a limited set of python versions. Check [here](https://github.com/conan-io/conan/blob/develop/.github/PR_INCREASE_TESTING.md) how to increase the testing level by writing some tags in the current PR body text.</sup>



Patch:
diff --git a/conan/tools/apple/xcodedeps.py b/conan/tools/apple/xcodedeps.py
index 6011e030b..2cc64fbe6 100644
--- a/conan/tools/apple/xcodedeps.py
+++ b/conan/tools/apple/xcodedeps.py
@@ -245,7 +245,9 @@ class XcodeDeps(object):
         # Generate the config files for each component with name conan_pkgname_compname.xcconfig
         # If a package has no components the name is conan_pkgname_pkgname.xcconfig
         # Then all components are included in the conan_pkgname.xcconfig file
-        for dep in self._conanfile.dependencies.host.values():
+        host_req = self._conanfile.dependencies.host
+        test_req = self._conanfile.dependencies.test
+        for dep in list(host_req.values()) + list(test_req.values()):
             dep_name = _format_name(dep.ref.name)
 
             include_components_names = []
diff --git a/conan/tools/cmake/presets.py b/conan/tools/cmake/presets.py
index 81461199d..ca5bf0d30 100644
--- a/conan/tools/cmake/presets.py
+++ b/conan/tools/cmake/presets.py
@@ -95,18 +95,15 @@ def write_cmake_presets(conanfile, toolchain_file, generator):
         # We append the new configuration making sure that we don't overwrite it
         data = json.loads(load(preset_path))
         if multiconfig:
-            build_presets = data["buildPresets"]
-            build_preset_name = _build_preset_name(conanfile)
-            already_exist = any([b["configuration"]
-                                 for b in build_presets if b == build_preset_name])
+            new_build_preset_name = _build_preset_name(conanfile)
+            already_exist = any([b["name"] for b in data["buildPresets"]
+                                 if b["name"] == new_build_preset_name])
             if not already_exist:
                 data["buildPresets"].append(_add_build_preset(conanfile, multiconfig))
         else:
-            configure_presets = data["configurePresets"]
-            configure_preset_name = _configure_preset_name(conanfile, multiconfig)
-            already_exist = any([c["name"]
-                                 for c in configure_presets
-                                 if c["name"] == configure_preset_name])
+            new_configure_preset_name = _configure_preset_name(conanfile, multiconfig)
+            already_exist = any([c["name"] for c in data["configurePresets"]
+                                 if c["name"] == new_configure_preset_name])
             if not already_exist:
                 conf_preset = _add_configure_preset(conanfile, generator, cache_variables,
                                                     toolchain_file, multiconfig)
@@ -126,6 +123,8 @@ def write_cmake_presets(conanfile, toolchain_file, generator):
                 data = {"version": 4, "include": [preset_path]}
             else:
                 data = json.loads(load(user_presets_path))
+                # Clear the folders that have been deleted
+                data["include"] = [i for i in data["include"] if os.path.exists(i)]
                 if preset_path not in data["include"]:
                     data["include"].append(preset_path)
 
diff --git a/conan/tools/scm/git.py b/conan/tools/scm/git.py
index 01a90764b..9cd8041b9 100644
--- a/conan/tools/scm/git.py
+++ b/conan/tools/scm/git.py
@@ -22,7 +22,7 @@ class Git(object):
             # --full-history is needed to not avoid wrong commits:
             # https://github.com/conan-io/conan/issues/10971
             # https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt-Defaultmode
-            commit = self._run('rev-list HEAD -n 1 --full-history -- "{}"'.format(self.folder))
+            commit = self._run('rev-list HEAD -n 1 --full-history -- "."')
             return commit
         except Exception as e:
             raise ConanException("Unable to get git commit in '%s': %s" % (self.folder, str(e)))


Test patch:
diff --git a/conans/test/functional/toolchains/apple/test_xcodedeps_components.py b/conans/test/functional/toolchains/apple/test_xcodedeps_components.py
index fb3458831..397b3d1fb 100644
--- a/conans/test/functional/toolchains/apple/test_xcodedeps_components.py
+++ b/conans/test/functional/toolchains/apple/test_xcodedeps_components.py
@@ -202,3 +202,28 @@ def test_xcodedeps_components():
     assert "tcp/1.0: Hello World Release!" in client.out
     assert "client/1.0: Hello World Release!" in client.out
     assert "chat/1.0: Hello World Release!" in client.out
+
+
+@pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS")
+@pytest.mark.tool_cmake
+def test_xcodedeps_test_require():
+    client = TestClient()
+    client.run("new gtest/1.0 -m cmake_lib")
+    # client.run("new cmake_lib -d name=app -d version=1.0")
+    client.run("create . -tf=None")
+
+    # Create library having build and test requires
+    conanfile = textwrap.dedent(r'''
+        from conan import ConanFile
+        class HelloLib(ConanFile):
+            settings = "os", "compiler", "build_type", "arch"
+            def build_requirements(self):
+                self.test_requires('gtest/1.0')
+        ''')
+    client.save({"conanfile.py": conanfile}, clean_first=True)
+    client.run("install . -g XcodeDeps")
+    assert os.path.isfile(os.path.join(client.current_folder, "conan_gtest.xcconfig"))
+    assert os.path.isfile(os.path.join(client.current_folder, "conan_gtest_gtest.xcconfig"))
+    assert os.path.isfile(os.path.join(client.current_folder, "conan_gtest_gtest_release_x86_64.xcconfig"))
+    assert os.path.isfile(os.path.join(client.current_folder, "conan_gtest_gtest_vars_release_x86_64.xcconfig"))
+    assert '#include "conan_gtest.xcconfig"' in client.load("conandeps.xcconfig")
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
index 8b1311a59..4f6e67e30 100644
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
@@ -12,7 +12,7 @@ from conans.model.ref import ConanFileReference
 from conans.test.assets.cmake import gen_cmakelists
 from conans.test.assets.genconanfile import GenConanfile
 from conans.test.utils.tools import TestClient, TurboTestClient
-from conans.util.files import save, load
+from conans.util.files import save, load, rmdir
 
 
 @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows")
@@ -622,6 +622,56 @@ def test_cmake_presets_multiple_settings_single_config():
         assert "__cplusplus2020" in client.out
 
 
+@pytest.mark.parametrize("multiconfig", [True, False])
+def test_cmake_presets_duplicated_install(multiconfig):
+    # https://github.com/conan-io/conan/issues/11409
+    """Only failed when using a multiconfig generator"""
+    client = TestClient(path_with_spaces=False)
+    client.run("new hello/0.1 --template=cmake_exe")
+    settings = '-s compiler=gcc -s compiler.version=5 -s compiler.libcxx=libstdc++11 ' \
+               '-c tools.cmake.cmake_layout:build_folder_vars=' \
+               '\'["settings.compiler", "settings.compiler.version"]\' '
+    if multiconfig:
+        settings += '-c tools.cmake.cmaketoolchain:generator="Multi-Config"'
+    client.run("install . {}".format(settings))
+    client.run("install . {}".format(settings))
+    presets_path = os.path.join(client.current_folder, "build", "gcc-5", "generators",
+                                "CMakePresets.json")
+    assert os.path.exists(presets_path)
+    contents = json.loads(load(presets_path))
+    assert len(contents["buildPresets"]) == 1
+
+
+def test_remove_missing_presets():
+    # https://github.com/conan-io/conan/issues/11413
+    client = TestClient(path_with_spaces=False)
+    client.run("new hello/0.1 --template=cmake_exe")
+    settings = '-s compiler=gcc -s compiler.version=5 -s compiler.libcxx=libstdc++11 ' \
+               '-c tools.cmake.cmake_layout:build_folder_vars=' \
+               '\'["settings.compiler", "settings.compiler.version"]\' '
+    client.run("install . {}".format(settings))
+    client.run("install . {} -s compiler.version=6".format(settings))
+
+    presets_path_5 = os.path.join(client.current_folder, "build", "gcc-5")
+    assert os.path.exists(presets_path_5)
+
+    presets_path_6 = os.path.join(client.current_folder, "build", "gcc-6")
+    assert os.path.exists(presets_path_6)
+
+    rmdir(presets_path_5)
+
+    # If we generate another configuration, the missing one (removed) for gcc-5 is not included
+    client.run("install . {} -s compiler.version=11".format(settings))
+
+    user_presets_path = os.path.join(client.current_folder, "CMakeUserPresets.json")
+    assert os.path.exists(user_presets_path)
+
+    contents = json.loads(load(user_presets_path))
+    assert len(contents["include"]) == 2
+    assert "gcc-6" in contents["include"][0]
+    assert "gcc-11" in contents["include"][1]
+
+
 @pytest.mark.tool_cmake(version="3.23")
 def test_cmake_presets_options_single_config():
     client = TestClient(path_with_spaces=False)
diff --git a/conans/test/unittests/tools/scm/test_git_get_commit.py b/conans/test/unittests/tools/scm/test_git_get_commit.py
index be652adc1..4ee7f984f 100644
--- a/conans/test/unittests/tools/scm/test_git_get_commit.py
+++ b/conans/test/unittests/tools/scm/test_git_get_commit.py
@@ -1,6 +1,7 @@
 import os
 
 from conan.tools.scm import Git
+from conans.tools import chdir
 from conans.test.utils.mocks import MockConanfile
 from conans.test.utils.tools import TestClient
 
@@ -97,3 +98,109 @@ def test_multi_folder_repo():
     c.run_command("git rev-parse HEAD")
     commit_real = str(c.out).splitlines()[0]
     assert new_commit_root == commit_real
+
+def test_relative_folder_repo():
+    c = TestClient()
+    conanfile = MockConanfile({})
+    c.save({"lib_a/conanfile.py": ""})
+    c.run_command("git init .")
+    c.run_command('git config user.name myname')
+    c.run_command('git config user.email myname@mycompany.com')
+    c.run_command("git add .")
+    c.run_command('git commit -m "lib_a commit"')
+    c.save({"lib_b/conanfile.py": ""})
+    c.run_command("git add .")
+    c.run_command('git commit -m "lib_b commit"')
+    c.save({"lib_c/conanfile.py": ""})
+    c.run_command("git add .")
+    c.run_command('git commit -m "lib_c commit"')
+    c.save({"root_change": ""})
+    c.run_command("git add .")
+    c.run_command('git commit -m "root change"')
+
+    # Relative paths for folders, from the current_folder
+    with chdir(c.current_folder):
+        git = Git(conanfile, folder="lib_a")
+        commit_libA = git.get_commit()
+
+        git = Git(conanfile, folder="lib_b")
+        commit_libB = git.get_commit()
+
+        git = Git(conanfile, folder="./lib_c")
+        commit_libC = git.get_commit()
+
+        # this is folder default, but be explicit
+        git = Git(conanfile, folder=".")
+        commit_root = git.get_commit()
+
+    # All different
+    assert len({commit_libA, commit_libB, commit_libC, commit_root}) == 4
+
+    # Compare to Full paths
+    git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_a"))
+    full_commit_libA = git.get_commit()
+
+    git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_b"))
+    full_commit_libB = git.get_commit()
+
+    git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_c"))
+    full_commit_libC = git.get_commit()
+
+    git = Git(conanfile, folder=c.current_folder)
+    full_commit_root = git.get_commit()
+
+    assert full_commit_libA == commit_libA
+    assert full_commit_libB == commit_libB
+    assert full_commit_libC == commit_libC
+    assert full_commit_root == commit_root
+
+    # Sanity checks
+    c.run_command("git rev-parse HEAD")
+    commit_real_root = str(c.out).splitlines()[0]
+    assert commit_real_root == commit_root
+
+    c.run_command("git rev-list -n 1 --full-history HEAD -- lib_a")
+    commit_real_libA = str(c.out).splitlines()[0]
+    assert commit_real_libA == commit_libA
+
+def test_submodule_repo():
+    c = TestClient()
+    conanfile = MockConanfile({})
+    c.save({"conanfile.py": ""})
+    c.run_command("git init .")
+    c.run_command('git config user.name myname')
+    c.run_command('git config user.email myname@mycompany.com')
+    c.run_command("git add .")
+    c.run_command('git commit -m "Initial commit"')
+    c.run_command('git clone . source_subfolder')
+    c.run_command('git submodule add ../ source_subfolder')
+    c.run_command('git commit -m "submodule commit"')
+    c.save({"root_change": ""})
+    c.run_command("git add .")
+    c.run_command('git commit -m "root change"')
+
+    with chdir(c.current_folder):
+        # default case
+        git = Git(conanfile)
+        commit_root = git.get_commit()
+
+        # Relative paths
+        git = Git(conanfile, folder="source_subfolder")
+        commit_relA = git.get_commit()
+
+        git = Git(conanfile, folder="./source_subfolder")
+        commit_relB = git.get_commit()
+
+        # Full path
+        git = Git(conanfile, folder=os.path.join(c.current_folder, "source_subfolder"))
+        commit_full = git.get_commit()
+
+    assert commit_relA == commit_relB
+    assert commit_relA == commit_full
+    assert commit_root != commit_full
+
+    # This is the commit which modified the tree in the containing repo
+    # not the commit which the submodule is at
+    c.run_command("git rev-list HEAD -n 1 --full-history -- source_subfolder")
+    commit_submodule = str(c.out).splitlines()[0]
+    assert commit_submodule != commit_full