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

devonfw / IDEasy / 24842698420

23 Apr 2026 03:05PM UTC coverage: 70.623% (+0.009%) from 70.614%
24842698420

push

github

web-flow
#1693: Soft-delete corrupted repo after confirmation on 'ide update' (#1819)

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

4329 of 6774 branches covered (63.91%)

Branch coverage included in aggregate %.

11196 of 15209 relevant lines covered (73.61%)

3.11 hits per line

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

81.49
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 org.slf4j.Logger;
13
import org.slf4j.LoggerFactory;
14

15
import com.devonfw.tools.ide.cli.CliException;
16
import com.devonfw.tools.ide.context.AbstractIdeContext;
17
import com.devonfw.tools.ide.context.IdeContext;
18
import com.devonfw.tools.ide.context.IdeStartContextImpl;
19
import com.devonfw.tools.ide.git.GitContext;
20
import com.devonfw.tools.ide.git.GitUrl;
21
import com.devonfw.tools.ide.git.repository.RepositoryCommandlet;
22
import com.devonfw.tools.ide.io.FileAccess;
23
import com.devonfw.tools.ide.property.FlagProperty;
24
import com.devonfw.tools.ide.property.StringProperty;
25
import com.devonfw.tools.ide.step.Step;
26
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
27
import com.devonfw.tools.ide.tool.ToolCommandlet;
28
import com.devonfw.tools.ide.tool.ToolEdition;
29
import com.devonfw.tools.ide.tool.ToolEditionAndVersion;
30
import com.devonfw.tools.ide.tool.ToolInstallRequest;
31
import com.devonfw.tools.ide.tool.custom.CustomToolCommandlet;
32
import com.devonfw.tools.ide.tool.custom.CustomToolMetadata;
33
import com.devonfw.tools.ide.tool.extra.ExtraToolInstallation;
34
import com.devonfw.tools.ide.tool.extra.ExtraTools;
35
import com.devonfw.tools.ide.tool.extra.ExtraToolsMapper;
36
import com.devonfw.tools.ide.variable.IdeVariables;
37
import com.devonfw.tools.ide.version.VersionIdentifier;
38

39
/**
40
 * Abstract {@link Commandlet} base-class for both {@link UpdateCommandlet} and {@link CreateCommandlet}.
41
 */
42
public abstract class AbstractUpdateCommandlet extends Commandlet {
43

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

46
  private static final String MESSAGE_CODE_REPO_URL = """
47
      No code repository was given after '--code'.
48
      Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc
49
      Please enter the code repository below that includes your settings folder.""";
50

51
  private static final String MESSAGE_SETTINGS_REPO_URL = """
52
      No settings found at {} and no SETTINGS_URL is defined.
53
      Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc
54
      Please contact the technical lead of your project to get the SETTINGS_URL for your project to enter.
55
      In case you just want to test IDEasy you may simply hit return to install the default settings.""";
56

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

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

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

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

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

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

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

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

92
  @Override
93
  protected void doRun() {
94

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

100
    if (!this.context.isSettingsRepositorySymlinkOrJunction() || this.context.isForceMode() || forcePull.isTrue()) {
4 !
101
      updateSettings();
2 ✔
102
    }
103
    updateConf();
2 ✔
104
    reloadContext();
2 ✔
105

106
    updateSoftware();
2 ✔
107
    updateRepositories();
2 ✔
108
    createStartScripts();
2 ✔
109
  }
1 ✔
110

111
  private void reloadContext() {
112

113
    ((AbstractIdeContext) this.context).reload();
4 ✔
114
  }
1 ✔
115

116
  private void updateConf() {
117

118
    Path templatesFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_TEMPLATES);
6 ✔
119
    if (!Files.exists(templatesFolder)) {
5 ✔
120
      Path legacyTemplatesFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_LEGACY_TEMPLATES);
6 ✔
121
      if (Files.exists(legacyTemplatesFolder)) {
5 !
122
        templatesFolder = legacyTemplatesFolder;
×
123
      } else {
124
        LOG.warn("Templates folder is missing in settings repository.");
3 ✔
125
        return;
1 ✔
126
      }
127
    }
128

129
    Step step = this.context.newStep("Copy configuration templates", templatesFolder);
11 ✔
130
    final Path finalTemplatesFolder = templatesFolder;
2 ✔
131
    step.run(() -> setupConf(finalTemplatesFolder, this.context.getIdeHome()));
13 ✔
132
  }
1 ✔
133

134
  private void setupConf(Path template, Path conf) {
135

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

139
      String basename = child.getFileName().toString();
4 ✔
140
      Path confPath = conf.resolve(basename);
4 ✔
141

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

160
  /**
161
   * 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
162
   * latest current commit ID in the file ".commit.id".
163
   */
164
  protected void updateSettings() {
165

166
    this.context.newStep(getStepMessage()).run(this::updateSettingsInStep);
9 ✔
167
  }
1 ✔
168

169
  protected String getStepMessage() {
170

171
    return "update (pull) settings repository";
2 ✔
172
  }
173

174
  private void updateSettingsInStep() {
175
    Path settingsPath = this.context.getSettingsPath();
4 ✔
176
    GitContext gitContext = this.context.getGitContext();
4 ✔
177
    // here we do not use pullOrClone to prevent asking a pointless question for repository URL...
178
    if (Files.isDirectory(settingsPath) && this.context.getGitContext().isGitRepo(settingsPath) || this.context.isSettingsRepositorySymlinkOrJunction()) {
15 !
179
      if (this.context.isForcePull() || this.context.isForceMode()) {
8 !
180
        if (gitContext.hasUntrackedFiles(settingsPath)) {
×
181
          gitContext.pullSafelyWithStash(settingsPath);
×
182
        } else {
183
          gitContext.pull(settingsPath);
×
184
        }
185
        this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath());
×
186
      } else {
187
        LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling.");
4 ✔
188
      }
189
    } else {
190
      if (!this.context.getFileAccess().isEmptyDir(settingsPath)) {
6 !
191
        this.context.askToContinue(
×
192
          "Your settings repository seems to be broken ('.git' folder not present). We can fix this by moving "
193
          + " your settings the backed up. You will be asked for the settings git URL and your settings will be cloned from scratch. Do you want to proceed?"
194
        );
195

196
        this.context.getFileAccess().backup(settingsPath);
×
197
      }
198

199
      GitUrl gitUrl = getOrAskSettingsUrl();
3 ✔
200
      checkProjectNameConvention(gitUrl.getProjectName());
4 ✔
201
      initializeRepository(gitUrl);
3 ✔
202
    }
203
  }
1 ✔
204

205
  private GitUrl getOrAskSettingsUrl() {
206

207
    String repository = this.settingsRepo.getValue();
5 ✔
208
    repository = handleDefaultRepository(repository);
4 ✔
209
    String userPromt;
210
    String defaultUrl;
211
    if (isCodeRepository()) {
3 ✔
212
      userPromt = "Code repository URL:";
2 ✔
213
      defaultUrl = null;
2 ✔
214
      LOG.info(MESSAGE_CODE_REPO_URL);
4 ✔
215
    } else {
216
      userPromt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:";
2 ✔
217
      defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL;
2 ✔
218
      LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath());
6 ✔
219
    }
220
    GitUrl gitUrl = null;
2 ✔
221
    if (repository != null) {
2 ✔
222
      gitUrl = GitUrl.of(repository);
3 ✔
223
    }
224
    while ((gitUrl == null) || !gitUrl.isValid()) {
5 !
225
      repository = this.context.askForInput(userPromt, defaultUrl);
6 ✔
226
      repository = handleDefaultRepository(repository);
4 ✔
227
      gitUrl = GitUrl.of(repository);
3 ✔
228
      if (!gitUrl.isValid()) {
3 !
229
        LOG.warn("The input URL is not valid, please try again.");
×
230
      }
231
    }
232
    return gitUrl;
2 ✔
233
  }
234

235
  private String handleDefaultRepository(String repository) {
236
    if ("-".equals(repository)) {
4 ✔
237
      if (isCodeRepository()) {
3 ✔
238
        LOG.warn("'-' is found after '--code'. This is invalid.");
3 ✔
239
        repository = null;
3 ✔
240
      } else {
241
        LOG.info("'-' was found for settings repository, the default settings repository '{}' will be used.", IdeContext.DEFAULT_SETTINGS_REPO_URL);
4 ✔
242
        repository = IdeContext.DEFAULT_SETTINGS_REPO_URL;
2 ✔
243
      }
244
    }
245
    return repository;
2 ✔
246
  }
247

248
  private void checkProjectNameConvention(String projectName) {
249
    boolean isSettingsRepo = projectName.contains(IdeContext.SETTINGS_REPOSITORY_KEYWORD);
4 ✔
250
    boolean codeRepository = isCodeRepository();
3 ✔
251
    if (isSettingsRepo == codeRepository) {
3 ✔
252
      String warningTemplate;
253
      if (codeRepository) {
2 ✔
254
        warningTemplate = """
3 ✔
255
            Your git URL is pointing to the project name {} that contains the keyword '{}'.
256
            Therefore we assume that you did a mistake by adding the '--code' option to the ide project creation.
257
            Do you really want to create the project?""";
258
      } else {
259
        warningTemplate = """
2 ✔
260
            Your git URL is pointing to the project name {} that does not contain the keyword ''{}''.
261
            Therefore we assume that you forgot to add the '--code' option to the ide project creation.
262
            Do you really want to create the project?""";
263
      }
264
      this.context.askToContinue(warningTemplate, projectName, IdeContext.SETTINGS_REPOSITORY_KEYWORD);
14 ✔
265
    }
266
  }
1 ✔
267

268
  private void initializeRepository(GitUrl gitUrl) {
269

270
    GitContext gitContext = this.context.getGitContext();
4 ✔
271
    Path settingsPath = this.context.getSettingsPath();
4 ✔
272
    Path repoPath = settingsPath;
2 ✔
273
    boolean codeRepository = isCodeRepository();
3 ✔
274
    if (codeRepository) {
2 ✔
275
      // clone the given code repository into IDE_HOME/workspaces/main
276
      repoPath = context.getWorkspacePath().resolve(gitUrl.getProjectName());
7 ✔
277
    }
278
    gitContext.pullOrClone(gitUrl, repoPath);
4 ✔
279
    if (codeRepository) {
2 ✔
280
      // check for settings folder and create symlink to IDE_HOME/settings
281
      Path settingsFolder = repoPath.resolve(IdeContext.FOLDER_SETTINGS);
4 ✔
282
      if (Files.exists(settingsFolder)) {
5 !
283
        context.getFileAccess().symlink(settingsFolder, settingsPath);
×
284
      } else {
285
        throw new CliException("Invalid code repository " + gitUrl + ": missing a settings folder at " + settingsFolder);
9 ✔
286
      }
287
    }
288
    this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath());
8 ✔
289
  }
1 ✔
290

291

292
  private void updateSoftware() {
293

294
    if (this.skipTools.isTrue()) {
4 ✔
295
      LOG.info("Skipping installation/update of tools as specified by the user.");
3 ✔
296
      return;
1 ✔
297
    }
298
    Step step = this.context.newStep("Install or update software");
5 ✔
299
    step.run(() -> doUpdateSoftwareStep(step));
10 ✔
300
  }
1 ✔
301

302
  private void doUpdateSoftwareStep(Step step) {
303

304
    Set<ToolCommandlet> toolCommandlets = new HashSet<>();
4 ✔
305
    CommandletManager commandletManager = this.context.getCommandletManager();
4 ✔
306
    // installed tools in IDE_HOME/software
307
    List<Path> softwarePaths = this.context.getFileAccess().listChildren(this.context.getSoftwarePath(), Files::isDirectory);
14 ✔
308
    for (Path softwarePath : softwarePaths) {
10 ✔
309
      String toolName = softwarePath.getFileName().toString();
4 ✔
310
      ToolCommandlet toolCommandlet = commandletManager.getToolCommandlet(toolName);
4 ✔
311
      if (toolCommandlet != null) {
2 !
312
        toolCommandlets.add(toolCommandlet);
4 ✔
313
      }
314
    }
1 ✔
315

316
    // regular tools in $IDE_TOOLS
317
    List<String> regularTools = IdeVariables.IDE_TOOLS.get(this.context);
6 ✔
318
    if (regularTools != null) {
2 !
319
      for (String regularTool : regularTools) {
10 ✔
320
        ToolCommandlet toolCommandlet = commandletManager.getToolCommandlet(regularTool);
4 ✔
321
        if (toolCommandlet == null) {
2 !
322
          String displayName = (regularTool == null || regularTool.isBlank()) ? "<empty>" : "'" + regularTool + "'";
×
323
          LOG.error("Cannot install or update tool '{}''. No matching commandlet found. Please check your IDE_TOOLS configuration.", displayName);
×
324
        } else {
×
325
          toolCommandlets.add(toolCommandlet);
4 ✔
326
        }
327
      }
1 ✔
328
    }
329

330
    // custom tools in ide-custom-tools.json
331
    for (CustomToolMetadata customTool : this.context.getCustomToolRepository().getTools()) {
9 !
332
      CustomToolCommandlet customToolCommandlet = new CustomToolCommandlet(this.context, customTool);
×
333
      toolCommandlets.add(customToolCommandlet);
×
334
    }
×
335

336
    // update/install the toolCommandlets
337
    for (ToolCommandlet toolCommandlet : toolCommandlets) {
10 ✔
338
      this.context.newStep("Install " + toolCommandlet.getName()).run(() -> toolCommandlet.install(false));
15 ✔
339
    }
1 ✔
340

341
    ExtraTools extraTools = ExtraToolsMapper.get().loadJsonFromFolder(this.context.getSettingsPath());
7 ✔
342
    if (extraTools != null) {
2 ✔
343
      List<String> toolNames = extraTools.getSortedToolNames();
3 ✔
344
      LOG.info("Found extra installation of the following tools: {}", toolNames);
4 ✔
345
      for (String tool : toolNames) {
10 ✔
346
        List<ExtraToolInstallation> installations = extraTools.getExtraInstallations(tool);
4 ✔
347
        this.context.newStep("Install extra version(s) of " + tool).run(() -> installExtraToolInstallations(tool, installations));
16 ✔
348
      }
1 ✔
349
    }
350
  }
1 ✔
351

352
  private void installExtraToolInstallations(String tool, List<ExtraToolInstallation> extraInstallations) {
353

354
    CommandletManager commandletManager = this.context.getCommandletManager();
4 ✔
355
    FileAccess fileAccess = this.context.getFileAccess();
4 ✔
356
    Path extraPath = this.context.getSoftwareExtraPath();
4 ✔
357
    LocalToolCommandlet toolCommandlet = commandletManager.getRequiredLocalToolCommandlet(tool);
4 ✔
358
    for (ExtraToolInstallation extraInstallation : extraInstallations) {
10 ✔
359
      ToolInstallRequest request = new ToolInstallRequest(false);
5 ✔
360
      String edition = extraInstallation.edition();
3 ✔
361
      if (edition == null) {
2 ✔
362
        edition = toolCommandlet.getConfiguredEdition();
3 ✔
363
      }
364
      ToolEdition toolEdition = new ToolEdition(tool, edition);
6 ✔
365
      VersionIdentifier version = extraInstallation.version();
3 ✔
366
      request.setRequested(new ToolEditionAndVersion(toolEdition, version));
7 ✔
367
      Path extraToolPath = extraPath.resolve(tool);
4 ✔
368
      Path toolPath = extraToolPath.resolve(extraInstallation.name());
5 ✔
369
      request.setToolPathForExtraInstallation(toolPath);
3 ✔
370
      toolCommandlet.install(request);
4 ✔
371
    }
1 ✔
372
  }
1 ✔
373

374
  private void updateRepositories() {
375

376
    if (this.skipRepositories.isTrue()) {
4 !
377
      if (this.forceRepositories.isTrue()) {
×
378
        LOG.warn("Options to skip and force repositories are incompatible and should not be combined. Ignoring --force-repositories to proceed.");
×
379
      }
380
      LOG.info("Skipping setup of repositories as specified by the user.");
×
381
      return;
×
382
    }
383
    RepositoryCommandlet repositoryCommandlet = this.context.getCommandletManager().getCommandlet(RepositoryCommandlet.class);
7 ✔
384
    repositoryCommandlet.reset();
2 ✔
385
    repositoryCommandlet.run();
2 ✔
386
  }
1 ✔
387

388
  private void createStartScripts() {
389

390
    List<String> ides = IdeVariables.CREATE_START_SCRIPTS.get(this.context);
6 ✔
391
    if (ides == null) {
2 ✔
392
      LOG.info("Variable CREATE_START_SCRIPTS is undefined - skipping start script creation.");
3 ✔
393
      return;
1 ✔
394
    }
395
    for (String ide : ides) {
10 ✔
396
      ToolCommandlet tool = this.context.getCommandletManager().getToolCommandlet(ide);
6 ✔
397
      if (tool == null) {
2 !
398
        LOG.error("Undefined IDE '{}' configured in variable CREATE_START_SCRIPTS.", ide);
×
399
      } else {
400
        createStartScript(ide);
3 ✔
401
      }
402
    }
1 ✔
403
  }
1 ✔
404

405
  private void createStartScript(String ide) {
406

407
    LOG.info("Creating start scripts for {}", ide);
4 ✔
408
    Path workspaces = this.context.getIdeHome().resolve(IdeContext.FOLDER_WORKSPACES);
6 ✔
409
    try (Stream<Path> childStream = Files.list(workspaces)) {
3 ✔
410
      Iterator<Path> iterator = childStream.iterator();
3 ✔
411
      while (iterator.hasNext()) {
3 ✔
412
        Path child = iterator.next();
4 ✔
413
        if (Files.isDirectory(child)) {
5 !
414
          createStartScript(ide, child.getFileName().toString());
6 ✔
415
        }
416
      }
1 ✔
417
    } catch (IOException e) {
×
418
      throw new RuntimeException("Failed to list children of directory " + workspaces, e);
×
419
    }
1 ✔
420
  }
1 ✔
421

422
  private void createStartScript(String ide, String workspace) {
423

424
    Path ideHome = this.context.getIdeHome();
4 ✔
425
    String scriptName = ide + "-" + workspace;
4 ✔
426
    boolean windows = this.context.getSystemInfo().isWindows();
5 ✔
427
    if (windows) {
2 !
428
      scriptName = scriptName + ".bat";
×
429
    } else {
430
      scriptName = scriptName + ".sh";
3 ✔
431
    }
432
    Path scriptPath = ideHome.resolve(scriptName);
4 ✔
433
    if (Files.exists(scriptPath)) {
5 !
434
      return;
×
435
    }
436
    String scriptContent;
437
    if (windows) {
2 !
438
      scriptContent = "@echo off\r\n" + "pushd %~dp0\r\n" + "cd workspaces/" + workspace + "\r\n" + "call ide " + ide + "\r\n" + "popd\r\n";
×
439
    } else {
440
      scriptContent = "#!/usr/bin/env bash\n" + "cd \"$(dirname \"$0\")\"\n" + "cd workspaces/" + workspace + "\n" + "ideasy " + ide + "\n";
4 ✔
441
    }
442
    FileAccess fileAccess = this.context.getFileAccess();
4 ✔
443
    fileAccess.writeFileContent(scriptContent, scriptPath);
4 ✔
444
    fileAccess.makeExecutable(scriptPath);
3 ✔
445
  }
1 ✔
446

447
  /**
448
   * Judge if the repository is a code repository.
449
   *
450
   * @return true when the repository is a code repository, otherwise false.
451
   */
452
  protected boolean isCodeRepository() {
453
    return false;
2 ✔
454
  }
455

456
}
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