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

devonfw / IDEasy / 15855321673

24 Jun 2025 03:51PM UTC coverage: 68.191% (+0.4%) from 67.783%
15855321673

Pull #1375

github

web-flow
Merge 26e6591cb into 584febaaf
Pull Request #1375: #742: Show warning when git repo name does not meet name convention.

3198 of 5094 branches covered (62.78%)

Branch coverage included in aggregate %.

8213 of 11640 relevant lines covered (70.56%)

3.09 hits per line

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

65.17
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.time.LocalDateTime;
12
import java.util.ArrayList;
13
import java.util.HashMap;
14
import java.util.Iterator;
15
import java.util.List;
16
import java.util.Locale;
17
import java.util.Map;
18
import java.util.Objects;
19

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

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

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

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

84
  private final IdeStartContextImpl startContext;
85

86
  private Path ideHome;
87

88
  private final Path ideRoot;
89

90
  private Path confPath;
91

92
  protected Path settingsPath;
93

94
  private Path settingsCommitIdPath;
95

96
  protected Path pluginsPath;
97

98
  private Path workspacePath;
99

100
  private String workspaceName;
101

102
  private Path cwd;
103

104
  private Path downloadPath;
105

106
  protected Path userHome;
107

108
  private Path userHomeIde;
109

110
  private SystemPath path;
111

112
  private WindowsPathSyntax pathSyntax;
113

114
  private final SystemInfo systemInfo;
115

116
  private EnvironmentVariables variables;
117

118
  private final FileAccess fileAccess;
119

120
  protected CommandletManager commandletManager;
121

122
  protected ToolRepository defaultToolRepository;
123

124
  private CustomToolRepository customToolRepository;
125

126
  private MavenRepository mavenRepository;
127

128
  private DirectoryMerger workspaceMerger;
129

130
  protected UrlMetadata urlMetadata;
131

132
  protected Path defaultExecutionDirectory;
133

134
  private StepImpl currentStep;
135

136
  protected Boolean online;
137

138
  protected IdeSystem system;
139

140
  private NetworkProxy networkProxy;
141

142
  private WindowsHelper windowsHelper;
143

144
  /**
145
   * The constructor.
146
   *
147
   * @param startContext the {@link IdeLogger}.
148
   * @param workingDirectory the optional {@link Path} to current working directory.
149
   */
150
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
151

152
    super();
2✔
153
    this.startContext = startContext;
3✔
154
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
155
    this.commandletManager = new CommandletManagerImpl(this);
6✔
156
    this.fileAccess = new FileAccessImpl(this);
6✔
157
    String workspace = WORKSPACE_MAIN;
2✔
158
    if (workingDirectory == null) {
2!
159
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
160
    }
161
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
162
    if (Files.isDirectory(workingDirectory)) {
5✔
163
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
164
    } else {
165
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
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(false);
4✔
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
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
216
      ideRootPath = ideHomePath.getParent();
3✔
217
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
218
        warning(
12✔
219
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
220
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
221
            ideRootPathFromEnv,
222
            ideHomePath.getFileName(), ideRootPath);
6✔
223
      }
224

225
    } else if (!isTest()) {
4!
226
      ideRootPath = getIdeRootPathFromEnv(true);
×
227
    }
228
    return ideRootPath;
2✔
229
  }
230

231
  /**
232
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
233
   */
234
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
235

236
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
237
    if (root != null) {
×
238
      Path rootPath = Path.of(root);
×
239
      if (Files.isDirectory(rootPath)) {
×
240
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
241
        if (withSanityCheck) {
×
242
          int nameCount = rootPath.getNameCount();
×
243
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
244
          int delta = absoluteNameCount - nameCount;
×
245
          if (delta >= 0) {
×
246
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
247
              String rootName = rootPath.getName(nameIndex).toString();
×
248
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
249
              if (!rootName.equals(absoluteRootName)) {
×
250
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
251
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
252
                break;
×
253
              }
254
            }
255
          } else {
256
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath, absoluteRootPath);
×
257
          }
258
        }
259
        return absoluteRootPath;
×
260
      } else if (withSanityCheck) {
×
261
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
262
      }
263
    }
264
    return null;
×
265
  }
266

267
  @Override
268
  public void setCwd(Path userDir, String workspace, Path ideHome) {
269

270
    this.cwd = userDir;
3✔
271
    this.workspaceName = workspace;
3✔
272
    this.ideHome = ideHome;
3✔
273
    if (ideHome == null) {
2✔
274
      this.workspacePath = null;
3✔
275
      this.confPath = null;
3✔
276
      this.settingsPath = null;
3✔
277
      this.pluginsPath = null;
4✔
278
    } else {
279
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
280
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
281
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
282
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
283
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
284
    }
285
    if (isTest()) {
3!
286
      // only for testing...
287
      if (this.ideHome == null) {
3✔
288
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
289
      } else {
290
        this.userHome = this.ideHome.resolve("home");
7✔
291
      }
292
    } else {
293
      this.userHome = Path.of(getSystem().getProperty("user.home"));
×
294
    }
295
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
296
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
297

298
    this.path = computeSystemPath();
4✔
299
  }
1✔
300

301
  private String getMessageIdeHomeFound() {
302

303
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
304
  }
305

306
  private String getMessageNotInsideIdeProject() {
307

308
    return "You are not inside an IDE project: " + this.cwd;
5✔
309
  }
310

311
  private String getMessageIdeRootNotFound() {
312

313
    String root = getSystem().getEnv("IDE_ROOT");
5✔
314
    if (root == null) {
2!
315
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
316
    } else {
317
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
318
    }
319
  }
320

321
  /**
322
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
323
   */
324
  public boolean isTest() {
325

326
    return false;
×
327
  }
328

329
  protected SystemPath computeSystemPath() {
330

331
    return new SystemPath(this);
×
332
  }
333

334
  private boolean isIdeHome(Path dir) {
335

336
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
337
      return false;
2✔
338
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
339
      return false;
×
340
    }
341
    return true;
2✔
342
  }
343

344
  private EnvironmentVariables createVariables() {
345

346
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
347
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
348
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
349
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
350
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
351
    return conf.resolved();
3✔
352
  }
353

354
  protected AbstractEnvironmentVariables createSystemVariables() {
355

356
    return EnvironmentVariables.ofSystem(this);
3✔
357
  }
358

359
  @Override
360
  public SystemInfo getSystemInfo() {
361

362
    return this.systemInfo;
3✔
363
  }
364

365
  @Override
366
  public FileAccess getFileAccess() {
367

368
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
369
    configureNetworkProxy();
2✔
370
    return this.fileAccess;
3✔
371
  }
372

373
  @Override
374
  public CommandletManager getCommandletManager() {
375

376
    return this.commandletManager;
3✔
377
  }
378

379
  @Override
380
  public ToolRepository getDefaultToolRepository() {
381

382
    return this.defaultToolRepository;
3✔
383
  }
384

385
  @Override
386
  public MavenRepository getMavenToolRepository() {
387

388
    return this.mavenRepository;
3✔
389
  }
390

391
  @Override
392
  public CustomToolRepository getCustomToolRepository() {
393

394
    if (this.customToolRepository == null) {
3!
395
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
396
    }
397
    return this.customToolRepository;
3✔
398
  }
399

400
  @Override
401
  public Path getIdeHome() {
402

403
    return this.ideHome;
3✔
404
  }
405

406
  @Override
407
  public String getProjectName() {
408

409
    if (this.ideHome != null) {
3!
410
      return this.ideHome.getFileName().toString();
5✔
411
    }
412
    return "";
×
413
  }
414

415
  @Override
416
  public VersionIdentifier getProjectVersion() {
417

418
    if (this.ideHome != null) {
3!
419
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
420
      if (Files.exists(versionFile)) {
5✔
421
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
422
        return VersionIdentifier.of(version);
3✔
423
      }
424
    }
425
    return IdeMigrator.START_VERSION;
2✔
426
  }
427

428
  @Override
429
  public void setProjectVersion(VersionIdentifier version) {
430

431
    if (this.ideHome == null) {
3!
432
      throw new IllegalStateException("IDE_HOME not available!");
×
433
    }
434
    Objects.requireNonNull(version);
3✔
435
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
436
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
437
  }
1✔
438

439
  @Override
440
  public Path getIdeRoot() {
441

442
    return this.ideRoot;
3✔
443
  }
444

445
  @Override
446
  public Path getIdePath() {
447

448
    Path myIdeRoot = getIdeRoot();
3✔
449
    if (myIdeRoot == null) {
2!
450
      return null;
×
451
    }
452
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
453
  }
454

455
  @Override
456
  public Path getCwd() {
457

458
    return this.cwd;
3✔
459
  }
460

461
  @Override
462
  public Path getTempPath() {
463

464
    Path idePath = getIdePath();
3✔
465
    if (idePath == null) {
2!
466
      return null;
×
467
    }
468
    return idePath.resolve("tmp");
4✔
469
  }
470

471
  @Override
472
  public Path getTempDownloadPath() {
473

474
    Path tmp = getTempPath();
3✔
475
    if (tmp == null) {
2!
476
      return null;
×
477
    }
478
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
479
  }
480

481
  @Override
482
  public Path getUserHome() {
483

484
    return this.userHome;
3✔
485
  }
486

487
  @Override
488
  public Path getUserHomeIde() {
489

490
    return this.userHomeIde;
3✔
491
  }
492

493
  @Override
494
  public Path getSettingsPath() {
495

496
    return this.settingsPath;
3✔
497
  }
498

499
  @Override
500
  public Path getSettingsGitRepository() {
501

502
    Path settingsPath = getSettingsPath();
3✔
503
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
504
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
505
      error("Settings repository exists but is not a git repository.");
3✔
506
      return null;
2✔
507
    }
508
    return settingsPath;
2✔
509
  }
510

511
  @Override
512
  public boolean isSettingsRepositorySymlinkOrJunction() {
513

514
    Path settingsPath = getSettingsPath();
3✔
515
    if (settingsPath == null) {
2!
516
      return false;
×
517
    }
518
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
519
  }
520

521
  @Override
522
  public Path getSettingsCommitIdPath() {
523

524
    return this.settingsCommitIdPath;
3✔
525
  }
526

527
  @Override
528
  public Path getConfPath() {
529

530
    return this.confPath;
3✔
531
  }
532

533
  @Override
534
  public Path getSoftwarePath() {
535

536
    if (this.ideHome == null) {
3✔
537
      return null;
2✔
538
    }
539
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
540
  }
541

542
  @Override
543
  public Path getSoftwareExtraPath() {
544

545
    Path softwarePath = getSoftwarePath();
3✔
546
    if (softwarePath == null) {
2!
547
      return null;
×
548
    }
549
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
550
  }
551

552
  @Override
553
  public Path getSoftwareRepositoryPath() {
554

555
    Path idePath = getIdePath();
3✔
556
    if (idePath == null) {
2!
557
      return null;
×
558
    }
559
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
560
  }
561

562
  @Override
563
  public Path getPluginsPath() {
564

565
    return this.pluginsPath;
3✔
566
  }
567

568
  @Override
569
  public String getWorkspaceName() {
570

571
    return this.workspaceName;
3✔
572
  }
573

574
  @Override
575
  public Path getWorkspacePath() {
576

577
    return this.workspacePath;
3✔
578
  }
579

580
  @Override
581
  public Path getDownloadPath() {
582

583
    return this.downloadPath;
3✔
584
  }
585

586
  @Override
587
  public Path getUrlsPath() {
588

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

596
  @Override
597
  public Path getToolRepositoryPath() {
598

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

606
  @Override
607
  public SystemPath getPath() {
608

609
    return this.path;
3✔
610
  }
611

612
  @Override
613
  public EnvironmentVariables getVariables() {
614

615
    if (this.variables == null) {
3✔
616
      this.variables = createVariables();
4✔
617
    }
618
    return this.variables;
3✔
619
  }
620

621
  @Override
622
  public UrlMetadata getUrls() {
623

624
    if (this.urlMetadata == null) {
3✔
625
      if (!isTest()) {
3!
626
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
627
      }
628
      this.urlMetadata = new UrlMetadata(this);
6✔
629
    }
630
    return this.urlMetadata;
3✔
631
  }
632

633
  @Override
634
  public boolean isQuietMode() {
635

636
    return this.startContext.isQuietMode();
4✔
637
  }
638

639
  @Override
640
  public boolean isBatchMode() {
641

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

645
  @Override
646
  public boolean isForceMode() {
647

648
    return this.startContext.isForceMode();
4✔
649
  }
650

651
  @Override
652
  public boolean isForcePull() {
653

654
    return this.startContext.isForcePull();
×
655
  }
656

657
  @Override
658
  public boolean isForcePlugins() {
659

660
    return this.startContext.isForcePlugins();
×
661
  }
662

663
  @Override
664
  public boolean isForceRepositories() {
665

666
    return this.startContext.isForceRepositories();
×
667
  }
668

669
  @Override
670
  public boolean isOfflineMode() {
671

672
    return this.startContext.isOfflineMode();
4✔
673
  }
674

675
  @Override
676
  public boolean isSkipUpdatesMode() {
677

678
    return this.startContext.isSkipUpdatesMode();
4✔
679
  }
680

681
  @Override
682
  public boolean isOnline() {
683

684
    if (this.online == null) {
3✔
685
      configureNetworkProxy();
2✔
686
      // we currently assume we have only a CLI process that runs shortly
687
      // therefore we run this check only once to save resources when this method is called many times
688
      try {
689
        int timeout = 1000;
2✔
690
        //open a connection to github.com and try to retrieve data
691
        //getContent fails if there is no connection
692
        URLConnection connection = new URL("https://www.github.com").openConnection();
6✔
693
        connection.setConnectTimeout(timeout);
3✔
694
        connection.getContent();
3✔
695
        this.online = Boolean.TRUE;
3✔
696
      } catch (Exception ignored) {
×
697
        this.online = Boolean.FALSE;
×
698
      }
1✔
699
    }
700
    return this.online.booleanValue();
4✔
701
  }
702

703
  private void configureNetworkProxy() {
704

705
    if (this.networkProxy == null) {
3✔
706
      this.networkProxy = new NetworkProxy(this);
6✔
707
      this.networkProxy.configure();
3✔
708
    }
709
  }
1✔
710

711
  @Override
712
  public Locale getLocale() {
713

714
    Locale locale = this.startContext.getLocale();
4✔
715
    if (locale == null) {
2✔
716
      locale = Locale.getDefault();
2✔
717
    }
718
    return locale;
2✔
719
  }
720

721
  @Override
722
  public DirectoryMerger getWorkspaceMerger() {
723

724
    if (this.workspaceMerger == null) {
3✔
725
      this.workspaceMerger = new DirectoryMerger(this);
6✔
726
    }
727
    return this.workspaceMerger;
3✔
728
  }
729

730
  /**
731
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
732
   */
733
  @Override
734
  public Path getDefaultExecutionDirectory() {
735

736
    return this.defaultExecutionDirectory;
×
737
  }
738

739
  /**
740
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
741
   */
742
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
743

744
    if (defaultExecutionDirectory != null) {
×
745
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
746
    }
747
  }
×
748

749
  @Override
750
  public GitContext getGitContext() {
751

752
    return new GitContextImpl(this);
×
753
  }
754

755
  @Override
756
  public ProcessContext newProcess() {
757

758
    ProcessContext processContext = createProcessContext();
3✔
759
    if (this.defaultExecutionDirectory != null) {
3!
760
      processContext.directory(this.defaultExecutionDirectory);
×
761
    }
762
    return processContext;
2✔
763
  }
764

765
  @Override
766
  public IdeSystem getSystem() {
767

768
    if (this.system == null) {
×
769
      this.system = new IdeSystemImpl(this);
×
770
    }
771
    return this.system;
×
772
  }
773

774
  /**
775
   * @return a new instance of {@link ProcessContext}.
776
   * @see #newProcess()
777
   */
778
  protected ProcessContext createProcessContext() {
779

780
    return new ProcessContextImpl(this);
5✔
781
  }
782

783
  @Override
784
  public IdeSubLogger level(IdeLogLevel level) {
785

786
    return this.startContext.level(level);
5✔
787
  }
788

789
  @Override
790
  public void logIdeHomeAndRootStatus() {
791

792
    if (this.ideRoot != null) {
3!
793
      success("IDE_ROOT is set to {}", this.ideRoot);
×
794
    }
795
    if (this.ideHome == null) {
3!
796
      warning(getMessageNotInsideIdeProject());
5✔
797
    } else {
798
      success("IDE_HOME is set to {}", this.ideHome);
×
799
    }
800
  }
1✔
801

802
  @Override
803
  public String askForInput(String message, String defaultValue) {
804

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

819
  @Override
820
  public String askForInput(String message) {
821

822
    String input;
823
    do {
824
      info(message);
3✔
825
      input = readLine().trim();
4✔
826
    } while (input.isEmpty());
3!
827

828
    return input;
2✔
829
  }
830

831
  @SuppressWarnings("unchecked")
832
  @Override
833
  public <O> O question(O[] options, String question, Object... args) {
834

835
    assert (options.length >= 2);
5!
836
    interaction(question, args);
4✔
837
    return displayOptionsAndGetAnswer(options);
4✔
838
  }
839

840
  private <O> O displayOptionsAndGetAnswer(O[] options) {
841
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
842
    int i = 0;
2✔
843
    for (O option : options) {
16✔
844
      i++;
1✔
845
      String key = "" + option;
4✔
846
      addMapping(mapping, key, option);
4✔
847
      String numericKey = Integer.toString(i);
3✔
848
      if (numericKey.equals(key)) {
4!
849
        trace("Options should not be numeric: " + key);
×
850
      } else {
851
        addMapping(mapping, numericKey, option);
4✔
852
      }
853
      interaction("Option " + numericKey + ": " + key);
5✔
854
    }
855
    O option = null;
2✔
856
    if (isBatchMode()) {
3!
857
      if (isForceMode() || isForcePull()) {
×
858
        option = options[0];
×
859
        interaction("" + option);
×
860
      }
861
    } else {
862
      while (option == null) {
2✔
863
        String answer = readLine();
3✔
864
        option = mapping.get(answer);
4✔
865
        if (option == null) {
2!
866
          warning("Invalid answer: '" + answer + "' - please try again.");
×
867
        }
868
      }
1✔
869
    }
870
    return option;
2✔
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);
5✔
881
    if (duplicate != null) {
2!
882
      throw new IllegalArgumentException("Duplicated option " + key);
×
883
    }
884
  }
1✔
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
          if (cmd.isIdeHomeRequired()) {
3!
1003
            debug(getMessageIdeHomeFound());
4✔
1004
          }
1005
          Path settingsRepository = getSettingsGitRepository();
3✔
1006
          if (settingsRepository != null) {
2!
1007
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1008
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1009
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1010
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1011
                interaction(
×
1012
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1013

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

1037
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1038

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

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

1098
  @Override
1099
  public void verifyIdeMinVersion(boolean throwException) {
1100
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1101
    if (minVersion == null) {
2✔
1102
      return;
1✔
1103
    }
1104
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1105
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1106
          + "However, this is too old as your project requires at latest version %s\n"
1107
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1108
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1109
      if (throwException) {
2✔
1110
        throw new CliException(message);
5✔
1111
      } else {
1112
        warning(message);
3✔
1113
      }
1114
    }
1115
  }
1✔
1116

1117
  /**
1118
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1119
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1120
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1121
   */
1122
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1123

1124
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1125
    if (arguments.current().isStart()) {
4✔
1126
      arguments.next();
3✔
1127
    }
1128
    if (includeContextOptions) {
2✔
1129
      ContextCommandlet cc = new ContextCommandlet();
4✔
1130
      for (Property<?> property : cc.getProperties()) {
11✔
1131
        assert (property.isOption());
4!
1132
        property.apply(arguments, this, cc, collector);
7✔
1133
      }
1✔
1134
    }
1135
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1136
    CliArgument current = arguments.current();
3✔
1137
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1138
      collector.add(current.get(), null, null, null);
7✔
1139
    }
1140
    arguments.next();
3✔
1141
    while (commandletIterator.hasNext()) {
3✔
1142
      Commandlet cmd = commandletIterator.next();
4✔
1143
      if (!arguments.current().isEnd()) {
4✔
1144
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1145
      }
1146
    }
1✔
1147
    return collector.getSortedCandidates();
3✔
1148
  }
1149

1150
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1151

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

1208
  /**
1209
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1210
   *     {@link CliArguments#copy() copy} as needed.
1211
   * @param cmd the potential {@link Commandlet} to match.
1212
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1213
   */
1214
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1215

1216
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1217
    CliArgument currentArgument = arguments.current();
3✔
1218
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1219
    Property<?> property = null;
2✔
1220
    if (propertyIterator.hasNext()) {
3!
1221
      property = propertyIterator.next();
4✔
1222
    }
1223
    while (!currentArgument.isEnd()) {
3✔
1224
      trace("Trying to match argument '{}'", currentArgument);
9✔
1225
      Property<?> currentProperty = property;
2✔
1226
      if (!arguments.isEndOptions()) {
3!
1227
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1228
        if (option != null) {
2!
1229
          currentProperty = option;
×
1230
        }
1231
      }
1232
      if (currentProperty == null) {
2!
1233
        trace("No option or next value found");
×
1234
        ValidationState state = new ValidationState(null);
×
1235
        state.addErrorMessage("No matching property found");
×
1236
        return state;
×
1237
      }
1238
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1239
      if (currentProperty == property) {
3!
1240
        if (!property.isMultiValued()) {
3✔
1241
          if (propertyIterator.hasNext()) {
3✔
1242
            property = propertyIterator.next();
5✔
1243
          } else {
1244
            property = null;
2✔
1245
          }
1246
        }
1247
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1248
          arguments.stopSplitShortOptions();
2✔
1249
        }
1250
      }
1251
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1252
      if (!matches) {
2!
1253
        ValidationState state = new ValidationState(null);
×
1254
        state.addErrorMessage("No matching property found");
×
1255
        return state;
×
1256
      }
1257
      currentArgument = arguments.current();
3✔
1258
    }
1✔
1259
    return ValidationResultValid.get();
2✔
1260
  }
1261

1262
  @Override
1263
  public String findBash() {
1264

1265
    String bash = "bash";
2✔
1266
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1267
      bash = findBashOnWindows();
×
1268
    }
1269

1270
    return bash;
2✔
1271
  }
1272

1273
  private String findBashOnWindows() {
1274

1275
    // Check if Git Bash exists in the default location
1276
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1277
    if (Files.exists(defaultPath)) {
×
1278
      return defaultPath.toString();
×
1279
    }
1280

1281
    // If not found in the default location, try the registry query
1282
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1283
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1284
    String regQueryResult;
1285
    for (String bashVariant : bashVariants) {
×
1286
      for (String registryKey : registryKeys) {
×
1287
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1288
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1289

1290
        try {
1291
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1292
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1293
            StringBuilder output = new StringBuilder();
×
1294
            String line;
1295

1296
            while ((line = reader.readLine()) != null) {
×
1297
              output.append(line);
×
1298
            }
1299

1300
            int exitCode = process.waitFor();
×
1301
            if (exitCode != 0) {
×
1302
              return null;
×
1303
            }
1304

1305
            regQueryResult = output.toString();
×
1306
            if (regQueryResult != null) {
×
1307
              int index = regQueryResult.indexOf("REG_SZ");
×
1308
              if (index != -1) {
×
1309
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1310
                return path + "\\bin\\bash.exe";
×
1311
              }
1312
            }
1313

1314
          }
×
1315
        } catch (Exception e) {
×
1316
          return null;
×
1317
        }
×
1318
      }
1319
    }
1320
    // no bash found
1321
    return null;
×
1322
  }
1323

1324
  @Override
1325
  public WindowsPathSyntax getPathSyntax() {
1326

1327
    return this.pathSyntax;
3✔
1328
  }
1329

1330
  /**
1331
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1332
   */
1333
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1334

1335
    this.pathSyntax = pathSyntax;
3✔
1336
  }
1✔
1337

1338
  /**
1339
   * @return the {@link IdeStartContextImpl}.
1340
   */
1341
  public IdeStartContextImpl getStartContext() {
1342

1343
    return startContext;
3✔
1344
  }
1345

1346
  /**
1347
   * @return the {@link WindowsHelper}.
1348
   */
1349
  public final WindowsHelper getWindowsHelper() {
1350

1351
    if (this.windowsHelper == null) {
3✔
1352
      this.windowsHelper = createWindowsHelper();
4✔
1353
    }
1354
    return this.windowsHelper;
3✔
1355
  }
1356

1357
  /**
1358
   * @return the new {@link WindowsHelper} instance.
1359
   */
1360
  protected WindowsHelper createWindowsHelper() {
1361

1362
    return new WindowsHelperImpl(this);
×
1363
  }
1364

1365
  /**
1366
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1367
   */
1368
  public void reload() {
1369

1370
    this.variables = null;
3✔
1371
    this.customToolRepository = null;
3✔
1372
  }
1✔
1373

1374
  @Override
1375
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1376

1377
    assert (Files.isDirectory(installationPath));
6!
1378
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1379
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1380
  }
1✔
1381

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

© 2025 Coveralls, Inc