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

devonfw / IDEasy / 26649937969

29 May 2026 04:43PM UTC coverage: 71.203% (+0.09%) from 71.114%
26649937969

Pull #1858

github

web-flow
Merge 5bf2c49fe into be13f96f5
Pull Request #1858: #1457: Improve CLI error messages with suggestions

4595 of 7148 branches covered (64.28%)

Branch coverage included in aggregate %.

11796 of 15872 relevant lines covered (74.32%)

3.15 hits per line

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

66.83
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 = this.userHome.resolve("Downloads/ide");
6✔
383
    resetPrivacyMap();
2✔
384
    this.path = computeSystemPath();
4✔
385
  }
1✔
386

387
  private String getMessageIdeHomeFound() {
388

389
    String wks = this.workspaceName;
3✔
390
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
391
      wks = "*".repeat(wks.length());
×
392
    }
393
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
394
  }
395

396
  private String getMessageNotInsideIdeProject() {
397

398
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
399
  }
400

401
  private String getMessageIdeRootNotFound() {
402

403
    String root = getSystem().getEnv("IDE_ROOT");
5✔
404
    if (root == null) {
2!
405
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
406
    } else {
407
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
408
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
409
    }
410
  }
411

412
  /**
413
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
414
   */
415
  public boolean isTest() {
416

417
    return false;
×
418
  }
419

420
  protected SystemPath computeSystemPath() {
421

422
    return new SystemPath(this);
×
423
  }
424

425
  /**
426
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
427
   *
428
   * @param dir the directory to check.
429
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
430
   */
431
  protected boolean isIdeHome(Path dir) {
432

433
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
434
      return false;
2✔
435
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
436
      return false;
×
437
    }
438
    return true;
2✔
439
  }
440

441
  private EnvironmentVariables createVariables() {
442

443
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
444
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
445
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
446
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
447
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
448
    return conf.resolved();
3✔
449
  }
450

451
  protected AbstractEnvironmentVariables createSystemVariables() {
452

453
    return EnvironmentVariables.ofSystem(this);
3✔
454
  }
455

456
  @Override
457
  public SystemInfo getSystemInfo() {
458

459
    return this.systemInfo;
3✔
460
  }
461

462
  @Override
463
  public FileAccess getFileAccess() {
464

465
    return this.fileAccess;
3✔
466
  }
467

468
  @Override
469
  public CommandletManager getCommandletManager() {
470

471
    return this.commandletManager;
3✔
472
  }
473

474
  @Override
475
  public ToolRepository getDefaultToolRepository() {
476

477
    return this.defaultToolRepository;
3✔
478
  }
479

480
  @Override
481
  public MvnRepository getMvnRepository() {
482
    if (this.mvnRepository == null) {
3✔
483
      this.mvnRepository = createMvnRepository();
4✔
484
    }
485
    return this.mvnRepository;
3✔
486
  }
487

488
  @Override
489
  public NpmRepository getNpmRepository() {
490
    if (this.npmRepository == null) {
3✔
491
      this.npmRepository = createNpmRepository();
4✔
492
    }
493
    return this.npmRepository;
3✔
494
  }
495

496
  @Override
497
  public PipRepository getPipRepository() {
498
    if (this.pipRepository == null) {
3✔
499
      this.pipRepository = createPipRepository();
4✔
500
    }
501
    return this.pipRepository;
3✔
502
  }
503

504
  @Override
505
  public CustomToolRepository getCustomToolRepository() {
506

507
    if (this.customToolRepository == null) {
3!
508
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
509
    }
510
    return this.customToolRepository;
3✔
511
  }
512

513
  @Override
514
  public Path getIdeHome() {
515

516
    return this.ideHome;
3✔
517
  }
518

519
  @Override
520
  public String getProjectName() {
521

522
    if (this.ideHome != null) {
3!
523
      return this.ideHome.getFileName().toString();
5✔
524
    }
525
    return "";
×
526
  }
527

528
  @Override
529
  public VersionIdentifier getProjectVersion() {
530

531
    if (this.ideHome != null) {
3!
532
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
533
      if (Files.exists(versionFile)) {
5✔
534
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
535
        return VersionIdentifier.of(version);
3✔
536
      }
537
    }
538
    return IdeMigrator.START_VERSION;
2✔
539
  }
540

541
  @Override
542
  public void setProjectVersion(VersionIdentifier version) {
543

544
    if (this.ideHome == null) {
3!
545
      throw new IllegalStateException("IDE_HOME not available!");
×
546
    }
547
    Objects.requireNonNull(version);
3✔
548
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
549
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
550
  }
1✔
551

552
  @Override
553
  public Path getIdeRoot() {
554

555
    return this.ideRoot;
3✔
556
  }
557

558
  @Override
559
  public Path getIdePath() {
560

561
    Path myIdeRoot = getIdeRoot();
3✔
562
    if (myIdeRoot == null) {
2✔
563
      return null;
2✔
564
    }
565
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
566
  }
567

568
  @Override
569
  public Path getCwd() {
570

571
    return this.cwd;
3✔
572
  }
573

574
  @Override
575
  public Path getTempPath() {
576

577
    Path idePath = getIdePath();
3✔
578
    if (idePath == null) {
2!
579
      return null;
×
580
    }
581
    return idePath.resolve("tmp");
4✔
582
  }
583

584
  @Override
585
  public Path getTempDownloadPath() {
586

587
    Path tmp = getTempPath();
3✔
588
    if (tmp == null) {
2!
589
      return null;
×
590
    }
591
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
592
  }
593

594
  @Override
595
  public Path getUserHome() {
596

597
    return this.userHome;
3✔
598
  }
599

600
  /**
601
   * This method should only be used for tests to mock user home.
602
   *
603
   * @param userHome the new value of {@link #getUserHome()}.
604
   */
605
  protected void setUserHome(Path userHome) {
606

607
    this.userHome = userHome;
3✔
608
    this.userHomeIde = userHome.resolve(FOLDER_DOT_IDE);
5✔
609
    this.downloadPath = userHome.resolve("Downloads/ide");
5✔
610
    this.variables = null;
3✔
611
    resetPrivacyMap();
2✔
612
  }
1✔
613

614
  @Override
615
  public Path getUserHomeIde() {
616

617
    return this.userHomeIde;
3✔
618
  }
619

620
  @Override
621
  public Path getSettingsPath() {
622

623
    return this.settingsPath;
3✔
624
  }
625

626
  @Override
627
  public Path getSettingsGitRepository() {
628

629
    Path settingsPath = getSettingsPath();
3✔
630
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
631
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsCodeRepository()) {
12!
632
      LOG.error("Settings repository exists but is not a git repository.");
3✔
633
      return null;
2✔
634
    }
635
    return settingsPath;
2✔
636
  }
637

638
  @Override
639
  public boolean isSettingsCodeRepository() {
640

641
    Path settingsPath = getSettingsPath();
3✔
642
    if (settingsPath != null) {
2!
643
      boolean settingsIsLink = Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
644
      if (settingsIsLink) {
2!
645
        Path realPath = getFileAccess().toRealPath(this.settingsPath);
×
646
        if (realPath != null) {
×
647
          return getGitContext().isGitRepo(realPath.getParent());
×
648
        }
649
        return true;
×
650
      }
651
    }
652
    return false;
2✔
653
  }
654

655
  @Override
656
  public Path getSettingsCommitIdPath() {
657

658
    return this.settingsCommitIdPath;
3✔
659
  }
660

661
  @Override
662
  public Path getConfPath() {
663

664
    return this.confPath;
3✔
665
  }
666

667
  @Override
668
  public Path getSoftwarePath() {
669

670
    if (this.ideHome == null) {
3✔
671
      return null;
2✔
672
    }
673
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
674
  }
675

676
  @Override
677
  public Path getSoftwareExtraPath() {
678

679
    Path softwarePath = getSoftwarePath();
3✔
680
    if (softwarePath == null) {
2!
681
      return null;
×
682
    }
683
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
684
  }
685

686
  @Override
687
  public Path getSoftwareRepositoryPath() {
688

689
    Path idePath = getIdePath();
3✔
690
    if (idePath == null) {
2!
691
      return null;
×
692
    }
693
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
694
  }
695

696
  @Override
697
  public Path getPluginsPath() {
698

699
    return this.pluginsPath;
3✔
700
  }
701

702
  @Override
703
  public String getWorkspaceName() {
704

705
    return this.workspaceName;
3✔
706
  }
707

708
  @Override
709
  public Path getWorkspacesBasePath() {
710

711
    return this.workspacesBasePath;
3✔
712
  }
713

714
  @Override
715
  public Path getWorkspacePath() {
716

717
    return this.workspacePath;
3✔
718
  }
719

720
  @Override
721
  public Path getWorkspacePath(String workspace) {
722

723
    if (this.workspacesBasePath == null) {
3!
724
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
725
    }
726
    return this.workspacesBasePath.resolve(workspace);
5✔
727
  }
728

729
  @Override
730
  public Path getDownloadPath() {
731

732
    return this.downloadPath;
3✔
733
  }
734

735
  @Override
736
  public Path getUrlsPath() {
737

738
    Path idePath = getIdePath();
3✔
739
    if (idePath == null) {
2!
740
      return null;
×
741
    }
742
    return idePath.resolve(FOLDER_URLS);
4✔
743
  }
744

745
  @Override
746
  public Path getToolRepositoryPath() {
747

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

755
  @Override
756
  public SystemPath getPath() {
757

758
    return this.path;
3✔
759
  }
760

761
  @Override
762
  public EnvironmentVariables getVariables() {
763

764
    if (this.variables == null) {
3✔
765
      this.variables = createVariables();
4✔
766
    }
767
    return this.variables;
3✔
768
  }
769

770
  @Override
771
  public UrlMetadata getUrls() {
772

773
    if (this.urlMetadata == null) {
3✔
774
      if (!isTest()) {
3!
775
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
776
      }
777
      this.urlMetadata = new UrlMetadata(this);
6✔
778
    }
779
    return this.urlMetadata;
3✔
780
  }
781

782
  @Override
783
  public boolean isQuietMode() {
784

785
    return this.startContext.isQuietMode();
4✔
786
  }
787

788
  @Override
789
  public boolean isBatchMode() {
790

791
    return this.startContext.isBatchMode();
4✔
792
  }
793

794
  @Override
795
  public boolean isForceMode() {
796

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

800
  @Override
801
  public boolean isForcePull() {
802

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

806
  @Override
807
  public boolean isForcePlugins() {
808

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

812
  @Override
813
  public boolean isForceRepositories() {
814

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

818
  @Override
819
  public boolean isOfflineMode() {
820

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

824
  @Override
825
  public boolean isPrivacyMode() {
826
    return this.startContext.isPrivacyMode();
4✔
827
  }
828

829
  @Override
830
  public boolean isSkipUpdatesMode() {
831

832
    return this.startContext.isSkipUpdatesMode();
4✔
833
  }
834

835
  @Override
836
  public boolean isNoColorsMode() {
837

838
    return this.startContext.isNoColorsMode();
×
839
  }
840

841
  @Override
842
  public NetworkStatus getNetworkStatus() {
843

844
    if (this.networkStatus == null) {
×
845
      this.networkStatus = new NetworkStatusImpl(this);
×
846
    }
847
    return this.networkStatus;
×
848
  }
849

850
  @Override
851
  public Locale getLocale() {
852

853
    Locale locale = this.startContext.getLocale();
4✔
854
    if (locale == null) {
2✔
855
      locale = Locale.getDefault();
2✔
856
    }
857
    return locale;
2✔
858
  }
859

860
  @Override
861
  public DirectoryMerger getWorkspaceMerger() {
862

863
    if (this.workspaceMerger == null) {
3✔
864
      this.workspaceMerger = new DirectoryMerger(this);
6✔
865
    }
866
    return this.workspaceMerger;
3✔
867
  }
868

869
  /**
870
   * @return the default execution directory in which a command process is executed.
871
   */
872
  @Override
873
  public Path getDefaultExecutionDirectory() {
874

875
    return this.defaultExecutionDirectory;
×
876
  }
877

878
  /**
879
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
880
   */
881
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
882

883
    if (defaultExecutionDirectory != null) {
×
884
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
885
    }
886
  }
×
887

888
  @Override
889
  public GitContext getGitContext() {
890

891
    return new GitContextImpl(this);
×
892
  }
893

894
  @Override
895
  public ProcessContext newProcess() {
896

897
    ProcessContext processContext = createProcessContext();
3✔
898
    if (this.defaultExecutionDirectory != null) {
3!
899
      processContext.directory(this.defaultExecutionDirectory);
×
900
    }
901
    return processContext;
2✔
902
  }
903

904
  @Override
905
  public IdeSystem getSystem() {
906

907
    if (this.system == null) {
×
908
      this.system = new IdeSystemImpl();
×
909
    }
910
    return this.system;
×
911
  }
912

913
  /**
914
   * @return a new instance of {@link ProcessContext}.
915
   * @see #newProcess()
916
   */
917
  protected ProcessContext createProcessContext() {
918

919
    return new ProcessContextImpl(this);
×
920
  }
921

922
  @Override
923
  public IdeLogLevel getLogLevelConsole() {
924

925
    return this.startContext.getLogLevelConsole();
×
926
  }
927

928
  @Override
929
  public IdeLogLevel getLogLevelLogger() {
930

931
    return this.startContext.getLogLevelLogger();
×
932
  }
933

934
  @Override
935
  public IdeLogListener getLogListener() {
936

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

940
  @Override
941
  public void logIdeHomeAndRootStatus() {
942
    if (this.ideRoot != null) {
3!
943
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
×
944
    }
945
    if (this.ideHome == null) {
3✔
946
      LOG.warn(getMessageNotInsideIdeProject());
5✔
947
    } else {
948
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
949
    }
950
  }
1✔
951

952
  @Override
953
  public String formatArgument(Object argument) {
954

955
    if (argument == null) {
2✔
956
      return null;
2✔
957
    }
958
    String result = argument.toString();
3✔
959
    if (isPrivacyMode()) {
3✔
960
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
961
        initializePrivacyMap(this.userHome, "~");
×
962
        String projectName = getProjectName();
×
963
        if (!projectName.isEmpty()) {
×
964
          this.privacyMap.put(projectName, "project");
×
965
        }
966
      }
967
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
968
        result = result.replace(entry.getKey(), entry.getValue());
×
969
      }
×
970
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
971
    }
972
    return result;
2✔
973
  }
974

975
  /**
976
   * @param path the sensitive {@link Path} to
977
   * @param replacement the replacement to mask the {@link Path} in log output.
978
   */
979
  protected void initializePrivacyMap(Path path, String replacement) {
980

981
    if (path == null) {
×
982
      return;
×
983
    }
984
    if (this.systemInfo.isWindows()) {
×
985
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
986
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
987
    } else {
988
      this.privacyMap.put(path.toString(), replacement);
×
989
    }
990
  }
×
991

992
  /**
993
   * Resets the privacy map in case fundamental values have changed.
994
   */
995
  private void resetPrivacyMap() {
996

997
    this.privacyMap.clear();
3✔
998
  }
1✔
999

1000

1001
  @Override
1002
  public String askForInput(String message, String defaultValue) {
1003

1004
    while (true) {
1005
      if (!message.isBlank()) {
3!
1006
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
1007
      }
1008
      if (isBatchMode()) {
3!
1009
        if (isForceMode()) {
×
1010
          return defaultValue;
×
1011
        } else {
1012
          throw new CliAbortException();
×
1013
        }
1014
      }
1015
      String input = readLine().trim();
4✔
1016
      if (!input.isEmpty()) {
3!
1017
        return input;
2✔
1018
      } else {
1019
        if (defaultValue != null) {
×
1020
          return defaultValue;
×
1021
        }
1022
      }
1023
    }
×
1024
  }
1025

1026
  @Override
1027
  public <O> O question(O[] options, String question, Object... args) {
1028

1029
    assert (options.length > 0);
4!
1030
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1031
    return displayOptionsAndGetAnswer(options);
4✔
1032
  }
1033

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

1071
  private static String computeOptionKey(String option) {
1072
    String key = option;
2✔
1073
    int index = -1;
2✔
1074
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1075
      int currentIndex = key.indexOf(c);
4✔
1076
      if (currentIndex != -1) {
3✔
1077
        if ((index == -1) || (currentIndex < index)) {
3!
1078
          index = currentIndex;
2✔
1079
        }
1080
      }
1081
    }
1082
    if (index > 0) {
2✔
1083
      key = key.substring(0, index).trim();
6✔
1084
    }
1085
    return key;
2✔
1086
  }
1087

1088
  /**
1089
   * @return the input from the end-user (e.g. read from the console).
1090
   */
1091
  protected abstract String readLine();
1092

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

1095
    O duplicate = mapping.put(key, option);
5✔
1096
    if (duplicate != null) {
2!
1097
      throw new IllegalArgumentException("Duplicated option " + key);
×
1098
    }
1099
  }
1✔
1100

1101
  @Override
1102
  public Step getCurrentStep() {
1103

1104
    return this.currentStep;
×
1105
  }
1106

1107
  @Override
1108
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1109

1110
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1111
    return this.currentStep;
3✔
1112
  }
1113

1114
  /**
1115
   * Internal method to end the running {@link Step}.
1116
   *
1117
   * @param step the current {@link Step} to end.
1118
   */
1119
  public void endStep(StepImpl step) {
1120

1121
    if (step == this.currentStep) {
4!
1122
      this.currentStep = this.currentStep.getParent();
6✔
1123
    } else {
1124
      String currentStepName = "null";
×
1125
      if (this.currentStep != null) {
×
1126
        currentStepName = this.currentStep.getName();
×
1127
      }
1128
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1129
    }
1130
  }
1✔
1131

1132
  /**
1133
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1134
   *
1135
   * @param arguments the {@link CliArgument}.
1136
   * @return the return code of the execution.
1137
   */
1138
  public int run(CliArguments arguments) {
1139

1140
    CliArgument current = arguments.current();
3✔
1141
    if (current.isStart()) {
3✔
1142
      arguments.next();
3✔
1143
      current = arguments.current();
3✔
1144
    }
1145
    assert (this.currentStep == null);
4!
1146
    boolean supressStepSuccess = false;
2✔
1147
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1148
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1149
    Commandlet cmd = null;
2✔
1150
    ValidationResult result = null;
2✔
1151
    try {
1152
      while (commandletIterator.hasNext()) {
3✔
1153
        cmd = commandletIterator.next();
4✔
1154
        result = applyAndRun(arguments.copy(), cmd);
6✔
1155
        if (result.isValid()) {
3✔
1156
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1157
          step.success();
2✔
1158
          return ProcessResult.SUCCESS;
4✔
1159
        }
1160
      }
1161
      activateLogging(cmd);
3✔
1162
      verifyIdeMinVersion(false);
3✔
1163
      String commandKey = current.getKey();
3✔
1164

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

1210
  /**
1211
   * Finds the {@link Commandlet} with the given name.
1212
   *
1213
   * @param name the name of the {@link Commandlet} to find.
1214
   * @return the {@link Commandlet} with the given name or {@code null} if no such {@link Commandlet} exists.
1215
   */
1216
  private Commandlet findCommandletByName(String name) {
1217
    if (name == null) {
2!
1218
      return null;
×
1219
    }
1220
    for (Commandlet c : this.commandletManager.getCommandlets()) {
12✔
1221
      if (name.equals(c.getName())) {
5✔
1222
        return c;
2✔
1223
      }
1224
    }
1✔
1225
    return null;
2✔
1226
  }
1227

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

1238
  /**
1239
   * Ensure the logging system is initialized.
1240
   */
1241
  private void activateLogging(Commandlet cmd) {
1242

1243
    configureJavaUtilLogging(cmd);
3✔
1244
    this.startContext.activateLogging();
3✔
1245
  }
1✔
1246

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

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

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

1281
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1282

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

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

1328
  @Override
1329
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1330

1331
    this.startContext.deactivateLogging(threshold);
4✔
1332
    lambda.run();
2✔
1333
    this.startContext.activateLogging();
3✔
1334
  }
1✔
1335

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

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

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

1396

1397
  /**
1398
   * When an update is available for the settings repository, we log a message to the console, reminding the user to run {@code ide update}. This method
1399
   * determines the correct message to log, depending on whether the settings repository is a symlink/junction, or not. Should the user already be running the
1400
   * appropriate {@code ide update} command, the message is suppressed to avoid confusion.
1401
   *
1402
   * @param cmd the {@link Commandlet}.
1403
   * @return {@code msg} to log to the console. {@code null} if the message is suppressed.
1404
   */
1405
  private String determineSettingsUpdateMessage(Commandlet cmd) {
1406
    boolean update = cmd instanceof UpdateCommandlet;
×
1407
    if (isSettingsCodeRepository()) {
×
1408
      if (update && (isForceMode() || isForcePull())) {
×
1409
        return null;
×
1410
      }
1411
      return "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1412
    } else {
1413
      if (update) {
×
1414
        return null;
×
1415
      }
1416
      return "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
×
1417
    }
1418
  }
1419

1420
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1421

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

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

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

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

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

1530
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1531

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

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

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

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

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

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

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

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

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

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

1794
  @Override
1795
  public WindowsPathSyntax getPathSyntax() {
1796

1797
    return this.pathSyntax;
3✔
1798
  }
1799

1800
  /**
1801
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1802
   */
1803
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1804

1805
    this.pathSyntax = pathSyntax;
3✔
1806
  }
1✔
1807

1808
  /**
1809
   * @return the {@link IdeStartContextImpl}.
1810
   */
1811
  public IdeStartContextImpl getStartContext() {
1812

1813
    return startContext;
3✔
1814
  }
1815

1816
  /**
1817
   * @return the {@link WindowsHelper}.
1818
   */
1819
  public final WindowsHelper getWindowsHelper() {
1820

1821
    if (this.windowsHelper == null) {
3✔
1822
      this.windowsHelper = createWindowsHelper();
4✔
1823
    }
1824
    return this.windowsHelper;
3✔
1825
  }
1826

1827
  /**
1828
   * @return the new {@link WindowsHelper} instance.
1829
   */
1830
  protected WindowsHelper createWindowsHelper() {
1831

1832
    return new WindowsHelperImpl(this);
×
1833
  }
1834

1835
  /**
1836
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1837
   */
1838
  public void reload() {
1839

1840
    this.variables = null;
3✔
1841
    this.customToolRepository = null;
3✔
1842
  }
1✔
1843

1844
  @Override
1845
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1846

1847
    assert (Files.isDirectory(installationPath));
6!
1848
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1849
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1850
  }
1✔
1851

1852
  /*
1853
   * @param home the IDE_HOME directory.
1854
   * @param workspace the name of the active workspace folder.
1855
   */
1856
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1857

1858
  }
1859

1860
  /**
1861
   * Returns the default git path on Windows. Required to be overwritten in tests.
1862
   *
1863
   * @return default path to git on Windows.
1864
   */
1865
  public String getDefaultWindowsGitPath() {
1866
    return DEFAULT_WINDOWS_GIT_PATH;
×
1867
  }
1868

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