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

devonfw / IDEasy / 30532321896

30 Jul 2026 09:50AM UTC coverage: 72.592% (-0.001%) from 72.593%
30532321896

Pull #2204

github

web-flow
Merge 3ede9fc31 into ec264ffca
Pull Request #2204: #2192-support-for-auto-completion-synonyms

5000 of 7617 branches covered (65.64%)

Branch coverage included in aggregate %.

12888 of 17025 relevant lines covered (75.7%)

3.21 hits per line

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

70.6
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.io.ByteArrayInputStream;
6
import java.io.ByteArrayOutputStream;
7
import java.io.IOException;
8
import java.nio.file.Files;
9
import java.nio.file.Path;
10
import java.time.LocalDateTime;
11
import java.util.ArrayList;
12
import java.util.HashMap;
13
import java.util.Iterator;
14
import java.util.List;
15
import java.util.Locale;
16
import java.util.Map;
17
import java.util.Map.Entry;
18
import java.util.Objects;
19
import java.util.Properties;
20
import java.util.function.Predicate;
21
import java.util.logging.FileHandler;
22
import java.util.logging.LogManager;
23
import java.util.logging.SimpleFormatter;
24

25
import org.slf4j.Logger;
26
import org.slf4j.LoggerFactory;
27

28
import com.devonfw.tools.ide.cli.CliAbortException;
29
import com.devonfw.tools.ide.cli.CliArgument;
30
import com.devonfw.tools.ide.cli.CliArguments;
31
import com.devonfw.tools.ide.cli.CliException;
32
import com.devonfw.tools.ide.cli.CliSuggester;
33
import com.devonfw.tools.ide.commandlet.Commandlet;
34
import com.devonfw.tools.ide.commandlet.CommandletManager;
35
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
36
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
37
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
38
import com.devonfw.tools.ide.commandlet.UpdateCommandlet;
39
import com.devonfw.tools.ide.commandlet.UpgradeCommandlet;
40
import com.devonfw.tools.ide.common.SystemPath;
41
import com.devonfw.tools.ide.completion.CompletionCandidate;
42
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
43
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
44
import com.devonfw.tools.ide.environment.EnvironmentVariables;
45
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
46
import com.devonfw.tools.ide.environment.IdeSystem;
47
import com.devonfw.tools.ide.environment.IdeSystemImpl;
48
import com.devonfw.tools.ide.git.GitContext;
49
import com.devonfw.tools.ide.git.GitContextImpl;
50
import com.devonfw.tools.ide.git.GitUrl;
51
import com.devonfw.tools.ide.io.FileAccess;
52
import com.devonfw.tools.ide.io.FileAccessImpl;
53
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
54
import com.devonfw.tools.ide.log.IdeLogLevel;
55
import com.devonfw.tools.ide.log.IdeLogListener;
56
import com.devonfw.tools.ide.log.JulConsoleHandler;
57
import com.devonfw.tools.ide.merge.DirectoryMerger;
58
import com.devonfw.tools.ide.migration.IdeMigrator;
59
import com.devonfw.tools.ide.network.NetworkStatus;
60
import com.devonfw.tools.ide.network.NetworkStatusImpl;
61
import com.devonfw.tools.ide.os.SystemInfo;
62
import com.devonfw.tools.ide.os.SystemInfoImpl;
63
import com.devonfw.tools.ide.os.WindowsHelper;
64
import com.devonfw.tools.ide.os.WindowsHelperImpl;
65
import com.devonfw.tools.ide.os.WindowsPathSyntax;
66
import com.devonfw.tools.ide.process.ProcessContext;
67
import com.devonfw.tools.ide.process.ProcessContextImpl;
68
import com.devonfw.tools.ide.process.ProcessResult;
69
import com.devonfw.tools.ide.property.KeywordProperty;
70
import com.devonfw.tools.ide.property.Property;
71
import com.devonfw.tools.ide.step.Step;
72
import com.devonfw.tools.ide.step.StepImpl;
73
import com.devonfw.tools.ide.tool.custom.CustomToolRepository;
74
import com.devonfw.tools.ide.tool.custom.CustomToolRepositoryImpl;
75
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
76
import com.devonfw.tools.ide.tool.npm.NpmRepository;
77
import com.devonfw.tools.ide.tool.pip.PipRepository;
78
import com.devonfw.tools.ide.tool.python.PythonRepository;
79
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
80
import com.devonfw.tools.ide.tool.repository.ToolRepository;
81
import com.devonfw.tools.ide.tool.uv.UvRepository;
82
import com.devonfw.tools.ide.url.model.UrlMetadata;
83
import com.devonfw.tools.ide.util.DateTimeUtil;
84
import com.devonfw.tools.ide.util.PrivacyUtil;
85
import com.devonfw.tools.ide.validation.ValidationResult;
86
import com.devonfw.tools.ide.validation.ValidationResultValid;
87
import com.devonfw.tools.ide.validation.ValidationState;
88
import com.devonfw.tools.ide.variable.IdeVariables;
89
import com.devonfw.tools.ide.version.IdeVersion;
90
import com.devonfw.tools.ide.version.VersionIdentifier;
91

92
/**
93
 * Abstract base implementation of {@link IdeContext}.
94
 */
95
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
96

97
  static final Logger LOG = LoggerFactory.getLogger(AbstractIdeContext.class);
3✔
98

99
  /** The default shell bash (Bourne Again SHell). */
100
  public static final String BASH = "bash";
101

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

104
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
105

106
  private static final String DEFAULT_WINDOWS_GIT_PATH = "C:\\Program Files\\Git\\bin\\bash.exe";
107

108
  private static final String OPTION_DETAILS_START = "([";
109

110
  private final IdeStartContextImpl startContext;
111

112
  private Path ideHome;
113

114
  private Path ideRoot;
115

116
  private Path confPath;
117

118
  protected Path settingsPath;
119

120
  private Path settingsCommitIdPath;
121

122
  protected Path pluginsPath;
123

124
  private Path workspacePath;
125

126
  private Path workspacesBasePath;
127

128
  private String workspaceName;
129

130
  private Path cwd;
131

132
  private Path downloadPath;
133

134
  private Path userHome;
135

136
  private Path userHomeIde;
137

138
  private SystemPath path;
139

140
  private WindowsPathSyntax pathSyntax;
141

142
  private final SystemInfo systemInfo;
143

144
  private EnvironmentVariables variables;
145

146
  private final FileAccess fileAccess;
147

148
  protected CommandletManager commandletManager;
149

150
  protected ToolRepository defaultToolRepository;
151

152
  private CustomToolRepository customToolRepository;
153

154
  private MvnRepository mvnRepository;
155

156
  private NpmRepository npmRepository;
157

158
  private PipRepository pipRepository;
159

160
  private UvRepository uvRepository;
161

162
  private PythonRepository pythonRepository;
163

164
  private DirectoryMerger workspaceMerger;
165

166
  protected UrlMetadata urlMetadata;
167

168
  protected Path defaultExecutionDirectory;
169

170
  private StepImpl currentStep;
171

172
  private NetworkStatus networkStatus;
173

174
  protected IdeSystem system;
175

176
  private WindowsHelper windowsHelper;
177

178
  private final Map<String, String> privacyMap;
179

180
  private Path bash;
181

182
  private boolean julConfigured;
183

184
  private Path logfile;
185

186
  private CliSuggester cliSuggester;
187

188
  /**
189
   * The constructor.
190
   *
191
   * @param startContext the {@link IdeStartContextImpl}.
192
   * @param workingDirectory the optional {@link Path} to current working directory.
193
   */
194
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
195

196
    super();
2✔
197
    this.startContext = startContext;
3✔
198
    this.startContext.setArgFormatter(this);
4✔
199
    this.privacyMap = new HashMap<>();
5✔
200
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
201
    if (isTest()) {
3!
202
      configureJavaUtilLogging(null);
3✔
203
    }
204
    this.commandletManager = new CommandletManagerImpl(this);
6✔
205
    this.fileAccess = new FileAccessImpl(this);
6✔
206
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
207
    if (userHomeProperty != null) {
2!
208
      this.userHome = Path.of(userHomeProperty);
×
209
    }
210
    if (workingDirectory == null) {
2!
211
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
212
    }
213
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
214
    if (Files.isDirectory(workingDirectory)) {
5✔
215
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
216
    } else {
217
      LOG.warn("Current working directory does not exist: {}", workingDirectory);
4✔
218
    }
219
    this.cwd = workingDirectory;
3✔
220
    // detect IDE_HOME and WORKSPACE
221
    String workspace = null;
2✔
222
    Path ideHomeDir = null;
2✔
223
    IdeHomeAndWorkspace ideHomeAndWorkspace = findIdeHome(workingDirectory);
4✔
224
    if (ideHomeAndWorkspace != null) {
2!
225
      ideHomeDir = ideHomeAndWorkspace.home();
3✔
226
      workspace = ideHomeAndWorkspace.workspace();
3✔
227
    }
228

229
    // detection completed, initializing variables
230
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
231

232
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
233

234
    if (this.ideRoot != null) {
3✔
235
      Path tempDownloadPath = getTempDownloadPath();
3✔
236
      if (Files.isDirectory(tempDownloadPath)) {
6✔
237
        // TODO delete all files older than 1 day here...
238
      } else {
239
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
240
      }
241
    }
242
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
243
  }
1✔
244

245
  /**
246
   * 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
247
   * add additional validation or boundary checks.
248
   *
249
   * @param workingDirectory the starting directory for the search.
250
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
251
   */
252
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
253

254
    Path currentDir = workingDirectory;
2✔
255
    String name1 = "";
2✔
256
    String name2 = "";
2✔
257
    String workspace = WORKSPACE_MAIN;
2✔
258
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
259

260
    while (currentDir != null) {
2✔
261
      LOG.trace("Looking for IDE_HOME in {}", currentDir);
4✔
262
      if (isIdeHome(currentDir)) {
4✔
263
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
264
          workspace = name2;
3✔
265
        }
266
        break;
267
      }
268
      name2 = name1;
2✔
269
      int nameCount = currentDir.getNameCount();
3✔
270
      if (nameCount >= 1) {
3✔
271
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
272
      }
273
      currentDir = currentDir.getParent();
3✔
274
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
275
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
276
        currentDir = null;
×
277
      }
278
    }
1✔
279

280
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
281
  }
282

283
  /**
284
   * @return a new {@link MvnRepository}
285
   */
286
  protected MvnRepository createMvnRepository() {
287
    return new MvnRepository(this);
5✔
288
  }
289

290
  /**
291
   * @return a new {@link NpmRepository}
292
   */
293
  protected NpmRepository createNpmRepository() {
294
    return new NpmRepository(this);
×
295
  }
296

297
  /**
298
   * @return a new {@link PipRepository}
299
   */
300
  protected PipRepository createPipRepository() {
301
    return new PipRepository(this);
×
302
  }
303

304
  /**
305
   * @return a new {@link UvRepository}
306
   */
307
  protected UvRepository createUvRepository() {
308
    return new UvRepository(this);
×
309
  }
310

311
  /**
312
   * @return a new {@link PythonRepository}
313
   */
314
  protected PythonRepository createPythonRepository() {
315
    return new PythonRepository(this);
5✔
316
  }
317

318
  private Path findIdeRoot(Path ideHomePath) {
319

320
    Path ideRootPath = null;
2✔
321
    if (ideHomePath != null) {
2✔
322
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
323
      ideRootPath = ideHomePath.getParent();
3✔
324
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
2!
325
        LOG.warn(
×
326
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
327
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
328
            ideRootPathFromEnv,
329
            ideHomePath.getFileName(), ideRootPath);
×
330
      }
331
    } else if (!isTest()) {
4!
332
      ideRootPath = getIdeRootPathFromEnv(true);
×
333
    }
334
    return ideRootPath;
2✔
335
  }
336

337
  /**
338
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
339
   */
340
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
341

342
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
343
    if (root != null) {
×
344
      Path rootPath = Path.of(root);
×
345
      if (Files.isDirectory(rootPath)) {
×
346
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
347
        if (withSanityCheck) {
×
348
          int nameCount = rootPath.getNameCount();
×
349
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
350
          int delta = absoluteNameCount - nameCount;
×
351
          if (delta >= 0) {
×
352
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
353
              String rootName = rootPath.getName(nameIndex).toString();
×
354
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
355
              if (!rootName.equals(absoluteRootName)) {
×
356
                LOG.warn("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
357
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
358
                break;
×
359
              }
360
            }
361
          } else {
362
            LOG.warn("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
363
                absoluteRootPath);
364
          }
365
        }
366
        return absoluteRootPath;
×
367
      } else if (withSanityCheck) {
×
368
        LOG.warn("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
369
      }
370
    }
371
    return null;
×
372
  }
373

374
  @Override
375
  public void setCwd(Path userDir, String workspace, Path ideHome) {
376

377
    this.cwd = userDir;
3✔
378
    this.workspaceName = workspace;
3✔
379
    this.ideHome = ideHome;
3✔
380
    if (ideHome == null) {
2✔
381
      this.workspacesBasePath = null;
3✔
382
      this.workspacePath = null;
3✔
383
      this.confPath = null;
3✔
384
      this.settingsPath = null;
3✔
385
      this.pluginsPath = null;
4✔
386
    } else {
387
      this.workspacesBasePath = this.ideHome.resolve(FOLDER_WORKSPACES);
6✔
388
      this.workspacePath = this.workspacesBasePath.resolve(this.workspaceName);
7✔
389
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
390
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
391
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
392
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
393
    }
394
    if (isTest()) {
3!
395
      // only for testing...
396
      if (this.ideHome == null) {
3✔
397
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
398
      } else {
399
        this.userHome = this.ideHome.resolve("home");
6✔
400
      }
401
    }
402
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
403
    this.downloadPath = computeDownloadPath(this.userHome);
6✔
404
    resetPrivacyMap();
2✔
405
    this.path = computeSystemPath();
4✔
406
  }
1✔
407

408
  /**
409
   * On macOS, {@code ~/Downloads} is protected by the OS (TCC) and the CLI may not be allowed to delete it, so we put the cache under {@code ~/Library/Caches}
410
   * instead. Tests still use {@code ~/Downloads/ide} so existing fixtures keep working.
411
   */
412
  private Path computeDownloadPath(Path home) {
413

414
    if (!isTest() && this.systemInfo.isMac()) {
3!
415
      return home.resolve("Library/Caches/IDEasy/downloads");
×
416
    }
417
    return home.resolve("Downloads/ide");
4✔
418
  }
419

420
  private String getMessageIdeHomeFound() {
421

422
    String wks = this.workspaceName;
3✔
423
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
424
      wks = "*".repeat(wks.length());
×
425
    }
426
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
427
  }
428

429
  private String getMessageNotInsideIdeProject() {
430

431
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
432
  }
433

434
  private String getMessageIdeRootNotFound() {
435

436
    String root = getSystem().getEnv("IDE_ROOT");
5✔
437
    if (root == null) {
2!
438
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
439
    } else {
440
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
441
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
442
    }
443
  }
444

445
  /**
446
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
447
   */
448
  public boolean isTest() {
449

450
    return false;
×
451
  }
452

453
  protected SystemPath computeSystemPath() {
454

455
    return new SystemPath(this);
×
456
  }
457

458
  /**
459
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
460
   *
461
   * @param dir the directory to check.
462
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
463
   */
464
  protected boolean isIdeHome(Path dir) {
465

466
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
467
      return false;
2✔
468
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
469
      return false;
×
470
    }
471
    return true;
2✔
472
  }
473

474
  private EnvironmentVariables createVariables() {
475

476
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
477
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
478
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
479
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
480
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
481
    return conf.resolved();
3✔
482
  }
483

484
  protected AbstractEnvironmentVariables createSystemVariables() {
485

486
    return EnvironmentVariables.ofSystem(this);
3✔
487
  }
488

489
  @Override
490
  public SystemInfo getSystemInfo() {
491

492
    return this.systemInfo;
3✔
493
  }
494

495
  @Override
496
  public FileAccess getFileAccess() {
497

498
    return this.fileAccess;
3✔
499
  }
500

501
  @Override
502
  public CommandletManager getCommandletManager() {
503

504
    return this.commandletManager;
3✔
505
  }
506

507
  @Override
508
  public ToolRepository getDefaultToolRepository() {
509

510
    return this.defaultToolRepository;
3✔
511
  }
512

513
  @Override
514
  public MvnRepository getMvnRepository() {
515
    if (this.mvnRepository == null) {
3✔
516
      this.mvnRepository = createMvnRepository();
4✔
517
    }
518
    return this.mvnRepository;
3✔
519
  }
520

521
  @Override
522
  public NpmRepository getNpmRepository() {
523
    if (this.npmRepository == null) {
3✔
524
      this.npmRepository = createNpmRepository();
4✔
525
    }
526
    return this.npmRepository;
3✔
527
  }
528

529
  @Override
530
  public PipRepository getPipRepository() {
531
    if (this.pipRepository == null) {
3✔
532
      this.pipRepository = createPipRepository();
4✔
533
    }
534
    return this.pipRepository;
3✔
535
  }
536

537
  @Override
538
  public UvRepository getUvRepository() {
539
    if (this.uvRepository == null) {
3✔
540
      this.uvRepository = createUvRepository();
4✔
541
    }
542
    return this.uvRepository;
3✔
543
  }
544

545
  @Override
546
  public PythonRepository getPythonRepository() {
547
    if (this.pythonRepository == null) {
3✔
548
      this.pythonRepository = createPythonRepository();
4✔
549
    }
550
    return this.pythonRepository;
3✔
551
  }
552

553
  @Override
554
  public CustomToolRepository getCustomToolRepository() {
555

556
    if (this.customToolRepository == null) {
3✔
557
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
558
    }
559
    return this.customToolRepository;
3✔
560
  }
561

562
  @Override
563
  public Path getIdeHome() {
564

565
    return this.ideHome;
3✔
566
  }
567

568
  @Override
569
  public String getProjectName() {
570

571
    if (this.ideHome != null) {
3!
572
      return this.ideHome.getFileName().toString();
5✔
573
    }
574
    return "";
×
575
  }
576

577
  @Override
578
  public VersionIdentifier getProjectVersion() {
579

580
    if (this.ideHome != null) {
3!
581
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
582
      if (Files.exists(versionFile)) {
5✔
583
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
584
        return VersionIdentifier.of(version);
3✔
585
      }
586
    }
587
    return IdeMigrator.START_VERSION;
2✔
588
  }
589

590
  @Override
591
  public void setProjectVersion(VersionIdentifier version) {
592

593
    if (this.ideHome == null) {
3!
594
      throw new IllegalStateException("IDE_HOME not available!");
×
595
    }
596
    Objects.requireNonNull(version);
3✔
597
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
598
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
599
  }
1✔
600

601
  @Override
602
  public Path getIdeRoot() {
603

604
    return this.ideRoot;
3✔
605
  }
606

607
  @Override
608
  public void setIdeRoot(Path ideRoot) {
609

610
    this.ideRoot = ideRoot;
3✔
611
  }
1✔
612

613
  @Override
614
  public Path getIdePath() {
615

616
    Path myIdeRoot = getIdeRoot();
3✔
617
    if (myIdeRoot == null) {
2✔
618
      return null;
2✔
619
    }
620
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
621
  }
622

623
  @Override
624
  public Path getCwd() {
625

626
    return this.cwd;
3✔
627
  }
628

629
  @Override
630
  public Path getTempPath() {
631

632
    Path idePath = getIdePath();
3✔
633
    if (idePath == null) {
2!
634
      return null;
×
635
    }
636
    return idePath.resolve("tmp");
4✔
637
  }
638

639
  @Override
640
  public Path getTempDownloadPath() {
641

642
    Path tmp = getTempPath();
3✔
643
    if (tmp == null) {
2!
644
      return null;
×
645
    }
646
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
647
  }
648

649
  @Override
650
  public Path getUserHome() {
651

652
    return this.userHome;
3✔
653
  }
654

655
  /**
656
   * This method should only be used for tests to mock user home.
657
   *
658
   * @param userHome the new value of {@link #getUserHome()}.
659
   */
660
  protected void setUserHome(Path userHome) {
661

662
    this.userHome = userHome;
3✔
663
    this.userHomeIde = userHome.resolve(FOLDER_DOT_IDE);
5✔
664
    this.downloadPath = computeDownloadPath(userHome);
5✔
665
    this.variables = null;
3✔
666
    resetPrivacyMap();
2✔
667
  }
1✔
668

669
  @Override
670
  public Path getUserHomeIde() {
671

672
    return this.userHomeIde;
3✔
673
  }
674

675
  @Override
676
  public Path getSettingsPath() {
677

678
    return this.settingsPath;
3✔
679
  }
680

681
  @Override
682
  public Path getSettingsGitRepository() {
683

684
    Path settingsPath = getSettingsPath();
3✔
685
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
686
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsCodeRepository()) {
12!
687
      LOG.error("Settings repository exists but is not a git repository.");
3✔
688
      return null;
2✔
689
    }
690
    return settingsPath;
2✔
691
  }
692

693
  @Override
694
  public boolean isSettingsCodeRepository() {
695

696
    Path settingsPath = getSettingsPath();
3✔
697
    if (settingsPath != null) {
2!
698
      boolean settingsIsLink = Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
699
      if (settingsIsLink) {
2!
700
        Path realPath = getFileAccess().toRealPath(this.settingsPath);
×
701
        if (realPath != null) {
×
702
          return getGitContext().isGitRepo(realPath.getParent());
×
703
        }
704
        return true;
×
705
      }
706
    }
707
    return false;
2✔
708
  }
709

710
  @Override
711
  public Path getSettingsCommitIdPath() {
712

713
    return this.settingsCommitIdPath;
3✔
714
  }
715

716
  @Override
717
  public Path getConfPath() {
718

719
    return this.confPath;
3✔
720
  }
721

722
  @Override
723
  public Path getSoftwarePath() {
724

725
    if (this.ideHome == null) {
3✔
726
      return null;
2✔
727
    }
728
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
729
  }
730

731
  @Override
732
  public Path getSoftwareExtraPath() {
733

734
    Path softwarePath = getSoftwarePath();
3✔
735
    if (softwarePath == null) {
2✔
736
      return null;
2✔
737
    }
738
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
739
  }
740

741
  @Override
742
  public Path getSoftwareRepositoryPath() {
743

744
    Path idePath = getIdePath();
3✔
745
    if (idePath == null) {
2!
746
      return null;
×
747
    }
748
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
749
  }
750

751
  @Override
752
  public Path getPluginsPath() {
753

754
    return this.pluginsPath;
3✔
755
  }
756

757
  @Override
758
  public String getWorkspaceName() {
759

760
    return this.workspaceName;
3✔
761
  }
762

763
  @Override
764
  public Path getWorkspacesBasePath() {
765

766
    return this.workspacesBasePath;
3✔
767
  }
768

769
  @Override
770
  public Path getWorkspacePath() {
771

772
    return this.workspacePath;
3✔
773
  }
774

775
  @Override
776
  public Path getWorkspacePath(String workspace) {
777

778
    if (this.workspacesBasePath == null) {
3!
779
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
780
    }
781
    return this.workspacesBasePath.resolve(workspace);
5✔
782
  }
783

784
  @Override
785
  public Path getDownloadPath() {
786

787
    return this.downloadPath;
3✔
788
  }
789

790
  @Override
791
  public Path getUrlsPath() {
792

793
    Path idePath = getIdePath();
3✔
794
    if (idePath == null) {
2!
795
      return null;
×
796
    }
797
    return idePath.resolve(FOLDER_URLS);
4✔
798
  }
799

800
  @Override
801
  public Path getToolRepositoryPath() {
802

803
    Path idePath = getIdePath();
3✔
804
    if (idePath == null) {
2!
805
      return null;
×
806
    }
807
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
808
  }
809

810
  @Override
811
  public SystemPath getPath() {
812

813
    return this.path;
3✔
814
  }
815

816
  @Override
817
  public EnvironmentVariables getVariables() {
818

819
    if (this.variables == null) {
3✔
820
      this.variables = createVariables();
4✔
821
    }
822
    return this.variables;
3✔
823
  }
824

825
  @Override
826
  public UrlMetadata getUrls() {
827

828
    if (this.urlMetadata == null) {
3✔
829
      if (!isTest()) {
3!
830
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
831
      }
832
      this.urlMetadata = new UrlMetadata(this);
6✔
833
    }
834
    return this.urlMetadata;
3✔
835
  }
836

837
  @Override
838
  public boolean isQuietMode() {
839

840
    return this.startContext.isQuietMode();
4✔
841
  }
842

843
  @Override
844
  public boolean isBatchMode() {
845

846
    return this.startContext.isBatchMode();
4✔
847
  }
848

849
  @Override
850
  public boolean isForceMode() {
851

852
    return this.startContext.isForceMode();
4✔
853
  }
854

855
  @Override
856
  public boolean isForcePull() {
857

858
    return this.startContext.isForcePull();
4✔
859
  }
860

861
  @Override
862
  public boolean isForcePlugins() {
863

864
    return this.startContext.isForcePlugins();
4✔
865
  }
866

867
  @Override
868
  public boolean isForceRepositories() {
869

870
    return this.startContext.isForceRepositories();
4✔
871
  }
872

873
  @Override
874
  public boolean isOfflineMode() {
875

876
    return this.startContext.isOfflineMode();
4✔
877
  }
878

879
  @Override
880
  public boolean isPrivacyMode() {
881
    return this.startContext.isPrivacyMode();
4✔
882
  }
883

884
  @Override
885
  public boolean isSkipUpdatesMode() {
886

887
    return this.startContext.isSkipUpdatesMode();
4✔
888
  }
889

890
  @Override
891
  public boolean isNoColorsMode() {
892

893
    return this.startContext.isNoColorsMode();
×
894
  }
895

896
  @Override
897
  public NetworkStatus getNetworkStatus() {
898

899
    if (this.networkStatus == null) {
×
900
      this.networkStatus = new NetworkStatusImpl(this);
×
901
    }
902
    return this.networkStatus;
×
903
  }
904

905
  @Override
906
  public Locale getLocale() {
907

908
    Locale locale = this.startContext.getLocale();
4✔
909
    if (locale == null) {
2✔
910
      locale = Locale.getDefault();
2✔
911
    }
912
    return locale;
2✔
913
  }
914

915
  @Override
916
  public DirectoryMerger getWorkspaceMerger() {
917

918
    if (this.workspaceMerger == null) {
3✔
919
      this.workspaceMerger = new DirectoryMerger(this);
6✔
920
    }
921
    return this.workspaceMerger;
3✔
922
  }
923

924
  /**
925
   * @return the default execution directory in which a command process is executed.
926
   */
927
  @Override
928
  public Path getDefaultExecutionDirectory() {
929

930
    return this.defaultExecutionDirectory;
×
931
  }
932

933
  /**
934
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
935
   */
936
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
937

938
    if (defaultExecutionDirectory != null) {
×
939
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
940
    }
941
  }
×
942

943
  @Override
944
  public GitContext getGitContext() {
945

946
    return new GitContextImpl(this);
×
947
  }
948

949
  @Override
950
  public ProcessContext newProcess() {
951

952
    ProcessContext processContext = createProcessContext();
3✔
953
    if (this.defaultExecutionDirectory != null) {
3!
954
      processContext.directory(this.defaultExecutionDirectory);
×
955
    }
956
    return processContext;
2✔
957
  }
958

959
  @Override
960
  public IdeSystem getSystem() {
961

962
    if (this.system == null) {
×
963
      this.system = new IdeSystemImpl();
×
964
    }
965
    return this.system;
×
966
  }
967

968
  /**
969
   * @return a new instance of {@link ProcessContext}.
970
   * @see #newProcess()
971
   */
972
  protected ProcessContext createProcessContext() {
973

974
    return new ProcessContextImpl(this);
×
975
  }
976

977
  @Override
978
  public IdeLogLevel getLogLevelConsole() {
979

980
    return this.startContext.getLogLevelConsole();
4✔
981
  }
982

983
  @Override
984
  public IdeLogLevel getLogLevelLogger() {
985

986
    return this.startContext.getLogLevelLogger();
×
987
  }
988

989
  @Override
990
  public IdeLogListener getLogListener() {
991

992
    return this.startContext.getLogListener();
×
993
  }
994

995
  @Override
996
  public void logIdeHomeAndRootStatus() {
997
    if (this.ideRoot != null) {
3✔
998
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
11✔
999
    }
1000
    if (this.ideHome == null) {
3✔
1001
      LOG.warn(getMessageNotInsideIdeProject());
5✔
1002
    } else {
1003
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
1004
    }
1005
  }
1✔
1006

1007
  @Override
1008
  public String formatArgument(Object argument) {
1009

1010
    if (argument == null) {
2✔
1011
      return null;
2✔
1012
    }
1013
    String result = argument.toString();
3✔
1014
    if (isPrivacyMode()) {
3✔
1015
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
7!
1016
        initializePrivacyMap(this.userHome, "~");
5✔
1017
        String projectName = getProjectName();
3✔
1018
        if (!projectName.isEmpty()) {
3!
1019
          this.privacyMap.put(projectName, "project");
6✔
1020
        }
1021
      }
1022
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
12✔
1023
        result = result.replace(entry.getKey(), entry.getValue());
9✔
1024
      }
1✔
1025
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
1026
    }
1027
    return result;
2✔
1028
  }
1029

1030
  /**
1031
   * @param path the sensitive {@link Path} to
1032
   * @param replacement the replacement to mask the {@link Path} in log output.
1033
   */
1034
  protected void initializePrivacyMap(Path path, String replacement) {
1035

1036
    if (path == null) {
2!
1037
      return;
×
1038
    }
1039
    if (this.systemInfo.isWindows()) {
4!
1040
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
1041
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
1042
    } else {
1043
      this.privacyMap.put(path.toString(), replacement);
7✔
1044
    }
1045
  }
1✔
1046

1047
  /**
1048
   * Resets the privacy map in case fundamental values have changed.
1049
   */
1050
  private void resetPrivacyMap() {
1051

1052
    this.privacyMap.clear();
3✔
1053
  }
1✔
1054

1055

1056
  @Override
1057
  public String askForInput(String message, String defaultValue) {
1058

1059
    while (true) {
1060
      if (!message.isBlank()) {
3!
1061
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
1062
      }
1063
      if (isBatchMode()) {
3!
1064
        if (isForceMode()) {
×
1065
          return defaultValue;
×
1066
        } else {
1067
          throw new CliAbortException();
×
1068
        }
1069
      }
1070
      String input = readLine().trim();
4✔
1071
      if (!input.isEmpty()) {
3!
1072
        return input;
2✔
1073
      } else {
1074
        if (defaultValue != null) {
×
1075
          return defaultValue;
×
1076
        }
1077
      }
1078
    }
×
1079
  }
1080

1081
  @Override
1082
  public <O> O question(O[] options, String question, Object... args) {
1083

1084
    assert (options.length > 0);
4!
1085
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1086
    LOG.warn(question, args);
4✔
1087
    return displayOptionsAndGetAnswer(options);
4✔
1088
  }
1089

1090
  private <O> O displayOptionsAndGetAnswer(O[] options) {
1091
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
1092
    int i = 0;
2✔
1093
    for (O option : options) {
16✔
1094
      i++;
1✔
1095
      String title = "" + option;
4✔
1096
      String key = computeOptionKey(title);
3✔
1097
      addMapping(mapping, key, option);
4✔
1098
      String numericKey = Integer.toString(i);
3✔
1099
      if (numericKey.equals(key)) {
4!
1100
        LOG.trace("Options should not be numeric: {}", key);
×
1101
      } else {
1102
        addMapping(mapping, numericKey, option);
4✔
1103
      }
1104
      IdeLogLevel.INTERACTION.log(LOG, "Option {}: {}", numericKey, title);
14✔
1105
    }
1106
    if (options.length == 1) {
4✔
1107
      mapping.put("", options[0]);
7✔
1108
    }
1109
    O option = null;
2✔
1110
    if (isBatchMode()) {
3!
1111
      if (isForceMode()) {
×
1112
        option = options[0];
×
1113
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
×
1114
      }
1115
    } else {
1116
      while (option == null) {
2✔
1117
        String answer = readLine();
3✔
1118
        option = mapping.get(answer);
4✔
1119
        if (option == null) {
2!
1120
          LOG.warn("Invalid answer: '{}' - please try again.", answer);
×
1121
        }
1122
      }
1✔
1123
    }
1124
    return option;
2✔
1125
  }
1126

1127
  private static String computeOptionKey(String option) {
1128
    String key = option;
2✔
1129
    int index = -1;
2✔
1130
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1131
      int currentIndex = key.indexOf(c);
4✔
1132
      if (currentIndex != -1) {
3✔
1133
        if ((index == -1) || (currentIndex < index)) {
3!
1134
          index = currentIndex;
2✔
1135
        }
1136
      }
1137
    }
1138
    if (index > 0) {
2✔
1139
      key = key.substring(0, index).trim();
6✔
1140
    }
1141
    return key;
2✔
1142
  }
1143

1144
  /**
1145
   * @return the input from the end-user (e.g. read from the console).
1146
   */
1147
  protected abstract String readLine();
1148

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

1151
    O duplicate = mapping.put(key, option);
5✔
1152
    if (duplicate != null) {
2!
1153
      throw new IllegalArgumentException("Duplicated option " + key);
×
1154
    }
1155
  }
1✔
1156

1157
  @Override
1158
  public Step getCurrentStep() {
1159

1160
    return this.currentStep;
×
1161
  }
1162

1163
  @Override
1164
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1165

1166
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1167
    return this.currentStep;
3✔
1168
  }
1169

1170
  /**
1171
   * Internal method to end the running {@link Step}.
1172
   *
1173
   * @param step the current {@link Step} to end.
1174
   */
1175
  public void endStep(StepImpl step) {
1176

1177
    if (step == this.currentStep) {
4!
1178
      this.currentStep = this.currentStep.getParent();
6✔
1179
    } else {
1180
      String currentStepName = "null";
×
1181
      if (this.currentStep != null) {
×
1182
        currentStepName = this.currentStep.getName();
×
1183
      }
1184
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1185
    }
1186
  }
1✔
1187

1188
  /**
1189
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1190
   *
1191
   * @param arguments the {@link CliArgument}.
1192
   * @return the return code of the execution.
1193
   */
1194
  public int run(CliArguments arguments) {
1195

1196
    CliArgument current = arguments.current();
3✔
1197
    if (current.isStart()) {
3✔
1198
      arguments.next();
3✔
1199
      current = arguments.current();
3✔
1200
    }
1201
    assert (this.currentStep == null);
4!
1202
    boolean supressStepSuccess = false;
2✔
1203
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1204
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1205
    Commandlet cmd = null;
2✔
1206
    ValidationResult result = null;
2✔
1207
    try {
1208
      while (commandletIterator.hasNext()) {
3✔
1209
        cmd = commandletIterator.next();
4✔
1210
        result = applyAndRun(arguments.copy(), cmd);
6✔
1211
        if (result.isValid()) {
3✔
1212
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1213
          step.success();
2✔
1214
          return ProcessResult.SUCCESS;
4✔
1215
        }
1216
      }
1217
      activateLogging(cmd);
3✔
1218
      String commandKey = current.getKey();
3✔
1219

1220
      if (commandKey == null || commandKey.isBlank()) {
5!
1221
        return 0;
×
1222
      }
1223
      Commandlet commandletByName = this.commandletManager.getCommandlet(commandKey);
5✔
1224
      // Missing commandlet
1225
      if (commandletByName == null) {
2✔
1226
        if (getCliSuggester().isMissingCommandletHandled(commandKey, step)) {
6!
1227
          return 1;
4✔
1228
        }
1229
        return 0;
×
1230
      }
1231
      // Missing project context
1232
      if (getCliSuggester().isMissingProjectContextHandled(commandletByName, step)) {
6✔
1233
        return 1;
4✔
1234
      }
1235
      // Only validate options/arguments if same commandlet and proper type
1236
      if (cmd != commandletByName || !(result instanceof ValidationState validationState)) {
10!
1237
        return 0;
×
1238
      }
1239
      // Invalid option
1240
      if (getCliSuggester().isInvalidOptionHandled(validationState, commandletByName, step)) {
7✔
1241
        return 1;
4✔
1242
      }
1243
      // Invalid argument
1244
      if (getCliSuggester().isInvalidArgumentHandled(validationState, commandletByName)) {
6!
1245
        return 1;
4✔
1246
      }
1247
      LOG.error(result.getErrorMessage());
×
1248
      step.error("Invalid arguments: {}", current.getArgs());
×
1249
      IdeLogLevel.INTERACTION.log(LOG, "For additional details run ide help {}", cmd == null ? "" : cmd.getName());
×
1250
      return 1;
×
1251
    } catch (Throwable t) {
1✔
1252
      activateLogging(cmd);
3✔
1253
      step.error(t, true);
4✔
1254
      if (this.logfile != null) {
3!
1255
        // point the user to the logfile directly (does not make sense via logger)
1256
        System.err.println("Logfile can be found at " + this.logfile); // checkstyle:ignore SystemOut
×
1257
      }
1258
      throw t;
2✔
1259
    } finally {
1260
      step.close();
2✔
1261
      assert (this.currentStep == null);
4!
1262
      step.logSummary(supressStepSuccess);
3✔
1263
    }
1264
  }
1265

1266

1267
  /**
1268
   * @return the {@link CliSuggester} for CLI suggestions.
1269
   */
1270
  private CliSuggester getCliSuggester() {
1271
    if (this.cliSuggester == null) {
3✔
1272
      this.cliSuggester = new CliSuggester(this);
6✔
1273
    }
1274
    return this.cliSuggester;
3✔
1275
  }
1276

1277
  /**
1278
   * Ensure the logging system is initialized.
1279
   */
1280
  private void activateLogging(Commandlet cmd) {
1281

1282
    configureJavaUtilLogging(cmd);
3✔
1283
    this.startContext.activateLogging();
3✔
1284
  }
1✔
1285

1286
  /**
1287
   * Configures the logging system (JUL).
1288
   *
1289
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1290
   */
1291
  public void configureJavaUtilLogging(Commandlet cmd) {
1292

1293
    if (this.julConfigured) {
3✔
1294
      return;
1✔
1295
    }
1296
    boolean writeLogfile = isWriteLogfile(cmd);
4✔
1297
    this.startContext.setWriteLogfile(writeLogfile);
4✔
1298
    Properties properties = createJavaUtilLoggingProperties(writeLogfile, cmd);
5✔
1299
    try {
1300
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1301
      properties.store(out, null);
4✔
1302
      out.flush();
2✔
1303
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1304
      LogManager.getLogManager().readConfiguration(in);
3✔
1305
      this.julConfigured = true;
3✔
1306
      this.startContext.activateLogging();
3✔
1307
    } catch (IOException e) {
×
1308
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1309
    }
1✔
1310
  }
1✔
1311

1312
  protected boolean isWriteLogfile(Commandlet cmd) {
1313
    if ((cmd == null) || !cmd.isWriteLogFile()) {
×
1314
      return false;
×
1315
    }
1316
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1317
    return Boolean.TRUE.equals(writeLogfile);
×
1318
  }
1319

1320
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1321

1322
    Path idePath = getIdePath();
3✔
1323
    if (writeLogfile && (idePath == null)) {
2!
1324
      writeLogfile = false;
×
1325
      LOG.error("Cannot enable log-file since IDE_ROOT is undefined.");
×
1326
    }
1327
    Properties properties = new Properties();
4✔
1328
    // prevent 3rd party (e.g. java.lang.ProcessBuilder) logging into our console via JUL
1329
    // see JulLogLevel for the trick we did to workaround JUL flaws
1330
    properties.setProperty(".level", "SEVERE");
5✔
1331
    if (writeLogfile) {
2!
1332
      this.startContext.setLogLevelLogger(IdeLogLevel.TRACE);
×
1333
      String fileHandlerName = FileHandler.class.getName();
×
1334
      properties.setProperty("handlers", JulConsoleHandler.class.getName() + "," + fileHandlerName);
×
1335
      properties.setProperty(fileHandlerName + ".formatter", SimpleFormatter.class.getName());
×
1336
      properties.setProperty(fileHandlerName + ".encoding", "UTF-8");
×
1337
      this.logfile = createLogfilePath(idePath, cmd);
×
1338
      getFileAccess().mkdirs(this.logfile.getParent());
×
1339
      properties.setProperty(fileHandlerName + ".pattern", this.logfile.toString());
×
1340
    } else {
×
1341
      properties.setProperty("handlers", JulConsoleHandler.class.getName());
6✔
1342
    }
1343
    properties.setProperty(SimpleFormatter.class.getName() + ".format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL [%4$s] [%3$s] %5$s%6$s%n");
7✔
1344
    return properties;
2✔
1345
  }
1346

1347
  private Path createLogfilePath(Path idePath, Commandlet cmd) {
1348
    LocalDateTime now = LocalDateTime.now();
×
1349
    Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1350
    StringBuilder sb = new StringBuilder(32);
×
1351
    if (this.ideHome == null || ((cmd != null) && !cmd.isIdeHomeRequired())) {
×
1352
      sb.append("_ide-");
×
1353
    } else {
1354
      sb.append(this.ideHome.getFileName().toString());
×
1355
      sb.append('-');
×
1356
    }
1357
    sb.append("ide-");
×
1358
    if (cmd != null) {
×
1359
      sb.append(cmd.getName());
×
1360
      sb.append('-');
×
1361
    }
1362
    sb.append(DateTimeUtil.formatTime(now));
×
1363
    sb.append(".log");
×
1364
    return logsPath.resolve(sb.toString());
×
1365
  }
1366

1367
  @Override
1368
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1369

1370
    this.startContext.deactivateLogging(threshold);
4✔
1371
    lambda.run();
2✔
1372
    this.startContext.activateLogging();
3✔
1373
  }
1✔
1374

1375
  /**
1376
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1377
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1378
   *     {@link Commandlet} did not match and we have to try a different candidate).
1379
   */
1380
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1381

1382
    IdeLogLevel previousLogLevel = null;
2✔
1383
    cmd.reset();
2✔
1384
    ValidationResult result = apply(arguments, cmd);
5✔
1385
    if (result.isValid()) {
3✔
1386
      result = cmd.validate();
3✔
1387
    }
1388
    if (result.isValid()) {
3✔
1389
      LOG.debug("Running commandlet {}", cmd);
4✔
1390
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1391
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1392
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6✔
1393
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1394
      }
1395
      try {
1396
        if (cmd.isProcessableOutput()) {
3✔
1397
          if (!isLogLevelEnabled(IdeLogLevel.DEBUG)) {
4!
1398
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1399
            previousLogLevel = this.startContext.setLogLevelConsole(IdeLogLevel.PROCESSABLE);
×
1400
          }
1401
        } else {
1402
          if (cmd.isIdeHomeRequired()) {
3✔
1403
            LOG.debug(getMessageIdeHomeFound());
4✔
1404
          }
1405
          if (!(cmd instanceof UpgradeCommandlet)) {
3!
1406
            verifyIdeMinVersion(false);
3✔
1407
          }
1408
          Path settingsRepository = getSettingsGitRepository();
3✔
1409
          if (settingsRepository != null) {
2!
1410
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
8✔
1411
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
10!
1412
                    settingsRepository, getSettingsCommitIdPath()))) {
1✔
1413

1414
              // Inform the user that an update is available. The update message is suppressed if we are already running the update
1415
              String msg = determineSettingsUpdateMessage(cmd);
4✔
1416
              if (msg != null) {
2!
1417
                IdeLogLevel.INTERACTION.log(LOG, msg);
4✔
1418
              }
1419
            }
1420
          }
1421
        }
1422
        boolean success = ensureLicenseAgreement(cmd);
4✔
1423
        if (!success) {
2!
1424
          return ValidationResultValid.get();
×
1425
        }
1426
        cmd.run();
2✔
1427
      } finally {
1428
        if (previousLogLevel != null) {
2!
1429
          this.startContext.setLogLevelConsole(previousLogLevel);
×
1430
        }
1431
      }
1✔
1432
    } else {
1433
      LOG.trace("Commandlet did not match");
3✔
1434
    }
1435
    return result;
2✔
1436
  }
1437

1438

1439
  /**
1440
   * When an update is available for the settings repository, we log a message to the console, reminding the user to run {@code ide update}. This method
1441
   * determines the correct message to log, depending on whether the settings repository is a symlink/junction, or not. Should the user already be running the
1442
   * appropriate {@code ide update} command, the message is suppressed to avoid confusion.
1443
   *
1444
   * @param cmd the {@link Commandlet}.
1445
   * @return {@code msg} to log to the console. {@code null} if the message is suppressed.
1446
   */
1447
  private String determineSettingsUpdateMessage(Commandlet cmd) {
1448
    boolean update = cmd instanceof UpdateCommandlet;
3✔
1449
    if (isSettingsCodeRepository()) {
3!
1450
      if (update && (isForceMode() || isForcePull())) {
×
1451
        return null;
×
1452
      }
1453
      return "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1454
    } else {
1455
      if (update) {
2!
1456
        return null;
×
1457
      }
1458
      return "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
2✔
1459
    }
1460
  }
1461

1462
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1463

1464
    if (isTest()) {
3!
1465
      return true; // ignore for tests
2✔
1466
    }
1467
    getFileAccess().mkdirs(this.userHomeIde);
×
1468
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1469
    if (Files.isRegularFile(licenseAgreement)) {
×
1470
      return true; // success, license already accepted
×
1471
    }
1472
    if (cmd instanceof EnvironmentCommandlet) {
×
1473
      // 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
1474
      // 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
1475
      // printing anything anymore in such case.
1476
      return false;
×
1477
    }
1478
    activateLogging(cmd);
×
1479
    IdeLogLevel oldLogLevel = this.startContext.getLogLevelConsole();
×
1480
    IdeLogLevel newLogLevel = oldLogLevel;
×
1481
    if (oldLogLevel.ordinal() > IdeLogLevel.INFO.ordinal()) {
×
1482
      newLogLevel = IdeLogLevel.INFO;
×
1483
      this.startContext.setLogLevelConsole(newLogLevel);
×
1484
    }
1485
    StringBuilder sb = new StringBuilder(1180);
×
1486
    sb.append(LOGO).append("""
×
1487
        Welcome to IDEasy!
1488
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1489
        It supports automatic download and installation of arbitrary 3rd party tools.
1490
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1491
        But if explicitly configured, also commercial software that requires an additional license may be used.
1492
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1493
        You are solely responsible for all risks implied by using this software.
1494
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1495
        You will be able to find it online under the following URL:
1496
        """).append(LICENSE_URL);
×
1497
    if (this.ideRoot != null) {
×
1498
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1499
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1500
    }
1501
    LOG.info(sb.toString());
×
1502
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1503

1504
    sb.setLength(0);
×
1505
    LocalDateTime now = LocalDateTime.now();
×
1506
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1507
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1508
    try {
1509
      Files.writeString(licenseAgreement, sb);
×
1510
    } catch (Exception e) {
×
1511
      throw new RuntimeException("Failed to save license agreement!", e);
×
1512
    }
×
1513
    if (oldLogLevel != newLogLevel) {
×
1514
      this.startContext.setLogLevelConsole(oldLogLevel);
×
1515
    }
1516
    return true;
×
1517
  }
1518

1519
  @Override
1520
  public void verifyIdeMinVersion(boolean throwException) {
1521
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1522
    if (minVersion == null) {
2✔
1523
      return;
1✔
1524
    }
1525
    VersionIdentifier versionIdentifier = IdeVersion.getVersionIdentifier();
2✔
1526
    if (versionIdentifier.compareVersion(minVersion).isLess() && !IdeVersion.isUndefined()) {
7!
1527
      String warning = String.format("Your version of IDEasy is currently %s\n"
13✔
1528
          + "However, this is too old as your project requires at latest version %s", versionIdentifier, minVersion);
1529
      String interaction = "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
2✔
1530
          + "ide upgrade";
1531
      if (throwException) {
2✔
1532
        throw new CliException(warning + "\n" + interaction);
7✔
1533
      } else {
1534
        LOG.warn(warning);
3✔
1535
        IdeLogLevel.INTERACTION.log(LOG, interaction);
4✔
1536
      }
1537
    }
1538
  }
1✔
1539

1540
  /**
1541
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1542
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1543
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1544
   */
1545
  public List<CompletionCandidate> complete(CliArguments arguments, CompletionCandidateCollector collector, boolean includeContextOptions) {
1546

1547
    if (arguments.current().isStart()) {
4✔
1548
      arguments.next();
3✔
1549
    }
1550
    if (includeContextOptions) {
2✔
1551
      ContextCommandlet cc = new ContextCommandlet();
4✔
1552
      for (Property<?> property : cc.getProperties()) {
11✔
1553
        assert (property.isOption());
4!
1554
        property.apply(arguments, this, cc, collector);
7✔
1555
      }
1✔
1556
    }
1557
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1558
    CliArgument current = arguments.current();
3✔
1559
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1560
      collector.add(current.get(), null, null, null);
7✔
1561
    }
1562
    arguments.next();
3✔
1563
    while (commandletIterator.hasNext()) {
3✔
1564
      Commandlet cmd = commandletIterator.next();
4✔
1565
      if (!arguments.current().isEnd()) {
4✔
1566
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1567
      }
1568
    }
1✔
1569
    return collector.getSortedCandidates();
3✔
1570
  }
1571

1572
  /**
1573
   * Gets the next value property and applies its implicit end-options behavior if required.
1574
   *
1575
   * @param valueIterator the iterator over the commandlet value properties
1576
   * @param arguments the CLI arguments whose option parsing state may be updated
1577
   * @return the next value property or {@code null} if no further value property exists
1578
   */
1579
  private Property<?> nextValueProperty(Iterator<Property<?>> valueIterator, CliArguments arguments) {
1580

1581
    if (!valueIterator.hasNext()) {
3✔
1582
      return null;
2✔
1583
    }
1584

1585
    Property<?> valueProperty = valueIterator.next();
4✔
1586
    if (valueProperty.isEndOptions()) {
3✔
1587
      // Tool argument properties should accept values starting with "-" so stop option parsing here
1588
      arguments.endOptions();
2✔
1589
    }
1590
    return valueProperty;
2✔
1591
  }
1592

1593
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1594

1595
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1596

1597
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1598
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1599
    Property<?> currentValueProperty = nextValueProperty(valueIterator, arguments);
5✔
1600
    List<Property<?>> properties = cmd.getProperties();
3✔
1601
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1602
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1603
    for (Property<?> property : properties) {
10✔
1604
      if (property.isOption()) {
3✔
1605
        optionProperties.add(property);
4✔
1606
      }
1607
    }
1✔
1608
    CliArgument currentArgument = arguments.current();
3✔
1609
    while (!currentArgument.isEnd()) {
3✔
1610
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1611
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6✔
1612
        if (currentArgument.isCompletion()) {
3✔
1613
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1614
          while (optionIterator.hasNext()) {
3✔
1615
            Property<?> option = optionIterator.next();
4✔
1616
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1617
            if (success) {
2✔
1618
              optionIterator.remove();
2✔
1619
              arguments.next();
3✔
1620
            }
1621
          }
1✔
1622
        } else {
1✔
1623
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1624
          if (option != null) {
2✔
1625
            arguments.next();
3✔
1626
            boolean removed = optionProperties.remove(option);
4✔
1627
            if (!removed) {
2!
1628
              option = null;
×
1629
            }
1630
          }
1631
          if (option == null) {
2✔
1632
            LOG.trace("No such option was found.");
3✔
1633
            return;
1✔
1634
          }
1635
        }
1✔
1636
      } else {
1637
        if (currentValueProperty != null) {
2✔
1638
          boolean success = currentValueProperty.apply(arguments, this, cmd, collector);
7✔
1639
          if (!success) {
2✔
1640
            LOG.trace("Completion cannot match any further.");
3✔
1641
            return;
1✔
1642
          }
1643
          if (!currentValueProperty.isMultiValued()) {
3✔
1644
            currentValueProperty = nextValueProperty(valueIterator, arguments);
5✔
1645
          }
1646
        } else {
1✔
1647
          LOG.trace("No value left for completion.");
3✔
1648
          return;
1✔
1649
        }
1650
      }
1651
      currentArgument = arguments.current();
4✔
1652
    }
1653
  }
1✔
1654

1655
  /**
1656
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1657
   *     {@link CliArguments#copy() copy} as needed.
1658
   * @param cmd the potential {@link Commandlet} to match.
1659
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1660
   */
1661
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1662

1663
    LOG.trace("Trying to match arguments to commandlet {}", cmd.getName());
5✔
1664
    CliArgument currentArgument = arguments.current();
3✔
1665
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1666
    Property<?> property = null;
2✔
1667
    if (propertyIterator.hasNext()) {
3!
1668
      property = propertyIterator.next();
4✔
1669
    }
1670
    while (!currentArgument.isEnd()) {
3✔
1671
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1672
      Property<?> currentProperty = property;
2✔
1673
      if (!arguments.isEndOptions()) {
3✔
1674
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1675
        if (option != null) {
2✔
1676
          currentProperty = option;
3✔
1677
        } else {
1678
          boolean allowDashedValue = (property != null) && property.isValue()
6!
1679
              && (property.isMultiValued() || "-".equals(currentArgument.get()));
11!
1680
          boolean allowKeywordOption = (currentProperty instanceof KeywordProperty keywordProperty) && keywordProperty.matches(currentArgument.getKey());
15!
1681
          if (!allowDashedValue && !allowKeywordOption && currentArgument.isOption()) {
7✔
1682
            ValidationState state = new ValidationState(null);
5✔
1683
            state.addInvalidOption(currentArgument.getKey());
4✔
1684
            state.addErrorMessage("Invalid option \"" + currentArgument.getKey() + "\"");
5✔
1685
            return state;
2✔
1686
          }
1687
        }
1688
      }
1689
      if (currentProperty == null) {
2!
1690
        LOG.trace("No option or next value found");
×
1691
        ValidationState state = new ValidationState(null);
×
1692
        state.addErrorMessage("No matching property found");
×
1693
        return state;
×
1694
      }
1695
      LOG.trace("Next property candidate to match argument is {}", currentProperty);
4✔
1696
      if (currentProperty == property) {
3✔
1697
        if (!property.isMultiValued()) {
3✔
1698
          if (propertyIterator.hasNext()) {
3✔
1699
            property = propertyIterator.next();
5✔
1700
          } else {
1701
            property = null;
2✔
1702
          }
1703
        }
1704
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1705
          arguments.endOptions();
2✔
1706
        }
1707
      }
1708
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1709
      if (!matches) {
2✔
1710
        String invalidValue = currentProperty.getLastInvalidValue();
3✔
1711
        if (invalidValue != null) {
2!
1712
          ValidationState state = new ValidationState(null);
5✔
1713
          state.addInvalidArgument(invalidValue, currentProperty.getNameOrAlias());
5✔
1714
          state.addErrorMessage(
4✔
1715
              "Invalid CLI argument '" + invalidValue + "' for property '" + currentProperty.getNameOrAlias() + "' of commandlet '" + cmd.getName() + "'");
4✔
1716
          currentProperty.clearLastInvalidValue();
2✔
1717
          return state;
2✔
1718
        }
1719
        ValidationState state = new ValidationState(null);
×
1720
        state.addErrorMessage("No matching property found");
×
1721
        return state;
×
1722
      }
1723
      currentArgument = arguments.current();
3✔
1724
    }
1✔
1725
    return ValidationResultValid.get();
2✔
1726
  }
1727

1728
  @Override
1729
  public Path findBash() {
1730
    if (this.bash != null) {
3✔
1731
      return this.bash;
3✔
1732
    }
1733
    Path bashPath = findBashOnBashPath();
3✔
1734
    if (bashPath == null) {
2✔
1735
      bashPath = findBashInPath();
3✔
1736
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1737
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1738
        if (bashPath == null) {
2!
1739
          bashPath = findBashInWindowsRegistry();
3✔
1740
        }
1741
      }
1742
    }
1743
    if (bashPath == null) {
2✔
1744
      LOG.error("No bash executable could be found on your system.");
4✔
1745
    } else {
1746
      this.bash = bashPath;
3✔
1747
    }
1748
    return bashPath;
2✔
1749
  }
1750

1751
  private Path findBashOnBashPath() {
1752
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1753
    Path bash;
1754
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1755
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1756
    if (bashVariable != null) {
2✔
1757
      bash = Path.of(bashVariable);
5✔
1758
      if (Files.exists(bash)) {
5✔
1759
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1760
        return bash;
2✔
1761
      } else {
1762
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1763
        return null;
2✔
1764
      }
1765
    } else {
1766
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1767
      return null;
2✔
1768
    }
1769
  }
1770

1771
  /**
1772
   * @param path the path to check.
1773
   * @param toIgnore the String sequence which needs to be checked and ignored.
1774
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1775
   */
1776
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1777
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1778
    return !s.contains(toIgnore);
7!
1779
  }
1780

1781
  /**
1782
   * Tries to find the bash.exe within the PATH environment variable.
1783
   *
1784
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1785
   */
1786
  private Path findBashInPath() {
1787
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1788
    Path bash;
1789
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1790
    if (pathVariableName != null) {
2!
1791
      Path plainBash = Path.of(BASH);
5✔
1792
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1793
          "\\windows\\system32");
1794
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1795
      bash = bashPath.toAbsolutePath();
3✔
1796
      if (bashPath.equals(plainBash)) {
4✔
1797
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1798
        bash = null;
3✔
1799
      } else {
1800
        if (Files.exists(bashPath)) {
5!
1801
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1802
        } else {
1803
          bash = null;
×
1804
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1805
        }
1806
      }
1807
    } else {
1✔
1808
      bash = null;
×
1809
      // this should never happen...
1810
      LOG.error("PATH environment variable was not found");
×
1811
    }
1812
    return bash;
2✔
1813
  }
1814

1815
  /**
1816
   * Tries to find the bash.exe within the Windows registry.
1817
   *
1818
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1819
   */
1820
  protected Path findBashInWindowsRegistry() {
1821
    LOG.trace("Trying to find bash in Windows registry");
×
1822
    // If not found in the default location, try the registry query
1823
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1824
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1825
    for (String bashVariant : bashVariants) {
×
1826
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1827
      for (String registryKey : registryKeys) {
×
1828
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1829
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1830
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1831

1832
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1833
        if (path != null) {
×
1834
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1835
          if (Files.exists(bashPath)) {
×
1836
            LOG.debug("Found bash at: {}", bashPath);
×
1837
            return bashPath;
×
1838
          } else {
1839
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1840
            return null;
×
1841
          }
1842
        } else {
1843
          LOG.info("No bash executable could be found in the Windows registry.");
×
1844
        }
1845
      }
1846
    }
1847
    // no bash found
1848
    return null;
×
1849
  }
1850

1851
  private Path findBashOnWindowsDefaultGitPath() {
1852
    // Check if Git Bash exists in the default location
1853
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1854
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1855
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1856
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1857
      return defaultPath;
×
1858
    }
1859
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1860
    return null;
2✔
1861
  }
1862

1863
  @Override
1864
  public WindowsPathSyntax getPathSyntax() {
1865

1866
    return this.pathSyntax;
3✔
1867
  }
1868

1869
  /**
1870
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1871
   */
1872
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1873

1874
    this.pathSyntax = pathSyntax;
3✔
1875
  }
1✔
1876

1877
  /**
1878
   * @return the {@link IdeStartContextImpl}.
1879
   */
1880
  public IdeStartContextImpl getStartContext() {
1881

1882
    return startContext;
3✔
1883
  }
1884

1885
  /**
1886
   * @return the {@link WindowsHelper}.
1887
   */
1888
  public final WindowsHelper getWindowsHelper() {
1889

1890
    if (this.windowsHelper == null) {
3✔
1891
      this.windowsHelper = createWindowsHelper();
4✔
1892
    }
1893
    return this.windowsHelper;
3✔
1894
  }
1895

1896
  /**
1897
   * @return the new {@link WindowsHelper} instance.
1898
   */
1899
  protected WindowsHelper createWindowsHelper() {
1900

1901
    return new WindowsHelperImpl(this);
×
1902
  }
1903

1904
  /**
1905
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1906
   */
1907
  public void reload() {
1908

1909
    this.variables = null;
3✔
1910
    this.customToolRepository = null;
3✔
1911
  }
1✔
1912

1913
  @Override
1914
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1915

1916
    assert (Files.isDirectory(installationPath));
6!
1917
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1918
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1919
  }
1✔
1920

1921
  /*
1922
   * @param home the IDE_HOME directory.
1923
   * @param workspace the name of the active workspace folder.
1924
   */
1925
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1926

1927
  }
1928

1929
  /**
1930
   * Returns the default git path on Windows. Required to be overwritten in tests.
1931
   *
1932
   * @return default path to git on Windows.
1933
   */
1934
  public String getDefaultWindowsGitPath() {
1935
    return DEFAULT_WINDOWS_GIT_PATH;
×
1936
  }
1937

1938
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc