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

devonfw / IDEasy / 27607731518

16 Jun 2026 09:24AM UTC coverage: 71.285% (+0.2%) from 71.124%
27607731518

push

github

web-flow
#1457: Improve CLI error messages with suggestions (#1858)

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

4676 of 7250 branches covered (64.5%)

Branch coverage included in aggregate %.

12056 of 16222 relevant lines covered (74.32%)

3.15 hits per line

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

66.67
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.Property;
70
import com.devonfw.tools.ide.step.Step;
71
import com.devonfw.tools.ide.step.StepImpl;
72
import com.devonfw.tools.ide.tool.custom.CustomToolRepository;
73
import com.devonfw.tools.ide.tool.custom.CustomToolRepositoryImpl;
74
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
75
import com.devonfw.tools.ide.tool.npm.NpmRepository;
76
import com.devonfw.tools.ide.tool.pip.PipRepository;
77
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
78
import com.devonfw.tools.ide.tool.repository.ToolRepository;
79
import com.devonfw.tools.ide.url.model.UrlMetadata;
80
import com.devonfw.tools.ide.util.DateTimeUtil;
81
import com.devonfw.tools.ide.util.PrivacyUtil;
82
import com.devonfw.tools.ide.validation.ValidationResult;
83
import com.devonfw.tools.ide.validation.ValidationResultValid;
84
import com.devonfw.tools.ide.validation.ValidationState;
85
import com.devonfw.tools.ide.variable.IdeVariables;
86
import com.devonfw.tools.ide.version.IdeVersion;
87
import com.devonfw.tools.ide.version.VersionIdentifier;
88

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

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

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

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

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

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

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

107
  private final IdeStartContextImpl startContext;
108

109
  private Path ideHome;
110

111
  private final Path ideRoot;
112

113
  private Path confPath;
114

115
  protected Path settingsPath;
116

117
  private Path settingsCommitIdPath;
118

119
  protected Path pluginsPath;
120

121
  private Path workspacePath;
122

123
  private Path workspacesBasePath;
124

125
  private String workspaceName;
126

127
  private Path cwd;
128

129
  private Path downloadPath;
130

131
  private Path userHome;
132

133
  private Path userHomeIde;
134

135
  private SystemPath path;
136

137
  private WindowsPathSyntax pathSyntax;
138

139
  private final SystemInfo systemInfo;
140

141
  private EnvironmentVariables variables;
142

143
  private final FileAccess fileAccess;
144

145
  protected CommandletManager commandletManager;
146

147
  protected ToolRepository defaultToolRepository;
148

149
  private CustomToolRepository customToolRepository;
150

151
  private MvnRepository mvnRepository;
152

153
  private NpmRepository npmRepository;
154

155
  private PipRepository pipRepository;
156

157
  private DirectoryMerger workspaceMerger;
158

159
  protected UrlMetadata urlMetadata;
160

161
  protected Path defaultExecutionDirectory;
162

163
  private StepImpl currentStep;
164

165
  private NetworkStatus networkStatus;
166

167
  protected IdeSystem system;
168

169
  private WindowsHelper windowsHelper;
170

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

173
  private Path bash;
174

175
  private boolean julConfigured;
176

177
  private Path logfile;
178

179
  private CliSuggester cliSuggester;
180

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

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

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

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

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

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

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

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

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

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

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

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

297
  private Path findIdeRoot(Path ideHomePath) {
298

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

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

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

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

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

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

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

399
  private String getMessageIdeHomeFound() {
400

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

408
  private String getMessageNotInsideIdeProject() {
409

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

413
  private String getMessageIdeRootNotFound() {
414

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

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

429
    return false;
×
430
  }
431

432
  protected SystemPath computeSystemPath() {
433

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

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

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

453
  private EnvironmentVariables createVariables() {
454

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

463
  protected AbstractEnvironmentVariables createSystemVariables() {
464

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

468
  @Override
469
  public SystemInfo getSystemInfo() {
470

471
    return this.systemInfo;
3✔
472
  }
473

474
  @Override
475
  public FileAccess getFileAccess() {
476

477
    return this.fileAccess;
3✔
478
  }
479

480
  @Override
481
  public CommandletManager getCommandletManager() {
482

483
    return this.commandletManager;
3✔
484
  }
485

486
  @Override
487
  public ToolRepository getDefaultToolRepository() {
488

489
    return this.defaultToolRepository;
3✔
490
  }
491

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

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

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

516
  @Override
517
  public CustomToolRepository getCustomToolRepository() {
518

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

525
  @Override
526
  public Path getIdeHome() {
527

528
    return this.ideHome;
3✔
529
  }
530

531
  @Override
532
  public String getProjectName() {
533

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

540
  @Override
541
  public VersionIdentifier getProjectVersion() {
542

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

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

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

564
  @Override
565
  public Path getIdeRoot() {
566

567
    return this.ideRoot;
3✔
568
  }
569

570
  @Override
571
  public Path getIdePath() {
572

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

580
  @Override
581
  public Path getCwd() {
582

583
    return this.cwd;
3✔
584
  }
585

586
  @Override
587
  public Path getTempPath() {
588

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

596
  @Override
597
  public Path getTempDownloadPath() {
598

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

606
  @Override
607
  public Path getUserHome() {
608

609
    return this.userHome;
3✔
610
  }
611

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

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

626
  @Override
627
  public Path getUserHomeIde() {
628

629
    return this.userHomeIde;
3✔
630
  }
631

632
  @Override
633
  public Path getSettingsPath() {
634

635
    return this.settingsPath;
3✔
636
  }
637

638
  @Override
639
  public Path getSettingsGitRepository() {
640

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

650
  @Override
651
  public boolean isSettingsCodeRepository() {
652

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

667
  @Override
668
  public Path getSettingsCommitIdPath() {
669

670
    return this.settingsCommitIdPath;
3✔
671
  }
672

673
  @Override
674
  public Path getConfPath() {
675

676
    return this.confPath;
3✔
677
  }
678

679
  @Override
680
  public Path getSoftwarePath() {
681

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

688
  @Override
689
  public Path getSoftwareExtraPath() {
690

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

698
  @Override
699
  public Path getSoftwareRepositoryPath() {
700

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

708
  @Override
709
  public Path getPluginsPath() {
710

711
    return this.pluginsPath;
3✔
712
  }
713

714
  @Override
715
  public String getWorkspaceName() {
716

717
    return this.workspaceName;
3✔
718
  }
719

720
  @Override
721
  public Path getWorkspacesBasePath() {
722

723
    return this.workspacesBasePath;
3✔
724
  }
725

726
  @Override
727
  public Path getWorkspacePath() {
728

729
    return this.workspacePath;
3✔
730
  }
731

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

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

741
  @Override
742
  public Path getDownloadPath() {
743

744
    return this.downloadPath;
3✔
745
  }
746

747
  @Override
748
  public Path getUrlsPath() {
749

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

757
  @Override
758
  public Path getToolRepositoryPath() {
759

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

767
  @Override
768
  public SystemPath getPath() {
769

770
    return this.path;
3✔
771
  }
772

773
  @Override
774
  public EnvironmentVariables getVariables() {
775

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

782
  @Override
783
  public UrlMetadata getUrls() {
784

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

794
  @Override
795
  public boolean isQuietMode() {
796

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

800
  @Override
801
  public boolean isBatchMode() {
802

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

806
  @Override
807
  public boolean isForceMode() {
808

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

812
  @Override
813
  public boolean isForcePull() {
814

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

818
  @Override
819
  public boolean isForcePlugins() {
820

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

824
  @Override
825
  public boolean isForceRepositories() {
826

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

830
  @Override
831
  public boolean isOfflineMode() {
832

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

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

841
  @Override
842
  public boolean isSkipUpdatesMode() {
843

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

847
  @Override
848
  public boolean isNoColorsMode() {
849

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

853
  @Override
854
  public NetworkStatus getNetworkStatus() {
855

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

862
  @Override
863
  public Locale getLocale() {
864

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

872
  @Override
873
  public DirectoryMerger getWorkspaceMerger() {
874

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

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

887
    return this.defaultExecutionDirectory;
×
888
  }
889

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

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

900
  @Override
901
  public GitContext getGitContext() {
902

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

906
  @Override
907
  public ProcessContext newProcess() {
908

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

916
  @Override
917
  public IdeSystem getSystem() {
918

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

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

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

934
  @Override
935
  public IdeLogLevel getLogLevelConsole() {
936

937
    return this.startContext.getLogLevelConsole();
×
938
  }
939

940
  @Override
941
  public IdeLogLevel getLogLevelLogger() {
942

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

946
  @Override
947
  public IdeLogListener getLogListener() {
948

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

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

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

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

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

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

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

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

1012

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

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

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

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

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

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

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

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

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

1113
  @Override
1114
  public Step getCurrentStep() {
1115

1116
    return this.currentStep;
×
1117
  }
1118

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

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

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

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

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

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

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

1222

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1391

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

1415
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1416

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

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

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

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

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

1525
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1526

1527
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1528
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1529
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1530
    List<Property<?>> properties = cmd.getProperties();
3✔
1531
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1532
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1533
    for (Property<?> property : properties) {
10✔
1534
      if (property.isOption()) {
3✔
1535
        optionProperties.add(property);
4✔
1536
      }
1537
    }
1✔
1538
    CliArgument currentArgument = arguments.current();
3✔
1539
    while (!currentArgument.isEnd()) {
3✔
1540
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1541
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1542
        if (currentArgument.isCompletion()) {
3✔
1543
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1544
          while (optionIterator.hasNext()) {
3✔
1545
            Property<?> option = optionIterator.next();
4✔
1546
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1547
            if (success) {
2✔
1548
              optionIterator.remove();
2✔
1549
              arguments.next();
3✔
1550
            }
1551
          }
1✔
1552
        } else {
1✔
1553
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1554
          if (option != null) {
2✔
1555
            arguments.next();
3✔
1556
            boolean removed = optionProperties.remove(option);
4✔
1557
            if (!removed) {
2!
1558
              option = null;
×
1559
            }
1560
          }
1561
          if (option == null) {
2✔
1562
            LOG.trace("No such option was found.");
3✔
1563
            return;
1✔
1564
          }
1565
        }
1✔
1566
      } else {
1567
        if (valueIterator.hasNext()) {
3✔
1568
          Property<?> valueProperty = valueIterator.next();
4✔
1569
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1570
          if (!success) {
2✔
1571
            LOG.trace("Completion cannot match any further.");
3✔
1572
            return;
1✔
1573
          }
1574
        } else {
1✔
1575
          LOG.trace("No value left for completion.");
3✔
1576
          return;
1✔
1577
        }
1578
      }
1579
      currentArgument = arguments.current();
4✔
1580
    }
1581
  }
1✔
1582

1583
  /**
1584
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1585
   *     {@link CliArguments#copy() copy} as needed.
1586
   * @param cmd the potential {@link Commandlet} to match.
1587
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1588
   */
1589
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1590

1591
    LOG.trace("Trying to match arguments to commandlet {}", cmd.getName());
5✔
1592
    CliArgument currentArgument = arguments.current();
3✔
1593
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1594
    Property<?> property = null;
2✔
1595
    if (propertyIterator.hasNext()) {
3!
1596
      property = propertyIterator.next();
4✔
1597
    }
1598
    while (!currentArgument.isEnd()) {
3✔
1599
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1600
      Property<?> currentProperty = property;
2✔
1601
      if (!arguments.isEndOptions()) {
3!
1602
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1603
        if (option != null) {
2✔
1604
          currentProperty = option;
3✔
1605
        } else {
1606
          boolean allowDashedValue = (property != null && property.isValue() && property.isMultiValued());
12!
1607
          if (!allowDashedValue && currentArgument.isOption()) {
5✔
1608
            ValidationState state = new ValidationState(null);
5✔
1609
            state.addInvalidOption(currentArgument.getKey());
4✔
1610
            state.addErrorMessage("Invalid option \"" + currentArgument.getKey() + "\"");
5✔
1611
            return state;
2✔
1612
          }
1613
        }
1614
      }
1615
      if (currentProperty == null) {
2!
1616
        LOG.trace("No option or next value found");
×
1617
        ValidationState state = new ValidationState(null);
×
1618
        state.addErrorMessage("No matching property found");
×
1619
        return state;
×
1620
      }
1621
      LOG.trace("Next property candidate to match argument is {}", currentProperty);
4✔
1622
      if (currentProperty == property) {
3✔
1623
        if (!property.isMultiValued()) {
3✔
1624
          if (propertyIterator.hasNext()) {
3✔
1625
            property = propertyIterator.next();
5✔
1626
          } else {
1627
            property = null;
2✔
1628
          }
1629
        }
1630
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1631
          arguments.stopSplitShortOptions();
2✔
1632
        }
1633
      }
1634
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1635
      if (!matches) {
2✔
1636
        String invalidValue = currentProperty.getLastInvalidValue();
3✔
1637
        if (invalidValue != null) {
2!
1638
          ValidationState state = new ValidationState(null);
5✔
1639
          state.addInvalidArgument(invalidValue, currentProperty.getNameOrAlias());
5✔
1640
          state.addErrorMessage(
4✔
1641
              "Invalid CLI argument '" + invalidValue + "' for property '" + currentProperty.getNameOrAlias() + "' of commandlet '" + cmd.getName() + "'");
4✔
1642
          currentProperty.clearLastInvalidValue();
2✔
1643
          return state;
2✔
1644
        }
1645
        ValidationState state = new ValidationState(null);
×
1646
        state.addErrorMessage("No matching property found");
×
1647
        return state;
×
1648
      }
1649
      currentArgument = arguments.current();
3✔
1650
    }
1✔
1651
    return ValidationResultValid.get();
2✔
1652
  }
1653

1654
  @Override
1655
  public Path findBash() {
1656
    if (this.bash != null) {
3✔
1657
      return this.bash;
3✔
1658
    }
1659
    Path bashPath = findBashOnBashPath();
3✔
1660
    if (bashPath == null) {
2✔
1661
      bashPath = findBashInPath();
3✔
1662
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1663
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1664
        if (bashPath == null) {
2!
1665
          bashPath = findBashInWindowsRegistry();
3✔
1666
        }
1667
      }
1668
    }
1669
    if (bashPath == null) {
2✔
1670
      LOG.error("No bash executable could be found on your system.");
4✔
1671
    } else {
1672
      this.bash = bashPath;
3✔
1673
    }
1674
    return bashPath;
2✔
1675
  }
1676

1677
  private Path findBashOnBashPath() {
1678
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1679
    Path bash;
1680
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1681
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1682
    if (bashVariable != null) {
2✔
1683
      bash = Path.of(bashVariable);
5✔
1684
      if (Files.exists(bash)) {
5✔
1685
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1686
        return bash;
2✔
1687
      } else {
1688
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1689
        return null;
2✔
1690
      }
1691
    } else {
1692
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1693
      return null;
2✔
1694
    }
1695
  }
1696

1697
  /**
1698
   * @param path the path to check.
1699
   * @param toIgnore the String sequence which needs to be checked and ignored.
1700
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1701
   */
1702
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1703
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1704
    return !s.contains(toIgnore);
7!
1705
  }
1706

1707
  /**
1708
   * Tries to find the bash.exe within the PATH environment variable.
1709
   *
1710
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1711
   */
1712
  private Path findBashInPath() {
1713
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1714
    Path bash;
1715
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1716
    if (pathVariableName != null) {
2!
1717
      Path plainBash = Path.of(BASH);
5✔
1718
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1719
          "\\windows\\system32");
1720
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1721
      bash = bashPath.toAbsolutePath();
3✔
1722
      if (bashPath.equals(plainBash)) {
4✔
1723
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1724
        bash = null;
3✔
1725
      } else {
1726
        if (Files.exists(bashPath)) {
5!
1727
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1728
        } else {
1729
          bash = null;
×
1730
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1731
        }
1732
      }
1733
    } else {
1✔
1734
      bash = null;
×
1735
      // this should never happen...
1736
      LOG.error("PATH environment variable was not found");
×
1737
    }
1738
    return bash;
2✔
1739
  }
1740

1741
  /**
1742
   * Tries to find the bash.exe within the Windows registry.
1743
   *
1744
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1745
   */
1746
  protected Path findBashInWindowsRegistry() {
1747
    LOG.trace("Trying to find bash in Windows registry");
×
1748
    // If not found in the default location, try the registry query
1749
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1750
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1751
    for (String bashVariant : bashVariants) {
×
1752
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1753
      for (String registryKey : registryKeys) {
×
1754
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1755
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1756
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1757

1758
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1759
        if (path != null) {
×
1760
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1761
          if (Files.exists(bashPath)) {
×
1762
            LOG.debug("Found bash at: {}", bashPath);
×
1763
            return bashPath;
×
1764
          } else {
1765
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1766
            return null;
×
1767
          }
1768
        } else {
1769
          LOG.info("No bash executable could be found in the Windows registry.");
×
1770
        }
1771
      }
1772
    }
1773
    // no bash found
1774
    return null;
×
1775
  }
1776

1777
  private Path findBashOnWindowsDefaultGitPath() {
1778
    // Check if Git Bash exists in the default location
1779
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1780
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1781
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1782
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1783
      return defaultPath;
×
1784
    }
1785
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1786
    return null;
2✔
1787
  }
1788

1789
  @Override
1790
  public WindowsPathSyntax getPathSyntax() {
1791

1792
    return this.pathSyntax;
3✔
1793
  }
1794

1795
  /**
1796
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1797
   */
1798
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1799

1800
    this.pathSyntax = pathSyntax;
3✔
1801
  }
1✔
1802

1803
  /**
1804
   * @return the {@link IdeStartContextImpl}.
1805
   */
1806
  public IdeStartContextImpl getStartContext() {
1807

1808
    return startContext;
3✔
1809
  }
1810

1811
  /**
1812
   * @return the {@link WindowsHelper}.
1813
   */
1814
  public final WindowsHelper getWindowsHelper() {
1815

1816
    if (this.windowsHelper == null) {
3✔
1817
      this.windowsHelper = createWindowsHelper();
4✔
1818
    }
1819
    return this.windowsHelper;
3✔
1820
  }
1821

1822
  /**
1823
   * @return the new {@link WindowsHelper} instance.
1824
   */
1825
  protected WindowsHelper createWindowsHelper() {
1826

1827
    return new WindowsHelperImpl(this);
×
1828
  }
1829

1830
  /**
1831
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1832
   */
1833
  public void reload() {
1834

1835
    this.variables = null;
3✔
1836
    this.customToolRepository = null;
3✔
1837
  }
1✔
1838

1839
  @Override
1840
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1841

1842
    assert (Files.isDirectory(installationPath));
6!
1843
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1844
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1845
  }
1✔
1846

1847
  /*
1848
   * @param home the IDE_HOME directory.
1849
   * @param workspace the name of the active workspace folder.
1850
   */
1851
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1852

1853
  }
1854

1855
  /**
1856
   * Returns the default git path on Windows. Required to be overwritten in tests.
1857
   *
1858
   * @return default path to git on Windows.
1859
   */
1860
  public String getDefaultWindowsGitPath() {
1861
    return DEFAULT_WINDOWS_GIT_PATH;
×
1862
  }
1863

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