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

devonfw / IDEasy / 16112656538

07 Jul 2025 09:04AM UTC coverage: 68.411% (+0.5%) from 67.911%
16112656538

Pull #1375

github

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

3285 of 5204 branches covered (63.12%)

Branch coverage included in aggregate %.

8405 of 11884 relevant lines covered (70.73%)

3.12 hits per line

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

65.13
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
    String wks = this.workspaceName;
3✔
313
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
314
      wks = "*".repeat(wks.length());
×
315
    }
316
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
317
  }
318

319
  private String getMessageNotInsideIdeProject() {
320

321
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
322
  }
323

324
  private String getMessageIdeRootNotFound() {
325

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

335
  /**
336
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
337
   */
338
  public boolean isTest() {
339

340
    return false;
×
341
  }
342

343
  protected SystemPath computeSystemPath() {
344

345
    return new SystemPath(this);
×
346
  }
347

348
  private boolean isIdeHome(Path dir) {
349

350
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
351
      return false;
2✔
352
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
353
      return false;
×
354
    }
355
    return true;
2✔
356
  }
357

358
  private EnvironmentVariables createVariables() {
359

360
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
361
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
362
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
363
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
364
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
365
    return conf.resolved();
3✔
366
  }
367

368
  protected AbstractEnvironmentVariables createSystemVariables() {
369

370
    return EnvironmentVariables.ofSystem(this);
3✔
371
  }
372

373
  @Override
374
  public SystemInfo getSystemInfo() {
375

376
    return this.systemInfo;
3✔
377
  }
378

379
  @Override
380
  public FileAccess getFileAccess() {
381

382
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
383
    configureNetworkProxy();
2✔
384
    return this.fileAccess;
3✔
385
  }
386

387
  @Override
388
  public CommandletManager getCommandletManager() {
389

390
    return this.commandletManager;
3✔
391
  }
392

393
  @Override
394
  public ToolRepository getDefaultToolRepository() {
395

396
    return this.defaultToolRepository;
3✔
397
  }
398

399
  @Override
400
  public MavenRepository getMavenToolRepository() {
401

402
    return this.mavenRepository;
3✔
403
  }
404

405
  @Override
406
  public CustomToolRepository getCustomToolRepository() {
407

408
    if (this.customToolRepository == null) {
3!
409
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
410
    }
411
    return this.customToolRepository;
3✔
412
  }
413

414
  @Override
415
  public Path getIdeHome() {
416

417
    return this.ideHome;
3✔
418
  }
419

420
  @Override
421
  public String getProjectName() {
422

423
    if (this.ideHome != null) {
3!
424
      return this.ideHome.getFileName().toString();
5✔
425
    }
426
    return "";
×
427
  }
428

429
  @Override
430
  public VersionIdentifier getProjectVersion() {
431

432
    if (this.ideHome != null) {
3!
433
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
434
      if (Files.exists(versionFile)) {
5✔
435
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
436
        return VersionIdentifier.of(version);
3✔
437
      }
438
    }
439
    return IdeMigrator.START_VERSION;
2✔
440
  }
441

442
  @Override
443
  public void setProjectVersion(VersionIdentifier version) {
444

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

453
  @Override
454
  public Path getIdeRoot() {
455

456
    return this.ideRoot;
3✔
457
  }
458

459
  @Override
460
  public Path getIdePath() {
461

462
    Path myIdeRoot = getIdeRoot();
3✔
463
    if (myIdeRoot == null) {
2!
464
      return null;
×
465
    }
466
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
467
  }
468

469
  @Override
470
  public Path getCwd() {
471

472
    return this.cwd;
3✔
473
  }
474

475
  @Override
476
  public Path getTempPath() {
477

478
    Path idePath = getIdePath();
3✔
479
    if (idePath == null) {
2!
480
      return null;
×
481
    }
482
    return idePath.resolve("tmp");
4✔
483
  }
484

485
  @Override
486
  public Path getTempDownloadPath() {
487

488
    Path tmp = getTempPath();
3✔
489
    if (tmp == null) {
2!
490
      return null;
×
491
    }
492
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
493
  }
494

495
  @Override
496
  public Path getUserHome() {
497

498
    return this.userHome;
3✔
499
  }
500

501
  /**
502
   * This method should only be used for tests to mock user home.
503
   *
504
   * @param userHome the new value of {@link #getUserHome()}.
505
   */
506
  protected void setUserHome(Path userHome) {
507

508
    this.userHome = userHome;
3✔
509
    resetPrivacyMap();
2✔
510
  }
1✔
511

512
  @Override
513
  public Path getUserHomeIde() {
514

515
    return this.userHomeIde;
3✔
516
  }
517

518
  @Override
519
  public Path getSettingsPath() {
520

521
    return this.settingsPath;
3✔
522
  }
523

524
  @Override
525
  public Path getSettingsGitRepository() {
526

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

536
  @Override
537
  public boolean isSettingsRepositorySymlinkOrJunction() {
538

539
    Path settingsPath = getSettingsPath();
3✔
540
    if (settingsPath == null) {
2!
541
      return false;
×
542
    }
543
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
544
  }
545

546
  @Override
547
  public Path getSettingsCommitIdPath() {
548

549
    return this.settingsCommitIdPath;
3✔
550
  }
551

552
  @Override
553
  public Path getConfPath() {
554

555
    return this.confPath;
3✔
556
  }
557

558
  @Override
559
  public Path getSoftwarePath() {
560

561
    if (this.ideHome == null) {
3✔
562
      return null;
2✔
563
    }
564
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
565
  }
566

567
  @Override
568
  public Path getSoftwareExtraPath() {
569

570
    Path softwarePath = getSoftwarePath();
3✔
571
    if (softwarePath == null) {
2!
572
      return null;
×
573
    }
574
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
575
  }
576

577
  @Override
578
  public Path getSoftwareRepositoryPath() {
579

580
    Path idePath = getIdePath();
3✔
581
    if (idePath == null) {
2!
582
      return null;
×
583
    }
584
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
585
  }
586

587
  @Override
588
  public Path getPluginsPath() {
589

590
    return this.pluginsPath;
3✔
591
  }
592

593
  @Override
594
  public String getWorkspaceName() {
595

596
    return this.workspaceName;
3✔
597
  }
598

599
  @Override
600
  public Path getWorkspacePath() {
601

602
    return this.workspacePath;
3✔
603
  }
604

605
  @Override
606
  public Path getDownloadPath() {
607

608
    return this.downloadPath;
3✔
609
  }
610

611
  @Override
612
  public Path getUrlsPath() {
613

614
    Path idePath = getIdePath();
3✔
615
    if (idePath == null) {
2!
616
      return null;
×
617
    }
618
    return idePath.resolve(FOLDER_URLS);
4✔
619
  }
620

621
  @Override
622
  public Path getToolRepositoryPath() {
623

624
    Path idePath = getIdePath();
3✔
625
    if (idePath == null) {
2!
626
      return null;
×
627
    }
628
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
629
  }
630

631
  @Override
632
  public SystemPath getPath() {
633

634
    return this.path;
3✔
635
  }
636

637
  @Override
638
  public EnvironmentVariables getVariables() {
639

640
    if (this.variables == null) {
3✔
641
      this.variables = createVariables();
4✔
642
    }
643
    return this.variables;
3✔
644
  }
645

646
  @Override
647
  public UrlMetadata getUrls() {
648

649
    if (this.urlMetadata == null) {
3✔
650
      if (!isTest()) {
3!
651
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
652
      }
653
      this.urlMetadata = new UrlMetadata(this);
6✔
654
    }
655
    return this.urlMetadata;
3✔
656
  }
657

658
  @Override
659
  public boolean isQuietMode() {
660

661
    return this.startContext.isQuietMode();
4✔
662
  }
663

664
  @Override
665
  public boolean isBatchMode() {
666

667
    return this.startContext.isBatchMode();
4✔
668
  }
669

670
  @Override
671
  public boolean isForceMode() {
672

673
    return this.startContext.isForceMode();
4✔
674
  }
675

676
  @Override
677
  public boolean isForcePull() {
678

679
    return this.startContext.isForcePull();
×
680
  }
681

682
  @Override
683
  public boolean isForcePlugins() {
684

685
    return this.startContext.isForcePlugins();
×
686
  }
687

688
  @Override
689
  public boolean isForceRepositories() {
690

691
    return this.startContext.isForceRepositories();
×
692
  }
693

694
  @Override
695
  public boolean isOfflineMode() {
696

697
    return this.startContext.isOfflineMode();
4✔
698
  }
699

700
  @Override
701
  public boolean isPrivacyMode() {
702
    return this.startContext.isPrivacyMode();
4✔
703
  }
704

705
  @Override
706
  public boolean isSkipUpdatesMode() {
707

708
    return this.startContext.isSkipUpdatesMode();
4✔
709
  }
710

711
  @Override
712
  public boolean isOnline() {
713

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

737
  private void configureNetworkProxy() {
738

739
    if (this.networkProxy == null) {
3✔
740
      this.networkProxy = new NetworkProxy(this);
6✔
741
      this.networkProxy.configure();
3✔
742
    }
743
  }
1✔
744

745
  @Override
746
  public Locale getLocale() {
747

748
    Locale locale = this.startContext.getLocale();
4✔
749
    if (locale == null) {
2✔
750
      locale = Locale.getDefault();
2✔
751
    }
752
    return locale;
2✔
753
  }
754

755
  @Override
756
  public DirectoryMerger getWorkspaceMerger() {
757

758
    if (this.workspaceMerger == null) {
3✔
759
      this.workspaceMerger = new DirectoryMerger(this);
6✔
760
    }
761
    return this.workspaceMerger;
3✔
762
  }
763

764
  /**
765
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
766
   */
767
  @Override
768
  public Path getDefaultExecutionDirectory() {
769

770
    return this.defaultExecutionDirectory;
×
771
  }
772

773
  /**
774
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
775
   */
776
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
777

778
    if (defaultExecutionDirectory != null) {
×
779
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
780
    }
781
  }
×
782

783
  @Override
784
  public GitContext getGitContext() {
785

786
    return new GitContextImpl(this);
×
787
  }
788

789
  @Override
790
  public ProcessContext newProcess() {
791

792
    ProcessContext processContext = createProcessContext();
3✔
793
    if (this.defaultExecutionDirectory != null) {
3!
794
      processContext.directory(this.defaultExecutionDirectory);
×
795
    }
796
    return processContext;
2✔
797
  }
798

799
  @Override
800
  public IdeSystem getSystem() {
801

802
    if (this.system == null) {
×
803
      this.system = new IdeSystemImpl(this);
×
804
    }
805
    return this.system;
×
806
  }
807

808
  /**
809
   * @return a new instance of {@link ProcessContext}.
810
   * @see #newProcess()
811
   */
812
  protected ProcessContext createProcessContext() {
813

814
    return new ProcessContextImpl(this);
5✔
815
  }
816

817
  @Override
818
  public IdeSubLogger level(IdeLogLevel level) {
819

820
    return this.startContext.level(level);
5✔
821
  }
822

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

835
  @Override
836
  public String formatArgument(Object argument) {
837

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

858
  /**
859
   * @param path the sensitive {@link Path} to
860
   * @param replacement the replacement to mask the {@link Path} in log output.
861
   */
862
  protected void initializePrivacyMap(Path path, String replacement) {
863

864
    if (path == null) {
×
865
      return;
×
866
    }
867
    if (this.systemInfo.isWindows()) {
×
868
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
869
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
870
    } else {
871
      this.privacyMap.put(path.toString(), replacement);
×
872
    }
873
  }
×
874

875
  /**
876
   * Resets the privacy map in case fundamental values have changed.
877
   */
878
  private void resetPrivacyMap() {
879

880
    this.privacyMap.clear();
3✔
881
  }
1✔
882

883

884
  @Override
885
  public String askForInput(String message, String defaultValue) {
886

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

901
  @Override
902
  public String askForInput(String message) {
903

904
    String input;
905
    do {
906
      info(message);
3✔
907
      input = readLine().trim();
4✔
908
    } while (input.isEmpty());
3!
909

910
    return input;
2✔
911
  }
912

913
  @SuppressWarnings("unchecked")
914
  @Override
915
  public <O> O question(O[] options, String question, Object... args) {
916

917
    assert (options.length >= 2);
5!
918
    interaction(question, args);
4✔
919
    return displayOptionsAndGetAnswer(options);
4✔
920
  }
921

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

955
  /**
956
   * @return the input from the end-user (e.g. read from the console).
957
   */
958
  protected abstract String readLine();
959

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

962
    O duplicate = mapping.put(key, option);
5✔
963
    if (duplicate != null) {
2!
964
      throw new IllegalArgumentException("Duplicated option " + key);
×
965
    }
966
  }
1✔
967

968
  @Override
969
  public Step getCurrentStep() {
970

971
    return this.currentStep;
×
972
  }
973

974
  @Override
975
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
976

977
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
978
    return this.currentStep;
3✔
979
  }
980

981
  /**
982
   * Internal method to end the running {@link Step}.
983
   *
984
   * @param step the current {@link Step} to end.
985
   */
986
  public void endStep(StepImpl step) {
987

988
    if (step == this.currentStep) {
4!
989
      this.currentStep = this.currentStep.getParent();
6✔
990
    } else {
991
      String currentStepName = "null";
×
992
      if (this.currentStep != null) {
×
993
        currentStepName = this.currentStep.getName();
×
994
      }
995
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
996
    }
997
  }
1✔
998

999
  /**
1000
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1001
   *
1002
   * @param arguments the {@link CliArgument}.
1003
   * @return the return code of the execution.
1004
   */
1005
  public int run(CliArguments arguments) {
1006

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

1047
  @Override
1048
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1049

1050
    this.startContext.deactivateLogging(threshold);
4✔
1051
    lambda.run();
2✔
1052
    this.startContext.activateLogging();
3✔
1053
  }
1✔
1054

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

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

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

1119
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1120

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

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

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

1199
  /**
1200
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1201
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1202
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1203
   */
1204
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1205

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

1232
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1233

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

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

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

1344
  @Override
1345
  public String findBash() {
1346

1347
    String bash = "bash";
2✔
1348
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1349
      bash = findBashOnWindows();
×
1350
    }
1351

1352
    return bash;
2✔
1353
  }
1354

1355
  private String findBashOnWindows() {
1356

1357
    // Check if Git Bash exists in the default location
1358
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1359
    if (Files.exists(defaultPath)) {
×
1360
      return defaultPath.toString();
×
1361
    }
1362

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

1372
        try {
1373
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1374
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1375
            StringBuilder output = new StringBuilder();
×
1376
            String line;
1377

1378
            while ((line = reader.readLine()) != null) {
×
1379
              output.append(line);
×
1380
            }
1381

1382
            int exitCode = process.waitFor();
×
1383
            if (exitCode != 0) {
×
1384
              return null;
×
1385
            }
1386

1387
            regQueryResult = output.toString();
×
1388
            if (regQueryResult != null) {
×
1389
              int index = regQueryResult.indexOf("REG_SZ");
×
1390
              if (index != -1) {
×
1391
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1392
                return path + "\\bin\\bash.exe";
×
1393
              }
1394
            }
1395

1396
          }
×
1397
        } catch (Exception e) {
×
1398
          return null;
×
1399
        }
×
1400
      }
1401
    }
1402
    // no bash found
1403
    return null;
×
1404
  }
1405

1406
  @Override
1407
  public WindowsPathSyntax getPathSyntax() {
1408

1409
    return this.pathSyntax;
3✔
1410
  }
1411

1412
  /**
1413
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1414
   */
1415
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1416

1417
    this.pathSyntax = pathSyntax;
3✔
1418
  }
1✔
1419

1420
  /**
1421
   * @return the {@link IdeStartContextImpl}.
1422
   */
1423
  public IdeStartContextImpl getStartContext() {
1424

1425
    return startContext;
3✔
1426
  }
1427

1428
  /**
1429
   * @return the {@link WindowsHelper}.
1430
   */
1431
  public final WindowsHelper getWindowsHelper() {
1432

1433
    if (this.windowsHelper == null) {
3✔
1434
      this.windowsHelper = createWindowsHelper();
4✔
1435
    }
1436
    return this.windowsHelper;
3✔
1437
  }
1438

1439
  /**
1440
   * @return the new {@link WindowsHelper} instance.
1441
   */
1442
  protected WindowsHelper createWindowsHelper() {
1443

1444
    return new WindowsHelperImpl(this);
×
1445
  }
1446

1447
  /**
1448
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1449
   */
1450
  public void reload() {
1451

1452
    this.variables = null;
3✔
1453
    this.customToolRepository = null;
3✔
1454
  }
1✔
1455

1456
  @Override
1457
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1458

1459
    assert (Files.isDirectory(installationPath));
6!
1460
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1461
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1462
  }
1✔
1463

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