Repository: iterative/dvc

Issue:
cloud versioning: Using `dvc commit` to track updates doesn't work as expected 
## Setup

I am ttracking a dictionary with cloud versioning enabled:

```bash
$ dvc remote modify myremote version_aware true
$ dvc remote modify myremote worktree true
$ mkdir data
$ echo foo > data/foo
$ echo bar > data/bar
$ dvc add data
$ dvc push
$ cat data.dvc
outs:                                                                                                             
- md5: 168fd6761b9c3f2c081085b51fd7bf15.dir
  size: 8
  nfiles: 2
  path: data
  files:
  - size: 4
    etag: 08deb086f8d8ccfc021001
    md5: c157a79031e1c40f85931829bc5fc552
    relpath: bar
  - size: 4
    etag: 08a48b8df8d8ccfc021001
    md5: d3b07384d113edec49eaa6238ad5ff00
    relpath: foo
```

And performing an update:

## Working as expected with `dvc add`

If I run `dvc add data`, it works as expected: The .dvc file preserves "cloud-versioning" format and `dvc push` only uploads and creates a new version for the modified file:

```console
$ echo bar2 > data
$ dvc add data
$ cat data.dvc
outs:
- md5: 43f06a6517d71d0267bed23cfb1a53fb.dir
  size: 9
  nfiles: 2
  path: data
  files:
  - size: 5
    md5: 042967ff088f7f436015fbebdcad1f28
    relpath: bar
  - size: 4
    etag: 08a48b8df8d8ccfc021001
    md5: d3b07384d113edec49eaa6238ad5ff00
    relpath: foo
$ dvc push
1 file pushed                                                                                                     
$ cat data.dvc
outs:
- md5: 43f06a6517d71d0267bed23cfb1a53fb.dir
  size: 9
  nfiles: 2
  path: data
  files:
  - size: 5
    md5: 042967ff088f7f436015fbebdcad1f28
    relpath: bar
    etag: 08eca3ebfddaccfc021001
  - size: 4
    etag: 08f0f286fcdaccfc021001
    md5: d3b07384d113edec49eaa6238ad5ff00
    relpath: foo
```

## Unexpected behavior with `dvc commit`

However, If I instead run `dvc commit` to track the updates, first the `.dvc` file loses the "cloud versioning" format and then all files are uploaded and redundant versions are created on the remote:

```console
$ bar3 > data/bar
$ dvc commit
outputs ['data'] of stage: 'data.dvc' changed. Are you sure you want to commit it? [y/n] y 
$ cat data.dvc 
outs:                                                                                                             
- md5: a41477be72fa9617d877592a0b22542a.dir                                                                       
  size: 9
  nfiles: 2
  path: data
$ dvc push
2 files pushed 
$ cat data.dvc
outs:                                                                                                             
- md5: a41477be72fa9617d877592a0b22542a.dir
  size: 9
  nfiles: 2
  path: data
  files:
  - size: 5
    etag: 0898e6edffdaccfc021001
    md5: f19345bd4a711b82a13e55c61cad54e3
    relpath: bar
  - size: 4
    etag: 08bfe9e5ffdaccfc021001
    md5: d3b07384d113edec49eaa6238ad5ff00
    relpath: foo
```




Discussion:
@dberenbaum @pmrowla is this by design? 

Are there any problems with making `merge_versioned` arg of `stage.save` `True` by default (should do nothing if there are not cloud versioning outputs)?
> @dberenbaum @pmrowla is this by design?

No, it's not by design. Good catch.

Resolving patch:
diff --git a/dvc/repo/add.py b/dvc/repo/add.py
--- a/dvc/repo/add.py
+++ b/dvc/repo/add.py
@@ -194,7 +194,7 @@ def add(  # noqa: C901
                 stage.transfer(source, to_remote=to_remote, odb=odb, **kwargs)
             else:
                 try:
-                    stage.save(merge_versioned=True)
+                    stage.save()
                     if not no_commit:
                         stage.commit()
                 except CacheLinkError:
diff --git a/dvc/repo/worktree.py b/dvc/repo/worktree.py
--- a/dvc/repo/worktree.py
+++ b/dvc/repo/worktree.py
@@ -228,7 +228,7 @@ def outs_filter(out: "Output") -> bool:
                 )
                 continue
             _fetch_out_changes(out, local_index, remote_index, remote)
-        stage.save(merge_versioned=True)
+        stage.save()
         for out in stage.outs:
             _update_out_meta(out, remote_index)
         stage.dump(with_files=True, update_pipeline=False)
diff --git a/dvc/stage/__init__.py b/dvc/stage/__init__.py
--- a/dvc/stage/__init__.py
+++ b/dvc/stage/__init__.py
@@ -486,17 +486,11 @@ def compute_md5(self) -> Optional[str]:
         logger.debug("Computed %s md5: '%s'", self, m)
         return m
 
-    def save(
-        self,
-        allow_missing: bool = False,
-        merge_versioned: bool = False,
-        run_cache: bool = True,
-    ):
+    def save(self, allow_missing: bool = False, run_cache: bool = True):
         self.save_deps(allow_missing=allow_missing)
 
-        self.save_outs(
-            allow_missing=allow_missing, merge_versioned=merge_versioned
-        )
+        self.save_outs(allow_missing=allow_missing)
+
         self.md5 = self.compute_md5()
 
         if run_cache:
@@ -512,29 +506,26 @@ def save_deps(self, allow_missing=False):
                 if not allow_missing:
                     raise
 
-    def save_outs(
-        self, allow_missing: bool = False, merge_versioned: bool = False
-    ):
+    def save_outs(self, allow_missing: bool = False):
         from dvc.output import OutputDoesNotExistError
 
         from .exceptions import StageFileDoesNotExistError, StageNotFound
 
-        if merge_versioned:
-            try:
-                old = self.reload()
-                old_outs = {out.def_path: out for out in old.outs}
-                merge_versioned = any(
-                    (
-                        out.files is not None
-                        or (
-                            out.meta is not None
-                            and out.meta.version_id is not None
-                        )
+        try:
+            old = self.reload()
+            old_outs = {out.def_path: out for out in old.outs}
+            merge_versioned = any(
+                (
+                    out.files is not None
+                    or (
+                        out.meta is not None
+                        and out.meta.version_id is not None
                     )
-                    for out in old_outs.values()
                 )
-            except (StageFileDoesNotExistError, StageNotFound):
-                merge_versioned = False
+                for out in old_outs.values()
+            )
+        except (StageFileDoesNotExistError, StageNotFound):
+            merge_versioned = False
 
         for out in self.outs:
             try:


Test patch:
diff --git a/tests/func/test_commit.py b/tests/func/test_commit.py
--- a/tests/func/test_commit.py
+++ b/tests/func/test_commit.py
@@ -245,3 +245,61 @@ def test_imported_entries_unchanged(tmp_dir, dvc, erepo_dir):
     stage = dvc.imp(os.fspath(erepo_dir), "file")
 
     assert stage.changed_entries() == ([], [], None)
+
+
+def test_commit_updates_to_cloud_versioning_dir(tmp_dir, dvc):
+    data_dvc = tmp_dir / "data.dvc"
+    data_dvc.dump(
+        {
+            "outs": [
+                {
+                    "path": "data",
+                    "files": [
+                        {
+                            "size": 3,
+                            "version_id": "WYRG4BglP7pD.gEoJP6a4AqOhl.FRA.h",
+                            "etag": "acbd18db4cc2f85cedef654fccc4a4d8",
+                            "md5": "acbd18db4cc2f85cedef654fccc4a4d8",
+                            "relpath": "bar",
+                        },
+                        {
+                            "size": 3,
+                            "version_id": "0vL53tFVY5vVAoJ4HG2jCS1mEcohDPE0",
+                            "etag": "acbd18db4cc2f85cedef654fccc4a4d8",
+                            "md5": "acbd18db4cc2f85cedef654fccc4a4d8",
+                            "relpath": "foo",
+                        },
+                    ],
+                }
+            ]
+        }
+    )
+
+    data = tmp_dir / "data"
+    data.mkdir()
+    (data / "foo").write_text("foo")
+    (data / "bar").write_text("bar2")
+
+    dvc.commit("data", force=True)
+
+    assert (tmp_dir / "data.dvc").parse() == {
+        "outs": [
+            {
+                "path": "data",
+                "files": [
+                    {
+                        "size": 4,
+                        "md5": "224e2539f52203eb33728acd228b4432",
+                        "relpath": "bar",
+                    },
+                    {
+                        "size": 3,
+                        "version_id": "0vL53tFVY5vVAoJ4HG2jCS1mEcohDPE0",
+                        "etag": "acbd18db4cc2f85cedef654fccc4a4d8",
+                        "md5": "acbd18db4cc2f85cedef654fccc4a4d8",
+                        "relpath": "foo",
+                    },
+                ],
+            }
+        ]
+    }