Repository: iterative/dvc

Issue:
`data:status`: `--granular --with-dirs` returns duplicates for deleted directory
# Bug Report

<!--
## Issue name

Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug. 

Example: `repro: doesn't detect input changes`
-->

## Description

<!--
A clear and concise description of what the bug is.
-->

Working on integrating `dvc data status --show-json --with-dirs --granular --untracked --unchanged` into the extension. Noticed that deleting a tracked directory gives a strange result.

### Reproduce

<!--
Step list of how to reproduce the bug
-->

<!--
Example:

1. dvc init
2. Copy dataset.zip to the directory
3. dvc add dataset.zip
4. dvc run -d dataset.zip -o model ./train.sh
5. modify dataset.zip
6. dvc repro
-->

1. Open `vscode-dvc` demo repository
2. `rm -rf training_metrics` (contents was `training_metrics/scalars/loss.tsv` & `training_metrics/scalars/acc.tsv`)
3. `dvc data status --show-json --with-dirs --granular`

```bash
{
  "uncommitted": {
    "deleted": [
      "training_metrics",
      "training_metrics",
      "training_metrics"
    ]
  }
}
```

### Expected

<!--
A clear and concise description of what you expect to happen.
-->

Expected output would be:

```bash
{
  "uncommitted": {
    "deleted": [
      "training_metrics/",
      "training_metrics/scalars/acc.tsv",
      "training_metrics/scalars/loss.tsv"
    ]
  }
}
```

### Environment information

<!--
This is required to ensure that we can reproduce the bug.
-->

**Output of `dvc doctor`:**

```console
$ dvc doctor
DVC version: 2.15.0 (pip)
---------------------------------
Platform: Python 3.8.9 on macOS-12.2.1-arm64-arm-64bit
Supports:
        webhdfs (fsspec = 2022.5.0),
        http (aiohttp = 3.8.1, aiohttp-retry = 2.5.1),
        https (aiohttp = 3.8.1, aiohttp-retry = 2.5.1),
        s3 (s3fs = 2022.5.0, boto3 = 1.21.21)
Cache types: reflink, hardlink, symlink
Cache directory: apfs on /dev/disk3s1s1
Caches: local
Remotes: https
Workspace directory: apfs on /dev/disk3s1s1
Repo: dvc (subdir), git
```

**Additional Information (if any):**

<!--
Please check https://github.com/iterative/dvc/wiki/Debugging-DVC on ways to gather more information regarding the issue.

If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.
If the issue is regarding the performance, please attach the profiling information and the benchmark comparisons.
-->

Also verified against `2.15.1.dev7+gd0eda1d5`.

The `training_metrics` folder is produced by DVCLive. The nested structure does cause some issues with decorations/SCM in the UI but that is something that we can fix on the VS Code side.



Discussion:
The primary use case of the expected output would be to restore different parts of the deleted directory using checkout.

If this is not a normal/expected use case then I would expect:

```bash
{
  "uncommitted": {
    "deleted": [
      "training_metrics"
    ]
  }
}
``` 

Also happy to not provide this option if it will affect the performance of `data status`. 

LMK 🙏🏻. 
@skshetry Could you take a look, please?

Resolving patch:
diff --git a/dvc/repo/data.py b/dvc/repo/data.py
--- a/dvc/repo/data.py
+++ b/dvc/repo/data.py
@@ -68,13 +68,17 @@ def _granular_diff(
     from dvc_data.diff import diff as odiff
     from dvc_data.objects.tree import Tree
 
+    drop_root = False
+    trees = isinstance(old_obj, Tree) or isinstance(new_obj, Tree)
+    if trees:
+        drop_root = not with_dirs
+
     def path_join(root: str, *paths: str) -> str:
-        if not isinstance(new_obj, Tree):
+        if not trees and paths == ROOT:
             return root
         return os.path.sep.join([root, *paths])
 
     diff_data = odiff(old_obj, new_obj, cache)
-    drop_root = not with_dirs and isinstance(new_obj, Tree)
 
     output: Dict[str, List[str]] = defaultdict(list)
     for state in ("added", "deleted", "modified", "unchanged"):


Test patch: