Repository: iterative/dvc

Issue:
status: ignoring cache that is not in local cache nor in remote
`dvc status -c` should probably exit with non-zero code if some files are not presented neither in cache, neither in remote.

It is possible that some files are not presented neither in local cache, neither in remote. In this case `dvc status -c` will print out warning and exit with zero code:

```
$dvc status -c
WARNING: Some of the cache files do not exist neither locally nor on remote. Missing cache files:
name: data.csv, md5: 746308829575e17c3331bbcb00c0898b
Data and pipelines are up to date.
```

It may happen if Alice executes `dvc add data.csv`, `git add data.csv`, `git commit` and `git push` but doesn't execute `dvc push` and Bob executes `dvc status -c data.csv` on another machine.

There should be a way in such case to exit with non-zero from `dvc status` command. It will allow to handle such errors in different scripts.

Context: https://discordapp.com/channels/485586884165107732/563406153334128681/743159360745898044



Discussion:
`dvc status -c` shouldn't exit with non-zero code by default, because missing cache is just part of the normal status report. It should exit with non-zero in case `-q` is specified (there is already logic for that), but the problem is that we ignore STATUS_MISSING in https://github.com/iterative/dvc/blob/64f038fda83c2400263b335fca6bb4f63f6ecf0f/dvc/repo/status.py#L96 . So the first step here is to just stop ignoring it there and include it into the report. This is pretty simple and just requires adjusting the lines I've linked above.

After that we might consider introducing a special option that will tell `status` to error-out if it is not up-to-date.
 Thanks, @efiop ! I'll create PR with fix as soon as possible.
Reopening per https://github.com/iterative/dvc/pull/4398#issuecomment-678744603, just a small detail:

> > If missing is now an expected status, the WARNING is probably no longer needed
> I think I can achieve it by adding log_missing=True param into dvc.data_cloud.DataCloud.status...

Resolving patch:
diff --git a/dvc/cache/local.py b/dvc/cache/local.py
--- a/dvc/cache/local.py
+++ b/dvc/cache/local.py
@@ -114,6 +114,7 @@ def status(
         jobs=None,
         show_checksums=False,
         download=False,
+        log_missing=True,
     ):
         # Return flattened dict containing all status info
         dir_status, file_status, _ = self._status(
@@ -122,6 +123,7 @@ def status(
             jobs=jobs,
             show_checksums=show_checksums,
             download=download,
+            log_missing=log_missing,
         )
         return dict(dir_status, **file_status)
 
@@ -132,6 +134,7 @@ def _status(
         jobs=None,
         show_checksums=False,
         download=False,
+        log_missing=True,
     ):
         """Return a tuple of (dir_status_info, file_status_info, dir_contents).
 
@@ -172,11 +175,20 @@ def _status(
                     )
                 )
         return self._make_status(
-            named_cache, show_checksums, local_exists, remote_exists
+            named_cache,
+            show_checksums,
+            local_exists,
+            remote_exists,
+            log_missing,
         )
 
     def _make_status(
-        self, named_cache, show_checksums, local_exists, remote_exists
+        self,
+        named_cache,
+        show_checksums,
+        local_exists,
+        remote_exists,
+        log_missing,
     ):
         def make_names(hash_, names):
             return {"name": hash_ if show_checksums else " ".join(names)}
@@ -199,7 +211,8 @@ def make_names(hash_, names):
         self._fill_statuses(dir_status, local_exists, remote_exists)
         self._fill_statuses(file_status, local_exists, remote_exists)
 
-        self._log_missing_caches(dict(dir_status, **file_status))
+        if log_missing:
+            self._log_missing_caches(dict(dir_status, **file_status))
 
         return dir_status, file_status, dir_contents
 
diff --git a/dvc/data_cloud.py b/dvc/data_cloud.py
--- a/dvc/data_cloud.py
+++ b/dvc/data_cloud.py
@@ -100,7 +100,14 @@ def _save_pulled_checksums(self, cache):
                 # (see `RemoteBASE.download()`)
                 self.repo.state.save(cache_file, checksum)
 
-    def status(self, cache, jobs=None, remote=None, show_checksums=False):
+    def status(
+        self,
+        cache,
+        jobs=None,
+        remote=None,
+        show_checksums=False,
+        log_missing=True,
+    ):
         """Check status of data items in a cloud-agnostic way.
 
         Args:
@@ -111,8 +118,14 @@ def status(self, cache, jobs=None, remote=None, show_checksums=False):
                 is used.
             show_checksums (bool): show checksums instead of file names in
                 information messages.
+            log_missing (bool): log warning messages if file doesn't exist
+                neither in cache, neither in cloud.
         """
         remote = self.get_remote(remote, "status")
         return self.repo.cache.local.status(
-            cache, jobs=jobs, remote=remote, show_checksums=show_checksums
+            cache,
+            jobs=jobs,
+            remote=remote,
+            show_checksums=show_checksums,
+            log_missing=log_missing,
         )
diff --git a/dvc/repo/status.py b/dvc/repo/status.py
--- a/dvc/repo/status.py
+++ b/dvc/repo/status.py
@@ -53,6 +53,7 @@ def _cloud_status(
 
     - new: Remote doesn't have the file
     - deleted: File is no longer in the local cache
+    - missing: File doesn't exist neither in the cache, neither in remote
 
     Example:
             Given the following commands:
@@ -90,7 +91,9 @@ def _cloud_status(
     )
 
     ret = {}
-    status_info = self.cloud.status(used, jobs, remote=remote)
+    status_info = self.cloud.status(
+        used, jobs, remote=remote, log_missing=False
+    )
     for info in status_info.values():
         name = info["name"]
         status_ = info["status"]


Test patch:
diff --git a/tests/func/test_data_cloud.py b/tests/func/test_data_cloud.py
--- a/tests/func/test_data_cloud.py
+++ b/tests/func/test_data_cloud.py
@@ -248,10 +248,10 @@ def test_missing_cache(tmp_dir, dvc, local_remote, caplog):
     assert bar in caplog.text
 
     caplog.clear()
-    dvc.status(cloud=True)
-    assert header in caplog.text
-    assert foo in caplog.text
-    assert bar in caplog.text
+    assert dvc.status(cloud=True) == {"bar": "missing", "foo": "missing"}
+    assert header not in caplog.text
+    assert foo not in caplog.text
+    assert bar not in caplog.text
 
 
 def test_verify_hashes(