Skip to content

Commit dd1cd3a

Browse files
committed
Clean profile, patch, and replace in cargo remove
After a successful removal of a dependency, clean up the profile, patch, and replace sections to remove all references to the dependency.
1 parent 65b2149 commit dd1cd3a

File tree

23 files changed

+337
-27
lines changed

23 files changed

+337
-27
lines changed

src/bin/cargo/commands/remove.rs

+61-26
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use cargo::core::dependency::DepKind;
2+
use cargo::core::PackageIdSpec;
23
use cargo::core::Workspace;
34
use cargo::ops::cargo_remove::remove;
45
use cargo::ops::cargo_remove::RemoveOptions;
@@ -88,8 +89,8 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
8889
remove(&options)?;
8990

9091
if !dry_run {
91-
// Clean up workspace dependencies
92-
gc_workspace(&workspace, &options.dependencies)?;
92+
// Clean up the workspace
93+
gc_workspace(&workspace)?;
9394

9495
// Reload the workspace since we've changed dependencies
9596
let ws = args.workspace(config)?;
@@ -121,8 +122,9 @@ fn parse_section(args: &ArgMatches) -> DepTable {
121122
table
122123
}
123124

124-
/// Clean up workspace dependencies which no longer have a reference to them.
125-
fn gc_workspace(workspace: &Workspace<'_>, dependencies: &[String]) -> CargoResult<()> {
125+
/// Clean up the workspace.dependencies and replace sections of a root manifest
126+
/// by removing dependencies which no longer have a reference to them.
127+
fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
126128
let mut manifest: toml_edit::Document =
127129
cargo_util::paths::read(workspace.root_manifest())?.parse()?;
128130

@@ -131,39 +133,72 @@ fn gc_workspace(workspace: &Workspace<'_>, dependencies: &[String]) -> CargoResu
131133
.map(|p| LocalManifest::try_new(p.manifest_path()))
132134
.collect::<CargoResult<Vec<_>>>()?;
133135

134-
for dep in dependencies {
135-
if !dep_in_workspace(dep, &members) {
136-
remove_workspace_dep(dep, &mut manifest);
137-
}
138-
}
136+
gc_workspace_dependencies(&mut manifest, &members);
137+
gc_replace(&mut manifest, &members);
139138

140139
cargo_util::paths::write(workspace.root_manifest(), manifest.to_string().as_bytes())?;
141140

142141
Ok(())
143142
}
144143

145-
/// Get whether or not a dependency is depended upon in a workspace.
146-
fn dep_in_workspace(dep: &str, members: &[LocalManifest]) -> bool {
147-
members.iter().any(|manifest| {
148-
manifest.get_sections().iter().any(|(_, table)| {
149-
table
150-
.as_table_like()
151-
.unwrap()
152-
.get(dep)
153-
.and_then(|t| t.get("workspace"))
154-
.and_then(|v| v.as_bool())
155-
.unwrap_or(false)
144+
/// Clean up the workspace.dependencies section in a root manifest.
145+
fn gc_workspace_dependencies(manifest: &mut toml_edit::Document, members: &[LocalManifest]) {
146+
let in_workspace = |dep: &str| {
147+
members.iter().any(|manifest| {
148+
manifest.get_sections().iter().any(|(_, table)| {
149+
let dep_item = table.as_table_like().unwrap().get(dep);
150+
dep_item
151+
.and_then(|t| t.get("workspace"))
152+
.and_then(|i| i.as_bool())
153+
.unwrap_or(false)
154+
})
156155
})
157-
})
158-
}
156+
};
159157

160-
/// Remove a dependency from a workspace manifest.
161-
fn remove_workspace_dep(dep: &str, ws_manifest: &mut toml_edit::Document) {
162-
if let Some(toml_edit::Item::Table(table)) = ws_manifest
158+
if let Some(toml_edit::Item::Table(table)) = manifest
163159
.get_mut("workspace")
164160
.and_then(|t| t.get_mut("dependencies"))
165161
{
166162
table.set_implicit(true);
167-
table.remove(dep);
163+
164+
for (key, item) in table.iter_mut() {
165+
if !in_workspace(key.get()) {
166+
*item = toml_edit::Item::None;
167+
}
168+
}
169+
}
170+
}
171+
172+
/// Clean up the replace section in a root manifest.
173+
fn gc_replace(manifest: &mut toml_edit::Document, members: &[LocalManifest]) {
174+
let in_workspace = |dep: &str, version: &Option<&semver::Version>| {
175+
members.iter().any(|manifest| {
176+
let mut deps = manifest
177+
.get_dependency_versions(dep)
178+
.filter_map(|(_, d)| d.ok())
179+
.filter(|d| d.name == dep);
180+
181+
if let Some(version) = version {
182+
deps.any(|d| {
183+
d.version()
184+
.and_then(|v| semver::VersionReq::parse(v).ok())
185+
.map(|vq| vq.matches(version))
186+
.unwrap_or(false)
187+
})
188+
} else {
189+
deps.next().is_some()
190+
}
191+
})
192+
};
193+
194+
if let Some(toml_edit::Item::Table(table)) = manifest.get_mut("replace") {
195+
table.set_implicit(true);
196+
for (key, item) in table.iter_mut() {
197+
if let Ok(spec) = PackageIdSpec::parse(key.get()) {
198+
if !in_workspace(spec.name().as_str(), &spec.version()) {
199+
*item = toml_edit::Item::None;
200+
}
201+
}
202+
}
168203
}
169204
}

src/cargo/util/toml_mut/manifest.rs

+38-1
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ impl LocalManifest {
393393
let explicit_dep_activation = self.is_explicit_dep_activation(dep_key);
394394
let status = self.dep_status(dep_key);
395395

396+
// clean up features
396397
if let Some(toml_edit::Item::Table(feature_table)) =
397398
self.data.as_table_mut().get_mut("features")
398399
{
@@ -409,6 +410,42 @@ impl LocalManifest {
409410
}
410411
}
411412
}
413+
414+
// continue only if the dep is fully removed
415+
if status != DependencyStatus::None {
416+
return;
417+
}
418+
419+
// clean up profile sections
420+
if let Some(toml_edit::Item::Table(profile_table)) = self.data.get_mut("profile") {
421+
profile_table.set_implicit(true);
422+
for (_, item) in profile_table.iter_mut() {
423+
if let toml_edit::Item::Table(profile_table) = item {
424+
profile_table.set_implicit(true);
425+
if let Some(toml_edit::Item::Table(package_table)) =
426+
profile_table.get_mut("package")
427+
{
428+
package_table.set_implicit(true);
429+
package_table.remove(dep_key);
430+
}
431+
}
432+
}
433+
}
434+
435+
// clean up patch sections
436+
if let Some(toml_edit::Item::Table(patch_table)) = self.data.get_mut("patch") {
437+
patch_table.set_implicit(true);
438+
for (_, item) in patch_table.iter_mut() {
439+
if let toml_edit::Item::Table(table) = item {
440+
table.set_implicit(true);
441+
for (key, item) in table.iter_mut() {
442+
if key.get() == dep_key {
443+
*item = toml_edit::Item::None;
444+
}
445+
}
446+
}
447+
}
448+
}
412449
}
413450

414451
fn is_explicit_dep_activation(&self, dep_key: &str) -> bool {
@@ -500,7 +537,7 @@ fn fix_feature_activations(
500537
})
501538
.collect();
502539

503-
// Remove found idx in revers order so we don't invalidate the idx.
540+
// Remove found idx in reverse order so we don't invalidate the idx.
504541
for idx in remove_list.iter().rev() {
505542
feature_values.remove(*idx);
506543
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use cargo_test_support::basic_manifest;
2+
use cargo_test_support::compare::assert_ui;
3+
use cargo_test_support::curr_dir;
4+
use cargo_test_support::git;
5+
use cargo_test_support::project;
6+
use cargo_test_support::CargoCommand;
7+
8+
use crate::cargo_remove::init_registry;
9+
10+
#[cargo_test]
11+
fn case() {
12+
init_registry();
13+
14+
let git_project1 = git::new("bar1", |project| {
15+
project
16+
.file("Cargo.toml", &basic_manifest("bar", "0.1.0"))
17+
.file("src/lib.rs", "")
18+
})
19+
.url();
20+
21+
let git_project2 = git::new("bar2", |project| {
22+
project
23+
.file("Cargo.toml", &basic_manifest("bar", "0.1.0"))
24+
.file("src/lib.rs", "")
25+
})
26+
.url();
27+
28+
let in_project = project()
29+
.file(
30+
"Cargo.toml",
31+
&format!(
32+
"[package]\n\
33+
name = \"my-project\"\n\
34+
version = \"0.1.0\"\n\
35+
\n\
36+
[dependencies]\n\
37+
bar = {{ git = \"{git_project1}\" }}\n\
38+
\n\
39+
[patch.\"{git_project1}\"]\n\
40+
bar = {{ git = \"{git_project2}\" }}\n",
41+
),
42+
)
43+
.file("src/lib.rs", "")
44+
.build();
45+
46+
snapbox::cmd::Command::cargo_ui()
47+
.arg("remove")
48+
.args(["bar"])
49+
.current_dir(&in_project.root())
50+
.assert()
51+
.success()
52+
.stdout_matches_path(curr_dir!().join("stdout.log"))
53+
.stderr_matches_path(curr_dir!().join("stderr.log"));
54+
55+
assert_ui().subset_matches(curr_dir!().join("out"), &in_project.root());
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[package]
2+
name = "my-project"
3+
version = "0.1.0"

tests/testsuite/cargo_remove/gc_patch/out/src/lib.rs

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Removing bar from dependencies

tests/testsuite/cargo_remove/gc_patch/stdout.log

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[package]
2+
name = "cargo-remove-test-fixture"
3+
version = "0.1.0"
4+
5+
[[bin]]
6+
name = "main"
7+
path = "src/main.rs"
8+
9+
[build-dependencies]
10+
semver = "0.1.0"
11+
12+
[dependencies]
13+
docopt = "0.6"
14+
rustc-serialize = "0.4"
15+
semver = "0.1"
16+
toml = "0.1"
17+
clippy = "0.4"
18+
19+
[dev-dependencies]
20+
regex = "0.1.1"
21+
serde = "1.0.90"
22+
docopt = "0.6"
23+
24+
[features]
25+
std = ["serde/std", "semver/std"]
26+
27+
[profile.dev.package.docopt]
28+
opt-level = 3
29+
30+
[profile.dev.package.toml]
31+
opt-level = 3
32+
33+
[profile.release.package.toml]
34+
opt-level = 1
35+
overflow-checks = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::curr_dir;
3+
use cargo_test_support::CargoCommand;
4+
use cargo_test_support::Project;
5+
6+
use crate::cargo_remove::init_registry;
7+
8+
#[cargo_test]
9+
fn case() {
10+
init_registry();
11+
let project = Project::from_template(curr_dir!().join("in"));
12+
let project_root = project.root();
13+
let cwd = &project_root;
14+
15+
snapbox::cmd::Command::cargo_ui()
16+
.arg("remove")
17+
.args(["toml"])
18+
.current_dir(cwd)
19+
.assert()
20+
.success()
21+
.stdout_matches_path(curr_dir!().join("stdout.log"))
22+
.stderr_matches_path(curr_dir!().join("stderr.log"));
23+
24+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "cargo-remove-test-fixture"
3+
version = "0.1.0"
4+
5+
[[bin]]
6+
name = "main"
7+
path = "src/main.rs"
8+
9+
[build-dependencies]
10+
semver = "0.1.0"
11+
12+
[dependencies]
13+
docopt = "0.6"
14+
rustc-serialize = "0.4"
15+
semver = "0.1"
16+
clippy = "0.4"
17+
18+
[dev-dependencies]
19+
regex = "0.1.1"
20+
serde = "1.0.90"
21+
docopt = "0.6"
22+
23+
[features]
24+
std = ["serde/std", "semver/std"]
25+
26+
[profile.dev.package.docopt]
27+
opt-level = 3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Removing toml from dependencies
2+
Updating `dummy-registry` index

tests/testsuite/cargo_remove/gc_profile/stdout.log

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[workspace]
2+
members = [ "my-package" ]
3+
4+
[replace]
5+
"toml:0.1.0" = { path = "../toml" }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "my-package"
3+
version = "0.1.0"
4+
5+
[[bin]]
6+
name = "main"
7+
path = "src/main.rs"
8+
9+
[build-dependencies]
10+
semver = "0.1.0"
11+
12+
[dependencies]
13+
docopt = "0.6"
14+
rustc-serialize = "0.4"
15+
semver = "0.1"
16+
toml = "0.1"
17+
clippy = "0.4"
18+
19+
[dev-dependencies]
20+
regex = "0.1.1"
21+
serde = "1.0.90"
22+
docopt = "0.6"
23+
24+
[features]
25+
std = ["serde/std", "semver/std"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)