Repository: Enselic/cargo-public-api (rust)

Issue:
Support sparse creates.io registry
When enabling sparse registry, I consistently got this error:
```
Error: The repo at path /home/codespace/.cargo/registry/index/github.com-1ecc6299db9ec823 is unusable due to having an invalid HEAD reference: reference 'refs/heads/main' not found; class=Reference (4); code=NotFound (-3)
```
Once disabled, and recreating the registry (rming it then cargo fetch) it works.

Command: `cargo public-api diff latest`

Pull request description:
Support sparse crates.io index
Potential follow-up work:
* Contribute upstream patch to creates-index to reduce number of deps. Notably that way we get rid of our indirect git2 dep. But it is more important to support sparse index than not have a git2 dep. So this is not a blocker.

Fixes #408 

Patch:
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 162393d..2972d03 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -9,7 +9,6 @@ on:
 
 env:
   CARGO_TERM_COLOR: always
-  CARGO_REGISTRIES_CRATES_IO_PROTOCOL: git # Until https://github.com/Enselic/cargo-public-api/issues/408 "Support sparse creates.io registry" is fixed
 
 jobs:
   all-jobs:
@@ -67,12 +66,15 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os:
-          - macos-latest
-          - ubuntu-latest
-          - windows-latest
-    name: cargo test (${{ matrix.os }})
-    runs-on: ${{ matrix.os }}
+        variant:
+          - { name: Ubuntu,  os: ubuntu-latest,  protocol: git    }
+          - { name: Ubuntu,  os: ubuntu-latest,  protocol: sparse }
+          - { name: macOS,   os: macos-latest,   protocol: sparse }
+          - { name: Windows, os: windows-latest, protocol: sparse }
+    name: cargo test (${{ matrix.variant.name }} ${{ matrix.variant.protocol }})
+    runs-on: ${{ matrix.variant.os }}
+    env:
+      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: ${{ matrix.variant.protocol }}
     steps:
       - uses: actions/checkout@v3
       - run: zsh --version || (sudo apt-get install -y zsh && zsh --version)
diff --git a/Cargo.lock b/Cargo.lock
index e2c9346..254519a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -108,6 +108,15 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "bstr"
 version = "1.4.0"
@@ -155,11 +164,13 @@ dependencies = [
  "anyhow",
  "assert_cmd",
  "cargo-manifest",
+ "cargo-util",
  "cargo_metadata",
  "chrono",
  "clap",
  "clap_complete_command",
  "crates-index",
+ "curl",
  "diff",
  "dirs",
  "expect-test",
@@ -179,6 +190,28 @@ dependencies = [
  "toml",
 ]
 
+[[package]]
+name = "cargo-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e2320a2b1242f9181a3347ae0884bb497e1853d299da99780fa1e96f9abe23"
+dependencies = [
+ "anyhow",
+ "core-foundation",
+ "filetime",
+ "hex",
+ "jobserver",
+ "libc",
+ "log",
+ "miow",
+ "same-file",
+ "sha2",
+ "shell-escape",
+ "tempfile",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
 [[package]]
 name = "cargo_metadata"
 version = "0.15.4"
@@ -308,6 +341,31 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "crates-index"
 version = "0.19.10"
@@ -328,6 +386,46 @@ dependencies = [
  "toml",
 ]
 
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.63+curl-8.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
 [[package]]
 name = "cvt"
 version = "0.1.2"
@@ -349,6 +447,16 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
 
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
 [[package]]
 name = "dirs"
 version = "5.0.0"
@@ -427,11 +535,23 @@ dependencies = [
  "instant",
 ]
 
+[[package]]
+name = "filetime"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.2.16",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "form_urlencoded"
-version = "1.1.0"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
 dependencies = [
  "percent-encoding",
 ]
@@ -451,6 +571,16 @@ dependencies = [
  "windows-sys 0.45.0",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.9"
@@ -464,9 +594,9 @@ dependencies = [
 
 [[package]]
 name = "git2"
-version = "0.16.1"
+version = "0.17.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
+checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044"
 dependencies = [
  "bitflags",
  "libc",
@@ -528,9 +658,9 @@ dependencies = [
 
 [[package]]
 name = "idna"
-version = "0.3.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
 dependencies = [
  "unicode-bidi",
  "unicode-normalization",
@@ -616,9 +746,9 @@ checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
 
 [[package]]
 name = "libgit2-sys"
-version = "0.14.2+1.5.1"
+version = "0.15.2+1.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
+checksum = "a80df2e11fb4a61f4ba2ab42dbe7f74468da143f1a75c74e11dee7c813f694fa"
 dependencies = [
  "cc",
  "libc",
@@ -628,9 +758,9 @@ dependencies = [
 
 [[package]]
 name = "libz-sys"
-version = "1.1.8"
+version = "1.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
+checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db"
 dependencies = [
  "cc",
  "libc",
@@ -659,6 +789,15 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
+[[package]]
+name = "miow"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ffbca2f655e33c08be35d87278e5b18b89550a37dbd598c20db92f6a471123"
+dependencies = [
+ "windows-sys 0.42.0",
+]
+
 [[package]]
 name = "nix"
 version = "0.26.2"
@@ -714,17 +853,35 @@ version = "1.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
 
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "percent-encoding"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
 
 [[package]]
 name = "pkg-config"
-version = "0.3.26"
+version = "0.3.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
 
 [[package]]
 name = "predicates"
@@ -909,6 +1066,24 @@ version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
 
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
+dependencies = [
+ "windows-sys 0.42.0",
+]
+
 [[package]]
 name = "semver"
 version = "1.0.17"
@@ -958,6 +1133,23 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shell-escape"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
+
 [[package]]
 name = "smart-default"
 version = "0.6.0"
@@ -978,6 +1170,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "socket2"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "stable_deref_trait"
 version = "1.2.0"
@@ -1118,6 +1320,12 @@ dependencies = [
  "winnow",
 ]
 
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
 [[package]]
 name = "unicode-bidi"
 version = "0.3.13"
@@ -1141,9 +1349,9 @@ dependencies = [
 
 [[package]]
 name = "url"
-version = "2.3.1"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
 dependencies = [
  "form_urlencoded",
  "idna",
@@ -1162,6 +1370,12 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
 [[package]]
 name = "wait-timeout"
 version = "0.2.0"
@@ -1171,12 +1385,68 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "walkdir"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.45.0"
diff --git a/cargo-public-api/Cargo.toml b/cargo-public-api/Cargo.toml
index 529e5c1..283a8e6 100644
--- a/cargo-public-api/Cargo.toml
+++ b/cargo-public-api/Cargo.toml
@@ -16,6 +16,7 @@ nu-ansi-term = "0.48.0"
 anyhow = "1.0.71"
 is-terminal = "0.4.7"
 cargo-manifest = "0.9.0"
+cargo-util = "0.2.4"
 clap_complete_command = "0.5.1"
 diff = "0.1.13"
 dirs = "5.0.0"
@@ -30,6 +31,9 @@ serde = { version = "1.0.164", features = ["derive"] }
 version = "4.3.3"
 features = ["derive", "wrap_help"]
 
+[dependencies.curl]
+version = "0.4.44"
+
 [dependencies.crates-index]
 version = "0.19.10"
 default-features = false
diff --git a/cargo-public-api/src/published_crate.rs b/cargo-public-api/src/published_crate.rs
index 484e906..a746730 100644
--- a/cargo-public-api/src/published_crate.rs
+++ b/cargo-public-api/src/published_crate.rs
@@ -3,26 +3,14 @@
 //! project.
 
 use crate::{Args, LATEST_VERSION_ARG};
-use anyhow::{anyhow, Result};
-use std::path::{Path, PathBuf};
+use anyhow::{anyhow, Context, Result};
+use crates_index::{Crate, Version};
+use std::path::PathBuf;
 
 pub fn build_rustdoc_json(version: Option<&str>, args: &Args) -> Result<PathBuf> {
     let package_name = package_name_from_args(args).ok_or_else(|| anyhow!("You must specify a package with either `-p package-name` or `--manifest-path path/to/Cargo.toml`"))?;
-
-    let index = crates_index::Index::new_cargo_default().map_err(|e| match e {
-        // We have to look inside the string until the there is an enum variant
-        // https://github.com/frewsxcv/rust-crates-index/blob/286b2251ae8a286f8992831f7a845f88227107dd/src/bare_index.rs#L352-L353
-        crates_index::Error::Url(msg) if msg.contains("invalid HEAD") => anyhow!("sparse crates.io index not supported yet, see https://github.com/Enselic/cargo-public-api/issues/408"),
-        err => anyhow!(err),
-    })?;
-    let crate_ = index.crate_(&package_name).ok_or_else(|| {
-        anyhow!(
-            "Could not find crate `{package_name}` in {:?}",
-            index.path()
-        )
-    })?;
-
-    let crate_version = get_crate_version(&crate_, version, index.path())?;
+    let crate_ = http_get_crate(&package_name, args.verbose)?;
+    let crate_version = get_crate_version(&crate_, version)?;
     let build_dir = build_dir(args, &crate_version);
     std::fs::create_dir_all(&build_dir)?;
 
@@ -51,11 +39,7 @@ pub fn build_rustdoc_json(version: Option<&str>, args: &Args) -> Result<PathBuf>
     crate::api_source::build_rustdoc_json(builder)
 }
 
-fn get_crate_version(
-    crate_: &crates_index::Crate,
-    version: Option<&str>,
-    index_path: &Path,
-) -> Result<crates_index::Version, anyhow::Error> {
+fn get_crate_version(crate_: &Crate, version: Option<&str>) -> Result<Version, anyhow::Error> {
     match version {
         Some(LATEST_VERSION_ARG) | None => {
             let resolved = if version.is_none() {
@@ -77,10 +61,9 @@ fn get_crate_version(
             .map(Clone::clone)
             .ok_or_else(|| {
                 anyhow!(
-                    "Could not find version `{}` of crate `{}` in {:?}",
+                    "Could not find version `{}` of crate `{}`",
                     version,
                     crate_.name(),
-                    index_path,
                 )
             }),
     }
@@ -151,3 +134,34 @@ fn manifest_for(args: &Args, spec: &crates_index::Version) -> Result<String> {
         })?
     ))
 }
+
+// We use curl to significantly reduce the number of needed deps compared to
+// e.g. reqwest.
+#[allow(clippy::similar_names)]
+fn http_get_crate(name: &str, verbose: bool) -> Result<Crate> {
+    let mut body: Vec<u8> = vec![];
+    let dep_path = cargo_util::registry::make_dep_path(name, false /* prefix_only */);
+    let url = format!("https://index.crates.io/{dep_path}");
+    if verbose {
+        eprintln!("Using {url:?} to get info about crate {name:?}");
+    }
+
+    let mut curl = curl::easy::Easy::new();
+    curl.url(&url)?;
+    curl.follow_location(true)?;
+
+    let mut list = curl::easy::List::new();
+    list.append("Accept: application/json")?;
+    curl.http_headers(list)?;
+
+    {
+        let mut transfer = curl.transfer();
+        transfer.write_function(|data| {
+            body.extend_from_slice(data);
+            Ok(data.len())
+        })?;
+        transfer.perform()?;
+    }
+
+    Crate::from_slice(&body).with_context(|| anyhow!("Could not find crate `{name}`"))
+}


Test patch:
diff --git a/cargo-public-api/tests/cargo-public-api-bin-tests.rs b/cargo-public-api/tests/cargo-public-api-bin-tests.rs
index 329c7e9..d03c6f3 100644
--- a/cargo-public-api/tests/cargo-public-api-bin-tests.rs
+++ b/cargo-public-api/tests/cargo-public-api-bin-tests.rs
@@ -402,6 +402,38 @@ fn diff_with_invalid_published_crate_version() {
         .failure();
 }
 
+#[test]
+fn diff_with_crate_not_published() {
+    let tempdir = tempfile::tempdir().unwrap();
+
+    let write_file = |name: &str, contents: &str| -> std::io::Result<PathBuf> {
+        let mut path = tempdir.path().to_owned();
+        path.push(name);
+        std::fs::write(&path, contents)?;
+        Ok(path)
+    };
+
+    let manifest = toml::toml! {
+        [package]
+        name = "this-create-has-not-been-published-and-never-will"
+        version = "0.1.0"
+        [lib]
+        path = "lib.rs"
+    };
+
+    write_file("Cargo.toml", &manifest.to_string()).unwrap();
+    write_file("lib.rs", "// empty lib").unwrap();
+
+    let mut cmd = TestCmd::new();
+    cmd.current_dir(tempdir.path());
+    cmd.arg("diff");
+    cmd.assert()
+        .stderr(contains(
+            "Error: Could not find crate `this-create-has-not-been-published-and-never-will`",
+        ))
+        .failure();
+}
+
 #[test]
 fn diff_with_invalid_published_crate_version_number() {
     let mut cmd = TestCmd::new().with_test_repo();