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

devonfw / IDEasy / 15930369557

27 Jun 2025 03:47PM UTC coverage: 68.707% (+0.5%) from 68.252%
15930369557

Pull #1375

github

web-flow
Merge f1fb9af1f into e6e2881bb
Pull Request #1375: #742: Show warning when git repo name does not meet name convention.

3285 of 5190 branches covered (63.29%)

Branch coverage included in aggregate %.

8378 of 11785 relevant lines covered (71.09%)

3.14 hits per line

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

65.3
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.Map.Entry;
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.IdeLogArgFormatter;
46
import com.devonfw.tools.ide.log.IdeLogLevel;
47
import com.devonfw.tools.ide.log.IdeLogger;
48
import com.devonfw.tools.ide.log.IdeSubLogger;
49
import com.devonfw.tools.ide.merge.DirectoryMerger;
50
import com.devonfw.tools.ide.migration.IdeMigrator;
51
import com.devonfw.tools.ide.network.NetworkProxy;
52
import com.devonfw.tools.ide.os.SystemInfo;
53
import com.devonfw.tools.ide.os.SystemInfoImpl;
54
import com.devonfw.tools.ide.os.WindowsHelper;
55
import com.devonfw.tools.ide.os.WindowsHelperImpl;
56
import com.devonfw.tools.ide.os.WindowsPathSyntax;
57
import com.devonfw.tools.ide.process.ProcessContext;
58
import com.devonfw.tools.ide.process.ProcessContextImpl;
59
import com.devonfw.tools.ide.process.ProcessResult;
60
import com.devonfw.tools.ide.property.Property;
61
import com.devonfw.tools.ide.step.Step;
62
import com.devonfw.tools.ide.step.StepImpl;
63
import com.devonfw.tools.ide.tool.repository.CustomToolRepository;
64
import com.devonfw.tools.ide.tool.repository.CustomToolRepositoryImpl;
65
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
66
import com.devonfw.tools.ide.tool.repository.MavenRepository;
67
import com.devonfw.tools.ide.tool.repository.ToolRepository;
68
import com.devonfw.tools.ide.url.model.UrlMetadata;
69
import com.devonfw.tools.ide.util.DateTimeUtil;
70
import com.devonfw.tools.ide.util.PrivacyUtil;
71
import com.devonfw.tools.ide.validation.ValidationResult;
72
import com.devonfw.tools.ide.validation.ValidationResultValid;
73
import com.devonfw.tools.ide.validation.ValidationState;
74
import com.devonfw.tools.ide.variable.IdeVariables;
75
import com.devonfw.tools.ide.version.IdeVersion;
76
import com.devonfw.tools.ide.version.VersionIdentifier;
77

78
/**
79
 * Abstract base implementation of {@link IdeContext}.
80
 */
81
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
82

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

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

87
  private final IdeStartContextImpl startContext;
88

89
  private Path ideHome;
90

91
  private final Path ideRoot;
92

93
  private Path confPath;
94

95
  protected Path settingsPath;
96

97
  private Path settingsCommitIdPath;
98

99
  protected Path pluginsPath;
100

101
  private Path workspacePath;
102

103
  private String workspaceName;
104

105
  private Path cwd;
106

107
  private Path downloadPath;
108

109
  private Path userHome;
110

111
  private Path userHomeIde;
112

113
  private SystemPath path;
114

115
  private WindowsPathSyntax pathSyntax;
116

117
  private final SystemInfo systemInfo;
118

119
  private EnvironmentVariables variables;
120

121
  private final FileAccess fileAccess;
122

123
  protected CommandletManager commandletManager;
124

125
  protected ToolRepository defaultToolRepository;
126

127
  private CustomToolRepository customToolRepository;
128

129
  private final MavenRepository mavenRepository;
130

131
  private DirectoryMerger workspaceMerger;
132

133
  protected UrlMetadata urlMetadata;
134

135
  protected Path defaultExecutionDirectory;
136

137
  private StepImpl currentStep;
138

139
  protected Boolean online;
140

141
  protected IdeSystem system;
142

143
  private NetworkProxy networkProxy;
144

145
  private WindowsHelper windowsHelper;
146

147
  private final Map<String, String> privacyMap;
148

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

157
    super();
2✔
158
    this.startContext = startContext;
3✔
159
    this.startContext.setArgFormatter(this);
4✔
160
    this.privacyMap = new HashMap<>();
5✔
161
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
162
    this.commandletManager = new CommandletManagerImpl(this);
6✔
163
    this.fileAccess = new FileAccessImpl(this);
6✔
164
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
165
    if (userHomeProperty != null) {
2!
166
      this.userHome = Path.of(userHomeProperty);
×
167
    }
168
    String workspace = WORKSPACE_MAIN;
2✔
169
    if (workingDirectory == null) {
2!
170
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
171
    }
172
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
173
    if (Files.isDirectory(workingDirectory)) {
5✔
174
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
175
    } else {
176
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
177
    }
178
    this.cwd = workingDirectory;
3✔
179
    // detect IDE_HOME and WORKSPACE
180
    Path currentDir = workingDirectory;
2✔
181
    String name1 = "";
2✔
182
    String name2 = "";
2✔
183
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
184
    while (currentDir != null) {
2✔
185
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
186
      if (isIdeHome(currentDir)) {
4✔
187
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
188
          workspace = name2;
3✔
189
        }
190
        break;
191
      }
192
      name2 = name1;
2✔
193
      int nameCount = currentDir.getNameCount();
3✔
194
      if (nameCount >= 1) {
3✔
195
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
196
      }
197
      currentDir = currentDir.getParent();
3✔
198
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
199
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
200
        currentDir = null;
×
201
      }
202
    }
1✔
203

204
    // detection completed, initializing variables
205
    this.ideRoot = findIdeRoot(currentDir);
5✔
206

207
    setCwd(workingDirectory, workspace, currentDir);
5✔
208

209
    if (this.ideRoot != null) {
3✔
210
      Path tempDownloadPath = getTempDownloadPath();
3✔
211
      if (Files.isDirectory(tempDownloadPath)) {
6✔
212
        // TODO delete all files older than 1 day here...
213
      } else {
214
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
215
      }
216
    }
217

218
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
219
    this.mavenRepository = new MavenRepository(this);
6✔
220
  }
1✔
221

222
  private Path findIdeRoot(Path ideHomePath) {
223

224
    Path ideRootPath = null;
2✔
225
    if (ideHomePath != null) {
2✔
226
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
227
      ideRootPath = ideHomePath.getParent();
3✔
228
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
229
        warning(
12✔
230
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
231
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
232
            ideRootPathFromEnv,
233
            ideHomePath.getFileName(), ideRootPath);
6✔
234
      }
235
    } else if (!isTest()) {
4!
236
      ideRootPath = getIdeRootPathFromEnv(true);
×
237
    }
238
    return ideRootPath;
2✔
239
  }
240

241
  /**
242
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
243
   */
244
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
245

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

278
  @Override
279
  public void setCwd(Path userDir, String workspace, Path ideHome) {
280

281
    this.cwd = userDir;
3✔
282
    this.workspaceName = workspace;
3✔
283
    this.ideHome = ideHome;
3✔
284
    if (ideHome == null) {
2✔
285
      this.workspacePath = null;
3✔
286
      this.confPath = null;
3✔
287
      this.settingsPath = null;
3✔
288
      this.pluginsPath = null;
4✔
289
    } else {
290
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
291
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
292
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
293
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
294
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
295
    }
296
    if (isTest()) {
3!
297
      // only for testing...
298
      if (this.ideHome == null) {
3✔
299
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
300
      } else {
301
        this.userHome = this.ideHome.resolve("home");
6✔
302
      }
303
    }
304
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
305
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
306
    resetPrivacyMap();
2✔
307
    this.path = computeSystemPath();
4✔
308
  }
1✔
309

310
  private String getMessageIdeHomeFound() {
311

312
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
313
  }
314

315
  private String getMessageNotInsideIdeProject() {
316

317
    return "You are not inside an IDE project: " + this.cwd;
5✔
318
  }
319

320
  private String getMessageIdeRootNotFound() {
321

322
    String root = getSystem().getEnv("IDE_ROOT");
5✔
323
    if (root == null) {
2!
324
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
325
    } else {
326
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
327
    }
328
  }
329

330
  /**
331
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
332
   */
333
  public boolean isTest() {
334

335
    return false;
×
336
  }
337

338
  protected SystemPath computeSystemPath() {
339

340
    return new SystemPath(this);
×
341
  }
342

343
  private boolean isIdeHome(Path dir) {
344

345
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
346
      return false;
2✔
347
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
348
      return false;
×
349
    }
350
    return true;
2✔
351
  }
352

353
  private EnvironmentVariables createVariables() {
354

355
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
356
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
357
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
358
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
359
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
360
    return conf.resolved();
3✔
361
  }
362

363
  protected AbstractEnvironmentVariables createSystemVariables() {
364

365
    return EnvironmentVariables.ofSystem(this);
3✔
366
  }
367

368
  @Override
369
  public SystemInfo getSystemInfo() {
370

371
    return this.systemInfo;
3✔
372
  }
373

374
  @Override
375
  public FileAccess getFileAccess() {
376

377
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
378
    configureNetworkProxy();
2✔
379
    return this.fileAccess;
3✔
380
  }
381

382
  @Override
383
  public CommandletManager getCommandletManager() {
384

385
    return this.commandletManager;
3✔
386
  }
387

388
  @Override
389
  public ToolRepository getDefaultToolRepository() {
390

391
    return this.defaultToolRepository;
3✔
392
  }
393

394
  @Override
395
  public MavenRepository getMavenToolRepository() {
396

397
    return this.mavenRepository;
3✔
398
  }
399

400
  @Override
401
  public CustomToolRepository getCustomToolRepository() {
402

403
    if (this.customToolRepository == null) {
3!
404
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
405
    }
406
    return this.customToolRepository;
3✔
407
  }
408

409
  @Override
410
  public Path getIdeHome() {
411

412
    return this.ideHome;
3✔
413
  }
414

415
  @Override
416
  public String getProjectName() {
417

418
    if (this.ideHome != null) {
3!
419
      return this.ideHome.getFileName().toString();
5✔
420
    }
421
    return "";
×
422
  }
423

424
  @Override
425
  public VersionIdentifier getProjectVersion() {
426

427
    if (this.ideHome != null) {
3!
428
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
429
      if (Files.exists(versionFile)) {
5✔
430
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
431
        return VersionIdentifier.of(version);
3✔
432
      }
433
    }
434
    return IdeMigrator.START_VERSION;
2✔
435
  }
436

437
  @Override
438
  public void setProjectVersion(VersionIdentifier version) {
439

440
    if (this.ideHome == null) {
3!
441
      throw new IllegalStateException("IDE_HOME not available!");
×
442
    }
443
    Objects.requireNonNull(version);
3✔
444
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
445
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
446
  }
1✔
447

448
  @Override
449
  public Path getIdeRoot() {
450

451
    return this.ideRoot;
3✔
452
  }
453

454
  @Override
455
  public Path getIdePath() {
456

457
    Path myIdeRoot = getIdeRoot();
3✔
458
    if (myIdeRoot == null) {
2!
459
      return null;
×
460
    }
461
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
462
  }
463

464
  @Override
465
  public Path getCwd() {
466

467
    return this.cwd;
3✔
468
  }
469

470
  @Override
471
  public Path getTempPath() {
472

473
    Path idePath = getIdePath();
3✔
474
    if (idePath == null) {
2!
475
      return null;
×
476
    }
477
    return idePath.resolve("tmp");
4✔
478
  }
479

480
  @Override
481
  public Path getTempDownloadPath() {
482

483
    Path tmp = getTempPath();
3✔
484
    if (tmp == null) {
2!
485
      return null;
×
486
    }
487
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
488
  }
489

490
  @Override
491
  public Path getUserHome() {
492

493
    return this.userHome;
3✔
494
  }
495

496
  /**
497
   * This method should only be used for tests to mock user home.
498
   *
499
   * @param userHome the new value of {@link #getUserHome()}.
500
   */
501
  protected void setUserHome(Path userHome) {
502

503
    this.userHome = userHome;
3✔
504
    resetPrivacyMap();
2✔
505
  }
1✔
506

507
  @Override
508
  public Path getUserHomeIde() {
509

510
    return this.userHomeIde;
3✔
511
  }
512

513
  @Override
514
  public Path getSettingsPath() {
515

516
    return this.settingsPath;
3✔
517
  }
518

519
  @Override
520
  public Path getSettingsGitRepository() {
521

522
    Path settingsPath = getSettingsPath();
3✔
523
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
524
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
525
      error("Settings repository exists but is not a git repository.");
3✔
526
      return null;
2✔
527
    }
528
    return settingsPath;
2✔
529
  }
530

531
  @Override
532
  public boolean isSettingsRepositorySymlinkOrJunction() {
533

534
    Path settingsPath = getSettingsPath();
3✔
535
    if (settingsPath == null) {
2!
536
      return false;
×
537
    }
538
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
539
  }
540

541
  @Override
542
  public Path getSettingsCommitIdPath() {
543

544
    return this.settingsCommitIdPath;
3✔
545
  }
546

547
  @Override
548
  public Path getConfPath() {
549

550
    return this.confPath;
3✔
551
  }
552

553
  @Override
554
  public Path getSoftwarePath() {
555

556
    if (this.ideHome == null) {
3✔
557
      return null;
2✔
558
    }
559
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
560
  }
561

562
  @Override
563
  public Path getSoftwareExtraPath() {
564

565
    Path softwarePath = getSoftwarePath();
3✔
566
    if (softwarePath == null) {
2!
567
      return null;
×
568
    }
569
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
570
  }
571

572
  @Override
573
  public Path getSoftwareRepositoryPath() {
574

575
    Path idePath = getIdePath();
3✔
576
    if (idePath == null) {
2!
577
      return null;
×
578
    }
579
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
580
  }
581

582
  @Override
583
  public Path getPluginsPath() {
584

585
    return this.pluginsPath;
3✔
586
  }
587

588
  @Override
589
  public String getWorkspaceName() {
590

591
    return this.workspaceName;
3✔
592
  }
593

594
  @Override
595
  public Path getWorkspacePath() {
596

597
    return this.workspacePath;
3✔
598
  }
599

600
  @Override
601
  public Path getDownloadPath() {
602

603
    return this.downloadPath;
3✔
604
  }
605

606
  @Override
607
  public Path getUrlsPath() {
608

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

616
  @Override
617
  public Path getToolRepositoryPath() {
618

619
    Path idePath = getIdePath();
3✔
620
    if (idePath == null) {
2!
621
      return null;
×
622
    }
623
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
624
  }
625

626
  @Override
627
  public SystemPath getPath() {
628

629
    return this.path;
3✔
630
  }
631

632
  @Override
633
  public EnvironmentVariables getVariables() {
634

635
    if (this.variables == null) {
3✔
636
      this.variables = createVariables();
4✔
637
    }
638
    return this.variables;
3✔
639
  }
640

641
  @Override
642
  public UrlMetadata getUrls() {
643

644
    if (this.urlMetadata == null) {
3✔
645
      if (!isTest()) {
3!
646
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
647
      }
648
      this.urlMetadata = new UrlMetadata(this);
6✔
649
    }
650
    return this.urlMetadata;
3✔
651
  }
652

653
  @Override
654
  public boolean isQuietMode() {
655

656
    return this.startContext.isQuietMode();
4✔
657
  }
658

659
  @Override
660
  public boolean isBatchMode() {
661

662
    return this.startContext.isBatchMode();
4✔
663
  }
664

665
  @Override
666
  public boolean isForceMode() {
667

668
    return this.startContext.isForceMode();
4✔
669
  }
670

671
  @Override
672
  public boolean isForcePull() {
673

674
    return this.startContext.isForcePull();
×
675
  }
676

677
  @Override
678
  public boolean isForcePlugins() {
679

680
    return this.startContext.isForcePlugins();
×
681
  }
682

683
  @Override
684
  public boolean isForceRepositories() {
685

686
    return this.startContext.isForceRepositories();
×
687
  }
688

689
  @Override
690
  public boolean isOfflineMode() {
691

692
    return this.startContext.isOfflineMode();
4✔
693
  }
694

695
  @Override
696
  public boolean isPrivacyMode() {
697
    return this.startContext.isPrivacyMode();
4✔
698
  }
699

700
  @Override
701
  public boolean isSkipUpdatesMode() {
702

703
    return this.startContext.isSkipUpdatesMode();
4✔
704
  }
705

706
  @Override
707
  public boolean isOnline() {
708

709
    if (this.online == null) {
3✔
710
      configureNetworkProxy();
2✔
711
      // we currently assume we have only a CLI process that runs shortly
712
      // therefore we run this check only once to save resources when this method is called many times
713
      String url = "https://www.github.com";
2✔
714
      try {
715
        int timeout = 1000;
2✔
716
        //open a connection to github.com and try to retrieve data
717
        //getContent fails if there is no connection
718
        URLConnection connection = new URL(url).openConnection();
6✔
719
        connection.setConnectTimeout(timeout);
3✔
720
        connection.getContent();
3✔
721
        this.online = Boolean.TRUE;
3✔
722
      } catch (Exception e) {
×
723
        if (debug().isEnabled()) {
×
724
          debug().log(e, "Error when trying to connect to {}", url);
×
725
        }
726
        this.online = Boolean.FALSE;
×
727
      }
1✔
728
    }
729
    return this.online.booleanValue();
4✔
730
  }
731

732
  private void configureNetworkProxy() {
733

734
    if (this.networkProxy == null) {
3✔
735
      this.networkProxy = new NetworkProxy(this);
6✔
736
      this.networkProxy.configure();
3✔
737
    }
738
  }
1✔
739

740
  @Override
741
  public Locale getLocale() {
742

743
    Locale locale = this.startContext.getLocale();
4✔
744
    if (locale == null) {
2✔
745
      locale = Locale.getDefault();
2✔
746
    }
747
    return locale;
2✔
748
  }
749

750
  @Override
751
  public DirectoryMerger getWorkspaceMerger() {
752

753
    if (this.workspaceMerger == null) {
3✔
754
      this.workspaceMerger = new DirectoryMerger(this);
6✔
755
    }
756
    return this.workspaceMerger;
3✔
757
  }
758

759
  /**
760
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
761
   */
762
  @Override
763
  public Path getDefaultExecutionDirectory() {
764

765
    return this.defaultExecutionDirectory;
×
766
  }
767

768
  /**
769
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
770
   */
771
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
772

773
    if (defaultExecutionDirectory != null) {
×
774
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
775
    }
776
  }
×
777

778
  @Override
779
  public GitContext getGitContext() {
780

781
    return new GitContextImpl(this);
×
782
  }
783

784
  @Override
785
  public ProcessContext newProcess() {
786

787
    ProcessContext processContext = createProcessContext();
3✔
788
    if (this.defaultExecutionDirectory != null) {
3!
789
      processContext.directory(this.defaultExecutionDirectory);
×
790
    }
791
    return processContext;
2✔
792
  }
793

794
  @Override
795
  public IdeSystem getSystem() {
796

797
    if (this.system == null) {
×
798
      this.system = new IdeSystemImpl(this);
×
799
    }
800
    return this.system;
×
801
  }
802

803
  /**
804
   * @return a new instance of {@link ProcessContext}.
805
   * @see #newProcess()
806
   */
807
  protected ProcessContext createProcessContext() {
808

809
    return new ProcessContextImpl(this);
5✔
810
  }
811

812
  @Override
813
  public IdeSubLogger level(IdeLogLevel level) {
814

815
    return this.startContext.level(level);
5✔
816
  }
817

818
  @Override
819
  public void logIdeHomeAndRootStatus() {
820
    if (this.ideRoot != null) {
3!
821
      success("IDE_ROOT is set to {}", this.ideRoot);
×
822
    }
823
    if (this.ideHome == null) {
3✔
824
      warning(getMessageNotInsideIdeProject());
5✔
825
    } else {
826
      success("IDE_HOME is set to {}", this.ideHome);
10✔
827
    }
828
  }
1✔
829

830
  @Override
831
  public String formatArgument(Object argument) {
832

833
    if (argument == null) {
2✔
834
      return null;
2✔
835
    }
836
    String result = argument.toString();
3✔
837
    if (isPrivacyMode()) {
3✔
838
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
839
        initializePrivacyMap(this.userHome, "~");
×
840
        String projectName = getProjectName();
×
841
        if (!projectName.isEmpty()) {
×
842
          this.privacyMap.put(projectName, "project");
×
843
        }
844
      }
845
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
846
        result = result.replace(entry.getKey(), entry.getValue());
×
847
      }
×
848
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
849
    }
850
    return result;
2✔
851
  }
852

853
  /**
854
   * @param path the sensitive {@link Path} to
855
   * @param replacement the replacement to mask the {@link Path} in log output.
856
   */
857
  protected void initializePrivacyMap(Path path, String replacement) {
858

859
    if (path == null) {
×
860
      return;
×
861
    }
862
    if (this.systemInfo.isWindows()) {
×
863
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
864
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
865
    } else {
866
      this.privacyMap.put(path.toString(), replacement);
×
867
    }
868
  }
×
869

870
  /**
871
   * Resets the privacy map in case fundamental values have changed.
872
   */
873
  private void resetPrivacyMap() {
874

875
    this.privacyMap.clear();
3✔
876
  }
1✔
877

878

879
  @Override
880
  public String askForInput(String message, String defaultValue) {
881

882
    if (!message.isBlank()) {
3!
883
      info(message);
3✔
884
    }
885
    if (isBatchMode()) {
3!
886
      if (isForceMode() || isForcePull()) {
×
887
        return defaultValue;
×
888
      } else {
889
        throw new CliAbortException();
×
890
      }
891
    }
892
    String input = readLine().trim();
4✔
893
    return input.isEmpty() ? defaultValue : input;
5!
894
  }
895

896
  @Override
897
  public String askForInput(String message) {
898

899
    String input;
900
    do {
901
      info(message);
3✔
902
      input = readLine().trim();
4✔
903
    } while (input.isEmpty());
3!
904

905
    return input;
2✔
906
  }
907

908
  @SuppressWarnings("unchecked")
909
  @Override
910
  public <O> O question(O[] options, String question, Object... args) {
911

912
    assert (options.length >= 2);
5!
913
    interaction(question, args);
4✔
914
    return displayOptionsAndGetAnswer(options);
4✔
915
  }
916

917
  private <O> O displayOptionsAndGetAnswer(O[] options) {
918
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
919
    int i = 0;
2✔
920
    for (O option : options) {
16✔
921
      i++;
1✔
922
      String key = "" + option;
4✔
923
      addMapping(mapping, key, option);
4✔
924
      String numericKey = Integer.toString(i);
3✔
925
      if (numericKey.equals(key)) {
4!
926
        trace("Options should not be numeric: " + key);
×
927
      } else {
928
        addMapping(mapping, numericKey, option);
4✔
929
      }
930
      interaction("Option " + numericKey + ": " + key);
5✔
931
    }
932
    O option = null;
2✔
933
    if (isBatchMode()) {
3!
934
      if (isForceMode() || isForcePull()) {
×
935
        option = options[0];
×
936
        interaction("" + option);
×
937
      }
938
    } else {
939
      while (option == null) {
2✔
940
        String answer = readLine();
3✔
941
        option = mapping.get(answer);
4✔
942
        if (option == null) {
2!
943
          warning("Invalid answer: '" + answer + "' - please try again.");
×
944
        }
945
      }
1✔
946
    }
947
    return option;
2✔
948
  }
949

950
  /**
951
   * @return the input from the end-user (e.g. read from the console).
952
   */
953
  protected abstract String readLine();
954

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

957
    O duplicate = mapping.put(key, option);
5✔
958
    if (duplicate != null) {
2!
959
      throw new IllegalArgumentException("Duplicated option " + key);
×
960
    }
961
  }
1✔
962

963
  @Override
964
  public Step getCurrentStep() {
965

966
    return this.currentStep;
×
967
  }
968

969
  @Override
970
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
971

972
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
973
    return this.currentStep;
3✔
974
  }
975

976
  /**
977
   * Internal method to end the running {@link Step}.
978
   *
979
   * @param step the current {@link Step} to end.
980
   */
981
  public void endStep(StepImpl step) {
982

983
    if (step == this.currentStep) {
4!
984
      this.currentStep = this.currentStep.getParent();
6✔
985
    } else {
986
      String currentStepName = "null";
×
987
      if (this.currentStep != null) {
×
988
        currentStepName = this.currentStep.getName();
×
989
      }
990
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
991
    }
992
  }
1✔
993

994
  /**
995
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
996
   *
997
   * @param arguments the {@link CliArgument}.
998
   * @return the return code of the execution.
999
   */
1000
  public int run(CliArguments arguments) {
1001

1002
    CliArgument current = arguments.current();
3✔
1003
    assert (this.currentStep == null);
4!
1004
    boolean supressStepSuccess = false;
2✔
1005
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1006
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1007
    Commandlet cmd = null;
2✔
1008
    ValidationResult result = null;
2✔
1009
    try {
1010
      while (commandletIterator.hasNext()) {
3✔
1011
        cmd = commandletIterator.next();
4✔
1012
        result = applyAndRun(arguments.copy(), cmd);
6✔
1013
        if (result.isValid()) {
3!
1014
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1015
          step.success();
2✔
1016
          return ProcessResult.SUCCESS;
4✔
1017
        }
1018
      }
1019
      this.startContext.activateLogging();
3✔
1020
      verifyIdeMinVersion(false);
3✔
1021
      if (result != null) {
2!
1022
        error(result.getErrorMessage());
×
1023
      }
1024
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1025
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1026
      if (cmd != null) {
2!
1027
        help.commandlet.setValue(cmd);
×
1028
      }
1029
      help.run();
2✔
1030
      return 1;
4✔
1031
    } catch (Throwable t) {
1✔
1032
      this.startContext.activateLogging();
3✔
1033
      step.error(t, true);
4✔
1034
      throw t;
2✔
1035
    } finally {
1036
      step.close();
2✔
1037
      assert (this.currentStep == null);
4!
1038
      step.logSummary(supressStepSuccess);
3✔
1039
    }
1040
  }
1041

1042
  @Override
1043
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1044

1045
    this.startContext.deactivateLogging(threshold);
4✔
1046
    lambda.run();
2✔
1047
    this.startContext.activateLogging();
3✔
1048
  }
1✔
1049

1050
  /**
1051
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1052
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1053
   *     {@link Commandlet} did not match and we have to try a different candidate).
1054
   */
1055
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1056

1057
    IdeLogLevel previousLogLevel = null;
2✔
1058
    cmd.reset();
2✔
1059
    ValidationResult result = apply(arguments, cmd);
5✔
1060
    if (result.isValid()) {
3!
1061
      result = cmd.validate();
3✔
1062
    }
1063
    if (result.isValid()) {
3!
1064
      debug("Running commandlet {}", cmd);
9✔
1065
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1066
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1067
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1068
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1069
      }
1070
      try {
1071
        if (cmd.isProcessableOutput()) {
3!
1072
          if (!debug().isEnabled()) {
×
1073
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1074
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1075
          }
1076
          this.startContext.activateLogging();
×
1077
        } else {
1078
          this.startContext.activateLogging();
3✔
1079
          if (cmd.isIdeHomeRequired()) {
3!
1080
            debug(getMessageIdeHomeFound());
4✔
1081
          }
1082
          Path settingsRepository = getSettingsGitRepository();
3✔
1083
          if (settingsRepository != null) {
2!
1084
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1085
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1086
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1087
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1088
                interaction(
×
1089
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1090

1091
              } else {
1092
                interaction(
×
1093
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1094
              }
1095
            }
1096
          }
1097
        }
1098
        boolean success = ensureLicenseAgreement(cmd);
4✔
1099
        if (!success) {
2!
1100
          return ValidationResultValid.get();
×
1101
        }
1102
        cmd.run();
2✔
1103
      } finally {
1104
        if (previousLogLevel != null) {
2!
1105
          this.startContext.setLogLevel(previousLogLevel);
×
1106
        }
1107
      }
1✔
1108
    } else {
1109
      trace("Commandlet did not match");
×
1110
    }
1111
    return result;
2✔
1112
  }
1113

1114
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1115

1116
    if (isTest()) {
3!
1117
      return true; // ignore for tests
2✔
1118
    }
1119
    getFileAccess().mkdirs(this.userHomeIde);
×
1120
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1121
    if (Files.isRegularFile(licenseAgreement)) {
×
1122
      return true; // success, license already accepted
×
1123
    }
1124
    if (cmd instanceof EnvironmentCommandlet) {
×
1125
      // 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
1126
      // 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
1127
      // printing anything anymore in such case.
1128
      return false;
×
1129
    }
1130
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1131
    if (logLevelInfoDisabled) {
×
1132
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1133
    }
1134
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1135
    if (logLevelInteractionDisabled) {
×
1136
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1137
    }
1138
    StringBuilder sb = new StringBuilder(1180);
×
1139
    sb.append(LOGO).append("""
×
1140
        Welcome to IDEasy!
1141
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1142
        It supports automatic download and installation of arbitrary 3rd party tools.
1143
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1144
        But if explicitly configured, also commercial software that requires an additional license may be used.
1145
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1146
        You are solely responsible for all risks implied by using this software.
1147
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1148
        You will be able to find it online under the following URL:
1149
        """).append(LICENSE_URL);
×
1150
    if (this.ideRoot != null) {
×
1151
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1152
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1153
    }
1154
    info(sb.toString());
×
1155
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1156

1157
    sb.setLength(0);
×
1158
    LocalDateTime now = LocalDateTime.now();
×
1159
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1160
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1161
    try {
1162
      Files.writeString(licenseAgreement, sb);
×
1163
    } catch (Exception e) {
×
1164
      throw new RuntimeException("Failed to save license agreement!", e);
×
1165
    }
×
1166
    if (logLevelInfoDisabled) {
×
1167
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1168
    }
1169
    if (logLevelInteractionDisabled) {
×
1170
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1171
    }
1172
    return true;
×
1173
  }
1174

1175
  @Override
1176
  public void verifyIdeMinVersion(boolean throwException) {
1177
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1178
    if (minVersion == null) {
2✔
1179
      return;
1✔
1180
    }
1181
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1182
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1183
          + "However, this is too old as your project requires at latest version %s\n"
1184
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1185
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1186
      if (throwException) {
2✔
1187
        throw new CliException(message);
5✔
1188
      } else {
1189
        warning(message);
3✔
1190
      }
1191
    }
1192
  }
1✔
1193

1194
  /**
1195
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1196
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1197
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1198
   */
1199
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1200

1201
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1202
    if (arguments.current().isStart()) {
4✔
1203
      arguments.next();
3✔
1204
    }
1205
    if (includeContextOptions) {
2✔
1206
      ContextCommandlet cc = new ContextCommandlet();
4✔
1207
      for (Property<?> property : cc.getProperties()) {
11✔
1208
        assert (property.isOption());
4!
1209
        property.apply(arguments, this, cc, collector);
7✔
1210
      }
1✔
1211
    }
1212
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1213
    CliArgument current = arguments.current();
3✔
1214
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1215
      collector.add(current.get(), null, null, null);
7✔
1216
    }
1217
    arguments.next();
3✔
1218
    while (commandletIterator.hasNext()) {
3✔
1219
      Commandlet cmd = commandletIterator.next();
4✔
1220
      if (!arguments.current().isEnd()) {
4✔
1221
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1222
      }
1223
    }
1✔
1224
    return collector.getSortedCandidates();
3✔
1225
  }
1226

1227
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1228

1229
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1230
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1231
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1232
    List<Property<?>> properties = cmd.getProperties();
3✔
1233
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1234
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1235
    for (Property<?> property : properties) {
10✔
1236
      if (property.isOption()) {
3✔
1237
        optionProperties.add(property);
4✔
1238
      }
1239
    }
1✔
1240
    CliArgument currentArgument = arguments.current();
3✔
1241
    while (!currentArgument.isEnd()) {
3✔
1242
      trace("Trying to match argument '{}'", currentArgument);
9✔
1243
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1244
        if (currentArgument.isCompletion()) {
3✔
1245
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1246
          while (optionIterator.hasNext()) {
3✔
1247
            Property<?> option = optionIterator.next();
4✔
1248
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1249
            if (success) {
2✔
1250
              optionIterator.remove();
2✔
1251
              arguments.next();
3✔
1252
            }
1253
          }
1✔
1254
        } else {
1✔
1255
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1256
          if (option != null) {
2✔
1257
            arguments.next();
3✔
1258
            boolean removed = optionProperties.remove(option);
4✔
1259
            if (!removed) {
2!
1260
              option = null;
×
1261
            }
1262
          }
1263
          if (option == null) {
2✔
1264
            trace("No such option was found.");
3✔
1265
            return;
1✔
1266
          }
1267
        }
1✔
1268
      } else {
1269
        if (valueIterator.hasNext()) {
3✔
1270
          Property<?> valueProperty = valueIterator.next();
4✔
1271
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1272
          if (!success) {
2✔
1273
            trace("Completion cannot match any further.");
3✔
1274
            return;
1✔
1275
          }
1276
        } else {
1✔
1277
          trace("No value left for completion.");
3✔
1278
          return;
1✔
1279
        }
1280
      }
1281
      currentArgument = arguments.current();
4✔
1282
    }
1283
  }
1✔
1284

1285
  /**
1286
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1287
   *     {@link CliArguments#copy() copy} as needed.
1288
   * @param cmd the potential {@link Commandlet} to match.
1289
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1290
   */
1291
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1292

1293
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1294
    CliArgument currentArgument = arguments.current();
3✔
1295
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1296
    Property<?> property = null;
2✔
1297
    if (propertyIterator.hasNext()) {
3!
1298
      property = propertyIterator.next();
4✔
1299
    }
1300
    while (!currentArgument.isEnd()) {
3✔
1301
      trace("Trying to match argument '{}'", currentArgument);
9✔
1302
      Property<?> currentProperty = property;
2✔
1303
      if (!arguments.isEndOptions()) {
3!
1304
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1305
        if (option != null) {
2!
1306
          currentProperty = option;
×
1307
        }
1308
      }
1309
      if (currentProperty == null) {
2!
1310
        trace("No option or next value found");
×
1311
        ValidationState state = new ValidationState(null);
×
1312
        state.addErrorMessage("No matching property found");
×
1313
        return state;
×
1314
      }
1315
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1316
      if (currentProperty == property) {
3!
1317
        if (!property.isMultiValued()) {
3✔
1318
          if (propertyIterator.hasNext()) {
3✔
1319
            property = propertyIterator.next();
5✔
1320
          } else {
1321
            property = null;
2✔
1322
          }
1323
        }
1324
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1325
          arguments.stopSplitShortOptions();
2✔
1326
        }
1327
      }
1328
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1329
      if (!matches) {
2!
1330
        ValidationState state = new ValidationState(null);
×
1331
        state.addErrorMessage("No matching property found");
×
1332
        return state;
×
1333
      }
1334
      currentArgument = arguments.current();
3✔
1335
    }
1✔
1336
    return ValidationResultValid.get();
2✔
1337
  }
1338

1339
  @Override
1340
  public String findBash() {
1341

1342
    String bash = "bash";
2✔
1343
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1344
      bash = findBashOnWindows();
×
1345
    }
1346

1347
    return bash;
2✔
1348
  }
1349

1350
  private String findBashOnWindows() {
1351

1352
    // Check if Git Bash exists in the default location
1353
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1354
    if (Files.exists(defaultPath)) {
×
1355
      return defaultPath.toString();
×
1356
    }
1357

1358
    // If not found in the default location, try the registry query
1359
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1360
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1361
    String regQueryResult;
1362
    for (String bashVariant : bashVariants) {
×
1363
      for (String registryKey : registryKeys) {
×
1364
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1365
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1366

1367
        try {
1368
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1369
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1370
            StringBuilder output = new StringBuilder();
×
1371
            String line;
1372

1373
            while ((line = reader.readLine()) != null) {
×
1374
              output.append(line);
×
1375
            }
1376

1377
            int exitCode = process.waitFor();
×
1378
            if (exitCode != 0) {
×
1379
              return null;
×
1380
            }
1381

1382
            regQueryResult = output.toString();
×
1383
            if (regQueryResult != null) {
×
1384
              int index = regQueryResult.indexOf("REG_SZ");
×
1385
              if (index != -1) {
×
1386
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1387
                return path + "\\bin\\bash.exe";
×
1388
              }
1389
            }
1390

1391
          }
×
1392
        } catch (Exception e) {
×
1393
          return null;
×
1394
        }
×
1395
      }
1396
    }
1397
    // no bash found
1398
    return null;
×
1399
  }
1400

1401
  @Override
1402
  public WindowsPathSyntax getPathSyntax() {
1403

1404
    return this.pathSyntax;
3✔
1405
  }
1406

1407
  /**
1408
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1409
   */
1410
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1411

1412
    this.pathSyntax = pathSyntax;
3✔
1413
  }
1✔
1414

1415
  /**
1416
   * @return the {@link IdeStartContextImpl}.
1417
   */
1418
  public IdeStartContextImpl getStartContext() {
1419

1420
    return startContext;
3✔
1421
  }
1422

1423
  /**
1424
   * @return the {@link WindowsHelper}.
1425
   */
1426
  public final WindowsHelper getWindowsHelper() {
1427

1428
    if (this.windowsHelper == null) {
3✔
1429
      this.windowsHelper = createWindowsHelper();
4✔
1430
    }
1431
    return this.windowsHelper;
3✔
1432
  }
1433

1434
  /**
1435
   * @return the new {@link WindowsHelper} instance.
1436
   */
1437
  protected WindowsHelper createWindowsHelper() {
1438

1439
    return new WindowsHelperImpl(this);
×
1440
  }
1441

1442
  /**
1443
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1444
   */
1445
  public void reload() {
1446

1447
    this.variables = null;
3✔
1448
    this.customToolRepository = null;
3✔
1449
  }
1✔
1450

1451
  @Override
1452
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1453

1454
    assert (Files.isDirectory(installationPath));
6!
1455
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1456
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1457
  }
1✔
1458

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