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

devonfw / IDEasy / 15690064264

16 Jun 2025 07:27PM UTC coverage: 67.97% (+0.2%) from 67.746%
15690064264

Pull #1370

github

web-flow
Merge db8da133f into 7be183ce9
Pull Request #1370: #1369: Omit username from StatusCommandlet

3178 of 5084 branches covered (62.51%)

Branch coverage included in aggregate %.

8156 of 11591 relevant lines covered (70.36%)

3.08 hits per line

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

61.32
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.BufferedReader;
6
import java.io.InputStreamReader;
7
import java.net.URL;
8
import java.net.URLConnection;
9
import java.nio.file.Files;
10
import java.nio.file.Path;
11
import java.nio.file.Paths;
12
import java.time.LocalDateTime;
13
import java.util.ArrayList;
14
import java.util.HashMap;
15
import java.util.Iterator;
16
import java.util.List;
17
import java.util.Locale;
18
import java.util.Map;
19
import java.util.Objects;
20

21
import com.devonfw.tools.ide.cli.CliAbortException;
22
import com.devonfw.tools.ide.cli.CliArgument;
23
import com.devonfw.tools.ide.cli.CliArguments;
24
import com.devonfw.tools.ide.cli.CliException;
25
import com.devonfw.tools.ide.commandlet.Commandlet;
26
import com.devonfw.tools.ide.commandlet.CommandletManager;
27
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
28
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
29
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
30
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
31
import com.devonfw.tools.ide.common.SystemPath;
32
import com.devonfw.tools.ide.completion.CompletionCandidate;
33
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
34
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
35
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
36
import com.devonfw.tools.ide.environment.EnvironmentVariables;
37
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
38
import com.devonfw.tools.ide.environment.IdeSystem;
39
import com.devonfw.tools.ide.environment.IdeSystemImpl;
40
import com.devonfw.tools.ide.git.GitContext;
41
import com.devonfw.tools.ide.git.GitContextImpl;
42
import com.devonfw.tools.ide.git.GitUrl;
43
import com.devonfw.tools.ide.io.FileAccess;
44
import com.devonfw.tools.ide.io.FileAccessImpl;
45
import com.devonfw.tools.ide.log.IdeLogLevel;
46
import com.devonfw.tools.ide.log.IdeLogger;
47
import com.devonfw.tools.ide.log.IdeSubLogger;
48
import com.devonfw.tools.ide.merge.DirectoryMerger;
49
import com.devonfw.tools.ide.migration.IdeMigrator;
50
import com.devonfw.tools.ide.network.NetworkProxy;
51
import com.devonfw.tools.ide.os.SystemInfo;
52
import com.devonfw.tools.ide.os.SystemInfoImpl;
53
import com.devonfw.tools.ide.os.WindowsHelper;
54
import com.devonfw.tools.ide.os.WindowsHelperImpl;
55
import com.devonfw.tools.ide.os.WindowsPathSyntax;
56
import com.devonfw.tools.ide.process.ProcessContext;
57
import com.devonfw.tools.ide.process.ProcessContextImpl;
58
import com.devonfw.tools.ide.process.ProcessResult;
59
import com.devonfw.tools.ide.property.Property;
60
import com.devonfw.tools.ide.step.Step;
61
import com.devonfw.tools.ide.step.StepImpl;
62
import com.devonfw.tools.ide.tool.repository.CustomToolRepository;
63
import com.devonfw.tools.ide.tool.repository.CustomToolRepositoryImpl;
64
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
65
import com.devonfw.tools.ide.tool.repository.MavenRepository;
66
import com.devonfw.tools.ide.tool.repository.ToolRepository;
67
import com.devonfw.tools.ide.url.model.UrlMetadata;
68
import com.devonfw.tools.ide.util.DateTimeUtil;
69
import com.devonfw.tools.ide.validation.ValidationResult;
70
import com.devonfw.tools.ide.validation.ValidationResultValid;
71
import com.devonfw.tools.ide.validation.ValidationState;
72
import com.devonfw.tools.ide.variable.IdeVariables;
73
import com.devonfw.tools.ide.version.IdeVersion;
74
import com.devonfw.tools.ide.version.VersionIdentifier;
75

76
/**
77
 * Abstract base implementation of {@link IdeContext}.
78
 */
79
public abstract class AbstractIdeContext implements IdeContext {
80

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

83
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
84
  private static final String IDE_HOME_PLACEHOLDER = "$IDE_HOME";
85
  private static final String IDE_ROOT_PLACEHOLDER = "$IDE_ROOT";
86
  private static final String USER_HOME_PLACEHOLDER = "~";
87

88
  private final IdeStartContextImpl startContext;
89

90
  private Path ideHome;
91

92
  private final Path ideRoot;
93

94
  private Path confPath;
95

96
  protected Path settingsPath;
97

98
  private Path settingsCommitIdPath;
99

100
  protected Path pluginsPath;
101

102
  private Path workspacePath;
103

104
  private String workspaceName;
105

106
  private Path cwd;
107

108
  private Path downloadPath;
109

110
  protected Path userHome;
111

112
  private Path userHomeIde;
113

114
  private SystemPath path;
115

116
  private WindowsPathSyntax pathSyntax;
117

118
  private final SystemInfo systemInfo;
119

120
  private EnvironmentVariables variables;
121

122
  private final FileAccess fileAccess;
123

124
  protected CommandletManager commandletManager;
125

126
  protected ToolRepository defaultToolRepository;
127

128
  private CustomToolRepository customToolRepository;
129

130
  private MavenRepository mavenRepository;
131

132
  private DirectoryMerger workspaceMerger;
133

134
  protected UrlMetadata urlMetadata;
135

136
  protected Path defaultExecutionDirectory;
137

138
  private StepImpl currentStep;
139

140
  protected Boolean online;
141

142
  protected IdeSystem system;
143

144
  private NetworkProxy networkProxy;
145

146
  private WindowsHelper windowsHelper;
147

148
  /**
149
   * The constructor.
150
   *
151
   * @param startContext the {@link IdeLogger}.
152
   * @param workingDirectory the optional {@link Path} to current working directory.
153
   */
154
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
155

156
    super();
2✔
157
    this.startContext = startContext;
3✔
158
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
159
    this.commandletManager = new CommandletManagerImpl(this);
6✔
160
    this.fileAccess = new FileAccessImpl(this);
6✔
161
    String workspace = WORKSPACE_MAIN;
2✔
162
    if (workingDirectory == null) {
2!
163
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
164
    } else {
165
      workingDirectory = workingDirectory.toAbsolutePath();
3✔
166
    }
167
    this.cwd = workingDirectory;
3✔
168
    // detect IDE_HOME and WORKSPACE
169
    Path currentDir = workingDirectory;
2✔
170
    String name1 = "";
2✔
171
    String name2 = "";
2✔
172
    Path ideRootPath = getIdeRootPathFromEnv();
3✔
173
    while (currentDir != null) {
2✔
174
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
175
      if (isIdeHome(currentDir)) {
4✔
176
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
177
          workspace = name2;
3✔
178
        }
179
        break;
180
      }
181
      name2 = name1;
2✔
182
      int nameCount = currentDir.getNameCount();
3✔
183
      if (nameCount >= 1) {
3✔
184
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
185
      }
186
      currentDir = currentDir.getParent();
3✔
187
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
188
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
189
        currentDir = null;
×
190
      }
191
    }
1✔
192

193
    // detection completed, initializing variables
194
    this.ideRoot = findIdeRoot(currentDir);
5✔
195

196
    setCwd(workingDirectory, workspace, currentDir);
5✔
197

198
    if (this.ideRoot != null) {
3✔
199
      Path tempDownloadPath = getTempDownloadPath();
3✔
200
      if (Files.isDirectory(tempDownloadPath)) {
6✔
201
        // TODO delete all files older than 1 day here...
202
      } else {
203
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
204
      }
205
    }
206

207
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
208
    this.mavenRepository = new MavenRepository(this);
6✔
209
  }
1✔
210

211
  private Path findIdeRoot(Path ideHomePath) {
212

213
    Path ideRootPath = null;
2✔
214
    if (ideHomePath != null) {
2✔
215
      ideRootPath = ideHomePath.getParent();
4✔
216
    } else if (!isTest()) {
3!
217
      ideRootPath = getIdeRootPathFromEnv();
×
218
    }
219
    return ideRootPath;
2✔
220
  }
221

222
  /**
223
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
224
   */
225
  protected Path getIdeRootPathFromEnv() {
226

227
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
228
    if (root != null) {
×
229
      Path rootPath = Path.of(root);
×
230
      if (Files.isDirectory(rootPath)) {
×
231
        return rootPath;
×
232
      }
233
    }
234
    return null;
×
235
  }
236

237
  @Override
238
  public void setCwd(Path userDir, String workspace, Path ideHome) {
239

240
    this.cwd = userDir;
3✔
241
    this.workspaceName = workspace;
3✔
242
    this.ideHome = ideHome;
3✔
243
    if (ideHome == null) {
2✔
244
      this.workspacePath = null;
3✔
245
      this.confPath = null;
3✔
246
      this.settingsPath = null;
3✔
247
      this.pluginsPath = null;
4✔
248
    } else {
249
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
250
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
251
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
252
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
253
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
254
    }
255
    if (isTest()) {
3!
256
      // only for testing...
257
      if (this.ideHome == null) {
3✔
258
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
259
      } else {
260
        this.userHome = this.ideHome.resolve("home");
7✔
261
      }
262
    } else {
263
      this.userHome = Path.of(getSystem().getProperty("user.home"));
×
264
    }
265
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
266
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
267

268
    this.path = computeSystemPath();
4✔
269
  }
1✔
270

271
  private String getMessageIdeHomeFound() {
272

273
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
274
  }
275

276
  private String getMessageNotInsideIdeProject() {
277

278
    return "You are not inside an IDE project: " + formatLocationPathForDisplay(this.cwd);
6✔
279
  }
280

281
  private String getMessageIdeRootNotFound() {
282

283
    String root = getSystem().getEnv("IDE_ROOT");
5✔
284
    if (root == null) {
2!
285
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
286
    } else {
287
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
288
    }
289
  }
290

291
  /**
292
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
293
   */
294
  public boolean isTest() {
295

296
    return false;
×
297
  }
298

299
  protected SystemPath computeSystemPath() {
300

301
    return new SystemPath(this);
×
302
  }
303

304
  private boolean isIdeHome(Path dir) {
305

306
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
307
      return false;
2✔
308
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
309
      return false;
×
310
    }
311
    return true;
2✔
312
  }
313

314
  private EnvironmentVariables createVariables() {
315

316
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
317
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
318
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
319
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
320
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
321
    return conf.resolved();
3✔
322
  }
323

324
  protected AbstractEnvironmentVariables createSystemVariables() {
325

326
    return EnvironmentVariables.ofSystem(this);
3✔
327
  }
328

329
  @Override
330
  public SystemInfo getSystemInfo() {
331

332
    return this.systemInfo;
3✔
333
  }
334

335
  @Override
336
  public FileAccess getFileAccess() {
337

338
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
339
    configureNetworkProxy();
2✔
340
    return this.fileAccess;
3✔
341
  }
342

343
  @Override
344
  public CommandletManager getCommandletManager() {
345

346
    return this.commandletManager;
3✔
347
  }
348

349
  @Override
350
  public ToolRepository getDefaultToolRepository() {
351

352
    return this.defaultToolRepository;
3✔
353
  }
354

355
  @Override
356
  public MavenRepository getMavenToolRepository() {
357

358
    return this.mavenRepository;
3✔
359
  }
360

361
  @Override
362
  public CustomToolRepository getCustomToolRepository() {
363

364
    if (this.customToolRepository == null) {
3!
365
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
366
    }
367
    return this.customToolRepository;
3✔
368
  }
369

370
  @Override
371
  public Path getIdeHome() {
372

373
    return this.ideHome;
3✔
374
  }
375

376
  @Override
377
  public String getProjectName() {
378

379
    if (this.ideHome != null) {
3!
380
      return this.ideHome.getFileName().toString();
5✔
381
    }
382
    return "";
×
383
  }
384

385
  @Override
386
  public VersionIdentifier getProjectVersion() {
387

388
    if (this.ideHome != null) {
3!
389
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
390
      if (Files.exists(versionFile)) {
5✔
391
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
392
        return VersionIdentifier.of(version);
3✔
393
      }
394
    }
395
    return IdeMigrator.START_VERSION;
2✔
396
  }
397

398
  @Override
399
  public void setProjectVersion(VersionIdentifier version) {
400

401
    if (this.ideHome == null) {
3!
402
      throw new IllegalStateException("IDE_HOME not available!");
×
403
    }
404
    Objects.requireNonNull(version);
3✔
405
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
406
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
407
  }
1✔
408

409
  @Override
410
  public Path getIdeRoot() {
411

412
    return this.ideRoot;
3✔
413
  }
414

415
  @Override
416
  public Path getIdePath() {
417

418
    Path myIdeRoot = getIdeRoot();
3✔
419
    if (myIdeRoot == null) {
2!
420
      return null;
×
421
    }
422
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
423
  }
424

425
  @Override
426
  public Path getCwd() {
427

428
    return this.cwd;
3✔
429
  }
430

431
  @Override
432
  public Path getTempPath() {
433

434
    Path idePath = getIdePath();
3✔
435
    if (idePath == null) {
2!
436
      return null;
×
437
    }
438
    return idePath.resolve("tmp");
4✔
439
  }
440

441
  @Override
442
  public Path getTempDownloadPath() {
443

444
    Path tmp = getTempPath();
3✔
445
    if (tmp == null) {
2!
446
      return null;
×
447
    }
448
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
449
  }
450

451
  @Override
452
  public Path getUserHome() {
453

454
    return this.userHome;
3✔
455
  }
456

457
  @Override
458
  public Path getUserHomeIde() {
459

460
    return this.userHomeIde;
3✔
461
  }
462

463
  @Override
464
  public Path getSettingsPath() {
465

466
    return this.settingsPath;
3✔
467
  }
468

469
  @Override
470
  public Path getSettingsGitRepository() {
471

472
    Path settingsPath = getSettingsPath();
3✔
473
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
474
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
475
      error("Settings repository exists but is not a git repository.");
3✔
476
      return null;
2✔
477
    }
478
    return settingsPath;
2✔
479
  }
480

481
  @Override
482
  public boolean isSettingsRepositorySymlinkOrJunction() {
483

484
    Path settingsPath = getSettingsPath();
3✔
485
    if (settingsPath == null) {
2!
486
      return false;
×
487
    }
488
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
489
  }
490

491
  @Override
492
  public Path getSettingsCommitIdPath() {
493

494
    return this.settingsCommitIdPath;
3✔
495
  }
496

497
  @Override
498
  public Path getConfPath() {
499

500
    return this.confPath;
3✔
501
  }
502

503
  @Override
504
  public Path getSoftwarePath() {
505

506
    if (this.ideHome == null) {
3✔
507
      return null;
2✔
508
    }
509
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
510
  }
511

512
  @Override
513
  public Path getSoftwareExtraPath() {
514

515
    Path softwarePath = getSoftwarePath();
3✔
516
    if (softwarePath == null) {
2!
517
      return null;
×
518
    }
519
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
520
  }
521

522
  @Override
523
  public Path getSoftwareRepositoryPath() {
524

525
    Path idePath = getIdePath();
3✔
526
    if (idePath == null) {
2!
527
      return null;
×
528
    }
529
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
530
  }
531

532
  @Override
533
  public Path getPluginsPath() {
534

535
    return this.pluginsPath;
3✔
536
  }
537

538
  @Override
539
  public String getWorkspaceName() {
540

541
    return this.workspaceName;
3✔
542
  }
543

544
  @Override
545
  public Path getWorkspacePath() {
546

547
    return this.workspacePath;
3✔
548
  }
549

550
  @Override
551
  public Path getDownloadPath() {
552

553
    return this.downloadPath;
3✔
554
  }
555

556
  @Override
557
  public Path getUrlsPath() {
558

559
    Path idePath = getIdePath();
3✔
560
    if (idePath == null) {
2!
561
      return null;
×
562
    }
563
    return idePath.resolve(FOLDER_URLS);
4✔
564
  }
565

566
  @Override
567
  public Path getToolRepositoryPath() {
568

569
    Path idePath = getIdePath();
3✔
570
    if (idePath == null) {
2!
571
      return null;
×
572
    }
573
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
574
  }
575

576
  @Override
577
  public SystemPath getPath() {
578

579
    return this.path;
3✔
580
  }
581

582
  @Override
583
  public EnvironmentVariables getVariables() {
584

585
    if (this.variables == null) {
3✔
586
      this.variables = createVariables();
4✔
587
    }
588
    return this.variables;
3✔
589
  }
590

591
  @Override
592
  public UrlMetadata getUrls() {
593

594
    if (this.urlMetadata == null) {
3✔
595
      if (!isTest()) {
3!
596
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
597
      }
598
      this.urlMetadata = new UrlMetadata(this);
6✔
599
    }
600
    return this.urlMetadata;
3✔
601
  }
602

603
  @Override
604
  public boolean isQuietMode() {
605

606
    return this.startContext.isQuietMode();
4✔
607
  }
608

609
  @Override
610
  public boolean isBatchMode() {
611

612
    return this.startContext.isBatchMode();
4✔
613
  }
614

615
  @Override
616
  public boolean isForceMode() {
617

618
    return this.startContext.isForceMode();
4✔
619
  }
620

621
  @Override
622
  public boolean isForcePull() {
623

624
    return this.startContext.isForcePull();
×
625
  }
626

627
  @Override
628
  public boolean isForcePlugins() {
629

630
    return this.startContext.isForcePlugins();
×
631
  }
632

633
  @Override
634
  public boolean isForceRepositories() {
635

636
    return this.startContext.isForceRepositories();
×
637
  }
638

639
  @Override
640
  public boolean isOfflineMode() {
641

642
    return this.startContext.isOfflineMode();
4✔
643
  }
644

645
  @Override
646
  public boolean isPrivacyMode() {
647
    return this.startContext.isPrivacyMode();
×
648
  }
649

650
  @Override
651
  public boolean isSkipUpdatesMode() {
652

653
    return this.startContext.isSkipUpdatesMode();
4✔
654
  }
655

656
  @Override
657
  public boolean isOnline() {
658

659
    if (this.online == null) {
3✔
660
      configureNetworkProxy();
2✔
661
      // we currently assume we have only a CLI process that runs shortly
662
      // therefore we run this check only once to save resources when this method is called many times
663
      try {
664
        int timeout = 1000;
2✔
665
        //open a connection to github.com and try to retrieve data
666
        //getContent fails if there is no connection
667
        URLConnection connection = new URL("https://www.github.com").openConnection();
6✔
668
        connection.setConnectTimeout(timeout);
3✔
669
        connection.getContent();
3✔
670
        this.online = Boolean.TRUE;
3✔
671
      } catch (Exception ignored) {
×
672
        this.online = Boolean.FALSE;
×
673
      }
1✔
674
    }
675
    return this.online.booleanValue();
4✔
676
  }
677

678
  private void configureNetworkProxy() {
679

680
    if (this.networkProxy == null) {
3✔
681
      this.networkProxy = new NetworkProxy(this);
6✔
682
      this.networkProxy.configure();
3✔
683
    }
684
  }
1✔
685

686
  @Override
687
  public Locale getLocale() {
688

689
    Locale locale = this.startContext.getLocale();
4✔
690
    if (locale == null) {
2✔
691
      locale = Locale.getDefault();
2✔
692
    }
693
    return locale;
2✔
694
  }
695

696
  @Override
697
  public DirectoryMerger getWorkspaceMerger() {
698

699
    if (this.workspaceMerger == null) {
3✔
700
      this.workspaceMerger = new DirectoryMerger(this);
6✔
701
    }
702
    return this.workspaceMerger;
3✔
703
  }
704

705
  /**
706
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
707
   */
708
  @Override
709
  public Path getDefaultExecutionDirectory() {
710

711
    return this.defaultExecutionDirectory;
×
712
  }
713

714
  /**
715
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
716
   */
717
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
718

719
    if (defaultExecutionDirectory != null) {
×
720
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
721
    }
722
  }
×
723

724
  @Override
725
  public GitContext getGitContext() {
726

727
    return new GitContextImpl(this);
×
728
  }
729

730
  @Override
731
  public ProcessContext newProcess() {
732

733
    ProcessContext processContext = createProcessContext();
3✔
734
    if (this.defaultExecutionDirectory != null) {
3!
735
      processContext.directory(this.defaultExecutionDirectory);
×
736
    }
737
    return processContext;
2✔
738
  }
739

740
  @Override
741
  public IdeSystem getSystem() {
742

743
    if (this.system == null) {
×
744
      this.system = new IdeSystemImpl(this);
×
745
    }
746
    return this.system;
×
747
  }
748

749
  /**
750
   * @return a new instance of {@link ProcessContext}.
751
   * @see #newProcess()
752
   */
753
  protected ProcessContext createProcessContext() {
754

755
    return new ProcessContextImpl(this);
5✔
756
  }
757

758
  @Override
759
  public IdeSubLogger level(IdeLogLevel level) {
760

761
    return this.startContext.level(level);
5✔
762
  }
763

764
  @Override
765
  public void logIdeHomeAndRootStatus() {
766
    if (this.ideRoot != null) {
3!
767
      success("IDE_ROOT is set to {}", formatLocationPathForDisplay(this.ideRoot));
×
768
    }
769
    if (this.ideHome == null) {
3✔
770
      warning(getMessageNotInsideIdeProject());
5✔
771
    } else {
772
      success("IDE_HOME is set to {}", formatLocationPathForDisplay(this.ideHome));
12✔
773
    }
774
  }
1✔
775

776
  /**
777
   * Formats a path for GDPR compliance based on the privacy mode.
778
   *
779
   * @param location Path to format.
780
   * @return the formatted path string.
781
   */
782
  public String formatLocationPathForDisplay(Path location) {
783
    String locationString = location.toString();
3✔
784
    if (this.startContext.isPrivacyMode()) {
4✔
785
      Path normalizedPath = location.normalize();
3✔
786

787
      if (this.ideHome != null && normalizedPath.startsWith(this.ideHome)) {
8!
788
        Path relative = this.ideHome.relativize(normalizedPath);
5✔
789
        return Paths.get(IDE_HOME_PLACEHOLDER).resolve(relative).toString();
8✔
790
      }
791

792
      if (this.ideRoot != null && normalizedPath.startsWith(this.ideRoot)) {
×
793
        Path relative = this.ideRoot.relativize(normalizedPath);
×
794
        return Paths.get(IDE_ROOT_PLACEHOLDER).resolve(relative).toString();
×
795
      }
796

797
      if (this.userHome != null && normalizedPath.startsWith(this.userHome)) {
×
798
        Path relative = this.userHome.relativize(normalizedPath);
×
799
        return Paths.get(USER_HOME_PLACEHOLDER).resolve(relative).toString();
×
800
      }
801
      return location.toString();
×
802
    }
803
    return locationString;
2✔
804
  }
805

806
  @Override
807
  public String askForInput(String message, String defaultValue) {
808

809
    if (!message.isBlank()) {
×
810
      info(message);
×
811
    }
812
    if (isBatchMode()) {
×
813
      if (isForceMode() || isForcePull()) {
×
814
        return defaultValue;
×
815
      } else {
816
        throw new CliAbortException();
×
817
      }
818
    }
819
    String input = readLine().trim();
×
820
    return input.isEmpty() ? defaultValue : input;
×
821
  }
822

823
  @Override
824
  public String askForInput(String message) {
825

826
    String input;
827
    do {
828
      info(message);
3✔
829
      input = readLine().trim();
4✔
830
    } while (input.isEmpty());
3!
831

832
    return input;
2✔
833
  }
834

835
  @SuppressWarnings("unchecked")
836
  @Override
837
  public <O> O question(String question, O... options) {
838

839
    assert (options.length >= 2);
×
840
    interaction(question);
×
841
    Map<String, O> mapping = new HashMap<>(options.length);
×
842
    int i = 0;
×
843
    for (O option : options) {
×
844
      i++;
×
845
      String key = "" + option;
×
846
      addMapping(mapping, key, option);
×
847
      String numericKey = Integer.toString(i);
×
848
      if (numericKey.equals(key)) {
×
849
        trace("Options should not be numeric: " + key);
×
850
      } else {
851
        addMapping(mapping, numericKey, option);
×
852
      }
853
      interaction("Option " + numericKey + ": " + key);
×
854
    }
855
    O option = null;
×
856
    if (isBatchMode()) {
×
857
      if (isForceMode() || isForcePull()) {
×
858
        option = options[0];
×
859
        interaction("" + option);
×
860
      }
861
    } else {
862
      while (option == null) {
×
863
        String answer = readLine();
×
864
        option = mapping.get(answer);
×
865
        if (option == null) {
×
866
          warning("Invalid answer: '" + answer + "' - please try again.");
×
867
        }
868
      }
×
869
    }
870
    return option;
×
871
  }
872

873
  /**
874
   * @return the input from the end-user (e.g. read from the console).
875
   */
876
  protected abstract String readLine();
877

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

880
    O duplicate = mapping.put(key, option);
×
881
    if (duplicate != null) {
×
882
      throw new IllegalArgumentException("Duplicated option " + key);
×
883
    }
884
  }
×
885

886
  @Override
887
  public Step getCurrentStep() {
888

889
    return this.currentStep;
×
890
  }
891

892
  @Override
893
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
894

895
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
896
    return this.currentStep;
3✔
897
  }
898

899
  /**
900
   * Internal method to end the running {@link Step}.
901
   *
902
   * @param step the current {@link Step} to end.
903
   */
904
  public void endStep(StepImpl step) {
905

906
    if (step == this.currentStep) {
4!
907
      this.currentStep = this.currentStep.getParent();
6✔
908
    } else {
909
      String currentStepName = "null";
×
910
      if (this.currentStep != null) {
×
911
        currentStepName = this.currentStep.getName();
×
912
      }
913
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
914
    }
915
  }
1✔
916

917
  /**
918
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
919
   *
920
   * @param arguments the {@link CliArgument}.
921
   * @return the return code of the execution.
922
   */
923
  public int run(CliArguments arguments) {
924

925
    CliArgument current = arguments.current();
3✔
926
    assert (this.currentStep == null);
4!
927
    boolean supressStepSuccess = false;
2✔
928
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
929
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
930
    Commandlet cmd = null;
2✔
931
    ValidationResult result = null;
2✔
932
    try {
933
      while (commandletIterator.hasNext()) {
3✔
934
        cmd = commandletIterator.next();
4✔
935
        result = applyAndRun(arguments.copy(), cmd);
6✔
936
        if (result.isValid()) {
3!
937
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
938
          step.success();
2✔
939
          return ProcessResult.SUCCESS;
4✔
940
        }
941
      }
942
      this.startContext.activateLogging();
3✔
943
      verifyIdeMinVersion(false);
3✔
944
      if (result != null) {
2!
945
        error(result.getErrorMessage());
×
946
      }
947
      step.error("Invalid arguments: {}", current.getArgs());
10✔
948
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
949
      if (cmd != null) {
2!
950
        help.commandlet.setValue(cmd);
×
951
      }
952
      help.run();
2✔
953
      return 1;
4✔
954
    } catch (Throwable t) {
1✔
955
      this.startContext.activateLogging();
3✔
956
      step.error(t, true);
4✔
957
      throw t;
2✔
958
    } finally {
959
      step.close();
2✔
960
      assert (this.currentStep == null);
4!
961
      step.logSummary(supressStepSuccess);
3✔
962
    }
963
  }
964

965
  @Override
966
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
967

968
    this.startContext.deactivateLogging(threshold);
4✔
969
    lambda.run();
2✔
970
    this.startContext.activateLogging();
3✔
971
  }
1✔
972

973
  /**
974
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
975
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
976
   *     {@link Commandlet} did not match and we have to try a different candidate).
977
   */
978
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
979

980
    IdeLogLevel previousLogLevel = null;
2✔
981
    cmd.reset();
2✔
982
    ValidationResult result = apply(arguments, cmd);
5✔
983
    if (result.isValid()) {
3!
984
      result = cmd.validate();
3✔
985
    }
986
    if (result.isValid()) {
3!
987
      debug("Running commandlet {}", cmd);
9✔
988
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
989
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
990
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
991
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
992
      }
993
      try {
994
        if (cmd.isProcessableOutput()) {
3!
995
          if (!debug().isEnabled()) {
×
996
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
997
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
998
          }
999
          this.startContext.activateLogging();
×
1000
        } else {
1001
          this.startContext.activateLogging();
3✔
1002
          verifyIdeRoot();
2✔
1003
          if (cmd.isIdeHomeRequired()) {
3!
1004
            debug(getMessageIdeHomeFound());
4✔
1005
          }
1006
          Path settingsRepository = getSettingsGitRepository();
3✔
1007
          if (settingsRepository != null) {
2!
1008
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1009
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1010
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1011
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1012
                interaction(
×
1013
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1014

1015
              } else {
1016
                interaction(
×
1017
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1018
              }
1019
            }
1020
          }
1021
        }
1022
        boolean success = ensureLicenseAgreement(cmd);
4✔
1023
        if (!success) {
2!
1024
          return ValidationResultValid.get();
×
1025
        }
1026
        cmd.run();
2✔
1027
      } finally {
1028
        if (previousLogLevel != null) {
2!
1029
          this.startContext.setLogLevel(previousLogLevel);
×
1030
        }
1031
      }
1✔
1032
    } else {
1033
      trace("Commandlet did not match");
×
1034
    }
1035
    return result;
2✔
1036
  }
1037

1038
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1039

1040
    if (isTest()) {
3!
1041
      return true; // ignore for tests
2✔
1042
    }
1043
    getFileAccess().mkdirs(this.userHomeIde);
×
1044
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1045
    if (Files.isRegularFile(licenseAgreement)) {
×
1046
      return true; // success, license already accepted
×
1047
    }
1048
    if (cmd instanceof EnvironmentCommandlet) {
×
1049
      // 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
1050
      // 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
1051
      // printing anything anymore in such case.
1052
      return false;
×
1053
    }
1054
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1055
    if (logLevelInfoDisabled) {
×
1056
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1057
    }
1058
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1059
    if (logLevelInteractionDisabled) {
×
1060
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1061
    }
1062
    StringBuilder sb = new StringBuilder(1180);
×
1063
    sb.append(LOGO).append("""
×
1064
        Welcome to IDEasy!
1065
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1066
        It supports automatic download and installation of arbitrary 3rd party tools.
1067
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1068
        But if explicitly configured, also commercial software that requires an additional license may be used.
1069
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1070
        You are solely responsible for all risks implied by using this software.
1071
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1072
        You will be able to find it online under the following URL:
1073
        """).append(LICENSE_URL);
×
1074
    if (this.ideRoot != null) {
×
1075
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1076
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1077
    }
1078
    info(sb.toString());
×
1079
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1080

1081
    sb.setLength(0);
×
1082
    LocalDateTime now = LocalDateTime.now();
×
1083
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1084
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1085
    try {
1086
      Files.writeString(licenseAgreement, sb);
×
1087
    } catch (Exception e) {
×
1088
      throw new RuntimeException("Failed to save license agreement!", e);
×
1089
    }
×
1090
    if (logLevelInfoDisabled) {
×
1091
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1092
    }
1093
    if (logLevelInteractionDisabled) {
×
1094
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1095
    }
1096
    return true;
×
1097
  }
1098

1099
  private void verifyIdeRoot() {
1100

1101
    if (!isTest()) {
3!
1102
      if (this.ideRoot == null) {
×
1103
        warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again.");
×
1104
      } else if (this.ideHome != null) {
×
1105
        Path ideRootPath = getIdeRootPathFromEnv();
×
1106
        if (!this.ideRoot.equals(ideRootPath)) {
×
1107
          warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.",
×
1108
              formatLocationPathForDisplay(ideRootPath),
×
1109
              this.ideHome.getFileName(), formatLocationPathForDisplay(this.ideRoot));
×
1110
        }
1111
      }
1112
    }
1113
  }
1✔
1114

1115
  @Override
1116
  public void verifyIdeMinVersion(boolean throwException) {
1117
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1118
    if (minVersion == null) {
2✔
1119
      return;
1✔
1120
    }
1121
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1122
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1123
          + "However, this is too old as your project requires at latest version %s\n"
1124
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1125
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1126
      if (throwException) {
2✔
1127
        throw new CliException(message);
5✔
1128
      } else {
1129
        warning(message);
3✔
1130
      }
1131
    }
1132
  }
1✔
1133

1134
  /**
1135
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1136
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1137
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1138
   */
1139
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1140

1141
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1142
    if (arguments.current().isStart()) {
4✔
1143
      arguments.next();
3✔
1144
    }
1145
    if (includeContextOptions) {
2✔
1146
      ContextCommandlet cc = new ContextCommandlet();
4✔
1147
      for (Property<?> property : cc.getProperties()) {
11✔
1148
        assert (property.isOption());
4!
1149
        property.apply(arguments, this, cc, collector);
7✔
1150
      }
1✔
1151
    }
1152
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1153
    CliArgument current = arguments.current();
3✔
1154
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1155
      collector.add(current.get(), null, null, null);
7✔
1156
    }
1157
    arguments.next();
3✔
1158
    while (commandletIterator.hasNext()) {
3✔
1159
      Commandlet cmd = commandletIterator.next();
4✔
1160
      if (!arguments.current().isEnd()) {
4✔
1161
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1162
      }
1163
    }
1✔
1164
    return collector.getSortedCandidates();
3✔
1165
  }
1166

1167
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1168

1169
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1170
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1171
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1172
    List<Property<?>> properties = cmd.getProperties();
3✔
1173
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1174
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1175
    for (Property<?> property : properties) {
10✔
1176
      if (property.isOption()) {
3✔
1177
        optionProperties.add(property);
4✔
1178
      }
1179
    }
1✔
1180
    CliArgument currentArgument = arguments.current();
3✔
1181
    while (!currentArgument.isEnd()) {
3✔
1182
      trace("Trying to match argument '{}'", currentArgument);
9✔
1183
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1184
        if (currentArgument.isCompletion()) {
3✔
1185
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1186
          while (optionIterator.hasNext()) {
3✔
1187
            Property<?> option = optionIterator.next();
4✔
1188
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1189
            if (success) {
2✔
1190
              optionIterator.remove();
2✔
1191
              arguments.next();
3✔
1192
            }
1193
          }
1✔
1194
        } else {
1✔
1195
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1196
          if (option != null) {
2✔
1197
            arguments.next();
3✔
1198
            boolean removed = optionProperties.remove(option);
4✔
1199
            if (!removed) {
2!
1200
              option = null;
×
1201
            }
1202
          }
1203
          if (option == null) {
2✔
1204
            trace("No such option was found.");
3✔
1205
            return;
1✔
1206
          }
1207
        }
1✔
1208
      } else {
1209
        if (valueIterator.hasNext()) {
3✔
1210
          Property<?> valueProperty = valueIterator.next();
4✔
1211
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1212
          if (!success) {
2✔
1213
            trace("Completion cannot match any further.");
3✔
1214
            return;
1✔
1215
          }
1216
        } else {
1✔
1217
          trace("No value left for completion.");
3✔
1218
          return;
1✔
1219
        }
1220
      }
1221
      currentArgument = arguments.current();
4✔
1222
    }
1223
  }
1✔
1224

1225
  /**
1226
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1227
   *     {@link CliArguments#copy() copy} as needed.
1228
   * @param cmd the potential {@link Commandlet} to match.
1229
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1230
   */
1231
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1232

1233
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1234
    CliArgument currentArgument = arguments.current();
3✔
1235
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1236
    Property<?> property = null;
2✔
1237
    if (propertyIterator.hasNext()) {
3!
1238
      property = propertyIterator.next();
4✔
1239
    }
1240
    while (!currentArgument.isEnd()) {
3✔
1241
      trace("Trying to match argument '{}'", currentArgument);
9✔
1242
      Property<?> currentProperty = property;
2✔
1243
      if (!arguments.isEndOptions()) {
3!
1244
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1245
        if (option != null) {
2!
1246
          currentProperty = option;
×
1247
        }
1248
      }
1249
      if (currentProperty == null) {
2!
1250
        trace("No option or next value found");
×
1251
        ValidationState state = new ValidationState(null);
×
1252
        state.addErrorMessage("No matching property found");
×
1253
        return state;
×
1254
      }
1255
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1256
      if (currentProperty == property) {
3!
1257
        if (!property.isMultiValued()) {
3✔
1258
          if (propertyIterator.hasNext()) {
3✔
1259
            property = propertyIterator.next();
5✔
1260
          } else {
1261
            property = null;
2✔
1262
          }
1263
        }
1264
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1265
          arguments.stopSplitShortOptions();
2✔
1266
        }
1267
      }
1268
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1269
      if (!matches) {
2!
1270
        ValidationState state = new ValidationState(null);
×
1271
        state.addErrorMessage("No matching property found");
×
1272
        return state;
×
1273
      }
1274
      currentArgument = arguments.current();
3✔
1275
    }
1✔
1276
    return ValidationResultValid.get();
2✔
1277
  }
1278

1279
  @Override
1280
  public String findBash() {
1281

1282
    String bash = "bash";
2✔
1283
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1284
      bash = findBashOnWindows();
×
1285
    }
1286

1287
    return bash;
2✔
1288
  }
1289

1290
  private String findBashOnWindows() {
1291

1292
    // Check if Git Bash exists in the default location
1293
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1294
    if (Files.exists(defaultPath)) {
×
1295
      return defaultPath.toString();
×
1296
    }
1297

1298
    // If not found in the default location, try the registry query
1299
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1300
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1301
    String regQueryResult;
1302
    for (String bashVariant : bashVariants) {
×
1303
      for (String registryKey : registryKeys) {
×
1304
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1305
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1306

1307
        try {
1308
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1309
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1310
            StringBuilder output = new StringBuilder();
×
1311
            String line;
1312

1313
            while ((line = reader.readLine()) != null) {
×
1314
              output.append(line);
×
1315
            }
1316

1317
            int exitCode = process.waitFor();
×
1318
            if (exitCode != 0) {
×
1319
              return null;
×
1320
            }
1321

1322
            regQueryResult = output.toString();
×
1323
            if (regQueryResult != null) {
×
1324
              int index = regQueryResult.indexOf("REG_SZ");
×
1325
              if (index != -1) {
×
1326
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1327
                return path + "\\bin\\bash.exe";
×
1328
              }
1329
            }
1330

1331
          }
×
1332
        } catch (Exception e) {
×
1333
          return null;
×
1334
        }
×
1335
      }
1336
    }
1337
    // no bash found
1338
    return null;
×
1339
  }
1340

1341
  @Override
1342
  public WindowsPathSyntax getPathSyntax() {
1343

1344
    return this.pathSyntax;
3✔
1345
  }
1346

1347
  /**
1348
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1349
   */
1350
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1351

1352
    this.pathSyntax = pathSyntax;
3✔
1353
  }
1✔
1354

1355
  /**
1356
   * @return the {@link IdeStartContextImpl}.
1357
   */
1358
  public IdeStartContextImpl getStartContext() {
1359

1360
    return startContext;
3✔
1361
  }
1362

1363
  /**
1364
   * @return the {@link WindowsHelper}.
1365
   */
1366
  public final WindowsHelper getWindowsHelper() {
1367

1368
    if (this.windowsHelper == null) {
3✔
1369
      this.windowsHelper = createWindowsHelper();
4✔
1370
    }
1371
    return this.windowsHelper;
3✔
1372
  }
1373

1374
  /**
1375
   * @return the new {@link WindowsHelper} instance.
1376
   */
1377
  protected WindowsHelper createWindowsHelper() {
1378

1379
    return new WindowsHelperImpl(this);
×
1380
  }
1381

1382
  /**
1383
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1384
   */
1385
  public void reload() {
1386

1387
    this.variables = null;
3✔
1388
    this.customToolRepository = null;
3✔
1389
  }
1✔
1390

1391
  @Override
1392
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1393

1394
    assert (Files.isDirectory(installationPath));
6!
1395
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1396
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1397
  }
1✔
1398

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