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

devonfw / IDEasy / 16204424644

10 Jul 2025 07:39PM UTC coverage: 68.433% (-0.01%) from 68.446%
16204424644

Pull #1407

github

web-flow
Merge 445c8df79 into ab7eb024e
Pull Request #1407: Improve IDE_MIN_VERSION support by moving verification to AbstractUpdateCommandlet

3285 of 5202 branches covered (63.15%)

Branch coverage included in aggregate %.

8404 of 11879 relevant lines covered (70.75%)

3.12 hits per line

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

83.33
cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java
1
package com.devonfw.tools.ide.commandlet;
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.Set;
10
import java.util.stream.Stream;
11

12
import com.devonfw.tools.ide.context.AbstractIdeContext;
13
import com.devonfw.tools.ide.context.IdeContext;
14
import com.devonfw.tools.ide.git.GitContext;
15
import com.devonfw.tools.ide.git.GitUrl;
16
import com.devonfw.tools.ide.git.repository.RepositoryCommandlet;
17
import com.devonfw.tools.ide.io.FileAccess;
18
import com.devonfw.tools.ide.property.FlagProperty;
19
import com.devonfw.tools.ide.property.StringProperty;
20
import com.devonfw.tools.ide.step.Step;
21
import com.devonfw.tools.ide.tool.CustomToolCommandlet;
22
import com.devonfw.tools.ide.tool.ToolCommandlet;
23
import com.devonfw.tools.ide.tool.repository.CustomToolMetadata;
24
import com.devonfw.tools.ide.variable.IdeVariables;
25

26
/**
27
 * Abstract {@link Commandlet} base-class for both {@link UpdateCommandlet} and {@link CreateCommandlet}.
28
 */
29
public abstract class AbstractUpdateCommandlet extends Commandlet {
30

31
  /** {@link StringProperty} for the settings repository URL. */
32
  public final StringProperty settingsRepo;
33

34
  /** {@link FlagProperty} for skipping installation/updating of tools. */
35
  public final FlagProperty skipTools;
36

37
  /** {@link FlagProperty} for skipping the setup of git repositories. */
38
  public final FlagProperty skipRepositories;
39

40
  /** {@link FlagProperty} to force the update of the settings git repository. */
41
  public final FlagProperty forcePull;
42

43
  /** {@link FlagProperty} to force the installation/update of plugins. */
44
  public final FlagProperty forcePlugins;
45

46
  /** {@link FlagProperty} to force the setup of git repositories. */
47
  public final FlagProperty forceRepositories;
48

49
  /**
50
   * The constructor.
51
   *
52
   * @param context the {@link IdeContext}.
53
   */
54
  public AbstractUpdateCommandlet(IdeContext context) {
55

56
    super(context);
3✔
57
    addKeyword(getName());
4✔
58
    this.skipTools = add(new FlagProperty("--skip-tools"));
9✔
59
    this.skipRepositories = add(new FlagProperty("--skip-repositories"));
9✔
60
    this.forcePull = add(new FlagProperty("--force-pull"));
9✔
61
    this.forcePlugins = add(new FlagProperty("--force-plugins"));
9✔
62
    this.forceRepositories = add(new FlagProperty("--force-repositories"));
9✔
63
    this.settingsRepo = new StringProperty("", false, "settingsRepository");
8✔
64
  }
1✔
65

66
  @Override
67
  public void run() {
68

69
    this.context.setForcePull(forcePull.isTrue());
6✔
70
    this.context.setForcePlugins(forcePlugins.isTrue());
6✔
71
    this.context.setForceRepositories(forceRepositories.isTrue());
6✔
72

73
    if (!this.context.isSettingsRepositorySymlinkOrJunction() || this.context.isForceMode() || forcePull.isTrue()) {
4!
74
      updateSettings();
2✔
75
    }
76
    updateConf();
2✔
77
    reloadContext();
2✔
78
    this.context.verifyIdeMinVersion(true);
4✔
79

80
    updateSoftware();
2✔
81
    updateRepositories();
2✔
82
    createStartScripts();
2✔
83
  }
1✔
84

85
  private void reloadContext() {
86

87
    ((AbstractIdeContext) this.context).reload();
4✔
88
  }
1✔
89

90
  private void updateConf() {
91

92
    Path templatesFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_TEMPLATES);
6✔
93
    if (!Files.exists(templatesFolder)) {
5✔
94
      Path legacyTemplatesFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_LEGACY_TEMPLATES);
6✔
95
      if (Files.exists(legacyTemplatesFolder)) {
5!
96
        templatesFolder = legacyTemplatesFolder;
×
97
      } else {
98
        this.context.warning("Templates folder is missing in settings repository.");
4✔
99
        return;
1✔
100
      }
101
    }
102

103
    Step step = this.context.newStep("Copy configuration templates", templatesFolder);
11✔
104
    final Path finalTemplatesFolder = templatesFolder;
2✔
105
    step.run(() -> setupConf(finalTemplatesFolder, this.context.getIdeHome()));
13✔
106
  }
1✔
107

108
  private void setupConf(Path template, Path conf) {
109

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

113
      String basename = child.getFileName().toString();
4✔
114
      Path confPath = conf.resolve(basename);
4✔
115

116
      if (Files.isDirectory(child)) {
5✔
117
        if (!Files.isDirectory(confPath)) {
5!
118
          this.context.getFileAccess().mkdirs(confPath);
5✔
119
        }
120
        setupConf(child, confPath);
5✔
121
      } else if (Files.isRegularFile(child)) {
5!
122
        if (Files.isRegularFile(confPath)) {
5!
123
          this.context.debug("Configuration {} already exists - skipping to copy from {}", confPath, child);
×
124
        } else {
125
          if (!basename.equals("settings.xml")) {
4!
126
            this.context.info("Copying template {} to {}.", child, conf);
14✔
127
            this.context.getFileAccess().copy(child, conf);
6✔
128
          }
129
        }
130
      }
131
    }
1✔
132
  }
1✔
133

134
  /**
135
   * Process a repository.
136
   * <p>
137
   * Default behavior is to use strategy for settings repository.
138
   */
139
  protected void processRepository() {
140
    RepositoryStrategy repositoryStrategy = new SettingsRepositoryStrategy();
4✔
141

142
    processRepositoryUsingStrategy(repositoryStrategy);
3✔
143
  }
1✔
144

145
  /**
146
   * 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
147
   * latest current commit ID in the file ".commit.id".
148
   */
149
  protected void updateSettings() {
150

151
    Path settingsPath = this.context.getSettingsPath();
4✔
152
    GitContext gitContext = this.context.getGitContext();
4✔
153
    Step step = null;
2✔
154
    try {
155
      // here we do not use pullOrClone to prevent asking a pointless question for repository URL...
156
      if (Files.isDirectory(settingsPath) && !this.context.getFileAccess().isEmptyDir(settingsPath)) {
11!
157
        step = this.context.newStep("Pull settings repository");
5✔
158
        gitContext.pull(settingsPath);
3✔
159
        this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath());
8✔
160
        step.success("Successfully updated settings repository.");
4✔
161
      } else {
162
        processRepository();
2✔
163
      }
164
    } finally {
165
      if (step != null) {
2✔
166
        step.close();
2✔
167
      }
168
    }
169
  }
1✔
170

171
  private void updateSoftware() {
172

173
    if (this.skipTools.isTrue()) {
4✔
174
      this.context.info("Skipping installation/update of tools as specified by the user.");
4✔
175
      return;
1✔
176
    }
177
    Step step = this.context.newStep("Install or update software");
5✔
178
    step.run(() -> doUpdateSoftwareStep(step));
10✔
179
  }
1✔
180

181
  private void doUpdateSoftwareStep(Step step) {
182

183
    Set<ToolCommandlet> toolCommandlets = new HashSet<>();
4✔
184
    // installed tools in IDE_HOME/software
185
    List<Path> softwarePaths = this.context.getFileAccess().listChildren(this.context.getSoftwarePath(), Files::isDirectory);
14✔
186
    for (Path softwarePath : softwarePaths) {
10✔
187
      String toolName = softwarePath.getFileName().toString();
4✔
188
      ToolCommandlet toolCommandlet = this.context.getCommandletManager().getToolCommandlet(toolName);
6✔
189
      if (toolCommandlet != null) {
2!
190
        toolCommandlets.add(toolCommandlet);
4✔
191
      }
192
    }
1✔
193

194
    // regular tools in $IDE_TOOLS
195
    List<String> regularTools = IdeVariables.IDE_TOOLS.get(this.context);
6✔
196
    if (regularTools != null) {
2!
197
      for (String regularTool : regularTools) {
10✔
198
        toolCommandlets.add(this.context.getCommandletManager().getRequiredToolCommandlet(regularTool));
8✔
199
      }
1✔
200
    }
201

202
    // custom tools in ide-custom-tools.json
203
    for (CustomToolMetadata customTool : this.context.getCustomToolRepository().getTools()) {
9!
204
      CustomToolCommandlet customToolCommandlet = new CustomToolCommandlet(this.context, customTool);
×
205
      toolCommandlets.add(customToolCommandlet);
×
206
    }
×
207

208
    // update/install the toolCommandlets
209
    for (ToolCommandlet toolCommandlet : toolCommandlets) {
10✔
210
      this.context.newStep("Install " + toolCommandlet.getName()).run(() -> toolCommandlet.install(false));
15✔
211
    }
1✔
212
  }
1✔
213

214
  private void updateRepositories() {
215

216
    if (this.skipRepositories.isTrue()) {
4!
217
      if (this.forceRepositories.isTrue()) {
×
218
        this.context.warning("Options to skip and force repositories are incompatible and should not be combined. Ignoring --force-repositories to proceed.");
×
219
      }
220
      this.context.info("Skipping setup of repositories as specified by the user.");
×
221
      return;
×
222
    }
223
    RepositoryCommandlet repositoryCommandlet = this.context.getCommandletManager().getCommandlet(RepositoryCommandlet.class);
7✔
224
    repositoryCommandlet.reset();
2✔
225
    repositoryCommandlet.run();
2✔
226
  }
1✔
227

228
  private void createStartScripts() {
229

230
    List<String> ides = IdeVariables.CREATE_START_SCRIPTS.get(this.context);
6✔
231
    if (ides == null) {
2✔
232
      this.context.info("Variable CREATE_START_SCRIPTS is undefined - skipping start script creation.");
4✔
233
      return;
1✔
234
    }
235
    for (String ide : ides) {
10✔
236
      ToolCommandlet tool = this.context.getCommandletManager().getToolCommandlet(ide);
6✔
237
      if (tool == null) {
2!
238
        this.context.error("Undefined IDE '{}' configured in variable CREATE_START_SCRIPTS.");
×
239
      } else {
240
        createStartScript(ide);
3✔
241
      }
242
    }
1✔
243
  }
1✔
244

245
  private void createStartScript(String ide) {
246

247
    this.context.info("Creating start scripts for {}", ide);
10✔
248
    Path workspaces = this.context.getIdeHome().resolve(IdeContext.FOLDER_WORKSPACES);
6✔
249
    try (Stream<Path> childStream = Files.list(workspaces)) {
3✔
250
      Iterator<Path> iterator = childStream.iterator();
3✔
251
      while (iterator.hasNext()) {
3✔
252
        Path child = iterator.next();
4✔
253
        if (Files.isDirectory(child)) {
5!
254
          createStartScript(ide, child.getFileName().toString());
6✔
255
        }
256
      }
1✔
257
    } catch (IOException e) {
×
258
      throw new RuntimeException("Failed to list children of directory " + workspaces, e);
×
259
    }
1✔
260
  }
1✔
261

262
  private void createStartScript(String ide, String workspace) {
263

264
    Path ideHome = this.context.getIdeHome();
4✔
265
    String scriptName = ide + "-" + workspace;
4✔
266
    boolean windows = this.context.getSystemInfo().isWindows();
5✔
267
    if (windows) {
2!
268
      scriptName = scriptName + ".bat";
×
269
    } else {
270
      scriptName = scriptName + ".sh";
3✔
271
    }
272
    Path scriptPath = ideHome.resolve(scriptName);
4✔
273
    if (Files.exists(scriptPath)) {
5!
274
      return;
×
275
    }
276
    String scriptContent;
277
    if (windows) {
2!
278
      scriptContent = "@echo off\r\n"
×
279
          + "pushd %~dp0\r\n"
280
          + "cd workspaces/" + workspace + "\r\n"
281
          + "call ide " + ide + "\r\n"
282
          + "popd\r\n";
283
    } else {
284
      scriptContent = "#!/usr/bin/env bash\n"
4✔
285
          + "cd \"$(dirname \"$0\")\"\n"
286
          + "cd workspaces/" + workspace + "\n"
287
          + "ideasy " + ide + "\n";
288
    }
289
    FileAccess fileAccess = this.context.getFileAccess();
4✔
290
    fileAccess.writeFileContent(scriptContent, scriptPath);
4✔
291
    fileAccess.makeExecutable(scriptPath);
3✔
292
  }
1✔
293

294
  /**
295
   * Judge if the repository is a code repository.
296
   *
297
   * @return true when the repository is a code repository, otherwise false.
298
   */
299
  protected boolean isCodeRepository() {
300
    return false;
×
301
  }
302

303
  /**
304
   * Strategy for handling repository.
305
   */
306
  protected interface RepositoryStrategy {
307

308
    /**
309
     * Handler for blank repository, displays warning and asks for input of repository URL.
310
     *
311
     * @param context ide context
312
     * @return repository url from user input
313
     */
314
    String handleBlankRepository(IdeContext context);
315

316
    /**
317
     * Handler for default repository "-".
318
     *
319
     * @param context ide context
320
     * @return repository url
321
     */
322
    String handleDefaultRepository(IdeContext context);
323

324
    /**
325
     * Check the given project name, displays warning when name does not meet convention.
326
     *
327
     * @param context ide context
328
     * @param projectName the project name of repository
329
     */
330
    void checkProjectNameConvention(IdeContext context, String projectName);
331

332
    /**
333
     * Initialize the given Git repository.
334
     *
335
     * @param context ide context
336
     * @param gitUrl URL of the git repository
337
     */
338
    void initializeRepository(IdeContext context, GitUrl gitUrl);
339

340
    /**
341
     * Create a new commandlet step.
342
     *
343
     * @param context ide context
344
     * @return the created new commandlet Step
345
     */
346
    Step createNewStep(IdeContext context);
347

348
    /**
349
     * Resolve the given commandlet step.
350
     *
351
     * @param step to resolve
352
     */
353
    void resolveStep(Step step);
354
  }
355

356
  /**
357
   * Strategy implementation for code repository.
358
   */
359
  static class CodeRepositoryStrategy implements RepositoryStrategy {
3✔
360

361
    @Override
362
    public String handleBlankRepository(IdeContext context) {
363
      String message = """
×
364
          No code repository was given after '--code'.
365
          Please give the code repository below that includes your settings folder.
366
          Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc
367
          Code repository URL:""";
368
      return context.askForInput(message);
×
369
    }
370

371
    @Override
372
    public String handleDefaultRepository(IdeContext context) {
373
      String warning = "'-' is found after '--code'. This is invalid.";
2✔
374
      context.warning(warning);
3✔
375
      String message = """
2✔
376
          Please give the code repository below that includes your settings folder.
377
          Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc
378
          Code repository URL:""";
379
      return context.askForInput(message);
4✔
380
    }
381

382
    @Override
383
    public void checkProjectNameConvention(IdeContext context, String projectName) {
384
      if (projectName.contains(IdeContext.SETTINGS_REPOSITORY_KEYWORD)) {
4✔
385
        String warningTemplate = """
2✔
386
            Your git URL is pointing to the project name {} that contains the keyword '{}'.
387
            Therefore we assume that you did a mistake by adding the '--code' option to the ide project creation.
388
            Do you really want to create the project?""";
389
        context.askToContinue(warningTemplate, projectName,
13✔
390
            IdeContext.SETTINGS_REPOSITORY_KEYWORD);
391
      }
392
    }
1✔
393

394
    @Override
395
    public void initializeRepository(IdeContext context, GitUrl gitUrl) {
396
      // clone the given repository into IDE_HOME/workspaces/main
397
      Path codeRepoPath = context.getWorkspacePath().resolve(gitUrl.getProjectName());
6✔
398
      context.getGitContext().pullOrClone(gitUrl, codeRepoPath);
5✔
399

400
      // check for settings folder and create symlink to IDE_HOME/settings
401
      Path settingsFolder = codeRepoPath.resolve(IdeContext.FOLDER_SETTINGS);
4✔
402
      if (Files.exists(settingsFolder)) {
5!
403
        context.getFileAccess().symlink(settingsFolder, context.getSettingsPath());
×
404
        // create a file in IDE_HOME with the current local commit id
405
        context.getGitContext().saveCurrentCommitId(codeRepoPath,
×
406
            context.getSettingsCommitIdPath());
×
407
      } else {
408
        context.warning("No settings folder was found inside the code repository.");
3✔
409
      }
410
    }
1✔
411

412
    @Override
413
    public Step createNewStep(IdeContext context) {
414
      return context.newStep("Clone code repository");
4✔
415
    }
416

417
    @Override
418
    public void resolveStep(Step step) {
419
      step.success("Successfully updated code repository.");
3✔
420
    }
1✔
421
  }
422

423
  /**
424
   * Strategy implementation for settings repository.
425
   */
426
  static class SettingsRepositoryStrategy implements RepositoryStrategy {
3✔
427

428
    @Override
429
    public String handleBlankRepository(IdeContext context) {
430
      Path settingsPath = context.getSettingsPath();
3✔
431
      String message = "Missing your settings at " + settingsPath
4✔
432
          + " and no SETTINGS_URL is defined.\n"
433
          + "Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc\n"
434
          + "Please contact the technical lead of your project to get the SETTINGS_URL for your project.\n"
435
          + "In case you just want to test IDEasy you may simply hit return to install the default settings.\n"
436
          + "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:";
437
      return context.askForInput(message, IdeContext.DEFAULT_SETTINGS_REPO_URL);
5✔
438
    }
439

440
    @Override
441
    public String handleDefaultRepository(IdeContext context) {
442
      String message = "'-' is found for settings repository, the default settings repository '{}' will be used.";
2✔
443
      context.info(message, IdeContext.DEFAULT_SETTINGS_REPO_URL);
9✔
444
      return IdeContext.DEFAULT_SETTINGS_REPO_URL;
2✔
445
    }
446

447
    @Override
448
    public void checkProjectNameConvention(IdeContext context, String projectName) {
449
      if (!projectName.contains(IdeContext.SETTINGS_REPOSITORY_KEYWORD)) {
4✔
450
        String warningTemplate = """
2✔
451
            Your git URL is pointing to the project name {} that does not contain the keyword ''{}''.
452
            Therefore we assume that you forgot to add the '--code' option to the ide project creation.
453
            Do you really want to create the project?""";
454
        context.askToContinue(warningTemplate, projectName,
13✔
455
            IdeContext.SETTINGS_REPOSITORY_KEYWORD);
456
      }
457
    }
1✔
458

459
    @Override
460
    public void initializeRepository(IdeContext context, GitUrl gitUrl) {
461
      Path settingsPath = context.getSettingsPath();
3✔
462
      GitContext gitContext = context.getGitContext();
3✔
463
      gitContext.pullOrClone(gitUrl, settingsPath);
4✔
464
      context.getGitContext().saveCurrentCommitId(settingsPath,
5✔
465
          context.getSettingsCommitIdPath());
1✔
466
    }
1✔
467

468
    @Override
469
    public Step createNewStep(IdeContext context) {
470
      return context.newStep("Clone settings repository");
4✔
471
    }
472

473
    @Override
474
    public void resolveStep(Step step) {
475
      step.success("Successfully updated settings repository.");
3✔
476
    }
1✔
477
  }
478

479
  protected void processRepositoryUsingStrategy(RepositoryStrategy strategy) {
480
    Step step = strategy.createNewStep(this.context);
5✔
481
    String repository = this.settingsRepo.getValue();
5✔
482
    while (repository == null || repository.isBlank()) {
5!
483
      repository = strategy.handleBlankRepository(this.context);
6✔
484
    }
485
    while ("-".equals(repository)) {
4✔
486
      repository = strategy.handleDefaultRepository(context);
6✔
487
    }
488
    GitUrl gitUrl = GitUrl.of(repository);
3✔
489
    strategy.checkProjectNameConvention(this.context, gitUrl.getProjectName());
6✔
490
    strategy.initializeRepository(this.context, gitUrl);
5✔
491
    strategy.resolveStep(step);
3✔
492
  }
1✔
493
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc