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

devonfw / IDEasy / 13063267543

30 Jan 2025 11:34PM UTC coverage: 68.379% (-0.2%) from 68.557%
13063267543

push

github

web-flow
#954: improve repository support (#990)

Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>
Co-authored-by: jan-vcapgemini <jan-vincent.hoelzle@capgemini.com>

2857 of 4597 branches covered (62.15%)

Branch coverage included in aggregate %.

7391 of 10390 relevant lines covered (71.14%)

3.1 hits per line

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

59.32
cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
1
package com.devonfw.tools.ide.context;
2

3
import java.io.BufferedReader;
4
import java.io.InputStreamReader;
5
import java.net.URL;
6
import java.net.URLConnection;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.time.LocalDateTime;
10
import java.util.ArrayList;
11
import java.util.HashMap;
12
import java.util.Iterator;
13
import java.util.List;
14
import java.util.Locale;
15
import java.util.Map;
16

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

66
/**
67
 * Abstract base implementation of {@link IdeContext}.
68
 */
69
public abstract class AbstractIdeContext implements IdeContext {
70

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

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

75
  private final IdeStartContextImpl startContext;
76

77
  private Path ideHome;
78

79
  private final Path ideRoot;
80

81
  private final Path ideInstallationPath;
82

83
  private Path confPath;
84

85
  protected Path settingsPath;
86

87
  private Path settingsCommitIdPath;
88

89
  private Path softwarePath;
90

91
  private Path softwareExtraPath;
92

93
  private final Path softwareRepositoryPath;
94

95
  protected Path pluginsPath;
96

97
  private Path workspacePath;
98

99
  private String workspaceName;
100

101
  protected Path urlsPath;
102

103
  private final Path tempPath;
104

105
  private final Path tempDownloadPath;
106

107
  private Path cwd;
108

109
  private Path downloadPath;
110

111
  private final Path toolRepositoryPath;
112

113
  protected Path userHome;
114

115
  private Path userHomeIde;
116

117
  private SystemPath path;
118

119
  private WindowsPathSyntax pathSyntax;
120

121
  private final SystemInfo systemInfo;
122

123
  private EnvironmentVariables variables;
124

125
  private final FileAccess fileAccess;
126

127
  protected CommandletManager commandletManager;
128

129
  protected ToolRepository defaultToolRepository;
130

131
  private CustomToolRepository customToolRepository;
132

133
  private DirectoryMerger workspaceMerger;
134

135
  protected UrlMetadata urlMetadata;
136

137
  protected Path defaultExecutionDirectory;
138

139
  private StepImpl currentStep;
140

141
  protected Boolean online;
142

143
  protected IdeSystem system;
144

145
  private NetworkProxy networkProxy;
146

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

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

186
    // detection completed, initializing variables
187
    this.ideRoot = findIdeRoot(currentDir);
5✔
188

189
    setCwd(workingDirectory, workspace, currentDir);
5✔
190

191
    if (this.ideRoot == null) {
3✔
192
      this.ideInstallationPath = null;
3✔
193
      this.toolRepositoryPath = null;
3✔
194
      this.urlsPath = null;
3✔
195
      this.tempPath = null;
3✔
196
      this.tempDownloadPath = null;
3✔
197
      this.softwareRepositoryPath = null;
4✔
198
    } else {
199
      this.ideInstallationPath = this.ideRoot.resolve(FOLDER_IDE_INSTALLATION);
6✔
200
      this.toolRepositoryPath = this.ideInstallationPath.resolve("software");
6✔
201
      this.urlsPath = this.ideInstallationPath.resolve("urls");
6✔
202
      this.tempPath = this.ideInstallationPath.resolve("tmp");
6✔
203
      this.tempDownloadPath = this.tempPath.resolve(FOLDER_DOWNLOADS);
6✔
204
      this.softwareRepositoryPath = this.ideInstallationPath.resolve(FOLDER_SOFTWARE);
6✔
205
      if (Files.isDirectory(this.tempPath)) {
7✔
206
        // TODO delete all files older than 1 day here...
207
      } else {
208
        this.fileAccess.mkdirs(this.tempDownloadPath);
5✔
209
      }
210
    }
211

212
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
213
  }
1✔
214

215
  private Path findIdeRoot(Path ideHomePath) {
216

217
    Path ideRootPath = null;
2✔
218
    if (ideHomePath != null) {
2✔
219
      ideRootPath = ideHomePath.getParent();
4✔
220
    } else if (!isTest()) {
3!
221
      ideRootPath = getIdeRootPathFromEnv();
×
222
    }
223
    return ideRootPath;
2✔
224
  }
225

226
  private Path getIdeRootPathFromEnv() {
227

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

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

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

273
    this.path = computeSystemPath();
4✔
274
  }
1✔
275

276
  private String getMessageIdeHomeFound() {
277

278
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
279
  }
280

281
  private String getMessageIdeHomeNotFound() {
282

283
    return "You are not inside an IDE installation: " + this.cwd;
5✔
284
  }
285

286
  private String getMessageIdeRootNotFound() {
287

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

296
  /**
297
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
298
   */
299
  public boolean isTest() {
300

301
    return false;
×
302
  }
303

304
  protected SystemPath computeSystemPath() {
305

306
    return new SystemPath(this);
×
307
  }
308

309
  private boolean isIdeHome(Path dir) {
310

311
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
312
      return false;
2✔
313
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
314
      return false;
×
315
    }
316
    return true;
2✔
317
  }
318

319
  private EnvironmentVariables createVariables() {
320

321
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
322
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
323
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
324
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
325
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
326
    return conf.resolved();
3✔
327
  }
328

329
  protected AbstractEnvironmentVariables createSystemVariables() {
330

331
    return EnvironmentVariables.ofSystem(this);
3✔
332
  }
333

334
  @Override
335
  public SystemInfo getSystemInfo() {
336

337
    return this.systemInfo;
3✔
338
  }
339

340
  @Override
341
  public FileAccess getFileAccess() {
342

343
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
344
    configureNetworkProxy();
2✔
345
    return this.fileAccess;
3✔
346
  }
347

348
  @Override
349
  public CommandletManager getCommandletManager() {
350

351
    return this.commandletManager;
3✔
352
  }
353

354
  @Override
355
  public ToolRepository getDefaultToolRepository() {
356

357
    return this.defaultToolRepository;
3✔
358
  }
359

360
  @Override
361
  public CustomToolRepository getCustomToolRepository() {
362

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

369
  @Override
370
  public Path getIdeHome() {
371

372
    return this.ideHome;
3✔
373
  }
374

375
  @Override
376
  public String getProjectName() {
377

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

384
  @Override
385
  public Path getIdeRoot() {
386

387
    return this.ideRoot;
3✔
388
  }
389

390
  @Override
391
  public Path getIdeInstallationPath() {
392

393
    return this.ideInstallationPath;
×
394
  }
395

396
  @Override
397
  public Path getCwd() {
398

399
    return this.cwd;
3✔
400
  }
401

402
  @Override
403
  public Path getTempPath() {
404

405
    return this.tempPath;
3✔
406
  }
407

408
  @Override
409
  public Path getTempDownloadPath() {
410

411
    return this.tempDownloadPath;
3✔
412
  }
413

414
  @Override
415
  public Path getUserHome() {
416

417
    return this.userHome;
3✔
418
  }
419

420
  @Override
421
  public Path getUserHomeIde() {
422

423
    return this.userHomeIde;
3✔
424
  }
425

426
  @Override
427
  public Path getSettingsPath() {
428

429
    return this.settingsPath;
3✔
430
  }
431

432
  @Override
433
  public Path getSettingsGitRepository() {
434

435
    Path settingsPath = getSettingsPath();
3✔
436

437
    if (settingsPath == null) {
2✔
438
      error("No settings repository was found.");
3✔
439
      return null;
2✔
440
    }
441

442
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
443
    if (!Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
10!
444
      error("Settings repository exists but is not a git repository.");
3✔
445
      return null;
2✔
446
    }
447

448
    return settingsPath;
×
449
  }
450

451
  public boolean isSettingsRepositorySymlinkOrJunction() {
452

453
    Path settingsPath = getSettingsPath();
3✔
454
    if (settingsPath == null) {
2!
455
      return false;
×
456
    }
457
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
458
  }
459

460
  @Override
461
  public Path getSettingsCommitIdPath() {
462

463
    return this.settingsCommitIdPath;
3✔
464
  }
465

466
  @Override
467
  public Path getConfPath() {
468

469
    return this.confPath;
3✔
470
  }
471

472
  @Override
473
  public Path getSoftwarePath() {
474

475
    return this.softwarePath;
3✔
476
  }
477

478
  @Override
479
  public Path getSoftwareExtraPath() {
480

481
    return this.softwareExtraPath;
3✔
482
  }
483

484
  @Override
485
  public Path getSoftwareRepositoryPath() {
486

487
    return this.softwareRepositoryPath;
3✔
488
  }
489

490
  @Override
491
  public Path getPluginsPath() {
492

493
    return this.pluginsPath;
3✔
494
  }
495

496
  @Override
497
  public String getWorkspaceName() {
498

499
    return this.workspaceName;
3✔
500
  }
501

502
  @Override
503
  public Path getWorkspacePath() {
504

505
    return this.workspacePath;
3✔
506
  }
507

508
  @Override
509
  public Path getDownloadPath() {
510

511
    return this.downloadPath;
3✔
512
  }
513

514
  @Override
515
  public Path getUrlsPath() {
516

517
    return this.urlsPath;
3✔
518
  }
519

520
  @Override
521
  public Path getToolRepositoryPath() {
522

523
    return this.toolRepositoryPath;
3✔
524
  }
525

526
  @Override
527
  public SystemPath getPath() {
528

529
    return this.path;
3✔
530
  }
531

532
  @Override
533
  public EnvironmentVariables getVariables() {
534

535
    if (this.variables == null) {
3✔
536
      this.variables = createVariables();
4✔
537
    }
538
    return this.variables;
3✔
539
  }
540

541
  @Override
542
  public UrlMetadata getUrls() {
543

544
    if (this.urlMetadata == null) {
3✔
545
      if (!isTest()) {
3!
546
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, this.urlsPath, null);
×
547
      }
548
      this.urlMetadata = new UrlMetadata(this);
6✔
549
    }
550
    return this.urlMetadata;
3✔
551
  }
552

553
  @Override
554
  public boolean isQuietMode() {
555

556
    return this.startContext.isQuietMode();
4✔
557
  }
558

559
  @Override
560
  public boolean isBatchMode() {
561

562
    return this.startContext.isBatchMode();
×
563
  }
564

565
  @Override
566
  public boolean isForceMode() {
567

568
    return this.startContext.isForceMode();
4✔
569
  }
570

571
  @Override
572
  public boolean isOfflineMode() {
573

574
    return this.startContext.isOfflineMode();
4✔
575
  }
576

577
  @Override
578
  public boolean isSkipUpdatesMode() {
579

580
    return this.startContext.isSkipUpdatesMode();
×
581
  }
582

583
  @Override
584
  public boolean isOnline() {
585

586
    if (this.online == null) {
3✔
587
      configureNetworkProxy();
2✔
588
      // we currently assume we have only a CLI process that runs shortly
589
      // therefore we run this check only once to save resources when this method is called many times
590
      try {
591
        int timeout = 1000;
2✔
592
        //open a connection to github.com and try to retrieve data
593
        //getContent fails if there is no connection
594
        URLConnection connection = new URL("https://www.github.com").openConnection();
6✔
595
        connection.setConnectTimeout(timeout);
3✔
596
        connection.getContent();
3✔
597
        this.online = Boolean.TRUE;
3✔
598
      } catch (Exception ignored) {
×
599
        this.online = Boolean.FALSE;
×
600
      }
1✔
601
    }
602
    return this.online.booleanValue();
4✔
603
  }
604

605
  private void configureNetworkProxy() {
606

607
    if (this.networkProxy == null) {
3✔
608
      this.networkProxy = new NetworkProxy(this);
6✔
609
      this.networkProxy.configure();
3✔
610
    }
611
  }
1✔
612

613
  @Override
614
  public Locale getLocale() {
615

616
    Locale locale = this.startContext.getLocale();
4✔
617
    if (locale == null) {
2!
618
      locale = Locale.getDefault();
×
619
    }
620
    return locale;
2✔
621
  }
622

623
  @Override
624
  public DirectoryMerger getWorkspaceMerger() {
625

626
    if (this.workspaceMerger == null) {
3✔
627
      this.workspaceMerger = new DirectoryMerger(this);
6✔
628
    }
629
    return this.workspaceMerger;
3✔
630
  }
631

632
  /**
633
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
634
   */
635
  @Override
636
  public Path getDefaultExecutionDirectory() {
637

638
    return this.defaultExecutionDirectory;
×
639
  }
640

641
  /**
642
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
643
   */
644
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
645

646
    if (defaultExecutionDirectory != null) {
×
647
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
648
    }
649
  }
×
650

651
  @Override
652
  public GitContext getGitContext() {
653

654
    return new GitContextImpl(this);
×
655
  }
656

657
  @Override
658
  public ProcessContext newProcess() {
659

660
    ProcessContext processContext = createProcessContext();
3✔
661
    if (this.defaultExecutionDirectory != null) {
3!
662
      processContext.directory(this.defaultExecutionDirectory);
×
663
    }
664
    return processContext;
2✔
665
  }
666

667
  @Override
668
  public IdeSystem getSystem() {
669

670
    if (this.system == null) {
×
671
      this.system = new IdeSystemImpl(this);
×
672
    }
673
    return this.system;
×
674
  }
675

676
  /**
677
   * @return a new instance of {@link ProcessContext}.
678
   * @see #newProcess()
679
   */
680
  protected ProcessContext createProcessContext() {
681

682
    return new ProcessContextImpl(this);
×
683
  }
684

685
  @Override
686
  public IdeSubLogger level(IdeLogLevel level) {
687

688
    return this.startContext.level(level);
5✔
689
  }
690

691
  @Override
692
  public void logIdeHomeAndRootStatus() {
693

694
    if (this.ideRoot != null) {
3!
695
      success("IDE_ROOT is set to {}", this.ideRoot);
×
696
    }
697
    if (this.ideHome == null) {
3!
698
      warning(getMessageIdeHomeNotFound());
5✔
699
    } else {
700
      success("IDE_HOME is set to {}", this.ideHome);
×
701
    }
702
  }
1✔
703

704
  @Override
705
  public String askForInput(String message, String defaultValue) {
706

707
    if (!message.isBlank()) {
×
708
      info(message);
×
709
    }
710
    if (isBatchMode()) {
×
711
      if (isForceMode()) {
×
712
        return defaultValue;
×
713
      } else {
714
        throw new CliAbortException();
×
715
      }
716
    }
717
    String input = readLine().trim();
×
718
    return input.isEmpty() ? defaultValue : input;
×
719
  }
720

721
  @Override
722
  public String askForInput(String message) {
723

724
    String input;
725
    do {
726
      info(message);
3✔
727
      input = readLine().trim();
4✔
728
    } while (input.isEmpty());
3!
729

730
    return input;
2✔
731
  }
732

733
  @SuppressWarnings("unchecked")
734
  @Override
735
  public <O> O question(String question, O... options) {
736

737
    assert (options.length >= 2);
×
738
    interaction(question);
×
739
    Map<String, O> mapping = new HashMap<>(options.length);
×
740
    int i = 0;
×
741
    for (O option : options) {
×
742
      i++;
×
743
      String key = "" + option;
×
744
      addMapping(mapping, key, option);
×
745
      String numericKey = Integer.toString(i);
×
746
      if (numericKey.equals(key)) {
×
747
        trace("Options should not be numeric: " + key);
×
748
      } else {
749
        addMapping(mapping, numericKey, option);
×
750
      }
751
      interaction("Option " + numericKey + ": " + key);
×
752
    }
753
    O option = null;
×
754
    if (isBatchMode()) {
×
755
      if (isForceMode()) {
×
756
        option = options[0];
×
757
        interaction("" + option);
×
758
      }
759
    } else {
760
      while (option == null) {
×
761
        String answer = readLine();
×
762
        option = mapping.get(answer);
×
763
        if (option == null) {
×
764
          warning("Invalid answer: '" + answer + "' - please try again.");
×
765
        }
766
      }
×
767
    }
768
    return option;
×
769
  }
770

771
  /**
772
   * @return the input from the end-user (e.g. read from the console).
773
   */
774
  protected abstract String readLine();
775

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

778
    O duplicate = mapping.put(key, option);
×
779
    if (duplicate != null) {
×
780
      throw new IllegalArgumentException("Duplicated option " + key);
×
781
    }
782
  }
×
783

784
  @Override
785
  public Step getCurrentStep() {
786

787
    return this.currentStep;
×
788
  }
789

790
  @Override
791
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
792

793
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
794
    return this.currentStep;
3✔
795
  }
796

797
  /**
798
   * Internal method to end the running {@link Step}.
799
   *
800
   * @param step the current {@link Step} to end.
801
   */
802
  public void endStep(StepImpl step) {
803

804
    if (step == this.currentStep) {
4!
805
      this.currentStep = this.currentStep.getParent();
6✔
806
    } else {
807
      String currentStepName = "null";
×
808
      if (this.currentStep != null) {
×
809
        currentStepName = this.currentStep.getName();
×
810
      }
811
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
812
    }
813
  }
1✔
814

815
  /**
816
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
817
   *
818
   * @param arguments the {@link CliArgument}.
819
   * @return the return code of the execution.
820
   */
821
  public int run(CliArguments arguments) {
822

823
    CliArgument current = arguments.current();
3✔
824
    assert (this.currentStep == null);
4!
825
    boolean supressStepSuccess = false;
2✔
826
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
827
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
828
    Commandlet cmd = null;
2✔
829
    ValidationResult result = null;
2✔
830
    try {
831
      while (commandletIterator.hasNext()) {
3!
832
        cmd = commandletIterator.next();
4✔
833
        result = applyAndRun(arguments.copy(), cmd);
6✔
834
        if (result.isValid()) {
3!
835
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
836
          step.success();
2✔
837
          return ProcessResult.SUCCESS;
4✔
838
        }
839
      }
840
      this.startContext.activateLogging();
×
841
      if (result != null) {
×
842
        error(result.getErrorMessage());
×
843
      }
844
      step.error("Invalid arguments: {}", current.getArgs());
×
845
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
×
846
      if (cmd != null) {
×
847
        help.commandlet.setValue(cmd);
×
848
      }
849
      help.run();
×
850
      return 1;
×
851
    } catch (Throwable t) {
1✔
852
      this.startContext.activateLogging();
3✔
853
      step.error(t, true);
4✔
854
      throw t;
2✔
855
    } finally {
856
      step.close();
2✔
857
      assert (this.currentStep == null);
4!
858
      step.logSummary(supressStepSuccess);
3✔
859
    }
860
  }
861

862
  /**
863
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
864
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
865
   *     {@link Commandlet} did not match and we have to try a different candidate).
866
   */
867
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
868

869
    IdeLogLevel previousLogLevel = null;
2✔
870
    cmd.reset();
2✔
871
    ValidationResult result = apply(arguments, cmd);
5✔
872
    if (result.isValid()) {
3!
873
      result = cmd.validate();
3✔
874
    }
875
    if (result.isValid()) {
3!
876
      debug("Running commandlet {}", cmd);
9✔
877
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
878
        throw new CliException(getMessageIdeHomeNotFound(), ProcessResult.NO_IDE_HOME);
×
879
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6✔
880
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
881
      }
882
      try {
883
        if (cmd.isProcessableOutput()) {
3!
884
          if (!debug().isEnabled()) {
×
885
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
886
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
887
          }
888
          this.startContext.activateLogging();
×
889
        } else {
890
          this.startContext.activateLogging();
3✔
891
          verifyIdeRoot();
2✔
892
          if (cmd.isIdeHomeRequired()) {
3✔
893
            debug(getMessageIdeHomeFound());
4✔
894
          }
895
          Path settingsRepository = getSettingsGitRepository();
3✔
896
          if (settingsRepository != null) {
2!
897
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
898
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
899
                    settingsRepository, getSettingsCommitIdPath()))) {
×
900
              if (isSettingsRepositorySymlinkOrJunction()) {
×
901
                interaction(
×
902
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
903

904
              } else {
905
                interaction(
×
906
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
907
              }
908
            }
909
          }
910
        }
911
        boolean success = ensureLicenseAgreement(cmd);
4✔
912
        if (!success) {
2!
913
          return ValidationResultValid.get();
×
914
        }
915
        cmd.run();
2✔
916
      } finally {
917
        if (previousLogLevel != null) {
2!
918
          this.startContext.setLogLevel(previousLogLevel);
×
919
        }
920
      }
1✔
921
    } else {
922
      trace("Commandlet did not match");
×
923
    }
924
    return result;
2✔
925
  }
926

927
  private boolean ensureLicenseAgreement(Commandlet cmd) {
928

929
    if (isTest()) {
3!
930
      return true; // ignore for tests
2✔
931
    }
932
    getFileAccess().mkdirs(this.userHomeIde);
×
933
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
934
    if (Files.isRegularFile(licenseAgreement)) {
×
935
      return true; // success, license already accepted
×
936
    }
937
    if (cmd instanceof EnvironmentCommandlet) {
×
938
      // 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
939
      // 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
940
      // printing anything anymore in such case.
941
      return false;
×
942
    }
943
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
944
    if (logLevelInfoDisabled) {
×
945
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
946
    }
947
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
948
    if (logLevelInteractionDisabled) {
×
949
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
950
    }
951
    StringBuilder sb = new StringBuilder(1180);
×
952
    sb.append(LOGO).append("""
×
953
        Welcome to IDEasy!
954
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
955
        It supports automatic download and installation of arbitrary 3rd party tools.
956
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
957
        But if explicitly configured, also commercial software that requires an additional license may be used.
958
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
959
        You are solely responsible for all risks implied by using this software.
960
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
961
        You will be able to find it online under the following URL:
962
        """).append(LICENSE_URL);
×
963
    if (this.ideRoot != null) {
×
964
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
965
          append(this.ideInstallationPath.resolve("IDEasy.pdf").toString()).append("\n");
×
966
    }
967
    info(sb.toString());
×
968
    askToContinue("Do you accept these terms of use and all license agreements?");
×
969

970
    sb.setLength(0);
×
971
    LocalDateTime now = LocalDateTime.now();
×
972
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
973
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
974
    try {
975
      Files.writeString(licenseAgreement, sb);
×
976
    } catch (Exception e) {
×
977
      throw new RuntimeException("Failed to save license agreement!", e);
×
978
    }
×
979
    if (logLevelInfoDisabled) {
×
980
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
981
    }
982
    if (logLevelInteractionDisabled) {
×
983
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
984
    }
985
    return true;
×
986
  }
987

988
  private void verifyIdeRoot() {
989

990
    if (!isTest()) {
3!
991
      if (this.ideRoot == null) {
×
992
        warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again.");
×
993
      } else if (this.ideHome != null) {
×
994
        Path ideRootPath = getIdeRootPathFromEnv();
×
995
        if (!this.ideRoot.equals(ideRootPath)) {
×
996
          warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath,
×
997
              this.ideHome.getFileName(), this.ideRoot);
×
998
        }
999
      }
1000
    }
1001
  }
1✔
1002

1003
  /**
1004
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1005
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1006
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1007
   */
1008
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1009

1010
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1011
    if (arguments.current().isStart()) {
4✔
1012
      arguments.next();
3✔
1013
    }
1014
    if (includeContextOptions) {
2✔
1015
      ContextCommandlet cc = new ContextCommandlet();
4✔
1016
      for (Property<?> property : cc.getProperties()) {
11✔
1017
        assert (property.isOption());
4!
1018
        property.apply(arguments, this, cc, collector);
7✔
1019
      }
1✔
1020
    }
1021
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1022
    CliArgument current = arguments.current();
3✔
1023
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1024
      collector.add(current.get(), null, null, null);
7✔
1025
    }
1026
    arguments.next();
3✔
1027
    while (commandletIterator.hasNext()) {
3✔
1028
      Commandlet cmd = commandletIterator.next();
4✔
1029
      if (!arguments.current().isEnd()) {
4✔
1030
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1031
      }
1032
    }
1✔
1033
    return collector.getSortedCandidates();
3✔
1034
  }
1035

1036
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1037

1038
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1039
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1040
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1041
    List<Property<?>> properties = cmd.getProperties();
3✔
1042
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1043
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1044
    for (Property<?> property : properties) {
10✔
1045
      if (property.isOption()) {
3✔
1046
        optionProperties.add(property);
4✔
1047
      }
1048
    }
1✔
1049
    CliArgument currentArgument = arguments.current();
3✔
1050
    while (!currentArgument.isEnd()) {
3✔
1051
      trace("Trying to match argument '{}'", currentArgument);
9✔
1052
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1053
        if (currentArgument.isCompletion()) {
3✔
1054
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1055
          while (optionIterator.hasNext()) {
3✔
1056
            Property<?> option = optionIterator.next();
4✔
1057
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1058
            if (success) {
2✔
1059
              optionIterator.remove();
2✔
1060
              arguments.next();
3✔
1061
            }
1062
          }
1✔
1063
        } else {
1✔
1064
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1065
          if (option != null) {
2✔
1066
            arguments.next();
3✔
1067
            boolean removed = optionProperties.remove(option);
4✔
1068
            if (!removed) {
2!
1069
              option = null;
×
1070
            }
1071
          }
1072
          if (option == null) {
2✔
1073
            trace("No such option was found.");
3✔
1074
            return;
1✔
1075
          }
1076
        }
1✔
1077
      } else {
1078
        if (valueIterator.hasNext()) {
3✔
1079
          Property<?> valueProperty = valueIterator.next();
4✔
1080
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1081
          if (!success) {
2✔
1082
            trace("Completion cannot match any further.");
3✔
1083
            return;
1✔
1084
          }
1085
        } else {
1✔
1086
          trace("No value left for completion.");
3✔
1087
          return;
1✔
1088
        }
1089
      }
1090
      currentArgument = arguments.current();
4✔
1091
    }
1092
  }
1✔
1093

1094
  /**
1095
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1096
   *     {@link CliArguments#copy() copy} as needed.
1097
   * @param cmd the potential {@link Commandlet} to match.
1098
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1099
   */
1100
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1101

1102
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1103
    CliArgument currentArgument = arguments.current();
3✔
1104
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1105
    Property<?> property = null;
2✔
1106
    if (propertyIterator.hasNext()) {
3!
1107
      property = propertyIterator.next();
4✔
1108
    }
1109
    while (!currentArgument.isEnd()) {
3✔
1110
      trace("Trying to match argument '{}'", currentArgument);
9✔
1111
      Property<?> currentProperty = property;
2✔
1112
      if (!arguments.isEndOptions()) {
3!
1113
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1114
        if (option != null) {
2!
1115
          currentProperty = option;
×
1116
        }
1117
      }
1118
      if (currentProperty == null) {
2!
1119
        trace("No option or next value found");
×
1120
        ValidationState state = new ValidationState(null);
×
1121
        state.addErrorMessage("No matching property found");
×
1122
        return state;
×
1123
      }
1124
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1125
      if (currentProperty == property) {
3!
1126
        if (!property.isMultiValued()) {
3✔
1127
          if (propertyIterator.hasNext()) {
3✔
1128
            property = propertyIterator.next();
5✔
1129
          } else {
1130
            property = null;
2✔
1131
          }
1132
        }
1133
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1134
          arguments.stopSplitShortOptions();
2✔
1135
        }
1136
      }
1137
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1138
      if (!matches && currentArgument.isCompletion()) {
2!
1139
        ValidationState state = new ValidationState(null);
×
1140
        state.addErrorMessage("No matching property found");
×
1141
        return state;
×
1142
      }
1143
      currentArgument = arguments.current();
3✔
1144
    }
1✔
1145
    return ValidationResultValid.get();
2✔
1146
  }
1147

1148
  @Override
1149
  public String findBash() {
1150

1151
    String bash = "bash";
2✔
1152
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1153
      bash = findBashOnWindows();
×
1154
    }
1155

1156
    return bash;
2✔
1157
  }
1158

1159
  private String findBashOnWindows() {
1160

1161
    // Check if Git Bash exists in the default location
1162
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1163
    if (Files.exists(defaultPath)) {
×
1164
      return defaultPath.toString();
×
1165
    }
1166

1167
    // If not found in the default location, try the registry query
1168
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1169
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1170
    String regQueryResult;
1171
    for (String bashVariant : bashVariants) {
×
1172
      for (String registryKey : registryKeys) {
×
1173
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1174
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1175

1176
        try {
1177
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1178
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1179
            StringBuilder output = new StringBuilder();
×
1180
            String line;
1181

1182
            while ((line = reader.readLine()) != null) {
×
1183
              output.append(line);
×
1184
            }
1185

1186
            int exitCode = process.waitFor();
×
1187
            if (exitCode != 0) {
×
1188
              return null;
×
1189
            }
1190

1191
            regQueryResult = output.toString();
×
1192
            if (regQueryResult != null) {
×
1193
              int index = regQueryResult.indexOf("REG_SZ");
×
1194
              if (index != -1) {
×
1195
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1196
                return path + "\\bin\\bash.exe";
×
1197
              }
1198
            }
1199

1200
          }
×
1201
        } catch (Exception e) {
×
1202
          return null;
×
1203
        }
×
1204
      }
1205
    }
1206
    // no bash found
1207
    return null;
×
1208
  }
1209

1210
  @Override
1211
  public WindowsPathSyntax getPathSyntax() {
1212

1213
    return this.pathSyntax;
3✔
1214
  }
1215

1216
  /**
1217
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1218
   */
1219
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1220

1221
    this.pathSyntax = pathSyntax;
3✔
1222
  }
1✔
1223

1224
  /**
1225
   * @return the {@link IdeStartContextImpl}.
1226
   */
1227
  public IdeStartContextImpl getStartContext() {
1228

1229
    return startContext;
3✔
1230
  }
1231

1232
  /**
1233
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1234
   */
1235
  public void reload() {
1236

1237
    this.variables = null;
3✔
1238
    this.customToolRepository = null;
3✔
1239
  }
1✔
1240
}
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