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

devonfw / IDEasy / 21624876932

03 Feb 2026 09:35AM UTC coverage: 70.444% (-0.002%) from 70.446%
21624876932

Pull #1700

github

web-flow
Merge f91d4d4bb into 70a7fbf5d
Pull Request #1700: #1699 verify input git url

4044 of 6326 branches covered (63.93%)

Branch coverage included in aggregate %.

10490 of 14306 relevant lines covered (73.33%)

3.18 hits per line

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

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

36
/**
37
 * Abstract {@link Commandlet} base-class for both {@link UpdateCommandlet} and {@link CreateCommandlet}.
38
 */
39
public abstract class AbstractUpdateCommandlet extends Commandlet {
40

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

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

52
  /** {@link StringProperty} for the settings repository URL. */
53
  public final StringProperty settingsRepo;
54

55
  /** {@link FlagProperty} for skipping installation/updating of tools. */
56
  public final FlagProperty skipTools;
57

58
  /** {@link FlagProperty} for skipping the setup of git repositories. */
59
  public final FlagProperty skipRepositories;
60

61
  /** {@link FlagProperty} to force the update of the settings git repository. */
62
  public final FlagProperty forcePull;
63

64
  /** {@link FlagProperty} to force the installation/update of plugins. */
65
  public final FlagProperty forcePlugins;
66

67
  /** {@link FlagProperty} to force the setup of git repositories. */
68
  public final FlagProperty forceRepositories;
69

70
  /**
71
   * The constructor.
72
   *
73
   * @param context the {@link IdeContext}.
74
   */
75
  public AbstractUpdateCommandlet(IdeContext context) {
76

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

87
  @Override
88
  public void run() {
89

90
    IdeStartContextImpl startContext = ((AbstractIdeContext) this.context).getStartContext();
5✔
91
    startContext.setForcePull(forcePull.isTrue());
5✔
92
    startContext.setForcePlugins(forcePlugins.isTrue());
5✔
93
    startContext.setForceRepositories(forceRepositories.isTrue());
5✔
94

95
    if (!this.context.isSettingsRepositorySymlinkOrJunction() || this.context.isForceMode() || forcePull.isTrue()) {
4!
96
      updateSettings();
2✔
97
    }
98
    updateConf();
2✔
99
    reloadContext();
2✔
100

101
    updateSoftware();
2✔
102
    updateRepositories();
2✔
103
    createStartScripts();
2✔
104
  }
1✔
105

106
  private void reloadContext() {
107

108
    ((AbstractIdeContext) this.context).reload();
4✔
109
  }
1✔
110

111
  private void updateConf() {
112

113
    Path templatesFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_TEMPLATES);
6✔
114
    if (!Files.exists(templatesFolder)) {
5✔
115
      Path legacyTemplatesFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_LEGACY_TEMPLATES);
6✔
116
      if (Files.exists(legacyTemplatesFolder)) {
5!
117
        templatesFolder = legacyTemplatesFolder;
×
118
      } else {
119
        this.context.warning("Templates folder is missing in settings repository.");
4✔
120
        return;
1✔
121
      }
122
    }
123

124
    Step step = this.context.newStep("Copy configuration templates", templatesFolder);
11✔
125
    final Path finalTemplatesFolder = templatesFolder;
2✔
126
    step.run(() -> setupConf(finalTemplatesFolder, this.context.getIdeHome()));
13✔
127
  }
1✔
128

129
  private void setupConf(Path template, Path conf) {
130

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

134
      String basename = child.getFileName().toString();
4✔
135
      Path confPath = conf.resolve(basename);
4✔
136

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

155
  /**
156
   * 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
157
   * latest current commit ID in the file ".commit.id".
158
   */
159
  protected void updateSettings() {
160

161
    this.context.newStep(getStepMessage()).run(this::updateSettingsInStep);
9✔
162
  }
1✔
163

164
  protected String getStepMessage() {
165

166
    return "update (pull) settings repository";
2✔
167
  }
168

169
  private void updateSettingsInStep() {
170
    Path settingsPath = this.context.getSettingsPath();
4✔
171
    GitContext gitContext = this.context.getGitContext();
4✔
172
    // here we do not use pullOrClone to prevent asking a pointless question for repository URL...
173
    if (Files.isDirectory(settingsPath) && !this.context.getFileAccess().isEmptyDir(settingsPath)) {
11!
174
      if (this.context.isForcePull() || this.context.isForceMode() || Files.isDirectory(settingsPath.resolve(GitContext.GIT_FOLDER))) {
15!
175
        gitContext.pull(settingsPath);
×
176
        this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath());
×
177
      } else {
178
        this.context.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling.");
5✔
179
      }
180
    } else {
181
      GitUrl gitUrl = getOrAskSettingsUrl();
3✔
182
      checkProjectNameConvention(gitUrl.getProjectName());
4✔
183
      initializeRepository(gitUrl);
3✔
184
    }
185
  }
1✔
186

187
  private GitUrl getOrAskSettingsUrl() {
188

189
    String repository = this.settingsRepo.getValue();
5✔
190
    repository = handleDefaultRepository(repository);
4✔
191
    String userPromt;
192
    String defaultUrl;
193
    if (isCodeRepository()) {
3✔
194
      userPromt = "Code repository URL:";
2✔
195
      defaultUrl = null;
2✔
196
      this.context.info(MESSAGE_CODE_REPO_URL);
5✔
197
    } else {
198
      userPromt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:";
2✔
199
      defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL;
2✔
200
      this.context.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath());
12✔
201
    }
202
    while (repository == null || repository.isBlank()) {
5!
203
      repository = this.context.askForInput(userPromt, defaultUrl);
6✔
204
      repository = handleDefaultRepository(repository);
5✔
205
    }
206
    GitUrl gitUrl = GitUrl.of(repository);
3✔
207
    while (!gitUrl.isValid()) {
3!
208
      this.context.warning("The input URL is not valid, please try again:");
×
209
      repository = this.context.askForInput(userPromt, defaultUrl);
×
210
      repository = handleDefaultRepository(repository);
×
211
      gitUrl = GitUrl.of(repository);
×
212
    }
213
    return gitUrl;
2✔
214
  }
215

216
  private String handleDefaultRepository(String repository) {
217
    if ("-".equals(repository)) {
4✔
218
      if (isCodeRepository()) {
3✔
219
        this.context.warning("'-' is found after '--code'. This is invalid.");
4✔
220
        repository = null;
3✔
221
      } else {
222
        this.context.info("'-' was found for settings repository, the default settings repository '{}' will be used.", IdeContext.DEFAULT_SETTINGS_REPO_URL);
10✔
223
        repository = IdeContext.DEFAULT_SETTINGS_REPO_URL;
2✔
224
      }
225
    }
226
    return repository;
2✔
227
  }
228

229
  private void checkProjectNameConvention(String projectName) {
230
    boolean isSettingsRepo = projectName.contains(IdeContext.SETTINGS_REPOSITORY_KEYWORD);
4✔
231
    boolean codeRepository = isCodeRepository();
3✔
232
    if (isSettingsRepo == codeRepository) {
3✔
233
      String warningTemplate;
234
      if (codeRepository) {
2✔
235
        warningTemplate = """
3✔
236
            Your git URL is pointing to the project name {} that contains the keyword '{}'.
237
            Therefore we assume that you did a mistake by adding the '--code' option to the ide project creation.
238
            Do you really want to create the project?""";
239
      } else {
240
        warningTemplate = """
2✔
241
            Your git URL is pointing to the project name {} that does not contain the keyword ''{}''.
242
            Therefore we assume that you forgot to add the '--code' option to the ide project creation.
243
            Do you really want to create the project?""";
244
      }
245
      this.context.askToContinue(warningTemplate, projectName,
14✔
246
          IdeContext.SETTINGS_REPOSITORY_KEYWORD);
247
    }
248
  }
1✔
249

250
  private void initializeRepository(GitUrl gitUrl) {
251

252
    GitContext gitContext = this.context.getGitContext();
4✔
253
    Path settingsPath = this.context.getSettingsPath();
4✔
254
    Path repoPath = settingsPath;
2✔
255
    boolean codeRepository = isCodeRepository();
3✔
256
    if (codeRepository) {
2✔
257
      // clone the given code repository into IDE_HOME/workspaces/main
258
      repoPath = context.getWorkspacePath().resolve(gitUrl.getProjectName());
7✔
259
    }
260
    gitContext.pullOrClone(gitUrl, repoPath);
4✔
261
    if (codeRepository) {
2✔
262
      // check for settings folder and create symlink to IDE_HOME/settings
263
      Path settingsFolder = repoPath.resolve(IdeContext.FOLDER_SETTINGS);
4✔
264
      if (Files.exists(settingsFolder)) {
5!
265
        context.getFileAccess().symlink(settingsFolder, settingsPath);
×
266
      } else {
267
        throw new CliException("Invalid code repository " + gitUrl + ": missing a settings folder at " + settingsFolder);
9✔
268
      }
269
    }
270
    this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath());
8✔
271
  }
1✔
272

273

274
  private void updateSoftware() {
275

276
    if (this.skipTools.isTrue()) {
4✔
277
      this.context.info("Skipping installation/update of tools as specified by the user.");
4✔
278
      return;
1✔
279
    }
280
    Step step = this.context.newStep("Install or update software");
5✔
281
    step.run(() -> doUpdateSoftwareStep(step));
10✔
282
  }
1✔
283

284
  private void doUpdateSoftwareStep(Step step) {
285

286
    Set<ToolCommandlet> toolCommandlets = new HashSet<>();
4✔
287
    CommandletManager commandletManager = this.context.getCommandletManager();
4✔
288
    // installed tools in IDE_HOME/software
289
    List<Path> softwarePaths = this.context.getFileAccess().listChildren(this.context.getSoftwarePath(), Files::isDirectory);
14✔
290
    for (Path softwarePath : softwarePaths) {
10✔
291
      String toolName = softwarePath.getFileName().toString();
4✔
292
      ToolCommandlet toolCommandlet = commandletManager.getToolCommandlet(toolName);
4✔
293
      if (toolCommandlet != null) {
2!
294
        toolCommandlets.add(toolCommandlet);
4✔
295
      }
296
    }
1✔
297

298
    // regular tools in $IDE_TOOLS
299
    List<String> regularTools = IdeVariables.IDE_TOOLS.get(this.context);
6✔
300
    if (regularTools != null) {
2!
301
      for (String regularTool : regularTools) {
10✔
302
        ToolCommandlet toolCommandlet = commandletManager.getToolCommandlet(regularTool);
4✔
303
        if (toolCommandlet == null) {
2!
304
          String displayName = (regularTool == null || regularTool.isBlank()) ? "<empty>" : "'" + regularTool + "'";
×
305
          this.context.error("Cannot install or update tool '{}''. No matching commandlet found. Please check your IDE_TOOLS configuration.", displayName);
×
306
        } else {
×
307
          toolCommandlets.add(toolCommandlet);
4✔
308
        }
309
      }
1✔
310
    }
311

312
    // custom tools in ide-custom-tools.json
313
    for (CustomToolMetadata customTool : this.context.getCustomToolRepository().getTools()) {
9!
314
      CustomToolCommandlet customToolCommandlet = new CustomToolCommandlet(this.context, customTool);
×
315
      toolCommandlets.add(customToolCommandlet);
×
316
    }
×
317

318
    // update/install the toolCommandlets
319
    for (ToolCommandlet toolCommandlet : toolCommandlets) {
10✔
320
      this.context.newStep("Install " + toolCommandlet.getName()).run(() -> toolCommandlet.install(false));
15✔
321
    }
1✔
322

323
    ExtraTools extraTools = ExtraToolsMapper.get().loadJsonFromFolder(this.context.getSettingsPath());
7✔
324
    if (extraTools != null) {
2✔
325
      List<String> toolNames = extraTools.getSortedToolNames();
3✔
326
      this.context.info("Found extra installation of the following tools: {}", toolNames);
10✔
327
      for (String tool : toolNames) {
10✔
328
        List<ExtraToolInstallation> installations = extraTools.getExtraInstallations(tool);
4✔
329
        this.context.newStep("Install extra version(s) of " + tool).run(() -> installExtraToolInstallations(tool, installations));
16✔
330
      }
1✔
331
    }
332
  }
1✔
333

334
  private void installExtraToolInstallations(String tool, List<ExtraToolInstallation> extraInstallations) {
335

336
    CommandletManager commandletManager = this.context.getCommandletManager();
4✔
337
    FileAccess fileAccess = this.context.getFileAccess();
4✔
338
    Path extraPath = this.context.getSoftwareExtraPath();
4✔
339
    LocalToolCommandlet toolCommandlet = commandletManager.getRequiredLocalToolCommandlet(tool);
4✔
340
    for (ExtraToolInstallation extraInstallation : extraInstallations) {
10✔
341
      ToolInstallRequest request = new ToolInstallRequest(false);
5✔
342
      String edition = extraInstallation.edition();
3✔
343
      if (edition == null) {
2✔
344
        edition = toolCommandlet.getConfiguredEdition();
3✔
345
      }
346
      ToolEdition toolEdition = new ToolEdition(tool, edition);
6✔
347
      VersionIdentifier version = extraInstallation.version();
3✔
348
      request.setRequested(new ToolEditionAndVersion(toolEdition, version));
7✔
349
      Path extraToolPath = extraPath.resolve(tool);
4✔
350
      Path toolPath = extraToolPath.resolve(extraInstallation.name());
5✔
351
      request.setToolPathForExtraInstallation(toolPath);
3✔
352
      toolCommandlet.install(request);
4✔
353
    }
1✔
354
  }
1✔
355

356
  private void updateRepositories() {
357

358
    if (this.skipRepositories.isTrue()) {
4!
359
      if (this.forceRepositories.isTrue()) {
×
360
        this.context.warning("Options to skip and force repositories are incompatible and should not be combined. Ignoring --force-repositories to proceed.");
×
361
      }
362
      this.context.info("Skipping setup of repositories as specified by the user.");
×
363
      return;
×
364
    }
365
    RepositoryCommandlet repositoryCommandlet = this.context.getCommandletManager().getCommandlet(RepositoryCommandlet.class);
7✔
366
    repositoryCommandlet.reset();
2✔
367
    repositoryCommandlet.run();
2✔
368
  }
1✔
369

370
  private void createStartScripts() {
371

372
    List<String> ides = IdeVariables.CREATE_START_SCRIPTS.get(this.context);
6✔
373
    if (ides == null) {
2✔
374
      this.context.info("Variable CREATE_START_SCRIPTS is undefined - skipping start script creation.");
4✔
375
      return;
1✔
376
    }
377
    for (String ide : ides) {
10✔
378
      ToolCommandlet tool = this.context.getCommandletManager().getToolCommandlet(ide);
6✔
379
      if (tool == null) {
2!
380
        this.context.error("Undefined IDE '{}' configured in variable CREATE_START_SCRIPTS.");
×
381
      } else {
382
        createStartScript(ide);
3✔
383
      }
384
    }
1✔
385
  }
1✔
386

387
  private void createStartScript(String ide) {
388

389
    this.context.info("Creating start scripts for {}", ide);
10✔
390
    Path workspaces = this.context.getIdeHome().resolve(IdeContext.FOLDER_WORKSPACES);
6✔
391
    try (Stream<Path> childStream = Files.list(workspaces)) {
3✔
392
      Iterator<Path> iterator = childStream.iterator();
3✔
393
      while (iterator.hasNext()) {
3✔
394
        Path child = iterator.next();
4✔
395
        if (Files.isDirectory(child)) {
5!
396
          createStartScript(ide, child.getFileName().toString());
6✔
397
        }
398
      }
1✔
399
    } catch (IOException e) {
×
400
      throw new RuntimeException("Failed to list children of directory " + workspaces, e);
×
401
    }
1✔
402
  }
1✔
403

404
  private void createStartScript(String ide, String workspace) {
405

406
    Path ideHome = this.context.getIdeHome();
4✔
407
    String scriptName = ide + "-" + workspace;
4✔
408
    boolean windows = this.context.getSystemInfo().isWindows();
5✔
409
    if (windows) {
2!
410
      scriptName = scriptName + ".bat";
×
411
    } else {
412
      scriptName = scriptName + ".sh";
3✔
413
    }
414
    Path scriptPath = ideHome.resolve(scriptName);
4✔
415
    if (Files.exists(scriptPath)) {
5!
416
      return;
×
417
    }
418
    String scriptContent;
419
    if (windows) {
2!
420
      scriptContent = "@echo off\r\n"
×
421
          + "pushd %~dp0\r\n"
422
          + "cd workspaces/" + workspace + "\r\n"
423
          + "call ide " + ide + "\r\n"
424
          + "popd\r\n";
425
    } else {
426
      scriptContent = "#!/usr/bin/env bash\n"
4✔
427
          + "cd \"$(dirname \"$0\")\"\n"
428
          + "cd workspaces/" + workspace + "\n"
429
          + "ideasy " + ide + "\n";
430
    }
431
    FileAccess fileAccess = this.context.getFileAccess();
4✔
432
    fileAccess.writeFileContent(scriptContent, scriptPath);
4✔
433
    fileAccess.makeExecutable(scriptPath);
3✔
434
  }
1✔
435

436
  /**
437
   * Judge if the repository is a code repository.
438
   *
439
   * @return true when the repository is a code repository, otherwise false.
440
   */
441
  protected boolean isCodeRepository() {
442
    return false;
2✔
443
  }
444

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