1 //! Handle `cargo add` arguments
2 
3 use cargo_edit::{find, registry_url, Dependency};
4 use cargo_edit::{get_latest_dependency, CrateName};
5 use std::path::PathBuf;
6 use structopt::StructOpt;
7 
8 use crate::errors::*;
9 
10 #[derive(Debug, StructOpt)]
11 #[structopt(bin_name = "cargo")]
12 pub enum Command {
13     /// Add dependency to a Cargo.toml manifest file.
14     #[structopt(name = "add")]
15     #[structopt(
16         after_help = "This command allows you to add a dependency to a Cargo.toml manifest file. If <crate> is a github
17 or gitlab repository URL, or a local path, `cargo add` will try to automatically get the crate name
18 and set the appropriate `--git` or `--path` value.
19 
20 Please note that Cargo treats versions like '1.2.3' as '^1.2.3' (and that '^1.2.3' is specified
21 as '>=1.2.3 and <2.0.0'). By default, `cargo add` will use this format, as it is the one that the
22 crates.io registry suggests. One goal of `cargo add` is to prevent you from using wildcard
23 dependencies (version set to '*')."
24     )]
25     Add(Args),
26 }
27 
28 #[derive(Debug, StructOpt)]
29 pub struct Args {
30     /// Crates to be added.
31     #[structopt(name = "crate", required = true)]
32     pub crates: Vec<String>,
33 
34     /// Rename a dependency in Cargo.toml,
35     /// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#renaming-dependencies-in-cargotoml
36     /// Only works when specifying a single dependency.
37     #[structopt(long = "rename", short = "r")]
38     pub rename: Option<String>,
39 
40     /// Add crate as development dependency.
41     #[structopt(long = "dev", short = "D", conflicts_with = "build")]
42     pub dev: bool,
43 
44     /// Add crate as build dependency.
45     #[structopt(long = "build", short = "B", conflicts_with = "dev")]
46     pub build: bool,
47 
48     /// Specify the version to grab from the registry(crates.io).
49     /// You can also specify version as part of name, e.g
50     /// `cargo add bitflags@0.3.2`.
51     #[structopt(long = "vers", value_name = "uri", conflicts_with = "git")]
52     pub vers: Option<String>,
53 
54     /// Specify a git repository to download the crate from.
55     #[structopt(
56         long = "git",
57         value_name = "uri",
58         conflicts_with = "vers",
59         conflicts_with = "path"
60     )]
61     pub git: Option<String>,
62 
63     /// Specify a git branch to download the crate from.
64     #[structopt(
65         long = "branch",
66         value_name = "branch",
67         conflicts_with = "vers",
68         conflicts_with = "path"
69     )]
70     pub branch: Option<String>,
71 
72     /// Specify the path the crate should be loaded from.
73     #[structopt(long = "path", conflicts_with = "git")]
74     pub path: Option<PathBuf>,
75 
76     /// Add as dependency to the given target platform.
77     #[structopt(long = "target", conflicts_with = "dev", conflicts_with = "build")]
78     pub target: Option<String>,
79 
80     /// Add as an optional dependency (for use in features).
81     #[structopt(long = "optional", conflicts_with = "dev", conflicts_with = "build")]
82     pub optional: bool,
83 
84     /// Path to the manifest to add a dependency to.
85     #[structopt(long = "manifest-path", value_name = "path", conflicts_with = "pkgid")]
86     pub manifest_path: Option<PathBuf>,
87 
88     /// Package id of the crate to add this dependency to.
89     #[structopt(
90         long = "package",
91         short = "p",
92         value_name = "pkgid",
93         conflicts_with = "path"
94     )]
95     pub pkgid: Option<String>,
96 
97     /// Choose method of semantic version upgrade.  Must be one of "none" (exact version, `=`
98     /// modifier), "patch" (`~` modifier), "minor" (`^` modifier), "all" (`>=`), or "default" (no
99     /// modifier).
100     #[structopt(
101         long = "upgrade",
102         value_name = "method",
103         possible_value = "none",
104         possible_value = "patch",
105         possible_value = "minor",
106         possible_value = "all",
107         possible_value = "default",
108         default_value = "default"
109     )]
110     pub upgrade: String,
111 
112     /// Include prerelease versions when fetching from crates.io (e.g.
113     /// '0.6.0-alpha').
114     #[structopt(long = "allow-prerelease")]
115     pub allow_prerelease: bool,
116 
117     /// Space-separated list of features to add. For an alternative approach to
118     /// enabling features, consider installing the `cargo-feature` utility.
119     #[structopt(long = "features", number_of_values = 1)]
120     pub features: Option<Vec<String>>,
121 
122     /// Set `default-features = false` for the added dependency.
123     #[structopt(long = "no-default-features")]
124     pub no_default_features: bool,
125 
126     /// Do not print any output in case of success.
127     #[structopt(long = "quiet", short = "q")]
128     pub quiet: bool,
129 
130     /// Run without accessing the network
131     #[structopt(long = "offline")]
132     pub offline: bool,
133 
134     /// Sort dependencies even if currently unsorted
135     #[structopt(long = "sort", short = "s")]
136     pub sort: bool,
137 
138     /// Registry to use
139     #[structopt(long = "registry", conflicts_with = "git", conflicts_with = "path")]
140     pub registry: Option<String>,
141 }
142 
parse_version_req(s: &str) -> Result<&str>143 fn parse_version_req(s: &str) -> Result<&str> {
144     semver::VersionReq::parse(s).chain_err(|| "Invalid dependency version requirement")?;
145     Ok(s)
146 }
147 
148 impl Args {
149     /// Get dependency section
get_section(&self) -> Vec<String>150     pub fn get_section(&self) -> Vec<String> {
151         if self.dev {
152             vec!["dev-dependencies".to_owned()]
153         } else if self.build {
154             vec!["build-dependencies".to_owned()]
155         } else if let Some(ref target) = self.target {
156             if target.is_empty() {
157                 panic!("Target specification may not be empty");
158             }
159             vec![
160                 "target".to_owned(),
161                 target.clone(),
162                 "dependencies".to_owned(),
163             ]
164         } else {
165             vec!["dependencies".to_owned()]
166         }
167     }
168 
parse_single_dependency(&self, crate_name: &str) -> Result<Dependency>169     fn parse_single_dependency(&self, crate_name: &str) -> Result<Dependency> {
170         let crate_name = CrateName::new(crate_name);
171 
172         if let Some(mut dependency) = crate_name.parse_as_version()? {
173             // crate specifier includes a version (e.g. `docopt@0.8`)
174             if let Some(ref url) = self.git {
175                 let url = url.clone();
176                 let version = dependency.version().unwrap().to_string();
177                 return Err(ErrorKind::GitUrlWithVersion(url, version).into());
178             }
179 
180             if let Some(ref path) = self.path {
181                 dependency = dependency.set_path(path.to_str().unwrap());
182             }
183 
184             Ok(dependency)
185         } else if crate_name.is_url_or_path() {
186             Ok(crate_name.parse_crate_name_from_uri()?)
187         } else {
188             assert_eq!(self.git.is_some() && self.vers.is_some(), false);
189             assert_eq!(self.git.is_some() && self.path.is_some(), false);
190             assert_eq!(self.git.is_some() && self.registry.is_some(), false);
191             assert_eq!(self.path.is_some() && self.registry.is_some(), false);
192 
193             let mut dependency = Dependency::new(crate_name.name());
194 
195             if let Some(repo) = &self.git {
196                 dependency = dependency.set_git(repo, self.branch.clone());
197             }
198             if let Some(path) = &self.path {
199                 dependency = dependency.set_path(path.to_str().unwrap());
200             }
201             if let Some(version) = &self.vers {
202                 dependency = dependency.set_version(parse_version_req(version)?);
203             }
204             let registry_url = if let Some(registry) = &self.registry {
205                 Some(registry_url(&find(&self.manifest_path)?, Some(registry))?)
206             } else {
207                 None
208             };
209 
210             if self.git.is_none() && self.path.is_none() && self.vers.is_none() {
211                 let dep = get_latest_dependency(
212                     crate_name.name(),
213                     self.allow_prerelease,
214                     &find(&self.manifest_path)?,
215                     &registry_url,
216                 )?;
217                 let v = format!(
218                     "{prefix}{version}",
219                     prefix = self.get_upgrade_prefix(),
220                     // If version is unavailable `get_latest_dependency` must have
221                     // returned `Err(FetchVersionError::GetVersion)`
222                     version = dep.version().unwrap_or_else(|| unreachable!())
223                 );
224                 dependency = dep.set_version(&v);
225             }
226 
227             // Set the registry after getting the latest version as
228             // get_latest_dependency returns a registry-less Dependency
229             if let Some(registry) = &self.registry {
230                 dependency = dependency.set_registry(registry);
231             }
232             Ok(dependency)
233         }
234     }
235 
236     /// Build dependencies from arguments
parse_dependencies(&self) -> Result<Vec<Dependency>>237     pub fn parse_dependencies(&self) -> Result<Vec<Dependency>> {
238         if self.crates.len() > 1
239             && (self.git.is_some() || self.path.is_some() || self.vers.is_some())
240         {
241             return Err(ErrorKind::MultipleCratesWithGitOrPathOrVers.into());
242         }
243 
244         if self.crates.len() > 1 && self.rename.is_some() {
245             return Err(ErrorKind::MultipleCratesWithRename.into());
246         }
247 
248         if self.crates.len() > 1 && self.features.is_some() {
249             return Err(ErrorKind::MultipleCratesWithFeatures.into());
250         }
251 
252         self.crates
253             .iter()
254             .map(|crate_name| {
255                 self.parse_single_dependency(crate_name).map(|x| {
256                     let mut x = x
257                         .set_optional(self.optional)
258                         .set_features(self.features.clone())
259                         .set_default_features(!self.no_default_features);
260                     if let Some(ref rename) = self.rename {
261                         x = x.set_rename(rename);
262                     }
263                     x
264                 })
265             })
266             .collect()
267     }
268 
get_upgrade_prefix(&self) -> &'static str269     fn get_upgrade_prefix(&self) -> &'static str {
270         match self.upgrade.as_ref() {
271             "default" => "",
272             "none" => "=",
273             "patch" => "~",
274             "minor" => "^",
275             "all" => ">=",
276             _ => unreachable!(),
277         }
278     }
279 }
280 
281 #[cfg(test)]
282 impl Default for Args {
default() -> Args283     fn default() -> Args {
284         Args {
285             crates: vec!["demo".to_owned()],
286             rename: None,
287             dev: false,
288             build: false,
289             vers: None,
290             git: None,
291             branch: None,
292             path: None,
293             target: None,
294             optional: false,
295             manifest_path: None,
296             pkgid: None,
297             upgrade: "minor".to_string(),
298             allow_prerelease: false,
299             features: None,
300             no_default_features: false,
301             quiet: false,
302             offline: true,
303             sort: false,
304             registry: None,
305         }
306     }
307 }
308 
309 #[cfg(test)]
310 mod tests {
311     use super::*;
312     use cargo_edit::Dependency;
313 
314     #[test]
test_dependency_parsing()315     fn test_dependency_parsing() {
316         let args = Args {
317             vers: Some("0.4.2".to_owned()),
318             ..Args::default()
319         };
320 
321         assert_eq!(
322             args.parse_dependencies().unwrap(),
323             vec![Dependency::new("demo").set_version("0.4.2")]
324         );
325     }
326 
327     #[test]
328     #[cfg(feature = "test-external-apis")]
test_repo_as_arg_parsing()329     fn test_repo_as_arg_parsing() {
330         let github_url = "https://github.com/killercup/cargo-edit/";
331         let args_github = Args {
332             crates: vec![github_url.to_owned()],
333             ..Args::default()
334         };
335         assert_eq!(
336             args_github.parse_dependencies().unwrap(),
337             vec![Dependency::new("cargo-edit").set_git(github_url, None)]
338         );
339 
340         let gitlab_url = "https://gitlab.com/Polly-lang/Polly.git";
341         let args_gitlab = Args {
342             crates: vec![gitlab_url.to_owned()],
343             ..Args::default()
344         };
345         assert_eq!(
346             args_gitlab.parse_dependencies().unwrap(),
347             vec![Dependency::new("polly").set_git(gitlab_url, None)]
348         );
349     }
350 
351     #[test]
test_path_as_arg_parsing()352     fn test_path_as_arg_parsing() {
353         let self_path = ".";
354         let args_path = Args {
355             crates: vec![self_path.to_owned()],
356             ..Args::default()
357         };
358         assert_eq!(
359             args_path.parse_dependencies().unwrap(),
360             vec![Dependency::new("cargo-edit").set_path(self_path)]
361         );
362     }
363 }
364