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

devonfw / IDEasy / 16112656538

07 Jul 2025 09:04AM UTC coverage: 68.411% (+0.5%) from 67.911%
16112656538

Pull #1375

github

web-flow
Merge 44b28de4a into ba246604e
Pull Request #1375: #742: Show warning when git repo name does not meet name convention.

3285 of 5204 branches covered (63.12%)

Branch coverage included in aggregate %.

8405 of 11884 relevant lines covered (70.73%)

3.12 hits per line

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

83.27
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

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

84
  private void reloadContext() {
85

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

89
  private void updateConf() {
90

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

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

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

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

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

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

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

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

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

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

170
  private void updateSoftware() {
171

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

180
  private void doUpdateSoftwareStep(Step step) {
181

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

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

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

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

213
  private void updateRepositories() {
214

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

227
  private void createStartScripts() {
228

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

244
  private void createStartScript(String ide) {
245

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

478
  protected void processRepositoryUsingStrategy(RepositoryStrategy strategy) {
479
    Step step = strategy.createNewStep(this.context);
5✔
480
    String repository = this.settingsRepo.getValue();
5✔
481
    while (repository == null || repository.isBlank()) {
5!
482
      repository = strategy.handleBlankRepository(this.context);
6✔
483
    }
484
    while ("-".equals(repository)) {
4✔
485
      repository = strategy.handleDefaultRepository(context);
6✔
486
    }
487
    GitUrl gitUrl = GitUrl.of(repository);
3✔
488
    strategy.checkProjectNameConvention(this.context, gitUrl.getProjectName());
6✔
489
    strategy.initializeRepository(this.context, gitUrl);
5✔
490
    strategy.resolveStep(step);
3✔
491
  }
1✔
492
}
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