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

devonfw / IDEasy / 28829551315

06 Jul 2026 11:10PM UTC coverage: 72.066% (+0.3%) from 71.786%
28829551315

push

github

web-flow
#1311: improve IDE_MIN_VERSION support (#2121)

Co-authored-by: vivu001 <duc.vu@tuta.com>
Co-authored-by: Duc Vu <43269393+vivu001@users.noreply.github.com>
Co-authored-by: Malte Brunnlieb <maybeec@users.noreply.github.com>

4856 of 7434 branches covered (65.32%)

Branch coverage included in aggregate %.

12434 of 16558 relevant lines covered (75.09%)

3.18 hits per line

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

67.8
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.url.model.UrlMetadata;
82
import com.devonfw.tools.ide.util.DateTimeUtil;
83
import com.devonfw.tools.ide.util.PrivacyUtil;
84
import com.devonfw.tools.ide.validation.ValidationResult;
85
import com.devonfw.tools.ide.validation.ValidationResultValid;
86
import com.devonfw.tools.ide.validation.ValidationState;
87
import com.devonfw.tools.ide.variable.IdeVariables;
88
import com.devonfw.tools.ide.version.IdeVersion;
89
import com.devonfw.tools.ide.version.VersionIdentifier;
90

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

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

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

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

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

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

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

109
  private final IdeStartContextImpl startContext;
110

111
  private Path ideHome;
112

113
  private final Path ideRoot;
114

115
  private Path confPath;
116

117
  protected Path settingsPath;
118

119
  private Path settingsCommitIdPath;
120

121
  protected Path pluginsPath;
122

123
  private Path workspacePath;
124

125
  private Path workspacesBasePath;
126

127
  private String workspaceName;
128

129
  private Path cwd;
130

131
  private Path downloadPath;
132

133
  private Path userHome;
134

135
  private Path userHomeIde;
136

137
  private SystemPath path;
138

139
  private WindowsPathSyntax pathSyntax;
140

141
  private final SystemInfo systemInfo;
142

143
  private EnvironmentVariables variables;
144

145
  private final FileAccess fileAccess;
146

147
  protected CommandletManager commandletManager;
148

149
  protected ToolRepository defaultToolRepository;
150

151
  private CustomToolRepository customToolRepository;
152

153
  private MvnRepository mvnRepository;
154

155
  private NpmRepository npmRepository;
156

157
  private PipRepository pipRepository;
158

159
  private DirectoryMerger workspaceMerger;
160

161
  protected UrlMetadata urlMetadata;
162

163
  protected Path defaultExecutionDirectory;
164

165
  private StepImpl currentStep;
166

167
  private NetworkStatus networkStatus;
168

169
  protected IdeSystem system;
170

171
  private WindowsHelper windowsHelper;
172

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

175
  private Path bash;
176

177
  private boolean julConfigured;
178

179
  private Path logfile;
180

181
  private CliSuggester cliSuggester;
182

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

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

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

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

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

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

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

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

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

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

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

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

299
  private Path findIdeRoot(Path ideHomePath) {
300

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

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

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

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

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

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

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

401
  private String getMessageIdeHomeFound() {
402

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

410
  private String getMessageNotInsideIdeProject() {
411

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

415
  private String getMessageIdeRootNotFound() {
416

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

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

431
    return false;
×
432
  }
433

434
  protected SystemPath computeSystemPath() {
435

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

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

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

455
  private EnvironmentVariables createVariables() {
456

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

465
  protected AbstractEnvironmentVariables createSystemVariables() {
466

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

470
  @Override
471
  public SystemInfo getSystemInfo() {
472

473
    return this.systemInfo;
3✔
474
  }
475

476
  @Override
477
  public FileAccess getFileAccess() {
478

479
    return this.fileAccess;
3✔
480
  }
481

482
  @Override
483
  public CommandletManager getCommandletManager() {
484

485
    return this.commandletManager;
3✔
486
  }
487

488
  @Override
489
  public ToolRepository getDefaultToolRepository() {
490

491
    return this.defaultToolRepository;
3✔
492
  }
493

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

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

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

518
  @Override
519
  public CustomToolRepository getCustomToolRepository() {
520

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

527
  @Override
528
  public Path getIdeHome() {
529

530
    return this.ideHome;
3✔
531
  }
532

533
  @Override
534
  public String getProjectName() {
535

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

542
  @Override
543
  public VersionIdentifier getProjectVersion() {
544

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

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

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

566
  @Override
567
  public Path getIdeRoot() {
568

569
    return this.ideRoot;
3✔
570
  }
571

572
  @Override
573
  public Path getIdePath() {
574

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

582
  @Override
583
  public Path getCwd() {
584

585
    return this.cwd;
3✔
586
  }
587

588
  @Override
589
  public Path getTempPath() {
590

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

598
  @Override
599
  public Path getTempDownloadPath() {
600

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

608
  @Override
609
  public Path getUserHome() {
610

611
    return this.userHome;
3✔
612
  }
613

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

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

628
  @Override
629
  public Path getUserHomeIde() {
630

631
    return this.userHomeIde;
3✔
632
  }
633

634
  @Override
635
  public Path getSettingsPath() {
636

637
    return this.settingsPath;
3✔
638
  }
639

640
  @Override
641
  public Path getSettingsGitRepository() {
642

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

652
  @Override
653
  public boolean isSettingsCodeRepository() {
654

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

669
  @Override
670
  public Path getSettingsCommitIdPath() {
671

672
    return this.settingsCommitIdPath;
3✔
673
  }
674

675
  @Override
676
  public Path getConfPath() {
677

678
    return this.confPath;
3✔
679
  }
680

681
  @Override
682
  public Path getSoftwarePath() {
683

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

690
  @Override
691
  public Path getSoftwareExtraPath() {
692

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

700
  @Override
701
  public Path getSoftwareRepositoryPath() {
702

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

710
  @Override
711
  public Path getPluginsPath() {
712

713
    return this.pluginsPath;
3✔
714
  }
715

716
  @Override
717
  public String getWorkspaceName() {
718

719
    return this.workspaceName;
3✔
720
  }
721

722
  @Override
723
  public Path getWorkspacesBasePath() {
724

725
    return this.workspacesBasePath;
3✔
726
  }
727

728
  @Override
729
  public Path getWorkspacePath() {
730

731
    return this.workspacePath;
3✔
732
  }
733

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

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

743
  @Override
744
  public Path getDownloadPath() {
745

746
    return this.downloadPath;
3✔
747
  }
748

749
  @Override
750
  public Path getUrlsPath() {
751

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

759
  @Override
760
  public Path getToolRepositoryPath() {
761

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

769
  @Override
770
  public SystemPath getPath() {
771

772
    return this.path;
3✔
773
  }
774

775
  @Override
776
  public EnvironmentVariables getVariables() {
777

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

784
  @Override
785
  public UrlMetadata getUrls() {
786

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

796
  @Override
797
  public boolean isQuietMode() {
798

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

802
  @Override
803
  public boolean isBatchMode() {
804

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

808
  @Override
809
  public boolean isForceMode() {
810

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

814
  @Override
815
  public boolean isForcePull() {
816

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

820
  @Override
821
  public boolean isForcePlugins() {
822

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

826
  @Override
827
  public boolean isForceRepositories() {
828

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

832
  @Override
833
  public boolean isOfflineMode() {
834

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

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

843
  @Override
844
  public boolean isSkipUpdatesMode() {
845

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

849
  @Override
850
  public boolean isNoColorsMode() {
851

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

855
  @Override
856
  public NetworkStatus getNetworkStatus() {
857

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

864
  @Override
865
  public Locale getLocale() {
866

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

874
  @Override
875
  public DirectoryMerger getWorkspaceMerger() {
876

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

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

889
    return this.defaultExecutionDirectory;
×
890
  }
891

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

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

902
  @Override
903
  public GitContext getGitContext() {
904

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

908
  @Override
909
  public ProcessContext newProcess() {
910

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

918
  @Override
919
  public IdeSystem getSystem() {
920

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

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

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

936
  @Override
937
  public IdeLogLevel getLogLevelConsole() {
938

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

942
  @Override
943
  public IdeLogLevel getLogLevelLogger() {
944

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

948
  @Override
949
  public IdeLogListener getLogListener() {
950

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

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

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

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

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

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

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

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

1014

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

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

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

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

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

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

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

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

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

1116
  @Override
1117
  public Step getCurrentStep() {
1118

1119
    return this.currentStep;
×
1120
  }
1121

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

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

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

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

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

1155
    CliArgument current = arguments.current();
3✔
1156
    if (current.isStart()) {
3✔
1157
      arguments.next();
3✔
1158
      current = arguments.current();
3✔
1159
    }
1160
    assert (this.currentStep == null);
4!
1161
    boolean supressStepSuccess = false;
2✔
1162
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1163
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1164
    Commandlet cmd = null;
2✔
1165
    ValidationResult result = null;
2✔
1166
    try {
1167
      while (commandletIterator.hasNext()) {
3✔
1168
        cmd = commandletIterator.next();
4✔
1169
        result = applyAndRun(arguments.copy(), cmd);
6✔
1170
        if (result.isValid()) {
3✔
1171
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1172
          step.success();
2✔
1173
          return ProcessResult.SUCCESS;
4✔
1174
        }
1175
      }
1176
      activateLogging(cmd);
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
          if (!(cmd instanceof UpgradeCommandlet)) {
3!
1364
            verifyIdeMinVersion(false);
3✔
1365
          }
1366
          Path settingsRepository = getSettingsGitRepository();
3✔
1367
          if (settingsRepository != null) {
2!
1368
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
8!
1369
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
4!
1370
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1371

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

1396

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

1420
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1421

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

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

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

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

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

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

1540
    if (!valueIterator.hasNext()) {
3✔
1541
      return null;
2✔
1542
    }
1543

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

1552
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1553

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

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

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

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

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

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

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

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

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

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

1820
  @Override
1821
  public WindowsPathSyntax getPathSyntax() {
1822

1823
    return this.pathSyntax;
3✔
1824
  }
1825

1826
  /**
1827
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1828
   */
1829
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1830

1831
    this.pathSyntax = pathSyntax;
3✔
1832
  }
1✔
1833

1834
  /**
1835
   * @return the {@link IdeStartContextImpl}.
1836
   */
1837
  public IdeStartContextImpl getStartContext() {
1838

1839
    return startContext;
3✔
1840
  }
1841

1842
  /**
1843
   * @return the {@link WindowsHelper}.
1844
   */
1845
  public final WindowsHelper getWindowsHelper() {
1846

1847
    if (this.windowsHelper == null) {
3✔
1848
      this.windowsHelper = createWindowsHelper();
4✔
1849
    }
1850
    return this.windowsHelper;
3✔
1851
  }
1852

1853
  /**
1854
   * @return the new {@link WindowsHelper} instance.
1855
   */
1856
  protected WindowsHelper createWindowsHelper() {
1857

1858
    return new WindowsHelperImpl(this);
×
1859
  }
1860

1861
  /**
1862
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1863
   */
1864
  public void reload() {
1865

1866
    this.variables = null;
3✔
1867
    this.customToolRepository = null;
3✔
1868
  }
1✔
1869

1870
  @Override
1871
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1872

1873
    assert (Files.isDirectory(installationPath));
6!
1874
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1875
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1876
  }
1✔
1877

1878
  /*
1879
   * @param home the IDE_HOME directory.
1880
   * @param workspace the name of the active workspace folder.
1881
   */
1882
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1883

1884
  }
1885

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

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