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

devonfw / IDEasy / 28790126080

06 Jul 2026 12:03PM UTC coverage: 71.844% (+0.01%) from 71.83%
28790126080

push

github

web-flow
#1178: Clean up and Solution for new qualifier snapshot (#2057)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

4785 of 7350 branches covered (65.1%)

Branch coverage included in aggregate %.

12257 of 16371 relevant lines covered (74.87%)

3.17 hits per line

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

67.59
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.common.SystemPath;
40
import com.devonfw.tools.ide.completion.CompletionCandidate;
41
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
42
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
43
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
44
import com.devonfw.tools.ide.environment.EnvironmentVariables;
45
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
46
import com.devonfw.tools.ide.environment.IdeSystem;
47
import com.devonfw.tools.ide.environment.IdeSystemImpl;
48
import com.devonfw.tools.ide.git.GitContext;
49
import com.devonfw.tools.ide.git.GitContextImpl;
50
import com.devonfw.tools.ide.git.GitUrl;
51
import com.devonfw.tools.ide.io.FileAccess;
52
import com.devonfw.tools.ide.io.FileAccessImpl;
53
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
54
import com.devonfw.tools.ide.log.IdeLogLevel;
55
import com.devonfw.tools.ide.log.IdeLogListener;
56
import com.devonfw.tools.ide.log.JulConsoleHandler;
57
import com.devonfw.tools.ide.merge.DirectoryMerger;
58
import com.devonfw.tools.ide.migration.IdeMigrator;
59
import com.devonfw.tools.ide.network.NetworkStatus;
60
import com.devonfw.tools.ide.network.NetworkStatusImpl;
61
import com.devonfw.tools.ide.os.SystemInfo;
62
import com.devonfw.tools.ide.os.SystemInfoImpl;
63
import com.devonfw.tools.ide.os.WindowsHelper;
64
import com.devonfw.tools.ide.os.WindowsHelperImpl;
65
import com.devonfw.tools.ide.os.WindowsPathSyntax;
66
import com.devonfw.tools.ide.process.ProcessContext;
67
import com.devonfw.tools.ide.process.ProcessContextImpl;
68
import com.devonfw.tools.ide.process.ProcessResult;
69
import com.devonfw.tools.ide.property.KeywordProperty;
70
import com.devonfw.tools.ide.property.Property;
71
import com.devonfw.tools.ide.step.Step;
72
import com.devonfw.tools.ide.step.StepImpl;
73
import com.devonfw.tools.ide.tool.custom.CustomToolRepository;
74
import com.devonfw.tools.ide.tool.custom.CustomToolRepositoryImpl;
75
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
76
import com.devonfw.tools.ide.tool.npm.NpmRepository;
77
import com.devonfw.tools.ide.tool.pip.PipRepository;
78
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
79
import com.devonfw.tools.ide.tool.repository.ToolRepository;
80
import com.devonfw.tools.ide.url.model.UrlMetadata;
81
import com.devonfw.tools.ide.util.DateTimeUtil;
82
import com.devonfw.tools.ide.util.PrivacyUtil;
83
import com.devonfw.tools.ide.validation.ValidationResult;
84
import com.devonfw.tools.ide.validation.ValidationResultValid;
85
import com.devonfw.tools.ide.validation.ValidationState;
86
import com.devonfw.tools.ide.variable.IdeVariables;
87
import com.devonfw.tools.ide.version.IdeVersion;
88
import com.devonfw.tools.ide.version.VersionIdentifier;
89

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

95
  static final Logger LOG = LoggerFactory.getLogger(AbstractIdeContext.class);
3✔
96

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

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

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

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

106
  private static final String OPTION_DETAILS_START = "([";
107

108
  private final IdeStartContextImpl startContext;
109

110
  private Path ideHome;
111

112
  private final Path ideRoot;
113

114
  private Path confPath;
115

116
  protected Path settingsPath;
117

118
  private Path settingsCommitIdPath;
119

120
  protected Path pluginsPath;
121

122
  private Path workspacePath;
123

124
  private Path workspacesBasePath;
125

126
  private String workspaceName;
127

128
  private Path cwd;
129

130
  private Path downloadPath;
131

132
  private Path userHome;
133

134
  private Path userHomeIde;
135

136
  private SystemPath path;
137

138
  private WindowsPathSyntax pathSyntax;
139

140
  private final SystemInfo systemInfo;
141

142
  private EnvironmentVariables variables;
143

144
  private final FileAccess fileAccess;
145

146
  protected CommandletManager commandletManager;
147

148
  protected ToolRepository defaultToolRepository;
149

150
  private CustomToolRepository customToolRepository;
151

152
  private MvnRepository mvnRepository;
153

154
  private NpmRepository npmRepository;
155

156
  private PipRepository pipRepository;
157

158
  private DirectoryMerger workspaceMerger;
159

160
  protected UrlMetadata urlMetadata;
161

162
  protected Path defaultExecutionDirectory;
163

164
  private StepImpl currentStep;
165

166
  private NetworkStatus networkStatus;
167

168
  protected IdeSystem system;
169

170
  private WindowsHelper windowsHelper;
171

172
  private final Map<String, String> privacyMap;
173

174
  private Path bash;
175

176
  private boolean julConfigured;
177

178
  private Path logfile;
179

180
  private CliSuggester cliSuggester;
181

182
  /**
183
   * The constructor.
184
   *
185
   * @param startContext the {@link IdeStartContextImpl}.
186
   * @param workingDirectory the optional {@link Path} to current working directory.
187
   */
188
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
189

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

223
    // detection completed, initializing variables
224
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
225

226
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
227

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

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

248
    Path currentDir = workingDirectory;
2✔
249
    String name1 = "";
2✔
250
    String name2 = "";
2✔
251
    String workspace = WORKSPACE_MAIN;
2✔
252
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
253

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

274
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
275
  }
276

277
  /**
278
   * @return a new {@link MvnRepository}
279
   */
280
  protected MvnRepository createMvnRepository() {
281
    return new MvnRepository(this);
5✔
282
  }
283

284
  /**
285
   * @return a new {@link NpmRepository}
286
   */
287
  protected NpmRepository createNpmRepository() {
288
    return new NpmRepository(this);
×
289
  }
290

291
  /**
292
   * @return a new {@link PipRepository}
293
   */
294
  protected PipRepository createPipRepository() {
295
    return new PipRepository(this);
×
296
  }
297

298
  private Path findIdeRoot(Path ideHomePath) {
299

300
    Path ideRootPath = null;
2✔
301
    if (ideHomePath != null) {
2✔
302
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
303
      ideRootPath = ideHomePath.getParent();
3✔
304
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
2!
305
        LOG.warn(
×
306
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
307
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
308
            ideRootPathFromEnv,
309
            ideHomePath.getFileName(), ideRootPath);
×
310
      }
311
    } else if (!isTest()) {
4!
312
      ideRootPath = getIdeRootPathFromEnv(true);
×
313
    }
314
    return ideRootPath;
2✔
315
  }
316

317
  /**
318
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
319
   */
320
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
321

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

354
  @Override
355
  public void setCwd(Path userDir, String workspace, Path ideHome) {
356

357
    this.cwd = userDir;
3✔
358
    this.workspaceName = workspace;
3✔
359
    this.ideHome = ideHome;
3✔
360
    if (ideHome == null) {
2✔
361
      this.workspacesBasePath = null;
3✔
362
      this.workspacePath = null;
3✔
363
      this.confPath = null;
3✔
364
      this.settingsPath = null;
3✔
365
      this.pluginsPath = null;
4✔
366
    } else {
367
      this.workspacesBasePath = this.ideHome.resolve(FOLDER_WORKSPACES);
6✔
368
      this.workspacePath = this.workspacesBasePath.resolve(this.workspaceName);
7✔
369
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
370
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
371
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
372
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
373
    }
374
    if (isTest()) {
3!
375
      // only for testing...
376
      if (this.ideHome == null) {
3✔
377
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
378
      } else {
379
        this.userHome = this.ideHome.resolve("home");
6✔
380
      }
381
    }
382
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
383
    this.downloadPath = computeDownloadPath(this.userHome);
6✔
384
    resetPrivacyMap();
2✔
385
    this.path = computeSystemPath();
4✔
386
  }
1✔
387

388
  /**
389
   * 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}
390
   * instead. Tests still use {@code ~/Downloads/ide} so existing fixtures keep working.
391
   */
392
  private Path computeDownloadPath(Path home) {
393

394
    if (!isTest() && this.systemInfo.isMac()) {
3!
395
      return home.resolve("Library/Caches/IDEasy/downloads");
×
396
    }
397
    return home.resolve("Downloads/ide");
4✔
398
  }
399

400
  private String getMessageIdeHomeFound() {
401

402
    String wks = this.workspaceName;
3✔
403
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
404
      wks = "*".repeat(wks.length());
×
405
    }
406
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
407
  }
408

409
  private String getMessageNotInsideIdeProject() {
410

411
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
412
  }
413

414
  private String getMessageIdeRootNotFound() {
415

416
    String root = getSystem().getEnv("IDE_ROOT");
5✔
417
    if (root == null) {
2!
418
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
419
    } else {
420
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
421
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
422
    }
423
  }
424

425
  /**
426
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
427
   */
428
  public boolean isTest() {
429

430
    return false;
×
431
  }
432

433
  protected SystemPath computeSystemPath() {
434

435
    return new SystemPath(this);
×
436
  }
437

438
  /**
439
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
440
   *
441
   * @param dir the directory to check.
442
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
443
   */
444
  protected boolean isIdeHome(Path dir) {
445

446
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
447
      return false;
2✔
448
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
449
      return false;
×
450
    }
451
    return true;
2✔
452
  }
453

454
  private EnvironmentVariables createVariables() {
455

456
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
457
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
458
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
459
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
460
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
461
    return conf.resolved();
3✔
462
  }
463

464
  protected AbstractEnvironmentVariables createSystemVariables() {
465

466
    return EnvironmentVariables.ofSystem(this);
3✔
467
  }
468

469
  @Override
470
  public SystemInfo getSystemInfo() {
471

472
    return this.systemInfo;
3✔
473
  }
474

475
  @Override
476
  public FileAccess getFileAccess() {
477

478
    return this.fileAccess;
3✔
479
  }
480

481
  @Override
482
  public CommandletManager getCommandletManager() {
483

484
    return this.commandletManager;
3✔
485
  }
486

487
  @Override
488
  public ToolRepository getDefaultToolRepository() {
489

490
    return this.defaultToolRepository;
3✔
491
  }
492

493
  @Override
494
  public MvnRepository getMvnRepository() {
495
    if (this.mvnRepository == null) {
3✔
496
      this.mvnRepository = createMvnRepository();
4✔
497
    }
498
    return this.mvnRepository;
3✔
499
  }
500

501
  @Override
502
  public NpmRepository getNpmRepository() {
503
    if (this.npmRepository == null) {
3✔
504
      this.npmRepository = createNpmRepository();
4✔
505
    }
506
    return this.npmRepository;
3✔
507
  }
508

509
  @Override
510
  public PipRepository getPipRepository() {
511
    if (this.pipRepository == null) {
3✔
512
      this.pipRepository = createPipRepository();
4✔
513
    }
514
    return this.pipRepository;
3✔
515
  }
516

517
  @Override
518
  public CustomToolRepository getCustomToolRepository() {
519

520
    if (this.customToolRepository == null) {
3✔
521
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
522
    }
523
    return this.customToolRepository;
3✔
524
  }
525

526
  @Override
527
  public Path getIdeHome() {
528

529
    return this.ideHome;
3✔
530
  }
531

532
  @Override
533
  public String getProjectName() {
534

535
    if (this.ideHome != null) {
3!
536
      return this.ideHome.getFileName().toString();
5✔
537
    }
538
    return "";
×
539
  }
540

541
  @Override
542
  public VersionIdentifier getProjectVersion() {
543

544
    if (this.ideHome != null) {
3!
545
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
546
      if (Files.exists(versionFile)) {
5✔
547
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
548
        return VersionIdentifier.of(version);
3✔
549
      }
550
    }
551
    return IdeMigrator.START_VERSION;
2✔
552
  }
553

554
  @Override
555
  public void setProjectVersion(VersionIdentifier version) {
556

557
    if (this.ideHome == null) {
3!
558
      throw new IllegalStateException("IDE_HOME not available!");
×
559
    }
560
    Objects.requireNonNull(version);
3✔
561
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
562
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
563
  }
1✔
564

565
  @Override
566
  public Path getIdeRoot() {
567

568
    return this.ideRoot;
3✔
569
  }
570

571
  @Override
572
  public Path getIdePath() {
573

574
    Path myIdeRoot = getIdeRoot();
3✔
575
    if (myIdeRoot == null) {
2✔
576
      return null;
2✔
577
    }
578
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
579
  }
580

581
  @Override
582
  public Path getCwd() {
583

584
    return this.cwd;
3✔
585
  }
586

587
  @Override
588
  public Path getTempPath() {
589

590
    Path idePath = getIdePath();
3✔
591
    if (idePath == null) {
2!
592
      return null;
×
593
    }
594
    return idePath.resolve("tmp");
4✔
595
  }
596

597
  @Override
598
  public Path getTempDownloadPath() {
599

600
    Path tmp = getTempPath();
3✔
601
    if (tmp == null) {
2!
602
      return null;
×
603
    }
604
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
605
  }
606

607
  @Override
608
  public Path getUserHome() {
609

610
    return this.userHome;
3✔
611
  }
612

613
  /**
614
   * This method should only be used for tests to mock user home.
615
   *
616
   * @param userHome the new value of {@link #getUserHome()}.
617
   */
618
  protected void setUserHome(Path userHome) {
619

620
    this.userHome = userHome;
3✔
621
    this.userHomeIde = userHome.resolve(FOLDER_DOT_IDE);
5✔
622
    this.downloadPath = computeDownloadPath(userHome);
5✔
623
    this.variables = null;
3✔
624
    resetPrivacyMap();
2✔
625
  }
1✔
626

627
  @Override
628
  public Path getUserHomeIde() {
629

630
    return this.userHomeIde;
3✔
631
  }
632

633
  @Override
634
  public Path getSettingsPath() {
635

636
    return this.settingsPath;
3✔
637
  }
638

639
  @Override
640
  public Path getSettingsGitRepository() {
641

642
    Path settingsPath = getSettingsPath();
3✔
643
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
644
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsCodeRepository()) {
12!
645
      LOG.error("Settings repository exists but is not a git repository.");
3✔
646
      return null;
2✔
647
    }
648
    return settingsPath;
2✔
649
  }
650

651
  @Override
652
  public boolean isSettingsCodeRepository() {
653

654
    Path settingsPath = getSettingsPath();
3✔
655
    if (settingsPath != null) {
2!
656
      boolean settingsIsLink = Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
657
      if (settingsIsLink) {
2!
658
        Path realPath = getFileAccess().toRealPath(this.settingsPath);
×
659
        if (realPath != null) {
×
660
          return getGitContext().isGitRepo(realPath.getParent());
×
661
        }
662
        return true;
×
663
      }
664
    }
665
    return false;
2✔
666
  }
667

668
  @Override
669
  public Path getSettingsCommitIdPath() {
670

671
    return this.settingsCommitIdPath;
3✔
672
  }
673

674
  @Override
675
  public Path getConfPath() {
676

677
    return this.confPath;
3✔
678
  }
679

680
  @Override
681
  public Path getSoftwarePath() {
682

683
    if (this.ideHome == null) {
3✔
684
      return null;
2✔
685
    }
686
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
687
  }
688

689
  @Override
690
  public Path getSoftwareExtraPath() {
691

692
    Path softwarePath = getSoftwarePath();
3✔
693
    if (softwarePath == null) {
2✔
694
      return null;
2✔
695
    }
696
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
697
  }
698

699
  @Override
700
  public Path getSoftwareRepositoryPath() {
701

702
    Path idePath = getIdePath();
3✔
703
    if (idePath == null) {
2!
704
      return null;
×
705
    }
706
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
707
  }
708

709
  @Override
710
  public Path getPluginsPath() {
711

712
    return this.pluginsPath;
3✔
713
  }
714

715
  @Override
716
  public String getWorkspaceName() {
717

718
    return this.workspaceName;
3✔
719
  }
720

721
  @Override
722
  public Path getWorkspacesBasePath() {
723

724
    return this.workspacesBasePath;
3✔
725
  }
726

727
  @Override
728
  public Path getWorkspacePath() {
729

730
    return this.workspacePath;
3✔
731
  }
732

733
  @Override
734
  public Path getWorkspacePath(String workspace) {
735

736
    if (this.workspacesBasePath == null) {
3!
737
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
738
    }
739
    return this.workspacesBasePath.resolve(workspace);
5✔
740
  }
741

742
  @Override
743
  public Path getDownloadPath() {
744

745
    return this.downloadPath;
3✔
746
  }
747

748
  @Override
749
  public Path getUrlsPath() {
750

751
    Path idePath = getIdePath();
3✔
752
    if (idePath == null) {
2!
753
      return null;
×
754
    }
755
    return idePath.resolve(FOLDER_URLS);
4✔
756
  }
757

758
  @Override
759
  public Path getToolRepositoryPath() {
760

761
    Path idePath = getIdePath();
3✔
762
    if (idePath == null) {
2!
763
      return null;
×
764
    }
765
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
766
  }
767

768
  @Override
769
  public SystemPath getPath() {
770

771
    return this.path;
3✔
772
  }
773

774
  @Override
775
  public EnvironmentVariables getVariables() {
776

777
    if (this.variables == null) {
3✔
778
      this.variables = createVariables();
4✔
779
    }
780
    return this.variables;
3✔
781
  }
782

783
  @Override
784
  public UrlMetadata getUrls() {
785

786
    if (this.urlMetadata == null) {
3✔
787
      if (!isTest()) {
3!
788
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
789
      }
790
      this.urlMetadata = new UrlMetadata(this);
6✔
791
    }
792
    return this.urlMetadata;
3✔
793
  }
794

795
  @Override
796
  public boolean isQuietMode() {
797

798
    return this.startContext.isQuietMode();
4✔
799
  }
800

801
  @Override
802
  public boolean isBatchMode() {
803

804
    return this.startContext.isBatchMode();
4✔
805
  }
806

807
  @Override
808
  public boolean isForceMode() {
809

810
    return this.startContext.isForceMode();
4✔
811
  }
812

813
  @Override
814
  public boolean isForcePull() {
815

816
    return this.startContext.isForcePull();
4✔
817
  }
818

819
  @Override
820
  public boolean isForcePlugins() {
821

822
    return this.startContext.isForcePlugins();
4✔
823
  }
824

825
  @Override
826
  public boolean isForceRepositories() {
827

828
    return this.startContext.isForceRepositories();
4✔
829
  }
830

831
  @Override
832
  public boolean isOfflineMode() {
833

834
    return this.startContext.isOfflineMode();
4✔
835
  }
836

837
  @Override
838
  public boolean isPrivacyMode() {
839
    return this.startContext.isPrivacyMode();
4✔
840
  }
841

842
  @Override
843
  public boolean isSkipUpdatesMode() {
844

845
    return this.startContext.isSkipUpdatesMode();
4✔
846
  }
847

848
  @Override
849
  public boolean isNoColorsMode() {
850

851
    return this.startContext.isNoColorsMode();
×
852
  }
853

854
  @Override
855
  public NetworkStatus getNetworkStatus() {
856

857
    if (this.networkStatus == null) {
×
858
      this.networkStatus = new NetworkStatusImpl(this);
×
859
    }
860
    return this.networkStatus;
×
861
  }
862

863
  @Override
864
  public Locale getLocale() {
865

866
    Locale locale = this.startContext.getLocale();
4✔
867
    if (locale == null) {
2✔
868
      locale = Locale.getDefault();
2✔
869
    }
870
    return locale;
2✔
871
  }
872

873
  @Override
874
  public DirectoryMerger getWorkspaceMerger() {
875

876
    if (this.workspaceMerger == null) {
3✔
877
      this.workspaceMerger = new DirectoryMerger(this);
6✔
878
    }
879
    return this.workspaceMerger;
3✔
880
  }
881

882
  /**
883
   * @return the default execution directory in which a command process is executed.
884
   */
885
  @Override
886
  public Path getDefaultExecutionDirectory() {
887

888
    return this.defaultExecutionDirectory;
×
889
  }
890

891
  /**
892
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
893
   */
894
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
895

896
    if (defaultExecutionDirectory != null) {
×
897
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
898
    }
899
  }
×
900

901
  @Override
902
  public GitContext getGitContext() {
903

904
    return new GitContextImpl(this);
×
905
  }
906

907
  @Override
908
  public ProcessContext newProcess() {
909

910
    ProcessContext processContext = createProcessContext();
3✔
911
    if (this.defaultExecutionDirectory != null) {
3!
912
      processContext.directory(this.defaultExecutionDirectory);
×
913
    }
914
    return processContext;
2✔
915
  }
916

917
  @Override
918
  public IdeSystem getSystem() {
919

920
    if (this.system == null) {
×
921
      this.system = new IdeSystemImpl();
×
922
    }
923
    return this.system;
×
924
  }
925

926
  /**
927
   * @return a new instance of {@link ProcessContext}.
928
   * @see #newProcess()
929
   */
930
  protected ProcessContext createProcessContext() {
931

932
    return new ProcessContextImpl(this);
×
933
  }
934

935
  @Override
936
  public IdeLogLevel getLogLevelConsole() {
937

938
    return this.startContext.getLogLevelConsole();
4✔
939
  }
940

941
  @Override
942
  public IdeLogLevel getLogLevelLogger() {
943

944
    return this.startContext.getLogLevelLogger();
×
945
  }
946

947
  @Override
948
  public IdeLogListener getLogListener() {
949

950
    return this.startContext.getLogListener();
×
951
  }
952

953
  @Override
954
  public void logIdeHomeAndRootStatus() {
955
    if (this.ideRoot != null) {
3!
956
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
×
957
    }
958
    if (this.ideHome == null) {
3✔
959
      LOG.warn(getMessageNotInsideIdeProject());
5✔
960
    } else {
961
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
962
    }
963
  }
1✔
964

965
  @Override
966
  public String formatArgument(Object argument) {
967

968
    if (argument == null) {
2✔
969
      return null;
2✔
970
    }
971
    String result = argument.toString();
3✔
972
    if (isPrivacyMode()) {
3✔
973
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
974
        initializePrivacyMap(this.userHome, "~");
×
975
        String projectName = getProjectName();
×
976
        if (!projectName.isEmpty()) {
×
977
          this.privacyMap.put(projectName, "project");
×
978
        }
979
      }
980
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
981
        result = result.replace(entry.getKey(), entry.getValue());
×
982
      }
×
983
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
984
    }
985
    return result;
2✔
986
  }
987

988
  /**
989
   * @param path the sensitive {@link Path} to
990
   * @param replacement the replacement to mask the {@link Path} in log output.
991
   */
992
  protected void initializePrivacyMap(Path path, String replacement) {
993

994
    if (path == null) {
×
995
      return;
×
996
    }
997
    if (this.systemInfo.isWindows()) {
×
998
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
999
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
1000
    } else {
1001
      this.privacyMap.put(path.toString(), replacement);
×
1002
    }
1003
  }
×
1004

1005
  /**
1006
   * Resets the privacy map in case fundamental values have changed.
1007
   */
1008
  private void resetPrivacyMap() {
1009

1010
    this.privacyMap.clear();
3✔
1011
  }
1✔
1012

1013

1014
  @Override
1015
  public String askForInput(String message, String defaultValue) {
1016

1017
    while (true) {
1018
      if (!message.isBlank()) {
3!
1019
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
1020
      }
1021
      if (isBatchMode()) {
3!
1022
        if (isForceMode()) {
×
1023
          return defaultValue;
×
1024
        } else {
1025
          throw new CliAbortException();
×
1026
        }
1027
      }
1028
      String input = readLine().trim();
4✔
1029
      if (!input.isEmpty()) {
3!
1030
        return input;
2✔
1031
      } else {
1032
        if (defaultValue != null) {
×
1033
          return defaultValue;
×
1034
        }
1035
      }
1036
    }
×
1037
  }
1038

1039
  @Override
1040
  public <O> O question(O[] options, String question, Object... args) {
1041

1042
    assert (options.length > 0);
4!
1043
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1044
    LOG.warn(question, args);
4✔
1045
    return displayOptionsAndGetAnswer(options);
4✔
1046
  }
1047

1048
  private <O> O displayOptionsAndGetAnswer(O[] options) {
1049
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
1050
    int i = 0;
2✔
1051
    for (O option : options) {
16✔
1052
      i++;
1✔
1053
      String title = "" + option;
4✔
1054
      String key = computeOptionKey(title);
3✔
1055
      addMapping(mapping, key, option);
4✔
1056
      String numericKey = Integer.toString(i);
3✔
1057
      if (numericKey.equals(key)) {
4!
1058
        LOG.trace("Options should not be numeric: {}", key);
×
1059
      } else {
1060
        addMapping(mapping, numericKey, option);
4✔
1061
      }
1062
      IdeLogLevel.INTERACTION.log(LOG, "Option {}: {}", numericKey, title);
14✔
1063
    }
1064
    if (options.length == 1) {
4✔
1065
      mapping.put("", options[0]);
7✔
1066
    }
1067
    O option = null;
2✔
1068
    if (isBatchMode()) {
3!
1069
      if (isForceMode()) {
×
1070
        option = options[0];
×
1071
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
×
1072
      }
1073
    } else {
1074
      while (option == null) {
2✔
1075
        String answer = readLine();
3✔
1076
        option = mapping.get(answer);
4✔
1077
        if (option == null) {
2!
1078
          LOG.warn("Invalid answer: '{}' - please try again.", answer);
×
1079
        }
1080
      }
1✔
1081
    }
1082
    return option;
2✔
1083
  }
1084

1085
  private static String computeOptionKey(String option) {
1086
    String key = option;
2✔
1087
    int index = -1;
2✔
1088
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1089
      int currentIndex = key.indexOf(c);
4✔
1090
      if (currentIndex != -1) {
3✔
1091
        if ((index == -1) || (currentIndex < index)) {
3!
1092
          index = currentIndex;
2✔
1093
        }
1094
      }
1095
    }
1096
    if (index > 0) {
2✔
1097
      key = key.substring(0, index).trim();
6✔
1098
    }
1099
    return key;
2✔
1100
  }
1101

1102
  /**
1103
   * @return the input from the end-user (e.g. read from the console).
1104
   */
1105
  protected abstract String readLine();
1106

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

1109
    O duplicate = mapping.put(key, option);
5✔
1110
    if (duplicate != null) {
2!
1111
      throw new IllegalArgumentException("Duplicated option " + key);
×
1112
    }
1113
  }
1✔
1114

1115
  @Override
1116
  public Step getCurrentStep() {
1117

1118
    return this.currentStep;
×
1119
  }
1120

1121
  @Override
1122
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1123

1124
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1125
    return this.currentStep;
3✔
1126
  }
1127

1128
  /**
1129
   * Internal method to end the running {@link Step}.
1130
   *
1131
   * @param step the current {@link Step} to end.
1132
   */
1133
  public void endStep(StepImpl step) {
1134

1135
    if (step == this.currentStep) {
4!
1136
      this.currentStep = this.currentStep.getParent();
6✔
1137
    } else {
1138
      String currentStepName = "null";
×
1139
      if (this.currentStep != null) {
×
1140
        currentStepName = this.currentStep.getName();
×
1141
      }
1142
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1143
    }
1144
  }
1✔
1145

1146
  /**
1147
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1148
   *
1149
   * @param arguments the {@link CliArgument}.
1150
   * @return the return code of the execution.
1151
   */
1152
  public int run(CliArguments arguments) {
1153

1154
    CliArgument current = arguments.current();
3✔
1155
    if (current.isStart()) {
3✔
1156
      arguments.next();
3✔
1157
      current = arguments.current();
3✔
1158
    }
1159
    assert (this.currentStep == null);
4!
1160
    boolean supressStepSuccess = false;
2✔
1161
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1162
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1163
    Commandlet cmd = null;
2✔
1164
    ValidationResult result = null;
2✔
1165
    try {
1166
      while (commandletIterator.hasNext()) {
3✔
1167
        cmd = commandletIterator.next();
4✔
1168
        result = applyAndRun(arguments.copy(), cmd);
6✔
1169
        if (result.isValid()) {
3✔
1170
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1171
          step.success();
2✔
1172
          return ProcessResult.SUCCESS;
4✔
1173
        }
1174
      }
1175
      activateLogging(cmd);
3✔
1176
      verifyIdeMinVersion(false);
3✔
1177
      String commandKey = current.getKey();
3✔
1178

1179
      if (commandKey == null || commandKey.isBlank()) {
5!
1180
        return 0;
×
1181
      }
1182
      Commandlet commandletByName = this.commandletManager.getCommandlet(commandKey);
5✔
1183
      // Missing commandlet
1184
      if (commandletByName == null) {
2✔
1185
        if (getCliSuggester().isMissingCommandletHandled(commandKey, step)) {
6!
1186
          return 1;
4✔
1187
        }
1188
        return 0;
×
1189
      }
1190
      // Missing project context
1191
      if (getCliSuggester().isMissingProjectContextHandled(commandletByName, step)) {
6✔
1192
        return 1;
4✔
1193
      }
1194
      // Only validate options/arguments if same commandlet and proper type
1195
      if (cmd != commandletByName || !(result instanceof ValidationState validationState)) {
10!
1196
        return 0;
×
1197
      }
1198
      // Invalid option
1199
      if (getCliSuggester().isInvalidOptionHandled(validationState, commandletByName, step)) {
7✔
1200
        return 1;
4✔
1201
      }
1202
      // Invalid argument
1203
      if (getCliSuggester().isInvalidArgumentHandled(validationState, commandletByName)) {
6!
1204
        return 1;
4✔
1205
      }
1206
      LOG.error(result.getErrorMessage());
×
1207
      step.error("Invalid arguments: {}", current.getArgs());
×
1208
      IdeLogLevel.INTERACTION.log(LOG, "For additional details run ide help {}", cmd == null ? "" : cmd.getName());
×
1209
      return 1;
×
1210
    } catch (Throwable t) {
1✔
1211
      activateLogging(cmd);
3✔
1212
      step.error(t, true);
4✔
1213
      if (this.logfile != null) {
3!
1214
        System.err.println("Logfile can be found at " + this.logfile); // do not use logger
×
1215
      }
1216
      throw t;
2✔
1217
    } finally {
1218
      step.close();
2✔
1219
      assert (this.currentStep == null);
4!
1220
      step.logSummary(supressStepSuccess);
3✔
1221
    }
1222
  }
1223

1224

1225
  /**
1226
   * @return the {@link CliSuggester} for CLI suggestions.
1227
   */
1228
  private CliSuggester getCliSuggester() {
1229
    if (this.cliSuggester == null) {
3✔
1230
      this.cliSuggester = new CliSuggester(this);
6✔
1231
    }
1232
    return this.cliSuggester;
3✔
1233
  }
1234

1235
  /**
1236
   * Ensure the logging system is initialized.
1237
   */
1238
  private void activateLogging(Commandlet cmd) {
1239

1240
    configureJavaUtilLogging(cmd);
3✔
1241
    this.startContext.activateLogging();
3✔
1242
  }
1✔
1243

1244
  /**
1245
   * Configures the logging system (JUL).
1246
   *
1247
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1248
   */
1249
  public void configureJavaUtilLogging(Commandlet cmd) {
1250

1251
    if (this.julConfigured) {
3✔
1252
      return;
1✔
1253
    }
1254
    boolean writeLogfile = isWriteLogfile(cmd);
4✔
1255
    this.startContext.setWriteLogfile(writeLogfile);
4✔
1256
    Properties properties = createJavaUtilLoggingProperties(writeLogfile, cmd);
5✔
1257
    try {
1258
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1259
      properties.store(out, null);
4✔
1260
      out.flush();
2✔
1261
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1262
      LogManager.getLogManager().readConfiguration(in);
3✔
1263
      this.julConfigured = true;
3✔
1264
      this.startContext.activateLogging();
3✔
1265
    } catch (IOException e) {
×
1266
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1267
    }
1✔
1268
  }
1✔
1269

1270
  protected boolean isWriteLogfile(Commandlet cmd) {
1271
    if ((cmd == null) || !cmd.isWriteLogFile()) {
×
1272
      return false;
×
1273
    }
1274
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1275
    return Boolean.TRUE.equals(writeLogfile);
×
1276
  }
1277

1278
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1279

1280
    Path idePath = getIdePath();
3✔
1281
    if (writeLogfile && (idePath == null)) {
2!
1282
      writeLogfile = false;
×
1283
      LOG.error("Cannot enable log-file since IDE_ROOT is undefined.");
×
1284
    }
1285
    Properties properties = new Properties();
4✔
1286
    // prevent 3rd party (e.g. java.lang.ProcessBuilder) logging into our console via JUL
1287
    // see JulLogLevel for the trick we did to workaround JUL flaws
1288
    properties.setProperty(".level", "SEVERE");
5✔
1289
    if (writeLogfile) {
2!
1290
      this.startContext.setLogLevelLogger(IdeLogLevel.TRACE);
×
1291
      String fileHandlerName = FileHandler.class.getName();
×
1292
      properties.setProperty("handlers", JulConsoleHandler.class.getName() + "," + fileHandlerName);
×
1293
      properties.setProperty(fileHandlerName + ".formatter", SimpleFormatter.class.getName());
×
1294
      properties.setProperty(fileHandlerName + ".encoding", "UTF-8");
×
1295
      this.logfile = createLogfilePath(idePath, cmd);
×
1296
      getFileAccess().mkdirs(this.logfile.getParent());
×
1297
      properties.setProperty(fileHandlerName + ".pattern", this.logfile.toString());
×
1298
    } else {
×
1299
      properties.setProperty("handlers", JulConsoleHandler.class.getName());
6✔
1300
    }
1301
    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✔
1302
    return properties;
2✔
1303
  }
1304

1305
  private Path createLogfilePath(Path idePath, Commandlet cmd) {
1306
    LocalDateTime now = LocalDateTime.now();
×
1307
    Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1308
    StringBuilder sb = new StringBuilder(32);
×
1309
    if (this.ideHome == null || ((cmd != null) && !cmd.isIdeHomeRequired())) {
×
1310
      sb.append("_ide-");
×
1311
    } else {
1312
      sb.append(this.ideHome.getFileName().toString());
×
1313
      sb.append('-');
×
1314
    }
1315
    sb.append("ide-");
×
1316
    if (cmd != null) {
×
1317
      sb.append(cmd.getName());
×
1318
      sb.append('-');
×
1319
    }
1320
    sb.append(DateTimeUtil.formatTime(now));
×
1321
    sb.append(".log");
×
1322
    return logsPath.resolve(sb.toString());
×
1323
  }
1324

1325
  @Override
1326
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1327

1328
    this.startContext.deactivateLogging(threshold);
4✔
1329
    lambda.run();
2✔
1330
    this.startContext.activateLogging();
3✔
1331
  }
1✔
1332

1333
  /**
1334
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1335
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1336
   *     {@link Commandlet} did not match and we have to try a different candidate).
1337
   */
1338
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1339

1340
    IdeLogLevel previousLogLevel = null;
2✔
1341
    cmd.reset();
2✔
1342
    ValidationResult result = apply(arguments, cmd);
5✔
1343
    if (result.isValid()) {
3✔
1344
      result = cmd.validate();
3✔
1345
    }
1346
    if (result.isValid()) {
3✔
1347
      LOG.debug("Running commandlet {}", cmd);
4✔
1348
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1349
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1350
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6✔
1351
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1352
      }
1353
      try {
1354
        if (cmd.isProcessableOutput()) {
3✔
1355
          if (!isLogLevelEnabled(IdeLogLevel.DEBUG)) {
4!
1356
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1357
            previousLogLevel = this.startContext.setLogLevelConsole(IdeLogLevel.PROCESSABLE);
×
1358
          }
1359
        } else {
1360
          if (cmd.isIdeHomeRequired()) {
3!
1361
            LOG.debug(getMessageIdeHomeFound());
4✔
1362
          }
1363
          Path settingsRepository = getSettingsGitRepository();
3✔
1364
          if (settingsRepository != null) {
2!
1365
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
8!
1366
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
4!
1367
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1368

1369
              // Inform the user that an update is available. The update message is suppressed if we are already running the update
1370
              String msg = determineSettingsUpdateMessage(cmd);
×
1371
              if (msg != null) {
×
1372
                IdeLogLevel.INTERACTION.log(LOG, msg);
×
1373
              }
1374
            }
1375
          }
1376
        }
1377
        boolean success = ensureLicenseAgreement(cmd);
4✔
1378
        if (!success) {
2!
1379
          return ValidationResultValid.get();
×
1380
        }
1381
        cmd.run();
2✔
1382
      } finally {
1383
        if (previousLogLevel != null) {
2!
1384
          this.startContext.setLogLevelConsole(previousLogLevel);
×
1385
        }
1386
      }
1✔
1387
    } else {
1388
      LOG.trace("Commandlet did not match");
3✔
1389
    }
1390
    return result;
2✔
1391
  }
1392

1393

1394
  /**
1395
   * 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
1396
   * 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
1397
   * appropriate {@code ide update} command, the message is suppressed to avoid confusion.
1398
   *
1399
   * @param cmd the {@link Commandlet}.
1400
   * @return {@code msg} to log to the console. {@code null} if the message is suppressed.
1401
   */
1402
  private String determineSettingsUpdateMessage(Commandlet cmd) {
1403
    boolean update = cmd instanceof UpdateCommandlet;
×
1404
    if (isSettingsCodeRepository()) {
×
1405
      if (update && (isForceMode() || isForcePull())) {
×
1406
        return null;
×
1407
      }
1408
      return "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1409
    } else {
1410
      if (update) {
×
1411
        return null;
×
1412
      }
1413
      return "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
×
1414
    }
1415
  }
1416

1417
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1418

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

1459
    sb.setLength(0);
×
1460
    LocalDateTime now = LocalDateTime.now();
×
1461
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1462
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1463
    try {
1464
      Files.writeString(licenseAgreement, sb);
×
1465
    } catch (Exception e) {
×
1466
      throw new RuntimeException("Failed to save license agreement!", e);
×
1467
    }
×
1468
    if (oldLogLevel != newLogLevel) {
×
1469
      this.startContext.setLogLevelConsole(oldLogLevel);
×
1470
    }
1471
    return true;
×
1472
  }
1473

1474
  @Override
1475
  public void verifyIdeMinVersion(boolean throwException) {
1476
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1477
    if (minVersion == null) {
2✔
1478
      return;
1✔
1479
    }
1480
    VersionIdentifier versionIdentifier = IdeVersion.getVersionIdentifier();
2✔
1481
    if (versionIdentifier.compareVersion(minVersion).isLess() && !IdeVersion.isUndefined()) {
7!
1482
      String message = String.format("Your version of IDEasy is currently %s\n"
13✔
1483
          + "However, this is too old as your project requires at latest version %s\n"
1484
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1485
          + "ide upgrade", versionIdentifier, minVersion);
1486
      if (throwException) {
2✔
1487
        throw new CliException(message);
5✔
1488
      } else {
1489
        LOG.warn(message);
3✔
1490
      }
1491
    }
1492
  }
1✔
1493

1494
  /**
1495
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1496
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1497
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1498
   */
1499
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1500

1501
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1502
    if (arguments.current().isStart()) {
4✔
1503
      arguments.next();
3✔
1504
    }
1505
    if (includeContextOptions) {
2✔
1506
      ContextCommandlet cc = new ContextCommandlet();
4✔
1507
      for (Property<?> property : cc.getProperties()) {
11✔
1508
        assert (property.isOption());
4!
1509
        property.apply(arguments, this, cc, collector);
7✔
1510
      }
1✔
1511
    }
1512
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1513
    CliArgument current = arguments.current();
3✔
1514
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1515
      collector.add(current.get(), null, null, null);
7✔
1516
    }
1517
    arguments.next();
3✔
1518
    while (commandletIterator.hasNext()) {
3✔
1519
      Commandlet cmd = commandletIterator.next();
4✔
1520
      if (!arguments.current().isEnd()) {
4✔
1521
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1522
      }
1523
    }
1✔
1524
    return collector.getSortedCandidates();
3✔
1525
  }
1526

1527
  /**
1528
   * Gets the next value property and applies its implicit end-options behavior if required.
1529
   *
1530
   * @param valueIterator the iterator over the commandlet value properties
1531
   * @param arguments the CLI arguments whose option parsing state may be updated
1532
   * @return the next value property or {@code null} if no further value property exists
1533
   */
1534
  private Property<?> nextValueProperty(Iterator<Property<?>> valueIterator, CliArguments arguments) {
1535

1536
    if (!valueIterator.hasNext()) {
3✔
1537
      return null;
2✔
1538
    }
1539

1540
    Property<?> valueProperty = valueIterator.next();
4✔
1541
    if (valueProperty.isEndOptions()) {
3✔
1542
      // Tool argument properties should accept values starting with "-" so stop option parsing here
1543
      arguments.endOptions();
2✔
1544
    }
1545
    return valueProperty;
2✔
1546
  }
1547

1548
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1549

1550
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1551
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1552
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1553
    Property<?> currentValueProperty = nextValueProperty(valueIterator, arguments);
5✔
1554
    List<Property<?>> properties = cmd.getProperties();
3✔
1555
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1556
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1557
    for (Property<?> property : properties) {
10✔
1558
      if (property.isOption()) {
3✔
1559
        optionProperties.add(property);
4✔
1560
      }
1561
    }
1✔
1562
    CliArgument currentArgument = arguments.current();
3✔
1563
    while (!currentArgument.isEnd()) {
3✔
1564
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1565
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1566
        if (currentArgument.isCompletion()) {
3✔
1567
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1568
          while (optionIterator.hasNext()) {
3✔
1569
            Property<?> option = optionIterator.next();
4✔
1570
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1571
            if (success) {
2✔
1572
              optionIterator.remove();
2✔
1573
              arguments.next();
3✔
1574
            }
1575
          }
1✔
1576
        } else {
1✔
1577
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1578
          if (option != null) {
2✔
1579
            arguments.next();
3✔
1580
            boolean removed = optionProperties.remove(option);
4✔
1581
            if (!removed) {
2!
1582
              option = null;
×
1583
            }
1584
          }
1585
          if (option == null) {
2✔
1586
            LOG.trace("No such option was found.");
3✔
1587
            return;
1✔
1588
          }
1589
        }
1✔
1590
      } else {
1591
        if (currentValueProperty != null) {
2✔
1592
          boolean success = currentValueProperty.apply(arguments, this, cmd, collector);
7✔
1593
          if (!success) {
2✔
1594
            LOG.trace("Completion cannot match any further.");
3✔
1595
            return;
1✔
1596
          }
1597
          if (!currentValueProperty.isMultiValued()) {
3✔
1598
            currentValueProperty = nextValueProperty(valueIterator, arguments);
5✔
1599
          }
1600
        } else {
1✔
1601
          LOG.trace("No value left for completion.");
3✔
1602
          return;
1✔
1603
        }
1604
      }
1605
      currentArgument = arguments.current();
4✔
1606
    }
1607
  }
1✔
1608

1609
  /**
1610
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1611
   *     {@link CliArguments#copy() copy} as needed.
1612
   * @param cmd the potential {@link Commandlet} to match.
1613
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1614
   */
1615
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1616

1617
    LOG.trace("Trying to match arguments to commandlet {}", cmd.getName());
5✔
1618
    CliArgument currentArgument = arguments.current();
3✔
1619
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1620
    Property<?> property = null;
2✔
1621
    if (propertyIterator.hasNext()) {
3!
1622
      property = propertyIterator.next();
4✔
1623
    }
1624
    while (!currentArgument.isEnd()) {
3✔
1625
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1626
      Property<?> currentProperty = property;
2✔
1627
      if (!arguments.isEndOptions()) {
3✔
1628
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1629
        if (option != null) {
2✔
1630
          currentProperty = option;
3✔
1631
        } else {
1632
          boolean allowDashedValue = (property != null && property.isValue() && property.isMultiValued());
10!
1633
          boolean allowKeywordOption = (currentProperty instanceof KeywordProperty keywordProperty) && keywordProperty.matches(currentArgument.getKey());
15!
1634
          if (!allowDashedValue && !allowKeywordOption && currentArgument.isOption()) {
7!
1635
            ValidationState state = new ValidationState(null);
5✔
1636
            state.addInvalidOption(currentArgument.getKey());
4✔
1637
            state.addErrorMessage("Invalid option \"" + currentArgument.getKey() + "\"");
5✔
1638
            return state;
2✔
1639
          }
1640
        }
1641
      }
1642
      if (currentProperty == null) {
2!
1643
        LOG.trace("No option or next value found");
×
1644
        ValidationState state = new ValidationState(null);
×
1645
        state.addErrorMessage("No matching property found");
×
1646
        return state;
×
1647
      }
1648
      LOG.trace("Next property candidate to match argument is {}", currentProperty);
4✔
1649
      if (currentProperty == property) {
3✔
1650
        if (!property.isMultiValued()) {
3✔
1651
          if (propertyIterator.hasNext()) {
3✔
1652
            property = propertyIterator.next();
5✔
1653
          } else {
1654
            property = null;
2✔
1655
          }
1656
        }
1657
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1658
          arguments.endOptions();
2✔
1659
        }
1660
      }
1661
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1662
      if (!matches) {
2✔
1663
        String invalidValue = currentProperty.getLastInvalidValue();
3✔
1664
        if (invalidValue != null) {
2!
1665
          ValidationState state = new ValidationState(null);
5✔
1666
          state.addInvalidArgument(invalidValue, currentProperty.getNameOrAlias());
5✔
1667
          state.addErrorMessage(
4✔
1668
              "Invalid CLI argument '" + invalidValue + "' for property '" + currentProperty.getNameOrAlias() + "' of commandlet '" + cmd.getName() + "'");
4✔
1669
          currentProperty.clearLastInvalidValue();
2✔
1670
          return state;
2✔
1671
        }
1672
        ValidationState state = new ValidationState(null);
×
1673
        state.addErrorMessage("No matching property found");
×
1674
        return state;
×
1675
      }
1676
      currentArgument = arguments.current();
3✔
1677
    }
1✔
1678
    return ValidationResultValid.get();
2✔
1679
  }
1680

1681
  @Override
1682
  public Path findBash() {
1683
    if (this.bash != null) {
3✔
1684
      return this.bash;
3✔
1685
    }
1686
    Path bashPath = findBashOnBashPath();
3✔
1687
    if (bashPath == null) {
2✔
1688
      bashPath = findBashInPath();
3✔
1689
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1690
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1691
        if (bashPath == null) {
2!
1692
          bashPath = findBashInWindowsRegistry();
3✔
1693
        }
1694
      }
1695
    }
1696
    if (bashPath == null) {
2✔
1697
      LOG.error("No bash executable could be found on your system.");
4✔
1698
    } else {
1699
      this.bash = bashPath;
3✔
1700
    }
1701
    return bashPath;
2✔
1702
  }
1703

1704
  private Path findBashOnBashPath() {
1705
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1706
    Path bash;
1707
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1708
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1709
    if (bashVariable != null) {
2✔
1710
      bash = Path.of(bashVariable);
5✔
1711
      if (Files.exists(bash)) {
5✔
1712
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1713
        return bash;
2✔
1714
      } else {
1715
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1716
        return null;
2✔
1717
      }
1718
    } else {
1719
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1720
      return null;
2✔
1721
    }
1722
  }
1723

1724
  /**
1725
   * @param path the path to check.
1726
   * @param toIgnore the String sequence which needs to be checked and ignored.
1727
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1728
   */
1729
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1730
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1731
    return !s.contains(toIgnore);
7!
1732
  }
1733

1734
  /**
1735
   * Tries to find the bash.exe within the PATH environment variable.
1736
   *
1737
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1738
   */
1739
  private Path findBashInPath() {
1740
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1741
    Path bash;
1742
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1743
    if (pathVariableName != null) {
2!
1744
      Path plainBash = Path.of(BASH);
5✔
1745
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1746
          "\\windows\\system32");
1747
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1748
      bash = bashPath.toAbsolutePath();
3✔
1749
      if (bashPath.equals(plainBash)) {
4✔
1750
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1751
        bash = null;
3✔
1752
      } else {
1753
        if (Files.exists(bashPath)) {
5!
1754
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1755
        } else {
1756
          bash = null;
×
1757
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1758
        }
1759
      }
1760
    } else {
1✔
1761
      bash = null;
×
1762
      // this should never happen...
1763
      LOG.error("PATH environment variable was not found");
×
1764
    }
1765
    return bash;
2✔
1766
  }
1767

1768
  /**
1769
   * Tries to find the bash.exe within the Windows registry.
1770
   *
1771
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1772
   */
1773
  protected Path findBashInWindowsRegistry() {
1774
    LOG.trace("Trying to find bash in Windows registry");
×
1775
    // If not found in the default location, try the registry query
1776
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1777
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1778
    for (String bashVariant : bashVariants) {
×
1779
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1780
      for (String registryKey : registryKeys) {
×
1781
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1782
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1783
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1784

1785
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1786
        if (path != null) {
×
1787
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1788
          if (Files.exists(bashPath)) {
×
1789
            LOG.debug("Found bash at: {}", bashPath);
×
1790
            return bashPath;
×
1791
          } else {
1792
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1793
            return null;
×
1794
          }
1795
        } else {
1796
          LOG.info("No bash executable could be found in the Windows registry.");
×
1797
        }
1798
      }
1799
    }
1800
    // no bash found
1801
    return null;
×
1802
  }
1803

1804
  private Path findBashOnWindowsDefaultGitPath() {
1805
    // Check if Git Bash exists in the default location
1806
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1807
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1808
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1809
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1810
      return defaultPath;
×
1811
    }
1812
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1813
    return null;
2✔
1814
  }
1815

1816
  @Override
1817
  public WindowsPathSyntax getPathSyntax() {
1818

1819
    return this.pathSyntax;
3✔
1820
  }
1821

1822
  /**
1823
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1824
   */
1825
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1826

1827
    this.pathSyntax = pathSyntax;
3✔
1828
  }
1✔
1829

1830
  /**
1831
   * @return the {@link IdeStartContextImpl}.
1832
   */
1833
  public IdeStartContextImpl getStartContext() {
1834

1835
    return startContext;
3✔
1836
  }
1837

1838
  /**
1839
   * @return the {@link WindowsHelper}.
1840
   */
1841
  public final WindowsHelper getWindowsHelper() {
1842

1843
    if (this.windowsHelper == null) {
3✔
1844
      this.windowsHelper = createWindowsHelper();
4✔
1845
    }
1846
    return this.windowsHelper;
3✔
1847
  }
1848

1849
  /**
1850
   * @return the new {@link WindowsHelper} instance.
1851
   */
1852
  protected WindowsHelper createWindowsHelper() {
1853

1854
    return new WindowsHelperImpl(this);
×
1855
  }
1856

1857
  /**
1858
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1859
   */
1860
  public void reload() {
1861

1862
    this.variables = null;
3✔
1863
    this.customToolRepository = null;
3✔
1864
  }
1✔
1865

1866
  @Override
1867
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1868

1869
    assert (Files.isDirectory(installationPath));
6!
1870
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1871
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1872
  }
1✔
1873

1874
  /*
1875
   * @param home the IDE_HOME directory.
1876
   * @param workspace the name of the active workspace folder.
1877
   */
1878
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1879

1880
  }
1881

1882
  /**
1883
   * Returns the default git path on Windows. Required to be overwritten in tests.
1884
   *
1885
   * @return default path to git on Windows.
1886
   */
1887
  public String getDefaultWindowsGitPath() {
1888
    return DEFAULT_WINDOWS_GIT_PATH;
×
1889
  }
1890

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

© 2026 Coveralls, Inc