Repository: rust-lang/cargo (rust)

Issue:
cargo vendor doesn't re-vendor if the version number hasn't changed
<!-- Thanks for filing a 🐛 bug report 😄! -->

**Problem**
<!-- A clear and concise description of what the bug is. -->
<!-- including what currently happens and what you expected to happen. -->

`cargo vendor` fails to re-vendor a package if:
- the package vendor dir has a version number (eg because it's not the most recent, or `--versioned-dirs` is in use)
- the code has changed

This can happen if the code is coming from a git repo and the `rev` has been updated, if a patch has been added in the `[patch]` section of the Cargo.toml, or when switching between crates.io and git as a source.

This doesn't happen for non-versioned directories because `cargo vendor` unconditionally re-vendors their contents.

**Possible Solution(s)**
Rather than just comparing the version, it should also check for changes to the patch and/or origin specification.

<!-- Not obligatory, but suggest a fix/reason for the bug, -->
<!-- or ideas how to implement the addition or change -->

**Notes**

Output of `cargo version`: 1.42, but this hasn't changed in recent versions

<!-- Also, any additional context or information you feel may be relevant to the issue. -->
<!-- (e.g rust version, OS platform/distribution/version, target toolchain(s), release channel.. -->


Pull request description:
fix(vendor): trust crate version only when coming from registries
### What does this PR try to resolve?

Fixes #8181
Relates to #11897 and #14525

### How should we test and review this PR?

As mentioned in the contribution guide, I made a first commit adding a test that passes with the actual behaviour. Then, I made a second commit with a fix and modified the test with the new expected  behaviour.

### Additional information

The fix doesn't take into account switching from a git dependency to crates.io, which is not handled correctly on master either, and would probably require the vendoring to serialize the source ID to detect source changes.

I specifically limited the trust of immutable version to crates.io, but it could be extended to other registries.

Patch:
diff --git a/src/cargo/ops/vendor.rs b/src/cargo/ops/vendor.rs
index f3d0883bd..35d0e0c94 100644
--- a/src/cargo/ops/vendor.rs
+++ b/src/cargo/ops/vendor.rs
@@ -207,8 +207,10 @@ fn sync(
         let dst = canonical_destination.join(&dst_name);
         to_remove.remove(&dst);
         let cksum = dst.join(".cargo-checksum.json");
-        if dir_has_version_suffix && cksum.exists() {
-            // Always re-copy directory without version suffix in case the version changed
+        // Registries are the only immutable sources,
+        // path and git dependencies' versions cannot be trusted to mean "no change"
+        if dir_has_version_suffix && id.source_id().is_registry() && cksum.exists() {
+            // Don't re-copy directory with version suffix in case it comes from a registry
             continue;
         }
 


Test patch:
diff --git a/tests/testsuite/vendor.rs b/tests/testsuite/vendor.rs
index 7f7ef2b24..39651268b 100644
--- a/tests/testsuite/vendor.rs
+++ b/tests/testsuite/vendor.rs
@@ -1568,6 +1568,67 @@ path = [..]
     );
 }
 
+#[cargo_test]
+fn git_update_rev() {
+    let (git_project, git_repo) = git::new_repo("git", |p| {
+        p.file("Cargo.toml", &basic_manifest("a", "0.1.0"))
+            .file("src/lib.rs", "")
+    });
+    let url = git_project.url();
+    let ref_1 = "initial";
+    let ref_2 = "update";
+
+    git::tag(&git_repo, ref_1);
+
+    git_project.change_file("src/lib.rs", "pub fn f() {}");
+    git::add(&git_repo);
+    git::commit(&git_repo);
+    git::tag(&git_repo, ref_2);
+
+    let p = project()
+        .file(
+            "Cargo.toml",
+            &format!(
+                r#"
+                    [package]
+                    name = "foo"
+                    version = "0.1.0"
+
+                    [dependencies]
+                    a = {{ git = '{url}', rev = '{ref_1}' }}
+                "#
+            ),
+        )
+        .file("src/lib.rs", "")
+        .build();
+
+    p.cargo("vendor --respect-source-config --versioned-dirs")
+        .run();
+
+    let lib = p.read_file("vendor/a-0.1.0/src/lib.rs");
+    assert_e2e().eq(lib, "");
+
+    p.change_file(
+        "Cargo.toml",
+        &format!(
+            r#"
+                    [package]
+                    name = "foo"
+                    version = "0.1.0"
+
+                    [dependencies]
+                    a = {{ git = '{url}', rev = '{ref_2}' }}
+                "#
+        ),
+    );
+
+    p.cargo("vendor --respect-source-config --versioned-dirs")
+        .run();
+
+    let lib = p.read_file("vendor/a-0.1.0/src/lib.rs");
+    assert_e2e().eq(lib, "pub fn f() {}");
+}
+
 #[cargo_test]
 fn depend_on_vendor_dir_not_deleted() {
     let p = project()