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

devonfw / IDEasy / 28893927583

07 Jul 2026 07:46PM UTC coverage: 72.095% (-0.03%) from 72.123%
28893927583

Pull #2128

github

web-flow
Merge 947ca869f into 92aaa9a0e
Pull Request #2128: #2123: configure Checkstyle suppression for legitimate System.out usages

4867 of 7456 branches covered (65.28%)

Branch coverage included in aggregate %.

12554 of 16708 relevant lines covered (75.14%)

3.18 hits per line

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

67.95
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.completion.CompletionCandidateCollectorDefault;
44
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
45
import com.devonfw.tools.ide.environment.EnvironmentVariables;
46
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
47
import com.devonfw.tools.ide.environment.IdeSystem;
48
import com.devonfw.tools.ide.environment.IdeSystemImpl;
49
import com.devonfw.tools.ide.git.GitContext;
50
import com.devonfw.tools.ide.git.GitContextImpl;
51
import com.devonfw.tools.ide.git.GitUrl;
52
import com.devonfw.tools.ide.io.FileAccess;
53
import com.devonfw.tools.ide.io.FileAccessImpl;
54
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
55
import com.devonfw.tools.ide.log.IdeLogLevel;
56
import com.devonfw.tools.ide.log.IdeLogListener;
57
import com.devonfw.tools.ide.log.JulConsoleHandler;
58
import com.devonfw.tools.ide.merge.DirectoryMerger;
59
import com.devonfw.tools.ide.migration.IdeMigrator;
60
import com.devonfw.tools.ide.network.NetworkStatus;
61
import com.devonfw.tools.ide.network.NetworkStatusImpl;
62
import com.devonfw.tools.ide.os.SystemInfo;
63
import com.devonfw.tools.ide.os.SystemInfoImpl;
64
import com.devonfw.tools.ide.os.WindowsHelper;
65
import com.devonfw.tools.ide.os.WindowsHelperImpl;
66
import com.devonfw.tools.ide.os.WindowsPathSyntax;
67
import com.devonfw.tools.ide.process.ProcessContext;
68
import com.devonfw.tools.ide.process.ProcessContextImpl;
69
import com.devonfw.tools.ide.process.ProcessResult;
70
import com.devonfw.tools.ide.property.KeywordProperty;
71
import com.devonfw.tools.ide.property.Property;
72
import com.devonfw.tools.ide.step.Step;
73
import com.devonfw.tools.ide.step.StepImpl;
74
import com.devonfw.tools.ide.tool.custom.CustomToolRepository;
75
import com.devonfw.tools.ide.tool.custom.CustomToolRepositoryImpl;
76
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
77
import com.devonfw.tools.ide.tool.npm.NpmRepository;
78
import com.devonfw.tools.ide.tool.pip.PipRepository;
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 final 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 DirectoryMerger workspaceMerger;
163

164
  protected UrlMetadata urlMetadata;
165

166
  protected Path defaultExecutionDirectory;
167

168
  private StepImpl currentStep;
169

170
  private NetworkStatus networkStatus;
171

172
  protected IdeSystem system;
173

174
  private WindowsHelper windowsHelper;
175

176
  private final Map<String, String> privacyMap;
177

178
  private Path bash;
179

180
  private boolean julConfigured;
181

182
  private Path logfile;
183

184
  private CliSuggester cliSuggester;
185

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

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

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

230
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
231

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

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

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

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

278
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
279
  }
280

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

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

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

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

309
  private Path findIdeRoot(Path ideHomePath) {
310

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

328
  /**
329
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
330
   */
331
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
332

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

365
  @Override
366
  public void setCwd(Path userDir, String workspace, Path ideHome) {
367

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

399
  /**
400
   * 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}
401
   * instead. Tests still use {@code ~/Downloads/ide} so existing fixtures keep working.
402
   */
403
  private Path computeDownloadPath(Path home) {
404

405
    if (!isTest() && this.systemInfo.isMac()) {
3!
406
      return home.resolve("Library/Caches/IDEasy/downloads");
×
407
    }
408
    return home.resolve("Downloads/ide");
4✔
409
  }
410

411
  private String getMessageIdeHomeFound() {
412

413
    String wks = this.workspaceName;
3✔
414
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
415
      wks = "*".repeat(wks.length());
×
416
    }
417
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
418
  }
419

420
  private String getMessageNotInsideIdeProject() {
421

422
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
423
  }
424

425
  private String getMessageIdeRootNotFound() {
426

427
    String root = getSystem().getEnv("IDE_ROOT");
5✔
428
    if (root == null) {
2!
429
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
430
    } else {
431
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
432
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
433
    }
434
  }
435

436
  /**
437
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
438
   */
439
  public boolean isTest() {
440

441
    return false;
×
442
  }
443

444
  protected SystemPath computeSystemPath() {
445

446
    return new SystemPath(this);
×
447
  }
448

449
  /**
450
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
451
   *
452
   * @param dir the directory to check.
453
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
454
   */
455
  protected boolean isIdeHome(Path dir) {
456

457
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
458
      return false;
2✔
459
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
460
      return false;
×
461
    }
462
    return true;
2✔
463
  }
464

465
  private EnvironmentVariables createVariables() {
466

467
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
468
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
469
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
470
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
471
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
472
    return conf.resolved();
3✔
473
  }
474

475
  protected AbstractEnvironmentVariables createSystemVariables() {
476

477
    return EnvironmentVariables.ofSystem(this);
3✔
478
  }
479

480
  @Override
481
  public SystemInfo getSystemInfo() {
482

483
    return this.systemInfo;
3✔
484
  }
485

486
  @Override
487
  public FileAccess getFileAccess() {
488

489
    return this.fileAccess;
3✔
490
  }
491

492
  @Override
493
  public CommandletManager getCommandletManager() {
494

495
    return this.commandletManager;
3✔
496
  }
497

498
  @Override
499
  public ToolRepository getDefaultToolRepository() {
500

501
    return this.defaultToolRepository;
3✔
502
  }
503

504
  @Override
505
  public MvnRepository getMvnRepository() {
506
    if (this.mvnRepository == null) {
3✔
507
      this.mvnRepository = createMvnRepository();
4✔
508
    }
509
    return this.mvnRepository;
3✔
510
  }
511

512
  @Override
513
  public NpmRepository getNpmRepository() {
514
    if (this.npmRepository == null) {
3✔
515
      this.npmRepository = createNpmRepository();
4✔
516
    }
517
    return this.npmRepository;
3✔
518
  }
519

520
  @Override
521
  public PipRepository getPipRepository() {
522
    if (this.pipRepository == null) {
3✔
523
      this.pipRepository = createPipRepository();
4✔
524
    }
525
    return this.pipRepository;
3✔
526
  }
527

528
  @Override
529
  public UvRepository getUvRepository() {
530
    if (this.uvRepository == null) {
3✔
531
      this.uvRepository = createUvRepository();
4✔
532
    }
533
    return this.uvRepository;
3✔
534
  }
535

536
  @Override
537
  public CustomToolRepository getCustomToolRepository() {
538

539
    if (this.customToolRepository == null) {
3✔
540
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
541
    }
542
    return this.customToolRepository;
3✔
543
  }
544

545
  @Override
546
  public Path getIdeHome() {
547

548
    return this.ideHome;
3✔
549
  }
550

551
  @Override
552
  public String getProjectName() {
553

554
    if (this.ideHome != null) {
3!
555
      return this.ideHome.getFileName().toString();
5✔
556
    }
557
    return "";
×
558
  }
559

560
  @Override
561
  public VersionIdentifier getProjectVersion() {
562

563
    if (this.ideHome != null) {
3!
564
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
565
      if (Files.exists(versionFile)) {
5✔
566
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
567
        return VersionIdentifier.of(version);
3✔
568
      }
569
    }
570
    return IdeMigrator.START_VERSION;
2✔
571
  }
572

573
  @Override
574
  public void setProjectVersion(VersionIdentifier version) {
575

576
    if (this.ideHome == null) {
3!
577
      throw new IllegalStateException("IDE_HOME not available!");
×
578
    }
579
    Objects.requireNonNull(version);
3✔
580
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
581
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
582
  }
1✔
583

584
  @Override
585
  public Path getIdeRoot() {
586

587
    return this.ideRoot;
3✔
588
  }
589

590
  @Override
591
  public Path getIdePath() {
592

593
    Path myIdeRoot = getIdeRoot();
3✔
594
    if (myIdeRoot == null) {
2✔
595
      return null;
2✔
596
    }
597
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
598
  }
599

600
  @Override
601
  public Path getCwd() {
602

603
    return this.cwd;
3✔
604
  }
605

606
  @Override
607
  public Path getTempPath() {
608

609
    Path idePath = getIdePath();
3✔
610
    if (idePath == null) {
2!
611
      return null;
×
612
    }
613
    return idePath.resolve("tmp");
4✔
614
  }
615

616
  @Override
617
  public Path getTempDownloadPath() {
618

619
    Path tmp = getTempPath();
3✔
620
    if (tmp == null) {
2!
621
      return null;
×
622
    }
623
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
624
  }
625

626
  @Override
627
  public Path getUserHome() {
628

629
    return this.userHome;
3✔
630
  }
631

632
  /**
633
   * This method should only be used for tests to mock user home.
634
   *
635
   * @param userHome the new value of {@link #getUserHome()}.
636
   */
637
  protected void setUserHome(Path userHome) {
638

639
    this.userHome = userHome;
3✔
640
    this.userHomeIde = userHome.resolve(FOLDER_DOT_IDE);
5✔
641
    this.downloadPath = computeDownloadPath(userHome);
5✔
642
    this.variables = null;
3✔
643
    resetPrivacyMap();
2✔
644
  }
1✔
645

646
  @Override
647
  public Path getUserHomeIde() {
648

649
    return this.userHomeIde;
3✔
650
  }
651

652
  @Override
653
  public Path getSettingsPath() {
654

655
    return this.settingsPath;
3✔
656
  }
657

658
  @Override
659
  public Path getSettingsGitRepository() {
660

661
    Path settingsPath = getSettingsPath();
3✔
662
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
663
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsCodeRepository()) {
12!
664
      LOG.error("Settings repository exists but is not a git repository.");
3✔
665
      return null;
2✔
666
    }
667
    return settingsPath;
2✔
668
  }
669

670
  @Override
671
  public boolean isSettingsCodeRepository() {
672

673
    Path settingsPath = getSettingsPath();
3✔
674
    if (settingsPath != null) {
2!
675
      boolean settingsIsLink = Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
676
      if (settingsIsLink) {
2!
677
        Path realPath = getFileAccess().toRealPath(this.settingsPath);
×
678
        if (realPath != null) {
×
679
          return getGitContext().isGitRepo(realPath.getParent());
×
680
        }
681
        return true;
×
682
      }
683
    }
684
    return false;
2✔
685
  }
686

687
  @Override
688
  public Path getSettingsCommitIdPath() {
689

690
    return this.settingsCommitIdPath;
3✔
691
  }
692

693
  @Override
694
  public Path getConfPath() {
695

696
    return this.confPath;
3✔
697
  }
698

699
  @Override
700
  public Path getSoftwarePath() {
701

702
    if (this.ideHome == null) {
3✔
703
      return null;
2✔
704
    }
705
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
706
  }
707

708
  @Override
709
  public Path getSoftwareExtraPath() {
710

711
    Path softwarePath = getSoftwarePath();
3✔
712
    if (softwarePath == null) {
2✔
713
      return null;
2✔
714
    }
715
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
716
  }
717

718
  @Override
719
  public Path getSoftwareRepositoryPath() {
720

721
    Path idePath = getIdePath();
3✔
722
    if (idePath == null) {
2!
723
      return null;
×
724
    }
725
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
726
  }
727

728
  @Override
729
  public Path getPluginsPath() {
730

731
    return this.pluginsPath;
3✔
732
  }
733

734
  @Override
735
  public String getWorkspaceName() {
736

737
    return this.workspaceName;
3✔
738
  }
739

740
  @Override
741
  public Path getWorkspacesBasePath() {
742

743
    return this.workspacesBasePath;
3✔
744
  }
745

746
  @Override
747
  public Path getWorkspacePath() {
748

749
    return this.workspacePath;
3✔
750
  }
751

752
  @Override
753
  public Path getWorkspacePath(String workspace) {
754

755
    if (this.workspacesBasePath == null) {
3!
756
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
757
    }
758
    return this.workspacesBasePath.resolve(workspace);
5✔
759
  }
760

761
  @Override
762
  public Path getDownloadPath() {
763

764
    return this.downloadPath;
3✔
765
  }
766

767
  @Override
768
  public Path getUrlsPath() {
769

770
    Path idePath = getIdePath();
3✔
771
    if (idePath == null) {
2!
772
      return null;
×
773
    }
774
    return idePath.resolve(FOLDER_URLS);
4✔
775
  }
776

777
  @Override
778
  public Path getToolRepositoryPath() {
779

780
    Path idePath = getIdePath();
3✔
781
    if (idePath == null) {
2!
782
      return null;
×
783
    }
784
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
785
  }
786

787
  @Override
788
  public SystemPath getPath() {
789

790
    return this.path;
3✔
791
  }
792

793
  @Override
794
  public EnvironmentVariables getVariables() {
795

796
    if (this.variables == null) {
3✔
797
      this.variables = createVariables();
4✔
798
    }
799
    return this.variables;
3✔
800
  }
801

802
  @Override
803
  public UrlMetadata getUrls() {
804

805
    if (this.urlMetadata == null) {
3✔
806
      if (!isTest()) {
3!
807
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
808
      }
809
      this.urlMetadata = new UrlMetadata(this);
6✔
810
    }
811
    return this.urlMetadata;
3✔
812
  }
813

814
  @Override
815
  public boolean isQuietMode() {
816

817
    return this.startContext.isQuietMode();
4✔
818
  }
819

820
  @Override
821
  public boolean isBatchMode() {
822

823
    return this.startContext.isBatchMode();
4✔
824
  }
825

826
  @Override
827
  public boolean isForceMode() {
828

829
    return this.startContext.isForceMode();
4✔
830
  }
831

832
  @Override
833
  public boolean isForcePull() {
834

835
    return this.startContext.isForcePull();
4✔
836
  }
837

838
  @Override
839
  public boolean isForcePlugins() {
840

841
    return this.startContext.isForcePlugins();
4✔
842
  }
843

844
  @Override
845
  public boolean isForceRepositories() {
846

847
    return this.startContext.isForceRepositories();
4✔
848
  }
849

850
  @Override
851
  public boolean isOfflineMode() {
852

853
    return this.startContext.isOfflineMode();
4✔
854
  }
855

856
  @Override
857
  public boolean isPrivacyMode() {
858
    return this.startContext.isPrivacyMode();
4✔
859
  }
860

861
  @Override
862
  public boolean isSkipUpdatesMode() {
863

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

867
  @Override
868
  public boolean isNoColorsMode() {
869

870
    return this.startContext.isNoColorsMode();
×
871
  }
872

873
  @Override
874
  public NetworkStatus getNetworkStatus() {
875

876
    if (this.networkStatus == null) {
×
877
      this.networkStatus = new NetworkStatusImpl(this);
×
878
    }
879
    return this.networkStatus;
×
880
  }
881

882
  @Override
883
  public Locale getLocale() {
884

885
    Locale locale = this.startContext.getLocale();
4✔
886
    if (locale == null) {
2✔
887
      locale = Locale.getDefault();
2✔
888
    }
889
    return locale;
2✔
890
  }
891

892
  @Override
893
  public DirectoryMerger getWorkspaceMerger() {
894

895
    if (this.workspaceMerger == null) {
3✔
896
      this.workspaceMerger = new DirectoryMerger(this);
6✔
897
    }
898
    return this.workspaceMerger;
3✔
899
  }
900

901
  /**
902
   * @return the default execution directory in which a command process is executed.
903
   */
904
  @Override
905
  public Path getDefaultExecutionDirectory() {
906

907
    return this.defaultExecutionDirectory;
×
908
  }
909

910
  /**
911
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
912
   */
913
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
914

915
    if (defaultExecutionDirectory != null) {
×
916
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
917
    }
918
  }
×
919

920
  @Override
921
  public GitContext getGitContext() {
922

923
    return new GitContextImpl(this);
×
924
  }
925

926
  @Override
927
  public ProcessContext newProcess() {
928

929
    ProcessContext processContext = createProcessContext();
3✔
930
    if (this.defaultExecutionDirectory != null) {
3!
931
      processContext.directory(this.defaultExecutionDirectory);
×
932
    }
933
    return processContext;
2✔
934
  }
935

936
  @Override
937
  public IdeSystem getSystem() {
938

939
    if (this.system == null) {
×
940
      this.system = new IdeSystemImpl();
×
941
    }
942
    return this.system;
×
943
  }
944

945
  /**
946
   * @return a new instance of {@link ProcessContext}.
947
   * @see #newProcess()
948
   */
949
  protected ProcessContext createProcessContext() {
950

951
    return new ProcessContextImpl(this);
×
952
  }
953

954
  @Override
955
  public IdeLogLevel getLogLevelConsole() {
956

957
    return this.startContext.getLogLevelConsole();
4✔
958
  }
959

960
  @Override
961
  public IdeLogLevel getLogLevelLogger() {
962

963
    return this.startContext.getLogLevelLogger();
×
964
  }
965

966
  @Override
967
  public IdeLogListener getLogListener() {
968

969
    return this.startContext.getLogListener();
×
970
  }
971

972
  @Override
973
  public void logIdeHomeAndRootStatus() {
974
    if (this.ideRoot != null) {
3✔
975
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
11✔
976
    }
977
    if (this.ideHome == null) {
3✔
978
      LOG.warn(getMessageNotInsideIdeProject());
5✔
979
    } else {
980
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
981
    }
982
  }
1✔
983

984
  @Override
985
  public String formatArgument(Object argument) {
986

987
    if (argument == null) {
2✔
988
      return null;
2✔
989
    }
990
    String result = argument.toString();
3✔
991
    if (isPrivacyMode()) {
3✔
992
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
993
        initializePrivacyMap(this.userHome, "~");
×
994
        String projectName = getProjectName();
×
995
        if (!projectName.isEmpty()) {
×
996
          this.privacyMap.put(projectName, "project");
×
997
        }
998
      }
999
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
1000
        result = result.replace(entry.getKey(), entry.getValue());
×
1001
      }
×
1002
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
1003
    }
1004
    return result;
2✔
1005
  }
1006

1007
  /**
1008
   * @param path the sensitive {@link Path} to
1009
   * @param replacement the replacement to mask the {@link Path} in log output.
1010
   */
1011
  protected void initializePrivacyMap(Path path, String replacement) {
1012

1013
    if (path == null) {
×
1014
      return;
×
1015
    }
1016
    if (this.systemInfo.isWindows()) {
×
1017
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
1018
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
1019
    } else {
1020
      this.privacyMap.put(path.toString(), replacement);
×
1021
    }
1022
  }
×
1023

1024
  /**
1025
   * Resets the privacy map in case fundamental values have changed.
1026
   */
1027
  private void resetPrivacyMap() {
1028

1029
    this.privacyMap.clear();
3✔
1030
  }
1✔
1031

1032

1033
  @Override
1034
  public String askForInput(String message, String defaultValue) {
1035

1036
    while (true) {
1037
      if (!message.isBlank()) {
3!
1038
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
1039
      }
1040
      if (isBatchMode()) {
3!
1041
        if (isForceMode()) {
×
1042
          return defaultValue;
×
1043
        } else {
1044
          throw new CliAbortException();
×
1045
        }
1046
      }
1047
      String input = readLine().trim();
4✔
1048
      if (!input.isEmpty()) {
3!
1049
        return input;
2✔
1050
      } else {
1051
        if (defaultValue != null) {
×
1052
          return defaultValue;
×
1053
        }
1054
      }
1055
    }
×
1056
  }
1057

1058
  @Override
1059
  public <O> O question(O[] options, String question, Object... args) {
1060

1061
    assert (options.length > 0);
4!
1062
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1063
    LOG.warn(question, args);
4✔
1064
    return displayOptionsAndGetAnswer(options);
4✔
1065
  }
1066

1067
  private <O> O displayOptionsAndGetAnswer(O[] options) {
1068
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
1069
    int i = 0;
2✔
1070
    for (O option : options) {
16✔
1071
      i++;
1✔
1072
      String title = "" + option;
4✔
1073
      String key = computeOptionKey(title);
3✔
1074
      addMapping(mapping, key, option);
4✔
1075
      String numericKey = Integer.toString(i);
3✔
1076
      if (numericKey.equals(key)) {
4!
1077
        LOG.trace("Options should not be numeric: {}", key);
×
1078
      } else {
1079
        addMapping(mapping, numericKey, option);
4✔
1080
      }
1081
      IdeLogLevel.INTERACTION.log(LOG, "Option {}: {}", numericKey, title);
14✔
1082
    }
1083
    if (options.length == 1) {
4✔
1084
      mapping.put("", options[0]);
7✔
1085
    }
1086
    O option = null;
2✔
1087
    if (isBatchMode()) {
3!
1088
      if (isForceMode()) {
×
1089
        option = options[0];
×
1090
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
×
1091
      }
1092
    } else {
1093
      while (option == null) {
2✔
1094
        String answer = readLine();
3✔
1095
        option = mapping.get(answer);
4✔
1096
        if (option == null) {
2!
1097
          LOG.warn("Invalid answer: '{}' - please try again.", answer);
×
1098
        }
1099
      }
1✔
1100
    }
1101
    return option;
2✔
1102
  }
1103

1104
  private static String computeOptionKey(String option) {
1105
    String key = option;
2✔
1106
    int index = -1;
2✔
1107
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1108
      int currentIndex = key.indexOf(c);
4✔
1109
      if (currentIndex != -1) {
3✔
1110
        if ((index == -1) || (currentIndex < index)) {
3!
1111
          index = currentIndex;
2✔
1112
        }
1113
      }
1114
    }
1115
    if (index > 0) {
2✔
1116
      key = key.substring(0, index).trim();
6✔
1117
    }
1118
    return key;
2✔
1119
  }
1120

1121
  /**
1122
   * @return the input from the end-user (e.g. read from the console).
1123
   */
1124
  protected abstract String readLine();
1125

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

1128
    O duplicate = mapping.put(key, option);
5✔
1129
    if (duplicate != null) {
2!
1130
      throw new IllegalArgumentException("Duplicated option " + key);
×
1131
    }
1132
  }
1✔
1133

1134
  @Override
1135
  public Step getCurrentStep() {
1136

1137
    return this.currentStep;
×
1138
  }
1139

1140
  @Override
1141
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1142

1143
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1144
    return this.currentStep;
3✔
1145
  }
1146

1147
  /**
1148
   * Internal method to end the running {@link Step}.
1149
   *
1150
   * @param step the current {@link Step} to end.
1151
   */
1152
  public void endStep(StepImpl step) {
1153

1154
    if (step == this.currentStep) {
4!
1155
      this.currentStep = this.currentStep.getParent();
6✔
1156
    } else {
1157
      String currentStepName = "null";
×
1158
      if (this.currentStep != null) {
×
1159
        currentStepName = this.currentStep.getName();
×
1160
      }
1161
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1162
    }
1163
  }
1✔
1164

1165
  /**
1166
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1167
   *
1168
   * @param arguments the {@link CliArgument}.
1169
   * @return the return code of the execution.
1170
   */
1171
  public int run(CliArguments arguments) {
1172

1173
    CliArgument current = arguments.current();
3✔
1174
    if (current.isStart()) {
3✔
1175
      arguments.next();
3✔
1176
      current = arguments.current();
3✔
1177
    }
1178
    assert (this.currentStep == null);
4!
1179
    boolean supressStepSuccess = false;
2✔
1180
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1181
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1182
    Commandlet cmd = null;
2✔
1183
    ValidationResult result = null;
2✔
1184
    try {
1185
      while (commandletIterator.hasNext()) {
3✔
1186
        cmd = commandletIterator.next();
4✔
1187
        result = applyAndRun(arguments.copy(), cmd);
6✔
1188
        if (result.isValid()) {
3✔
1189
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1190
          step.success();
2✔
1191
          return ProcessResult.SUCCESS;
4✔
1192
        }
1193
      }
1194
      activateLogging(cmd);
3✔
1195
      String commandKey = current.getKey();
3✔
1196

1197
      if (commandKey == null || commandKey.isBlank()) {
5!
1198
        return 0;
×
1199
      }
1200
      Commandlet commandletByName = this.commandletManager.getCommandlet(commandKey);
5✔
1201
      // Missing commandlet
1202
      if (commandletByName == null) {
2✔
1203
        if (getCliSuggester().isMissingCommandletHandled(commandKey, step)) {
6!
1204
          return 1;
4✔
1205
        }
1206
        return 0;
×
1207
      }
1208
      // Missing project context
1209
      if (getCliSuggester().isMissingProjectContextHandled(commandletByName, step)) {
6✔
1210
        return 1;
4✔
1211
      }
1212
      // Only validate options/arguments if same commandlet and proper type
1213
      if (cmd != commandletByName || !(result instanceof ValidationState validationState)) {
10!
1214
        return 0;
×
1215
      }
1216
      // Invalid option
1217
      if (getCliSuggester().isInvalidOptionHandled(validationState, commandletByName, step)) {
7✔
1218
        return 1;
4✔
1219
      }
1220
      // Invalid argument
1221
      if (getCliSuggester().isInvalidArgumentHandled(validationState, commandletByName)) {
6!
1222
        return 1;
4✔
1223
      }
1224
      LOG.error(result.getErrorMessage());
×
1225
      step.error("Invalid arguments: {}", current.getArgs());
×
1226
      IdeLogLevel.INTERACTION.log(LOG, "For additional details run ide help {}", cmd == null ? "" : cmd.getName());
×
1227
      return 1;
×
1228
    } catch (Throwable t) {
1✔
1229
      activateLogging(cmd);
3✔
1230
      step.error(t, true);
4✔
1231
      if (this.logfile != null) {
3!
1232
        // logging just failed, so point the user to the logfile directly
1233
        System.err.println("Logfile can be found at " + this.logfile); // checkstyle:ignore SystemOut
×
1234
      }
1235
      throw t;
2✔
1236
    } finally {
1237
      step.close();
2✔
1238
      assert (this.currentStep == null);
4!
1239
      step.logSummary(supressStepSuccess);
3✔
1240
    }
1241
  }
1242

1243

1244
  /**
1245
   * @return the {@link CliSuggester} for CLI suggestions.
1246
   */
1247
  private CliSuggester getCliSuggester() {
1248
    if (this.cliSuggester == null) {
3✔
1249
      this.cliSuggester = new CliSuggester(this);
6✔
1250
    }
1251
    return this.cliSuggester;
3✔
1252
  }
1253

1254
  /**
1255
   * Ensure the logging system is initialized.
1256
   */
1257
  private void activateLogging(Commandlet cmd) {
1258

1259
    configureJavaUtilLogging(cmd);
3✔
1260
    this.startContext.activateLogging();
3✔
1261
  }
1✔
1262

1263
  /**
1264
   * Configures the logging system (JUL).
1265
   *
1266
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1267
   */
1268
  public void configureJavaUtilLogging(Commandlet cmd) {
1269

1270
    if (this.julConfigured) {
3✔
1271
      return;
1✔
1272
    }
1273
    boolean writeLogfile = isWriteLogfile(cmd);
4✔
1274
    this.startContext.setWriteLogfile(writeLogfile);
4✔
1275
    Properties properties = createJavaUtilLoggingProperties(writeLogfile, cmd);
5✔
1276
    try {
1277
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1278
      properties.store(out, null);
4✔
1279
      out.flush();
2✔
1280
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1281
      LogManager.getLogManager().readConfiguration(in);
3✔
1282
      this.julConfigured = true;
3✔
1283
      this.startContext.activateLogging();
3✔
1284
    } catch (IOException e) {
×
1285
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1286
    }
1✔
1287
  }
1✔
1288

1289
  protected boolean isWriteLogfile(Commandlet cmd) {
1290
    if ((cmd == null) || !cmd.isWriteLogFile()) {
×
1291
      return false;
×
1292
    }
1293
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1294
    return Boolean.TRUE.equals(writeLogfile);
×
1295
  }
1296

1297
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1298

1299
    Path idePath = getIdePath();
3✔
1300
    if (writeLogfile && (idePath == null)) {
2!
1301
      writeLogfile = false;
×
1302
      LOG.error("Cannot enable log-file since IDE_ROOT is undefined.");
×
1303
    }
1304
    Properties properties = new Properties();
4✔
1305
    // prevent 3rd party (e.g. java.lang.ProcessBuilder) logging into our console via JUL
1306
    // see JulLogLevel for the trick we did to workaround JUL flaws
1307
    properties.setProperty(".level", "SEVERE");
5✔
1308
    if (writeLogfile) {
2!
1309
      this.startContext.setLogLevelLogger(IdeLogLevel.TRACE);
×
1310
      String fileHandlerName = FileHandler.class.getName();
×
1311
      properties.setProperty("handlers", JulConsoleHandler.class.getName() + "," + fileHandlerName);
×
1312
      properties.setProperty(fileHandlerName + ".formatter", SimpleFormatter.class.getName());
×
1313
      properties.setProperty(fileHandlerName + ".encoding", "UTF-8");
×
1314
      this.logfile = createLogfilePath(idePath, cmd);
×
1315
      getFileAccess().mkdirs(this.logfile.getParent());
×
1316
      properties.setProperty(fileHandlerName + ".pattern", this.logfile.toString());
×
1317
    } else {
×
1318
      properties.setProperty("handlers", JulConsoleHandler.class.getName());
6✔
1319
    }
1320
    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✔
1321
    return properties;
2✔
1322
  }
1323

1324
  private Path createLogfilePath(Path idePath, Commandlet cmd) {
1325
    LocalDateTime now = LocalDateTime.now();
×
1326
    Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1327
    StringBuilder sb = new StringBuilder(32);
×
1328
    if (this.ideHome == null || ((cmd != null) && !cmd.isIdeHomeRequired())) {
×
1329
      sb.append("_ide-");
×
1330
    } else {
1331
      sb.append(this.ideHome.getFileName().toString());
×
1332
      sb.append('-');
×
1333
    }
1334
    sb.append("ide-");
×
1335
    if (cmd != null) {
×
1336
      sb.append(cmd.getName());
×
1337
      sb.append('-');
×
1338
    }
1339
    sb.append(DateTimeUtil.formatTime(now));
×
1340
    sb.append(".log");
×
1341
    return logsPath.resolve(sb.toString());
×
1342
  }
1343

1344
  @Override
1345
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1346

1347
    this.startContext.deactivateLogging(threshold);
4✔
1348
    lambda.run();
2✔
1349
    this.startContext.activateLogging();
3✔
1350
  }
1✔
1351

1352
  /**
1353
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1354
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1355
   *     {@link Commandlet} did not match and we have to try a different candidate).
1356
   */
1357
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1358

1359
    IdeLogLevel previousLogLevel = null;
2✔
1360
    cmd.reset();
2✔
1361
    ValidationResult result = apply(arguments, cmd);
5✔
1362
    if (result.isValid()) {
3✔
1363
      result = cmd.validate();
3✔
1364
    }
1365
    if (result.isValid()) {
3✔
1366
      LOG.debug("Running commandlet {}", cmd);
4✔
1367
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1368
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1369
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6✔
1370
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1371
      }
1372
      try {
1373
        if (cmd.isProcessableOutput()) {
3✔
1374
          if (!isLogLevelEnabled(IdeLogLevel.DEBUG)) {
4!
1375
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1376
            previousLogLevel = this.startContext.setLogLevelConsole(IdeLogLevel.PROCESSABLE);
×
1377
          }
1378
        } else {
1379
          if (cmd.isIdeHomeRequired()) {
3✔
1380
            LOG.debug(getMessageIdeHomeFound());
4✔
1381
          }
1382
          if (!(cmd instanceof UpgradeCommandlet)) {
3!
1383
            verifyIdeMinVersion(false);
3✔
1384
          }
1385
          Path settingsRepository = getSettingsGitRepository();
3✔
1386
          if (settingsRepository != null) {
2!
1387
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
8!
1388
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
4!
1389
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1390

1391
              // Inform the user that an update is available. The update message is suppressed if we are already running the update
1392
              String msg = determineSettingsUpdateMessage(cmd);
×
1393
              if (msg != null) {
×
1394
                IdeLogLevel.INTERACTION.log(LOG, msg);
×
1395
              }
1396
            }
1397
          }
1398
        }
1399
        boolean success = ensureLicenseAgreement(cmd);
4✔
1400
        if (!success) {
2!
1401
          return ValidationResultValid.get();
×
1402
        }
1403
        cmd.run();
2✔
1404
      } finally {
1405
        if (previousLogLevel != null) {
2!
1406
          this.startContext.setLogLevelConsole(previousLogLevel);
×
1407
        }
1408
      }
1✔
1409
    } else {
1410
      LOG.trace("Commandlet did not match");
3✔
1411
    }
1412
    return result;
2✔
1413
  }
1414

1415

1416
  /**
1417
   * 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
1418
   * 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
1419
   * appropriate {@code ide update} command, the message is suppressed to avoid confusion.
1420
   *
1421
   * @param cmd the {@link Commandlet}.
1422
   * @return {@code msg} to log to the console. {@code null} if the message is suppressed.
1423
   */
1424
  private String determineSettingsUpdateMessage(Commandlet cmd) {
1425
    boolean update = cmd instanceof UpdateCommandlet;
×
1426
    if (isSettingsCodeRepository()) {
×
1427
      if (update && (isForceMode() || isForcePull())) {
×
1428
        return null;
×
1429
      }
1430
      return "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1431
    } else {
1432
      if (update) {
×
1433
        return null;
×
1434
      }
1435
      return "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
×
1436
    }
1437
  }
1438

1439
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1440

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

1481
    sb.setLength(0);
×
1482
    LocalDateTime now = LocalDateTime.now();
×
1483
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1484
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1485
    try {
1486
      Files.writeString(licenseAgreement, sb);
×
1487
    } catch (Exception e) {
×
1488
      throw new RuntimeException("Failed to save license agreement!", e);
×
1489
    }
×
1490
    if (oldLogLevel != newLogLevel) {
×
1491
      this.startContext.setLogLevelConsole(oldLogLevel);
×
1492
    }
1493
    return true;
×
1494
  }
1495

1496
  @Override
1497
  public void verifyIdeMinVersion(boolean throwException) {
1498
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1499
    if (minVersion == null) {
2✔
1500
      return;
1✔
1501
    }
1502
    VersionIdentifier versionIdentifier = IdeVersion.getVersionIdentifier();
2✔
1503
    if (versionIdentifier.compareVersion(minVersion).isLess() && !IdeVersion.isUndefined()) {
7!
1504
      String warning = String.format("Your version of IDEasy is currently %s\n"
13✔
1505
          + "However, this is too old as your project requires at latest version %s", versionIdentifier, minVersion);
1506
      String interaction = "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
2✔
1507
          + "ide upgrade";
1508
      if (throwException) {
2✔
1509
        throw new CliException(warning + "\n" + interaction);
7✔
1510
      } else {
1511
        LOG.warn(warning);
3✔
1512
        IdeLogLevel.INTERACTION.log(LOG, interaction);
4✔
1513
      }
1514
    }
1515
  }
1✔
1516

1517
  /**
1518
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1519
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1520
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1521
   */
1522
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1523

1524
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1525
    if (arguments.current().isStart()) {
4✔
1526
      arguments.next();
3✔
1527
    }
1528
    if (includeContextOptions) {
2✔
1529
      ContextCommandlet cc = new ContextCommandlet();
4✔
1530
      for (Property<?> property : cc.getProperties()) {
11✔
1531
        assert (property.isOption());
4!
1532
        property.apply(arguments, this, cc, collector);
7✔
1533
      }
1✔
1534
    }
1535
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1536
    CliArgument current = arguments.current();
3✔
1537
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1538
      collector.add(current.get(), null, null, null);
7✔
1539
    }
1540
    arguments.next();
3✔
1541
    while (commandletIterator.hasNext()) {
3✔
1542
      Commandlet cmd = commandletIterator.next();
4✔
1543
      if (!arguments.current().isEnd()) {
4✔
1544
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1545
      }
1546
    }
1✔
1547
    return collector.getSortedCandidates();
3✔
1548
  }
1549

1550
  /**
1551
   * Gets the next value property and applies its implicit end-options behavior if required.
1552
   *
1553
   * @param valueIterator the iterator over the commandlet value properties
1554
   * @param arguments the CLI arguments whose option parsing state may be updated
1555
   * @return the next value property or {@code null} if no further value property exists
1556
   */
1557
  private Property<?> nextValueProperty(Iterator<Property<?>> valueIterator, CliArguments arguments) {
1558

1559
    if (!valueIterator.hasNext()) {
3✔
1560
      return null;
2✔
1561
    }
1562

1563
    Property<?> valueProperty = valueIterator.next();
4✔
1564
    if (valueProperty.isEndOptions()) {
3✔
1565
      // Tool argument properties should accept values starting with "-" so stop option parsing here
1566
      arguments.endOptions();
2✔
1567
    }
1568
    return valueProperty;
2✔
1569
  }
1570

1571
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1572

1573
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1574
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1575
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1576
    Property<?> currentValueProperty = nextValueProperty(valueIterator, arguments);
5✔
1577
    List<Property<?>> properties = cmd.getProperties();
3✔
1578
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1579
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1580
    for (Property<?> property : properties) {
10✔
1581
      if (property.isOption()) {
3✔
1582
        optionProperties.add(property);
4✔
1583
      }
1584
    }
1✔
1585
    CliArgument currentArgument = arguments.current();
3✔
1586
    while (!currentArgument.isEnd()) {
3✔
1587
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1588
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6✔
1589
        if (currentArgument.isCompletion()) {
3✔
1590
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1591
          while (optionIterator.hasNext()) {
3✔
1592
            Property<?> option = optionIterator.next();
4✔
1593
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1594
            if (success) {
2✔
1595
              optionIterator.remove();
2✔
1596
              arguments.next();
3✔
1597
            }
1598
          }
1✔
1599
        } else {
1✔
1600
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1601
          if (option != null) {
2✔
1602
            arguments.next();
3✔
1603
            boolean removed = optionProperties.remove(option);
4✔
1604
            if (!removed) {
2!
1605
              option = null;
×
1606
            }
1607
          }
1608
          if (option == null) {
2✔
1609
            LOG.trace("No such option was found.");
3✔
1610
            return;
1✔
1611
          }
1612
        }
1✔
1613
      } else {
1614
        if (currentValueProperty != null) {
2✔
1615
          boolean success = currentValueProperty.apply(arguments, this, cmd, collector);
7✔
1616
          if (!success) {
2✔
1617
            LOG.trace("Completion cannot match any further.");
3✔
1618
            return;
1✔
1619
          }
1620
          if (!currentValueProperty.isMultiValued()) {
3✔
1621
            currentValueProperty = nextValueProperty(valueIterator, arguments);
5✔
1622
          }
1623
        } else {
1✔
1624
          LOG.trace("No value left for completion.");
3✔
1625
          return;
1✔
1626
        }
1627
      }
1628
      currentArgument = arguments.current();
4✔
1629
    }
1630
  }
1✔
1631

1632
  /**
1633
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1634
   *     {@link CliArguments#copy() copy} as needed.
1635
   * @param cmd the potential {@link Commandlet} to match.
1636
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1637
   */
1638
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1639

1640
    LOG.trace("Trying to match arguments to commandlet {}", cmd.getName());
5✔
1641
    CliArgument currentArgument = arguments.current();
3✔
1642
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1643
    Property<?> property = null;
2✔
1644
    if (propertyIterator.hasNext()) {
3!
1645
      property = propertyIterator.next();
4✔
1646
    }
1647
    while (!currentArgument.isEnd()) {
3✔
1648
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1649
      Property<?> currentProperty = property;
2✔
1650
      if (!arguments.isEndOptions()) {
3✔
1651
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1652
        if (option != null) {
2✔
1653
          currentProperty = option;
3✔
1654
        } else {
1655
          boolean allowDashedValue = (property != null && property.isValue() && property.isMultiValued());
10!
1656
          boolean allowKeywordOption = (currentProperty instanceof KeywordProperty keywordProperty) && keywordProperty.matches(currentArgument.getKey());
15!
1657
          if (!allowDashedValue && !allowKeywordOption && currentArgument.isOption()) {
7!
1658
            ValidationState state = new ValidationState(null);
5✔
1659
            state.addInvalidOption(currentArgument.getKey());
4✔
1660
            state.addErrorMessage("Invalid option \"" + currentArgument.getKey() + "\"");
5✔
1661
            return state;
2✔
1662
          }
1663
        }
1664
      }
1665
      if (currentProperty == null) {
2!
1666
        LOG.trace("No option or next value found");
×
1667
        ValidationState state = new ValidationState(null);
×
1668
        state.addErrorMessage("No matching property found");
×
1669
        return state;
×
1670
      }
1671
      LOG.trace("Next property candidate to match argument is {}", currentProperty);
4✔
1672
      if (currentProperty == property) {
3✔
1673
        if (!property.isMultiValued()) {
3✔
1674
          if (propertyIterator.hasNext()) {
3✔
1675
            property = propertyIterator.next();
5✔
1676
          } else {
1677
            property = null;
2✔
1678
          }
1679
        }
1680
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1681
          arguments.endOptions();
2✔
1682
        }
1683
      }
1684
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1685
      if (!matches) {
2✔
1686
        String invalidValue = currentProperty.getLastInvalidValue();
3✔
1687
        if (invalidValue != null) {
2!
1688
          ValidationState state = new ValidationState(null);
5✔
1689
          state.addInvalidArgument(invalidValue, currentProperty.getNameOrAlias());
5✔
1690
          state.addErrorMessage(
4✔
1691
              "Invalid CLI argument '" + invalidValue + "' for property '" + currentProperty.getNameOrAlias() + "' of commandlet '" + cmd.getName() + "'");
4✔
1692
          currentProperty.clearLastInvalidValue();
2✔
1693
          return state;
2✔
1694
        }
1695
        ValidationState state = new ValidationState(null);
×
1696
        state.addErrorMessage("No matching property found");
×
1697
        return state;
×
1698
      }
1699
      currentArgument = arguments.current();
3✔
1700
    }
1✔
1701
    return ValidationResultValid.get();
2✔
1702
  }
1703

1704
  @Override
1705
  public Path findBash() {
1706
    if (this.bash != null) {
3✔
1707
      return this.bash;
3✔
1708
    }
1709
    Path bashPath = findBashOnBashPath();
3✔
1710
    if (bashPath == null) {
2✔
1711
      bashPath = findBashInPath();
3✔
1712
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1713
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1714
        if (bashPath == null) {
2!
1715
          bashPath = findBashInWindowsRegistry();
3✔
1716
        }
1717
      }
1718
    }
1719
    if (bashPath == null) {
2✔
1720
      LOG.error("No bash executable could be found on your system.");
4✔
1721
    } else {
1722
      this.bash = bashPath;
3✔
1723
    }
1724
    return bashPath;
2✔
1725
  }
1726

1727
  private Path findBashOnBashPath() {
1728
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1729
    Path bash;
1730
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1731
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1732
    if (bashVariable != null) {
2✔
1733
      bash = Path.of(bashVariable);
5✔
1734
      if (Files.exists(bash)) {
5✔
1735
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1736
        return bash;
2✔
1737
      } else {
1738
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1739
        return null;
2✔
1740
      }
1741
    } else {
1742
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1743
      return null;
2✔
1744
    }
1745
  }
1746

1747
  /**
1748
   * @param path the path to check.
1749
   * @param toIgnore the String sequence which needs to be checked and ignored.
1750
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1751
   */
1752
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1753
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1754
    return !s.contains(toIgnore);
7!
1755
  }
1756

1757
  /**
1758
   * Tries to find the bash.exe within the PATH environment variable.
1759
   *
1760
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1761
   */
1762
  private Path findBashInPath() {
1763
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1764
    Path bash;
1765
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1766
    if (pathVariableName != null) {
2!
1767
      Path plainBash = Path.of(BASH);
5✔
1768
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1769
          "\\windows\\system32");
1770
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1771
      bash = bashPath.toAbsolutePath();
3✔
1772
      if (bashPath.equals(plainBash)) {
4✔
1773
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1774
        bash = null;
3✔
1775
      } else {
1776
        if (Files.exists(bashPath)) {
5!
1777
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1778
        } else {
1779
          bash = null;
×
1780
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1781
        }
1782
      }
1783
    } else {
1✔
1784
      bash = null;
×
1785
      // this should never happen...
1786
      LOG.error("PATH environment variable was not found");
×
1787
    }
1788
    return bash;
2✔
1789
  }
1790

1791
  /**
1792
   * Tries to find the bash.exe within the Windows registry.
1793
   *
1794
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1795
   */
1796
  protected Path findBashInWindowsRegistry() {
1797
    LOG.trace("Trying to find bash in Windows registry");
×
1798
    // If not found in the default location, try the registry query
1799
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1800
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1801
    for (String bashVariant : bashVariants) {
×
1802
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1803
      for (String registryKey : registryKeys) {
×
1804
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1805
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1806
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1807

1808
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1809
        if (path != null) {
×
1810
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1811
          if (Files.exists(bashPath)) {
×
1812
            LOG.debug("Found bash at: {}", bashPath);
×
1813
            return bashPath;
×
1814
          } else {
1815
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1816
            return null;
×
1817
          }
1818
        } else {
1819
          LOG.info("No bash executable could be found in the Windows registry.");
×
1820
        }
1821
      }
1822
    }
1823
    // no bash found
1824
    return null;
×
1825
  }
1826

1827
  private Path findBashOnWindowsDefaultGitPath() {
1828
    // Check if Git Bash exists in the default location
1829
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1830
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1831
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1832
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1833
      return defaultPath;
×
1834
    }
1835
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1836
    return null;
2✔
1837
  }
1838

1839
  @Override
1840
  public WindowsPathSyntax getPathSyntax() {
1841

1842
    return this.pathSyntax;
3✔
1843
  }
1844

1845
  /**
1846
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1847
   */
1848
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1849

1850
    this.pathSyntax = pathSyntax;
3✔
1851
  }
1✔
1852

1853
  /**
1854
   * @return the {@link IdeStartContextImpl}.
1855
   */
1856
  public IdeStartContextImpl getStartContext() {
1857

1858
    return startContext;
3✔
1859
  }
1860

1861
  /**
1862
   * @return the {@link WindowsHelper}.
1863
   */
1864
  public final WindowsHelper getWindowsHelper() {
1865

1866
    if (this.windowsHelper == null) {
3✔
1867
      this.windowsHelper = createWindowsHelper();
4✔
1868
    }
1869
    return this.windowsHelper;
3✔
1870
  }
1871

1872
  /**
1873
   * @return the new {@link WindowsHelper} instance.
1874
   */
1875
  protected WindowsHelper createWindowsHelper() {
1876

1877
    return new WindowsHelperImpl(this);
×
1878
  }
1879

1880
  /**
1881
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1882
   */
1883
  public void reload() {
1884

1885
    this.variables = null;
3✔
1886
    this.customToolRepository = null;
3✔
1887
  }
1✔
1888

1889
  @Override
1890
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1891

1892
    assert (Files.isDirectory(installationPath));
6!
1893
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1894
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1895
  }
1✔
1896

1897
  /*
1898
   * @param home the IDE_HOME directory.
1899
   * @param workspace the name of the active workspace folder.
1900
   */
1901
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1902

1903
  }
1904

1905
  /**
1906
   * Returns the default git path on Windows. Required to be overwritten in tests.
1907
   *
1908
   * @return default path to git on Windows.
1909
   */
1910
  public String getDefaultWindowsGitPath() {
1911
    return DEFAULT_WINDOWS_GIT_PATH;
×
1912
  }
1913

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

© 2026 Coveralls, Inc