@@ -28,6 +28,7 @@ use crate::core::Package;
2828use crate :: core:: PackageId ;
2929use crate :: core:: PackageIdSpecQuery ;
3030use crate :: core:: SourceId ;
31+ use crate :: core:: Summary ;
3132use crate :: core:: Workspace ;
3233use crate :: core:: dependency:: DepKind ;
3334use crate :: core:: manifest:: ManifestMetadata ;
@@ -85,15 +86,17 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
8586 . into_iter ( )
8687 . partition ( |( pkg, _) | pkg. publish ( ) == & Some ( vec ! [ ] ) ) ;
8788 // If `--workspace` is passed,
88- // the intent is more like "publish all publisable packages in this workspace",
89- // so skip `publish=false` packages.
90- let allow_unpublishable = match & opts. to_publish {
89+ // the intent is more like "publish all publisable packages in this workspace".
90+ // Hence,
91+ // * skip `publish=false` packages
92+ // * skip already published packages
93+ let is_workspace_publish = match & opts. to_publish {
9194 Packages :: Default => ws. is_virtual ( ) ,
9295 Packages :: All ( _) => true ,
9396 Packages :: OptOut ( _) => true ,
9497 Packages :: Packages ( _) => false ,
9598 } ;
96- if !unpublishable. is_empty ( ) && !allow_unpublishable {
99+ if !unpublishable. is_empty ( ) && !is_workspace_publish {
97100 bail ! (
98101 "{} cannot be published.\n \
99102 `package.publish` must be set to `true` or a non-empty list in Cargo.toml to publish.",
@@ -105,7 +108,7 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
105108 }
106109
107110 if pkgs. is_empty ( ) {
108- if allow_unpublishable {
111+ if is_workspace_publish {
109112 let n = unpublishable. len ( ) ;
110113 let plural = if n == 1 { "" } else { "s" } ;
111114 ws. gctx ( ) . shell ( ) . warn ( format_args ! (
@@ -154,13 +157,30 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
154157 Some ( Operation :: Read ) . filter ( |_| !opts. dry_run ) ,
155158 ) ?;
156159
160+ // `maybe_published` tracks package versions that already exist in the registry,
161+ // meaning they might have been published before.
162+ // Later, we verify the tarball checksum to see
163+ // if the local package matches the registry.
164+ // This helps catch cases where the local version
165+ // wasn’t bumped but files changed.
166+ let mut maybe_published = HashMap :: new ( ) ;
167+
157168 {
158169 let _lock = opts
159170 . gctx
160171 . acquire_package_cache_lock ( CacheLockMode :: DownloadExclusive ) ?;
161172
162173 for ( pkg, _) in & pkgs {
163- verify_unpublished ( pkg, & mut source, & source_ids, opts. dry_run , opts. gctx ) ?;
174+ if let Some ( summary) = verify_unpublished (
175+ pkg,
176+ & mut source,
177+ & source_ids,
178+ opts. dry_run ,
179+ is_workspace_publish,
180+ opts. gctx ,
181+ ) ? {
182+ maybe_published. insert ( pkg. package_id ( ) , summary) ;
183+ }
164184 verify_dependencies ( pkg, & registry, source_ids. original ) . map_err ( |err| {
165185 ManifestError :: new (
166186 err. context ( format ! (
@@ -213,15 +233,38 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
213233 let mut ready = plan. take_ready ( ) ;
214234 while let Some ( pkg_id) = ready. pop_first ( ) {
215235 let ( pkg, ( _features, tarball) ) = & pkg_dep_graph. packages [ & pkg_id] ;
216- opts. gctx . shell ( ) . status ( "Uploading" , pkg. package_id ( ) ) ?;
217-
218- if !opts. dry_run {
219- let ver = pkg. version ( ) . to_string ( ) ;
220236
237+ if opts. dry_run {
238+ opts. gctx . shell ( ) . status ( "Uploading" , pkg. package_id ( ) ) ?;
239+ } else {
221240 tarball. file ( ) . seek ( SeekFrom :: Start ( 0 ) ) ?;
222241 let hash = cargo_util:: Sha256 :: new ( )
223242 . update_file ( tarball. file ( ) ) ?
224243 . finish_hex ( ) ;
244+
245+ if let Some ( summary) = maybe_published. get ( & pkg. package_id ( ) ) {
246+ if summary. checksum ( ) == Some ( hash. as_str ( ) ) {
247+ opts. gctx . shell ( ) . warn ( format_args ! (
248+ "skipping upload for crate {}@{}: already exists on {}" ,
249+ pkg. name( ) ,
250+ pkg. version( ) ,
251+ source. describe( )
252+ ) ) ?;
253+ plan. mark_confirmed ( [ pkg. package_id ( ) ] ) ;
254+ continue ;
255+ }
256+ bail ! (
257+ "crate {}@{} already exists on {} but tarball checksum mismatched\n \
258+ perhaps local files have changed but forgot to bump the version?",
259+ pkg. name( ) ,
260+ pkg. version( ) ,
261+ source. describe( )
262+ ) ;
263+ }
264+
265+ opts. gctx . shell ( ) . status ( "Uploading" , pkg. package_id ( ) ) ?;
266+
267+ let ver = pkg. version ( ) . to_string ( ) ;
225268 let operation = Operation :: Publish {
226269 name : pkg. name ( ) . as_str ( ) ,
227270 vers : & ver,
@@ -273,6 +316,12 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
273316 }
274317 }
275318
319+ if to_confirm. is_empty ( ) {
320+ // nothing to confirm because some are already uploaded before
321+ // this cargo invocation.
322+ continue ;
323+ }
324+
276325 let confirmed = if opts. dry_run {
277326 to_confirm. clone ( )
278327 } else {
@@ -440,13 +489,18 @@ fn poll_one_package(
440489 Ok ( !summaries. is_empty ( ) )
441490}
442491
492+ /// Checks if a package is already published.
493+ ///
494+ /// Returns a [`Summary`] for computing the tarball checksum
495+ /// to compare with the registry index later, if needed.
443496fn verify_unpublished (
444497 pkg : & Package ,
445498 source : & mut RegistrySource < ' _ > ,
446499 source_ids : & RegistrySourceIds ,
447500 dry_run : bool ,
501+ skip_already_publish : bool ,
448502 gctx : & GlobalContext ,
449- ) -> CargoResult < ( ) > {
503+ ) -> CargoResult < Option < Summary > > {
450504 let query = Dependency :: parse (
451505 pkg. name ( ) ,
452506 Some ( & pkg. version ( ) . to_exact_req ( ) . to_string ( ) ) ,
@@ -460,28 +514,36 @@ fn verify_unpublished(
460514 std:: task:: Poll :: Pending => source. block_until_ready ( ) ?,
461515 }
462516 } ;
463- if !duplicate_query. is_empty ( ) {
464- // Move the registry error earlier in the publish process.
465- // Since dry-run wouldn't talk to the registry to get the error, we downgrade it to a
466- // warning.
467- if dry_run {
468- gctx. shell ( ) . warn ( format ! (
469- "crate {}@{} already exists on {}" ,
470- pkg. name( ) ,
471- pkg. version( ) ,
472- source. describe( )
473- ) ) ?;
474- } else {
475- bail ! (
476- "crate {}@{} already exists on {}" ,
477- pkg. name( ) ,
478- pkg. version( ) ,
479- source. describe( )
480- ) ;
481- }
517+ if duplicate_query. is_empty ( ) {
518+ return Ok ( None ) ;
482519 }
483520
484- Ok ( ( ) )
521+ // Move the registry error earlier in the publish process.
522+ // Since dry-run wouldn't talk to the registry to get the error,
523+ // we downgrade it to a warning.
524+ if skip_already_publish || dry_run {
525+ gctx. shell ( ) . warn ( format ! (
526+ "crate {}@{} already exists on {}" ,
527+ pkg. name( ) ,
528+ pkg. version( ) ,
529+ source. describe( )
530+ ) ) ?;
531+ } else {
532+ bail ! (
533+ "crate {}@{} already exists on {}" ,
534+ pkg. name( ) ,
535+ pkg. version( ) ,
536+ source. describe( )
537+ ) ;
538+ }
539+
540+ assert_eq ! (
541+ duplicate_query. len( ) ,
542+ 1 ,
543+ "registry must not have duplicat versions" ,
544+ ) ;
545+ let summary = duplicate_query. into_iter ( ) . next ( ) . unwrap ( ) . into_summary ( ) ;
546+ Ok ( skip_already_publish. then_some ( summary) )
485547}
486548
487549fn verify_dependencies (
0 commit comments