Repository: conan-io/conan

Issue:
[feature] Store more metadata in cache folders
### What is your suggestion?

For example, as far as I know there is no way to determine the reference that a folder in the cache belongs to. This is information that is known to Conan when the folder is created, so why is it not stored there? It is possible to navigate from ref to path using `conan cache path` but not the other way around. This could be helpful to determine the full reference for broken packages when dealing with compiler errors. So I would suggest serializing such information into a machine readable format and put it into the cache folders.

### Have you read the CONTRIBUTING guide?

- [x] I've read the CONTRIBUTING guide


Discussion:
Hi @jasal82 

Thanks for the feedback.
We need to check where to put that info, because it is not possible to put those files inside many folders, as that would affect the package contents. It might also have some maintenance challenges, like needing to remove it in some situations? We need to check that.

It was already discussed that at least the ``conan cache ref <path>`` (it returns the ref based on the path) would be useful, and I think at least that can be added, so I am marking this as a feature.
@memsharded Thanks, I guess a `conan cache ref <path>` command would definitely make sense and solve some problems. However, I would still pursue the solution of making the cache navigable in both directions purely at file system level, so that you don't have to rely on a tool to retrieve information. This could also speed up Conan commands because it would serve as a cache (data structure) that you can read back.
https://github.com/conan-io/conan/pull/18518 PR is proposing a new ``conan cache ref <path>`` command that can return an artifact reference based on a specific folder.

We have discussed in the team, and there is unanimous opinion to not add any extra files in the Conan cache with that information, as that would add some maintenance burden of this metadata (it is already stored in the DB). Every Conan command has full output of the export folder, the source folder, the build folder, the package folder being used in the command, so navigating to those folders after a command is the recommended way, rather than navigating the raw cache filesystem



Patch:
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index 621f5aa6064..d65bbcd188b 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -259,6 +259,14 @@ def get_backup_sources(self, package_list=None, exclude=True, only_upload=True):
         download_cache = DownloadCache(download_cache_path)
         return download_cache.get_backup_sources_files(excluded_urls, package_list, only_upload)
 
+    def path_to_ref(self, path):
+        cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf)
+        result = cache.path_to_ref(path)
+        if result is None:
+            base, folder = os.path.split(path)
+            result = cache.path_to_ref(base)
+        return result
+
 
 def _resolve_latest_ref(cache, ref):
     if ref.revision is None or ref.revision == "latest":
diff --git a/conan/cli/commands/cache.py b/conan/cli/commands/cache.py
index 52abdb58b9f..975ff02b3c3 100644
--- a/conan/cli/commands/cache.py
+++ b/conan/cli/commands/cache.py
@@ -64,6 +64,19 @@ def cache_path(conan_api: ConanAPI, parser, subparser, *args):
     return path
 
 
+@conan_subcommand(formatters={"text": cli_out_write})
+def cache_ref(conan_api: ConanAPI, parser, subparser, *args):
+    """
+    Show the reference for a given Conan cache folder
+    """
+    subparser.add_argument("path", help="Path to a Conan cache folder")
+    args = parser.parse_args(*args)
+    ref = conan_api.cache.path_to_ref(args.path)
+    if ref is None:
+        raise ConanException("Reference for this path not found in cache")
+    return ref.repr_notime()
+
+
 @conan_subcommand()
 def cache_clean(conan_api: ConanAPI, parser, subparser, *args):
     """
diff --git a/conan/internal/cache/cache.py b/conan/internal/cache/cache.py
index fca75a69e2e..c21d4db4134 100644
--- a/conan/internal/cache/cache.py
+++ b/conan/internal/cache/cache.py
@@ -277,3 +277,11 @@ def get_package_lru(self, pref):
 
     def update_package_lru(self, pref):
         self._db.update_package_lru(pref)
+
+    def path_to_ref(self, path):
+        try:
+            path = os.path.relpath(path, self._base_folder)
+            path = path.replace("\\", "/")  # Uniform for Windows and Linux
+        except ValueError:
+            raise ConanException(f"Invalid path: {path}")
+        return self._db.path_to_ref(path)
diff --git a/conan/internal/cache/db/cache_database.py b/conan/internal/cache/db/cache_database.py
index 7baac7d2f7c..e18468950c5 100644
--- a/conan/internal/cache/db/cache_database.py
+++ b/conan/internal/cache/db/cache_database.py
@@ -99,3 +99,9 @@ def get_package_revisions_references(self, pref: PkgReference, only_latest_prev=
     def get_package_references(self, ref: RecipeReference, only_latest_prev=True):
         return [d["pref"]
                 for d in self._packages.get_package_references(ref, only_latest_prev)]
+
+    def path_to_ref(self, path):
+        ref = self._recipes.path_to_ref(path)
+        if ref is not None:
+            return ref
+        return self._packages.path_to_ref(path)
diff --git a/conan/internal/cache/db/packages_table.py b/conan/internal/cache/db/packages_table.py
index 4b583e418c0..5523c65bc30 100644
--- a/conan/internal/cache/db/packages_table.py
+++ b/conan/internal/cache/db/packages_table.py
@@ -240,3 +240,16 @@ def get_package_references_with_build_id_match(self, ref: RecipeReference, build
             if row:
                 return self._as_dict(self.row_type(*row))
             return None
+
+    def path_to_ref(self, path):
+        query = f'SELECT * FROM {self.table_name} ' \
+                f"WHERE {self.columns.path}='{path}'"
+        with self.db_connection() as conn:
+            r = conn.execute(query)
+            row = r.fetchone()
+            if not row:
+                return None
+            ref = RecipeReference.loads(row[0])
+            ref.revision = row[1]
+            pref = PkgReference(ref, row[2], row[3], row[5])
+            return pref
diff --git a/conan/internal/cache/db/recipes_table.py b/conan/internal/cache/db/recipes_table.py
index 6ceb662c485..c9030c96024 100644
--- a/conan/internal/cache/db/recipes_table.py
+++ b/conan/internal/cache/db/recipes_table.py
@@ -127,3 +127,16 @@ def get_recipe_revisions_references(self, ref: RecipeReference):
             r = conn.execute(query)
             ret = [self._as_dict(self.row_type(*row))["ref"] for row in r.fetchall()]
         return ret
+
+    def path_to_ref(self, path):
+        query = f'SELECT * FROM {self.table_name} ' \
+                f"WHERE {self.columns.path}='{path}'"
+        with self.db_connection() as conn:
+            r = conn.execute(query)
+            row = r.fetchone()
+            if not row:
+                return None
+            ref = RecipeReference.loads(row[0])
+            ref.revision = row[1]
+            ref.timestamp = row[3]
+            return ref


Test patch:
diff --git a/test/integration/command/cache/test_cache_path.py b/test/integration/command/cache/test_cache_path.py
index d95c145b47b..f7f5294966b 100644
--- a/test/integration/command/cache/test_cache_path.py
+++ b/test/integration/command/cache/test_cache_path.py
@@ -1,15 +1,17 @@
 import json
 import os
+import platform
 
 import pytest
 
+from conan.api.model import RecipeReference, PkgReference
 from conan.test.assets.genconanfile import GenConanfile
 from conan.test.utils.tools import TestClient
 
 
 @pytest.fixture(scope="module")
 def created_package():
-    t = TestClient()
+    t = TestClient(light=True)
     t.save({"conanfile.py": GenConanfile()})
     t.run("create . --name foo --version 1.0")
     recipe_layout = t.exported_layout()
@@ -115,6 +117,7 @@ def test_cache_path_does_not_exist_folder():
     client.run(f"cache path {pref} --folder build", assert_error=True)
     assert f"ERROR: 'build' folder does not exist for the reference {pref}" in client.out
 
+
 def test_cache_path_output_json():
     client = TestClient()
     conanfile = GenConanfile("mypkg", "0.1")
@@ -124,3 +127,60 @@ def test_cache_path_output_json():
     client.run("cache path mypkg/0.1 --format=json")
     output = json.loads(client.stdout)
     assert output == {"cache_path": os.path.join(layout.base_folder, "e")}
+
+
+class TestCacheRef:
+
+    def test_cache_path(self, created_package):
+        t, recipe_layout, pkg_layout = created_package
+
+        t.run(f'cache ref "{os.path.dirname(recipe_layout.export())}"')
+        assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out
+
+        t.run(f'cache ref "{recipe_layout.export()}"')
+        assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out
+
+        t.run(f'cache ref "{recipe_layout.source()}"')
+        assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out
+
+        t.run(f'cache ref "{os.path.dirname(pkg_layout.package())}"')
+        assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out
+
+        t.run(f'cache ref "{pkg_layout.build()}"')
+        assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out
+
+        t.run(f'cache ref "{pkg_layout.package()}"')
+        assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out
+
+    def test_downloaded_paths(self):
+        t = TestClient(light=True, default_server_user=True)
+        t.save({"conanfile.py": GenConanfile()})
+        t.run("create . --name foo --version 1.0")
+        t.run("upload * -c -r=default")
+        t.run("remove * -c")
+        t.run("install --requires=foo/1.0")
+        ref = RecipeReference.loads("foo/1.0#4d670581ccb765839f2239cc8dff8fbd")
+        pref = PkgReference(ref, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
+        recipe_layout = t.get_latest_ref_layout(ref)
+        pkg_layout = t.get_latest_pkg_layout(pref)
+
+        t.run(f'cache ref "{os.path.dirname(recipe_layout.export())}"')
+        assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out
+
+        t.run(f'cache ref "{recipe_layout.export()}"')
+        assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out
+
+        t.run(f'cache ref "{os.path.dirname(pkg_layout.package())}"')
+        assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out
+
+        t.run(f'cache ref "{pkg_layout.package()}"')
+        assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out
+
+    def test_errors(self):
+        t = TestClient(light=True)
+        t.run('cache ref "/some/non/existing/path"', assert_error=True)
+        assert "ERROR: Reference for this path not found in cache" in t.out
+
+        if platform.system() == "Windows":
+            t.run("cache ref J:/not/path", assert_error=True)
+            assert "ERROR: Invalid path: J:/not/path" in t.out