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

devonfw / IDEasy / 29803149984

21 Jul 2026 05:08AM UTC coverage: 72.439% (-0.04%) from 72.476%
29803149984

Pull #2179

github

web-flow
Merge c6c1d1813 into 3221b7580
Pull Request #2179: Feature/2165 interactive template variables

4961 of 7574 branches covered (65.5%)

Branch coverage included in aggregate %.

12804 of 16950 relevant lines covered (75.54%)

3.2 hits per line

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

70.06
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 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 void setIdeRoot(Path ideRoot) {
592

593
    this.ideRoot = ideRoot;
3✔
594
  }
1✔
595

596
  @Override
597
  public Path getIdePath() {
598

599
    Path myIdeRoot = getIdeRoot();
3✔
600
    if (myIdeRoot == null) {
2✔
601
      return null;
2✔
602
    }
603
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
604
  }
605

606
  @Override
607
  public Path getCwd() {
608

609
    return this.cwd;
3✔
610
  }
611

612
  @Override
613
  public Path getTempPath() {
614

615
    Path idePath = getIdePath();
3✔
616
    if (idePath == null) {
2!
617
      return null;
×
618
    }
619
    return idePath.resolve("tmp");
4✔
620
  }
621

622
  @Override
623
  public Path getTempDownloadPath() {
624

625
    Path tmp = getTempPath();
3✔
626
    if (tmp == null) {
2!
627
      return null;
×
628
    }
629
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
630
  }
631

632
  @Override
633
  public Path getUserHome() {
634

635
    return this.userHome;
3✔
636
  }
637

638
  /**
639
   * This method should only be used for tests to mock user home.
640
   *
641
   * @param userHome the new value of {@link #getUserHome()}.
642
   */
643
  protected void setUserHome(Path userHome) {
644

645
    this.userHome = userHome;
3✔
646
    this.userHomeIde = userHome.resolve(FOLDER_DOT_IDE);
5✔
647
    this.downloadPath = computeDownloadPath(userHome);
5✔
648
    this.variables = null;
3✔
649
    resetPrivacyMap();
2✔
650
  }
1✔
651

652
  @Override
653
  public Path getUserHomeIde() {
654

655
    return this.userHomeIde;
3✔
656
  }
657

658
  @Override
659
  public Path getSettingsPath() {
660

661
    return this.settingsPath;
3✔
662
  }
663

664
  @Override
665
  public Path getSettingsGitRepository() {
666

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

676
  @Override
677
  public boolean isSettingsCodeRepository() {
678

679
    Path settingsPath = getSettingsPath();
3✔
680
    if (settingsPath != null) {
2!
681
      boolean settingsIsLink = Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
682
      if (settingsIsLink) {
2!
683
        Path realPath = getFileAccess().toRealPath(this.settingsPath);
×
684
        if (realPath != null) {
×
685
          return getGitContext().isGitRepo(realPath.getParent());
×
686
        }
687
        return true;
×
688
      }
689
    }
690
    return false;
2✔
691
  }
692

693
  @Override
694
  public Path getSettingsCommitIdPath() {
695

696
    return this.settingsCommitIdPath;
3✔
697
  }
698

699
  @Override
700
  public Path getConfPath() {
701

702
    return this.confPath;
3✔
703
  }
704

705
  @Override
706
  public Path getSoftwarePath() {
707

708
    if (this.ideHome == null) {
3✔
709
      return null;
2✔
710
    }
711
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
712
  }
713

714
  @Override
715
  public Path getSoftwareExtraPath() {
716

717
    Path softwarePath = getSoftwarePath();
3✔
718
    if (softwarePath == null) {
2✔
719
      return null;
2✔
720
    }
721
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
722
  }
723

724
  @Override
725
  public Path getSoftwareRepositoryPath() {
726

727
    Path idePath = getIdePath();
3✔
728
    if (idePath == null) {
2!
729
      return null;
×
730
    }
731
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
732
  }
733

734
  @Override
735
  public Path getPluginsPath() {
736

737
    return this.pluginsPath;
3✔
738
  }
739

740
  @Override
741
  public String getWorkspaceName() {
742

743
    return this.workspaceName;
3✔
744
  }
745

746
  @Override
747
  public Path getWorkspacesBasePath() {
748

749
    return this.workspacesBasePath;
3✔
750
  }
751

752
  @Override
753
  public Path getWorkspacePath() {
754

755
    return this.workspacePath;
3✔
756
  }
757

758
  @Override
759
  public Path getWorkspacePath(String workspace) {
760

761
    if (this.workspacesBasePath == null) {
3!
762
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
763
    }
764
    return this.workspacesBasePath.resolve(workspace);
5✔
765
  }
766

767
  @Override
768
  public Path getDownloadPath() {
769

770
    return this.downloadPath;
3✔
771
  }
772

773
  @Override
774
  public Path getUrlsPath() {
775

776
    Path idePath = getIdePath();
3✔
777
    if (idePath == null) {
2!
778
      return null;
×
779
    }
780
    return idePath.resolve(FOLDER_URLS);
4✔
781
  }
782

783
  @Override
784
  public Path getToolRepositoryPath() {
785

786
    Path idePath = getIdePath();
3✔
787
    if (idePath == null) {
2!
788
      return null;
×
789
    }
790
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
791
  }
792

793
  @Override
794
  public SystemPath getPath() {
795

796
    return this.path;
3✔
797
  }
798

799
  @Override
800
  public EnvironmentVariables getVariables() {
801

802
    if (this.variables == null) {
3✔
803
      this.variables = createVariables();
4✔
804
    }
805
    return this.variables;
3✔
806
  }
807

808
  @Override
809
  public UrlMetadata getUrls() {
810

811
    if (this.urlMetadata == null) {
3✔
812
      if (!isTest()) {
3!
813
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
814
      }
815
      this.urlMetadata = new UrlMetadata(this);
6✔
816
    }
817
    return this.urlMetadata;
3✔
818
  }
819

820
  @Override
821
  public boolean isQuietMode() {
822

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

826
  @Override
827
  public boolean isBatchMode() {
828

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

832
  @Override
833
  public boolean isForceMode() {
834

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

838
  @Override
839
  public boolean isForcePull() {
840

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

844
  @Override
845
  public boolean isForcePlugins() {
846

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

850
  @Override
851
  public boolean isForceRepositories() {
852

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

856
  @Override
857
  public boolean isOfflineMode() {
858

859
    return this.startContext.isOfflineMode();
4✔
860
  }
861

862
  @Override
863
  public boolean isPrivacyMode() {
864
    return this.startContext.isPrivacyMode();
4✔
865
  }
866

867
  @Override
868
  public boolean isSkipUpdatesMode() {
869

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

873
  @Override
874
  public boolean isNoColorsMode() {
875

876
    return this.startContext.isNoColorsMode();
×
877
  }
878

879
  @Override
880
  public NetworkStatus getNetworkStatus() {
881

882
    if (this.networkStatus == null) {
×
883
      this.networkStatus = new NetworkStatusImpl(this);
×
884
    }
885
    return this.networkStatus;
×
886
  }
887

888
  @Override
889
  public Locale getLocale() {
890

891
    Locale locale = this.startContext.getLocale();
4✔
892
    if (locale == null) {
2✔
893
      locale = Locale.getDefault();
2✔
894
    }
895
    return locale;
2✔
896
  }
897

898
  @Override
899
  public DirectoryMerger getWorkspaceMerger() {
900

901
    if (this.workspaceMerger == null) {
3✔
902
      this.workspaceMerger = new DirectoryMerger(this);
6✔
903
    }
904
    return this.workspaceMerger;
3✔
905
  }
906

907
  /**
908
   * @return the default execution directory in which a command process is executed.
909
   */
910
  @Override
911
  public Path getDefaultExecutionDirectory() {
912

913
    return this.defaultExecutionDirectory;
×
914
  }
915

916
  /**
917
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
918
   */
919
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
920

921
    if (defaultExecutionDirectory != null) {
×
922
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
923
    }
924
  }
×
925

926
  @Override
927
  public GitContext getGitContext() {
928

929
    return new GitContextImpl(this);
×
930
  }
931

932
  @Override
933
  public ProcessContext newProcess() {
934

935
    ProcessContext processContext = createProcessContext();
3✔
936
    if (this.defaultExecutionDirectory != null) {
3!
937
      processContext.directory(this.defaultExecutionDirectory);
×
938
    }
939
    return processContext;
2✔
940
  }
941

942
  @Override
943
  public IdeSystem getSystem() {
944

945
    if (this.system == null) {
×
946
      this.system = new IdeSystemImpl();
×
947
    }
948
    return this.system;
×
949
  }
950

951
  /**
952
   * @return a new instance of {@link ProcessContext}.
953
   * @see #newProcess()
954
   */
955
  protected ProcessContext createProcessContext() {
956

957
    return new ProcessContextImpl(this);
×
958
  }
959

960
  @Override
961
  public IdeLogLevel getLogLevelConsole() {
962

963
    return this.startContext.getLogLevelConsole();
4✔
964
  }
965

966
  @Override
967
  public IdeLogLevel getLogLevelLogger() {
968

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

972
  @Override
973
  public IdeLogListener getLogListener() {
974

975
    return this.startContext.getLogListener();
×
976
  }
977

978
  @Override
979
  public void logIdeHomeAndRootStatus() {
980
    if (this.ideRoot != null) {
3✔
981
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
11✔
982
    }
983
    if (this.ideHome == null) {
3✔
984
      LOG.warn(getMessageNotInsideIdeProject());
5✔
985
    } else {
986
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
987
    }
988
  }
1✔
989

990
  @Override
991
  public String formatArgument(Object argument) {
992

993
    if (argument == null) {
2✔
994
      return null;
2✔
995
    }
996
    String result = argument.toString();
3✔
997
    if (isPrivacyMode()) {
3✔
998
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
7!
999
        initializePrivacyMap(this.userHome, "~");
5✔
1000
        String projectName = getProjectName();
3✔
1001
        if (!projectName.isEmpty()) {
3!
1002
          this.privacyMap.put(projectName, "project");
6✔
1003
        }
1004
      }
1005
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
12✔
1006
        result = result.replace(entry.getKey(), entry.getValue());
9✔
1007
      }
1✔
1008
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
1009
    }
1010
    return result;
2✔
1011
  }
1012

1013
  /**
1014
   * @param path the sensitive {@link Path} to
1015
   * @param replacement the replacement to mask the {@link Path} in log output.
1016
   */
1017
  protected void initializePrivacyMap(Path path, String replacement) {
1018

1019
    if (path == null) {
2!
1020
      return;
×
1021
    }
1022
    if (this.systemInfo.isWindows()) {
4!
1023
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
1024
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
1025
    } else {
1026
      this.privacyMap.put(path.toString(), replacement);
7✔
1027
    }
1028
  }
1✔
1029

1030
  /**
1031
   * Resets the privacy map in case fundamental values have changed.
1032
   */
1033
  private void resetPrivacyMap() {
1034

1035
    this.privacyMap.clear();
3✔
1036
  }
1✔
1037

1038

1039
  @Override
1040
  public String askForInput(String message, String defaultValue) {
1041

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

1064
  @Override
1065
  public String askForSecret(String message) {
1066

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

1085
  /**
1086
   * @return a single line of input read from the user that should not be echoed to the console while typing (if supported). By default this simply delegates
1087
   *     to {@link #readLine()} and gets overridden where masked input is supported.
1088
   */
1089
  protected String readSecretLine() {
1090

1091
    return readLine();
3✔
1092
  }
1093

1094
  @Override
1095
  public <O> O question(O[] options, String question, Object... args) {
1096

1097
    assert (options.length > 0);
4!
1098
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1099
    LOG.warn(question, args);
4✔
1100
    return displayOptionsAndGetAnswer(options);
4✔
1101
  }
1102

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

1140
  private static String computeOptionKey(String option) {
1141
    String key = option;
2✔
1142
    int index = -1;
2✔
1143
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1144
      int currentIndex = key.indexOf(c);
4✔
1145
      if (currentIndex != -1) {
3✔
1146
        if ((index == -1) || (currentIndex < index)) {
3!
1147
          index = currentIndex;
2✔
1148
        }
1149
      }
1150
    }
1151
    if (index > 0) {
2✔
1152
      key = key.substring(0, index).trim();
6✔
1153
    }
1154
    return key;
2✔
1155
  }
1156

1157
  /**
1158
   * @return the input from the end-user (e.g. read from the console).
1159
   */
1160
  protected abstract String readLine();
1161

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

1164
    O duplicate = mapping.put(key, option);
5✔
1165
    if (duplicate != null) {
2!
1166
      throw new IllegalArgumentException("Duplicated option " + key);
×
1167
    }
1168
  }
1✔
1169

1170
  @Override
1171
  public Step getCurrentStep() {
1172

1173
    return this.currentStep;
×
1174
  }
1175

1176
  @Override
1177
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1178

1179
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1180
    return this.currentStep;
3✔
1181
  }
1182

1183
  /**
1184
   * Internal method to end the running {@link Step}.
1185
   *
1186
   * @param step the current {@link Step} to end.
1187
   */
1188
  public void endStep(StepImpl step) {
1189

1190
    if (step == this.currentStep) {
4!
1191
      this.currentStep = this.currentStep.getParent();
6✔
1192
    } else {
1193
      String currentStepName = "null";
×
1194
      if (this.currentStep != null) {
×
1195
        currentStepName = this.currentStep.getName();
×
1196
      }
1197
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1198
    }
1199
  }
1✔
1200

1201
  /**
1202
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1203
   *
1204
   * @param arguments the {@link CliArgument}.
1205
   * @return the return code of the execution.
1206
   */
1207
  public int run(CliArguments arguments) {
1208

1209
    CliArgument current = arguments.current();
3✔
1210
    if (current.isStart()) {
3✔
1211
      arguments.next();
3✔
1212
      current = arguments.current();
3✔
1213
    }
1214
    assert (this.currentStep == null);
4!
1215
    boolean supressStepSuccess = false;
2✔
1216
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1217
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1218
    Commandlet cmd = null;
2✔
1219
    ValidationResult result = null;
2✔
1220
    try {
1221
      while (commandletIterator.hasNext()) {
3✔
1222
        cmd = commandletIterator.next();
4✔
1223
        result = applyAndRun(arguments.copy(), cmd);
6✔
1224
        if (result.isValid()) {
3✔
1225
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1226
          step.success();
2✔
1227
          return ProcessResult.SUCCESS;
4✔
1228
        }
1229
      }
1230
      activateLogging(cmd);
3✔
1231
      String commandKey = current.getKey();
3✔
1232

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

1279

1280
  /**
1281
   * @return the {@link CliSuggester} for CLI suggestions.
1282
   */
1283
  private CliSuggester getCliSuggester() {
1284
    if (this.cliSuggester == null) {
3✔
1285
      this.cliSuggester = new CliSuggester(this);
6✔
1286
    }
1287
    return this.cliSuggester;
3✔
1288
  }
1289

1290
  /**
1291
   * Ensure the logging system is initialized.
1292
   */
1293
  private void activateLogging(Commandlet cmd) {
1294

1295
    configureJavaUtilLogging(cmd);
3✔
1296
    this.startContext.activateLogging();
3✔
1297
  }
1✔
1298

1299
  /**
1300
   * Configures the logging system (JUL).
1301
   *
1302
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1303
   */
1304
  public void configureJavaUtilLogging(Commandlet cmd) {
1305

1306
    if (this.julConfigured) {
3✔
1307
      return;
1✔
1308
    }
1309
    boolean writeLogfile = isWriteLogfile(cmd);
4✔
1310
    this.startContext.setWriteLogfile(writeLogfile);
4✔
1311
    Properties properties = createJavaUtilLoggingProperties(writeLogfile, cmd);
5✔
1312
    try {
1313
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1314
      properties.store(out, null);
4✔
1315
      out.flush();
2✔
1316
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1317
      LogManager.getLogManager().readConfiguration(in);
3✔
1318
      this.julConfigured = true;
3✔
1319
      this.startContext.activateLogging();
3✔
1320
    } catch (IOException e) {
×
1321
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1322
    }
1✔
1323
  }
1✔
1324

1325
  protected boolean isWriteLogfile(Commandlet cmd) {
1326
    if ((cmd == null) || !cmd.isWriteLogFile()) {
×
1327
      return false;
×
1328
    }
1329
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1330
    return Boolean.TRUE.equals(writeLogfile);
×
1331
  }
1332

1333
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1334

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

1360
  private Path createLogfilePath(Path idePath, Commandlet cmd) {
1361
    LocalDateTime now = LocalDateTime.now();
×
1362
    Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1363
    StringBuilder sb = new StringBuilder(32);
×
1364
    if (this.ideHome == null || ((cmd != null) && !cmd.isIdeHomeRequired())) {
×
1365
      sb.append("_ide-");
×
1366
    } else {
1367
      sb.append(this.ideHome.getFileName().toString());
×
1368
      sb.append('-');
×
1369
    }
1370
    sb.append("ide-");
×
1371
    if (cmd != null) {
×
1372
      sb.append(cmd.getName());
×
1373
      sb.append('-');
×
1374
    }
1375
    sb.append(DateTimeUtil.formatTime(now));
×
1376
    sb.append(".log");
×
1377
    return logsPath.resolve(sb.toString());
×
1378
  }
1379

1380
  @Override
1381
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1382

1383
    this.startContext.deactivateLogging(threshold);
4✔
1384
    lambda.run();
2✔
1385
    this.startContext.activateLogging();
3✔
1386
  }
1✔
1387

1388
  /**
1389
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1390
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1391
   *     {@link Commandlet} did not match and we have to try a different candidate).
1392
   */
1393
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1394

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

1427
              // Inform the user that an update is available. The update message is suppressed if we are already running the update
1428
              String msg = determineSettingsUpdateMessage(cmd);
4✔
1429
              if (msg != null) {
2!
1430
                IdeLogLevel.INTERACTION.log(LOG, msg);
4✔
1431
              }
1432
            }
1433
          }
1434
        }
1435
        boolean success = ensureLicenseAgreement(cmd);
4✔
1436
        if (!success) {
2!
1437
          return ValidationResultValid.get();
×
1438
        }
1439
        cmd.run();
2✔
1440
      } finally {
1441
        if (previousLogLevel != null) {
2!
1442
          this.startContext.setLogLevelConsole(previousLogLevel);
×
1443
        }
1444
      }
1✔
1445
    } else {
1446
      LOG.trace("Commandlet did not match");
3✔
1447
    }
1448
    return result;
2✔
1449
  }
1450

1451

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

1475
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1476

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

1517
    sb.setLength(0);
×
1518
    LocalDateTime now = LocalDateTime.now();
×
1519
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1520
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1521
    try {
1522
      Files.writeString(licenseAgreement, sb);
×
1523
    } catch (Exception e) {
×
1524
      throw new RuntimeException("Failed to save license agreement!", e);
×
1525
    }
×
1526
    if (oldLogLevel != newLogLevel) {
×
1527
      this.startContext.setLogLevelConsole(oldLogLevel);
×
1528
    }
1529
    return true;
×
1530
  }
1531

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

1553
  /**
1554
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1555
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1556
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1557
   */
1558
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1559

1560
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1561
    if (arguments.current().isStart()) {
4✔
1562
      arguments.next();
3✔
1563
    }
1564
    if (includeContextOptions) {
2✔
1565
      ContextCommandlet cc = new ContextCommandlet();
4✔
1566
      for (Property<?> property : cc.getProperties()) {
11✔
1567
        assert (property.isOption());
4!
1568
        property.apply(arguments, this, cc, collector);
7✔
1569
      }
1✔
1570
    }
1571
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1572
    CliArgument current = arguments.current();
3✔
1573
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1574
      collector.add(current.get(), null, null, null);
7✔
1575
    }
1576
    arguments.next();
3✔
1577
    while (commandletIterator.hasNext()) {
3✔
1578
      Commandlet cmd = commandletIterator.next();
4✔
1579
      if (!arguments.current().isEnd()) {
4✔
1580
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1581
      }
1582
    }
1✔
1583
    return collector.getSortedCandidates();
3✔
1584
  }
1585

1586
  /**
1587
   * Gets the next value property and applies its implicit end-options behavior if required.
1588
   *
1589
   * @param valueIterator the iterator over the commandlet value properties
1590
   * @param arguments the CLI arguments whose option parsing state may be updated
1591
   * @return the next value property or {@code null} if no further value property exists
1592
   */
1593
  private Property<?> nextValueProperty(Iterator<Property<?>> valueIterator, CliArguments arguments) {
1594

1595
    if (!valueIterator.hasNext()) {
3✔
1596
      return null;
2✔
1597
    }
1598

1599
    Property<?> valueProperty = valueIterator.next();
4✔
1600
    if (valueProperty.isEndOptions()) {
3✔
1601
      // Tool argument properties should accept values starting with "-" so stop option parsing here
1602
      arguments.endOptions();
2✔
1603
    }
1604
    return valueProperty;
2✔
1605
  }
1606

1607
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1608

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

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

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

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

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

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

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

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

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

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

1876
  @Override
1877
  public WindowsPathSyntax getPathSyntax() {
1878

1879
    return this.pathSyntax;
3✔
1880
  }
1881

1882
  /**
1883
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1884
   */
1885
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1886

1887
    this.pathSyntax = pathSyntax;
3✔
1888
  }
1✔
1889

1890
  /**
1891
   * @return the {@link IdeStartContextImpl}.
1892
   */
1893
  public IdeStartContextImpl getStartContext() {
1894

1895
    return startContext;
3✔
1896
  }
1897

1898
  /**
1899
   * @return the {@link WindowsHelper}.
1900
   */
1901
  public final WindowsHelper getWindowsHelper() {
1902

1903
    if (this.windowsHelper == null) {
3✔
1904
      this.windowsHelper = createWindowsHelper();
4✔
1905
    }
1906
    return this.windowsHelper;
3✔
1907
  }
1908

1909
  /**
1910
   * @return the new {@link WindowsHelper} instance.
1911
   */
1912
  protected WindowsHelper createWindowsHelper() {
1913

1914
    return new WindowsHelperImpl(this);
×
1915
  }
1916

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

1922
    this.variables = null;
3✔
1923
    this.customToolRepository = null;
3✔
1924
  }
1✔
1925

1926
  @Override
1927
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1928

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

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

1940
  }
1941

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

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