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

devonfw / IDEasy / 20271179405

16 Dec 2025 02:21PM UTC coverage: 70.061% (-0.08%) from 70.142%
20271179405

push

github

web-flow
#1660: status robustness #1475: fix tests to work offline (#1661)

3965 of 6233 branches covered (63.61%)

Branch coverage included in aggregate %.

10162 of 13931 relevant lines covered (72.95%)

3.15 hits per line

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

67.32
cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
1
package com.devonfw.tools.ide.context;
2

3
import static com.devonfw.tools.ide.variable.IdeVariables.IDE_MIN_VERSION;
4

5
import java.nio.file.Files;
6
import java.nio.file.Path;
7
import java.time.LocalDateTime;
8
import java.util.ArrayList;
9
import java.util.HashMap;
10
import java.util.Iterator;
11
import java.util.List;
12
import java.util.Locale;
13
import java.util.Map;
14
import java.util.Map.Entry;
15
import java.util.Objects;
16
import java.util.function.Predicate;
17

18
import com.devonfw.tools.ide.cli.CliAbortException;
19
import com.devonfw.tools.ide.cli.CliArgument;
20
import com.devonfw.tools.ide.cli.CliArguments;
21
import com.devonfw.tools.ide.cli.CliException;
22
import com.devonfw.tools.ide.commandlet.Commandlet;
23
import com.devonfw.tools.ide.commandlet.CommandletManager;
24
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
25
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
26
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
27
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
28
import com.devonfw.tools.ide.common.SystemPath;
29
import com.devonfw.tools.ide.completion.CompletionCandidate;
30
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
31
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
32
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
33
import com.devonfw.tools.ide.environment.EnvironmentVariables;
34
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
35
import com.devonfw.tools.ide.environment.IdeSystem;
36
import com.devonfw.tools.ide.environment.IdeSystemImpl;
37
import com.devonfw.tools.ide.git.GitContext;
38
import com.devonfw.tools.ide.git.GitContextImpl;
39
import com.devonfw.tools.ide.git.GitUrl;
40
import com.devonfw.tools.ide.io.FileAccess;
41
import com.devonfw.tools.ide.io.FileAccessImpl;
42
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
43
import com.devonfw.tools.ide.log.IdeLogLevel;
44
import com.devonfw.tools.ide.log.IdeLogger;
45
import com.devonfw.tools.ide.log.IdeSubLogger;
46
import com.devonfw.tools.ide.merge.DirectoryMerger;
47
import com.devonfw.tools.ide.migration.IdeMigrator;
48
import com.devonfw.tools.ide.network.NetworkStatus;
49
import com.devonfw.tools.ide.network.NetworkStatusImpl;
50
import com.devonfw.tools.ide.os.SystemInfo;
51
import com.devonfw.tools.ide.os.SystemInfoImpl;
52
import com.devonfw.tools.ide.os.WindowsHelper;
53
import com.devonfw.tools.ide.os.WindowsHelperImpl;
54
import com.devonfw.tools.ide.os.WindowsPathSyntax;
55
import com.devonfw.tools.ide.process.ProcessContext;
56
import com.devonfw.tools.ide.process.ProcessContextImpl;
57
import com.devonfw.tools.ide.process.ProcessResult;
58
import com.devonfw.tools.ide.property.Property;
59
import com.devonfw.tools.ide.step.Step;
60
import com.devonfw.tools.ide.step.StepImpl;
61
import com.devonfw.tools.ide.tool.repository.CustomToolRepository;
62
import com.devonfw.tools.ide.tool.repository.CustomToolRepositoryImpl;
63
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
64
import com.devonfw.tools.ide.tool.repository.MvnRepository;
65
import com.devonfw.tools.ide.tool.repository.NpmRepository;
66
import com.devonfw.tools.ide.tool.repository.PipRepository;
67
import com.devonfw.tools.ide.tool.repository.ToolRepository;
68
import com.devonfw.tools.ide.url.model.UrlMetadata;
69
import com.devonfw.tools.ide.util.DateTimeUtil;
70
import com.devonfw.tools.ide.util.PrivacyUtil;
71
import com.devonfw.tools.ide.validation.ValidationResult;
72
import com.devonfw.tools.ide.validation.ValidationResultValid;
73
import com.devonfw.tools.ide.validation.ValidationState;
74
import com.devonfw.tools.ide.variable.IdeVariables;
75
import com.devonfw.tools.ide.version.IdeVersion;
76
import com.devonfw.tools.ide.version.VersionIdentifier;
77

78
/**
79
 * Abstract base implementation of {@link IdeContext}.
80
 */
81
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
82

83
  private static final GitUrl IDE_URLS_GIT = new GitUrl("https://github.com/devonfw/ide-urls.git", null);
7✔
84

85
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
86
  public static final String BASH = "bash";
87
  private static final String DEFAULT_WINDOWS_GIT_PATH = "C:\\Program Files\\Git\\bin\\bash.exe";
88

89
  private static final String OPTION_DETAILS_START = "([";
90

91
  private final IdeStartContextImpl startContext;
92

93
  private Path ideHome;
94

95
  private final Path ideRoot;
96

97
  private Path confPath;
98

99
  protected Path settingsPath;
100

101
  private Path settingsCommitIdPath;
102

103
  protected Path pluginsPath;
104

105
  private Path workspacePath;
106

107
  private String workspaceName;
108

109
  private Path cwd;
110

111
  private Path downloadPath;
112

113
  private Path userHome;
114

115
  private Path userHomeIde;
116

117
  private SystemPath path;
118

119
  private WindowsPathSyntax pathSyntax;
120

121
  private final SystemInfo systemInfo;
122

123
  private EnvironmentVariables variables;
124

125
  private final FileAccess fileAccess;
126

127
  protected CommandletManager commandletManager;
128

129
  protected ToolRepository defaultToolRepository;
130

131
  private CustomToolRepository customToolRepository;
132

133
  private MvnRepository mvnRepository;
134

135
  private NpmRepository npmRepository;
136

137
  private PipRepository pipRepository;
138

139
  private DirectoryMerger workspaceMerger;
140

141
  protected UrlMetadata urlMetadata;
142

143
  protected Path defaultExecutionDirectory;
144

145
  private StepImpl currentStep;
146

147
  private NetworkStatus networkStatus;
148

149
  protected IdeSystem system;
150

151
  private WindowsHelper windowsHelper;
152

153
  private final Map<String, String> privacyMap;
154

155
  private Path bash;
156

157
  /**
158
   * The constructor.
159
   *
160
   * @param startContext the {@link IdeLogger}.
161
   * @param workingDirectory the optional {@link Path} to current working directory.
162
   */
163
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
164

165
    super();
2✔
166
    this.startContext = startContext;
3✔
167
    this.startContext.setArgFormatter(this);
4✔
168
    this.privacyMap = new HashMap<>();
5✔
169
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
170
    this.commandletManager = new CommandletManagerImpl(this);
6✔
171
    this.fileAccess = new FileAccessImpl(this);
6✔
172
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
173
    if (userHomeProperty != null) {
2!
174
      this.userHome = Path.of(userHomeProperty);
×
175
    }
176
    if (workingDirectory == null) {
2!
177
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
178
    }
179
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
180
    if (Files.isDirectory(workingDirectory)) {
5✔
181
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
182
    } else {
183
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
184
    }
185
    this.cwd = workingDirectory;
3✔
186
    // detect IDE_HOME and WORKSPACE
187
    String workspace = null;
2✔
188
    Path ideHomeDir = null;
2✔
189
    IdeHomeAndWorkspace ideHomeAndWorkspace = findIdeHome(workingDirectory);
4✔
190
    if (ideHomeAndWorkspace != null) {
2!
191
      ideHomeDir = ideHomeAndWorkspace.home();
3✔
192
      workspace = ideHomeAndWorkspace.workspace();
3✔
193
    }
194

195
    // detection completed, initializing variables
196
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
197

198
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
199

200
    if (this.ideRoot != null) {
3✔
201
      Path tempDownloadPath = getTempDownloadPath();
3✔
202
      if (Files.isDirectory(tempDownloadPath)) {
6✔
203
        // TODO delete all files older than 1 day here...
204
      } else {
205
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
206
      }
207
    }
208

209
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
210
  }
1✔
211

212
  /**
213
   * Searches for the IDE home directory by traversing up the directory tree from the given working directory. This method can be overridden in test contexts to
214
   * add additional validation or boundary checks.
215
   *
216
   * @param workingDirectory the starting directory for the search.
217
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
218
   */
219
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
220

221
    Path currentDir = workingDirectory;
2✔
222
    String name1 = "";
2✔
223
    String name2 = "";
2✔
224
    String workspace = WORKSPACE_MAIN;
2✔
225
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
226

227
    while (currentDir != null) {
2✔
228
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
229
      if (isIdeHome(currentDir)) {
4✔
230
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
231
          workspace = name2;
3✔
232
        }
233
        break;
234
      }
235
      name2 = name1;
2✔
236
      int nameCount = currentDir.getNameCount();
3✔
237
      if (nameCount >= 1) {
3✔
238
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
239
      }
240
      currentDir = currentDir.getParent();
3✔
241
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
242
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
243
        currentDir = null;
×
244
      }
245
    }
1✔
246

247
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
248
  }
249

250
  /**
251
   * @return a new {@link MvnRepository}
252
   */
253
  protected MvnRepository createMvnRepository() {
254
    return new MvnRepository(this);
5✔
255
  }
256

257
  /**
258
   * @return a new {@link NpmRepository}
259
   */
260
  protected NpmRepository createNpmRepository() {
261
    return new NpmRepository(this);
×
262
  }
263

264
  /**
265
   * @return a new {@link PipRepository}
266
   */
267
  protected PipRepository createPipRepository() {
268
    return new PipRepository(this);
×
269
  }
270

271
  private Path findIdeRoot(Path ideHomePath) {
272

273
    Path ideRootPath = null;
2✔
274
    if (ideHomePath != null) {
2✔
275
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
276
      ideRootPath = ideHomePath.getParent();
3✔
277
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
2!
278
        warning(
×
279
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
280
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
281
            ideRootPathFromEnv,
282
            ideHomePath.getFileName(), ideRootPath);
×
283
      }
284
    } else if (!isTest()) {
4!
285
      ideRootPath = getIdeRootPathFromEnv(true);
×
286
    }
287
    return ideRootPath;
2✔
288
  }
289

290
  /**
291
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
292
   */
293
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
294

295
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
296
    if (root != null) {
×
297
      Path rootPath = Path.of(root);
×
298
      if (Files.isDirectory(rootPath)) {
×
299
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
300
        if (withSanityCheck) {
×
301
          int nameCount = rootPath.getNameCount();
×
302
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
303
          int delta = absoluteNameCount - nameCount;
×
304
          if (delta >= 0) {
×
305
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
306
              String rootName = rootPath.getName(nameIndex).toString();
×
307
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
308
              if (!rootName.equals(absoluteRootName)) {
×
309
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
310
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
311
                break;
×
312
              }
313
            }
314
          } else {
315
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
316
                absoluteRootPath);
317
          }
318
        }
319
        return absoluteRootPath;
×
320
      } else if (withSanityCheck) {
×
321
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
322
      }
323
    }
324
    return null;
×
325
  }
326

327
  @Override
328
  public void setCwd(Path userDir, String workspace, Path ideHome) {
329

330
    this.cwd = userDir;
3✔
331
    this.workspaceName = workspace;
3✔
332
    this.ideHome = ideHome;
3✔
333
    if (ideHome == null) {
2✔
334
      this.workspacePath = null;
3✔
335
      this.confPath = null;
3✔
336
      this.settingsPath = null;
3✔
337
      this.pluginsPath = null;
4✔
338
    } else {
339
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
340
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
341
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
342
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
343
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
344
    }
345
    if (isTest()) {
3!
346
      // only for testing...
347
      if (this.ideHome == null) {
3✔
348
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
349
      } else {
350
        this.userHome = this.ideHome.resolve("home");
6✔
351
      }
352
    }
353
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
354
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
355
    resetPrivacyMap();
2✔
356
    this.path = computeSystemPath();
4✔
357
  }
1✔
358

359
  private String getMessageIdeHomeFound() {
360

361
    String wks = this.workspaceName;
3✔
362
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
363
      wks = "*".repeat(wks.length());
×
364
    }
365
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
366
  }
367

368
  private String getMessageNotInsideIdeProject() {
369

370
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
371
  }
372

373
  private String getMessageIdeRootNotFound() {
374

375
    String root = getSystem().getEnv("IDE_ROOT");
5✔
376
    if (root == null) {
2!
377
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
378
    } else {
379
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
380
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
381
    }
382
  }
383

384
  /**
385
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
386
   */
387
  public boolean isTest() {
388

389
    return false;
×
390
  }
391

392
  protected SystemPath computeSystemPath() {
393

394
    return new SystemPath(this);
×
395
  }
396

397
  /**
398
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
399
   *
400
   * @param dir the directory to check.
401
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
402
   */
403
  protected boolean isIdeHome(Path dir) {
404

405
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
406
      return false;
2✔
407
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
408
      return false;
×
409
    }
410
    return true;
2✔
411
  }
412

413
  private EnvironmentVariables createVariables() {
414

415
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
416
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
417
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
418
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
419
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
420
    return conf.resolved();
3✔
421
  }
422

423
  protected AbstractEnvironmentVariables createSystemVariables() {
424

425
    return EnvironmentVariables.ofSystem(this);
3✔
426
  }
427

428
  @Override
429
  public SystemInfo getSystemInfo() {
430

431
    return this.systemInfo;
3✔
432
  }
433

434
  @Override
435
  public FileAccess getFileAccess() {
436

437
    return this.fileAccess;
3✔
438
  }
439

440
  @Override
441
  public CommandletManager getCommandletManager() {
442

443
    return this.commandletManager;
3✔
444
  }
445

446
  @Override
447
  public ToolRepository getDefaultToolRepository() {
448

449
    return this.defaultToolRepository;
3✔
450
  }
451

452
  @Override
453
  public MvnRepository getMvnRepository() {
454
    if (this.mvnRepository == null) {
3✔
455
      this.mvnRepository = createMvnRepository();
4✔
456
    }
457
    return this.mvnRepository;
3✔
458
  }
459

460
  @Override
461
  public NpmRepository getNpmRepository() {
462
    if (this.npmRepository == null) {
3✔
463
      this.npmRepository = createNpmRepository();
4✔
464
    }
465
    return this.npmRepository;
3✔
466
  }
467

468
  @Override
469
  public PipRepository getPipRepository() {
470
    if (this.pipRepository == null) {
3✔
471
      this.pipRepository = createPipRepository();
4✔
472
    }
473
    return this.pipRepository;
3✔
474
  }
475

476
  @Override
477
  public CustomToolRepository getCustomToolRepository() {
478

479
    if (this.customToolRepository == null) {
3!
480
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
481
    }
482
    return this.customToolRepository;
3✔
483
  }
484

485
  @Override
486
  public Path getIdeHome() {
487

488
    return this.ideHome;
3✔
489
  }
490

491
  @Override
492
  public String getProjectName() {
493

494
    if (this.ideHome != null) {
3!
495
      return this.ideHome.getFileName().toString();
5✔
496
    }
497
    return "";
×
498
  }
499

500
  @Override
501
  public VersionIdentifier getProjectVersion() {
502

503
    if (this.ideHome != null) {
3!
504
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
505
      if (Files.exists(versionFile)) {
5✔
506
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
507
        return VersionIdentifier.of(version);
3✔
508
      }
509
    }
510
    return IdeMigrator.START_VERSION;
2✔
511
  }
512

513
  @Override
514
  public void setProjectVersion(VersionIdentifier version) {
515

516
    if (this.ideHome == null) {
3!
517
      throw new IllegalStateException("IDE_HOME not available!");
×
518
    }
519
    Objects.requireNonNull(version);
3✔
520
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
521
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
522
  }
1✔
523

524
  @Override
525
  public Path getIdeRoot() {
526

527
    return this.ideRoot;
3✔
528
  }
529

530
  @Override
531
  public Path getIdePath() {
532

533
    Path myIdeRoot = getIdeRoot();
3✔
534
    if (myIdeRoot == null) {
2!
535
      return null;
×
536
    }
537
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
538
  }
539

540
  @Override
541
  public Path getCwd() {
542

543
    return this.cwd;
3✔
544
  }
545

546
  @Override
547
  public Path getTempPath() {
548

549
    Path idePath = getIdePath();
3✔
550
    if (idePath == null) {
2!
551
      return null;
×
552
    }
553
    return idePath.resolve("tmp");
4✔
554
  }
555

556
  @Override
557
  public Path getTempDownloadPath() {
558

559
    Path tmp = getTempPath();
3✔
560
    if (tmp == null) {
2!
561
      return null;
×
562
    }
563
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
564
  }
565

566
  @Override
567
  public Path getUserHome() {
568

569
    return this.userHome;
3✔
570
  }
571

572
  /**
573
   * This method should only be used for tests to mock user home.
574
   *
575
   * @param userHome the new value of {@link #getUserHome()}.
576
   */
577
  protected void setUserHome(Path userHome) {
578

579
    this.userHome = userHome;
3✔
580
    resetPrivacyMap();
2✔
581
  }
1✔
582

583
  @Override
584
  public Path getUserHomeIde() {
585

586
    return this.userHomeIde;
3✔
587
  }
588

589
  @Override
590
  public Path getSettingsPath() {
591

592
    return this.settingsPath;
3✔
593
  }
594

595
  @Override
596
  public Path getSettingsGitRepository() {
597

598
    Path settingsPath = getSettingsPath();
3✔
599
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
600
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
601
      error("Settings repository exists but is not a git repository.");
3✔
602
      return null;
2✔
603
    }
604
    return settingsPath;
2✔
605
  }
606

607
  @Override
608
  public boolean isSettingsRepositorySymlinkOrJunction() {
609

610
    Path settingsPath = getSettingsPath();
3✔
611
    if (settingsPath == null) {
2!
612
      return false;
×
613
    }
614
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
615
  }
616

617
  @Override
618
  public Path getSettingsCommitIdPath() {
619

620
    return this.settingsCommitIdPath;
3✔
621
  }
622

623
  @Override
624
  public Path getConfPath() {
625

626
    return this.confPath;
3✔
627
  }
628

629
  @Override
630
  public Path getSoftwarePath() {
631

632
    if (this.ideHome == null) {
3✔
633
      return null;
2✔
634
    }
635
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
636
  }
637

638
  @Override
639
  public Path getSoftwareExtraPath() {
640

641
    Path softwarePath = getSoftwarePath();
3✔
642
    if (softwarePath == null) {
2!
643
      return null;
×
644
    }
645
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
646
  }
647

648
  @Override
649
  public Path getSoftwareRepositoryPath() {
650

651
    Path idePath = getIdePath();
3✔
652
    if (idePath == null) {
2!
653
      return null;
×
654
    }
655
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
656
  }
657

658
  @Override
659
  public Path getPluginsPath() {
660

661
    return this.pluginsPath;
3✔
662
  }
663

664
  @Override
665
  public String getWorkspaceName() {
666

667
    return this.workspaceName;
3✔
668
  }
669

670
  @Override
671
  public Path getWorkspacePath() {
672

673
    return this.workspacePath;
3✔
674
  }
675

676
  @Override
677
  public Path getDownloadPath() {
678

679
    return this.downloadPath;
3✔
680
  }
681

682
  @Override
683
  public Path getUrlsPath() {
684

685
    Path idePath = getIdePath();
3✔
686
    if (idePath == null) {
2!
687
      return null;
×
688
    }
689
    return idePath.resolve(FOLDER_URLS);
4✔
690
  }
691

692
  @Override
693
  public Path getToolRepositoryPath() {
694

695
    Path idePath = getIdePath();
3✔
696
    if (idePath == null) {
2!
697
      return null;
×
698
    }
699
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
700
  }
701

702
  @Override
703
  public SystemPath getPath() {
704

705
    return this.path;
3✔
706
  }
707

708
  @Override
709
  public EnvironmentVariables getVariables() {
710

711
    if (this.variables == null) {
3✔
712
      this.variables = createVariables();
4✔
713
    }
714
    return this.variables;
3✔
715
  }
716

717
  @Override
718
  public UrlMetadata getUrls() {
719

720
    if (this.urlMetadata == null) {
3✔
721
      if (!isTest()) {
3!
722
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
723
      }
724
      this.urlMetadata = new UrlMetadata(this);
6✔
725
    }
726
    return this.urlMetadata;
3✔
727
  }
728

729
  @Override
730
  public boolean isQuietMode() {
731

732
    return this.startContext.isQuietMode();
4✔
733
  }
734

735
  @Override
736
  public boolean isBatchMode() {
737

738
    return this.startContext.isBatchMode();
4✔
739
  }
740

741
  @Override
742
  public boolean isForceMode() {
743

744
    return this.startContext.isForceMode();
4✔
745
  }
746

747
  @Override
748
  public boolean isForcePull() {
749

750
    return this.startContext.isForcePull();
4✔
751
  }
752

753
  @Override
754
  public boolean isForcePlugins() {
755

756
    return this.startContext.isForcePlugins();
4✔
757
  }
758

759
  @Override
760
  public boolean isForceRepositories() {
761

762
    return this.startContext.isForceRepositories();
4✔
763
  }
764

765
  @Override
766
  public boolean isOfflineMode() {
767

768
    return this.startContext.isOfflineMode();
4✔
769
  }
770

771
  @Override
772
  public boolean isPrivacyMode() {
773
    return this.startContext.isPrivacyMode();
4✔
774
  }
775

776
  @Override
777
  public boolean isSkipUpdatesMode() {
778

779
    return this.startContext.isSkipUpdatesMode();
4✔
780
  }
781

782
  @Override
783
  public boolean isNoColorsMode() {
784

785
    return this.startContext.isNoColorsMode();
×
786
  }
787

788
  @Override
789
  public NetworkStatus getNetworkStatus() {
790

791
    if (this.networkStatus == null) {
×
792
      this.networkStatus = new NetworkStatusImpl(this);
×
793
    }
794
    return this.networkStatus;
×
795
  }
796

797
  @Override
798
  public Locale getLocale() {
799

800
    Locale locale = this.startContext.getLocale();
4✔
801
    if (locale == null) {
2✔
802
      locale = Locale.getDefault();
2✔
803
    }
804
    return locale;
2✔
805
  }
806

807
  @Override
808
  public DirectoryMerger getWorkspaceMerger() {
809

810
    if (this.workspaceMerger == null) {
3✔
811
      this.workspaceMerger = new DirectoryMerger(this);
6✔
812
    }
813
    return this.workspaceMerger;
3✔
814
  }
815

816
  /**
817
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
818
   */
819
  @Override
820
  public Path getDefaultExecutionDirectory() {
821

822
    return this.defaultExecutionDirectory;
×
823
  }
824

825
  /**
826
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
827
   */
828
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
829

830
    if (defaultExecutionDirectory != null) {
×
831
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
832
    }
833
  }
×
834

835
  @Override
836
  public GitContext getGitContext() {
837

838
    return new GitContextImpl(this);
×
839
  }
840

841
  @Override
842
  public ProcessContext newProcess() {
843

844
    ProcessContext processContext = createProcessContext();
3✔
845
    if (this.defaultExecutionDirectory != null) {
3!
846
      processContext.directory(this.defaultExecutionDirectory);
×
847
    }
848
    return processContext;
2✔
849
  }
850

851
  @Override
852
  public IdeSystem getSystem() {
853

854
    if (this.system == null) {
×
855
      this.system = new IdeSystemImpl(this);
×
856
    }
857
    return this.system;
×
858
  }
859

860
  /**
861
   * @return a new instance of {@link ProcessContext}.
862
   * @see #newProcess()
863
   */
864
  protected ProcessContext createProcessContext() {
865

866
    return new ProcessContextImpl(this);
5✔
867
  }
868

869
  @Override
870
  public IdeSubLogger level(IdeLogLevel level) {
871

872
    return this.startContext.level(level);
5✔
873
  }
874

875
  @Override
876
  public void logIdeHomeAndRootStatus() {
877
    if (this.ideRoot != null) {
3!
878
      success("IDE_ROOT is set to {}", this.ideRoot);
×
879
    }
880
    if (this.ideHome == null) {
3✔
881
      warning(getMessageNotInsideIdeProject());
5✔
882
    } else {
883
      success("IDE_HOME is set to {}", this.ideHome);
10✔
884
    }
885
  }
1✔
886

887
  @Override
888
  public String formatArgument(Object argument) {
889

890
    if (argument == null) {
2✔
891
      return null;
2✔
892
    }
893
    String result = argument.toString();
3✔
894
    if (isPrivacyMode()) {
3✔
895
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
896
        initializePrivacyMap(this.userHome, "~");
×
897
        String projectName = getProjectName();
×
898
        if (!projectName.isEmpty()) {
×
899
          this.privacyMap.put(projectName, "project");
×
900
        }
901
      }
902
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
903
        result = result.replace(entry.getKey(), entry.getValue());
×
904
      }
×
905
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
906
    }
907
    return result;
2✔
908
  }
909

910
  /**
911
   * @param path the sensitive {@link Path} to
912
   * @param replacement the replacement to mask the {@link Path} in log output.
913
   */
914
  protected void initializePrivacyMap(Path path, String replacement) {
915

916
    if (path == null) {
×
917
      return;
×
918
    }
919
    if (this.systemInfo.isWindows()) {
×
920
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
921
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
922
    } else {
923
      this.privacyMap.put(path.toString(), replacement);
×
924
    }
925
  }
×
926

927
  /**
928
   * Resets the privacy map in case fundamental values have changed.
929
   */
930
  private void resetPrivacyMap() {
931

932
    this.privacyMap.clear();
3✔
933
  }
1✔
934

935

936
  @Override
937
  public String askForInput(String message, String defaultValue) {
938

939
    while (true) {
940
      if (!message.isBlank()) {
3!
941
        interaction(message);
3✔
942
      }
943
      if (isBatchMode()) {
3!
944
        if (isForceMode()) {
×
945
          return defaultValue;
×
946
        } else {
947
          throw new CliAbortException();
×
948
        }
949
      }
950
      String input = readLine().trim();
4✔
951
      if (!input.isEmpty()) {
3!
952
        return input;
2✔
953
      } else {
954
        if (defaultValue != null) {
×
955
          return defaultValue;
×
956
        }
957
      }
958
    }
×
959
  }
960

961
  @SuppressWarnings("unchecked")
962
  @Override
963
  public <O> O question(O[] options, String question, Object... args) {
964

965
    assert (options.length > 0);
4!
966
    interaction(question, args);
4✔
967
    return displayOptionsAndGetAnswer(options);
4✔
968
  }
969

970
  private <O> O displayOptionsAndGetAnswer(O[] options) {
971
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
972
    int i = 0;
2✔
973
    for (O option : options) {
16✔
974
      i++;
1✔
975
      String title = "" + option;
4✔
976
      String key = computeOptionKey(title);
3✔
977
      addMapping(mapping, key, option);
4✔
978
      String numericKey = Integer.toString(i);
3✔
979
      if (numericKey.equals(key)) {
4!
980
        trace("Options should not be numeric: " + key);
×
981
      } else {
982
        addMapping(mapping, numericKey, option);
4✔
983
      }
984
      interaction("Option " + numericKey + ": " + title);
5✔
985
    }
986
    O option = null;
2✔
987
    if (isBatchMode()) {
3!
988
      if (isForceMode()) {
×
989
        option = options[0];
×
990
        interaction("" + option);
×
991
      }
992
    } else {
993
      while (option == null) {
2✔
994
        String answer = readLine();
3✔
995
        option = mapping.get(answer);
4✔
996
        if (option == null) {
2!
997
          warning("Invalid answer: '" + answer + "' - please try again.");
×
998
        }
999
      }
1✔
1000
    }
1001
    return option;
2✔
1002
  }
1003

1004
  private static String computeOptionKey(String option) {
1005
    String key = option;
2✔
1006
    int index = -1;
2✔
1007
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1008
      int currentIndex = key.indexOf(c);
4✔
1009
      if (currentIndex != -1) {
3✔
1010
        if ((index == -1) || (currentIndex < index)) {
3!
1011
          index = currentIndex;
2✔
1012
        }
1013
      }
1014
    }
1015
    if (index > 0) {
2✔
1016
      key = key.substring(0, index).trim();
6✔
1017
    }
1018
    return key;
2✔
1019
  }
1020

1021
  /**
1022
   * @return the input from the end-user (e.g. read from the console).
1023
   */
1024
  protected abstract String readLine();
1025

1026
  private static <O> void addMapping(Map<String, O> mapping, String key, O option) {
1027

1028
    O duplicate = mapping.put(key, option);
5✔
1029
    if (duplicate != null) {
2!
1030
      throw new IllegalArgumentException("Duplicated option " + key);
×
1031
    }
1032
  }
1✔
1033

1034
  @Override
1035
  public Step getCurrentStep() {
1036

1037
    return this.currentStep;
×
1038
  }
1039

1040
  @Override
1041
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1042

1043
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1044
    return this.currentStep;
3✔
1045
  }
1046

1047
  /**
1048
   * Internal method to end the running {@link Step}.
1049
   *
1050
   * @param step the current {@link Step} to end.
1051
   */
1052
  public void endStep(StepImpl step) {
1053

1054
    if (step == this.currentStep) {
4!
1055
      this.currentStep = this.currentStep.getParent();
6✔
1056
    } else {
1057
      String currentStepName = "null";
×
1058
      if (this.currentStep != null) {
×
1059
        currentStepName = this.currentStep.getName();
×
1060
      }
1061
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1062
    }
1063
  }
1✔
1064

1065
  /**
1066
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1067
   *
1068
   * @param arguments the {@link CliArgument}.
1069
   * @return the return code of the execution.
1070
   */
1071
  public int run(CliArguments arguments) {
1072

1073
    CliArgument current = arguments.current();
3✔
1074
    assert (this.currentStep == null);
4!
1075
    boolean supressStepSuccess = false;
2✔
1076
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1077
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1078
    Commandlet cmd = null;
2✔
1079
    ValidationResult result = null;
2✔
1080
    try {
1081
      while (commandletIterator.hasNext()) {
3✔
1082
        cmd = commandletIterator.next();
4✔
1083
        result = applyAndRun(arguments.copy(), cmd);
6✔
1084
        if (result.isValid()) {
3!
1085
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1086
          step.success();
2✔
1087
          return ProcessResult.SUCCESS;
4✔
1088
        }
1089
      }
1090
      this.startContext.activateLogging();
3✔
1091
      verifyIdeMinVersion(false);
3✔
1092
      if (result != null) {
2!
1093
        error(result.getErrorMessage());
×
1094
      }
1095
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1096
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1097
      if (cmd != null) {
2!
1098
        help.commandlet.setValue(cmd);
×
1099
      }
1100
      help.run();
2✔
1101
      return 1;
4✔
1102
    } catch (Throwable t) {
1✔
1103
      this.startContext.activateLogging();
3✔
1104
      step.error(t, true);
4✔
1105
      throw t;
2✔
1106
    } finally {
1107
      step.close();
2✔
1108
      assert (this.currentStep == null);
4!
1109
      step.logSummary(supressStepSuccess);
3✔
1110
    }
1111
  }
1112

1113
  @Override
1114
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1115

1116
    this.startContext.deactivateLogging(threshold);
4✔
1117
    lambda.run();
2✔
1118
    this.startContext.activateLogging();
3✔
1119
  }
1✔
1120

1121
  /**
1122
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1123
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1124
   *     {@link Commandlet} did not match and we have to try a different candidate).
1125
   */
1126
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1127

1128
    IdeLogLevel previousLogLevel = null;
2✔
1129
    cmd.reset();
2✔
1130
    ValidationResult result = apply(arguments, cmd);
5✔
1131
    if (result.isValid()) {
3!
1132
      result = cmd.validate();
3✔
1133
    }
1134
    if (result.isValid()) {
3!
1135
      debug("Running commandlet {}", cmd);
9✔
1136
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1137
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1138
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1139
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1140
      }
1141
      try {
1142
        if (cmd.isProcessableOutput()) {
3!
1143
          if (!debug().isEnabled()) {
×
1144
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1145
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1146
          }
1147
          this.startContext.activateLogging();
×
1148
        } else {
1149
          this.startContext.activateLogging();
3✔
1150
          if (cmd.isIdeHomeRequired()) {
3!
1151
            debug(getMessageIdeHomeFound());
4✔
1152
          }
1153
          Path settingsRepository = getSettingsGitRepository();
3✔
1154
          if (settingsRepository != null) {
2!
1155
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1156
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1157
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1158
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1159
                interaction(
×
1160
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1161

1162
              } else {
1163
                interaction(
×
1164
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1165
              }
1166
            }
1167
          }
1168
        }
1169
        boolean success = ensureLicenseAgreement(cmd);
4✔
1170
        if (!success) {
2!
1171
          return ValidationResultValid.get();
×
1172
        }
1173
        cmd.run();
2✔
1174
      } finally {
1175
        if (previousLogLevel != null) {
2!
1176
          this.startContext.setLogLevel(previousLogLevel);
×
1177
        }
1178
      }
1✔
1179
    } else {
1180
      trace("Commandlet did not match");
×
1181
    }
1182
    return result;
2✔
1183
  }
1184

1185
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1186

1187
    if (isTest()) {
3!
1188
      return true; // ignore for tests
2✔
1189
    }
1190
    getFileAccess().mkdirs(this.userHomeIde);
×
1191
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1192
    if (Files.isRegularFile(licenseAgreement)) {
×
1193
      return true; // success, license already accepted
×
1194
    }
1195
    if (cmd instanceof EnvironmentCommandlet) {
×
1196
      // if the license was not accepted, "$(ideasy env --bash)" that is written into a variable prevents the user from seeing the question he is asked
1197
      // in such situation the user could not open a bash terminal anymore and gets blocked what would really annoy the user so we exit here without doing or
1198
      // printing anything anymore in such case.
1199
      return false;
×
1200
    }
1201
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1202
    if (logLevelInfoDisabled) {
×
1203
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1204
    }
1205
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1206
    if (logLevelInteractionDisabled) {
×
1207
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1208
    }
1209
    StringBuilder sb = new StringBuilder(1180);
×
1210
    sb.append(LOGO).append("""
×
1211
        Welcome to IDEasy!
1212
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1213
        It supports automatic download and installation of arbitrary 3rd party tools.
1214
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1215
        But if explicitly configured, also commercial software that requires an additional license may be used.
1216
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1217
        You are solely responsible for all risks implied by using this software.
1218
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1219
        You will be able to find it online under the following URL:
1220
        """).append(LICENSE_URL);
×
1221
    if (this.ideRoot != null) {
×
1222
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1223
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1224
    }
1225
    info(sb.toString());
×
1226
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1227

1228
    sb.setLength(0);
×
1229
    LocalDateTime now = LocalDateTime.now();
×
1230
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1231
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1232
    try {
1233
      Files.writeString(licenseAgreement, sb);
×
1234
    } catch (Exception e) {
×
1235
      throw new RuntimeException("Failed to save license agreement!", e);
×
1236
    }
×
1237
    if (logLevelInfoDisabled) {
×
1238
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1239
    }
1240
    if (logLevelInteractionDisabled) {
×
1241
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1242
    }
1243
    return true;
×
1244
  }
1245

1246
  @Override
1247
  public void verifyIdeMinVersion(boolean throwException) {
1248
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1249
    if (minVersion == null) {
2✔
1250
      return;
1✔
1251
    }
1252
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1253
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1254
          + "However, this is too old as your project requires at latest version %s\n"
1255
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1256
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1257
      if (throwException) {
2✔
1258
        throw new CliException(message);
5✔
1259
      } else {
1260
        warning(message);
3✔
1261
      }
1262
    }
1263
  }
1✔
1264

1265
  /**
1266
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1267
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1268
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1269
   */
1270
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1271

1272
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1273
    if (arguments.current().isStart()) {
4✔
1274
      arguments.next();
3✔
1275
    }
1276
    if (includeContextOptions) {
2✔
1277
      ContextCommandlet cc = new ContextCommandlet();
4✔
1278
      for (Property<?> property : cc.getProperties()) {
11✔
1279
        assert (property.isOption());
4!
1280
        property.apply(arguments, this, cc, collector);
7✔
1281
      }
1✔
1282
    }
1283
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1284
    CliArgument current = arguments.current();
3✔
1285
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1286
      collector.add(current.get(), null, null, null);
7✔
1287
    }
1288
    arguments.next();
3✔
1289
    while (commandletIterator.hasNext()) {
3✔
1290
      Commandlet cmd = commandletIterator.next();
4✔
1291
      if (!arguments.current().isEnd()) {
4✔
1292
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1293
      }
1294
    }
1✔
1295
    return collector.getSortedCandidates();
3✔
1296
  }
1297

1298
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1299

1300
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1301
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1302
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1303
    List<Property<?>> properties = cmd.getProperties();
3✔
1304
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1305
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1306
    for (Property<?> property : properties) {
10✔
1307
      if (property.isOption()) {
3✔
1308
        optionProperties.add(property);
4✔
1309
      }
1310
    }
1✔
1311
    CliArgument currentArgument = arguments.current();
3✔
1312
    while (!currentArgument.isEnd()) {
3✔
1313
      trace("Trying to match argument '{}'", currentArgument);
9✔
1314
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1315
        if (currentArgument.isCompletion()) {
3✔
1316
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1317
          while (optionIterator.hasNext()) {
3✔
1318
            Property<?> option = optionIterator.next();
4✔
1319
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1320
            if (success) {
2✔
1321
              optionIterator.remove();
2✔
1322
              arguments.next();
3✔
1323
            }
1324
          }
1✔
1325
        } else {
1✔
1326
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1327
          if (option != null) {
2✔
1328
            arguments.next();
3✔
1329
            boolean removed = optionProperties.remove(option);
4✔
1330
            if (!removed) {
2!
1331
              option = null;
×
1332
            }
1333
          }
1334
          if (option == null) {
2✔
1335
            trace("No such option was found.");
3✔
1336
            return;
1✔
1337
          }
1338
        }
1✔
1339
      } else {
1340
        if (valueIterator.hasNext()) {
3✔
1341
          Property<?> valueProperty = valueIterator.next();
4✔
1342
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1343
          if (!success) {
2✔
1344
            trace("Completion cannot match any further.");
3✔
1345
            return;
1✔
1346
          }
1347
        } else {
1✔
1348
          trace("No value left for completion.");
3✔
1349
          return;
1✔
1350
        }
1351
      }
1352
      currentArgument = arguments.current();
4✔
1353
    }
1354
  }
1✔
1355

1356
  /**
1357
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1358
   *     {@link CliArguments#copy() copy} as needed.
1359
   * @param cmd the potential {@link Commandlet} to match.
1360
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1361
   */
1362
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1363

1364
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1365
    CliArgument currentArgument = arguments.current();
3✔
1366
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1367
    Property<?> property = null;
2✔
1368
    if (propertyIterator.hasNext()) {
3!
1369
      property = propertyIterator.next();
4✔
1370
    }
1371
    while (!currentArgument.isEnd()) {
3✔
1372
      trace("Trying to match argument '{}'", currentArgument);
9✔
1373
      Property<?> currentProperty = property;
2✔
1374
      if (!arguments.isEndOptions()) {
3!
1375
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1376
        if (option != null) {
2!
1377
          currentProperty = option;
×
1378
        }
1379
      }
1380
      if (currentProperty == null) {
2!
1381
        trace("No option or next value found");
×
1382
        ValidationState state = new ValidationState(null);
×
1383
        state.addErrorMessage("No matching property found");
×
1384
        return state;
×
1385
      }
1386
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1387
      if (currentProperty == property) {
3!
1388
        if (!property.isMultiValued()) {
3✔
1389
          if (propertyIterator.hasNext()) {
3✔
1390
            property = propertyIterator.next();
5✔
1391
          } else {
1392
            property = null;
2✔
1393
          }
1394
        }
1395
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1396
          arguments.stopSplitShortOptions();
2✔
1397
        }
1398
      }
1399
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1400
      if (!matches) {
2!
1401
        ValidationState state = new ValidationState(null);
×
1402
        state.addErrorMessage("No matching property found");
×
1403
        return state;
×
1404
      }
1405
      currentArgument = arguments.current();
3✔
1406
    }
1✔
1407
    return ValidationResultValid.get();
2✔
1408
  }
1409

1410
  @Override
1411
  public Path findBash() {
1412
    if (this.bash != null) {
3✔
1413
      return this.bash;
3✔
1414
    }
1415
    Path bashPath = findBashOnBashPath();
3✔
1416
    if (bashPath == null) {
2✔
1417
      bashPath = findBashInPath();
3✔
1418
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1419
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1420
        if (bashPath == null) {
2!
1421
          bashPath = findBashInWindowsRegistry();
3✔
1422
        }
1423
      }
1424
    }
1425
    if (bashPath == null) {
2✔
1426
      error("No bash executable could be found on your system.");
4✔
1427
    } else {
1428
      this.bash = bashPath;
3✔
1429
    }
1430
    return bashPath;
2✔
1431
  }
1432

1433
  private Path findBashOnBashPath() {
1434
    trace("Trying to find BASH_PATH environment variable.");
3✔
1435
    Path bash;
1436
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1437
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1438
    if (bashVariable != null) {
2✔
1439
      bash = Path.of(bashVariable);
5✔
1440
      if (Files.exists(bash)) {
5✔
1441
        debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
13✔
1442
        return bash;
2✔
1443
      } else {
1444
        error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
13✔
1445
        return null;
2✔
1446
      }
1447
    } else {
1448
      debug("{} environment variable was not found", bashPathVariableName);
9✔
1449
      return null;
2✔
1450
    }
1451
  }
1452

1453
  /**
1454
   * @param path the path to check.
1455
   * @param toIgnore the String sequence which needs to be checked and ignored.
1456
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1457
   */
1458
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1459
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1460
    return !s.contains(toIgnore);
7!
1461
  }
1462

1463
  /**
1464
   * Tries to find the bash.exe within the PATH environment variable.
1465
   *
1466
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1467
   */
1468
  private Path findBashInPath() {
1469
    trace("Trying to find bash in PATH environment variable.");
3✔
1470
    Path bash;
1471
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1472
    if (pathVariableName != null) {
2!
1473
      Path plainBash = Path.of(BASH);
5✔
1474
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1475
          "\\windows\\system32");
1476
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1477
      bash = bashPath.toAbsolutePath();
3✔
1478
      if (bashPath.equals(plainBash)) {
4✔
1479
        warning("No usable bash executable was found in your PATH environment variable!");
3✔
1480
        bash = null;
3✔
1481
      } else {
1482
        if (Files.exists(bashPath)) {
5!
1483
          debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
10✔
1484
        } else {
1485
          bash = null;
×
1486
          error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1487
        }
1488
      }
1489
    } else {
1✔
1490
      bash = null;
×
1491
      // this should never happen...
1492
      error("PATH environment variable was not found");
×
1493
    }
1494
    return bash;
2✔
1495
  }
1496

1497
  /**
1498
   * Tries to find the bash.exe within the Windows registry.
1499
   *
1500
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1501
   */
1502
  protected Path findBashInWindowsRegistry() {
1503
    trace("Trying to find bash in Windows registry");
×
1504
    // If not found in the default location, try the registry query
1505
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1506
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1507
    for (String bashVariant : bashVariants) {
×
1508
      trace("Trying to find bash variant: {}", bashVariant);
×
1509
      for (String registryKey : registryKeys) {
×
1510
        trace("Trying to find bash from registry key: {}", registryKey);
×
1511
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1512
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1513

1514
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1515
        if (path != null) {
×
1516
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1517
          if (Files.exists(bashPath)) {
×
1518
            debug("Found bash at: {}", bashPath);
×
1519
            return bashPath;
×
1520
          } else {
1521
            error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1522
            return null;
×
1523
          }
1524
        } else {
1525
          info("No bash executable could be found in the Windows registry.");
×
1526
        }
1527
      }
1528
    }
1529
    // no bash found
1530
    return null;
×
1531
  }
1532

1533
  private Path findBashOnWindowsDefaultGitPath() {
1534
    // Check if Git Bash exists in the default location
1535
    trace("Trying to find bash on the Windows default git path.");
3✔
1536
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1537
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1538
      trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1539
      return defaultPath;
×
1540
    }
1541
    debug("No bash was found on the Windows default git path.");
3✔
1542
    return null;
2✔
1543
  }
1544

1545
  @Override
1546
  public WindowsPathSyntax getPathSyntax() {
1547

1548
    return this.pathSyntax;
3✔
1549
  }
1550

1551
  /**
1552
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1553
   */
1554
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1555

1556
    this.pathSyntax = pathSyntax;
3✔
1557
  }
1✔
1558

1559
  /**
1560
   * @return the {@link IdeStartContextImpl}.
1561
   */
1562
  public IdeStartContextImpl getStartContext() {
1563

1564
    return startContext;
3✔
1565
  }
1566

1567
  /**
1568
   * @return the {@link WindowsHelper}.
1569
   */
1570
  public final WindowsHelper getWindowsHelper() {
1571

1572
    if (this.windowsHelper == null) {
3✔
1573
      this.windowsHelper = createWindowsHelper();
4✔
1574
    }
1575
    return this.windowsHelper;
3✔
1576
  }
1577

1578
  /**
1579
   * @return the new {@link WindowsHelper} instance.
1580
   */
1581
  protected WindowsHelper createWindowsHelper() {
1582

1583
    return new WindowsHelperImpl(this);
×
1584
  }
1585

1586
  /**
1587
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1588
   */
1589
  public void reload() {
1590

1591
    this.variables = null;
3✔
1592
    this.customToolRepository = null;
3✔
1593
  }
1✔
1594

1595
  @Override
1596
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1597

1598
    assert (Files.isDirectory(installationPath));
6!
1599
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1600
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1601
  }
1✔
1602

1603
  /**
1604
   * @param home the IDE_HOME directory.
1605
   * @param workspace the name of the active workspace folder.
1606
   */
1607
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1608

1609
  }
1610

1611
  /**
1612
   * Returns the default git path on Windows. Required to be overwritten in tests.
1613
   *
1614
   * @return default path to git on Windows.
1615
   */
1616
  public String getDefaultWindowsGitPath() {
1617
    return DEFAULT_WINDOWS_GIT_PATH;
×
1618
  }
1619

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

© 2025 Coveralls, Inc