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

devonfw / IDEasy / 27775254245

18 Jun 2026 04:49PM UTC coverage: 71.339% (+0.06%) from 71.279%
27775254245

push

github

web-flow
Fix ide -v and --version handling (#2049)

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

4696 of 7284 branches covered (64.47%)

Branch coverage included in aggregate %.

12110 of 16274 relevant lines covered (74.41%)

3.15 hits per line

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

67.48
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
    return displayOptionsAndGetAnswer(options);
4✔
1045
  }
1046

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

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

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

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

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

1114
  @Override
1115
  public Step getCurrentStep() {
1116

1117
    return this.currentStep;
×
1118
  }
1119

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

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

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

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

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

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

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

1223

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1392

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

1416
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1417

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1815
  @Override
1816
  public WindowsPathSyntax getPathSyntax() {
1817

1818
    return this.pathSyntax;
3✔
1819
  }
1820

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

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

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

1834
    return startContext;
3✔
1835
  }
1836

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

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

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

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

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

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

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

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

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

1879
  }
1880

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

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