From 0d2349dc05fd458e441e3014f9616a56cd16dc51 Mon Sep 17 00:00:00 2001
From: Ed Page <eopage@gmail.com>
Date: Mon, 30 Sep 2024 14:33:16 -0500
Subject: [PATCH 1/7] refactor(update): Tighten scope of transitive reporting
 checks

---
 src/cargo/ops/cargo_update.rs | 50 ++++++++++++++++++++---------------
 1 file changed, 29 insertions(+), 21 deletions(-)

diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs
index 0c9b885da70..08341b6ca79 100644
--- a/src/cargo/ops/cargo_update.rs
+++ b/src/cargo/ops/cargo_update.rs
@@ -795,21 +795,25 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
         return Some(report);
     }
 
-    if !change.is_transitive.unwrap_or(true) {
-        let incompat_ver_compat_msrv_summary = possibilities
-            .iter()
-            .map(|s| s.as_summary())
-            .filter(|s| {
-                if let (Some(summary_rust_version), Some(required_rust_version)) =
-                    (s.rust_version(), required_rust_version)
-                {
-                    summary_rust_version.is_compatible_with(required_rust_version)
-                } else {
-                    true
-                }
-            })
-            .filter(|s| is_latest(s.version(), package_id.version()))
-            .max_by_key(|s| s.version());
+    {
+        let incompat_ver_compat_msrv_summary = if !change.is_transitive.unwrap_or(true) {
+            possibilities
+                .iter()
+                .map(|s| s.as_summary())
+                .filter(|s| {
+                    if let (Some(summary_rust_version), Some(required_rust_version)) =
+                        (s.rust_version(), required_rust_version)
+                    {
+                        summary_rust_version.is_compatible_with(required_rust_version)
+                    } else {
+                        true
+                    }
+                })
+                .filter(|s| is_latest(s.version(), package_id.version()))
+                .max_by_key(|s| s.version())
+        } else {
+            None
+        };
         if let Some(summary) = incompat_ver_compat_msrv_summary {
             let warn = style::WARN;
             let version = summary.version();
@@ -834,12 +838,16 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
         return Some(report);
     }
 
-    if !change.is_transitive.unwrap_or(true) {
-        let incompat_ver_summary = possibilities
-            .iter()
-            .map(|s| s.as_summary())
-            .filter(|s| is_latest(s.version(), package_id.version()))
-            .max_by_key(|s| s.version());
+    {
+        let incompat_ver_summary = if !change.is_transitive.unwrap_or(true) {
+            possibilities
+                .iter()
+                .map(|s| s.as_summary())
+                .filter(|s| is_latest(s.version(), package_id.version()))
+                .max_by_key(|s| s.version())
+        } else {
+            None
+        };
         if let Some(summary) = incompat_ver_summary {
             let msrv_note = summary
                 .rust_version()

From 1b3c91bbb4c10fdefb754e6e57edf8c42c8c3068 Mon Sep 17 00:00:00 2001
From: Ed Page <eopage@gmail.com>
Date: Mon, 30 Sep 2024 14:33:46 -0500
Subject: [PATCH 2/7] refactor(update): Remove redundant scopes

---
 src/cargo/ops/cargo_update.rs | 86 +++++++++++++++++------------------
 1 file changed, 41 insertions(+), 45 deletions(-)

diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs
index 08341b6ca79..c8703afc766 100644
--- a/src/cargo/ops/cargo_update.rs
+++ b/src/cargo/ops/cargo_update.rs
@@ -795,31 +795,29 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
         return Some(report);
     }
 
-    {
-        let incompat_ver_compat_msrv_summary = if !change.is_transitive.unwrap_or(true) {
-            possibilities
-                .iter()
-                .map(|s| s.as_summary())
-                .filter(|s| {
-                    if let (Some(summary_rust_version), Some(required_rust_version)) =
-                        (s.rust_version(), required_rust_version)
-                    {
-                        summary_rust_version.is_compatible_with(required_rust_version)
-                    } else {
-                        true
-                    }
-                })
-                .filter(|s| is_latest(s.version(), package_id.version()))
-                .max_by_key(|s| s.version())
-        } else {
-            None
-        };
-        if let Some(summary) = incompat_ver_compat_msrv_summary {
-            let warn = style::WARN;
-            let version = summary.version();
-            let report = format!(" {warn}(available: v{version}){warn:#}");
-            return Some(report);
-        }
+    let incompat_ver_compat_msrv_summary = if !change.is_transitive.unwrap_or(true) {
+        possibilities
+            .iter()
+            .map(|s| s.as_summary())
+            .filter(|s| {
+                if let (Some(summary_rust_version), Some(required_rust_version)) =
+                    (s.rust_version(), required_rust_version)
+                {
+                    summary_rust_version.is_compatible_with(required_rust_version)
+                } else {
+                    true
+                }
+            })
+            .filter(|s| is_latest(s.version(), package_id.version()))
+            .max_by_key(|s| s.version())
+    } else {
+        None
+    };
+    if let Some(summary) = incompat_ver_compat_msrv_summary {
+        let warn = style::WARN;
+        let version = summary.version();
+        let report = format!(" {warn}(available: v{version}){warn:#}");
+        return Some(report);
     }
 
     let compat_ver_summary = possibilities
@@ -838,26 +836,24 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
         return Some(report);
     }
 
-    {
-        let incompat_ver_summary = if !change.is_transitive.unwrap_or(true) {
-            possibilities
-                .iter()
-                .map(|s| s.as_summary())
-                .filter(|s| is_latest(s.version(), package_id.version()))
-                .max_by_key(|s| s.version())
-        } else {
-            None
-        };
-        if let Some(summary) = incompat_ver_summary {
-            let msrv_note = summary
-                .rust_version()
-                .map(|rv| format!(", requires Rust {rv}"))
-                .unwrap_or_default();
-            let warn = style::NOP;
-            let version = summary.version();
-            let report = format!(" {warn}(available: v{version}{msrv_note}){warn:#}");
-            return Some(report);
-        }
+    let incompat_ver_summary = if !change.is_transitive.unwrap_or(true) {
+        possibilities
+            .iter()
+            .map(|s| s.as_summary())
+            .filter(|s| is_latest(s.version(), package_id.version()))
+            .max_by_key(|s| s.version())
+    } else {
+        None
+    };
+    if let Some(summary) = incompat_ver_summary {
+        let msrv_note = summary
+            .rust_version()
+            .map(|rv| format!(", requires Rust {rv}"))
+            .unwrap_or_default();
+        let warn = style::NOP;
+        let version = summary.version();
+        let report = format!(" {warn}(available: v{version}{msrv_note}){warn:#}");
+        return Some(report);
     }
 
     None

From 63e27c60cce6f03d1a5412b0a85b37abeb16b8f2 Mon Sep 17 00:00:00 2001
From: Ed Page <eopage@gmail.com>
Date: Mon, 30 Sep 2024 14:34:44 -0500
Subject: [PATCH 3/7] refactor(update): Calculate all reporting states

---
 src/cargo/ops/cargo_update.rs | 45 ++++++++++++++++++-----------------
 1 file changed, 23 insertions(+), 22 deletions(-)

diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs
index c8703afc766..1da0af54a14 100644
--- a/src/cargo/ops/cargo_update.rs
+++ b/src/cargo/ops/cargo_update.rs
@@ -788,12 +788,6 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
         })
         .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
         .max_by_key(|s| s.version());
-    if let Some(summary) = compat_ver_compat_msrv_summary {
-        let warn = style::WARN;
-        let version = summary.version();
-        let report = format!(" {warn}(available: v{version}){warn:#}");
-        return Some(report);
-    }
 
     let incompat_ver_compat_msrv_summary = if !change.is_transitive.unwrap_or(true) {
         possibilities
@@ -813,28 +807,12 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
     } else {
         None
     };
-    if let Some(summary) = incompat_ver_compat_msrv_summary {
-        let warn = style::WARN;
-        let version = summary.version();
-        let report = format!(" {warn}(available: v{version}){warn:#}");
-        return Some(report);
-    }
 
     let compat_ver_summary = possibilities
         .iter()
         .map(|s| s.as_summary())
         .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
         .max_by_key(|s| s.version());
-    if let Some(summary) = compat_ver_summary {
-        let msrv_note = summary
-            .rust_version()
-            .map(|rv| format!(", requires Rust {rv}"))
-            .unwrap_or_default();
-        let warn = style::NOP;
-        let version = summary.version();
-        let report = format!(" {warn}(available: v{version}{msrv_note}){warn:#}");
-        return Some(report);
-    }
 
     let incompat_ver_summary = if !change.is_transitive.unwrap_or(true) {
         possibilities
@@ -845,6 +823,29 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
     } else {
         None
     };
+
+    if let Some(summary) = compat_ver_compat_msrv_summary {
+        let warn = style::WARN;
+        let version = summary.version();
+        let report = format!(" {warn}(available: v{version}){warn:#}");
+        return Some(report);
+    }
+    if let Some(summary) = incompat_ver_compat_msrv_summary {
+        let warn = style::WARN;
+        let version = summary.version();
+        let report = format!(" {warn}(available: v{version}){warn:#}");
+        return Some(report);
+    }
+    if let Some(summary) = compat_ver_summary {
+        let msrv_note = summary
+            .rust_version()
+            .map(|rv| format!(", requires Rust {rv}"))
+            .unwrap_or_default();
+        let warn = style::NOP;
+        let version = summary.version();
+        let report = format!(" {warn}(available: v{version}{msrv_note}){warn:#}");
+        return Some(report);
+    }
     if let Some(summary) = incompat_ver_summary {
         let msrv_note = summary
             .rust_version()

From fbe79f5456bbeeae71fc2ff8b168707e20e196c5 Mon Sep 17 00:00:00 2001
From: Ed Page <eopage@gmail.com>
Date: Mon, 30 Sep 2024 14:35:50 -0500
Subject: [PATCH 4/7] refactor(update): Clarify reporting states are mutually
 exclusive

---
 src/cargo/ops/cargo_update.rs | 21 +++++++++------------
 1 file changed, 9 insertions(+), 12 deletions(-)

diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs
index 1da0af54a14..8fc406df943 100644
--- a/src/cargo/ops/cargo_update.rs
+++ b/src/cargo/ops/cargo_update.rs
@@ -828,15 +828,13 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
         let warn = style::WARN;
         let version = summary.version();
         let report = format!(" {warn}(available: v{version}){warn:#}");
-        return Some(report);
-    }
-    if let Some(summary) = incompat_ver_compat_msrv_summary {
+        Some(report)
+    } else if let Some(summary) = incompat_ver_compat_msrv_summary {
         let warn = style::WARN;
         let version = summary.version();
         let report = format!(" {warn}(available: v{version}){warn:#}");
-        return Some(report);
-    }
-    if let Some(summary) = compat_ver_summary {
+        Some(report)
+    } else if let Some(summary) = compat_ver_summary {
         let msrv_note = summary
             .rust_version()
             .map(|rv| format!(", requires Rust {rv}"))
@@ -844,9 +842,8 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
         let warn = style::NOP;
         let version = summary.version();
         let report = format!(" {warn}(available: v{version}{msrv_note}){warn:#}");
-        return Some(report);
-    }
-    if let Some(summary) = incompat_ver_summary {
+        Some(report)
+    } else if let Some(summary) = incompat_ver_summary {
         let msrv_note = summary
             .rust_version()
             .map(|rv| format!(", requires Rust {rv}"))
@@ -854,10 +851,10 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
         let warn = style::NOP;
         let version = summary.version();
         let report = format!(" {warn}(available: v{version}{msrv_note}){warn:#}");
-        return Some(report);
+        Some(report)
+    } else {
+        None
     }
-
-    None
 }
 
 fn is_latest(candidate: &semver::Version, current: &semver::Version) -> bool {

From a246fd589bbee8b53c0313db531cc92c5fa42087 Mon Sep 17 00:00:00 2001
From: Ed Page <eopage@gmail.com>
Date: Fri, 13 Sep 2024 11:17:50 -0400
Subject: [PATCH 5/7] refactor(resolve): Change how we calculate unchanged

---
 src/cargo/ops/cargo_update.rs | 78 +++++++++++++++++++++++++++++------
 1 file changed, 65 insertions(+), 13 deletions(-)

diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs
index 8fc406df943..bcb9a3838ec 100644
--- a/src/cargo/ops/cargo_update.rs
+++ b/src/cargo/ops/cargo_update.rs
@@ -538,8 +538,8 @@ fn print_lockfile_generation(
                     vec![]
                 };
 
-                let required_rust_version = report_required_rust_version(resolve, change);
-                let latest = report_latest(&possibilities, change);
+                let required_rust_version = report_required_rust_version(resolve, change, None);
+                let latest = report_latest(&possibilities, change, None);
                 let note = required_rust_version.or(latest);
 
                 if let Some(note) = note {
@@ -601,8 +601,8 @@ fn print_lockfile_sync(
                     vec![]
                 };
 
-                let required_rust_version = report_required_rust_version(resolve, change);
-                let latest = report_latest(&possibilities, change);
+                let required_rust_version = report_required_rust_version(resolve, change, None);
+                let latest = report_latest(&possibilities, change, None);
                 let note = required_rust_version.or(latest).unwrap_or_default();
 
                 ws.gctx().shell().status_with_color(
@@ -654,8 +654,8 @@ fn print_lockfile_updates(
             PackageChangeKind::Added
             | PackageChangeKind::Upgraded
             | PackageChangeKind::Downgraded => {
-                let required_rust_version = report_required_rust_version(resolve, change);
-                let latest = report_latest(&possibilities, change);
+                let required_rust_version = report_required_rust_version(resolve, change, None);
+                let latest = report_latest(&possibilities, change, None);
                 let note = required_rust_version.or(latest).unwrap_or_default();
 
                 ws.gctx().shell().status_with_color(
@@ -672,14 +672,16 @@ fn print_lockfile_updates(
                 )?;
             }
             PackageChangeKind::Unchanged => {
-                let required_rust_version = report_required_rust_version(resolve, change);
-                let latest = report_latest(&possibilities, change);
+                let mut unchanged_stats = UpdateStats::default();
+                let required_rust_version =
+                    report_required_rust_version(resolve, change, Some(&mut unchanged_stats));
+                let latest = report_latest(&possibilities, change, Some(&mut unchanged_stats));
                 let note = required_rust_version.as_deref().or(latest.as_deref());
 
+                if unchanged_stats.behind() {
+                    unchanged_behind += 1;
+                }
                 if let Some(note) = note {
-                    if latest.is_some() {
-                        unchanged_behind += 1;
-                    }
                     if ws.gctx().shell().verbosity() == Verbosity::Verbose {
                         ws.gctx().shell().status_with_color(
                             change.kind.status(),
@@ -748,7 +750,11 @@ fn required_rust_version(ws: &Workspace<'_>) -> Option<PartialVersion> {
     }
 }
 
-fn report_required_rust_version(resolve: &Resolve, change: &PackageChange) -> Option<String> {
+fn report_required_rust_version(
+    resolve: &Resolve,
+    change: &PackageChange,
+    stats: Option<&mut UpdateStats>,
+) -> Option<String> {
     if change.package_id.source_id().is_path() {
         return None;
     }
@@ -759,13 +765,20 @@ fn report_required_rust_version(resolve: &Resolve, change: &PackageChange) -> Op
         return None;
     }
 
+    if let Some(stats) = stats {
+        stats.required_rust_version += 1;
+    }
     let error = style::ERROR;
     Some(format!(
         " {error}(requires Rust {package_rust_version}){error:#}"
     ))
 }
 
-fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Option<String> {
+fn report_latest(
+    possibilities: &[IndexSummary],
+    change: &PackageChange,
+    mut stats: Option<&mut UpdateStats>,
+) -> Option<String> {
     let package_id = change.package_id;
     if !package_id.source_id().is_registry() {
         return None;
@@ -788,6 +801,11 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
         })
         .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
         .max_by_key(|s| s.version());
+    if let Some(ref mut stats) = stats {
+        if compat_ver_compat_msrv_summary.is_some() {
+            stats.compat_ver_compat_msrv += 1;
+        }
+    }
 
     let incompat_ver_compat_msrv_summary = if !change.is_transitive.unwrap_or(true) {
         possibilities
@@ -807,12 +825,22 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
     } else {
         None
     };
+    if let Some(stats) = stats.as_mut() {
+        if incompat_ver_compat_msrv_summary.is_some() {
+            stats.incompat_ver_compat_msrv += 1;
+        }
+    }
 
     let compat_ver_summary = possibilities
         .iter()
         .map(|s| s.as_summary())
         .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
         .max_by_key(|s| s.version());
+    if let Some(stats) = stats.as_mut() {
+        if compat_ver_summary.is_some() {
+            stats.compat_ver += 1;
+        }
+    }
 
     let incompat_ver_summary = if !change.is_transitive.unwrap_or(true) {
         possibilities
@@ -823,6 +851,11 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
     } else {
         None
     };
+    if let Some(stats) = stats.as_mut() {
+        if incompat_ver_summary.is_some() {
+            stats.incompat_ver += 1;
+        }
+    }
 
     if let Some(summary) = compat_ver_compat_msrv_summary {
         let warn = style::WARN;
@@ -857,6 +890,25 @@ fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Opti
     }
 }
 
+#[derive(Default)]
+struct UpdateStats {
+    required_rust_version: usize,
+    compat_ver_compat_msrv: usize,
+    incompat_ver_compat_msrv: usize,
+    compat_ver: usize,
+    incompat_ver: usize,
+}
+
+impl UpdateStats {
+    fn behind(&self) -> bool {
+        self.compat_ver_compat_msrv
+            + self.incompat_ver_compat_msrv
+            + self.compat_ver
+            + self.incompat_ver
+            != 0
+    }
+}
+
 fn is_latest(candidate: &semver::Version, current: &semver::Version) -> bool {
     current < candidate
                 // Only match pre-release if major.minor.patch are the same

From be2e7f4f94c3d87105840f0a0bed3cc63e03d6ed Mon Sep 17 00:00:00 2001
From: Ed Page <eopage@gmail.com>
Date: Fri, 13 Sep 2024 22:21:57 -0400
Subject: [PATCH 6/7] test(resolve): Show more reporting cases

---
 tests/testsuite/rust_version.rs | 30 ++++++++++++++++++++++++++++--
 1 file changed, 28 insertions(+), 2 deletions(-)

diff --git a/tests/testsuite/rust_version.rs b/tests/testsuite/rust_version.rs
index c36416e64af..4fef1d7c59a 100644
--- a/tests/testsuite/rust_version.rs
+++ b/tests/testsuite/rust_version.rs
@@ -1113,33 +1113,57 @@ fn report_rust_versions() {
         .rust_version("1.55.0")
         .file("src/lib.rs", "fn other_stuff() {}")
         .publish();
+
+    Package::new("dep-only-low-incompatible", "1.55.0")
+        .rust_version("1.55.0")
+        .file("src/lib.rs", "fn other_stuff() {}")
+        .publish();
     Package::new("dep-only-low-incompatible", "1.75.0")
         .rust_version("1.75.0")
         .file("src/lib.rs", "fn other_stuff() {}")
         .publish();
+
+    Package::new("dep-only-high-compatible", "1.55.0")
+        .rust_version("1.55.0")
+        .file("src/lib.rs", "fn other_stuff() {}")
+        .publish();
     Package::new("dep-only-high-compatible", "1.65.0")
         .rust_version("1.65.0")
         .file("src/lib.rs", "fn other_stuff() {}")
         .publish();
+
+    Package::new("dep-only-high-incompatible", "1.55.0")
+        .rust_version("1.55.0")
+        .file("src/lib.rs", "fn other_stuff() {}")
+        .publish();
     Package::new("dep-only-high-incompatible", "1.75.0")
         .rust_version("1.75.0")
         .file("src/lib.rs", "fn other_stuff() {}")
         .publish();
+
     Package::new("dep-only-unset-unset", "1.0.0")
         .file("src/lib.rs", "fn other_stuff() {}")
         .publish();
+
+    Package::new("dep-only-unset-compatible", "1.55.0")
+        .rust_version("1.55.0")
+        .file("src/lib.rs", "fn other_stuff() {}")
+        .publish();
     Package::new("dep-only-unset-compatible", "1.75.0")
         .rust_version("1.75.0")
         .file("src/lib.rs", "fn other_stuff() {}")
         .publish();
+
     Package::new("dep-only-unset-incompatible", "1.2345.0")
         .rust_version("1.2345.0")
         .file("src/lib.rs", "fn other_stuff() {}")
         .publish();
+
     Package::new("dep-shared-compatible", "1.55.0")
         .rust_version("1.55.0")
         .file("src/lib.rs", "fn other_stuff() {}")
         .publish();
+
     Package::new("dep-shared-incompatible", "1.75.0")
         .rust_version("1.75.0")
         .file("src/lib.rs", "fn other_stuff() {}")
@@ -1210,8 +1234,10 @@ fn report_rust_versions() {
         .with_stderr_data(str![[r#"
 [UPDATING] `dummy-registry` index
 [LOCKING] 9 packages to latest Rust 1.60.0 compatible versions
-[ADDING] dep-only-high-incompatible v1.75.0 (requires Rust 1.75.0)
-[ADDING] dep-only-low-incompatible v1.75.0 (requires Rust 1.75.0)
+[ADDING] dep-only-high-compatible v1.55.0 (available: v1.65.0)
+[ADDING] dep-only-high-incompatible v1.55.0 (available: v1.75.0, requires Rust 1.75.0)
+[ADDING] dep-only-low-incompatible v1.55.0 (available: v1.75.0, requires Rust 1.75.0)
+[ADDING] dep-only-unset-compatible v1.55.0 (available: v1.75.0)
 [ADDING] dep-only-unset-incompatible v1.2345.0 (requires Rust 1.2345.0)
 [ADDING] dep-shared-incompatible v1.75.0 (requires Rust 1.75.0)
 

From 818eead9dbfc8cb0f3731affd6ef95f9bb3cb262 Mon Sep 17 00:00:00 2001
From: Ed Page <eopage@gmail.com>
Date: Fri, 13 Sep 2024 23:33:58 -0400
Subject: [PATCH 7/7] feat(resolve): Direct people to working around less
 optimal MSRV-resolver results

In discussing #14414, the general problem of the resolver picking a
version older than a package needs for its MSRV (or lack of one) because of the MSRV of
other packages came up.
This tries to patch over that problem by telling users that a dependency
might be able to be newer than the resolver selected.

The message is fairly generic and might be misread to be about any MSRV
update which an MSRV `fallback` strategy allows, which would make the
count off.
The reason it is so generic is we don't know with precision why it was
held back
- Direct dependents may have a non-semver upper bound on the version as
  we aren't trying to unify the version requirements across direct
  dependents at this time
- A dependency could have removed a feature without making a breaking
  change
  - This seems like it should instead be an error but thats a
    conversation for another day
- ~~The user enabled `-Zminimal-versions`~~
  - This is now detected and the message skipped

Note: separate from this, we may also print the status suffix for this
case if the package was not selected for update (e.g. passing
`--workspace`).
---
 src/cargo/ops/cargo_update.rs   | 55 ++++++++++++++++++++++++++++-----
 tests/testsuite/rust_version.rs |  1 +
 tests/testsuite/workspaces.rs   |  1 +
 3 files changed, 50 insertions(+), 7 deletions(-)

diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs
index bcb9a3838ec..a2852da7cc4 100644
--- a/src/cargo/ops/cargo_update.rs
+++ b/src/cargo/ops/cargo_update.rs
@@ -519,6 +519,7 @@ fn print_lockfile_generation(
     annotate_required_rust_version(ws, resolve, &mut changes);
 
     status_locking(ws, num_pkgs)?;
+    let mut changed_stats = UpdateStats::default();
     for change in changes.values() {
         if change.is_member.unwrap_or(false) {
             continue;
@@ -538,8 +539,9 @@ fn print_lockfile_generation(
                     vec![]
                 };
 
-                let required_rust_version = report_required_rust_version(resolve, change, None);
-                let latest = report_latest(&possibilities, change, None);
+                let required_rust_version =
+                    report_required_rust_version(resolve, change, Some(&mut changed_stats));
+                let latest = report_latest(&possibilities, change, Some(&mut changed_stats));
                 let note = required_rust_version.or(latest);
 
                 if let Some(note) = note {
@@ -559,6 +561,22 @@ fn print_lockfile_generation(
         }
     }
 
+    let compat_ver_compat_msrv = changed_stats.compat_ver_compat_msrv;
+    if 0 < compat_ver_compat_msrv
+        && !ws.gctx().cli_unstable().direct_minimal_versions
+        && !ws.gctx().cli_unstable().minimal_versions
+    {
+        if compat_ver_compat_msrv == 1 {
+            ws.gctx().shell().note(format!(
+                "{compat_ver_compat_msrv} package may have a higher, compatible version. To update it, run `cargo update <name> --precise <version>"
+            ))?;
+        } else {
+            ws.gctx().shell().note(format!(
+                "{compat_ver_compat_msrv} packages may have a higher, compatible version. To update them, run `cargo update <name> --precise <version>"
+            ))?;
+        }
+    }
+
     Ok(())
 }
 
@@ -580,6 +598,7 @@ fn print_lockfile_sync(
     annotate_required_rust_version(ws, resolve, &mut changes);
 
     status_locking(ws, num_pkgs)?;
+    let mut changed_stats = UpdateStats::default();
     for change in changes.values() {
         if change.is_member.unwrap_or(false) {
             continue;
@@ -601,8 +620,9 @@ fn print_lockfile_sync(
                     vec![]
                 };
 
-                let required_rust_version = report_required_rust_version(resolve, change, None);
-                let latest = report_latest(&possibilities, change, None);
+                let required_rust_version =
+                    report_required_rust_version(resolve, change, Some(&mut changed_stats));
+                let latest = report_latest(&possibilities, change, Some(&mut changed_stats));
                 let note = required_rust_version.or(latest).unwrap_or_default();
 
                 ws.gctx().shell().status_with_color(
@@ -615,6 +635,16 @@ fn print_lockfile_sync(
         }
     }
 
+    let compat_ver_compat_msrv = changed_stats.compat_ver_compat_msrv;
+    if 0 < compat_ver_compat_msrv
+        && !ws.gctx().cli_unstable().direct_minimal_versions
+        && !ws.gctx().cli_unstable().minimal_versions
+    {
+        ws.gctx().shell().note(format!(
+                "{compat_ver_compat_msrv} were updated but higher versions may be available by manually updating with `cargo update <name> --precise <version>"
+            ))?;
+    }
+
     Ok(())
 }
 
@@ -636,6 +666,7 @@ fn print_lockfile_updates(
         status_locking(ws, num_pkgs)?;
     }
     let mut unchanged_behind = 0;
+    let mut changed_stats = UpdateStats::default();
     for change in changes.values() {
         let possibilities = if let Some(query) = change.alternatives_query() {
             loop {
@@ -654,8 +685,9 @@ fn print_lockfile_updates(
             PackageChangeKind::Added
             | PackageChangeKind::Upgraded
             | PackageChangeKind::Downgraded => {
-                let required_rust_version = report_required_rust_version(resolve, change, None);
-                let latest = report_latest(&possibilities, change, None);
+                let required_rust_version =
+                    report_required_rust_version(resolve, change, Some(&mut changed_stats));
+                let latest = report_latest(&possibilities, change, Some(&mut changed_stats));
                 let note = required_rust_version.or(latest).unwrap_or_default();
 
                 ws.gctx().shell().status_with_color(
@@ -694,6 +726,15 @@ fn print_lockfile_updates(
         }
     }
 
+    let compat_ver_compat_msrv = changed_stats.compat_ver_compat_msrv;
+    if 0 < compat_ver_compat_msrv
+        && !ws.gctx().cli_unstable().direct_minimal_versions
+        && !ws.gctx().cli_unstable().minimal_versions
+    {
+        ws.gctx().shell().note(format!(
+                "{compat_ver_compat_msrv} were updated but higher versions may be available by manually updating with `cargo update <name> --precise <version>"
+            ))?;
+    }
     if ws.gctx().shell().verbosity() == Verbosity::Verbose {
         ws.gctx().shell().note(
             "to see how you depend on a package, run `cargo tree --invert --package <dep>@<ver>`",
@@ -890,7 +931,7 @@ fn report_latest(
     }
 }
 
-#[derive(Default)]
+#[derive(Copy, Clone, Default, Debug)]
 struct UpdateStats {
     required_rust_version: usize,
     compat_ver_compat_msrv: usize,
diff --git a/tests/testsuite/rust_version.rs b/tests/testsuite/rust_version.rs
index 4fef1d7c59a..c3d9e24eb38 100644
--- a/tests/testsuite/rust_version.rs
+++ b/tests/testsuite/rust_version.rs
@@ -1240,6 +1240,7 @@ fn report_rust_versions() {
 [ADDING] dep-only-unset-compatible v1.55.0 (available: v1.75.0)
 [ADDING] dep-only-unset-incompatible v1.2345.0 (requires Rust 1.2345.0)
 [ADDING] dep-shared-incompatible v1.75.0 (requires Rust 1.75.0)
+[NOTE] 2 packages may have a higher, compatible version. To update them, run `cargo update <name> --precise <version>
 
 "#]])
         .run();
diff --git a/tests/testsuite/workspaces.rs b/tests/testsuite/workspaces.rs
index 9e831fa394f..f0489be99f9 100644
--- a/tests/testsuite/workspaces.rs
+++ b/tests/testsuite/workspaces.rs
@@ -698,6 +698,7 @@ fn share_dependencies() {
 [UPDATING] `dummy-registry` index
 [LOCKING] 1 package to latest compatible version
 [ADDING] dep1 v0.1.3 (available: v0.1.8)
+[NOTE] 1 package may have a higher, compatible version. To update it, run `cargo update <name> --precise <version>
 [DOWNLOADING] crates ...
 [DOWNLOADED] dep1 v0.1.3 (registry `dummy-registry`)
 [CHECKING] dep1 v0.1.3