Skip to content

Commit

Permalink
Add '--to-lockfile' flag to cargo-upgrade
Browse files Browse the repository at this point in the history
When this flag is present, all dependencies in 'Cargo.toml'
are upgraded to the locked version as recorded in 'Cargo.lock'.

Multiple manifests in a workspace may request the same dependency
at different versions. The version of each dependency is
updated to the corresponding semver compatible version in the
lockfile.
  • Loading branch information
tofay committed Sep 29, 2019
1 parent 8f8028e commit a769f8a
Show file tree
Hide file tree
Showing 13 changed files with 2,003 additions and 37 deletions.
2 changes: 1 addition & 1 deletion src/bin/add/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ use cargo_edit::{find, update_registry_index, Dependency, Manifest};
use std::io::Write;
use std::process;
use structopt::StructOpt;
use toml_edit::Item as TomlItem;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
use toml_edit::Item as TomlItem;

mod args;

Expand Down
131 changes: 101 additions & 30 deletions src/bin/upgrade/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ Dev, build, and all target dependencies will also be upgraded. Only dependencies
supported. Git/path dependencies will be ignored.
All packages in the workspace will be upgraded if the `--all` flag is supplied. The `--all` flag may
be supplied in the presence of a virtual manifest."
be supplied in the presence of a virtual manifest.
If the '--to-lockfile' flag is supplied, all dependencies will be upraged to the currently locked
version as recorded in the local lock file. The local lock file must be present and up to date if
this flag is passed."
)]
Upgrade(Args),
}
Expand Down Expand Up @@ -82,11 +86,42 @@ struct Args {
/// Run without accessing the network
#[structopt(long = "offline")]
pub offline: bool,

/// Upgrade all packages to the version in the lockfile.
#[structopt(long = "to-lockfile", conflicts_with = "dependency")]
pub to_lockfile: bool,
}

/// A collection of manifests.
struct Manifests(Vec<(LocalManifest, cargo_metadata::Package)>);

/// Helper function to check whether a `cargo_metadata::Dependency` is a version dependency.
fn is_version_dep(dependency: &cargo_metadata::Dependency) -> bool {
match dependency.source {
// This is the criterion cargo uses (in `SourceId::from_url`) to decide whether a
// dependency has the 'registry' kind.
Some(ref s) => s.splitn(2, '+').next() == Some("registry"),
_ => false,
}
}

fn dry_run_message() -> Result<()> {
let bufwtr = BufferWriter::stdout(ColorChoice::Always);
let mut buffer = bufwtr.buffer();
buffer
.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)).set_bold(true))
.chain_err(|| "Failed to set output colour")?;
write!(&mut buffer, "Starting dry run. ").chain_err(|| "Failed to write dry run message")?;
buffer
.set_color(&ColorSpec::new())
.chain_err(|| "Failed to clear output colour")?;
writeln!(&mut buffer, "Changes will not be saved.")
.chain_err(|| "Failed to write dry run message")?;
bufwtr
.print(&buffer)
.chain_err(|| "Failed to print dry run message")
}

impl Manifests {
/// Get all manifests in the workspace.
fn get_all(manifest_path: &Option<PathBuf>) -> Result<Self> {
Expand Down Expand Up @@ -143,16 +178,6 @@ impl Manifests {
/// Get the the combined set of dependencies to upgrade. If the user has specified
/// per-dependency desired versions, extract those here.
fn get_dependencies(&self, only_update: Vec<String>) -> Result<DesiredUpgrades> {
/// Helper function to check whether a `cargo_metadata::Dependency` is a version dependency.
fn is_version_dep(dependency: &cargo_metadata::Dependency) -> bool {
match dependency.source {
// This is the criterion cargo uses (in `SourceId::from_url`) to decide whether a
// dependency has the 'registry' kind.
Some(ref s) => s.splitn(2, '+').next() == Some("registry"),
_ => false,
}
}

Ok(DesiredUpgrades(if only_update.is_empty() {
// User hasn't asked for any specific dependencies to be upgraded, so upgrade all the
// dependencies.
Expand Down Expand Up @@ -182,21 +207,7 @@ impl Manifests {
/// Upgrade the manifests on disk following the previously-determined upgrade schema.
fn upgrade(self, upgraded_deps: &ActualUpgrades, dry_run: bool) -> Result<()> {
if dry_run {
let bufwtr = BufferWriter::stdout(ColorChoice::Always);
let mut buffer = bufwtr.buffer();
buffer
.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)).set_bold(true))
.chain_err(|| "Failed to set output colour")?;
write!(&mut buffer, "Starting dry run. ")
.chain_err(|| "Failed to write dry run message")?;
buffer
.set_color(&ColorSpec::new())
.chain_err(|| "Failed to clear output colour")?;
writeln!(&mut buffer, "Changes will not be saved.")
.chain_err(|| "Failed to write dry run message")?;
bufwtr
.print(&buffer)
.chain_err(|| "Failed to print dry run message")?;
dry_run_message()?;
}

for (mut manifest, package) in self.0 {
Expand All @@ -209,6 +220,61 @@ impl Manifests {

Ok(())
}

/// Update dependencies in Cargo.toml file(s) to match the corresponding
/// version in Cargo.lock.
fn sync_to_lockfile(self, dry_run: bool) -> Result<()> {
// Get locked dependencies. For workspaces with multiple Cargo.toml
// files, there is only a single lockfile, so it suffices to get
// metadata for any one of Cargo.toml files.
let (manifest, _package) =
self.0.iter().next().ok_or_else(|| {
ErrorKind::CargoEditLib(::cargo_edit::ErrorKind::InvalidCargoConfig)
})?;
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.manifest_path(manifest.path.clone());
cmd.other_options(vec!["--locked".to_string()]);

let result = cmd
.exec()
.map_err(|e| Error::from(e.compat()).chain_err(|| "Invalid manifest"))?;

let locked = result
.packages
.into_iter()
.filter(|p| p.source.is_some()) // Source is none for local packages
.collect::<Vec<_>>();

if dry_run {
dry_run_message()?;
}

for (mut manifest, package) in self.0 {
println!("{}:", package.name);

// Upgrade the manifests one at a time, as multiple manifests may
// request the same dependency at differing versions.
for (name, version) in package
.dependencies
.clone()
.into_iter()
.filter(is_version_dep)
.filter_map(|d| {
for p in &locked {
// The requested depenency may be present in the lock file with different versions,
// but only one will be semver-compatible with requested version.
if d.name == p.name && d.req.matches(&p.version) {
return Some((d.name, p.version.to_string()));
}
}
None
})
{
manifest.upgrade(&Dependency::new(&name).set_version(&version), dry_run)?;
}
}
Ok(())
}
}

/// The set of dependencies to be upgraded, alongside desired versions, if specified by the user.
Expand Down Expand Up @@ -255,6 +321,7 @@ fn process(args: Args) -> Result<()> {
all,
allow_prerelease,
dry_run,
to_lockfile,
..
} = args;

Expand All @@ -268,12 +335,16 @@ fn process(args: Args) -> Result<()> {
Manifests::get_local_one(&manifest_path)
}?;

let existing_dependencies = manifests.get_dependencies(dependency)?;
if to_lockfile {
manifests.sync_to_lockfile(dry_run)
} else {
let existing_dependencies = manifests.get_dependencies(dependency)?;

let upgraded_dependencies =
existing_dependencies.get_upgraded(allow_prerelease, &find(&manifest_path)?)?;
let upgraded_dependencies =
existing_dependencies.get_upgraded(allow_prerelease, &find(&manifest_path)?)?;

manifests.upgrade(&upgraded_dependencies, dry_run)
manifests.upgrade(&upgraded_dependencies, dry_run)
}
}

fn main() {
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ impl str::FromStr for Manifest {
#[derive(Debug)]
pub struct LocalManifest {
/// Path to the manifest
path: PathBuf,
pub path: PathBuf,
/// Manifest contents
manifest: Manifest,
}
Expand Down
5 changes: 3 additions & 2 deletions tests/cargo-add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1101,7 +1101,9 @@ fn adds_sorted_dependencies() {

// and all the dependencies in the output get sorted
let toml = get_toml(&manifest);
assert_eq!(toml.to_string(), r#"[package]
assert_eq!(
toml.to_string(),
r#"[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
Expand All @@ -1112,4 +1114,3 @@ toml_edit = "0.1.5"
"#
);
}

37 changes: 36 additions & 1 deletion tests/cargo-upgrade.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#[macro_use]
extern crate pretty_assertions;

use std::fs;
use std::{fs, path::Path};

mod utils;
use crate::utils::{clone_out_test, execute_command, get_command_path, get_toml};
Expand Down Expand Up @@ -38,6 +38,7 @@ pub fn copy_workspace_test() -> (tempdir::TempDir, String, Vec<String>) {

let root_manifest_path = copy_in(".", "Cargo.toml");
copy_in(".", "dummy.rs");
copy_in(".", "Cargo.lock");

let workspace_manifest_paths = ["one", "two", "implicit/three", "explicit/four"]
.iter()
Expand Down Expand Up @@ -309,6 +310,40 @@ For more information try --help ",
.unwrap();
}

// Verify that an upgraded Cargo.toml matches what we expect.
#[test]
fn upgrade_to_lockfile() {
let (tmpdir, manifest) = clone_out_test("tests/fixtures/upgrade/Cargo.toml.source");
fs::copy(
Path::new("tests/fixtures/upgrade/Cargo.lock"),
tmpdir.path().join("Cargo.lock"),
)
.unwrap_or_else(|err| panic!("could not copy test lock file: {}", err));;
execute_command(&["upgrade", "--to-lockfile"], &manifest);

let upgraded = get_toml(&manifest);
let target = get_toml("tests/fixtures/upgrade/Cargo.toml.lockfile_target");

assert_eq!(target.to_string(), upgraded.to_string());
}

#[test]
fn upgrade_workspace_to_lockfile() {
let (tmpdir, root_manifest, _workspace_manifests) = copy_workspace_test();

execute_command(&["upgrade", "--all", "--to-lockfile"], &root_manifest);

// The members one and two both request different, semver incompatible
// versions of rand. Test that both were upgraded correctly.
let one_upgraded = get_toml(tmpdir.path().join("one/Cargo.toml").to_str().unwrap());
let one_target = get_toml("tests/fixtures/workspace/one/Cargo.toml.lockfile_target");
assert_eq!(one_target.to_string(), one_upgraded.to_string());

let two_upgraded = get_toml(tmpdir.path().join("two/Cargo.toml").to_str().unwrap());
let two_target = get_toml("tests/fixtures/workspace/two/Cargo.toml.lockfile_target");
assert_eq!(two_target.to_string(), two_upgraded.to_string());
}

#[test]
#[cfg(feature = "test-external-apis")]
fn upgrade_prints_messages() {
Expand Down
Loading

0 comments on commit a769f8a

Please sign in to comment.