Repository: iterative/dvc

Issue:
dvc pull: does not warn about missing versions in a versioned S3
# Bug Report

<!--
## dvc pull: does not warn about missing versions in a versioned S3

-->

## Description

If not all versions where pushed to a versioned S3, then `dvc pull` in an older commit, or in a cloned repository pulls the wrong version **with a mismatched hash** without any warnings. 

### Reproduce

1. Change data, add to dvc, push to a **versioned S3 remote**, commit the updated `.dvc` to git (`.dvc` contains ETag and Version ID).
2. Change the same data, add to dvc, **do not** push to the dvc remote, but still commit the updated `.dvc` to git (`.dvc` does not contain ETag, nor version ID). This creates a commit with a missing version on a versioned S3.
3. Change the same data, add to dvc, commit the updated `.dvc` to git, ﻿do a `dvc push` (`dvc push -a` does not work due to #8427)
4. Clear local cache and data.
5. Checkout the commit created in (2).
6. `dvc pull` pulls the data, but hashes mismatch and dvc does not throw any warnings. `dvc status` also says that everything is up-to-date.

### Expected

An error that the data is not present in remote S3, or a warning that the hash in `.dvc` does match the actual hash. 

### Environment information

<!--
This is required to ensure that we can reproduce the bug.
-->

**Output of `dvc doctor`:**

```console
$ dvc doctor
DVC version: 2.51.0 (pip)
-------------------------
Platform: Python 3.11.2 on Linux-5.15.49-linuxkit-aarch64-with-glibc2.37
Subprojects:
        dvc_data = 0.44.1
        dvc_objects = 0.21.1
        dvc_render = 0.3.1
        dvc_task = 0.2.0
        scmrepo = 0.1.15
Supports:
        azure (adlfs = 2023.1.0, knack = 0.10.1, azure-identity = 1.12.0),
        gdrive (pydrive2 = 1.15.1),
        gs (gcsfs = 2023.3.0),
        hdfs (fsspec = 2023.3.0, pyarrow = 11.0.0),
        http (aiohttp = 3.8.4, aiohttp-retry = 2.8.3),
        https (aiohttp = 3.8.4, aiohttp-retry = 2.8.3),
        oss (ossfs = 2021.8.0),
        s3 (s3fs = 2023.3.0, boto3 = 1.24.59),
        ssh (sshfs = 2023.1.0),
        webdav (webdav4 = 0.9.8),
        webdavs (webdav4 = 0.9.8),
        webhdfs (fsspec = 2023.3.0)
```



Discussion:
Hi @nickto , thanks for the detailed report. TLDR: We agree that raising a warning makes sense.

To give some clarity, this is currently the expected behavior as we internally consider `version_id=None` to mean "get me the latest version from the remote" when we are fetching data.

However, I discussed this with @pmrowla and we agree that it might not be the most valuable default behavior from the user's perspective, so raising a warning would be preferable as default. 
For people wanting the "get me the latest" behavior, we would recommend using `import-url` + `update`.

Resolving patch:
diff --git a/dvc/repo/fetch.py b/dvc/repo/fetch.py
--- a/dvc/repo/fetch.py
+++ b/dvc/repo/fetch.py
@@ -1,6 +1,8 @@
 import logging
+from typing import Dict, List, Tuple
 
 from dvc.exceptions import DownloadError
+from dvc_data.index import DataIndex, FileStorage
 
 from . import locked
 
@@ -84,6 +86,7 @@ def fetch(  # noqa: C901, PLR0913
         data = collect(
             indexes, cache_index=self.data_index, cache_key=cache_key, callback=cb
         )
+    failed_count += _log_unversioned(data)
 
     with Callback.as_tqdm_callback(
         desc="Fetching",
@@ -109,3 +112,31 @@ def fetch(  # noqa: C901, PLR0913
         raise DownloadError(failed_count)
 
     return transferred_count
+
+
+def _log_unversioned(data: Dict[Tuple[str, str], "DataIndex"]) -> int:
+    unversioned: List[str] = []
+    for by_fs, fs_index in data.items():
+        remote = fs_index.storage_map[()].remote
+        if not isinstance(remote, FileStorage) or not remote.fs.version_aware:
+            continue
+
+        fs = remote.fs
+        index = DataIndex()
+        index.storage_map = fs_index.storage_map
+        for key, entry in fs_index.iteritems():
+            if entry.meta and not entry.meta.isdir and entry.meta.version_id is None:
+                unversioned.append(fs.unstrip_protocol(fs.path.join(remote.path, *key)))
+            else:
+                index[key] = entry
+        fs_index.close()
+        data[by_fs] = index
+    if unversioned:
+        logger.warning(
+            (
+                "Some files are missing cloud version information and will not be "
+                "fetched from the remote:\n%s"
+            ),
+            "\n".join(unversioned),
+        )
+    return len(unversioned)
diff --git a/dvc/repo/pull.py b/dvc/repo/pull.py
--- a/dvc/repo/pull.py
+++ b/dvc/repo/pull.py
@@ -1,5 +1,6 @@
 import logging
 
+from dvc.exceptions import CheckoutError
 from dvc.repo import locked
 from dvc.utils import glob_targets
 
@@ -38,13 +39,17 @@ def pull(  # noqa: PLR0913
         recursive=recursive,
         run_cache=run_cache,
     )
-    stats = self.checkout(
-        targets=expanded_targets,
-        with_deps=with_deps,
-        force=force,
-        recursive=recursive,
-        allow_missing=allow_missing,
-    )
+    try:
+        stats = self.checkout(
+            targets=expanded_targets,
+            with_deps=with_deps,
+            force=force,
+            recursive=recursive,
+            allow_missing=allow_missing,
+        )
+    except CheckoutError as exc:
+        exc.stats["fetched"] = processed_files_count
+        raise
 
     stats["fetched"] = processed_files_count
     return stats


Test patch: