• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 42210990621

06 Nov 2026 11:32AM UTC coverage: 75.403% (+0.1%) from 75.305%
42210990621

push

github

web-flow
#945: skip redundant installs and CVE checks for shared dependencies (#2642)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

6071 of 8928 branches covered (68.0%)

Branch coverage included in aggregate %.

15750 of 20011 relevant lines covered (78.71%)

3.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.55
cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java
1
package com.devonfw.tools.ide.commandlet.update;
2

3
import java.io.IOException;
4
import java.nio.file.Files;
5
import java.nio.file.Path;
6
import java.util.HashSet;
7
import java.util.Iterator;
8
import java.util.List;
9
import java.util.Objects;
10
import java.util.Set;
11
import java.util.stream.Stream;
12

13
import org.slf4j.Logger;
14
import org.slf4j.LoggerFactory;
15

16
import com.devonfw.tools.ide.cli.CliException;
17
import com.devonfw.tools.ide.cli.CliFatalException;
18
import com.devonfw.tools.ide.commandlet.AbstractCommandlet;
19
import com.devonfw.tools.ide.commandlet.CommandletManager;
20
import com.devonfw.tools.ide.commandlet.CreateCommandlet;
21
import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult;
22
import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult;
23
import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateStatus;
24
import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater;
25
import com.devonfw.tools.ide.context.AbstractIdeContext;
26
import com.devonfw.tools.ide.context.IdeContext;
27
import com.devonfw.tools.ide.context.IdeStartContextImpl;
28
import com.devonfw.tools.ide.git.repository.RepositoryCommandlet;
29
import com.devonfw.tools.ide.git.repository.RepositoryType;
30
import com.devonfw.tools.ide.io.FileAccess;
31
import com.devonfw.tools.ide.property.FlagProperty;
32
import com.devonfw.tools.ide.property.StringProperty;
33
import com.devonfw.tools.ide.step.Step;
34
import com.devonfw.tools.ide.tool.AbstractLocalToolCommandlet;
35
import com.devonfw.tools.ide.tool.AbstractToolCommandlet;
36
import com.devonfw.tools.ide.tool.ToolEdition;
37
import com.devonfw.tools.ide.tool.ToolEditionAndVersion;
38
import com.devonfw.tools.ide.tool.ToolInstallRequest;
39
import com.devonfw.tools.ide.tool.ToolInstallTracker;
40
import com.devonfw.tools.ide.tool.custom.CustomToolCommandlet;
41
import com.devonfw.tools.ide.tool.custom.CustomToolMetadata;
42
import com.devonfw.tools.ide.tool.extra.ExtraToolInstallation;
43
import com.devonfw.tools.ide.tool.extra.ExtraTools;
44
import com.devonfw.tools.ide.tool.extra.ExtraToolsMapper;
45
import com.devonfw.tools.ide.variable.IdeVariables;
46
import com.devonfw.tools.ide.version.VersionIdentifier;
47

48
/**
49
 * Abstract {@link AbstractCommandlet} base-class for both {@link UpdateCommandlet} and {@link CreateCommandlet}.
50
 */
51
public abstract class AbstractUpdateCommandlet extends AbstractCommandlet {
52

53
  private static final Logger LOG = LoggerFactory.getLogger(AbstractUpdateCommandlet.class);
4 ✔
54

55
  /** {@link StringProperty} for the settings repository URL. */
56
  public final StringProperty settingsRepo;
57

58
  /** {@link FlagProperty} for skipping installation/updating of tools. */
59
  public final FlagProperty skipTools;
60

61
  /** {@link FlagProperty} for skipping the setup of git repositories. */
62
  public final FlagProperty skipRepositories;
63

64
  /** {@link FlagProperty} to force the update of the settings git repository. */
65
  public final FlagProperty forcePull;
66

67
  /** {@link FlagProperty} to force the installation/update of plugins. */
68
  public final FlagProperty forcePlugins;
69

70
  /** {@link FlagProperty} to force the setup of git repositories. */
71
  public final FlagProperty forceRepositories;
72

73
  /**
74
   * The constructor.
75
   *
76
   * @param context the {@link IdeContext}.
77
   */
78
  public AbstractUpdateCommandlet(IdeContext context) {
79

80
    super(context);
3 ✔
81
    addKeyword(getName());
4 ✔
82
    this.skipTools = add(new FlagProperty("--skip-tools"));
9 ✔
83
    this.skipRepositories = add(new FlagProperty("--skip-repositories"));
9 ✔
84
    this.forcePull = add(new FlagProperty("--force-pull"));
9 ✔
85
    this.forcePlugins = add(new FlagProperty("--force-plugins"));
9 ✔
86
    this.forceRepositories = add(new FlagProperty("--force-repositories"));
9 ✔
87
    this.settingsRepo = new StringProperty("", false, "settingsRepository");
8 ✔
88
  }
1 ✔
89

90
  @Override
91
  protected void doRun() {
92

93
    IdeStartContextImpl startContext = ((AbstractIdeContext) this.context).getStartContext();
5 ✔
94
    startContext.setForcePull(forcePull.isTrue());
5 ✔
95
    startContext.setForcePlugins(forcePlugins.isTrue());
5 ✔
96
    startContext.setForceRepositories(forceRepositories.isTrue());
5 ✔
97

98
    updateSettings();
2 ✔
99
    updateConf();
2 ✔
100
    reloadContext();
2 ✔
101
    this.context.verifyIdeMinVersion(true);
4 ✔
102

103
    updateSoftware();
2 ✔
104
    updateRepositories();
2 ✔
105
    createStartScripts();
2 ✔
106
  }
1 ✔
107

108
  /**
109
   * Hook that is called after the settings passed the health check but before they are moved to their final location. Does nothing by default and is overridden
110
   * by {@link CreateCommandlet} to create the project structure so that no project is created at all if the health check failed.
111
   */
112
  protected void onSettingHealthCheckFinished() {
113

114
    // nothing to do by default
115
  }
1 ✔
116

117
  private void reloadContext() {
118

119
    ((AbstractIdeContext) this.context).reload();
4 ✔
120
  }
1 ✔
121

122
  private void updateConf() {
123

124
    Path templatesFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_TEMPLATES);
6 ✔
125
    if (!Files.exists(templatesFolder)) {
5 ✔
126
      Path legacyTemplatesFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_LEGACY_TEMPLATES);
6 ✔
127
      if (Files.exists(legacyTemplatesFolder)) {
5 !
128
        templatesFolder = legacyTemplatesFolder;
×
129
      } else {
130
        LOG.warn("Templates folder is missing in settings repository.");
3 ✔
131
        return;
1 ✔
132
      }
133
    }
134

135
    Step step = this.context.newStep("Copy configuration templates", templatesFolder);
11 ✔
136
    final Path finalTemplatesFolder = templatesFolder;
2 ✔
137
    step.run(() -> setupConf(finalTemplatesFolder, this.context.getIdeHome()));
13 ✔
138
  }
1 ✔
139

140
  private void setupConf(Path template, Path conf) {
141

142
    List<Path> children = this.context.getFileAccess().listChildren(template, f -> true);
9 ✔
143
    for (Path child : children) {
10 ✔
144

145
      String basename = child.getFileName().toString();
4 ✔
146
      Path confPath = conf.resolve(basename);
4 ✔
147

148
      if (Files.isDirectory(child)) {
5 ✔
149
        if (!Files.isDirectory(confPath)) {
5 !
150
          this.context.getFileAccess().mkdirs(confPath);
5 ✔
151
        }
152
        setupConf(child, confPath);
5 ✔
153
      } else if (Files.isRegularFile(child)) {
5 !
154
        if (Files.isRegularFile(confPath)) {
5 !
155
          LOG.debug("Configuration {} already exists - skipping to copy from {}", confPath, child);
×
156
        } else {
157
          if (!basename.equals("settings.xml")) {
4 !
158
            LOG.info("Copying template {} to {}.", child, conf);
5 ✔
159
            this.context.getFileAccess().copy(child, conf);
6 ✔
160
          }
161
        }
162
      }
163
    }
1 ✔
164
  }
1 ✔
165

166
  /**
167
   * Updates the settings repository in IDE_HOME/settings by either cloning if no such repository exists or pulling if the repository exists then saves the
168
   * latest current commit ID in the file ".commit.id". The settings are always cloned into a temporary directory first where a health check is performed. Only
169
   * if that health check succeeded the settings are pulled or the verified clone is moved to its final location.
170
   */
171
  protected void updateSettings() {
172

173
    RepositoryType repositoryType = RepositoryType.ofSettingsPath(getSettingsPathForSettingsUpdate(), context);
6 ✔
174
    if (repositoryType == RepositoryType.CODE_SETTINGS_COMBINED && !(this.context.isForceMode() || this.forcePull.isTrue())) {
3 !
175
      LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling.");
×
176
      return;
×
177
    }
178
    Step step = this.context.newStep(getStepMessage());
6 ✔
179
    step.run(this::updateSettingsInStep);
5 ✔
180
  }
1 ✔
181

182
  protected String getStepMessage() {
183

184
    return "Update settings repository";
2 ✔
185
  }
186

187
  private void updateSettingsInStep() {
188

189
    SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo);
8 ✔
190
    try {
191
      //Step 1: Perform health check
192
      Step healthCheckStep = this.context.newStep("Performing settings health check");
5 ✔
193
      SettingsHealthCheckResult healthCheckResult = healthCheckStep.call(() -> checkSettingsInStep(settingsUpdater), () -> null);
12 ✔
194

195
      // If the health check failed (healthCheckResult is null) the settings have not been verified, so skip applying them and fail the "Update settings"
196
      // step. A non-null result is only produced when the health check passed or the user explicitly chose to continue anyway (force mode), so this never
197
      // aborts in force mode.
198
      if (healthCheckResult == null) {
2 !
199
        throw new CliException("Settings update aborted due to error in health check");
×
200
      }
201

202
      //Step 2: Let create/update commandlets prepare themselves for the settings update.
203
      onSettingHealthCheckFinished();
2 ✔
204

205
      //Step 3: Apply (move/pull newest version) settings
206
      Step applySettingsStep = this.context.newStep("Applying settings");
5 ✔
207
      applySettingsStep.run(() -> applySettingsUpdateInStep(settingsUpdater, healthCheckResult));
12 ✔
208

209
      //Make sure to always fail the parent step if the "Apply settings" step fails.
210
      if (applySettingsStep.isFailure()) {
3 ✔
211
        throw new CliException("Settings update failed due to error while applying the settings update");
5 ✔
212
      }
213
    } finally {
214
      // the verified clone lives across both steps and the prepareProject hook so it is only here that its lifetime ends
215
      settingsUpdater.cleanup();
2 ✔
216
    }
217
  }
1 ✔
218

219
  private SettingsHealthCheckResult checkSettingsInStep(SettingsUpdater settingsUpdater) {
220
    SettingsHealthCheckResult healthCheckResult = settingsUpdater.checkSettings(getSettingsPathForSettingsUpdate());
5 ✔
221
    RepositoryType repositoryType = healthCheckResult.repositoryType();
3 ✔
222

223
    if ((repositoryType == null || !repositoryType.isValid()) && !healthCheckResult.isExistingProject()) {
8 !
224
      throw new CliFatalException("Fatal error while cloning settings: The settings health check failed: " + healthCheckResult.errorMessage());
7 ✔
225
    } else if ((repositoryType == null || !repositoryType.isValid())) {
5 !
226
      throw new CliException("The settings health check failed: " + healthCheckResult.errorMessage());
×
227
    }
228
    return healthCheckResult;
2 ✔
229
  }
230

231
  private void applySettingsUpdateInStep(SettingsUpdater settingsUpdater, SettingsHealthCheckResult healthCheckResult) {
232
    SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(healthCheckResult);
4 ✔
233
    if (settingsUpdateResult == null) {
2 !
234

235
      throw new CliException("Failed to apply the settings update due to unknown error.");
×
236
    }
237

238
    if (Objects.requireNonNull(settingsUpdateResult.updateStatus()) == SettingsUpdateStatus.SETTINGS_UPDATE_FAILED) {
5 !
239
      String errorMessage = "The settings update could not be applied: " + settingsUpdateResult.errorMessage();
×
240
      if (!healthCheckResult.isExistingProject()) {
×
241
        throw new CliFatalException(errorMessage);
×
242
      }
243
      throw new CliException(errorMessage);
×
244
    }
245
  }
1 ✔
246

247
  private void updateSoftware() {
248

249
    if (this.skipTools.isTrue()) {
4 ✔
250
      LOG.info("Skipping installation/update of tools as specified by the user.");
3 ✔
251
      return;
1 ✔
252
    }
253
    Step step = this.context.newStep("Install or update software");
5 ✔
254
    step.run(() -> doUpdateSoftwareStep(step));
10 ✔
255
  }
1 ✔
256

257
  private void doUpdateSoftwareStep(Step step) {
258

259
    Set<AbstractToolCommandlet> toolCommandlets = new HashSet<>();
4 ✔
260
    CommandletManager commandletManager = this.context.getCommandletManager();
4 ✔
261
    // installed tools in IDE_HOME/software
262
    List<Path> softwarePaths = this.context.getFileAccess().listChildren(this.context.getSoftwarePath(), Files::isDirectory);
14 ✔
263
    for (Path softwarePath : softwarePaths) {
10 ✔
264
      String toolName = softwarePath.getFileName().toString();
4 ✔
265
      AbstractToolCommandlet toolCommandlet = commandletManager.getToolCommandlet(toolName);
4 ✔
266
      if (toolCommandlet != null) {
2 !
267
        toolCommandlets.add(toolCommandlet);
4 ✔
268
      }
269
    }
1 ✔
270

271
    // regular tools in $IDE_TOOLS
272
    List<String> regularTools = IdeVariables.IDE_TOOLS.get(this.context);
6 ✔
273
    if (regularTools != null) {
2 !
274
      for (String regularTool : regularTools) {
10 ✔
275
        AbstractToolCommandlet toolCommandlet = commandletManager.getToolCommandlet(regularTool);
4 ✔
276
        if (toolCommandlet == null) {
2 !
277
          String displayName = (regularTool == null || regularTool.isBlank()) ? "<empty>" : "'" + regularTool + "'";
×
278
          LOG.error("Cannot install or update tool '{}''. No matching commandlet found. Please check your IDE_TOOLS configuration.", displayName);
×
279
        } else {
×
280
          toolCommandlets.add(toolCommandlet);
4 ✔
281
        }
282
      }
1 ✔
283
    }
284

285
    // custom tools in ide-custom-tools.json
286
    for (CustomToolMetadata customTool : this.context.getCustomToolRepository().getTools()) {
9 !
287
      CustomToolCommandlet customToolCommandlet = new CustomToolCommandlet(this.context, customTool);
×
288
      toolCommandlets.add(customToolCommandlet);
×
289
    }
×
290

291
    // Track tools installed during this operation so a shared dependency is not installed and CVE-checked redundantly. A single tracker is shared across
292
    // all tools so cross-tool references are deduplicated too - see https://github.com/devonfw/IDEasy/issues/945
293
    ToolInstallTracker tracker = new ToolInstallTracker();
4 ✔
294

295
    // update/install the toolCommandlets
296
    for (AbstractToolCommandlet toolCommandlet : toolCommandlets) {
10 ✔
297
      this.context.newStep("Install " + toolCommandlet.getName()).run(() -> toolCommandlet.install(tracker));
16 ✔
298
    }
1 ✔
299

300
    ExtraTools extraTools = ExtraToolsMapper.get().loadJsonFromFolder(this.context.getSettingsPath());
7 ✔
301
    if (extraTools != null) {
2 ✔
302
      List<String> toolNames = extraTools.getSortedToolNames();
3 ✔
303
      LOG.info("Found extra installation of the following tools: {}", toolNames);
4 ✔
304
      for (String tool : toolNames) {
10 ✔
305
        List<ExtraToolInstallation> installations = extraTools.getExtraInstallations(tool);
4 ✔
306
        this.context.newStep("Install extra version(s) of " + tool).run(() -> installExtraToolInstallations(tool, installations, tracker));
18 ✔
307
      }
1 ✔
308
    }
309

310
  }
1 ✔
311

312
  private void installExtraToolInstallations(String tool, List<ExtraToolInstallation> extraInstallations, ToolInstallTracker tracker) {
313

314
    CommandletManager commandletManager = this.context.getCommandletManager();
4 ✔
315
    FileAccess fileAccess = this.context.getFileAccess();
4 ✔
316
    Path extraPath = this.context.getSoftwareExtraPath();
4 ✔
317
    AbstractLocalToolCommandlet toolCommandlet = commandletManager.getRequiredLocalToolCommandlet(tool);
4 ✔
318
    for (ExtraToolInstallation extraInstallation : extraInstallations) {
10 ✔
319
      ToolInstallRequest request = new ToolInstallRequest(false);
5 ✔
320
      String edition = extraInstallation.edition();
3 ✔
321
      if (edition == null) {
2 ✔
322
        edition = toolCommandlet.getConfiguredEdition();
3 ✔
323
      }
324
      ToolEdition toolEdition = new ToolEdition(tool, edition);
6 ✔
325
      VersionIdentifier version = extraInstallation.version();
3 ✔
326
      request.setRequested(new ToolEditionAndVersion(toolEdition, version));
7 ✔
327
      Path extraToolPath = extraPath.resolve(tool);
4 ✔
328
      Path toolPath = extraToolPath.resolve(extraInstallation.name());
5 ✔
329
      request.setToolPathForExtraInstallation(toolPath);
3 ✔
330
      request.setTracker(tracker);
3 ✔
331
      toolCommandlet.install(request);
4 ✔
332
    }
1 ✔
333
  }
1 ✔
334

335
  private void updateRepositories() {
336

337
    if (this.skipRepositories.isTrue()) {
4 !
338
      if (this.forceRepositories.isTrue()) {
×
339
        LOG.warn("Options to skip and force repositories are incompatible and should not be combined. Ignoring --force-repositories to proceed.");
×
340
      }
341
      LOG.info("Skipping setup of repositories as specified by the user.");
×
342
      return;
×
343
    }
344
    RepositoryCommandlet repositoryCommandlet = this.context.getCommandletManager().getCommandlet(RepositoryCommandlet.class);
7 ✔
345
    repositoryCommandlet.reset();
2 ✔
346
    repositoryCommandlet.run();
2 ✔
347
  }
1 ✔
348

349
  private void createStartScripts() {
350

351
    List<String> ides = IdeVariables.CREATE_START_SCRIPTS.get(this.context);
6 ✔
352
    if (ides == null) {
2 ✔
353
      LOG.info("Variable CREATE_START_SCRIPTS is undefined - skipping start script creation.");
3 ✔
354
      return;
1 ✔
355
    }
356
    for (String ide : ides) {
10 ✔
357
      AbstractToolCommandlet tool = this.context.getCommandletManager().getToolCommandlet(ide);
6 ✔
358
      if (tool == null) {
2 !
359
        LOG.error("Undefined IDE '{}' configured in variable CREATE_START_SCRIPTS.", ide);
×
360
      } else {
361
        createStartScript(ide);
3 ✔
362
      }
363
    }
1 ✔
364
  }
1 ✔
365

366
  private void createStartScript(String ide) {
367

368
    LOG.info("Creating start scripts for {}", ide);
4 ✔
369
    Path workspaces = this.context.getIdeHome().resolve(IdeContext.FOLDER_WORKSPACES);
6 ✔
370
    try (Stream<Path> childStream = Files.list(workspaces)) {
3 ✔
371
      Iterator<Path> iterator = childStream.iterator();
3 ✔
372
      while (iterator.hasNext()) {
3 ✔
373
        Path child = iterator.next();
4 ✔
374
        if (Files.isDirectory(child)) {
5 !
375
          createStartScript(ide, child.getFileName().toString());
6 ✔
376
        }
377
      }
1 ✔
378
    } catch (IOException e) {
×
379
      throw new RuntimeException("Failed to list children of directory " + workspaces, e);
×
380
    }
1 ✔
381
  }
1 ✔
382

383
  private void createStartScript(String ide, String workspace) {
384

385
    Path ideHome = this.context.getIdeHome();
4 ✔
386
    String scriptName = ide + "-" + workspace;
4 ✔
387
    boolean windows = this.context.getSystemInfo().isWindows();
5 ✔
388
    if (windows) {
2 !
389
      scriptName = scriptName + ".bat";
×
390
    } else {
391
      scriptName = scriptName + ".sh";
3 ✔
392
    }
393
    Path scriptPath = ideHome.resolve(scriptName);
4 ✔
394
    if (Files.exists(scriptPath)) {
5 !
395
      return;
×
396
    }
397
    String scriptContent;
398
    if (windows) {
2 !
399
      scriptContent = "@echo off\r\n" + "pushd %~dp0\r\n" + "cd workspaces/" + workspace + "\r\n" + "call ide " + ide + "\r\n" + "popd\r\n";
×
400
    } else {
401
      scriptContent = "#!/usr/bin/env bash\n" + "cd \"$(dirname \"$0\")\"\n" + "cd workspaces/" + workspace + "\n" + "ideasy " + ide + "\n";
4 ✔
402
    }
403
    FileAccess fileAccess = this.context.getFileAccess();
4 ✔
404
    fileAccess.writeFileContent(scriptContent, scriptPath);
4 ✔
405
    fileAccess.makeExecutable(scriptPath);
3 ✔
406
  }
1 ✔
407

408
  /**
409
   * This method returns the path to the settings for the case of a settings update.
410
   *
411
   * @return The {@link Path} to the settings folder; if not overridden we will get the path from the context
412
   */
413
  protected Path getSettingsPathForSettingsUpdate() {
414

415
    return this.context.getSettingsPath();
4 ✔
416
  }
417
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc