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

devonfw / IDEasy / 25505184832

07 May 2026 03:23PM UTC coverage: 70.917% (+0.2%) from 70.741%
25505184832

Pull #1858

github

web-flow
Merge f96fc2947 into fd215c395
Pull Request #1858: #1457: Improve CLI error messages with suggestions

4523 of 7036 branches covered (64.28%)

Branch coverage included in aggregate %.

11527 of 15596 relevant lines covered (73.91%)

3.13 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")) && !isSettingsRepositorySymlinkOrJunction()) {
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 isSettingsRepositorySymlinkOrJunction() {
640

641
    Path settingsPath = getSettingsPath();
3✔
642
    if (settingsPath == null) {
2!
643
      return false;
×
644
    }
645
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
646
  }
647

648
  @Override
649
  public Path getSettingsCommitIdPath() {
650

651
    return this.settingsCommitIdPath;
3✔
652
  }
653

654
  @Override
655
  public Path getConfPath() {
656

657
    return this.confPath;
3✔
658
  }
659

660
  @Override
661
  public Path getSoftwarePath() {
662

663
    if (this.ideHome == null) {
3✔
664
      return null;
2✔
665
    }
666
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
667
  }
668

669
  @Override
670
  public Path getSoftwareExtraPath() {
671

672
    Path softwarePath = getSoftwarePath();
3✔
673
    if (softwarePath == null) {
2!
674
      return null;
×
675
    }
676
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
677
  }
678

679
  @Override
680
  public Path getSoftwareRepositoryPath() {
681

682
    Path idePath = getIdePath();
3✔
683
    if (idePath == null) {
2!
684
      return null;
×
685
    }
686
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
687
  }
688

689
  @Override
690
  public Path getPluginsPath() {
691

692
    return this.pluginsPath;
3✔
693
  }
694

695
  @Override
696
  public String getWorkspaceName() {
697

698
    return this.workspaceName;
3✔
699
  }
700

701
  @Override
702
  public Path getWorkspacesBasePath() {
703

704
    return this.workspacesBasePath;
3✔
705
  }
706

707
  @Override
708
  public Path getWorkspacePath() {
709

710
    return this.workspacePath;
3✔
711
  }
712

713
  @Override
714
  public Path getWorkspacePath(String workspace) {
715

716
    if (this.workspacesBasePath == null) {
3!
717
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
718
    }
719
    return this.workspacesBasePath.resolve(workspace);
5✔
720
  }
721

722
  @Override
723
  public Path getDownloadPath() {
724

725
    return this.downloadPath;
3✔
726
  }
727

728
  @Override
729
  public Path getUrlsPath() {
730

731
    Path idePath = getIdePath();
3✔
732
    if (idePath == null) {
2!
733
      return null;
×
734
    }
735
    return idePath.resolve(FOLDER_URLS);
4✔
736
  }
737

738
  @Override
739
  public Path getToolRepositoryPath() {
740

741
    Path idePath = getIdePath();
3✔
742
    if (idePath == null) {
2!
743
      return null;
×
744
    }
745
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
746
  }
747

748
  @Override
749
  public SystemPath getPath() {
750

751
    return this.path;
3✔
752
  }
753

754
  @Override
755
  public EnvironmentVariables getVariables() {
756

757
    if (this.variables == null) {
3✔
758
      this.variables = createVariables();
4✔
759
    }
760
    return this.variables;
3✔
761
  }
762

763
  @Override
764
  public UrlMetadata getUrls() {
765

766
    if (this.urlMetadata == null) {
3✔
767
      if (!isTest()) {
3!
768
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
769
      }
770
      this.urlMetadata = new UrlMetadata(this);
6✔
771
    }
772
    return this.urlMetadata;
3✔
773
  }
774

775
  @Override
776
  public boolean isQuietMode() {
777

778
    return this.startContext.isQuietMode();
4✔
779
  }
780

781
  @Override
782
  public boolean isBatchMode() {
783

784
    return this.startContext.isBatchMode();
4✔
785
  }
786

787
  @Override
788
  public boolean isForceMode() {
789

790
    return this.startContext.isForceMode();
4✔
791
  }
792

793
  @Override
794
  public boolean isForcePull() {
795

796
    return this.startContext.isForcePull();
4✔
797
  }
798

799
  @Override
800
  public boolean isForcePlugins() {
801

802
    return this.startContext.isForcePlugins();
4✔
803
  }
804

805
  @Override
806
  public boolean isForceRepositories() {
807

808
    return this.startContext.isForceRepositories();
4✔
809
  }
810

811
  @Override
812
  public boolean isOfflineMode() {
813

814
    return this.startContext.isOfflineMode();
4✔
815
  }
816

817
  @Override
818
  public boolean isPrivacyMode() {
819
    return this.startContext.isPrivacyMode();
4✔
820
  }
821

822
  @Override
823
  public boolean isSkipUpdatesMode() {
824

825
    return this.startContext.isSkipUpdatesMode();
4✔
826
  }
827

828
  @Override
829
  public boolean isNoColorsMode() {
830

831
    return this.startContext.isNoColorsMode();
×
832
  }
833

834
  @Override
835
  public NetworkStatus getNetworkStatus() {
836

837
    if (this.networkStatus == null) {
×
838
      this.networkStatus = new NetworkStatusImpl(this);
×
839
    }
840
    return this.networkStatus;
×
841
  }
842

843
  @Override
844
  public Locale getLocale() {
845

846
    Locale locale = this.startContext.getLocale();
4✔
847
    if (locale == null) {
2✔
848
      locale = Locale.getDefault();
2✔
849
    }
850
    return locale;
2✔
851
  }
852

853
  @Override
854
  public DirectoryMerger getWorkspaceMerger() {
855

856
    if (this.workspaceMerger == null) {
3✔
857
      this.workspaceMerger = new DirectoryMerger(this);
6✔
858
    }
859
    return this.workspaceMerger;
3✔
860
  }
861

862
  /**
863
   * @return the default execution directory in which a command process is executed.
864
   */
865
  @Override
866
  public Path getDefaultExecutionDirectory() {
867

868
    return this.defaultExecutionDirectory;
×
869
  }
870

871
  /**
872
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
873
   */
874
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
875

876
    if (defaultExecutionDirectory != null) {
×
877
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
878
    }
879
  }
×
880

881
  @Override
882
  public GitContext getGitContext() {
883

884
    return new GitContextImpl(this);
×
885
  }
886

887
  @Override
888
  public ProcessContext newProcess() {
889

890
    ProcessContext processContext = createProcessContext();
3✔
891
    if (this.defaultExecutionDirectory != null) {
3!
892
      processContext.directory(this.defaultExecutionDirectory);
×
893
    }
894
    return processContext;
2✔
895
  }
896

897
  @Override
898
  public IdeSystem getSystem() {
899

900
    if (this.system == null) {
×
901
      this.system = new IdeSystemImpl();
×
902
    }
903
    return this.system;
×
904
  }
905

906
  /**
907
   * @return a new instance of {@link ProcessContext}.
908
   * @see #newProcess()
909
   */
910
  protected ProcessContext createProcessContext() {
911

912
    return new ProcessContextImpl(this);
×
913
  }
914

915
  @Override
916
  public IdeLogLevel getLogLevelConsole() {
917

918
    return this.startContext.getLogLevelConsole();
×
919
  }
920

921
  @Override
922
  public IdeLogLevel getLogLevelLogger() {
923

924
    return this.startContext.getLogLevelLogger();
×
925
  }
926

927
  @Override
928
  public IdeLogListener getLogListener() {
929

930
    return this.startContext.getLogListener();
×
931
  }
932

933
  @Override
934
  public void logIdeHomeAndRootStatus() {
935
    if (this.ideRoot != null) {
3!
936
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
×
937
    }
938
    if (this.ideHome == null) {
3✔
939
      LOG.warn(getMessageNotInsideIdeProject());
5✔
940
    } else {
941
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
942
    }
943
  }
1✔
944

945
  @Override
946
  public String formatArgument(Object argument) {
947

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

968
  /**
969
   * @param path the sensitive {@link Path} to
970
   * @param replacement the replacement to mask the {@link Path} in log output.
971
   */
972
  protected void initializePrivacyMap(Path path, String replacement) {
973

974
    if (path == null) {
×
975
      return;
×
976
    }
977
    if (this.systemInfo.isWindows()) {
×
978
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
979
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
980
    } else {
981
      this.privacyMap.put(path.toString(), replacement);
×
982
    }
983
  }
×
984

985
  /**
986
   * Resets the privacy map in case fundamental values have changed.
987
   */
988
  private void resetPrivacyMap() {
989

990
    this.privacyMap.clear();
3✔
991
  }
1✔
992

993

994
  @Override
995
  public String askForInput(String message, String defaultValue) {
996

997
    while (true) {
998
      if (!message.isBlank()) {
3!
999
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
1000
      }
1001
      if (isBatchMode()) {
3!
1002
        if (isForceMode()) {
×
1003
          return defaultValue;
×
1004
        } else {
1005
          throw new CliAbortException();
×
1006
        }
1007
      }
1008
      String input = readLine().trim();
4✔
1009
      if (!input.isEmpty()) {
3!
1010
        return input;
2✔
1011
      } else {
1012
        if (defaultValue != null) {
×
1013
          return defaultValue;
×
1014
        }
1015
      }
1016
    }
×
1017
  }
1018

1019
  @Override
1020
  public <O> O question(O[] options, String question, Object... args) {
1021

1022
    assert (options.length > 0);
4!
1023
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1024
    return displayOptionsAndGetAnswer(options);
4✔
1025
  }
1026

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

1064
  private static String computeOptionKey(String option) {
1065
    String key = option;
2✔
1066
    int index = -1;
2✔
1067
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1068
      int currentIndex = key.indexOf(c);
4✔
1069
      if (currentIndex != -1) {
3✔
1070
        if ((index == -1) || (currentIndex < index)) {
3!
1071
          index = currentIndex;
2✔
1072
        }
1073
      }
1074
    }
1075
    if (index > 0) {
2✔
1076
      key = key.substring(0, index).trim();
6✔
1077
    }
1078
    return key;
2✔
1079
  }
1080

1081
  /**
1082
   * @return the input from the end-user (e.g. read from the console).
1083
   */
1084
  protected abstract String readLine();
1085

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

1088
    O duplicate = mapping.put(key, option);
5✔
1089
    if (duplicate != null) {
2!
1090
      throw new IllegalArgumentException("Duplicated option " + key);
×
1091
    }
1092
  }
1✔
1093

1094
  @Override
1095
  public Step getCurrentStep() {
1096

1097
    return this.currentStep;
×
1098
  }
1099

1100
  @Override
1101
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1102

1103
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1104
    return this.currentStep;
3✔
1105
  }
1106

1107
  /**
1108
   * Internal method to end the running {@link Step}.
1109
   *
1110
   * @param step the current {@link Step} to end.
1111
   */
1112
  public void endStep(StepImpl step) {
1113

1114
    if (step == this.currentStep) {
4!
1115
      this.currentStep = this.currentStep.getParent();
6✔
1116
    } else {
1117
      String currentStepName = "null";
×
1118
      if (this.currentStep != null) {
×
1119
        currentStepName = this.currentStep.getName();
×
1120
      }
1121
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1122
    }
1123
  }
1✔
1124

1125
  /**
1126
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1127
   *
1128
   * @param arguments the {@link CliArgument}.
1129
   * @return the return code of the execution.
1130
   */
1131
  public int run(CliArguments arguments) {
1132

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

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

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

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

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

1236
    configureJavaUtilLogging(cmd);
3✔
1237
    this.startContext.activateLogging();
3✔
1238
  }
1✔
1239

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

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

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

1274
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1275

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

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

1321
  @Override
1322
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1323

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

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

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

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

1389

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

1412
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1413

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

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

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

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

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

1522
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1523

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

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

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

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

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

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

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

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

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

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

1786
  @Override
1787
  public WindowsPathSyntax getPathSyntax() {
1788

1789
    return this.pathSyntax;
3✔
1790
  }
1791

1792
  /**
1793
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1794
   */
1795
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1796

1797
    this.pathSyntax = pathSyntax;
3✔
1798
  }
1✔
1799

1800
  /**
1801
   * @return the {@link IdeStartContextImpl}.
1802
   */
1803
  public IdeStartContextImpl getStartContext() {
1804

1805
    return startContext;
3✔
1806
  }
1807

1808
  /**
1809
   * @return the {@link WindowsHelper}.
1810
   */
1811
  public final WindowsHelper getWindowsHelper() {
1812

1813
    if (this.windowsHelper == null) {
3✔
1814
      this.windowsHelper = createWindowsHelper();
4✔
1815
    }
1816
    return this.windowsHelper;
3✔
1817
  }
1818

1819
  /**
1820
   * @return the new {@link WindowsHelper} instance.
1821
   */
1822
  protected WindowsHelper createWindowsHelper() {
1823

1824
    return new WindowsHelperImpl(this);
×
1825
  }
1826

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

1832
    this.variables = null;
3✔
1833
    this.customToolRepository = null;
3✔
1834
  }
1✔
1835

1836
  @Override
1837
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1838

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

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

1850
  }
1851

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

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