@@ -29,6 +29,7 @@ use crate::core::Package;
2929use crate :: core:: PackageId ;
3030use crate :: core:: PackageIdSpecQuery ;
3131use crate :: core:: SourceId ;
32+ use crate :: core:: Summary ;
3233use crate :: core:: Workspace ;
3334use crate :: core:: dependency:: DepKind ;
3435use crate :: core:: manifest:: ManifestMetadata ;
@@ -86,15 +87,17 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
8687 . into_iter ( )
8788 . partition ( |( pkg, _) | pkg. publish ( ) == & Some ( vec ! [ ] ) ) ;
8889 // If `--workspace` is passed,
89- // the intent is more like "publish all publisable packages in this workspace",
90- // so skip `publish=false` packages.
91- let allow_unpublishable = match & opts. to_publish {
90+ // the intent is more like "publish all publisable packages in this workspace".
91+ // Hence,
92+ // * skip `publish=false` packages
93+ // * skip already published packages
94+ let is_workspace_publish = match & opts. to_publish {
9295 Packages :: Default => ws. is_virtual ( ) ,
9396 Packages :: All ( _) => true ,
9497 Packages :: OptOut ( _) => true ,
9598 Packages :: Packages ( _) => false ,
9699 } ;
97- if !unpublishable. is_empty ( ) && !allow_unpublishable {
100+ if !unpublishable. is_empty ( ) && !is_workspace_publish {
98101 bail ! (
99102 "{} cannot be published.\n \
100103 `package.publish` must be set to `true` or a non-empty list in Cargo.toml to publish.",
@@ -106,7 +109,7 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
106109 }
107110
108111 if pkgs. is_empty ( ) {
109- if allow_unpublishable {
112+ if is_workspace_publish {
110113 let n = unpublishable. len ( ) ;
111114 let plural = if n == 1 { "" } else { "s" } ;
112115 ws. gctx ( ) . shell ( ) . print_report (
@@ -159,13 +162,30 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
159162 Some ( Operation :: Read ) . filter ( |_| !opts. dry_run ) ,
160163 ) ?;
161164
165+ // `maybe_published` tracks package versions that already exist in the registry,
166+ // meaning they might have been published before.
167+ // Later, we verify the tarball checksum to see
168+ // if the local package matches the registry.
169+ // This helps catch cases where the local version
170+ // wasn’t bumped but files changed.
171+ let mut maybe_published = HashMap :: new ( ) ;
172+
162173 {
163174 let _lock = opts
164175 . gctx
165176 . acquire_package_cache_lock ( CacheLockMode :: DownloadExclusive ) ?;
166177
167178 for ( pkg, _) in & pkgs {
168- verify_unpublished ( pkg, & mut source, & source_ids, opts. dry_run , opts. gctx ) ?;
179+ if let Some ( summary) = verify_unpublished (
180+ pkg,
181+ & mut source,
182+ & source_ids,
183+ opts. dry_run ,
184+ is_workspace_publish,
185+ opts. gctx ,
186+ ) ? {
187+ maybe_published. insert ( pkg. package_id ( ) , summary) ;
188+ }
169189 verify_dependencies ( pkg, & registry, source_ids. original ) . map_err ( |err| {
170190 ManifestError :: new (
171191 err. context ( format ! (
@@ -218,15 +238,38 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
218238 let mut ready = plan. take_ready ( ) ;
219239 while let Some ( pkg_id) = ready. pop_first ( ) {
220240 let ( pkg, ( _features, tarball) ) = & pkg_dep_graph. packages [ & pkg_id] ;
221- opts. gctx . shell ( ) . status ( "Uploading" , pkg. package_id ( ) ) ?;
222-
223- if !opts. dry_run {
224- let ver = pkg. version ( ) . to_string ( ) ;
225241
242+ if opts. dry_run {
243+ opts. gctx . shell ( ) . status ( "Uploading" , pkg. package_id ( ) ) ?;
244+ } else {
226245 tarball. file ( ) . seek ( SeekFrom :: Start ( 0 ) ) ?;
227246 let hash = cargo_util:: Sha256 :: new ( )
228247 . update_file ( tarball. file ( ) ) ?
229248 . finish_hex ( ) ;
249+
250+ if let Some ( summary) = maybe_published. get ( & pkg. package_id ( ) ) {
251+ if summary. checksum ( ) == Some ( hash. as_str ( ) ) {
252+ opts. gctx . shell ( ) . warn ( format_args ! (
253+ "skipping upload for crate {}@{}: already exists on {}" ,
254+ pkg. name( ) ,
255+ pkg. version( ) ,
256+ source. describe( )
257+ ) ) ?;
258+ plan. mark_confirmed ( [ pkg. package_id ( ) ] ) ;
259+ continue ;
260+ }
261+ bail ! (
262+ "crate {}@{} already exists on {} but tarball checksum mismatched\n \
263+ perhaps local files have changed but forgot to bump the version?",
264+ pkg. name( ) ,
265+ pkg. version( ) ,
266+ source. describe( )
267+ ) ;
268+ }
269+
270+ opts. gctx . shell ( ) . status ( "Uploading" , pkg. package_id ( ) ) ?;
271+
272+ let ver = pkg. version ( ) . to_string ( ) ;
230273 let operation = Operation :: Publish {
231274 name : pkg. name ( ) . as_str ( ) ,
232275 vers : & ver,
@@ -278,6 +321,12 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
278321 }
279322 }
280323
324+ if to_confirm. is_empty ( ) {
325+ // nothing to confirm because some are already uploaded before
326+ // this cargo invocation.
327+ continue ;
328+ }
329+
281330 let confirmed = if opts. dry_run {
282331 to_confirm. clone ( )
283332 } else {
@@ -450,13 +499,18 @@ fn poll_one_package(
450499 Ok ( !summaries. is_empty ( ) )
451500}
452501
502+ /// Checks if a package is already published.
503+ ///
504+ /// Returns a [`Summary`] for computing the tarball checksum
505+ /// to compare with the registry index later, if needed.
453506fn verify_unpublished (
454507 pkg : & Package ,
455508 source : & mut RegistrySource < ' _ > ,
456509 source_ids : & RegistrySourceIds ,
457510 dry_run : bool ,
511+ skip_already_publish : bool ,
458512 gctx : & GlobalContext ,
459- ) -> CargoResult < ( ) > {
513+ ) -> CargoResult < Option < Summary > > {
460514 let query = Dependency :: parse (
461515 pkg. name ( ) ,
462516 Some ( & pkg. version ( ) . to_exact_req ( ) . to_string ( ) ) ,
@@ -470,28 +524,36 @@ fn verify_unpublished(
470524 std:: task:: Poll :: Pending => source. block_until_ready ( ) ?,
471525 }
472526 } ;
473- if !duplicate_query. is_empty ( ) {
474- // Move the registry error earlier in the publish process.
475- // Since dry-run wouldn't talk to the registry to get the error, we downgrade it to a
476- // warning.
477- if dry_run {
478- gctx. shell ( ) . warn ( format ! (
479- "crate {}@{} already exists on {}" ,
480- pkg. name( ) ,
481- pkg. version( ) ,
482- source. describe( )
483- ) ) ?;
484- } else {
485- bail ! (
486- "crate {}@{} already exists on {}" ,
487- pkg. name( ) ,
488- pkg. version( ) ,
489- source. describe( )
490- ) ;
491- }
527+ if duplicate_query. is_empty ( ) {
528+ return Ok ( None ) ;
492529 }
493530
494- Ok ( ( ) )
531+ // Move the registry error earlier in the publish process.
532+ // Since dry-run wouldn't talk to the registry to get the error,
533+ // we downgrade it to a warning.
534+ if skip_already_publish || dry_run {
535+ gctx. shell ( ) . warn ( format ! (
536+ "crate {}@{} already exists on {}" ,
537+ pkg. name( ) ,
538+ pkg. version( ) ,
539+ source. describe( )
540+ ) ) ?;
541+ } else {
542+ bail ! (
543+ "crate {}@{} already exists on {}" ,
544+ pkg. name( ) ,
545+ pkg. version( ) ,
546+ source. describe( )
547+ ) ;
548+ }
549+
550+ assert_eq ! (
551+ duplicate_query. len( ) ,
552+ 1 ,
553+ "registry must not have duplicat versions" ,
554+ ) ;
555+ let summary = duplicate_query. into_iter ( ) . next ( ) . unwrap ( ) . into_summary ( ) ;
556+ Ok ( skip_already_publish. then_some ( summary) )
495557}
496558
497559fn verify_dependencies (
0 commit comments