Skip to content

Commit 2056f8d

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 16b0978 commit 2056f8d

File tree

22 files changed

+458
-30
lines changed

22 files changed

+458
-30
lines changed

src/bin/cargo/commands/remove.rs

Lines changed: 188 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
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;
56
use cargo::ops::resolve_ws;
67
use cargo::util::command_prelude::*;
78
use cargo::util::print_available_packages;
9+
use cargo::util::toml_mut::dependency::Dependency;
10+
use cargo::util::toml_mut::dependency::MaybeWorkspace;
11+
use cargo::util::toml_mut::dependency::Source;
812
use cargo::util::toml_mut::manifest::DepTable;
913
use cargo::util::toml_mut::manifest::LocalManifest;
1014
use cargo::CargoResult;
@@ -86,7 +90,7 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
8690
.get_many::<String>("dependencies")
8791
.expect("required(true)")
8892
.cloned()
89-
.collect();
93+
.collect::<Vec<_>>();
9094

9195
let section = parse_section(args);
9296

@@ -100,8 +104,8 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
100104
remove(&options)?;
101105

102106
if !dry_run {
103-
// Clean up workspace dependencies
104-
gc_workspace(&workspace, &options.dependencies)?;
107+
// Clean up the workspace
108+
gc_workspace(&workspace)?;
105109

106110
// Reload the workspace since we've changed dependencies
107111
let ws = args.workspace(config)?;
@@ -133,49 +137,203 @@ fn parse_section(args: &ArgMatches) -> DepTable {
133137
table
134138
}
135139

136-
/// Clean up workspace dependencies which no longer have a reference to them.
137-
fn gc_workspace(workspace: &Workspace<'_>, dependencies: &[String]) -> CargoResult<()> {
140+
/// Clean up the workspace.dependencies, profile, patch, and replace sections of the root manifest
141+
/// by removing dependencies which no longer have a reference to them.
142+
fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
138143
let mut manifest: toml_edit::Document =
139144
cargo_util::paths::read(workspace.root_manifest())?.parse()?;
145+
let mut is_modified = true;
140146

141147
let members = workspace
142148
.members()
143149
.map(|p| LocalManifest::try_new(p.manifest_path()))
144150
.collect::<CargoResult<Vec<_>>>()?;
145151

146-
for dep in dependencies {
147-
if !dep_in_workspace(dep, &members) {
148-
remove_workspace_dep(dep, &mut manifest);
152+
let mut dependencies = members
153+
.iter()
154+
.flat_map(|manifest| {
155+
manifest.get_sections().into_iter().flat_map(|(_, table)| {
156+
table
157+
.as_table_like()
158+
.unwrap()
159+
.iter()
160+
.map(|(key, item)| Dependency::from_toml(&manifest.path, key, item))
161+
.collect::<Vec<_>>()
162+
})
163+
})
164+
.collect::<CargoResult<Vec<_>>>()?;
165+
166+
// Clean up the workspace.dependencies section and replace instances of
167+
// workspace dependencies with their definitions
168+
if let Some(toml_edit::Item::Table(deps_table)) = manifest
169+
.get_mut("workspace")
170+
.and_then(|t| t.get_mut("dependencies"))
171+
{
172+
deps_table.set_implicit(true);
173+
for (key, item) in deps_table.iter_mut() {
174+
let ws_dep = Dependency::from_toml(&workspace.root(), key.get(), item)?;
175+
176+
// search for uses of this workspace dependency
177+
let mut is_used = false;
178+
for dep in dependencies.iter_mut().filter(|d| {
179+
d.toml_key() == key.get() && matches!(d.source(), Some(Source::Workspace(_)))
180+
}) {
181+
// replace the reference with the definition
182+
*dep = ws_dep.clone();
183+
184+
is_used = true;
185+
}
186+
187+
if !is_used {
188+
*item = toml_edit::Item::None;
189+
is_modified = true;
190+
}
149191
}
150192
}
151193

152-
cargo_util::paths::write(workspace.root_manifest(), manifest.to_string().as_bytes())?;
194+
// Clean up the profile section
195+
//
196+
// Example tables:
197+
// - profile.dev.package.foo
198+
// - profile.release.package."*"
199+
// - profile.release.package."foo:2.1.0"
200+
if let Some(toml_edit::Item::Table(profile_section_table)) = manifest.get_mut("profile") {
201+
profile_section_table.set_implicit(true);
202+
203+
for (_, item) in profile_section_table.iter_mut() {
204+
if let toml_edit::Item::Table(profile_table) = item {
205+
profile_table.set_implicit(true);
206+
207+
if let Some(toml_edit::Item::Table(package_table)) =
208+
profile_table.get_mut("package")
209+
{
210+
package_table.set_implicit(true);
211+
212+
for (key, item) in package_table.iter_mut() {
213+
if !spec_has_match(
214+
&PackageIdSpec::parse(key.get())?,
215+
&dependencies,
216+
workspace.config(),
217+
)? {
218+
*item = toml_edit::Item::None;
219+
is_modified = true;
220+
}
221+
}
222+
}
223+
}
224+
}
225+
}
226+
227+
// Clean up the patch section
228+
if let Some(toml_edit::Item::Table(patch_section_table)) = manifest.get_mut("patch") {
229+
patch_section_table.set_implicit(true);
230+
231+
// The key in each of the subtables is a source (either a registry or a URL)
232+
for (source, item) in patch_section_table.iter_mut() {
233+
if let toml_edit::Item::Table(patch_table) = item {
234+
patch_table.set_implicit(true);
235+
236+
for (key, item) in patch_table.iter_mut() {
237+
let package_name =
238+
Dependency::from_toml(&workspace.root_manifest(), key.get(), item)?.name;
239+
if !source_has_match(
240+
&package_name,
241+
source.get(),
242+
&dependencies,
243+
workspace.config(),
244+
)? {
245+
*item = toml_edit::Item::None;
246+
}
247+
}
248+
}
249+
}
250+
}
251+
252+
// Clean up the replace section
253+
if let Some(toml_edit::Item::Table(table)) = manifest.get_mut("replace") {
254+
table.set_implicit(true);
255+
256+
for (key, item) in table.iter_mut() {
257+
if !spec_has_match(
258+
&PackageIdSpec::parse(key.get())?,
259+
&dependencies,
260+
workspace.config(),
261+
)? {
262+
*item = toml_edit::Item::None;
263+
is_modified = true;
264+
}
265+
}
266+
}
267+
268+
if is_modified {
269+
cargo_util::paths::write(workspace.root_manifest(), manifest.to_string().as_bytes())?;
270+
}
153271

154272
Ok(())
155273
}
156274

157-
/// Get whether or not a dependency is depended upon in a workspace.
158-
fn dep_in_workspace(dep: &str, members: &[LocalManifest]) -> bool {
159-
members.iter().any(|manifest| {
160-
manifest.get_sections().iter().any(|(_, table)| {
161-
table
162-
.as_table_like()
163-
.unwrap()
164-
.get(dep)
165-
.and_then(|t| t.get("workspace"))
166-
.and_then(|v| v.as_bool())
167-
.unwrap_or(false)
168-
})
169-
})
275+
/// Check whether or not a package ID spec matches any non-workspace dependencies.
276+
fn spec_has_match(
277+
spec: &PackageIdSpec,
278+
dependencies: &[Dependency],
279+
config: &Config,
280+
) -> CargoResult<bool> {
281+
for dep in dependencies {
282+
if spec.name().as_str() != &dep.name {
283+
continue;
284+
}
285+
286+
let version_matches = match (spec.version(), dep.version()) {
287+
(Some(v), Some(vq)) => semver::VersionReq::parse(vq)?.matches(v),
288+
(Some(_), None) => false,
289+
(None, None | Some(_)) => true,
290+
};
291+
if !version_matches {
292+
continue;
293+
}
294+
295+
match dep.source_id(config)? {
296+
MaybeWorkspace::Other(source_id) => {
297+
if spec.url().map(|u| u == source_id.url()).unwrap_or(true) {
298+
return Ok(true);
299+
}
300+
}
301+
MaybeWorkspace::Workspace(_) => {}
302+
}
303+
}
304+
305+
Ok(false)
170306
}
171307

172-
/// Remove a dependency from a workspace manifest.
173-
fn remove_workspace_dep(dep: &str, ws_manifest: &mut toml_edit::Document) {
174-
if let Some(toml_edit::Item::Table(table)) = ws_manifest
175-
.get_mut("workspace")
176-
.and_then(|t| t.get_mut("dependencies"))
177-
{
178-
table.set_implicit(true);
179-
table.remove(dep);
308+
/// Check whether or not a source (URL or registry name) matches any non-workspace dependencies.
309+
fn source_has_match(
310+
name: &str,
311+
source: &str,
312+
dependencies: &[Dependency],
313+
config: &Config,
314+
) -> CargoResult<bool> {
315+
for dep in dependencies {
316+
if &dep.name != name {
317+
continue;
318+
}
319+
320+
match dep.source_id(config)? {
321+
MaybeWorkspace::Other(source_id) => {
322+
if source_id.is_registry() {
323+
if source_id.display_registry_name() == source
324+
|| source_id.url().as_str() == source
325+
{
326+
return Ok(true);
327+
}
328+
} else if source_id.is_git() {
329+
if source_id.url().as_str() == source {
330+
return Ok(true);
331+
}
332+
}
333+
}
334+
MaybeWorkspace::Workspace(_) => {}
335+
}
180336
}
337+
338+
Ok(false)
181339
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
"[workspace]\n\
33+
members = [ \"my-member\" ]\n\
34+
\n\
35+
[package]\n\
36+
name = \"my-project\"\n\
37+
version = \"0.1.0\"\n\
38+
\n\
39+
[dependencies]\n\
40+
bar = {{ git = \"{git_project1}\" }}\n\
41+
\n\
42+
[patch.\"{git_project1}\"]\n\
43+
bar = {{ git = \"{git_project2}\" }}\n\
44+
\n\
45+
[patch.crates-io]\n\
46+
bar = {{ git = \"{git_project2}\" }}\n",
47+
),
48+
)
49+
.file("src/lib.rs", "")
50+
.file(
51+
"my-member/Cargo.toml",
52+
"[package]\n\
53+
name = \"my-member\"\n\
54+
version = \"0.1.0\"\n\
55+
\n\
56+
[dependencies]\n\
57+
bar = \"0.1.0\"\n",
58+
)
59+
.file("my-member/src/lib.rs", "")
60+
.build();
61+
62+
snapbox::cmd::Command::cargo_ui()
63+
.arg("remove")
64+
.args(["bar"])
65+
.current_dir(&in_project.root())
66+
.assert()
67+
.success()
68+
.stdout_matches_path(curr_dir!().join("stdout.log"))
69+
.stderr_matches_path(curr_dir!().join("stderr.log"));
70+
71+
assert_ui().subset_matches(curr_dir!().join("out"), &in_project.root());
72+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[workspace]
2+
members = [ "my-member" ]
3+
4+
[package]
5+
name = "my-project"
6+
version = "0.1.0"
7+
8+
[patch.crates-io]
9+
bar = { git = "[ROOTURL]/bar2" }

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

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Removing bar from dependencies
2+
Updating git repository `[ROOTURL]/bar2`
3+
Updating `dummy-registry` index

tests/testsuite/cargo_remove/gc_patch/stdout.log

Whitespace-only changes.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
toml = "0.2.3"
23+
docopt = "0.6"
24+
25+
[features]
26+
std = ["serde/std", "semver/std"]
27+
28+
[profile.dev.package.docopt]
29+
opt-level = 3
30+
31+
[profile.dev.package."toml@0.1.0"]
32+
opt-level = 3
33+
34+
[profile.release.package.toml]
35+
opt-level = 1
36+
overflow-checks = false
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)