@@ -14,6 +14,7 @@ use cargo_util::paths;
1414use cargo_util_schemas:: core:: PartialVersion ;
1515use cargo_util_schemas:: manifest:: PathBaseName ;
1616use cargo_util_schemas:: manifest:: RustVersion ;
17+ use crate_spec:: CrateSpecResolutionError ;
1718use indexmap:: IndexSet ;
1819use itertools:: Itertools ;
1920use toml_edit:: Item as TomlItem ;
@@ -104,6 +105,10 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
104105 options. gctx ,
105106 & mut registry,
106107 )
108+ . map_err ( |err| match err. downcast :: < CrateSpecResolutionError > ( ) {
109+ Ok ( err) => add_spec_fix_suggestion ( err, raw) ,
110+ Err ( other) => other,
111+ } )
107112 } )
108113 . collect :: < CargoResult < Vec < _ > > > ( ) ?
109114 } ;
@@ -1258,3 +1263,100 @@ fn precise_version(version_req: &semver::VersionReq) -> Option<String> {
12581263 . max ( )
12591264 . map ( |v| v. to_string ( ) )
12601265}
1266+
1267+ /// Help with invalid arguments to `cargo add`
1268+ fn add_spec_fix_suggestion ( err : CrateSpecResolutionError , arg : & DepOp ) -> crate :: Error {
1269+ if let Some ( note) = spec_fix_suggestion_inner ( arg) {
1270+ return anyhow:: format_err!( "{err}\n note: {note}" ) ;
1271+ }
1272+ err. into ( )
1273+ }
1274+
1275+ fn spec_fix_suggestion_inner ( arg : & DepOp ) -> Option < & ' static str > {
1276+ let spec = arg. crate_spec . as_deref ( ) ?;
1277+
1278+ let looks_like_git_url = spec. starts_with ( "git@" )
1279+ || spec. starts_with ( "ssh:" )
1280+ || spec. ends_with ( ".git" )
1281+ || spec. starts_with ( "https://git" ) ; // compromise between favoring a couple of top sites vs trying to list every git host
1282+
1283+ // check if the arg is present to avoid suggesting it redundantly
1284+ if arg. git . is_none ( ) && looks_like_git_url {
1285+ Some ( "git URLs must be specified with --git <URL>" )
1286+ } else if arg. registry . is_none ( )
1287+ && ( spec. starts_with ( "registry+" ) || spec. starts_with ( "sparse+" ) )
1288+ {
1289+ Some ( "registy can be specified with --registry <NAME>" )
1290+ } else if spec. contains ( "://" ) || looks_like_git_url {
1291+ Some ( "`cargo add` expects crates specified as 'name' or 'name@version', not URLs" )
1292+ } else if arg. path . is_none ( ) && spec. contains ( '/' ) {
1293+ Some ( "local crates can be added with --path <DIR>" )
1294+ } else {
1295+ None
1296+ }
1297+ }
1298+
1299+ #[ test]
1300+ fn test_spec_fix_suggestion ( ) {
1301+ fn dep ( crate_spec : & str ) -> DepOp {
1302+ DepOp {
1303+ crate_spec : Some ( crate_spec. into ( ) ) ,
1304+ rename : None ,
1305+ features : None ,
1306+ default_features : None ,
1307+ optional : None ,
1308+ public : None ,
1309+ registry : None ,
1310+ path : None ,
1311+ base : None ,
1312+ git : None ,
1313+ branch : None ,
1314+ rev : None ,
1315+ tag : None ,
1316+ }
1317+ }
1318+
1319+ #[ track_caller]
1320+ fn err_for ( dep : & DepOp ) -> String {
1321+ add_spec_fix_suggestion (
1322+ CrateSpec :: resolve ( dep. crate_spec . as_deref ( ) . unwrap ( ) ) . unwrap_err ( ) ,
1323+ & dep,
1324+ )
1325+ . to_string ( )
1326+ }
1327+
1328+ for path in [ "../some/path" , "/absolute/path" , "~/dir/Cargo.toml" ] {
1329+ let mut dep = dep ( path) ;
1330+ assert ! ( err_for( & dep) . contains( "note: local crates can be added with --path <DIR>" ) ) ;
1331+
1332+ dep. path = Some ( "." . into ( ) ) ;
1333+ assert ! ( !err_for( & dep) . contains( "--git" ) ) ;
1334+ assert ! ( !err_for( & dep) . contains( "--registry" ) ) ;
1335+ assert ! ( !err_for( & dep) . contains( "--path" ) ) ;
1336+ }
1337+
1338+ assert ! ( err_for( & dep(
1339+ "registry+https://private.local:8000/index#[email protected] " 1340+ ) )
1341+ . contains( "--registry <NAME>" ) ) ;
1342+
1343+ for git_url in [
1344+ "git@host:dir/repo.git" ,
1345+ "https://gitlab.com/~user/crate.git" ,
1346+ "ssh://host/path" ,
1347+ ] {
1348+ let mut dep = dep ( git_url) ;
1349+ let msg = err_for ( & dep) ;
1350+ assert ! (
1351+ msg. contains( "note: git URLs must be specified with --git <URL>" ) ,
1352+ "{msg} {dep:?}"
1353+ ) ;
1354+ assert ! ( !err_for( & dep) . contains( "--path" ) ) ;
1355+ assert ! ( !err_for( & dep) . contains( "--registry" ) ) ;
1356+
1357+ dep. git = Some ( "true" . into ( ) ) ;
1358+ let msg = err_for ( & dep) ;
1359+ assert ! ( !msg. contains( "--git" ) ) ;
1360+ assert ! ( msg. contains( "'name@version', not URLs" ) , "{msg} {dep:?}" ) ;
1361+ }
1362+ }
0 commit comments