Skip to content

#1953: Implement base functionality of Cleanup Commandlet#1957

Open
areinicke wants to merge 17 commits into
devonfw:mainfrom
areinicke:feature/1580-cleanup-commandlet
Open

#1953: Implement base functionality of Cleanup Commandlet#1957
areinicke wants to merge 17 commits into
devonfw:mainfrom
areinicke:feature/1580-cleanup-commandlet

Conversation

@areinicke
Copy link
Copy Markdown
Contributor

@areinicke areinicke commented May 20, 2026

This PR fixes #1953

Implemented changes:

  • Implemented the base functionality of the cleanup commandlet. Users can run ide cleanup to scan for unused tool installations and automatically remove them. A tool is considered as unused if no project links to the tool installation from its software folder at $IDE_ROOT/<project>/software or $IDE_ROOT/<project>/software/extra.
  • Users will be given a summary report and are prompted before any deletion takes place. The confirmation prompt can be skipped by providing the flag --fd.
  • Added ide help documentation for the new commandlet
  • Added a basic test for the new commandlet. This will need to be expanded in th future though.

Testing instructions

Please add conscise, understandable instructions on how a reviewer can test/verify the functionality of your contribution here:

  1. [Optional] Install some additional software using IDEasy. You can install additional software for IDEasy projects by switching around the required editions and versions of a tool using the set-edition and set-version commandlet repeatedly.
  2. Run ide cleanup
  3. Verify that the unused tool installations have been removed.

AND

  1. Run ide help and ide help cleanup to verify that the help documentation works as expected.

Checklist for this PR

Make sure everything is checked before merging this PR. For further info please also see
our DoD.

  • When running mvn clean test locally all tests pass and build is successful
  • PR title is of the form #«issue-id»: «brief summary» (e.g. #921: fixed setup.bat). If no issue ID exists, title only.
  • PR top-level comment summarizes what has been done and contains link to addressed issue(s)
  • PR and issue(s) have suitable labels
  • Issue is set to In Progress and assigned to you or there is no issue (might happen for very small PRs)
  • You followed all coding conventions
  • You have added the issue implemented by your PR in CHANGELOG.adoc unless issue is labeled
    with internal
  • You have formulated clear instructions on how to test your contribution under "Testing instructions"

@github-project-automation github-project-automation Bot moved this to 🆕 New in IDEasy board May 20, 2026
@areinicke areinicke self-assigned this May 20, 2026
@areinicke areinicke added enhancement New feature or request commandlet ide sub-command labels May 20, 2026
@areinicke areinicke moved this from 🆕 New to 🏗 In progress in IDEasy board May 20, 2026
@coveralls
Copy link
Copy Markdown
Collaborator

coveralls commented May 20, 2026

Coverage Report for CI Build 26655164025

Coverage decreased (-0.03%) to 71.061%

Details

  • Coverage decreased (-0.03%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 10 coverage regressions across 3 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

10 previously-covered lines in 3 files lost coverage.

File Lines Losing Coverage Coverage
com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java 8 90.59%
com/devonfw/tools/ide/tool/ide/IdeToolCommandlet.java 1 78.33%
com/devonfw/tools/ide/version/VersionSegment.java 1 88.71%

Coverage Stats

Coverage Status
Relevant Lines: 15926
Covered Lines: 11815
Line Coverage: 74.19%
Relevant Branches: 7098
Covered Branches: 4546
Branch Coverage: 64.05%
Branches in Coverage %: Yes
Coverage Strength: 3.14 hits per line

💛 - Coveralls

@areinicke areinicke marked this pull request as ready for review May 21, 2026 06:54
@areinicke areinicke moved this from 🏗 In progress to Team Review in IDEasy board May 21, 2026
Copy link
Copy Markdown
Contributor

@Caylipp Caylipp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested the cleanup commandlet locally.

For my manual test, I installed additional versions of gh and java and then ran ide cleanup. Only the extra unused versions were deleted, while the versions still referenced by a project were kept. So the basic deletion functionality worked for me.

I noticed an issue in ide shell where duplicate entries appeared on a second run. However, this issue has already been fixed.

Overall, the implementation looks good to me and my manual test worked. Very good!

Copy link
Copy Markdown
Member

@hohwille hohwille left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@areinicke thanks for your PR. Great that you divided the very large story and created a very reasonable starting point as first implementation that totally makes sense to me.
Also the logging as preview what will be deleted and the user confirmation looks good to me. Great job 👍
I left quite some review comments due to some conventions and design issues.
I hope you consider it as appreciation to learn some things to further advance your Java coding style and not as nasty picky pointless extra rework ;)


super(context);
addKeyword(getName());
this.forceDelete = add(new FlagProperty("--fd")); // Force-Delete flag: Skips confirmation prompts if provided
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POSIX standard defines options in two forms:

  • short option have only a single character -f, -d and are typically used by experts who know the CLI very well and are lazy and want to save typing work. These options can also be combined: -fd is the same as -f -d.
  • long options start with two dashes and should be long and self explanatory. This is used e.g. in scripts to make it easier to understand for the reader what is going on here so instead of -fd we can also write --force --debug and newbie users will be happy since they can way easier understand what is going on.
Suggested change
this.forceDelete = add(new FlagProperty("--fd")); // Force-Delete flag: Skips confirmation prompts if provided
this.forceDelete = add(new FlagProperty("--force-delete")); // Skips confirmation prompts if provided

Please note that obviously you also need to update the help texts again when applying this change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW: are you aware that you can already do the same without adding this property?
ide -f cleanup or ide --force cleanup will run the cleanup in force mode and the askToContinue method will be automatically confirmed then... So if you want to follow KISS, you could even omit this property.

Comment on lines +25 to +75
/**
* Class which represents individual IDE tools. Contains multiple parameter such as the name of the tool, the path where it is installed,
* a list of projects which use this tool and a boolean whether the tool is marked for deletion or not.
* Furthermore, it contains a list of editions of the edition (e.g. for intellij, we could have the editions "intellij" or "ultimate").
*/
private static class IdeToolEditionVersion {
String versionName;
private final Path path;
private final List<String> usedBy = new java.util.ArrayList<>();
private boolean delete = false;

IdeToolEditionVersion(String versionName, Path path) {
this.versionName = versionName;
this.path = path;
}
}

/**
* Class which represents individual editions of an IDE tool. Contains multiple parameter such as the name of the edition, the path where it is installed,
* a list of projects which use this edition and a boolean whether the edition is marked for deletion or not.
* Furthermore, it contains a list of versions of the edition (e.g. for intellij ultimate edition, we could have version 2022.3 and 2023.1 installed).
*/
private static class IdeToolEdition {
String editionName;
private final Path path;
private final List<String> usedBy = new java.util.ArrayList<>();
private boolean delete = false;
private final List<IdeToolEditionVersion> versions = new java.util.ArrayList<>();

IdeToolEdition(String editionName, Path path) {
this.editionName = editionName;
this.path = path;
}
}

/**
* Class which represents individual versions of an edition of an IDE tool. Contains multiple parameter such as the name of the version, the path where it is installed,
* a list of projects which use this version and a boolean whether the version is marked for deletion or not.
*/
private static class IdeTool {
String toolName;
private final Path path;
private final List<String> usedBy = new java.util.ArrayList<>();
private boolean delete = false;
private final List<IdeToolEdition> editions = new java.util.ArrayList<>();

IdeTool(String toolName, Path path) {
this.toolName = toolName;
this.path = path;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend to move this to top-level classes (allowing them to become public API that we can later reuse in the GUI so the user can see a Popup with a tree and tick or untick checkboxes.
Anyway if you keep inner classes follow the general conventions for Java types:

public [class|interface|record|enum|@interface] <name> ... {
  // constants

  // fields / member variables

  // constructors

  // methods while generic Object methods like equals/hashCode/toString usually go to the end

  // inner types
}

So move them to the end what makes the code easier to read for typical Java coders used to that convention.


// Identify and remove unused tools.
Step step = context.newStep("Identify and remove unused software");
step.run(() -> {discoverAndDeleteUnusedSoftware();});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
step.run(() -> {discoverAndDeleteUnusedSoftware();});
step.run(this::discoverAndDeleteUnusedSoftware);

*/
private void discoverAndDeleteUnusedSoftware() {
// Iterate over software in $IDE_ROOT/_ide/software folder and save installed software to a list
discoverInstalledSoftware(this.context.getIdeRoot().resolve("_ide/software/default"));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be improved by reusing more dedicated method and constants (may require import statement to apply suggestion):

Suggested change
discoverInstalledSoftware(this.context.getIdeRoot().resolve("_ide/software/default"));
discoverInstalledSoftware(this.context.getSoftwareRepositoryPath().resolve(ToolRepository.ID_DEFAULT));

Comment on lines +124 to +125
discoverUsedSoftware(ideasyProject.resolve("software"), ideasyProject.getFileName().toString());
discoverUsedSoftware(ideasyProject.resolve("software/extra"), ideasyProject.getFileName().toString());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse constants (this also allows to find usages in the workspace via IDE to see where in the code we are accessing this folder):

Suggested change
discoverUsedSoftware(ideasyProject.resolve("software"), ideasyProject.getFileName().toString());
discoverUsedSoftware(ideasyProject.resolve("software/extra"), ideasyProject.getFileName().toString());
Path ideasyProjectSoftware = ideasyProject.resolve(IdeContext.FOLDER_SOFTWARE);
discoverUsedSoftware(ideasyProjectSoftware, ideasyProject.getFileName().toString());
discoverUsedSoftware(ideasyProjectSoftware.resolve(IdeContext.FOLDER_EXTRA), ideasyProject.getFileName().toString());

private static class IdeTool {
String toolName;
private final Path path;
private final List<String> usedBy = new java.util.ArrayList<>();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am slightly confused why we have this usedBy in all 3 inner classes.
If we really want to do that, we could even use OOP and derive the 3 classes from an abstract class with a getUsedBy() and a nice centralised JavaDoc that is then inherited and reused by all 3 classes.
But IMHO we only need to track what tool+edition+version combination is used.
If all unused ones are deleted and we end up with en empty edition folder this does not really waste disc-space and we can still check that after deletion we also delete the parent folder if it became empty (and if we delete the edition folder we can also delete the version folder if that is now also empty).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW: Generic types like String or Number always need explanation. So if you see List<String> you do not know what this is unless you have further JavaDoc or other hints.
If you instead see List<IdeProject> and IdeProject is a class or record with JavaDoc then things immediately get clear. Such IdeProject type could wrap the project name as String but also maybe contain the Path to the project and later could even have methods like getWorkspaces() or whatever - see what @laim2003 is currently implementing for the GUI.

// Check if directory contains a software version file. If so, read version and add to list of used software.
if (Files.exists(current_folder.resolve(IdeContext.FILE_SOFTWARE_VERSION))
|| Files.exists(current_folder.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION))) {
if (!current_folder.startsWith(this.context.getIdeRoot().resolve("_ide/software/default"))) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here as in line 114

* @param software_folder The software folder of the IDEasy project to scan for used software
* @param project_name The name of the project we are currently scanning
*/
private void discoverUsedSoftware(Path software_folder, String project_name) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please get used to camlCase for all Java variables (except for constants in UPPER_CASE).

String tool_name = current_folder.getParent().getParent().getFileName().toString();
String tool_edition = current_folder.getParent().getFileName().toString();
String tool_version = current_folder.getFileName().toString();
// Check if software exists in global IdeTool list. If so, mark as used
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice to have but would be easier to read when extracting local variables.
So something like this:

Suggested change
// Check if software exists in global IdeTool list. If so, mark as used
// currentFolder has the structure «repo-path»/«tool»/«edition»/«version»
String toolVersion = currentFolder.getFileName().toString();
Path toolEditionFolder = currentFolder.getParent();
String toolEdition = toolEditionFolder.getFileName().toString();
String toolName = toolEditionFolder.getParent().getFileName().toString();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow GitHub messed the line markers. I wanted to suggest this for 196-199.


// assert
assertThat(context.getIdeRoot().resolve("_ide/software/default/az")).doesNotExist();
assertThat(context).logAtSuccess().hasMessage("Unused tools have been deleted successfully.");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice to have: you are just asserting that we logged this message and have deleted something/anything.
We should rather assert that something that is still used was not deleted and something that was unused has been deleted. Otherwise your implementation can be entirely broken but the test may still be green.

@github-project-automation github-project-automation Bot moved this from Team Review to 👀 In review in IDEasy board May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commandlet ide sub-command enhancement New feature or request

Projects

Status: 👀 In review

Development

Successfully merging this pull request may close these issues.

Implement Base Functionality of Cleanup Commandlet

4 participants