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

devonfw / IDEasy / 32505851110

21 Aug 2026 05:00PM UTC coverage: 73.448% (-0.04%) from 73.49%
32505851110

Pull #2335

github

web-flow
Merge b9ed3e799 into bcf1ac79a
Pull Request #2335: #1695: Clone settings to temporary directory, analyse, and then move

5277 of 8010 branches covered (65.88%)

Branch coverage included in aggregate %.

13862 of 18048 relevant lines covered (76.81%)

3.27 hits per line

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

78.66
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.git.repository.RepositoryUtil;
23
import com.devonfw.tools.ide.io.FileAccess;
24
import com.devonfw.tools.ide.property.FlagProperty;
25
import com.devonfw.tools.ide.property.StringProperty;
26
import com.devonfw.tools.ide.step.Step;
27
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
28
import com.devonfw.tools.ide.tool.ToolCommandlet;
29
import com.devonfw.tools.ide.tool.ToolEdition;
30
import com.devonfw.tools.ide.tool.ToolEditionAndVersion;
31
import com.devonfw.tools.ide.tool.ToolInstallRequest;
32
import com.devonfw.tools.ide.tool.custom.CustomToolCommandlet;
33
import com.devonfw.tools.ide.tool.custom.CustomToolMetadata;
34
import com.devonfw.tools.ide.tool.extra.ExtraToolInstallation;
35
import com.devonfw.tools.ide.tool.extra.ExtraTools;
36
import com.devonfw.tools.ide.tool.extra.ExtraToolsMapper;
37
import com.devonfw.tools.ide.variable.IdeVariables;
38
import com.devonfw.tools.ide.version.VersionIdentifier;
39

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

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

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

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

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

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

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

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

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

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

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

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

93
  @Override
94
  protected void doRun() {
95

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

101
    updateSettings();
2✔
102
    updateConf();
2✔
103
    reloadContext();
2✔
104
    this.context.verifyIdeMinVersion(true);
4✔
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
    boolean codeRepository = this.context.isSettingsCodeRepository();
4✔
167
    if (codeRepository && !(this.context.isForceMode() || forcePull.isTrue())) {
2!
168
      LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling.");
×
169
      return;
×
170
    }
171
    this.context.newStep(getStepMessage()).run(() -> updateSettingsInStep(codeRepository), true);
15✔
172
  }
1✔
173

174
  protected String getStepMessage() {
175

176
    return "update (pull) settings repository";
2✔
177
  }
178

179
  private void updateSettingsInStep(boolean codeRepository) {
180
    Path settingsPath = this.context.getSettingsPath();
4✔
181
    if (!codeRepository) {
2!
182
      boolean settingsRepository = this.context.getGitContext().isGitRepo(settingsPath);
6✔
183
      if (!settingsRepository) {
2✔
184
        if (Files.exists(settingsPath)) {
5!
185
          if (!this.context.getFileAccess().isEmptyDir(settingsPath)) {
×
186
            // settings folder seems to be invalid
187
            this.context.askToContinue(
×
188
                "Your settings repository seems to be broken ('.git' folder not present). "
189
                    + "We can fix this by moving  your settings the backed up. "
190
                    + "You will be asked for the settings git URL and your settings will be cloned from scratch. "
191
                    + "Do you want to proceed?"
192
            );
193
          }
194
          this.context.getFileAccess().backup(settingsPath);
×
195
        }
196
        //settings folder does not exist (yet), lets retrieve the settings url to pull
197
        GitUrl gitUrl = getOrAskSettingsUrl();
3✔
198
        pullAndCheckIntegrity(gitUrl);
3✔
199
        return;
1✔
200
      }
201
    }
202
    GitContext gitContext = this.context.getGitContext();
4✔
203
    if (gitContext.hasUntrackedFiles(settingsPath)) {
4!
204
      gitContext.pullSafelyWithStash(settingsPath);
×
205
    } else {
206
      gitContext.pull(settingsPath);
3✔
207
    }
208
    this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath());
8✔
209
  }
1✔
210

211
  private GitUrl getOrAskSettingsUrl() {
212

213
    String repository = this.settingsRepo.getValue();
5✔
214
    repository = handleDefaultRepository(repository);
4✔
215
    String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:";
2✔
216
    String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL;
2✔
217
    LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath());
6✔
218

219
    GitUrl gitUrl = null;
2✔
220
    if (repository != null) {
2✔
221
      gitUrl = GitUrl.of(repository);
3✔
222
    }
223
    while ((gitUrl == null) || !gitUrl.isValid()) {
5!
224
      repository = this.context.askForInput(userPrompt, defaultUrl);
6✔
225
      repository = handleDefaultRepository(repository);
4✔
226
      gitUrl = GitUrl.of(repository);
3✔
227
      if (!gitUrl.isValid()) {
3!
228
        LOG.warn("The input URL is not valid, please try again.");
×
229
      }
230
    }
231
    return gitUrl;
2✔
232
  }
233

234
  /**
235
   * We pull the settings repo from the remote into a temporary folder to perform health checks.
236
   */
237
  private void pullAndCheckIntegrity(GitUrl gitUrl) {
238
    GitContext gitContext = this.context.getGitContext();
4✔
239
    Path tempProjectPath = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(this.context.getProjectName());
10✔
240

241
    gitContext.pullOrClone(gitUrl, tempProjectPath);
4✔
242

243
    checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName());
5✔
244
  }
1✔
245

246
  private void checkIntegrityAndMove(Path projectPath, String gitProjectName) {
247

248
    FileAccess fileAccess = this.context.getFileAccess();
4✔
249

250
    if (!Files.exists(projectPath)) {
5!
251
      throw new CliException(getIntegrityCheckErrorMessage("Git pull target folder does not exist."));
×
252
    }
253

254
    Path targetDirectory;
255
    switch (RepositoryUtil.getRepositoryType(projectPath, gitProjectName)) {
7!
256
      case SETTINGS -> {
257

258
        targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS);
6✔
259
        moveProject(projectPath, targetDirectory);
5✔
260
        this.context.getGitContext().saveCurrentCommitId(targetDirectory, this.context.getSettingsCommitIdPath());
8✔
261
      }
1✔
262
      case CODE_SETTINGS_COMBINED -> {
263

264
        //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings.
265
        //(Formerly managed by the obsolete "--code" flag)
266
        targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName);
×
267
        moveProject(projectPath, targetDirectory);
×
268

269
        Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS);
×
270
        Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS);
×
271

272
        fileAccess.symlink(symlinkTargetPath, symlinkPath);
×
273
        this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath());
×
274
      }
×
275
      default -> {
276
        fileAccess.backup(projectPath);
4✔
277
        throw new CliException(getIntegrityCheckErrorMessage(String.format(
14✔
278
            "The given git repository URL does not point to a valid settings or code-settings repository. "
279
                + "Please verify and try again. Before trying again, please delete the folder %s",
280
            this.context.getIdeHome())));
2✔
281
      }
282
    }
283
  }
1✔
284

285
  private Path moveProject(Path from, Path to) {
286

287
    FileAccess fileAccess = this.context.getFileAccess();
4✔
288
    try {
289
      fileAccess.move(from, to);
6✔
290
    } catch (Exception e) {
×
291
      throw new CliException(getIntegrityCheckErrorMessage(String.format("Failed to move project from %s to %s", from, to)), e);
×
292
    }
1✔
293
    return to;
2✔
294
  }
295

296
  private String getIntegrityCheckErrorMessage(String message) {
297
    return String.format("Settings repository integrity check failed: %s", message);
9✔
298
  }
299

300
  private String handleDefaultRepository(String repository) {
301
    if ("-".equals(repository)) {
4✔
302
      LOG.info("'-' was found for the repository, the default settings repository '{}' will be used.", IdeContext.DEFAULT_SETTINGS_REPO_URL);
4✔
303
      repository = IdeContext.DEFAULT_SETTINGS_REPO_URL;
2✔
304
    }
305
    return repository;
2✔
306
  }
307

308
  private void updateSoftware() {
309

310
    if (this.skipTools.isTrue()) {
4✔
311
      LOG.info("Skipping installation/update of tools as specified by the user.");
3✔
312
      return;
1✔
313
    }
314
    Step step = this.context.newStep("Install or update software");
5✔
315
    step.run(() -> doUpdateSoftwareStep(step));
10✔
316
  }
1✔
317

318
  private void doUpdateSoftwareStep(Step step) {
319

320
    Set<ToolCommandlet> toolCommandlets = new HashSet<>();
4✔
321
    CommandletManager commandletManager = this.context.getCommandletManager();
4✔
322
    // installed tools in IDE_HOME/software
323
    List<Path> softwarePaths = this.context.getFileAccess().listChildren(this.context.getSoftwarePath(), Files::isDirectory);
14✔
324
    for (Path softwarePath : softwarePaths) {
10✔
325
      String toolName = softwarePath.getFileName().toString();
4✔
326
      ToolCommandlet toolCommandlet = commandletManager.getToolCommandlet(toolName);
4✔
327
      if (toolCommandlet != null) {
2!
328
        toolCommandlets.add(toolCommandlet);
4✔
329
      }
330
    }
1✔
331

332
    // regular tools in $IDE_TOOLS
333
    List<String> regularTools = IdeVariables.IDE_TOOLS.get(this.context);
6✔
334
    if (regularTools != null) {
2!
335
      for (String regularTool : regularTools) {
10✔
336
        ToolCommandlet toolCommandlet = commandletManager.getToolCommandlet(regularTool);
4✔
337
        if (toolCommandlet == null) {
2!
338
          String displayName = (regularTool == null || regularTool.isBlank()) ? "<empty>" : "'" + regularTool + "'";
×
339
          LOG.error("Cannot install or update tool '{}''. No matching commandlet found. Please check your IDE_TOOLS configuration.", displayName);
×
340
        } else {
×
341
          toolCommandlets.add(toolCommandlet);
4✔
342
        }
343
      }
1✔
344
    }
345

346
    // custom tools in ide-custom-tools.json
347
    for (CustomToolMetadata customTool : this.context.getCustomToolRepository().getTools()) {
9!
348
      CustomToolCommandlet customToolCommandlet = new CustomToolCommandlet(this.context, customTool);
×
349
      toolCommandlets.add(customToolCommandlet);
×
350
    }
×
351

352
    // update/install the toolCommandlets
353
    for (ToolCommandlet toolCommandlet : toolCommandlets) {
10✔
354
      this.context.newStep("Install " + toolCommandlet.getName()).run(() -> toolCommandlet.install(false));
15✔
355
    }
1✔
356

357
    ExtraTools extraTools = ExtraToolsMapper.get().loadJsonFromFolder(this.context.getSettingsPath());
7✔
358
    if (extraTools != null) {
2✔
359
      List<String> toolNames = extraTools.getSortedToolNames();
3✔
360
      LOG.info("Found extra installation of the following tools: {}", toolNames);
4✔
361
      for (String tool : toolNames) {
10✔
362
        List<ExtraToolInstallation> installations = extraTools.getExtraInstallations(tool);
4✔
363
        this.context.newStep("Install extra version(s) of " + tool).run(() -> installExtraToolInstallations(tool, installations));
16✔
364
      }
1✔
365
    }
366

367
  }
1✔
368

369
  private void installExtraToolInstallations(String tool, List<ExtraToolInstallation> extraInstallations) {
370

371
    CommandletManager commandletManager = this.context.getCommandletManager();
4✔
372
    FileAccess fileAccess = this.context.getFileAccess();
4✔
373
    Path extraPath = this.context.getSoftwareExtraPath();
4✔
374
    LocalToolCommandlet toolCommandlet = commandletManager.getRequiredLocalToolCommandlet(tool);
4✔
375
    for (ExtraToolInstallation extraInstallation : extraInstallations) {
10✔
376
      ToolInstallRequest request = new ToolInstallRequest(false);
5✔
377
      String edition = extraInstallation.edition();
3✔
378
      if (edition == null) {
2✔
379
        edition = toolCommandlet.getConfiguredEdition();
3✔
380
      }
381
      ToolEdition toolEdition = new ToolEdition(tool, edition);
6✔
382
      VersionIdentifier version = extraInstallation.version();
3✔
383
      request.setRequested(new ToolEditionAndVersion(toolEdition, version));
7✔
384
      Path extraToolPath = extraPath.resolve(tool);
4✔
385
      Path toolPath = extraToolPath.resolve(extraInstallation.name());
5✔
386
      request.setToolPathForExtraInstallation(toolPath);
3✔
387
      toolCommandlet.install(request);
4✔
388
    }
1✔
389
  }
1✔
390

391
  private void updateRepositories() {
392

393
    if (this.skipRepositories.isTrue()) {
4!
394
      if (this.forceRepositories.isTrue()) {
×
395
        LOG.warn("Options to skip and force repositories are incompatible and should not be combined. Ignoring --force-repositories to proceed.");
×
396
      }
397
      LOG.info("Skipping setup of repositories as specified by the user.");
×
398
      return;
×
399
    }
400
    RepositoryCommandlet repositoryCommandlet = this.context.getCommandletManager().getCommandlet(RepositoryCommandlet.class);
7✔
401
    repositoryCommandlet.reset();
2✔
402
    repositoryCommandlet.run();
2✔
403
  }
1✔
404

405
  private void createStartScripts() {
406

407
    List<String> ides = IdeVariables.CREATE_START_SCRIPTS.get(this.context);
6✔
408
    if (ides == null) {
2✔
409
      LOG.info("Variable CREATE_START_SCRIPTS is undefined - skipping start script creation.");
3✔
410
      return;
1✔
411
    }
412
    for (String ide : ides) {
10✔
413
      ToolCommandlet tool = this.context.getCommandletManager().getToolCommandlet(ide);
6✔
414
      if (tool == null) {
2!
415
        LOG.error("Undefined IDE '{}' configured in variable CREATE_START_SCRIPTS.", ide);
×
416
      } else {
417
        createStartScript(ide);
3✔
418
      }
419
    }
1✔
420
  }
1✔
421

422
  private void createStartScript(String ide) {
423

424
    LOG.info("Creating start scripts for {}", ide);
4✔
425
    Path workspaces = this.context.getIdeHome().resolve(IdeContext.FOLDER_WORKSPACES);
6✔
426
    try (Stream<Path> childStream = Files.list(workspaces)) {
3✔
427
      Iterator<Path> iterator = childStream.iterator();
3✔
428
      while (iterator.hasNext()) {
3✔
429
        Path child = iterator.next();
4✔
430
        if (Files.isDirectory(child)) {
5!
431
          createStartScript(ide, child.getFileName().toString());
6✔
432
        }
433
      }
1✔
434
    } catch (IOException e) {
×
435
      throw new RuntimeException("Failed to list children of directory " + workspaces, e);
×
436
    }
1✔
437
  }
1✔
438

439
  private void createStartScript(String ide, String workspace) {
440

441
    Path ideHome = this.context.getIdeHome();
4✔
442
    String scriptName = ide + "-" + workspace;
4✔
443
    boolean windows = this.context.getSystemInfo().isWindows();
5✔
444
    if (windows) {
2!
445
      scriptName = scriptName + ".bat";
×
446
    } else {
447
      scriptName = scriptName + ".sh";
3✔
448
    }
449
    Path scriptPath = ideHome.resolve(scriptName);
4✔
450
    if (Files.exists(scriptPath)) {
5!
451
      return;
×
452
    }
453
    String scriptContent;
454
    if (windows) {
2!
455
      scriptContent = "@echo off\r\n" + "pushd %~dp0\r\n" + "cd workspaces/" + workspace + "\r\n" + "call ide " + ide + "\r\n" + "popd\r\n";
×
456
    } else {
457
      scriptContent = "#!/usr/bin/env bash\n" + "cd \"$(dirname \"$0\")\"\n" + "cd workspaces/" + workspace + "\n" + "ideasy " + ide + "\n";
4✔
458
    }
459
    FileAccess fileAccess = this.context.getFileAccess();
4✔
460
    fileAccess.writeFileContent(scriptContent, scriptPath);
4✔
461
    fileAccess.makeExecutable(scriptPath);
3✔
462
  }
1✔
463
}
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