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

devonfw / IDEasy / 28156840430

25 Jun 2026 08:21AM UTC coverage: 71.244% (-0.1%) from 71.356%
28156840430

Pull #2064

github

web-flow
Merge 2a02a83e3 into 459b15501
Pull Request #2064: #2026: Create UvRepository and UvBasedCommandlet

4705 of 7306 branches covered (64.4%)

Branch coverage included in aggregate %.

12152 of 16355 relevant lines covered (74.3%)

3.14 hits per line

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

67.64
cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
1
package com.devonfw.tools.ide.context;
2

3
import static com.devonfw.tools.ide.variable.IdeVariables.IDE_MIN_VERSION;
4

5
import java.io.ByteArrayInputStream;
6
import java.io.ByteArrayOutputStream;
7
import java.io.IOException;
8
import java.nio.file.Files;
9
import java.nio.file.Path;
10
import java.time.LocalDateTime;
11
import java.util.ArrayList;
12
import java.util.HashMap;
13
import java.util.Iterator;
14
import java.util.List;
15
import java.util.Locale;
16
import java.util.Map;
17
import java.util.Map.Entry;
18
import java.util.Objects;
19
import java.util.Properties;
20
import java.util.function.Predicate;
21
import java.util.logging.FileHandler;
22
import java.util.logging.LogManager;
23
import java.util.logging.SimpleFormatter;
24

25
import org.slf4j.Logger;
26
import org.slf4j.LoggerFactory;
27

28
import com.devonfw.tools.ide.cli.CliAbortException;
29
import com.devonfw.tools.ide.cli.CliArgument;
30
import com.devonfw.tools.ide.cli.CliArguments;
31
import com.devonfw.tools.ide.cli.CliException;
32
import com.devonfw.tools.ide.cli.CliSuggester;
33
import com.devonfw.tools.ide.commandlet.Commandlet;
34
import com.devonfw.tools.ide.commandlet.CommandletManager;
35
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
36
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
37
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
38
import com.devonfw.tools.ide.commandlet.UpdateCommandlet;
39
import com.devonfw.tools.ide.common.SystemPath;
40
import com.devonfw.tools.ide.completion.CompletionCandidate;
41
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
42
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
43
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
44
import com.devonfw.tools.ide.environment.EnvironmentVariables;
45
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
46
import com.devonfw.tools.ide.environment.IdeSystem;
47
import com.devonfw.tools.ide.environment.IdeSystemImpl;
48
import com.devonfw.tools.ide.git.GitContext;
49
import com.devonfw.tools.ide.git.GitContextImpl;
50
import com.devonfw.tools.ide.git.GitUrl;
51
import com.devonfw.tools.ide.io.FileAccess;
52
import com.devonfw.tools.ide.io.FileAccessImpl;
53
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
54
import com.devonfw.tools.ide.log.IdeLogLevel;
55
import com.devonfw.tools.ide.log.IdeLogListener;
56
import com.devonfw.tools.ide.log.JulConsoleHandler;
57
import com.devonfw.tools.ide.merge.DirectoryMerger;
58
import com.devonfw.tools.ide.migration.IdeMigrator;
59
import com.devonfw.tools.ide.network.NetworkStatus;
60
import com.devonfw.tools.ide.network.NetworkStatusImpl;
61
import com.devonfw.tools.ide.os.SystemInfo;
62
import com.devonfw.tools.ide.os.SystemInfoImpl;
63
import com.devonfw.tools.ide.os.WindowsHelper;
64
import com.devonfw.tools.ide.os.WindowsHelperImpl;
65
import com.devonfw.tools.ide.os.WindowsPathSyntax;
66
import com.devonfw.tools.ide.process.ProcessContext;
67
import com.devonfw.tools.ide.process.ProcessContextImpl;
68
import com.devonfw.tools.ide.process.ProcessResult;
69
import com.devonfw.tools.ide.property.KeywordProperty;
70
import com.devonfw.tools.ide.property.Property;
71
import com.devonfw.tools.ide.step.Step;
72
import com.devonfw.tools.ide.step.StepImpl;
73
import com.devonfw.tools.ide.tool.custom.CustomToolRepository;
74
import com.devonfw.tools.ide.tool.custom.CustomToolRepositoryImpl;
75
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
76
import com.devonfw.tools.ide.tool.npm.NpmRepository;
77
import com.devonfw.tools.ide.tool.pip.PipRepository;
78
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
79
import com.devonfw.tools.ide.tool.repository.ToolRepository;
80
import com.devonfw.tools.ide.tool.uv.UvRepository;
81
import com.devonfw.tools.ide.url.model.UrlMetadata;
82
import com.devonfw.tools.ide.util.DateTimeUtil;
83
import com.devonfw.tools.ide.util.PrivacyUtil;
84
import com.devonfw.tools.ide.validation.ValidationResult;
85
import com.devonfw.tools.ide.validation.ValidationResultValid;
86
import com.devonfw.tools.ide.validation.ValidationState;
87
import com.devonfw.tools.ide.variable.IdeVariables;
88
import com.devonfw.tools.ide.version.IdeVersion;
89
import com.devonfw.tools.ide.version.VersionIdentifier;
90

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

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

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

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

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

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

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

109
  private final IdeStartContextImpl startContext;
110

111
  private Path ideHome;
112

113
  private final Path ideRoot;
114

115
  private Path confPath;
116

117
  protected Path settingsPath;
118

119
  private Path settingsCommitIdPath;
120

121
  protected Path pluginsPath;
122

123
  private Path workspacePath;
124

125
  private Path workspacesBasePath;
126

127
  private String workspaceName;
128

129
  private Path cwd;
130

131
  private Path downloadPath;
132

133
  private Path userHome;
134

135
  private Path userHomeIde;
136

137
  private SystemPath path;
138

139
  private WindowsPathSyntax pathSyntax;
140

141
  private final SystemInfo systemInfo;
142

143
  private EnvironmentVariables variables;
144

145
  private final FileAccess fileAccess;
146

147
  protected CommandletManager commandletManager;
148

149
  protected ToolRepository defaultToolRepository;
150

151
  private CustomToolRepository customToolRepository;
152

153
  private MvnRepository mvnRepository;
154

155
  private NpmRepository npmRepository;
156

157
  private PipRepository pipRepository;
158

159
  private UvRepository uvRepository;
160

161
  private DirectoryMerger workspaceMerger;
162

163
  protected UrlMetadata urlMetadata;
164

165
  protected Path defaultExecutionDirectory;
166

167
  private StepImpl currentStep;
168

169
  private NetworkStatus networkStatus;
170

171
  protected IdeSystem system;
172

173
  private WindowsHelper windowsHelper;
174

175
  private final Map<String, String> privacyMap;
176

177
  private Path bash;
178

179
  private boolean julConfigured;
180

181
  private Path logfile;
182

183
  private CliSuggester cliSuggester;
184

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

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

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

229
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
230

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

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

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

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

277
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
278
  }
279

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

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

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

301
  /**
302
   * @return a new {@link UvRepository}
303
   */
304
  protected UvRepository createUvRepository() {
305
    return new UvRepository(this);
5✔
306
  }
307

308
  private Path findIdeRoot(Path ideHomePath) {
309

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

327
  /**
328
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
329
   */
330
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
331

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

364
  @Override
365
  public void setCwd(Path userDir, String workspace, Path ideHome) {
366

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

398
  /**
399
   * On macOS, {@code ~/Downloads} is protected by the OS (TCC) and the CLI may not be allowed to delete it, so we put the cache under {@code ~/Library/Caches}
400
   * instead. Tests still use {@code ~/Downloads/ide} so existing fixtures keep working.
401
   */
402
  private Path computeDownloadPath(Path home) {
403

404
    if (!isTest() && this.systemInfo.isMac()) {
3!
405
      return home.resolve("Library/Caches/IDEasy/downloads");
×
406
    }
407
    return home.resolve("Downloads/ide");
4✔
408
  }
409

410
  private String getMessageIdeHomeFound() {
411

412
    String wks = this.workspaceName;
3✔
413
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
414
      wks = "*".repeat(wks.length());
×
415
    }
416
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
417
  }
418

419
  private String getMessageNotInsideIdeProject() {
420

421
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
422
  }
423

424
  private String getMessageIdeRootNotFound() {
425

426
    String root = getSystem().getEnv("IDE_ROOT");
5✔
427
    if (root == null) {
2!
428
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
429
    } else {
430
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
431
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
432
    }
433
  }
434

435
  /**
436
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
437
   */
438
  public boolean isTest() {
439

440
    return false;
×
441
  }
442

443
  protected SystemPath computeSystemPath() {
444

445
    return new SystemPath(this);
×
446
  }
447

448
  /**
449
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
450
   *
451
   * @param dir the directory to check.
452
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
453
   */
454
  protected boolean isIdeHome(Path dir) {
455

456
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
457
      return false;
2✔
458
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
459
      return false;
×
460
    }
461
    return true;
2✔
462
  }
463

464
  private EnvironmentVariables createVariables() {
465

466
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
467
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
468
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
469
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
470
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
471
    return conf.resolved();
3✔
472
  }
473

474
  protected AbstractEnvironmentVariables createSystemVariables() {
475

476
    return EnvironmentVariables.ofSystem(this);
3✔
477
  }
478

479
  @Override
480
  public SystemInfo getSystemInfo() {
481

482
    return this.systemInfo;
3✔
483
  }
484

485
  @Override
486
  public FileAccess getFileAccess() {
487

488
    return this.fileAccess;
3✔
489
  }
490

491
  @Override
492
  public CommandletManager getCommandletManager() {
493

494
    return this.commandletManager;
3✔
495
  }
496

497
  @Override
498
  public ToolRepository getDefaultToolRepository() {
499

500
    return this.defaultToolRepository;
3✔
501
  }
502

503
  @Override
504
  public MvnRepository getMvnRepository() {
505
    if (this.mvnRepository == null) {
3✔
506
      this.mvnRepository = createMvnRepository();
4✔
507
    }
508
    return this.mvnRepository;
3✔
509
  }
510

511
  @Override
512
  public NpmRepository getNpmRepository() {
513
    if (this.npmRepository == null) {
3✔
514
      this.npmRepository = createNpmRepository();
4✔
515
    }
516
    return this.npmRepository;
3✔
517
  }
518

519
  @Override
520
  public PipRepository getPipRepository() {
521
    if (this.pipRepository == null) {
3✔
522
      this.pipRepository = createPipRepository();
4✔
523
    }
524
    return this.pipRepository;
3✔
525
  }
526

527
  @Override
528
  public UvRepository getUvRepository() {
529
    if (this.uvRepository == null) {
3✔
530
      this.uvRepository = createUvRepository();
4✔
531
    }
532
    return this.uvRepository;
3✔
533
  }
534

535
  @Override
536
  public CustomToolRepository getCustomToolRepository() {
537

538
    if (this.customToolRepository == null) {
3!
539
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
540
    }
541
    return this.customToolRepository;
3✔
542
  }
543

544
  @Override
545
  public Path getIdeHome() {
546

547
    return this.ideHome;
3✔
548
  }
549

550
  @Override
551
  public String getProjectName() {
552

553
    if (this.ideHome != null) {
3!
554
      return this.ideHome.getFileName().toString();
5✔
555
    }
556
    return "";
×
557
  }
558

559
  @Override
560
  public VersionIdentifier getProjectVersion() {
561

562
    if (this.ideHome != null) {
3!
563
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
564
      if (Files.exists(versionFile)) {
5✔
565
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
566
        return VersionIdentifier.of(version);
3✔
567
      }
568
    }
569
    return IdeMigrator.START_VERSION;
2✔
570
  }
571

572
  @Override
573
  public void setProjectVersion(VersionIdentifier version) {
574

575
    if (this.ideHome == null) {
3!
576
      throw new IllegalStateException("IDE_HOME not available!");
×
577
    }
578
    Objects.requireNonNull(version);
3✔
579
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
580
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
581
  }
1✔
582

583
  @Override
584
  public Path getIdeRoot() {
585

586
    return this.ideRoot;
3✔
587
  }
588

589
  @Override
590
  public Path getIdePath() {
591

592
    Path myIdeRoot = getIdeRoot();
3✔
593
    if (myIdeRoot == null) {
2✔
594
      return null;
2✔
595
    }
596
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
597
  }
598

599
  @Override
600
  public Path getCwd() {
601

602
    return this.cwd;
3✔
603
  }
604

605
  @Override
606
  public Path getTempPath() {
607

608
    Path idePath = getIdePath();
3✔
609
    if (idePath == null) {
2!
610
      return null;
×
611
    }
612
    return idePath.resolve("tmp");
4✔
613
  }
614

615
  @Override
616
  public Path getTempDownloadPath() {
617

618
    Path tmp = getTempPath();
3✔
619
    if (tmp == null) {
2!
620
      return null;
×
621
    }
622
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
623
  }
624

625
  @Override
626
  public Path getUserHome() {
627

628
    return this.userHome;
3✔
629
  }
630

631
  /**
632
   * This method should only be used for tests to mock user home.
633
   *
634
   * @param userHome the new value of {@link #getUserHome()}.
635
   */
636
  protected void setUserHome(Path userHome) {
637

638
    this.userHome = userHome;
3✔
639
    this.userHomeIde = userHome.resolve(FOLDER_DOT_IDE);
5✔
640
    this.downloadPath = computeDownloadPath(userHome);
5✔
641
    this.variables = null;
3✔
642
    resetPrivacyMap();
2✔
643
  }
1✔
644

645
  @Override
646
  public Path getUserHomeIde() {
647

648
    return this.userHomeIde;
3✔
649
  }
650

651
  @Override
652
  public Path getSettingsPath() {
653

654
    return this.settingsPath;
3✔
655
  }
656

657
  @Override
658
  public Path getSettingsGitRepository() {
659

660
    Path settingsPath = getSettingsPath();
3✔
661
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
662
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsCodeRepository()) {
12!
663
      LOG.error("Settings repository exists but is not a git repository.");
3✔
664
      return null;
2✔
665
    }
666
    return settingsPath;
2✔
667
  }
668

669
  @Override
670
  public boolean isSettingsCodeRepository() {
671

672
    Path settingsPath = getSettingsPath();
3✔
673
    if (settingsPath != null) {
2!
674
      boolean settingsIsLink = Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
675
      if (settingsIsLink) {
2!
676
        Path realPath = getFileAccess().toRealPath(this.settingsPath);
×
677
        if (realPath != null) {
×
678
          return getGitContext().isGitRepo(realPath.getParent());
×
679
        }
680
        return true;
×
681
      }
682
    }
683
    return false;
2✔
684
  }
685

686
  @Override
687
  public Path getSettingsCommitIdPath() {
688

689
    return this.settingsCommitIdPath;
3✔
690
  }
691

692
  @Override
693
  public Path getConfPath() {
694

695
    return this.confPath;
3✔
696
  }
697

698
  @Override
699
  public Path getSoftwarePath() {
700

701
    if (this.ideHome == null) {
3✔
702
      return null;
2✔
703
    }
704
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
705
  }
706

707
  @Override
708
  public Path getSoftwareExtraPath() {
709

710
    Path softwarePath = getSoftwarePath();
3✔
711
    if (softwarePath == null) {
2✔
712
      return null;
2✔
713
    }
714
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
715
  }
716

717
  @Override
718
  public Path getSoftwareRepositoryPath() {
719

720
    Path idePath = getIdePath();
3✔
721
    if (idePath == null) {
2!
722
      return null;
×
723
    }
724
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
725
  }
726

727
  @Override
728
  public Path getPluginsPath() {
729

730
    return this.pluginsPath;
3✔
731
  }
732

733
  @Override
734
  public String getWorkspaceName() {
735

736
    return this.workspaceName;
3✔
737
  }
738

739
  @Override
740
  public Path getWorkspacesBasePath() {
741

742
    return this.workspacesBasePath;
3✔
743
  }
744

745
  @Override
746
  public Path getWorkspacePath() {
747

748
    return this.workspacePath;
3✔
749
  }
750

751
  @Override
752
  public Path getWorkspacePath(String workspace) {
753

754
    if (this.workspacesBasePath == null) {
3!
755
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
756
    }
757
    return this.workspacesBasePath.resolve(workspace);
5✔
758
  }
759

760
  @Override
761
  public Path getDownloadPath() {
762

763
    return this.downloadPath;
3✔
764
  }
765

766
  @Override
767
  public Path getUrlsPath() {
768

769
    Path idePath = getIdePath();
3✔
770
    if (idePath == null) {
2!
771
      return null;
×
772
    }
773
    return idePath.resolve(FOLDER_URLS);
4✔
774
  }
775

776
  @Override
777
  public Path getToolRepositoryPath() {
778

779
    Path idePath = getIdePath();
3✔
780
    if (idePath == null) {
2!
781
      return null;
×
782
    }
783
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
784
  }
785

786
  @Override
787
  public SystemPath getPath() {
788

789
    return this.path;
3✔
790
  }
791

792
  @Override
793
  public EnvironmentVariables getVariables() {
794

795
    if (this.variables == null) {
3✔
796
      this.variables = createVariables();
4✔
797
    }
798
    return this.variables;
3✔
799
  }
800

801
  @Override
802
  public UrlMetadata getUrls() {
803

804
    if (this.urlMetadata == null) {
3✔
805
      if (!isTest()) {
3!
806
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
807
      }
808
      this.urlMetadata = new UrlMetadata(this);
6✔
809
    }
810
    return this.urlMetadata;
3✔
811
  }
812

813
  @Override
814
  public boolean isQuietMode() {
815

816
    return this.startContext.isQuietMode();
4✔
817
  }
818

819
  @Override
820
  public boolean isBatchMode() {
821

822
    return this.startContext.isBatchMode();
4✔
823
  }
824

825
  @Override
826
  public boolean isForceMode() {
827

828
    return this.startContext.isForceMode();
4✔
829
  }
830

831
  @Override
832
  public boolean isForcePull() {
833

834
    return this.startContext.isForcePull();
4✔
835
  }
836

837
  @Override
838
  public boolean isForcePlugins() {
839

840
    return this.startContext.isForcePlugins();
4✔
841
  }
842

843
  @Override
844
  public boolean isForceRepositories() {
845

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

849
  @Override
850
  public boolean isOfflineMode() {
851

852
    return this.startContext.isOfflineMode();
4✔
853
  }
854

855
  @Override
856
  public boolean isPrivacyMode() {
857
    return this.startContext.isPrivacyMode();
4✔
858
  }
859

860
  @Override
861
  public boolean isSkipUpdatesMode() {
862

863
    return this.startContext.isSkipUpdatesMode();
4✔
864
  }
865

866
  @Override
867
  public boolean isNoColorsMode() {
868

869
    return this.startContext.isNoColorsMode();
×
870
  }
871

872
  @Override
873
  public NetworkStatus getNetworkStatus() {
874

875
    if (this.networkStatus == null) {
×
876
      this.networkStatus = new NetworkStatusImpl(this);
×
877
    }
878
    return this.networkStatus;
×
879
  }
880

881
  @Override
882
  public Locale getLocale() {
883

884
    Locale locale = this.startContext.getLocale();
4✔
885
    if (locale == null) {
2✔
886
      locale = Locale.getDefault();
2✔
887
    }
888
    return locale;
2✔
889
  }
890

891
  @Override
892
  public DirectoryMerger getWorkspaceMerger() {
893

894
    if (this.workspaceMerger == null) {
3✔
895
      this.workspaceMerger = new DirectoryMerger(this);
6✔
896
    }
897
    return this.workspaceMerger;
3✔
898
  }
899

900
  /**
901
   * @return the default execution directory in which a command process is executed.
902
   */
903
  @Override
904
  public Path getDefaultExecutionDirectory() {
905

906
    return this.defaultExecutionDirectory;
×
907
  }
908

909
  /**
910
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
911
   */
912
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
913

914
    if (defaultExecutionDirectory != null) {
×
915
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
916
    }
917
  }
×
918

919
  @Override
920
  public GitContext getGitContext() {
921

922
    return new GitContextImpl(this);
×
923
  }
924

925
  @Override
926
  public ProcessContext newProcess() {
927

928
    ProcessContext processContext = createProcessContext();
3✔
929
    if (this.defaultExecutionDirectory != null) {
3!
930
      processContext.directory(this.defaultExecutionDirectory);
×
931
    }
932
    return processContext;
2✔
933
  }
934

935
  @Override
936
  public IdeSystem getSystem() {
937

938
    if (this.system == null) {
×
939
      this.system = new IdeSystemImpl();
×
940
    }
941
    return this.system;
×
942
  }
943

944
  /**
945
   * @return a new instance of {@link ProcessContext}.
946
   * @see #newProcess()
947
   */
948
  protected ProcessContext createProcessContext() {
949

950
    return new ProcessContextImpl(this);
×
951
  }
952

953
  @Override
954
  public IdeLogLevel getLogLevelConsole() {
955

956
    return this.startContext.getLogLevelConsole();
4✔
957
  }
958

959
  @Override
960
  public IdeLogLevel getLogLevelLogger() {
961

962
    return this.startContext.getLogLevelLogger();
×
963
  }
964

965
  @Override
966
  public IdeLogListener getLogListener() {
967

968
    return this.startContext.getLogListener();
×
969
  }
970

971
  @Override
972
  public void logIdeHomeAndRootStatus() {
973
    if (this.ideRoot != null) {
3!
974
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
×
975
    }
976
    if (this.ideHome == null) {
3✔
977
      LOG.warn(getMessageNotInsideIdeProject());
5✔
978
    } else {
979
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
980
    }
981
  }
1✔
982

983
  @Override
984
  public String formatArgument(Object argument) {
985

986
    if (argument == null) {
2✔
987
      return null;
2✔
988
    }
989
    String result = argument.toString();
3✔
990
    if (isPrivacyMode()) {
3✔
991
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
992
        initializePrivacyMap(this.userHome, "~");
×
993
        String projectName = getProjectName();
×
994
        if (!projectName.isEmpty()) {
×
995
          this.privacyMap.put(projectName, "project");
×
996
        }
997
      }
998
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
999
        result = result.replace(entry.getKey(), entry.getValue());
×
1000
      }
×
1001
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
1002
    }
1003
    return result;
2✔
1004
  }
1005

1006
  /**
1007
   * @param path the sensitive {@link Path} to
1008
   * @param replacement the replacement to mask the {@link Path} in log output.
1009
   */
1010
  protected void initializePrivacyMap(Path path, String replacement) {
1011

1012
    if (path == null) {
×
1013
      return;
×
1014
    }
1015
    if (this.systemInfo.isWindows()) {
×
1016
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
1017
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
1018
    } else {
1019
      this.privacyMap.put(path.toString(), replacement);
×
1020
    }
1021
  }
×
1022

1023
  /**
1024
   * Resets the privacy map in case fundamental values have changed.
1025
   */
1026
  private void resetPrivacyMap() {
1027

1028
    this.privacyMap.clear();
3✔
1029
  }
1✔
1030

1031

1032
  @Override
1033
  public String askForInput(String message, String defaultValue) {
1034

1035
    while (true) {
1036
      if (!message.isBlank()) {
3!
1037
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
1038
      }
1039
      if (isBatchMode()) {
3!
1040
        if (isForceMode()) {
×
1041
          return defaultValue;
×
1042
        } else {
1043
          throw new CliAbortException();
×
1044
        }
1045
      }
1046
      String input = readLine().trim();
4✔
1047
      if (!input.isEmpty()) {
3!
1048
        return input;
2✔
1049
      } else {
1050
        if (defaultValue != null) {
×
1051
          return defaultValue;
×
1052
        }
1053
      }
1054
    }
×
1055
  }
1056

1057
  @Override
1058
  public <O> O question(O[] options, String question, Object... args) {
1059

1060
    assert (options.length > 0);
4!
1061
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1062
    return displayOptionsAndGetAnswer(options);
4✔
1063
  }
1064

1065
  private <O> O displayOptionsAndGetAnswer(O[] options) {
1066
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
1067
    int i = 0;
2✔
1068
    for (O option : options) {
16✔
1069
      i++;
1✔
1070
      String title = "" + option;
4✔
1071
      String key = computeOptionKey(title);
3✔
1072
      addMapping(mapping, key, option);
4✔
1073
      String numericKey = Integer.toString(i);
3✔
1074
      if (numericKey.equals(key)) {
4!
1075
        LOG.trace("Options should not be numeric: {}", key);
×
1076
      } else {
1077
        addMapping(mapping, numericKey, option);
4✔
1078
      }
1079
      IdeLogLevel.INTERACTION.log(LOG, "Option {}: {}", numericKey, title);
14✔
1080
    }
1081
    if (options.length == 1) {
4✔
1082
      mapping.put("", options[0]);
7✔
1083
    }
1084
    O option = null;
2✔
1085
    if (isBatchMode()) {
3!
1086
      if (isForceMode()) {
×
1087
        option = options[0];
×
1088
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
×
1089
      }
1090
    } else {
1091
      while (option == null) {
2✔
1092
        String answer = readLine();
3✔
1093
        option = mapping.get(answer);
4✔
1094
        if (option == null) {
2!
1095
          LOG.warn("Invalid answer: '{}' - please try again.", answer);
×
1096
        }
1097
      }
1✔
1098
    }
1099
    return option;
2✔
1100
  }
1101

1102
  private static String computeOptionKey(String option) {
1103
    String key = option;
2✔
1104
    int index = -1;
2✔
1105
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1106
      int currentIndex = key.indexOf(c);
4✔
1107
      if (currentIndex != -1) {
3✔
1108
        if ((index == -1) || (currentIndex < index)) {
3!
1109
          index = currentIndex;
2✔
1110
        }
1111
      }
1112
    }
1113
    if (index > 0) {
2✔
1114
      key = key.substring(0, index).trim();
6✔
1115
    }
1116
    return key;
2✔
1117
  }
1118

1119
  /**
1120
   * @return the input from the end-user (e.g. read from the console).
1121
   */
1122
  protected abstract String readLine();
1123

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

1126
    O duplicate = mapping.put(key, option);
5✔
1127
    if (duplicate != null) {
2!
1128
      throw new IllegalArgumentException("Duplicated option " + key);
×
1129
    }
1130
  }
1✔
1131

1132
  @Override
1133
  public Step getCurrentStep() {
1134

1135
    return this.currentStep;
×
1136
  }
1137

1138
  @Override
1139
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1140

1141
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1142
    return this.currentStep;
3✔
1143
  }
1144

1145
  /**
1146
   * Internal method to end the running {@link Step}.
1147
   *
1148
   * @param step the current {@link Step} to end.
1149
   */
1150
  public void endStep(StepImpl step) {
1151

1152
    if (step == this.currentStep) {
4!
1153
      this.currentStep = this.currentStep.getParent();
6✔
1154
    } else {
1155
      String currentStepName = "null";
×
1156
      if (this.currentStep != null) {
×
1157
        currentStepName = this.currentStep.getName();
×
1158
      }
1159
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1160
    }
1161
  }
1✔
1162

1163
  /**
1164
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1165
   *
1166
   * @param arguments the {@link CliArgument}.
1167
   * @return the return code of the execution.
1168
   */
1169
  public int run(CliArguments arguments) {
1170

1171
    CliArgument current = arguments.current();
3✔
1172
    if (current.isStart()) {
3✔
1173
      arguments.next();
3✔
1174
      current = arguments.current();
3✔
1175
    }
1176
    assert (this.currentStep == null);
4!
1177
    boolean supressStepSuccess = false;
2✔
1178
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1179
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1180
    Commandlet cmd = null;
2✔
1181
    ValidationResult result = null;
2✔
1182
    try {
1183
      while (commandletIterator.hasNext()) {
3✔
1184
        cmd = commandletIterator.next();
4✔
1185
        result = applyAndRun(arguments.copy(), cmd);
6✔
1186
        if (result.isValid()) {
3✔
1187
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1188
          step.success();
2✔
1189
          return ProcessResult.SUCCESS;
4✔
1190
        }
1191
      }
1192
      activateLogging(cmd);
3✔
1193
      verifyIdeMinVersion(false);
3✔
1194
      String commandKey = current.getKey();
3✔
1195

1196
      if (commandKey == null || commandKey.isBlank()) {
5!
1197
        return 0;
×
1198
      }
1199
      Commandlet commandletByName = this.commandletManager.getCommandlet(commandKey);
5✔
1200
      // Missing commandlet
1201
      if (commandletByName == null) {
2✔
1202
        if (getCliSuggester().isMissingCommandletHandled(commandKey, step)) {
6!
1203
          return 1;
4✔
1204
        }
1205
        return 0;
×
1206
      }
1207
      // Missing project context
1208
      if (getCliSuggester().isMissingProjectContextHandled(commandletByName, step)) {
6✔
1209
        return 1;
4✔
1210
      }
1211
      // Only validate options/arguments if same commandlet and proper type
1212
      if (cmd != commandletByName || !(result instanceof ValidationState validationState)) {
10!
1213
        return 0;
×
1214
      }
1215
      // Invalid option
1216
      if (getCliSuggester().isInvalidOptionHandled(validationState, commandletByName, step)) {
7✔
1217
        return 1;
4✔
1218
      }
1219
      // Invalid argument
1220
      if (getCliSuggester().isInvalidArgumentHandled(validationState, commandletByName)) {
6!
1221
        return 1;
4✔
1222
      }
1223
      LOG.error(result.getErrorMessage());
×
1224
      step.error("Invalid arguments: {}", current.getArgs());
×
1225
      IdeLogLevel.INTERACTION.log(LOG, "For additional details run ide help {}", cmd == null ? "" : cmd.getName());
×
1226
      return 1;
×
1227
    } catch (Throwable t) {
1✔
1228
      activateLogging(cmd);
3✔
1229
      step.error(t, true);
4✔
1230
      if (this.logfile != null) {
3!
1231
        System.err.println("Logfile can be found at " + this.logfile); // do not use logger
×
1232
      }
1233
      throw t;
2✔
1234
    } finally {
1235
      step.close();
2✔
1236
      assert (this.currentStep == null);
4!
1237
      step.logSummary(supressStepSuccess);
3✔
1238
    }
1239
  }
1240

1241

1242
  /**
1243
   * @return the {@link CliSuggester} for CLI suggestions.
1244
   */
1245
  private CliSuggester getCliSuggester() {
1246
    if (this.cliSuggester == null) {
3✔
1247
      this.cliSuggester = new CliSuggester(this);
6✔
1248
    }
1249
    return this.cliSuggester;
3✔
1250
  }
1251

1252
  /**
1253
   * Ensure the logging system is initialized.
1254
   */
1255
  private void activateLogging(Commandlet cmd) {
1256

1257
    configureJavaUtilLogging(cmd);
3✔
1258
    this.startContext.activateLogging();
3✔
1259
  }
1✔
1260

1261
  /**
1262
   * Configures the logging system (JUL).
1263
   *
1264
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1265
   */
1266
  public void configureJavaUtilLogging(Commandlet cmd) {
1267

1268
    if (this.julConfigured) {
3✔
1269
      return;
1✔
1270
    }
1271
    boolean writeLogfile = isWriteLogfile(cmd);
4✔
1272
    this.startContext.setWriteLogfile(writeLogfile);
4✔
1273
    Properties properties = createJavaUtilLoggingProperties(writeLogfile, cmd);
5✔
1274
    try {
1275
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1276
      properties.store(out, null);
4✔
1277
      out.flush();
2✔
1278
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1279
      LogManager.getLogManager().readConfiguration(in);
3✔
1280
      this.julConfigured = true;
3✔
1281
      this.startContext.activateLogging();
3✔
1282
    } catch (IOException e) {
×
1283
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1284
    }
1✔
1285
  }
1✔
1286

1287
  protected boolean isWriteLogfile(Commandlet cmd) {
1288
    if ((cmd == null) || !cmd.isWriteLogFile()) {
×
1289
      return false;
×
1290
    }
1291
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1292
    return Boolean.TRUE.equals(writeLogfile);
×
1293
  }
1294

1295
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1296

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

1322
  private Path createLogfilePath(Path idePath, Commandlet cmd) {
1323
    LocalDateTime now = LocalDateTime.now();
×
1324
    Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1325
    StringBuilder sb = new StringBuilder(32);
×
1326
    if (this.ideHome == null || ((cmd != null) && !cmd.isIdeHomeRequired())) {
×
1327
      sb.append("_ide-");
×
1328
    } else {
1329
      sb.append(this.ideHome.getFileName().toString());
×
1330
      sb.append('-');
×
1331
    }
1332
    sb.append("ide-");
×
1333
    if (cmd != null) {
×
1334
      sb.append(cmd.getName());
×
1335
      sb.append('-');
×
1336
    }
1337
    sb.append(DateTimeUtil.formatTime(now));
×
1338
    sb.append(".log");
×
1339
    return logsPath.resolve(sb.toString());
×
1340
  }
1341

1342
  @Override
1343
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1344

1345
    this.startContext.deactivateLogging(threshold);
4✔
1346
    lambda.run();
2✔
1347
    this.startContext.activateLogging();
3✔
1348
  }
1✔
1349

1350
  /**
1351
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1352
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1353
   *     {@link Commandlet} did not match and we have to try a different candidate).
1354
   */
1355
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1356

1357
    IdeLogLevel previousLogLevel = null;
2✔
1358
    cmd.reset();
2✔
1359
    ValidationResult result = apply(arguments, cmd);
5✔
1360
    if (result.isValid()) {
3✔
1361
      result = cmd.validate();
3✔
1362
    }
1363
    if (result.isValid()) {
3✔
1364
      LOG.debug("Running commandlet {}", cmd);
4✔
1365
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1366
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1367
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6✔
1368
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1369
      }
1370
      try {
1371
        if (cmd.isProcessableOutput()) {
3✔
1372
          if (!isLogLevelEnabled(IdeLogLevel.DEBUG)) {
4!
1373
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1374
            previousLogLevel = this.startContext.setLogLevelConsole(IdeLogLevel.PROCESSABLE);
×
1375
          }
1376
        } else {
1377
          if (cmd.isIdeHomeRequired()) {
3!
1378
            LOG.debug(getMessageIdeHomeFound());
4✔
1379
          }
1380
          Path settingsRepository = getSettingsGitRepository();
3✔
1381
          if (settingsRepository != null) {
2!
1382
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
8!
1383
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
4!
1384
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1385

1386
              // Inform the user that an update is available. The update message is suppressed if we are already running the update
1387
              String msg = determineSettingsUpdateMessage(cmd);
×
1388
              if (msg != null) {
×
1389
                IdeLogLevel.INTERACTION.log(LOG, msg);
×
1390
              }
1391
            }
1392
          }
1393
        }
1394
        boolean success = ensureLicenseAgreement(cmd);
4✔
1395
        if (!success) {
2!
1396
          return ValidationResultValid.get();
×
1397
        }
1398
        cmd.run();
2✔
1399
      } finally {
1400
        if (previousLogLevel != null) {
2!
1401
          this.startContext.setLogLevelConsole(previousLogLevel);
×
1402
        }
1403
      }
1✔
1404
    } else {
1405
      LOG.trace("Commandlet did not match");
3✔
1406
    }
1407
    return result;
2✔
1408
  }
1409

1410

1411
  /**
1412
   * 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
1413
   * 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
1414
   * appropriate {@code ide update} command, the message is suppressed to avoid confusion.
1415
   *
1416
   * @param cmd the {@link Commandlet}.
1417
   * @return {@code msg} to log to the console. {@code null} if the message is suppressed.
1418
   */
1419
  private String determineSettingsUpdateMessage(Commandlet cmd) {
1420
    boolean update = cmd instanceof UpdateCommandlet;
×
1421
    if (isSettingsCodeRepository()) {
×
1422
      if (update && (isForceMode() || isForcePull())) {
×
1423
        return null;
×
1424
      }
1425
      return "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1426
    } else {
1427
      if (update) {
×
1428
        return null;
×
1429
      }
1430
      return "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
×
1431
    }
1432
  }
1433

1434
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1435

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

1476
    sb.setLength(0);
×
1477
    LocalDateTime now = LocalDateTime.now();
×
1478
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1479
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1480
    try {
1481
      Files.writeString(licenseAgreement, sb);
×
1482
    } catch (Exception e) {
×
1483
      throw new RuntimeException("Failed to save license agreement!", e);
×
1484
    }
×
1485
    if (oldLogLevel != newLogLevel) {
×
1486
      this.startContext.setLogLevelConsole(oldLogLevel);
×
1487
    }
1488
    return true;
×
1489
  }
1490

1491
  @Override
1492
  public void verifyIdeMinVersion(boolean throwException) {
1493
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1494
    if (minVersion == null) {
2✔
1495
      return;
1✔
1496
    }
1497
    VersionIdentifier versionIdentifier = IdeVersion.getVersionIdentifier();
2✔
1498
    if (versionIdentifier.compareVersion(minVersion).isLess() && !IdeVersion.isUndefined()) {
7!
1499
      String message = String.format("Your version of IDEasy is currently %s\n"
13✔
1500
          + "However, this is too old as your project requires at latest version %s\n"
1501
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1502
          + "ide upgrade", versionIdentifier, minVersion);
1503
      if (throwException) {
2✔
1504
        throw new CliException(message);
5✔
1505
      } else {
1506
        LOG.warn(message);
3✔
1507
      }
1508
    }
1509
  }
1✔
1510

1511
  /**
1512
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1513
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1514
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1515
   */
1516
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1517

1518
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1519
    if (arguments.current().isStart()) {
4✔
1520
      arguments.next();
3✔
1521
    }
1522
    if (includeContextOptions) {
2✔
1523
      ContextCommandlet cc = new ContextCommandlet();
4✔
1524
      for (Property<?> property : cc.getProperties()) {
11✔
1525
        assert (property.isOption());
4!
1526
        property.apply(arguments, this, cc, collector);
7✔
1527
      }
1✔
1528
    }
1529
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1530
    CliArgument current = arguments.current();
3✔
1531
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1532
      collector.add(current.get(), null, null, null);
7✔
1533
    }
1534
    arguments.next();
3✔
1535
    while (commandletIterator.hasNext()) {
3✔
1536
      Commandlet cmd = commandletIterator.next();
4✔
1537
      if (!arguments.current().isEnd()) {
4✔
1538
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1539
      }
1540
    }
1✔
1541
    return collector.getSortedCandidates();
3✔
1542
  }
1543

1544
  /**
1545
   * Gets the next value property and applies its implicit end-options behavior if required.
1546
   *
1547
   * @param valueIterator the iterator over the commandlet value properties
1548
   * @param arguments the CLI arguments whose option parsing state may be updated
1549
   * @return the next value property or {@code null} if no further value property exists
1550
   */
1551
  private Property<?> nextValueProperty(Iterator<Property<?>> valueIterator, CliArguments arguments) {
1552

1553
    if (!valueIterator.hasNext()) {
3✔
1554
      return null;
2✔
1555
    }
1556

1557
    Property<?> valueProperty = valueIterator.next();
4✔
1558
    if (valueProperty.isEndOptions()) {
3✔
1559
      // Tool argument properties should accept values starting with "-" so stop option parsing here
1560
      arguments.endOptions();
2✔
1561
    }
1562
    return valueProperty;
2✔
1563
  }
1564

1565
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1566

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

1626
  /**
1627
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1628
   *     {@link CliArguments#copy() copy} as needed.
1629
   * @param cmd the potential {@link Commandlet} to match.
1630
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1631
   */
1632
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1633

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

1698
  @Override
1699
  public Path findBash() {
1700
    if (this.bash != null) {
3✔
1701
      return this.bash;
3✔
1702
    }
1703
    Path bashPath = findBashOnBashPath();
3✔
1704
    if (bashPath == null) {
2✔
1705
      bashPath = findBashInPath();
3✔
1706
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1707
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1708
        if (bashPath == null) {
2!
1709
          bashPath = findBashInWindowsRegistry();
3✔
1710
        }
1711
      }
1712
    }
1713
    if (bashPath == null) {
2✔
1714
      LOG.error("No bash executable could be found on your system.");
4✔
1715
    } else {
1716
      this.bash = bashPath;
3✔
1717
    }
1718
    return bashPath;
2✔
1719
  }
1720

1721
  private Path findBashOnBashPath() {
1722
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1723
    Path bash;
1724
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1725
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1726
    if (bashVariable != null) {
2✔
1727
      bash = Path.of(bashVariable);
5✔
1728
      if (Files.exists(bash)) {
5✔
1729
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1730
        return bash;
2✔
1731
      } else {
1732
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1733
        return null;
2✔
1734
      }
1735
    } else {
1736
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1737
      return null;
2✔
1738
    }
1739
  }
1740

1741
  /**
1742
   * @param path the path to check.
1743
   * @param toIgnore the String sequence which needs to be checked and ignored.
1744
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1745
   */
1746
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1747
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1748
    return !s.contains(toIgnore);
7!
1749
  }
1750

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

1785
  /**
1786
   * Tries to find the bash.exe within the Windows registry.
1787
   *
1788
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1789
   */
1790
  protected Path findBashInWindowsRegistry() {
1791
    LOG.trace("Trying to find bash in Windows registry");
×
1792
    // If not found in the default location, try the registry query
1793
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1794
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1795
    for (String bashVariant : bashVariants) {
×
1796
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1797
      for (String registryKey : registryKeys) {
×
1798
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1799
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1800
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1801

1802
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1803
        if (path != null) {
×
1804
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1805
          if (Files.exists(bashPath)) {
×
1806
            LOG.debug("Found bash at: {}", bashPath);
×
1807
            return bashPath;
×
1808
          } else {
1809
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1810
            return null;
×
1811
          }
1812
        } else {
1813
          LOG.info("No bash executable could be found in the Windows registry.");
×
1814
        }
1815
      }
1816
    }
1817
    // no bash found
1818
    return null;
×
1819
  }
1820

1821
  private Path findBashOnWindowsDefaultGitPath() {
1822
    // Check if Git Bash exists in the default location
1823
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1824
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1825
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1826
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1827
      return defaultPath;
×
1828
    }
1829
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1830
    return null;
2✔
1831
  }
1832

1833
  @Override
1834
  public WindowsPathSyntax getPathSyntax() {
1835

1836
    return this.pathSyntax;
3✔
1837
  }
1838

1839
  /**
1840
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1841
   */
1842
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1843

1844
    this.pathSyntax = pathSyntax;
3✔
1845
  }
1✔
1846

1847
  /**
1848
   * @return the {@link IdeStartContextImpl}.
1849
   */
1850
  public IdeStartContextImpl getStartContext() {
1851

1852
    return startContext;
3✔
1853
  }
1854

1855
  /**
1856
   * @return the {@link WindowsHelper}.
1857
   */
1858
  public final WindowsHelper getWindowsHelper() {
1859

1860
    if (this.windowsHelper == null) {
3✔
1861
      this.windowsHelper = createWindowsHelper();
4✔
1862
    }
1863
    return this.windowsHelper;
3✔
1864
  }
1865

1866
  /**
1867
   * @return the new {@link WindowsHelper} instance.
1868
   */
1869
  protected WindowsHelper createWindowsHelper() {
1870

1871
    return new WindowsHelperImpl(this);
×
1872
  }
1873

1874
  /**
1875
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1876
   */
1877
  public void reload() {
1878

1879
    this.variables = null;
3✔
1880
    this.customToolRepository = null;
3✔
1881
  }
1✔
1882

1883
  @Override
1884
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1885

1886
    assert (Files.isDirectory(installationPath));
6!
1887
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1888
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1889
  }
1✔
1890

1891
  /*
1892
   * @param home the IDE_HOME directory.
1893
   * @param workspace the name of the active workspace folder.
1894
   */
1895
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1896

1897
  }
1898

1899
  /**
1900
   * Returns the default git path on Windows. Required to be overwritten in tests.
1901
   *
1902
   * @return default path to git on Windows.
1903
   */
1904
  public String getDefaultWindowsGitPath() {
1905
    return DEFAULT_WINDOWS_GIT_PATH;
×
1906
  }
1907

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