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

devonfw / IDEasy / 18684819019

21 Oct 2025 01:06PM UTC coverage: 68.534% (+0.01%) from 68.522%
18684819019

Pull #1532

github

web-flow
Merge ce365aa00 into 03c8a307b
Pull Request #1532: #1475: Fix isOnline check for Wiremock tests.

3468 of 5541 branches covered (62.59%)

Branch coverage included in aggregate %.

9058 of 12736 relevant lines covered (71.12%)

3.13 hits per line

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

63.87
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.MvnRepository;
67
import com.devonfw.tools.ide.tool.repository.NpmRepository;
68
import com.devonfw.tools.ide.tool.repository.ToolRepository;
69
import com.devonfw.tools.ide.url.model.UrlMetadata;
70
import com.devonfw.tools.ide.util.DateTimeUtil;
71
import com.devonfw.tools.ide.util.PrivacyUtil;
72
import com.devonfw.tools.ide.validation.ValidationResult;
73
import com.devonfw.tools.ide.validation.ValidationResultValid;
74
import com.devonfw.tools.ide.validation.ValidationState;
75
import com.devonfw.tools.ide.variable.IdeVariables;
76
import com.devonfw.tools.ide.version.IdeVersion;
77
import com.devonfw.tools.ide.version.VersionIdentifier;
78

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

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

86
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
87
  public static final String BASH = "bash";
88

89
  private final IdeStartContextImpl startContext;
90

91
  private Path ideHome;
92

93
  private final Path ideRoot;
94

95
  private Path confPath;
96

97
  protected Path settingsPath;
98

99
  private Path settingsCommitIdPath;
100

101
  protected Path pluginsPath;
102

103
  private Path workspacePath;
104

105
  private String workspaceName;
106

107
  private Path cwd;
108

109
  private Path downloadPath;
110

111
  private Path userHome;
112

113
  private Path userHomeIde;
114

115
  private SystemPath path;
116

117
  private WindowsPathSyntax pathSyntax;
118

119
  private final SystemInfo systemInfo;
120

121
  private EnvironmentVariables variables;
122

123
  private final FileAccess fileAccess;
124

125
  protected CommandletManager commandletManager;
126

127
  protected ToolRepository defaultToolRepository;
128

129
  private CustomToolRepository customToolRepository;
130

131
  private MvnRepository mvnRepository;
132

133
  private NpmRepository npmRepository;
134

135
  private DirectoryMerger workspaceMerger;
136

137
  protected UrlMetadata urlMetadata;
138

139
  protected Path defaultExecutionDirectory;
140

141
  private StepImpl currentStep;
142

143
  protected Boolean online;
144

145
  protected IdeSystem system;
146

147
  private NetworkProxy networkProxy;
148

149
  private WindowsHelper windowsHelper;
150

151
  private final Map<String, String> privacyMap;
152

153
  /**
154
   * The constructor.
155
   *
156
   * @param startContext the {@link IdeLogger}.
157
   * @param workingDirectory the optional {@link Path} to current working directory.
158
   */
159
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
160

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

208
    // detection completed, initializing variables
209
    this.ideRoot = findIdeRoot(currentDir);
5✔
210

211
    setCwd(workingDirectory, workspace, currentDir);
5✔
212

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

222
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
223
  }
1✔
224

225
  /**
226
   * @return a new {@link MvnRepository}
227
   */
228
  protected MvnRepository createMvnRepository() {
229
    return new MvnRepository(this);
5✔
230
  }
231

232
  /**
233
   * @return a new {@link NpmRepository}
234
   */
235
  protected NpmRepository createNpmRepository() {
236
    return new NpmRepository(this);
5✔
237
  }
238

239
  private Path findIdeRoot(Path ideHomePath) {
240

241
    Path ideRootPath = null;
2✔
242
    if (ideHomePath != null) {
2✔
243
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
244
      ideRootPath = ideHomePath.getParent();
3✔
245
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
246
        warning(
12✔
247
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
248
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
249
            ideRootPathFromEnv,
250
            ideHomePath.getFileName(), ideRootPath);
6✔
251
      }
252
    } else if (!isTest()) {
4!
253
      ideRootPath = getIdeRootPathFromEnv(true);
×
254
    }
255
    return ideRootPath;
2✔
256
  }
257

258
  /**
259
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
260
   */
261
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
262

263
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
264
    if (root != null) {
×
265
      Path rootPath = Path.of(root);
×
266
      if (Files.isDirectory(rootPath)) {
×
267
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
268
        if (withSanityCheck) {
×
269
          int nameCount = rootPath.getNameCount();
×
270
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
271
          int delta = absoluteNameCount - nameCount;
×
272
          if (delta >= 0) {
×
273
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
274
              String rootName = rootPath.getName(nameIndex).toString();
×
275
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
276
              if (!rootName.equals(absoluteRootName)) {
×
277
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
278
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
279
                break;
×
280
              }
281
            }
282
          } else {
283
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
284
                absoluteRootPath);
285
          }
286
        }
287
        return absoluteRootPath;
×
288
      } else if (withSanityCheck) {
×
289
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
290
      }
291
    }
292
    return null;
×
293
  }
294

295
  @Override
296
  public void setCwd(Path userDir, String workspace, Path ideHome) {
297

298
    this.cwd = userDir;
3✔
299
    this.workspaceName = workspace;
3✔
300
    this.ideHome = ideHome;
3✔
301
    if (ideHome == null) {
2✔
302
      this.workspacePath = null;
3✔
303
      this.confPath = null;
3✔
304
      this.settingsPath = null;
3✔
305
      this.pluginsPath = null;
4✔
306
    } else {
307
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
308
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
309
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
310
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
311
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
312
    }
313
    if (isTest()) {
3!
314
      // only for testing...
315
      if (this.ideHome == null) {
3✔
316
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
317
      } else {
318
        this.userHome = this.ideHome.resolve("home");
6✔
319
      }
320
    }
321
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
322
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
323
    resetPrivacyMap();
2✔
324
    this.path = computeSystemPath();
4✔
325
  }
1✔
326

327
  private String getMessageIdeHomeFound() {
328

329
    String wks = this.workspaceName;
3✔
330
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
331
      wks = "*".repeat(wks.length());
×
332
    }
333
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
334
  }
335

336
  private String getMessageNotInsideIdeProject() {
337

338
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
339
  }
340

341
  private String getMessageIdeRootNotFound() {
342

343
    String root = getSystem().getEnv("IDE_ROOT");
5✔
344
    if (root == null) {
2!
345
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
346
    } else {
347
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
348
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
349
    }
350
  }
351

352
  /**
353
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
354
   */
355
  public boolean isTest() {
356

357
    return false;
×
358
  }
359

360
  protected SystemPath computeSystemPath() {
361

362
    return new SystemPath(this);
×
363
  }
364

365
  private boolean isIdeHome(Path dir) {
366

367
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
368
      return false;
2✔
369
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
370
      return false;
×
371
    }
372
    return true;
2✔
373
  }
374

375
  private EnvironmentVariables createVariables() {
376

377
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
378
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
379
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
380
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
381
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
382
    return conf.resolved();
3✔
383
  }
384

385
  protected AbstractEnvironmentVariables createSystemVariables() {
386

387
    return EnvironmentVariables.ofSystem(this);
3✔
388
  }
389

390
  @Override
391
  public SystemInfo getSystemInfo() {
392

393
    return this.systemInfo;
3✔
394
  }
395

396
  @Override
397
  public FileAccess getFileAccess() {
398

399
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
400
    configureNetworkProxy();
2✔
401
    return this.fileAccess;
3✔
402
  }
403

404
  @Override
405
  public CommandletManager getCommandletManager() {
406

407
    return this.commandletManager;
3✔
408
  }
409

410
  @Override
411
  public ToolRepository getDefaultToolRepository() {
412

413
    return this.defaultToolRepository;
3✔
414
  }
415

416
  @Override
417
  public MvnRepository getMvnRepository() {
418
    if (this.mvnRepository == null) {
3✔
419
      this.mvnRepository = createMvnRepository();
4✔
420
    }
421
    return this.mvnRepository;
3✔
422
  }
423

424
  @Override
425
  public NpmRepository getNpmRepository() {
426
    if (this.npmRepository == null) {
3✔
427
      this.npmRepository = createNpmRepository();
4✔
428
    }
429
    return this.npmRepository;
3✔
430
  }
431

432
  @Override
433
  public CustomToolRepository getCustomToolRepository() {
434

435
    if (this.customToolRepository == null) {
3!
436
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
437
    }
438
    return this.customToolRepository;
3✔
439
  }
440

441
  @Override
442
  public Path getIdeHome() {
443

444
    return this.ideHome;
3✔
445
  }
446

447
  @Override
448
  public String getProjectName() {
449

450
    if (this.ideHome != null) {
3!
451
      return this.ideHome.getFileName().toString();
5✔
452
    }
453
    return "";
×
454
  }
455

456
  @Override
457
  public VersionIdentifier getProjectVersion() {
458

459
    if (this.ideHome != null) {
3!
460
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
461
      if (Files.exists(versionFile)) {
5✔
462
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
463
        return VersionIdentifier.of(version);
3✔
464
      }
465
    }
466
    return IdeMigrator.START_VERSION;
2✔
467
  }
468

469
  @Override
470
  public void setProjectVersion(VersionIdentifier version) {
471

472
    if (this.ideHome == null) {
3!
473
      throw new IllegalStateException("IDE_HOME not available!");
×
474
    }
475
    Objects.requireNonNull(version);
3✔
476
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
477
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
478
  }
1✔
479

480
  @Override
481
  public Path getIdeRoot() {
482

483
    return this.ideRoot;
3✔
484
  }
485

486
  @Override
487
  public Path getIdePath() {
488

489
    Path myIdeRoot = getIdeRoot();
3✔
490
    if (myIdeRoot == null) {
2!
491
      return null;
×
492
    }
493
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
494
  }
495

496
  @Override
497
  public Path getCwd() {
498

499
    return this.cwd;
3✔
500
  }
501

502
  @Override
503
  public Path getTempPath() {
504

505
    Path idePath = getIdePath();
3✔
506
    if (idePath == null) {
2!
507
      return null;
×
508
    }
509
    return idePath.resolve("tmp");
4✔
510
  }
511

512
  @Override
513
  public Path getTempDownloadPath() {
514

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

522
  @Override
523
  public Path getUserHome() {
524

525
    return this.userHome;
3✔
526
  }
527

528
  /**
529
   * This method should only be used for tests to mock user home.
530
   *
531
   * @param userHome the new value of {@link #getUserHome()}.
532
   */
533
  protected void setUserHome(Path userHome) {
534

535
    this.userHome = userHome;
3✔
536
    resetPrivacyMap();
2✔
537
  }
1✔
538

539
  @Override
540
  public Path getUserHomeIde() {
541

542
    return this.userHomeIde;
3✔
543
  }
544

545
  @Override
546
  public Path getSettingsPath() {
547

548
    return this.settingsPath;
3✔
549
  }
550

551
  @Override
552
  public Path getSettingsGitRepository() {
553

554
    Path settingsPath = getSettingsPath();
3✔
555
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
556
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
557
      error("Settings repository exists but is not a git repository.");
3✔
558
      return null;
2✔
559
    }
560
    return settingsPath;
2✔
561
  }
562

563
  @Override
564
  public boolean isSettingsRepositorySymlinkOrJunction() {
565

566
    Path settingsPath = getSettingsPath();
3✔
567
    if (settingsPath == null) {
2!
568
      return false;
×
569
    }
570
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
571
  }
572

573
  @Override
574
  public Path getSettingsCommitIdPath() {
575

576
    return this.settingsCommitIdPath;
3✔
577
  }
578

579
  @Override
580
  public Path getConfPath() {
581

582
    return this.confPath;
3✔
583
  }
584

585
  @Override
586
  public Path getSoftwarePath() {
587

588
    if (this.ideHome == null) {
3✔
589
      return null;
2✔
590
    }
591
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
592
  }
593

594
  @Override
595
  public Path getSoftwareExtraPath() {
596

597
    Path softwarePath = getSoftwarePath();
3✔
598
    if (softwarePath == null) {
2!
599
      return null;
×
600
    }
601
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
602
  }
603

604
  @Override
605
  public Path getSoftwareRepositoryPath() {
606

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

614
  @Override
615
  public Path getPluginsPath() {
616

617
    return this.pluginsPath;
3✔
618
  }
619

620
  @Override
621
  public String getWorkspaceName() {
622

623
    return this.workspaceName;
3✔
624
  }
625

626
  @Override
627
  public Path getWorkspacePath() {
628

629
    return this.workspacePath;
3✔
630
  }
631

632
  @Override
633
  public Path getDownloadPath() {
634

635
    return this.downloadPath;
3✔
636
  }
637

638
  @Override
639
  public Path getUrlsPath() {
640

641
    Path idePath = getIdePath();
3✔
642
    if (idePath == null) {
2!
643
      return null;
×
644
    }
645
    return idePath.resolve(FOLDER_URLS);
4✔
646
  }
647

648
  @Override
649
  public Path getToolRepositoryPath() {
650

651
    Path idePath = getIdePath();
3✔
652
    if (idePath == null) {
2!
653
      return null;
×
654
    }
655
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
656
  }
657

658
  @Override
659
  public SystemPath getPath() {
660

661
    return this.path;
3✔
662
  }
663

664
  @Override
665
  public EnvironmentVariables getVariables() {
666

667
    if (this.variables == null) {
3✔
668
      this.variables = createVariables();
4✔
669
    }
670
    return this.variables;
3✔
671
  }
672

673
  @Override
674
  public UrlMetadata getUrls() {
675

676
    if (this.urlMetadata == null) {
3✔
677
      if (!isTest()) {
3!
678
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
679
      }
680
      this.urlMetadata = new UrlMetadata(this);
6✔
681
    }
682
    return this.urlMetadata;
3✔
683
  }
684

685
  @Override
686
  public boolean isQuietMode() {
687

688
    return this.startContext.isQuietMode();
4✔
689
  }
690

691
  @Override
692
  public boolean isBatchMode() {
693

694
    return this.startContext.isBatchMode();
4✔
695
  }
696

697
  @Override
698
  public boolean isForceMode() {
699

700
    return this.startContext.isForceMode();
4✔
701
  }
702

703
  @Override
704
  public boolean isForcePull() {
705

706
    return this.startContext.isForcePull();
4✔
707
  }
708

709
  @Override
710
  public boolean isForcePlugins() {
711

712
    return this.startContext.isForcePlugins();
4✔
713
  }
714

715
  @Override
716
  public boolean isForceRepositories() {
717

718
    return this.startContext.isForceRepositories();
4✔
719
  }
720

721
  @Override
722
  public boolean isOfflineMode() {
723

724
    return this.startContext.isOfflineMode();
4✔
725
  }
726

727
  @Override
728
  public boolean isPrivacyMode() {
729
    return this.startContext.isPrivacyMode();
4✔
730
  }
731

732
  @Override
733
  public boolean isSkipUpdatesMode() {
734

735
    return this.startContext.isSkipUpdatesMode();
4✔
736
  }
737

738
  @Override
739
  public boolean isNoColorsMode() {
740

741
    return this.startContext.isNoColorsMode();
×
742
  }
743

744
  @Override
745
  public boolean isOnline() {
746
    // we currently assume we have only a CLI process that runs shortly
747
    // therefore we run this check only once to save resources when this method is called many times
748
    String url = "https://www.github.com";
2✔
749
    return isUrlReachable(url);
4✔
750
  }
751

752
  /**
753
   * This method will be used to test the connection to the given url.
754
   *
755
   * @param url the url to test.
756
   */
757
  protected boolean isUrlReachable(String url) {
758
    if (this.online == null) {
3✔
759
      configureNetworkProxy();
2✔
760
      try {
761
        int timeout = 1000;
2✔
762
        //open a connection to github.com and try to retrieve data
763
        //getContent fails if there is no connection
764
        URLConnection connection = new URL(url).openConnection();
6✔
765
        connection.setConnectTimeout(timeout);
3✔
766
        connection.getContent();
3✔
767
        this.online = Boolean.TRUE;
3✔
768
      } catch (Exception e) {
×
769
        if (debug().isEnabled()) {
×
770
          debug().log(e, "Error when trying to connect to {}", url);
×
771
        }
772
        this.online = Boolean.FALSE;
×
773
      }
1✔
774
    }
775
    return this.online;
4✔
776
  }
777

778
  private void configureNetworkProxy() {
779

780
    if (this.networkProxy == null) {
3✔
781
      this.networkProxy = new NetworkProxy(this);
6✔
782
      this.networkProxy.configure();
3✔
783
    }
784
  }
1✔
785

786
  @Override
787
  public Locale getLocale() {
788

789
    Locale locale = this.startContext.getLocale();
4✔
790
    if (locale == null) {
2✔
791
      locale = Locale.getDefault();
2✔
792
    }
793
    return locale;
2✔
794
  }
795

796
  @Override
797
  public DirectoryMerger getWorkspaceMerger() {
798

799
    if (this.workspaceMerger == null) {
3✔
800
      this.workspaceMerger = new DirectoryMerger(this);
6✔
801
    }
802
    return this.workspaceMerger;
3✔
803
  }
804

805
  /**
806
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
807
   */
808
  @Override
809
  public Path getDefaultExecutionDirectory() {
810

811
    return this.defaultExecutionDirectory;
×
812
  }
813

814
  /**
815
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
816
   */
817
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
818

819
    if (defaultExecutionDirectory != null) {
×
820
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
821
    }
822
  }
×
823

824
  @Override
825
  public GitContext getGitContext() {
826

827
    return new GitContextImpl(this);
×
828
  }
829

830
  @Override
831
  public ProcessContext newProcess() {
832

833
    ProcessContext processContext = createProcessContext();
3✔
834
    if (this.defaultExecutionDirectory != null) {
3!
835
      processContext.directory(this.defaultExecutionDirectory);
×
836
    }
837
    return processContext;
2✔
838
  }
839

840
  @Override
841
  public IdeSystem getSystem() {
842

843
    if (this.system == null) {
×
844
      this.system = new IdeSystemImpl(this);
×
845
    }
846
    return this.system;
×
847
  }
848

849
  /**
850
   * @return a new instance of {@link ProcessContext}.
851
   * @see #newProcess()
852
   */
853
  protected ProcessContext createProcessContext() {
854

855
    return new ProcessContextImpl(this);
5✔
856
  }
857

858
  @Override
859
  public IdeSubLogger level(IdeLogLevel level) {
860

861
    return this.startContext.level(level);
5✔
862
  }
863

864
  @Override
865
  public void logIdeHomeAndRootStatus() {
866
    if (this.ideRoot != null) {
3!
867
      success("IDE_ROOT is set to {}", this.ideRoot);
×
868
    }
869
    if (this.ideHome == null) {
3✔
870
      warning(getMessageNotInsideIdeProject());
5✔
871
    } else {
872
      success("IDE_HOME is set to {}", this.ideHome);
10✔
873
    }
874
  }
1✔
875

876
  @Override
877
  public String formatArgument(Object argument) {
878

879
    if (argument == null) {
2✔
880
      return null;
2✔
881
    }
882
    String result = argument.toString();
3✔
883
    if (isPrivacyMode()) {
3✔
884
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
885
        initializePrivacyMap(this.userHome, "~");
×
886
        String projectName = getProjectName();
×
887
        if (!projectName.isEmpty()) {
×
888
          this.privacyMap.put(projectName, "project");
×
889
        }
890
      }
891
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
892
        result = result.replace(entry.getKey(), entry.getValue());
×
893
      }
×
894
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
895
    }
896
    return result;
2✔
897
  }
898

899
  /**
900
   * @param path the sensitive {@link Path} to
901
   * @param replacement the replacement to mask the {@link Path} in log output.
902
   */
903
  protected void initializePrivacyMap(Path path, String replacement) {
904

905
    if (path == null) {
×
906
      return;
×
907
    }
908
    if (this.systemInfo.isWindows()) {
×
909
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
910
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
911
    } else {
912
      this.privacyMap.put(path.toString(), replacement);
×
913
    }
914
  }
×
915

916
  /**
917
   * Resets the privacy map in case fundamental values have changed.
918
   */
919
  private void resetPrivacyMap() {
920

921
    this.privacyMap.clear();
3✔
922
  }
1✔
923

924

925
  @Override
926
  public String askForInput(String message, String defaultValue) {
927

928
    while (true) {
929
      if (!message.isBlank()) {
3!
930
        interaction(message);
3✔
931
      }
932
      if (isBatchMode()) {
3!
933
        if (isForceMode()) {
×
934
          return defaultValue;
×
935
        } else {
936
          throw new CliAbortException();
×
937
        }
938
      }
939
      String input = readLine().trim();
4✔
940
      if (!input.isEmpty()) {
3!
941
        return input;
2✔
942
      } else {
943
        if (defaultValue != null) {
×
944
          return defaultValue;
×
945
        }
946
      }
947
    }
×
948
  }
949

950
  @SuppressWarnings("unchecked")
951
  @Override
952
  public <O> O question(O[] options, String question, Object... args) {
953

954
    assert (options.length >= 2);
5!
955
    interaction(question, args);
4✔
956
    return displayOptionsAndGetAnswer(options);
4✔
957
  }
958

959
  private <O> O displayOptionsAndGetAnswer(O[] options) {
960
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
961
    int i = 0;
2✔
962
    for (O option : options) {
16✔
963
      i++;
1✔
964
      String key = "" + option;
4✔
965
      addMapping(mapping, key, option);
4✔
966
      String numericKey = Integer.toString(i);
3✔
967
      if (numericKey.equals(key)) {
4!
968
        trace("Options should not be numeric: " + key);
×
969
      } else {
970
        addMapping(mapping, numericKey, option);
4✔
971
      }
972
      interaction("Option " + numericKey + ": " + key);
5✔
973
    }
974
    O option = null;
2✔
975
    if (isBatchMode()) {
3!
976
      if (isForceMode()) {
×
977
        option = options[0];
×
978
        interaction("" + option);
×
979
      }
980
    } else {
981
      while (option == null) {
2✔
982
        String answer = readLine();
3✔
983
        option = mapping.get(answer);
4✔
984
        if (option == null) {
2!
985
          warning("Invalid answer: '" + answer + "' - please try again.");
×
986
        }
987
      }
1✔
988
    }
989
    return option;
2✔
990
  }
991

992
  /**
993
   * @return the input from the end-user (e.g. read from the console).
994
   */
995
  protected abstract String readLine();
996

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

999
    O duplicate = mapping.put(key, option);
5✔
1000
    if (duplicate != null) {
2!
1001
      throw new IllegalArgumentException("Duplicated option " + key);
×
1002
    }
1003
  }
1✔
1004

1005
  @Override
1006
  public Step getCurrentStep() {
1007

1008
    return this.currentStep;
×
1009
  }
1010

1011
  @Override
1012
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1013

1014
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1015
    return this.currentStep;
3✔
1016
  }
1017

1018
  /**
1019
   * Internal method to end the running {@link Step}.
1020
   *
1021
   * @param step the current {@link Step} to end.
1022
   */
1023
  public void endStep(StepImpl step) {
1024

1025
    if (step == this.currentStep) {
4!
1026
      this.currentStep = this.currentStep.getParent();
6✔
1027
    } else {
1028
      String currentStepName = "null";
×
1029
      if (this.currentStep != null) {
×
1030
        currentStepName = this.currentStep.getName();
×
1031
      }
1032
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1033
    }
1034
  }
1✔
1035

1036
  /**
1037
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1038
   *
1039
   * @param arguments the {@link CliArgument}.
1040
   * @return the return code of the execution.
1041
   */
1042
  public int run(CliArguments arguments) {
1043

1044
    CliArgument current = arguments.current();
3✔
1045
    assert (this.currentStep == null);
4!
1046
    boolean supressStepSuccess = false;
2✔
1047
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1048
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1049
    Commandlet cmd = null;
2✔
1050
    ValidationResult result = null;
2✔
1051
    try {
1052
      while (commandletIterator.hasNext()) {
3✔
1053
        cmd = commandletIterator.next();
4✔
1054
        result = applyAndRun(arguments.copy(), cmd);
6✔
1055
        if (result.isValid()) {
3!
1056
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1057
          step.success();
2✔
1058
          return ProcessResult.SUCCESS;
4✔
1059
        }
1060
      }
1061
      this.startContext.activateLogging();
3✔
1062
      verifyIdeMinVersion(false);
3✔
1063
      if (result != null) {
2!
1064
        error(result.getErrorMessage());
×
1065
      }
1066
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1067
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1068
      if (cmd != null) {
2!
1069
        help.commandlet.setValue(cmd);
×
1070
      }
1071
      help.run();
2✔
1072
      return 1;
4✔
1073
    } catch (Throwable t) {
1✔
1074
      this.startContext.activateLogging();
3✔
1075
      step.error(t, true);
4✔
1076
      throw t;
2✔
1077
    } finally {
1078
      step.close();
2✔
1079
      assert (this.currentStep == null);
4!
1080
      step.logSummary(supressStepSuccess);
3✔
1081
    }
1082
  }
1083

1084
  @Override
1085
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1086

1087
    this.startContext.deactivateLogging(threshold);
4✔
1088
    lambda.run();
2✔
1089
    this.startContext.activateLogging();
3✔
1090
  }
1✔
1091

1092
  /**
1093
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1094
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1095
   *     {@link Commandlet} did not match and we have to try a different candidate).
1096
   */
1097
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1098

1099
    IdeLogLevel previousLogLevel = null;
2✔
1100
    cmd.reset();
2✔
1101
    ValidationResult result = apply(arguments, cmd);
5✔
1102
    if (result.isValid()) {
3!
1103
      result = cmd.validate();
3✔
1104
    }
1105
    if (result.isValid()) {
3!
1106
      debug("Running commandlet {}", cmd);
9✔
1107
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1108
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1109
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1110
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1111
      }
1112
      try {
1113
        if (cmd.isProcessableOutput()) {
3!
1114
          if (!debug().isEnabled()) {
×
1115
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1116
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1117
          }
1118
          this.startContext.activateLogging();
×
1119
        } else {
1120
          this.startContext.activateLogging();
3✔
1121
          if (cmd.isIdeHomeRequired()) {
3!
1122
            debug(getMessageIdeHomeFound());
4✔
1123
          }
1124
          Path settingsRepository = getSettingsGitRepository();
3✔
1125
          if (settingsRepository != null) {
2!
1126
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1127
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1128
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1129
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1130
                interaction(
×
1131
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1132

1133
              } else {
1134
                interaction(
×
1135
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1136
              }
1137
            }
1138
          }
1139
        }
1140
        boolean success = ensureLicenseAgreement(cmd);
4✔
1141
        if (!success) {
2!
1142
          return ValidationResultValid.get();
×
1143
        }
1144
        cmd.run();
2✔
1145
      } finally {
1146
        if (previousLogLevel != null) {
2!
1147
          this.startContext.setLogLevel(previousLogLevel);
×
1148
        }
1149
      }
1✔
1150
    } else {
1151
      trace("Commandlet did not match");
×
1152
    }
1153
    return result;
2✔
1154
  }
1155

1156
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1157

1158
    if (isTest()) {
3!
1159
      return true; // ignore for tests
2✔
1160
    }
1161
    getFileAccess().mkdirs(this.userHomeIde);
×
1162
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1163
    if (Files.isRegularFile(licenseAgreement)) {
×
1164
      return true; // success, license already accepted
×
1165
    }
1166
    if (cmd instanceof EnvironmentCommandlet) {
×
1167
      // 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
1168
      // 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
1169
      // printing anything anymore in such case.
1170
      return false;
×
1171
    }
1172
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1173
    if (logLevelInfoDisabled) {
×
1174
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1175
    }
1176
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1177
    if (logLevelInteractionDisabled) {
×
1178
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1179
    }
1180
    StringBuilder sb = new StringBuilder(1180);
×
1181
    sb.append(LOGO).append("""
×
1182
        Welcome to IDEasy!
1183
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1184
        It supports automatic download and installation of arbitrary 3rd party tools.
1185
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1186
        But if explicitly configured, also commercial software that requires an additional license may be used.
1187
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1188
        You are solely responsible for all risks implied by using this software.
1189
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1190
        You will be able to find it online under the following URL:
1191
        """).append(LICENSE_URL);
×
1192
    if (this.ideRoot != null) {
×
1193
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1194
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1195
    }
1196
    info(sb.toString());
×
1197
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1198

1199
    sb.setLength(0);
×
1200
    LocalDateTime now = LocalDateTime.now();
×
1201
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1202
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1203
    try {
1204
      Files.writeString(licenseAgreement, sb);
×
1205
    } catch (Exception e) {
×
1206
      throw new RuntimeException("Failed to save license agreement!", e);
×
1207
    }
×
1208
    if (logLevelInfoDisabled) {
×
1209
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1210
    }
1211
    if (logLevelInteractionDisabled) {
×
1212
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1213
    }
1214
    return true;
×
1215
  }
1216

1217
  @Override
1218
  public void verifyIdeMinVersion(boolean throwException) {
1219
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1220
    if (minVersion == null) {
2✔
1221
      return;
1✔
1222
    }
1223
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1224
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1225
          + "However, this is too old as your project requires at latest version %s\n"
1226
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1227
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1228
      if (throwException) {
2✔
1229
        throw new CliException(message);
5✔
1230
      } else {
1231
        warning(message);
3✔
1232
      }
1233
    }
1234
  }
1✔
1235

1236
  /**
1237
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1238
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1239
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1240
   */
1241
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1242

1243
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1244
    if (arguments.current().isStart()) {
4✔
1245
      arguments.next();
3✔
1246
    }
1247
    if (includeContextOptions) {
2✔
1248
      ContextCommandlet cc = new ContextCommandlet();
4✔
1249
      for (Property<?> property : cc.getProperties()) {
11✔
1250
        assert (property.isOption());
4!
1251
        property.apply(arguments, this, cc, collector);
7✔
1252
      }
1✔
1253
    }
1254
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1255
    CliArgument current = arguments.current();
3✔
1256
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1257
      collector.add(current.get(), null, null, null);
7✔
1258
    }
1259
    arguments.next();
3✔
1260
    while (commandletIterator.hasNext()) {
3✔
1261
      Commandlet cmd = commandletIterator.next();
4✔
1262
      if (!arguments.current().isEnd()) {
4✔
1263
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1264
      }
1265
    }
1✔
1266
    return collector.getSortedCandidates();
3✔
1267
  }
1268

1269
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1270

1271
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1272
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1273
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1274
    List<Property<?>> properties = cmd.getProperties();
3✔
1275
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1276
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1277
    for (Property<?> property : properties) {
10✔
1278
      if (property.isOption()) {
3✔
1279
        optionProperties.add(property);
4✔
1280
      }
1281
    }
1✔
1282
    CliArgument currentArgument = arguments.current();
3✔
1283
    while (!currentArgument.isEnd()) {
3✔
1284
      trace("Trying to match argument '{}'", currentArgument);
9✔
1285
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1286
        if (currentArgument.isCompletion()) {
3✔
1287
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1288
          while (optionIterator.hasNext()) {
3✔
1289
            Property<?> option = optionIterator.next();
4✔
1290
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1291
            if (success) {
2✔
1292
              optionIterator.remove();
2✔
1293
              arguments.next();
3✔
1294
            }
1295
          }
1✔
1296
        } else {
1✔
1297
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1298
          if (option != null) {
2✔
1299
            arguments.next();
3✔
1300
            boolean removed = optionProperties.remove(option);
4✔
1301
            if (!removed) {
2!
1302
              option = null;
×
1303
            }
1304
          }
1305
          if (option == null) {
2✔
1306
            trace("No such option was found.");
3✔
1307
            return;
1✔
1308
          }
1309
        }
1✔
1310
      } else {
1311
        if (valueIterator.hasNext()) {
3✔
1312
          Property<?> valueProperty = valueIterator.next();
4✔
1313
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1314
          if (!success) {
2✔
1315
            trace("Completion cannot match any further.");
3✔
1316
            return;
1✔
1317
          }
1318
        } else {
1✔
1319
          trace("No value left for completion.");
3✔
1320
          return;
1✔
1321
        }
1322
      }
1323
      currentArgument = arguments.current();
4✔
1324
    }
1325
  }
1✔
1326

1327
  /**
1328
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1329
   *     {@link CliArguments#copy() copy} as needed.
1330
   * @param cmd the potential {@link Commandlet} to match.
1331
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1332
   */
1333
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1334

1335
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1336
    CliArgument currentArgument = arguments.current();
3✔
1337
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1338
    Property<?> property = null;
2✔
1339
    if (propertyIterator.hasNext()) {
3!
1340
      property = propertyIterator.next();
4✔
1341
    }
1342
    while (!currentArgument.isEnd()) {
3✔
1343
      trace("Trying to match argument '{}'", currentArgument);
9✔
1344
      Property<?> currentProperty = property;
2✔
1345
      if (!arguments.isEndOptions()) {
3!
1346
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1347
        if (option != null) {
2!
1348
          currentProperty = option;
×
1349
        }
1350
      }
1351
      if (currentProperty == null) {
2!
1352
        trace("No option or next value found");
×
1353
        ValidationState state = new ValidationState(null);
×
1354
        state.addErrorMessage("No matching property found");
×
1355
        return state;
×
1356
      }
1357
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1358
      if (currentProperty == property) {
3!
1359
        if (!property.isMultiValued()) {
3✔
1360
          if (propertyIterator.hasNext()) {
3✔
1361
            property = propertyIterator.next();
5✔
1362
          } else {
1363
            property = null;
2✔
1364
          }
1365
        }
1366
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1367
          arguments.stopSplitShortOptions();
2✔
1368
        }
1369
      }
1370
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1371
      if (!matches) {
2!
1372
        ValidationState state = new ValidationState(null);
×
1373
        state.addErrorMessage("No matching property found");
×
1374
        return state;
×
1375
      }
1376
      currentArgument = arguments.current();
3✔
1377
    }
1✔
1378
    return ValidationResultValid.get();
2✔
1379
  }
1380

1381
  @Override
1382
  public String findBash() {
1383

1384
    String bash = BASH;
2✔
1385
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1386
      bash = findBashOnWindows();
×
1387
      if (bash == null) {
×
1388
        String variable = IdeVariables.BASH_PATH.getName();
×
1389
        bash = getVariables().get(variable);
×
1390
        if (bash == null) {
×
1391
          trace("Bash not found. Trying to search on system PATH.");
×
1392
          variable = IdeVariables.PATH.getName();
×
1393
          Path plainBash = Path.of(BASH);
×
1394
          Path bashPath = getPath().findBinary(plainBash);
×
1395
          bash = bashPath.toAbsolutePath().toString();
×
1396
          if (bash.contains("AppData\\Local\\Microsoft\\WindowsApps")) {
×
1397
            warning("Only found windows fake bash that is not usable!");
×
1398
            bash = null;
×
1399
          }
1400
        }
1401
        if (bash == null) {
×
1402
          info("Could not find bash in Windows registry, using bash from {} as fallback: {}", variable, bash);
×
1403
        }
1404
      }
1405
    }
1406
    return bash;
2✔
1407
  }
1408

1409
  private String findBashOnWindows() {
1410

1411
    // Check if Git Bash exists in the default location
1412
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1413
    if (Files.exists(defaultPath)) {
×
1414
      return defaultPath.toString();
×
1415
    }
1416

1417
    // If not found in the default location, try the registry query
1418
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1419
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1420
    String regQueryResult;
1421
    for (String bashVariant : bashVariants) {
×
1422
      trace("Trying to find bash variant: {}", bashVariant);
×
1423
      for (String registryKey : registryKeys) {
×
1424
        trace("Trying to find bash from registry key: {}", registryKey);
×
1425
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1426
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1427

1428
        try {
1429
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1430
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1431
            StringBuilder output = new StringBuilder();
×
1432
            String line;
1433

1434
            while ((line = reader.readLine()) != null) {
×
1435
              output.append(line);
×
1436
            }
1437

1438
            int exitCode = process.waitFor();
×
1439
            if (exitCode != 0) {
×
1440
              warning("Query to windows registry for finding bash failed with exit code {}", exitCode);
×
1441
              return null;
×
1442
            }
1443

1444
            regQueryResult = output.toString();
×
1445
            trace("Result from windows registry was: {}", regQueryResult);
×
1446
            int index = regQueryResult.indexOf("REG_SZ");
×
1447
            if (index != -1) {
×
1448
              String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1449
              String bashPath = path + "\\bin\\bash.exe";
×
1450
              debug("Found bash at: {}", bashPath);
×
1451
              return bashPath;
×
1452
            }
1453
          }
×
1454
        } catch (Exception e) {
×
1455
          error(e, "Query to windows registry for finding bash failed!");
×
1456
          return null;
×
1457
        }
×
1458
      }
1459
    }
1460
    // no bash found
1461
    return null;
×
1462
  }
1463

1464
  @Override
1465
  public WindowsPathSyntax getPathSyntax() {
1466

1467
    return this.pathSyntax;
3✔
1468
  }
1469

1470
  /**
1471
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1472
   */
1473
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1474

1475
    this.pathSyntax = pathSyntax;
3✔
1476
  }
1✔
1477

1478
  /**
1479
   * @return the {@link IdeStartContextImpl}.
1480
   */
1481
  public IdeStartContextImpl getStartContext() {
1482

1483
    return startContext;
3✔
1484
  }
1485

1486
  /**
1487
   * @return the {@link WindowsHelper}.
1488
   */
1489
  public final WindowsHelper getWindowsHelper() {
1490

1491
    if (this.windowsHelper == null) {
3✔
1492
      this.windowsHelper = createWindowsHelper();
4✔
1493
    }
1494
    return this.windowsHelper;
3✔
1495
  }
1496

1497
  /**
1498
   * @return the new {@link WindowsHelper} instance.
1499
   */
1500
  protected WindowsHelper createWindowsHelper() {
1501

1502
    return new WindowsHelperImpl(this);
×
1503
  }
1504

1505
  /**
1506
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1507
   */
1508
  public void reload() {
1509

1510
    this.variables = null;
3✔
1511
    this.customToolRepository = null;
3✔
1512
  }
1✔
1513

1514
  @Override
1515
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1516

1517
    assert (Files.isDirectory(installationPath));
6!
1518
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1519
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1520
  }
1✔
1521

1522
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc