@@ -116,7 +116,7 @@ enum FrozenMismatch {
116116 ReplaceWith ( String ) ,
117117 /// Remove the stale comment because no ref points at the pinned commit.
118118 Remove ,
119- /// Warn only because the pinned commit itself could not be resolved .
119+ /// Warn only because the pinned commit is not present in the fetched repository view .
120120 NoReplacement ,
121121}
122122
@@ -260,7 +260,7 @@ pub(crate) async fn auto_update(
260260 . then_with ( || a. target . required_hook_ids . cmp ( & b. target . required_hook_ids ) )
261261 } ) ;
262262
263- warn_frozen_mismatches ( & outcomes, dry_run , printer) ?;
263+ warn_frozen_mismatches ( & outcomes, printer) ?;
264264
265265 // Group results by project config file
266266 #[ expect( clippy:: mutable_key_type) ]
@@ -360,11 +360,7 @@ fn collect_repo_sources(workspace: &Workspace) -> Result<Vec<RepoSource<'_>>> {
360360}
361361
362362/// Emits all frozen-comment warnings before the normal update output.
363- fn warn_frozen_mismatches (
364- updates : & [ RepoUpdate < ' _ > ] ,
365- dry_run : bool ,
366- printer : Printer ,
367- ) -> Result < ( ) > {
363+ fn warn_frozen_mismatches ( updates : & [ RepoUpdate < ' _ > ] , printer : Printer ) -> Result < ( ) > {
368364 for update in updates {
369365 let Ok ( resolved) = & update. result else {
370366 continue ;
@@ -377,8 +373,7 @@ fn warn_frozen_mismatches(
377373 render_frozen_mismatch_warning(
378374 update. target. repo,
379375 update. target. current_rev,
380- mismatch,
381- dry_run
376+ mismatch
382377 )
383378 ) ?;
384379 }
@@ -536,10 +531,8 @@ async fn collect_frozen_mismatches<'a>(
536531 return Ok ( Vec :: new ( ) ) ;
537532 }
538533
539- let current_rev_is_valid = resolve_revision_to_commit ( repo_path, target. current_rev )
540- . await
541- . is_ok ( ) ;
542- let rev_tags = if current_rev_is_valid {
534+ let current_rev_is_present = is_commit_present ( repo_path, target. current_rev ) . await ?;
535+ let rev_tags = if current_rev_is_present {
543536 get_tags_pointing_at_revision ( tag_timestamps, target. current_rev )
544537 } else {
545538 Vec :: new ( )
@@ -566,7 +559,7 @@ async fn collect_frozen_mismatches<'a>(
566559 } ;
567560 let mismatch = select_best_tag ( & rev_tags, current_frozen, true ) . map_or_else (
568561 || {
569- if current_rev_is_valid {
562+ if current_rev_is_present {
570563 FrozenMismatch :: Remove
571564 } else {
572565 FrozenMismatch :: NoReplacement
@@ -708,16 +701,6 @@ async fn evaluate_repo_target<'a>(
708701/// Initializes a temporary git repo and fetches the remote HEAD plus tags.
709702async fn setup_and_fetch_repo ( repo_url : & str , repo_path : & Path ) -> Result < ( ) > {
710703 git:: init_repo ( repo_url, repo_path) . await ?;
711- git:: git_cmd ( "git config" ) ?
712- . arg ( "config" )
713- . arg ( "extensions.partialClone" )
714- . arg ( "true" )
715- . current_dir ( repo_path)
716- . remove_git_envs ( )
717- . stdout ( Stdio :: null ( ) )
718- . stderr ( Stdio :: null ( ) )
719- . status ( )
720- . await ?;
721704 git:: git_cmd ( "git fetch" ) ?
722705 . arg ( "fetch" )
723706 . arg ( "origin" )
@@ -749,6 +732,38 @@ async fn resolve_revision_to_commit(repo_path: &Path, rev: &str) -> Result<Strin
749732 Ok ( String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) )
750733}
751734
735+ /// Returns whether a pinned commit SHA is already present in the refs fetched for `auto-update`.
736+ ///
737+ /// `auto-update` fetches only `origin/HEAD` and tags, using `--filter=blob:none`. That filter
738+ /// still downloads commits and trees reachable from those refs, but omits blobs. We intentionally
739+ /// use `git --no-lazy-fetch cat-file -e` here instead of `rev-parse`: in a partial clone,
740+ /// `rev-parse` may lazily fetch a missing commit from the promisor remote on demand. On GitHub,
741+ /// that can make a fork-only "impostor commit" appear to belong to the parent repository.
742+ ///
743+ /// `auto-update` only selects updates from tags, or from `HEAD` in `--bleeding-edge` mode. It
744+ /// does not normally update to arbitrary branches, so we currently fetch only those refs here.
745+ ///
746+ /// So this helper answers a narrower question than "is this SHA valid anywhere on the remote?".
747+ /// It only checks whether the commit is already available from the refs we fetched for update
748+ /// selection. That means branch-only commits outside `HEAD` and tags are treated as absent for
749+ /// now. If that leads to false positives in practice, we can revisit this and fetch branches too.
750+ async fn is_commit_present ( repo_path : & Path , commit : & str ) -> Result < bool > {
751+ let status = git:: git_cmd ( "git cat-file" ) ?
752+ . arg ( "--no-lazy-fetch" )
753+ . arg ( "cat-file" )
754+ . arg ( "-e" )
755+ . arg ( format ! ( "{commit}^{{commit}}" ) )
756+ . check ( false )
757+ . current_dir ( repo_path)
758+ . remove_git_envs ( )
759+ . stdout ( Stdio :: null ( ) )
760+ . stderr ( Stdio :: null ( ) )
761+ . status ( )
762+ . await ?;
763+
764+ Ok ( status. success ( ) )
765+ }
766+
752767fn get_tags_pointing_at_revision < ' a > (
753768 tag_timestamps : & ' a [ TagTimestamp ] ,
754769 rev : & str ,
@@ -765,7 +780,6 @@ fn render_frozen_mismatch_warning(
765780 repo : & str ,
766781 current_rev : & str ,
767782 mismatch : & FrozenCommentMismatch < ' _ > ,
768- dry_run : bool ,
769783) -> String {
770784 let label = match mismatch. reason {
771785 FrozenMismatchReason :: ResolvesToDifferentCommit => {
@@ -780,19 +794,13 @@ fn render_frozen_mismatch_warning(
780794 } ;
781795 let details = match & mismatch. mismatch {
782796 FrozenMismatch :: ReplaceWith ( replacement) => {
783- format ! (
784- "{} frozen comment to `{replacement}`" ,
785- if dry_run { "would update" } else { "updating" }
786- )
797+ format ! ( "pinned commit `{current_rev}` is referenced by `{replacement}`" )
787798 }
788799 FrozenMismatch :: Remove => {
789- format ! (
790- "{} frozen comment because no tag points at the pinned commit" ,
791- if dry_run { "would remove" } else { "removing" }
792- )
800+ format ! ( "no tag points at the pinned commit `{current_rev}`" )
793801 }
794802 FrozenMismatch :: NoReplacement => {
795- format ! ( "pinned commit `{current_rev}` does not exist in the repo" )
803+ format ! ( "pinned commit `{current_rev}` is not present in the repo" )
796804 }
797805 } ;
798806 let title = format ! (
0 commit comments