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

devonfw / IDEasy / 29998786930

23 Jul 2026 10:18AM UTC coverage: 72.545% (+0.05%) from 72.496%
29998786930

Pull #2204

github

web-flow
Merge b7d5ee787 into 5b6cc3011
Pull Request #2204: #2192-support-for-auto-completion-synonyms

4986 of 7598 branches covered (65.62%)

Branch coverage included in aggregate %.

12850 of 16988 relevant lines covered (75.64%)

3.2 hits per line

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

70.76
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.HashSet;
14
import java.util.Iterator;
15
import java.util.List;
16
import java.util.Locale;
17
import java.util.Map;
18
import java.util.Map.Entry;
19
import java.util.Objects;
20
import java.util.Properties;
21
import java.util.Set;
22
import java.util.function.Predicate;
23
import java.util.logging.FileHandler;
24
import java.util.logging.LogManager;
25
import java.util.logging.SimpleFormatter;
26

27
import org.slf4j.Logger;
28
import org.slf4j.LoggerFactory;
29

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

95
/**
96
 * Abstract base implementation of {@link IdeContext}.
97
 */
98
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
99

100
  static final Logger LOG = LoggerFactory.getLogger(AbstractIdeContext.class);
3✔
101

102
  /** The default shell bash (Bourne Again SHell). */
103
  public static final String BASH = "bash";
104

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

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

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

111
  private static final String OPTION_DETAILS_START = "([";
112

113
  private final IdeStartContextImpl startContext;
114

115
  private Path ideHome;
116

117
  private Path ideRoot;
118

119
  private Path confPath;
120

121
  protected Path settingsPath;
122

123
  private Path settingsCommitIdPath;
124

125
  protected Path pluginsPath;
126

127
  private Path workspacePath;
128

129
  private Path workspacesBasePath;
130

131
  private String workspaceName;
132

133
  private Path cwd;
134

135
  private Path downloadPath;
136

137
  private Path userHome;
138

139
  private Path userHomeIde;
140

141
  private SystemPath path;
142

143
  private WindowsPathSyntax pathSyntax;
144

145
  private final SystemInfo systemInfo;
146

147
  private EnvironmentVariables variables;
148

149
  private final FileAccess fileAccess;
150

151
  protected CommandletManager commandletManager;
152

153
  protected ToolRepository defaultToolRepository;
154

155
  private CustomToolRepository customToolRepository;
156

157
  private MvnRepository mvnRepository;
158

159
  private NpmRepository npmRepository;
160

161
  private PipRepository pipRepository;
162

163
  private UvRepository uvRepository;
164

165
  private PythonRepository pythonRepository;
166

167
  private DirectoryMerger workspaceMerger;
168

169
  protected UrlMetadata urlMetadata;
170

171
  protected Path defaultExecutionDirectory;
172

173
  private StepImpl currentStep;
174

175
  private NetworkStatus networkStatus;
176

177
  protected IdeSystem system;
178

179
  private WindowsHelper windowsHelper;
180

181
  private final Map<String, String> privacyMap;
182

183
  private Path bash;
184

185
  private boolean julConfigured;
186

187
  private Path logfile;
188

189
  private CliSuggester cliSuggester;
190

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

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

232
    // detection completed, initializing variables
233
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
234

235
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
236

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

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

257
    Path currentDir = workingDirectory;
2✔
258
    String name1 = "";
2✔
259
    String name2 = "";
2✔
260
    String workspace = WORKSPACE_MAIN;
2✔
261
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
262

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

283
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
284
  }
285

286
  /**
287
   * @return a new {@link MvnRepository}
288
   */
289
  protected MvnRepository createMvnRepository() {
290
    return new MvnRepository(this);
5✔
291
  }
292

293
  /**
294
   * @return a new {@link NpmRepository}
295
   */
296
  protected NpmRepository createNpmRepository() {
297
    return new NpmRepository(this);
×
298
  }
299

300
  /**
301
   * @return a new {@link PipRepository}
302
   */
303
  protected PipRepository createPipRepository() {
304
    return new PipRepository(this);
×
305
  }
306

307
  /**
308
   * @return a new {@link UvRepository}
309
   */
310
  protected UvRepository createUvRepository() {
311
    return new UvRepository(this);
×
312
  }
313

314
  /**
315
   * @return a new {@link PythonRepository}
316
   */
317
  protected PythonRepository createPythonRepository() {
318
    return new PythonRepository(this);
5✔
319
  }
320

321
  private Path findIdeRoot(Path ideHomePath) {
322

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

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

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

377
  @Override
378
  public void setCwd(Path userDir, String workspace, Path ideHome) {
379

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

411
  /**
412
   * 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}
413
   * instead. Tests still use {@code ~/Downloads/ide} so existing fixtures keep working.
414
   */
415
  private Path computeDownloadPath(Path home) {
416

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

423
  private String getMessageIdeHomeFound() {
424

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

432
  private String getMessageNotInsideIdeProject() {
433

434
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
435
  }
436

437
  private String getMessageIdeRootNotFound() {
438

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

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

453
    return false;
×
454
  }
455

456
  protected SystemPath computeSystemPath() {
457

458
    return new SystemPath(this);
×
459
  }
460

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

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

477
  private EnvironmentVariables createVariables() {
478

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

487
  protected AbstractEnvironmentVariables createSystemVariables() {
488

489
    return EnvironmentVariables.ofSystem(this);
3✔
490
  }
491

492
  @Override
493
  public SystemInfo getSystemInfo() {
494

495
    return this.systemInfo;
3✔
496
  }
497

498
  @Override
499
  public FileAccess getFileAccess() {
500

501
    return this.fileAccess;
3✔
502
  }
503

504
  @Override
505
  public CommandletManager getCommandletManager() {
506

507
    return this.commandletManager;
3✔
508
  }
509

510
  @Override
511
  public ToolRepository getDefaultToolRepository() {
512

513
    return this.defaultToolRepository;
3✔
514
  }
515

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

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

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

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

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

556
  @Override
557
  public CustomToolRepository getCustomToolRepository() {
558

559
    if (this.customToolRepository == null) {
3✔
560
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
561
    }
562
    return this.customToolRepository;
3✔
563
  }
564

565
  @Override
566
  public Path getIdeHome() {
567

568
    return this.ideHome;
3✔
569
  }
570

571
  @Override
572
  public String getProjectName() {
573

574
    if (this.ideHome != null) {
3!
575
      return this.ideHome.getFileName().toString();
5✔
576
    }
577
    return "";
×
578
  }
579

580
  @Override
581
  public VersionIdentifier getProjectVersion() {
582

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

593
  @Override
594
  public void setProjectVersion(VersionIdentifier version) {
595

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

604
  @Override
605
  public Path getIdeRoot() {
606

607
    return this.ideRoot;
3✔
608
  }
609

610
  @Override
611
  public void setIdeRoot(Path ideRoot) {
612

613
    this.ideRoot = ideRoot;
3✔
614
  }
1✔
615

616
  @Override
617
  public Path getIdePath() {
618

619
    Path myIdeRoot = getIdeRoot();
3✔
620
    if (myIdeRoot == null) {
2✔
621
      return null;
2✔
622
    }
623
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
624
  }
625

626
  @Override
627
  public Path getCwd() {
628

629
    return this.cwd;
3✔
630
  }
631

632
  @Override
633
  public Path getTempPath() {
634

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

642
  @Override
643
  public Path getTempDownloadPath() {
644

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

652
  @Override
653
  public Path getUserHome() {
654

655
    return this.userHome;
3✔
656
  }
657

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

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

672
  @Override
673
  public Path getUserHomeIde() {
674

675
    return this.userHomeIde;
3✔
676
  }
677

678
  @Override
679
  public Path getSettingsPath() {
680

681
    return this.settingsPath;
3✔
682
  }
683

684
  @Override
685
  public Path getSettingsGitRepository() {
686

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

696
  @Override
697
  public boolean isSettingsCodeRepository() {
698

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

713
  @Override
714
  public Path getSettingsCommitIdPath() {
715

716
    return this.settingsCommitIdPath;
3✔
717
  }
718

719
  @Override
720
  public Path getConfPath() {
721

722
    return this.confPath;
3✔
723
  }
724

725
  @Override
726
  public Path getSoftwarePath() {
727

728
    if (this.ideHome == null) {
3✔
729
      return null;
2✔
730
    }
731
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
732
  }
733

734
  @Override
735
  public Path getSoftwareExtraPath() {
736

737
    Path softwarePath = getSoftwarePath();
3✔
738
    if (softwarePath == null) {
2✔
739
      return null;
2✔
740
    }
741
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
742
  }
743

744
  @Override
745
  public Path getSoftwareRepositoryPath() {
746

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

754
  @Override
755
  public Path getPluginsPath() {
756

757
    return this.pluginsPath;
3✔
758
  }
759

760
  @Override
761
  public String getWorkspaceName() {
762

763
    return this.workspaceName;
3✔
764
  }
765

766
  @Override
767
  public Path getWorkspacesBasePath() {
768

769
    return this.workspacesBasePath;
3✔
770
  }
771

772
  @Override
773
  public Path getWorkspacePath() {
774

775
    return this.workspacePath;
3✔
776
  }
777

778
  @Override
779
  public Path getWorkspacePath(String workspace) {
780

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

787
  @Override
788
  public Path getDownloadPath() {
789

790
    return this.downloadPath;
3✔
791
  }
792

793
  @Override
794
  public Path getUrlsPath() {
795

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

803
  @Override
804
  public Path getToolRepositoryPath() {
805

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

813
  @Override
814
  public SystemPath getPath() {
815

816
    return this.path;
3✔
817
  }
818

819
  @Override
820
  public EnvironmentVariables getVariables() {
821

822
    if (this.variables == null) {
3✔
823
      this.variables = createVariables();
4✔
824
    }
825
    return this.variables;
3✔
826
  }
827

828
  @Override
829
  public UrlMetadata getUrls() {
830

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

840
  @Override
841
  public boolean isQuietMode() {
842

843
    return this.startContext.isQuietMode();
4✔
844
  }
845

846
  @Override
847
  public boolean isBatchMode() {
848

849
    return this.startContext.isBatchMode();
4✔
850
  }
851

852
  @Override
853
  public boolean isForceMode() {
854

855
    return this.startContext.isForceMode();
4✔
856
  }
857

858
  @Override
859
  public boolean isForcePull() {
860

861
    return this.startContext.isForcePull();
4✔
862
  }
863

864
  @Override
865
  public boolean isForcePlugins() {
866

867
    return this.startContext.isForcePlugins();
4✔
868
  }
869

870
  @Override
871
  public boolean isForceRepositories() {
872

873
    return this.startContext.isForceRepositories();
4✔
874
  }
875

876
  @Override
877
  public boolean isOfflineMode() {
878

879
    return this.startContext.isOfflineMode();
4✔
880
  }
881

882
  @Override
883
  public boolean isPrivacyMode() {
884
    return this.startContext.isPrivacyMode();
4✔
885
  }
886

887
  @Override
888
  public boolean isSkipUpdatesMode() {
889

890
    return this.startContext.isSkipUpdatesMode();
4✔
891
  }
892

893
  @Override
894
  public boolean isNoColorsMode() {
895

896
    return this.startContext.isNoColorsMode();
×
897
  }
898

899
  @Override
900
  public NetworkStatus getNetworkStatus() {
901

902
    if (this.networkStatus == null) {
×
903
      this.networkStatus = new NetworkStatusImpl(this);
×
904
    }
905
    return this.networkStatus;
×
906
  }
907

908
  @Override
909
  public Locale getLocale() {
910

911
    Locale locale = this.startContext.getLocale();
4✔
912
    if (locale == null) {
2✔
913
      locale = Locale.getDefault();
2✔
914
    }
915
    return locale;
2✔
916
  }
917

918
  @Override
919
  public DirectoryMerger getWorkspaceMerger() {
920

921
    if (this.workspaceMerger == null) {
3✔
922
      this.workspaceMerger = new DirectoryMerger(this);
6✔
923
    }
924
    return this.workspaceMerger;
3✔
925
  }
926

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

933
    return this.defaultExecutionDirectory;
×
934
  }
935

936
  /**
937
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
938
   */
939
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
940

941
    if (defaultExecutionDirectory != null) {
×
942
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
943
    }
944
  }
×
945

946
  @Override
947
  public GitContext getGitContext() {
948

949
    return new GitContextImpl(this);
×
950
  }
951

952
  @Override
953
  public ProcessContext newProcess() {
954

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

962
  @Override
963
  public IdeSystem getSystem() {
964

965
    if (this.system == null) {
×
966
      this.system = new IdeSystemImpl();
×
967
    }
968
    return this.system;
×
969
  }
970

971
  /**
972
   * @return a new instance of {@link ProcessContext}.
973
   * @see #newProcess()
974
   */
975
  protected ProcessContext createProcessContext() {
976

977
    return new ProcessContextImpl(this);
×
978
  }
979

980
  @Override
981
  public IdeLogLevel getLogLevelConsole() {
982

983
    return this.startContext.getLogLevelConsole();
4✔
984
  }
985

986
  @Override
987
  public IdeLogLevel getLogLevelLogger() {
988

989
    return this.startContext.getLogLevelLogger();
×
990
  }
991

992
  @Override
993
  public IdeLogListener getLogListener() {
994

995
    return this.startContext.getLogListener();
×
996
  }
997

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

1010
  @Override
1011
  public String formatArgument(Object argument) {
1012

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

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

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

1050
  /**
1051
   * Resets the privacy map in case fundamental values have changed.
1052
   */
1053
  private void resetPrivacyMap() {
1054

1055
    this.privacyMap.clear();
3✔
1056
  }
1✔
1057

1058

1059
  @Override
1060
  public String askForInput(String message, String defaultValue) {
1061

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

1084
  @Override
1085
  public <O> O question(O[] options, String question, Object... args) {
1086

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

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

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

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

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

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

1160
  @Override
1161
  public Step getCurrentStep() {
1162

1163
    return this.currentStep;
×
1164
  }
1165

1166
  @Override
1167
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1168

1169
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1170
    return this.currentStep;
3✔
1171
  }
1172

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

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

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

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

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

1269

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

1280
  /**
1281
   * Ensure the logging system is initialized.
1282
   */
1283
  private void activateLogging(Commandlet cmd) {
1284

1285
    configureJavaUtilLogging(cmd);
3✔
1286
    this.startContext.activateLogging();
3✔
1287
  }
1✔
1288

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

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

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

1323
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1324

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

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

1370
  @Override
1371
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1372

1373
    this.startContext.deactivateLogging(threshold);
4✔
1374
    lambda.run();
2✔
1375
    this.startContext.activateLogging();
3✔
1376
  }
1✔
1377

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

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

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

1441

1442
  /**
1443
   * 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
1444
   * 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
1445
   * appropriate {@code ide update} command, the message is suppressed to avoid confusion.
1446
   *
1447
   * @param cmd the {@link Commandlet}.
1448
   * @return {@code msg} to log to the console. {@code null} if the message is suppressed.
1449
   */
1450
  private String determineSettingsUpdateMessage(Commandlet cmd) {
1451
    boolean update = cmd instanceof UpdateCommandlet;
3✔
1452
    if (isSettingsCodeRepository()) {
3!
1453
      if (update && (isForceMode() || isForcePull())) {
×
1454
        return null;
×
1455
      }
1456
      return "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1457
    } else {
1458
      if (update) {
2!
1459
        return null;
×
1460
      }
1461
      return "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
2✔
1462
    }
1463
  }
1464

1465
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1466

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

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

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

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

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

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

1585
    if (!valueIterator.hasNext()) {
3✔
1586
      return null;
2✔
1587
    }
1588

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

1597
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1598

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

1601
    // Collect all already-provided arguments (non-completion) to filter out synonyms
1602
    Set<String> alreadyProvided = new HashSet<>();
4✔
1603
    CliArguments argsCopy = arguments.copy();
3✔
1604
    while (!argsCopy.current().isEnd()) {
4✔
1605
      CliArgument arg = argsCopy.current();
3✔
1606
      if (!arg.isCompletion()) {
3✔
1607
        alreadyProvided.add(arg.get());
5✔
1608
      }
1609
      argsCopy.next();
3✔
1610
    }
1✔
1611
    collector.setAlreadyProvided(alreadyProvided);
3✔
1612

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

1671
  /**
1672
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1673
   *     {@link CliArguments#copy() copy} as needed.
1674
   * @param cmd the potential {@link Commandlet} to match.
1675
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1676
   */
1677
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1678

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

1744
  @Override
1745
  public Path findBash() {
1746
    if (this.bash != null) {
3✔
1747
      return this.bash;
3✔
1748
    }
1749
    Path bashPath = findBashOnBashPath();
3✔
1750
    if (bashPath == null) {
2✔
1751
      bashPath = findBashInPath();
3✔
1752
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1753
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1754
        if (bashPath == null) {
2!
1755
          bashPath = findBashInWindowsRegistry();
3✔
1756
        }
1757
      }
1758
    }
1759
    if (bashPath == null) {
2✔
1760
      LOG.error("No bash executable could be found on your system.");
4✔
1761
    } else {
1762
      this.bash = bashPath;
3✔
1763
    }
1764
    return bashPath;
2✔
1765
  }
1766

1767
  private Path findBashOnBashPath() {
1768
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1769
    Path bash;
1770
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1771
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1772
    if (bashVariable != null) {
2✔
1773
      bash = Path.of(bashVariable);
5✔
1774
      if (Files.exists(bash)) {
5✔
1775
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1776
        return bash;
2✔
1777
      } else {
1778
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1779
        return null;
2✔
1780
      }
1781
    } else {
1782
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1783
      return null;
2✔
1784
    }
1785
  }
1786

1787
  /**
1788
   * @param path the path to check.
1789
   * @param toIgnore the String sequence which needs to be checked and ignored.
1790
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1791
   */
1792
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1793
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1794
    return !s.contains(toIgnore);
7!
1795
  }
1796

1797
  /**
1798
   * Tries to find the bash.exe within the PATH environment variable.
1799
   *
1800
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1801
   */
1802
  private Path findBashInPath() {
1803
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1804
    Path bash;
1805
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1806
    if (pathVariableName != null) {
2!
1807
      Path plainBash = Path.of(BASH);
5✔
1808
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1809
          "\\windows\\system32");
1810
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1811
      bash = bashPath.toAbsolutePath();
3✔
1812
      if (bashPath.equals(plainBash)) {
4✔
1813
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1814
        bash = null;
3✔
1815
      } else {
1816
        if (Files.exists(bashPath)) {
5!
1817
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1818
        } else {
1819
          bash = null;
×
1820
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1821
        }
1822
      }
1823
    } else {
1✔
1824
      bash = null;
×
1825
      // this should never happen...
1826
      LOG.error("PATH environment variable was not found");
×
1827
    }
1828
    return bash;
2✔
1829
  }
1830

1831
  /**
1832
   * Tries to find the bash.exe within the Windows registry.
1833
   *
1834
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1835
   */
1836
  protected Path findBashInWindowsRegistry() {
1837
    LOG.trace("Trying to find bash in Windows registry");
×
1838
    // If not found in the default location, try the registry query
1839
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1840
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1841
    for (String bashVariant : bashVariants) {
×
1842
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1843
      for (String registryKey : registryKeys) {
×
1844
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1845
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1846
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1847

1848
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1849
        if (path != null) {
×
1850
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1851
          if (Files.exists(bashPath)) {
×
1852
            LOG.debug("Found bash at: {}", bashPath);
×
1853
            return bashPath;
×
1854
          } else {
1855
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1856
            return null;
×
1857
          }
1858
        } else {
1859
          LOG.info("No bash executable could be found in the Windows registry.");
×
1860
        }
1861
      }
1862
    }
1863
    // no bash found
1864
    return null;
×
1865
  }
1866

1867
  private Path findBashOnWindowsDefaultGitPath() {
1868
    // Check if Git Bash exists in the default location
1869
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1870
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1871
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1872
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1873
      return defaultPath;
×
1874
    }
1875
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1876
    return null;
2✔
1877
  }
1878

1879
  @Override
1880
  public WindowsPathSyntax getPathSyntax() {
1881

1882
    return this.pathSyntax;
3✔
1883
  }
1884

1885
  /**
1886
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1887
   */
1888
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1889

1890
    this.pathSyntax = pathSyntax;
3✔
1891
  }
1✔
1892

1893
  /**
1894
   * @return the {@link IdeStartContextImpl}.
1895
   */
1896
  public IdeStartContextImpl getStartContext() {
1897

1898
    return startContext;
3✔
1899
  }
1900

1901
  /**
1902
   * @return the {@link WindowsHelper}.
1903
   */
1904
  public final WindowsHelper getWindowsHelper() {
1905

1906
    if (this.windowsHelper == null) {
3✔
1907
      this.windowsHelper = createWindowsHelper();
4✔
1908
    }
1909
    return this.windowsHelper;
3✔
1910
  }
1911

1912
  /**
1913
   * @return the new {@link WindowsHelper} instance.
1914
   */
1915
  protected WindowsHelper createWindowsHelper() {
1916

1917
    return new WindowsHelperImpl(this);
×
1918
  }
1919

1920
  /**
1921
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1922
   */
1923
  public void reload() {
1924

1925
    this.variables = null;
3✔
1926
    this.customToolRepository = null;
3✔
1927
  }
1✔
1928

1929
  @Override
1930
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1931

1932
    assert (Files.isDirectory(installationPath));
6!
1933
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1934
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1935
  }
1✔
1936

1937
  /*
1938
   * @param home the IDE_HOME directory.
1939
   * @param workspace the name of the active workspace folder.
1940
   */
1941
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1942

1943
  }
1944

1945
  /**
1946
   * Returns the default git path on Windows. Required to be overwritten in tests.
1947
   *
1948
   * @return default path to git on Windows.
1949
   */
1950
  public String getDefaultWindowsGitPath() {
1951
    return DEFAULT_WINDOWS_GIT_PATH;
×
1952
  }
1953

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