diff --git a/Cargo.toml b/Cargo.toml index 1c77b43ded1..a998934a8e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ tar = { version = "0.4.15", default-features = false } tempfile = "3.0" termcolor = "1.0" toml = "0.4.2" +toml_edit = "0.1.1" url = "1.1" clap = "2.31.2" unicode-width = "0.1.5" diff --git a/src/bin/cargo/commands/add.rs b/src/bin/cargo/commands/add.rs new file mode 100644 index 00000000000..49f4572b4d4 --- /dev/null +++ b/src/bin/cargo/commands/add.rs @@ -0,0 +1,59 @@ +use command_prelude::*; + +use cargo::ops; + +use super::install; + +pub fn cli() -> App { + subcommand("add") + .about("Add a new dependency") + .arg(Arg::with_name("crate").empty_values(false).multiple(true)) + .arg( + opt("version", "Specify a version to add from crates.io") + .alias("vers") + .value_name("VERSION"), + ) + .arg(opt("git", "Git URL to add the specified crate from").value_name("URL")) + .arg(opt("branch", "Branch to use when add from git").value_name("BRANCH")) + .arg(opt("tag", "Tag to use when add from git").value_name("TAG")) + .arg(opt("rev", "Specific commit to use when adding from git").value_name("SHA")) + .arg(opt("path", "Filesystem path to local crate to add").value_name("PATH")) + .after_help( + "\ +This command allows you to add a dependency to a Cargo.toml manifest file. If is a github +or gitlab repository URL, or a local path, `cargo add` will try to automatically get the crate name +and set the appropriate `--git` or `--path` value. + +Please note that Cargo treats versions like \"1.2.3\" as \"^1.2.3\" (and that \"^1.2.3\" is specified +as \">=1.2.3 and <2.0.0\"). By default, `cargo add` will use this format, as it is the one that the +crates.io registry suggests. One goal of `cargo add` is to prevent you from using wildcard +dependencies (version set to \"*\").", + ) +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + let compile_opts = args.compile_options(config, CompileMode::Build)?; + + println!("cargo add subcommand executed"); + + let krates = args.values_of("crate") + .unwrap_or_default() + .collect::>(); + + println!("crate {:?}", krates); + + let (_from_cwd, source) = install::get_source_id(&config, &args, &krates)?; + + let version = args.value_of("version"); + + ops::add( + &ws, + krates, + &source, + version, + &compile_opts, + )?; + + Ok(()) +} diff --git a/src/bin/cargo/commands/install.rs b/src/bin/cargo/commands/install.rs index 6f6fd627648..e347a58a1b7 100644 --- a/src/bin/cargo/commands/install.rs +++ b/src/bin/cargo/commands/install.rs @@ -86,8 +86,29 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { .unwrap_or_default() .collect::>(); - let mut from_cwd = false; + let (from_cwd, source) = get_source_id(&config, &args, &krates)?; + + let version = args.value_of("version"); + let root = args.value_of("root"); + + if args.is_present("list") { + ops::install_list(root, config)?; + } else { + ops::install( + root, + krates, + &source, + from_cwd, + version, + &compile_opts, + args.is_present("force"), + )?; + } + Ok(()) +} +pub fn get_source_id(config: &Config, args: &ArgMatches, krates: &Vec<&str>) -> Result<(bool, SourceId), CliError> { + let mut from_cwd = false; let source = if let Some(url) = args.value_of("git") { let url = url.to_url()?; let gitref = if let Some(branch) = args.value_of("branch") { @@ -108,22 +129,5 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { } else { SourceId::crates_io(config)? }; - - let version = args.value_of("version"); - let root = args.value_of("root"); - - if args.is_present("list") { - ops::install_list(root, config)?; - } else { - ops::install( - root, - krates, - &source, - from_cwd, - version, - &compile_opts, - args.is_present("force"), - )?; - } - Ok(()) + Ok((from_cwd, source)) } diff --git a/src/bin/cargo/commands/mod.rs b/src/bin/cargo/commands/mod.rs index 057dc2f07b4..c0413044c75 100644 --- a/src/bin/cargo/commands/mod.rs +++ b/src/bin/cargo/commands/mod.rs @@ -2,6 +2,7 @@ use command_prelude::*; pub fn builtin() -> Vec { vec![ + add::cli(), bench::cli(), build::cli(), check::cli(), @@ -37,6 +38,7 @@ pub fn builtin() -> Vec { pub fn builtin_exec(cmd: &str) -> Option CliResult> { let f = match cmd { + "add" => add::exec, "bench" => bench::exec, "build" => build::exec, "check" => check::exec, @@ -72,6 +74,7 @@ pub fn builtin_exec(cmd: &str) -> Option CliResu Some(f) } +pub mod add; pub mod bench; pub mod build; pub mod check; diff --git a/src/cargo/lib.rs b/src/cargo/lib.rs index 6792ba12c38..4b7644c0c3d 100644 --- a/src/cargo/lib.rs +++ b/src/cargo/lib.rs @@ -58,6 +58,7 @@ extern crate tar; extern crate tempfile; extern crate termcolor; extern crate toml; +extern crate toml_edit; extern crate unicode_width; extern crate url; diff --git a/src/cargo/ops/cargo_add.rs b/src/cargo/ops/cargo_add.rs new file mode 100644 index 00000000000..de9dc442b1c --- /dev/null +++ b/src/cargo/ops/cargo_add.rs @@ -0,0 +1,104 @@ +use core::{SourceId, Workspace}; + +use ops::{self, cargo_install}; +use sources::{GitSource, SourceConfigMap}; +use util::errors::{CargoResult}; +use util::toml; + +pub fn add( + ws: &Workspace, + krates: Vec<&str>, + source_id: &SourceId, + vers: Option<&str>, + opts: &ops::CompileOptions, +) -> CargoResult<()> { + let cwd = ws.config().cwd(); + println!("cwd is {:?}", cwd); + + let map = SourceConfigMap::new(opts.config)?; + + let manifest_path = Some(toml::manifest::find(&Some(cwd.to_path_buf()))?); + let mut manifest = toml::manifest::Manifest::open(&manifest_path)?; + + let mut needs_update = true; + for krate in krates { + add_one( + &map, + krate, + source_id, + vers, + opts, + needs_update, + &mut manifest, + )?; + needs_update = false; + } + + let mut file = toml::manifest::Manifest::find_file(&manifest_path)?; + manifest.write_to_file(&mut file)?; + + Ok(()) +} + +fn add_one( + map: &SourceConfigMap, + krate: &str, + source_id: &SourceId, + vers: Option<&str>, + opts: &ops::CompileOptions, + needs_update: bool, + manifest: &mut toml::manifest::Manifest, + ) -> CargoResult<()> { + let (pkg, _source) = if source_id.is_git() { + cargo_install::select_pkg( + GitSource::new(source_id, opts.config)?, + Some(krate), + vers, + opts.config, + needs_update, + &mut |_| { + bail!( + "must specify a crate to install from \ + crates.io, or use --path or --git to \ + specify alternate source" + ) + }, + )? + } else { + cargo_install::select_pkg( + map.load(source_id)?, + Some(krate), + vers, + opts.config, + needs_update, + &mut |_| { + bail!( + "must specify a crate to install from \ + crates.io, or use --path or --git to \ + specify alternate source" + ) + }, + )? + }; + println!("pkg {:?}", pkg); + let package_id = pkg.package_id(); + println!("package_id is {:?}", package_id); + + let mut dependency = toml::dependency::Dependency::new(&krate) + .set_version(&format!("{}", package_id.version())); + + if source_id.is_git() { + dependency = dependency.set_path(&format!("{}", package_id.source_id())); + } + + println!("is git {}", source_id.is_git()); + println!("dependency is {:?}", dependency); + + manifest.insert_into_table(&get_section(), &dependency)?; + + Ok(()) +} + +fn get_section() -> Vec { + vec!["dependencies".to_owned()] +} diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs index 60e6c30ed8d..8ca81fe9d7a 100644 --- a/src/cargo/ops/cargo_install.rs +++ b/src/cargo/ops/cargo_install.rs @@ -416,7 +416,7 @@ fn path_source<'a>(source_id: &SourceId, config: &'a Config) -> CargoResult( +pub fn select_pkg<'a, T>( mut source: T, name: Option<&str>, vers: Option<&str>, diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 9c09f14f5ed..df57157cd80 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -1,3 +1,4 @@ +pub use self::cargo_add::{add}; pub use self::cargo_clean::{clean, CleanOptions}; pub use self::cargo_compile::{compile, compile_with_exec, compile_ws, CompileOptions}; pub use self::cargo_compile::{CompileFilter, FilterRule, Packages}; @@ -23,6 +24,7 @@ pub use self::resolve::{add_overrides, get_resolved_packages, resolve_with_previ pub use self::cargo_output_metadata::{output_metadata, ExportInfo, OutputMetadataOptions}; pub use self::fix::{fix, FixOptions, fix_maybe_exec_rustc}; +mod cargo_add; mod cargo_clean; mod cargo_compile; mod cargo_doc; diff --git a/src/cargo/util/toml/dependency.rs b/src/cargo/util/toml/dependency.rs new file mode 100644 index 00000000000..c40878b38a8 --- /dev/null +++ b/src/cargo/util/toml/dependency.rs @@ -0,0 +1,139 @@ +use toml_edit; + +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +enum DependencySource { + Version { + version: Option, + path: Option, + }, + Git(String), +} + +/// A dependency handled by Cargo +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +pub struct Dependency { + /// The name of the dependency (as it is set in its `Cargo.toml` and known to crates.io) + pub name: String, + optional: bool, + source: DependencySource, +} + +impl Default for Dependency { + fn default() -> Dependency { + Dependency { + name: "".into(), + optional: false, + source: DependencySource::Version { + version: None, + path: None, + }, + } + } +} + +impl Dependency { + /// Create a new dependency with a name + pub fn new(name: &str) -> Dependency { + Dependency { + name: name.into(), + ..Dependency::default() + } + } + + /// Set dependency to a given version + pub fn set_version(mut self, version: &str) -> Dependency { + let old_source = self.source; + let old_path = match old_source { + DependencySource::Version { path, .. } => path, + _ => None, + }; + self.source = DependencySource::Version { + version: Some(version.into()), + path: old_path, + }; + self + } + + /// Set dependency to a given repository + pub fn set_git(mut self, repo: &str) -> Dependency { + self.source = DependencySource::Git(repo.into()); + self + } + + /// Set dependency to a given path + pub fn set_path(mut self, path: &str) -> Dependency { + let old_source = self.source; + let old_version = match old_source { + DependencySource::Version { version, .. } => version, + _ => None, + }; + self.source = DependencySource::Version { + version: old_version, + path: Some(path.into()), + }; + self + } + + /// Set whether the dependency is optional + pub fn set_optional(mut self, opt: bool) -> Dependency { + self.optional = opt; + self + } + + /// Get version of dependency + pub fn version(&self) -> Option<&str> { + if let DependencySource::Version { + version: Some(ref version), + .. + } = self.source + { + Some(version) + } else { + None + } + } + + /// Convert dependency to TOML + /// + /// Returns a tuple with the dependency's name and either the version as a `String` + /// or the path/git repository as an `InlineTable`. + /// (If the dependency is set as `optional`, an `InlineTable` is returned in any case.) + pub fn to_toml(&self) -> (String, toml_edit::Item) { + let data: toml_edit::Item = match (self.optional, self.source.clone()) { + // Extra short when version flag only + ( + false, + DependencySource::Version { + version: Some(v), + path: None, + }, + ) => toml_edit::value(v), + // Other cases are represented as an inline table + (optional, source) => { + let mut data = toml_edit::InlineTable::default(); + + match source { + DependencySource::Version { version, path } => { + if let Some(v) = version { + data.get_or_insert("version", v); + } + if let Some(p) = path { + data.get_or_insert("path", p); + } + } + DependencySource::Git(v) => { + data.get_or_insert("git", v); + } + } + if self.optional { + data.get_or_insert("optional", optional); + } + + data.fmt(); + toml_edit::value(toml_edit::Value::InlineTable(data)) + } + }; + + (self.name.clone(), data) + } +} diff --git a/src/cargo/util/toml/manifest.rs b/src/cargo/util/toml/manifest.rs new file mode 100644 index 00000000000..fd265491d2c --- /dev/null +++ b/src/cargo/util/toml/manifest.rs @@ -0,0 +1,493 @@ +use std::fs::{self, File, OpenOptions}; +use std::io::{Read, Write}; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::{env, str}; + +use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; +use toml_edit; + +use util::errors::*; +use util::toml::dependency::Dependency; + +const MANIFEST_FILENAME: &str = "Cargo.toml"; + +/// A Cargo manifest +#[derive(Debug, Clone)] +pub struct Manifest { + /// Manifest contents as TOML data + pub data: toml_edit::Document, +} + +/// If a manifest is specified, return that one, otherwise perform a manifest search starting from +/// the current directory. +/// If a manifest is specified, return that one. If a path is specified, perform a manifest search +/// starting from there. If nothing is specified, start searching from the current directory +/// (`cwd`). +pub fn find(specified: &Option) -> CargoResult { + match *specified { + Some(ref path) + if fs::metadata(&path) + .chain_err(|| "Failed to get cargo file metadata")? + .is_file() => + { + Ok(path.to_owned()) + } + Some(ref path) => search(path), + None => search(&env::current_dir().chain_err(|| "Failed to get current directory")?), + } +} + +/// Search for Cargo.toml in this directory and recursively up the tree until one is found. +fn search(dir: &Path) -> CargoResult { + let manifest = dir.join(MANIFEST_FILENAME); + + if fs::metadata(&manifest).is_ok() { + Ok(manifest) + } else { + dir.parent() + .ok_or_else(|| format_err!("Unable to find Cargo.toml")) + .and_then(|dir| search(dir)) + } +} + +fn merge_inline_table(old_dep: &mut toml_edit::Item, new: &toml_edit::Item) { + for (k, v) in new.as_inline_table() + .expect("expected an inline table") + .iter() + { + old_dep[k] = toml_edit::value(v.clone()); + } +} + +fn str_or_1_len_table(item: &toml_edit::Item) -> bool { + item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false) +} +/// Merge a new dependency into an old entry. See `Dependency::to_toml` for what the format of the +/// new dependency will be. +fn merge_dependencies(old_dep: &mut toml_edit::Item, new: &Dependency) { + assert!(!old_dep.is_none()); + + let new_toml = new.to_toml().1; + + if str_or_1_len_table(old_dep) { + // The old dependency is just a version/git/path. We are safe to overwrite. + *old_dep = new_toml; + } else if old_dep.is_table_like() { + for key in &["version", "path", "git"] { + // remove this key/value pairs + old_dep[key] = toml_edit::Item::None; + } + if let Some(name) = new_toml.as_str() { + old_dep["version"] = toml_edit::value(name); + } else { + merge_inline_table(old_dep, &new_toml); + } + } else { + unreachable!("Invalid old dependency type"); + } + + old_dep.as_inline_table_mut().map(|t| t.fmt()); +} + +/// Print a message if the new dependency version is different from the old one. +fn print_upgrade_if_necessary( + crate_name: &str, + old_dep: &toml_edit::Item, + new_version: &toml_edit::Item, +) -> CargoResult<()> { + let old_version = if str_or_1_len_table(old_dep) { + old_dep.clone() + } else if old_dep.is_table_like() { + let version = old_dep["version"].clone(); + if version.is_none() { + bail!("Missing version field"); + } + version + } else { + unreachable!("Invalid old dependency type") + }; + + if let (Some(old_version), Some(new_version)) = (old_version.as_str(), new_version.as_str()) { + if old_version == new_version { + return Ok(()); + } + let bufwtr = BufferWriter::stdout(ColorChoice::Always); + let mut buffer = bufwtr.buffer(); + buffer + .set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true)) + .chain_err(|| "Failed to set output colour")?; + write!(&mut buffer, " Upgrading ").chain_err(|| "Failed to write upgrade message")?; + buffer + .set_color(&ColorSpec::new()) + .chain_err(|| "Failed to clear output colour")?; + write!( + &mut buffer, + "{} v{} -> v{}\n", + crate_name, old_version, new_version, + ).chain_err(|| "Failed to write upgrade versions")?; + bufwtr + .print(&buffer) + .chain_err(|| "Failed to print upgrade message")?; + } + Ok(()) +} + +impl Manifest { + /// Look for a `Cargo.toml` file + /// + /// Starts at the given path an goes into its parent directories until the manifest file is + /// found. If no path is given, the process's working directory is used as a starting point. + pub fn find_file(path: &Option) -> CargoResult { + find(path).and_then(|path| { + OpenOptions::new() + .read(true) + .write(true) + .open(path) + .chain_err(|| "Failed to find Cargo.toml") + .map_err(CargoError::from) + }) + } + + /// Open the `Cargo.toml` for a path (or the process' `cwd`) + pub fn open(path: &Option) -> CargoResult { + let mut file = Manifest::find_file(path)?; + let mut data = String::new(); + file.read_to_string(&mut data) + .chain_err(|| "Failed to read manifest contents")?; + + data.parse() + .chain_err(|| "Unable to parse Cargo.toml") + .map_err(CargoError::from) + } + + /// Get the specified table from the manifest. + pub fn get_table<'a>(&'a mut self, table_path: &[String]) -> CargoResult<&'a mut toml_edit::Item> { + /// Descend into a manifest until the required table is found. + fn descend<'a>( + input: &'a mut toml_edit::Item, + path: &[String], + ) -> CargoResult<&'a mut toml_edit::Item> { + if let Some(segment) = path.get(0) { + let value = input[&segment].or_insert(toml_edit::table()); + + if value.is_table_like() { + descend(value, &path[1..]) + } else { + bail!("The table `{}` could not be found.", segment); + } + } else { + Ok(input) + } + } + + descend(&mut self.data.root, table_path) + } + + /// Get all sections in the manifest that exist and might contain dependencies. + /// The returned items are always `Table` or `InlineTable`. + pub fn get_sections(&self) -> Vec<(Vec, toml_edit::Item)> { + let mut sections = Vec::new(); + + for dependency_type in &["dev-dependencies", "build-dependencies", "dependencies"] { + // Dependencies can be in the three standard sections... + if self.data[dependency_type].is_table_like() { + sections.push(( + vec![dependency_type.to_string()], + self.data[dependency_type].clone(), + )) + } + + // ... and in `target..(build-/dev-)dependencies`. + let target_sections = self.data + .as_table() + .get("target") + .and_then(toml_edit::Item::as_table_like) + .into_iter() + .flat_map(|t| t.iter()) + .filter_map(|(target_name, target_table)| { + let dependency_table = &target_table[dependency_type]; + dependency_table.as_table_like().map(|_| { + ( + vec![ + "target".to_string(), + target_name.to_string(), + dependency_type.to_string(), + ], + dependency_table.clone(), + ) + }) + }); + + sections.extend(target_sections); + } + + sections + } + + /// Overwrite a file with TOML data. + pub fn write_to_file(&self, file: &mut File) -> CargoResult<()> { + if self.data["package"].is_none() && self.data["project"].is_none() { + ensure!(self.data["workspace"].is_none(), + "Found virtual manifest, but this command requires running against an \ + actual package in this workspace."); + bail!("Cargo.toml missing expected `package` or `project` fields"); + } + + let s = self.data.to_string(); + let new_contents_bytes = s.as_bytes(); + + // We need to truncate the file, otherwise the new contents + // will be mixed up with the old ones. + file.set_len(new_contents_bytes.len() as u64) + .chain_err(|| "Failed to truncate Cargo.toml") + .map_err(CargoError::from)?; + file.write_all(new_contents_bytes) + .chain_err(|| "Failed to write updated Cargo.toml") + .map_err(CargoError::from)?; + Ok(()) + } + + /// Add entry to a Cargo.toml. + pub fn insert_into_table(&mut self, table_path: &[String], dep: &Dependency) -> CargoResult<()> { + let table = self.get_table(table_path)?; + + if table[&dep.name].is_none() { + // insert a new entry + let (ref name, ref mut new_dependency) = dep.to_toml(); + table[name] = new_dependency.clone(); + } else { + // update an existing entry + merge_dependencies(&mut table[&dep.name], dep); + table.as_inline_table_mut().map(|t| t.fmt()); + } + Ok(()) + } + + /// Update an entry in Cargo.toml. + pub fn update_table_entry( + &mut self, + table_path: &[String], + dep: &Dependency, + dry_run: bool, + ) -> CargoResult<()> { + let table = self.get_table(table_path)?; + let new_dep = dep.to_toml().1; + + // If (and only if) there is an old entry, merge the new one in. + if !table[&dep.name].is_none() { + if let Err(e) = print_upgrade_if_necessary(&dep.name, &table[&dep.name], &new_dep) { + eprintln!("Error while displaying upgrade message, {}", e); + } + if !dry_run { + merge_dependencies(&mut table[&dep.name], dep); + table.as_inline_table_mut().map(|t| t.fmt()); + } + } + + Ok(()) + } + + /// Remove entry from a Cargo.toml. + /// + /// # Examples + /// + /// ``` + /// # extern crate cargo_edit; + /// # extern crate toml_edit; + /// # fn main() { + /// use cargo_edit::{Dependency, Manifest}; + /// use toml_edit; + /// + /// let mut manifest = Manifest { data: toml_edit::Document::new() }; + /// let dep = Dependency::new("cargo-edit").set_version("0.1.0"); + /// let _ = manifest.insert_into_table(&vec!["dependencies".to_owned()], &dep); + /// assert!(manifest.remove_from_table("dependencies", &dep.name).is_ok()); + /// assert!(manifest.remove_from_table("dependencies", &dep.name).is_err()); + /// assert!(manifest.data["dependencies"].is_none()); + /// # } + /// ``` + pub fn remove_from_table(&mut self, table: &str, name: &str) -> CargoResult<()> { + ensure!(self.data[table].is_table_like(), + "The table `{}` could not be found.", table); + { + let dep = &mut self.data[table][name]; + if dep.is_none() { + bail!("The dependency `{}` could not be found in `{}`.", name, table); + } + // remove the dependency + *dep = toml_edit::Item::None; + } + + // remove table if empty + if self.data[table].as_table_like().unwrap().is_empty() { + self.data[table] = toml_edit::Item::None; + } + Ok(()) + } + + /// Add multiple dependencies to manifest + pub fn add_deps(&mut self, table: &[String], deps: &[Dependency]) -> CargoResult<()> { + deps.iter() + .map(|dep| self.insert_into_table(table, dep)) + .collect::>>()?; + + Ok(()) + } +} + +impl str::FromStr for Manifest { + type Err = CargoError; + + /// Read manifest data from string + fn from_str(input: &str) -> ::std::result::Result { + let d: toml_edit::Document = input.parse().chain_err(|| "Manifest not valid TOML")?; + + Ok(Manifest { data: d }) + } +} + +/// A Cargo manifest that is available locally. +#[derive(Debug)] +pub struct LocalManifest { + /// Path to the manifest + path: PathBuf, + /// Manifest contents + manifest: Manifest, +} + +impl Deref for LocalManifest { + type Target = Manifest; + + fn deref(&self) -> &Manifest { + &self.manifest + } +} + +impl LocalManifest { + /// Construct a `LocalManifest`. If no path is provided, make an educated guess as to which one + /// the user means. + pub fn find(path: &Option) -> CargoResult { + let path = find(path)?; + Self::try_new(&path) + } + + /// Construct the `LocalManifest` corresponding to the `Path` provided. + pub fn try_new(path: &Path) -> CargoResult { + let path = path.to_path_buf(); + Ok(LocalManifest { + manifest: Manifest::open(&Some(path.clone()))?, + path: path, + }) + } + + /// Get the `File` corresponding to this manifest. + fn get_file(&self) -> CargoResult { + Manifest::find_file(&Some(self.path.clone())) + } + + /// Instruct this manifest to upgrade a single dependency. If this manifest does not have that + /// dependency, it does nothing. + pub fn upgrade(&mut self, dependency: &Dependency, dry_run: bool) -> CargoResult<()> { + for (table_path, table) in self.get_sections() { + let table_like = table.as_table_like().expect("Unexpected non-table"); + for (name, _old_value) in table_like.iter() { + if name == dependency.name { + self.manifest + .update_table_entry(&table_path, dependency, dry_run)?; + } + } + } + + let mut file = self.get_file()?; + self.write_to_file(&mut file) + .chain_err(|| "Failed to write new manifest contents") + .map_err(CargoError::from) + } +} + +#[cfg(test)] +mod tests { + use util::toml::dependency::Dependency; + use super::*; + use toml_edit; + + #[test] + fn add_remove_dependency() { + let mut manifest = Manifest { + data: toml_edit::Document::new(), + }; + let clone = manifest.clone(); + let dep = Dependency::new("cargo-edit").set_version("0.1.0"); + let _ = manifest.insert_into_table(&["dependencies".to_owned()], &dep); + assert!( + manifest + .remove_from_table("dependencies", &dep.name) + .is_ok() + ); + assert_eq!(manifest.data.to_string(), clone.data.to_string()); + } + + #[test] + fn update_dependency() { + let mut manifest = Manifest { + data: toml_edit::Document::new(), + }; + let dep = Dependency::new("cargo-edit").set_version("0.1.0"); + manifest + .insert_into_table(&["dependencies".to_owned()], &dep) + .unwrap(); + + let new_dep = Dependency::new("cargo-edit").set_version("0.2.0"); + manifest + .update_table_entry(&["dependencies".to_owned()], &new_dep, false) + .unwrap(); + } + + #[test] + fn update_wrong_dependency() { + let mut manifest = Manifest { + data: toml_edit::Document::new(), + }; + let dep = Dependency::new("cargo-edit").set_version("0.1.0"); + manifest + .insert_into_table(&["dependencies".to_owned()], &dep) + .unwrap(); + let original = manifest.clone(); + + let new_dep = Dependency::new("wrong-dep").set_version("0.2.0"); + manifest + .update_table_entry(&["dependencies".to_owned()], &new_dep, false) + .unwrap(); + + assert_eq!(manifest.data.to_string(), original.data.to_string()); + } + + #[test] + fn remove_dependency_no_section() { + let mut manifest = Manifest { + data: toml_edit::Document::new(), + }; + let dep = Dependency::new("cargo-edit").set_version("0.1.0"); + assert!( + manifest + .remove_from_table("dependencies", &dep.name) + .is_err() + ); + } + + #[test] + fn remove_dependency_non_existent() { + let mut manifest = Manifest { + data: toml_edit::Document::new(), + }; + let dep = Dependency::new("cargo-edit").set_version("0.1.0"); + let other_dep = Dependency::new("other-dep").set_version("0.1.0"); + let _ = manifest.insert_into_table(&["dependencies".to_owned()], &other_dep); + assert!( + manifest + .remove_from_table("dependencies", &dep.name) + .is_err() + ); + } +} diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 9d83668569a..a73b12b5eab 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -23,6 +23,10 @@ use util::errors::{CargoError, CargoResult, CargoResultExt}; use util::paths; use util::{self, Config, ToUrl}; +// mod err; +pub mod dependency; +pub mod manifest; + mod targets; use self::targets::targets;