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

devonfw / IDEasy / 23013411057

12 Mar 2026 04:48PM UTC coverage: 70.479% (+0.1%) from 70.347%
23013411057

push

github

web-flow
#1735: implement repo link feature, #1736: fix link creation (#1739)

4139 of 6464 branches covered (64.03%)

Branch coverage included in aggregate %.

10730 of 14633 relevant lines covered (73.33%)

3.09 hits per line

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

65.97
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.ByteArrayInputStream;
6
import java.io.ByteArrayOutputStream;
7
import java.io.IOException;
8
import java.nio.file.Files;
9
import java.nio.file.Path;
10
import java.time.LocalDateTime;
11
import java.util.ArrayList;
12
import java.util.HashMap;
13
import java.util.Iterator;
14
import java.util.List;
15
import java.util.Locale;
16
import java.util.Map;
17
import java.util.Map.Entry;
18
import java.util.Objects;
19
import java.util.Properties;
20
import java.util.function.Predicate;
21
import java.util.logging.FileHandler;
22
import java.util.logging.LogManager;
23
import java.util.logging.SimpleFormatter;
24

25
import org.slf4j.Logger;
26
import org.slf4j.LoggerFactory;
27

28
import com.devonfw.tools.ide.cli.CliAbortException;
29
import com.devonfw.tools.ide.cli.CliArgument;
30
import com.devonfw.tools.ide.cli.CliArguments;
31
import com.devonfw.tools.ide.cli.CliException;
32
import com.devonfw.tools.ide.commandlet.Commandlet;
33
import com.devonfw.tools.ide.commandlet.CommandletManager;
34
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
35
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
36
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
37
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
38
import com.devonfw.tools.ide.common.SystemPath;
39
import com.devonfw.tools.ide.completion.CompletionCandidate;
40
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
41
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
42
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
43
import com.devonfw.tools.ide.environment.EnvironmentVariables;
44
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
45
import com.devonfw.tools.ide.environment.IdeSystem;
46
import com.devonfw.tools.ide.environment.IdeSystemImpl;
47
import com.devonfw.tools.ide.git.GitContext;
48
import com.devonfw.tools.ide.git.GitContextImpl;
49
import com.devonfw.tools.ide.git.GitUrl;
50
import com.devonfw.tools.ide.io.FileAccess;
51
import com.devonfw.tools.ide.io.FileAccessImpl;
52
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
53
import com.devonfw.tools.ide.log.IdeLogLevel;
54
import com.devonfw.tools.ide.log.IdeLogListener;
55
import com.devonfw.tools.ide.log.JulConsoleHandler;
56
import com.devonfw.tools.ide.merge.DirectoryMerger;
57
import com.devonfw.tools.ide.migration.IdeMigrator;
58
import com.devonfw.tools.ide.network.NetworkStatus;
59
import com.devonfw.tools.ide.network.NetworkStatusImpl;
60
import com.devonfw.tools.ide.os.SystemInfo;
61
import com.devonfw.tools.ide.os.SystemInfoImpl;
62
import com.devonfw.tools.ide.os.WindowsHelper;
63
import com.devonfw.tools.ide.os.WindowsHelperImpl;
64
import com.devonfw.tools.ide.os.WindowsPathSyntax;
65
import com.devonfw.tools.ide.process.ProcessContext;
66
import com.devonfw.tools.ide.process.ProcessContextImpl;
67
import com.devonfw.tools.ide.process.ProcessResult;
68
import com.devonfw.tools.ide.property.Property;
69
import com.devonfw.tools.ide.step.Step;
70
import com.devonfw.tools.ide.step.StepImpl;
71
import com.devonfw.tools.ide.tool.custom.CustomToolRepository;
72
import com.devonfw.tools.ide.tool.custom.CustomToolRepositoryImpl;
73
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
74
import com.devonfw.tools.ide.tool.npm.NpmRepository;
75
import com.devonfw.tools.ide.tool.pip.PipRepository;
76
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
77
import com.devonfw.tools.ide.tool.repository.ToolRepository;
78
import com.devonfw.tools.ide.url.model.UrlMetadata;
79
import com.devonfw.tools.ide.util.DateTimeUtil;
80
import com.devonfw.tools.ide.util.PrivacyUtil;
81
import com.devonfw.tools.ide.validation.ValidationResult;
82
import com.devonfw.tools.ide.validation.ValidationResultValid;
83
import com.devonfw.tools.ide.validation.ValidationState;
84
import com.devonfw.tools.ide.variable.IdeVariables;
85
import com.devonfw.tools.ide.version.IdeVersion;
86
import com.devonfw.tools.ide.version.VersionIdentifier;
87

88
/**
89
 * Abstract base implementation of {@link IdeContext}.
90
 */
91
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
92

93
  static final Logger LOG = LoggerFactory.getLogger(AbstractIdeContext.class);
3✔
94

95
  /** The default shell bash (Bourne Again SHell). */
96
  public static final String BASH = "bash";
97

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

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

102
  private static final String DEFAULT_WINDOWS_GIT_PATH = "C:\\Program Files\\Git\\bin\\bash.exe";
103

104
  private static final String OPTION_DETAILS_START = "([";
105

106
  private final IdeStartContextImpl startContext;
107

108
  private Path ideHome;
109

110
  private final Path ideRoot;
111

112
  private Path confPath;
113

114
  protected Path settingsPath;
115

116
  private Path settingsCommitIdPath;
117

118
  protected Path pluginsPath;
119

120
  private Path workspacePath;
121

122
  private Path workspacesBasePath;
123

124
  private String workspaceName;
125

126
  private Path cwd;
127

128
  private Path downloadPath;
129

130
  private Path userHome;
131

132
  private Path userHomeIde;
133

134
  private SystemPath path;
135

136
  private WindowsPathSyntax pathSyntax;
137

138
  private final SystemInfo systemInfo;
139

140
  private EnvironmentVariables variables;
141

142
  private final FileAccess fileAccess;
143

144
  protected CommandletManager commandletManager;
145

146
  protected ToolRepository defaultToolRepository;
147

148
  private CustomToolRepository customToolRepository;
149

150
  private MvnRepository mvnRepository;
151

152
  private NpmRepository npmRepository;
153

154
  private PipRepository pipRepository;
155

156
  private DirectoryMerger workspaceMerger;
157

158
  protected UrlMetadata urlMetadata;
159

160
  protected Path defaultExecutionDirectory;
161

162
  private StepImpl currentStep;
163

164
  private NetworkStatus networkStatus;
165

166
  protected IdeSystem system;
167

168
  private WindowsHelper windowsHelper;
169

170
  private final Map<String, String> privacyMap;
171

172
  private Path bash;
173

174
  private boolean julConfigured;
175

176
  private Path logfile;
177

178
  /**
179
   * The constructor.
180
   *
181
   * @param startContext the {@link IdeStartContextImpl}.
182
   * @param workingDirectory the optional {@link Path} to current working directory.
183
   */
184
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
185

186
    super();
2✔
187
    this.startContext = startContext;
3✔
188
    this.startContext.setArgFormatter(this);
4✔
189
    this.privacyMap = new HashMap<>();
5✔
190
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
191
    if (isTest()) {
3!
192
      configureJavaUtilLogging(null);
3✔
193
    }
194
    this.commandletManager = new CommandletManagerImpl(this);
6✔
195
    this.fileAccess = new FileAccessImpl(this);
6✔
196
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
197
    if (userHomeProperty != null) {
2!
198
      this.userHome = Path.of(userHomeProperty);
×
199
    }
200
    if (workingDirectory == null) {
2!
201
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
202
    }
203
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
204
    if (Files.isDirectory(workingDirectory)) {
5✔
205
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
206
    } else {
207
      LOG.warn("Current working directory does not exist: {}", workingDirectory);
4✔
208
    }
209
    this.cwd = workingDirectory;
3✔
210
    // detect IDE_HOME and WORKSPACE
211
    String workspace = null;
2✔
212
    Path ideHomeDir = null;
2✔
213
    IdeHomeAndWorkspace ideHomeAndWorkspace = findIdeHome(workingDirectory);
4✔
214
    if (ideHomeAndWorkspace != null) {
2!
215
      ideHomeDir = ideHomeAndWorkspace.home();
3✔
216
      workspace = ideHomeAndWorkspace.workspace();
3✔
217
    }
218

219
    // detection completed, initializing variables
220
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
221

222
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
223

224
    if (this.ideRoot != null) {
3✔
225
      Path tempDownloadPath = getTempDownloadPath();
3✔
226
      if (Files.isDirectory(tempDownloadPath)) {
6✔
227
        // TODO delete all files older than 1 day here...
228
      } else {
229
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
230
      }
231
    }
232
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
233
  }
1✔
234

235
  /**
236
   * Searches for the IDE home directory by traversing up the directory tree from the given working directory. This method can be overridden in test contexts to
237
   * add additional validation or boundary checks.
238
   *
239
   * @param workingDirectory the starting directory for the search.
240
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
241
   */
242
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
243

244
    Path currentDir = workingDirectory;
2✔
245
    String name1 = "";
2✔
246
    String name2 = "";
2✔
247
    String workspace = WORKSPACE_MAIN;
2✔
248
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
249

250
    while (currentDir != null) {
2✔
251
      LOG.trace("Looking for IDE_HOME in {}", currentDir);
4✔
252
      if (isIdeHome(currentDir)) {
4✔
253
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
254
          workspace = name2;
3✔
255
        }
256
        break;
257
      }
258
      name2 = name1;
2✔
259
      int nameCount = currentDir.getNameCount();
3✔
260
      if (nameCount >= 1) {
3✔
261
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
262
      }
263
      currentDir = currentDir.getParent();
3✔
264
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
265
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
266
        currentDir = null;
×
267
      }
268
    }
1✔
269

270
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
271
  }
272

273
  /**
274
   * @return a new {@link MvnRepository}
275
   */
276
  protected MvnRepository createMvnRepository() {
277
    return new MvnRepository(this);
5✔
278
  }
279

280
  /**
281
   * @return a new {@link NpmRepository}
282
   */
283
  protected NpmRepository createNpmRepository() {
284
    return new NpmRepository(this);
×
285
  }
286

287
  /**
288
   * @return a new {@link PipRepository}
289
   */
290
  protected PipRepository createPipRepository() {
291
    return new PipRepository(this);
×
292
  }
293

294
  private Path findIdeRoot(Path ideHomePath) {
295

296
    Path ideRootPath = null;
2✔
297
    if (ideHomePath != null) {
2✔
298
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
299
      ideRootPath = ideHomePath.getParent();
3✔
300
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
2!
301
        LOG.warn(
×
302
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
303
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
304
            ideRootPathFromEnv,
305
            ideHomePath.getFileName(), ideRootPath);
×
306
      }
307
    } else if (!isTest()) {
4!
308
      ideRootPath = getIdeRootPathFromEnv(true);
×
309
    }
310
    return ideRootPath;
2✔
311
  }
312

313
  /**
314
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
315
   */
316
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
317

318
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
319
    if (root != null) {
×
320
      Path rootPath = Path.of(root);
×
321
      if (Files.isDirectory(rootPath)) {
×
322
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
323
        if (withSanityCheck) {
×
324
          int nameCount = rootPath.getNameCount();
×
325
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
326
          int delta = absoluteNameCount - nameCount;
×
327
          if (delta >= 0) {
×
328
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
329
              String rootName = rootPath.getName(nameIndex).toString();
×
330
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
331
              if (!rootName.equals(absoluteRootName)) {
×
332
                LOG.warn("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
333
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
334
                break;
×
335
              }
336
            }
337
          } else {
338
            LOG.warn("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
339
                absoluteRootPath);
340
          }
341
        }
342
        return absoluteRootPath;
×
343
      } else if (withSanityCheck) {
×
344
        LOG.warn("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
345
      }
346
    }
347
    return null;
×
348
  }
349

350
  @Override
351
  public void setCwd(Path userDir, String workspace, Path ideHome) {
352

353
    this.cwd = userDir;
3✔
354
    this.workspaceName = workspace;
3✔
355
    this.ideHome = ideHome;
3✔
356
    if (ideHome == null) {
2✔
357
      this.workspacesBasePath = null;
3✔
358
      this.workspacePath = null;
3✔
359
      this.confPath = null;
3✔
360
      this.settingsPath = null;
3✔
361
      this.pluginsPath = null;
4✔
362
    } else {
363
      this.workspacesBasePath = this.ideHome.resolve(FOLDER_WORKSPACES);
6✔
364
      this.workspacePath = this.workspacesBasePath.resolve(this.workspaceName);
7✔
365
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
366
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
367
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
368
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
369
    }
370
    if (isTest()) {
3!
371
      // only for testing...
372
      if (this.ideHome == null) {
3✔
373
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
374
      } else {
375
        this.userHome = this.ideHome.resolve("home");
6✔
376
      }
377
    }
378
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
379
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
380
    resetPrivacyMap();
2✔
381
    this.path = computeSystemPath();
4✔
382
  }
1✔
383

384
  private String getMessageIdeHomeFound() {
385

386
    String wks = this.workspaceName;
3✔
387
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
388
      wks = "*".repeat(wks.length());
×
389
    }
390
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
391
  }
392

393
  private String getMessageNotInsideIdeProject() {
394

395
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
396
  }
397

398
  private String getMessageIdeRootNotFound() {
399

400
    String root = getSystem().getEnv("IDE_ROOT");
5✔
401
    if (root == null) {
2!
402
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
403
    } else {
404
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
405
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
406
    }
407
  }
408

409
  /**
410
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
411
   */
412
  public boolean isTest() {
413

414
    return false;
×
415
  }
416

417
  protected SystemPath computeSystemPath() {
418

419
    return new SystemPath(this);
×
420
  }
421

422
  /**
423
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
424
   *
425
   * @param dir the directory to check.
426
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
427
   */
428
  protected boolean isIdeHome(Path dir) {
429

430
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
431
      return false;
2✔
432
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
433
      return false;
×
434
    }
435
    return true;
2✔
436
  }
437

438
  private EnvironmentVariables createVariables() {
439

440
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
441
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
442
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
443
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
444
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
445
    return conf.resolved();
3✔
446
  }
447

448
  protected AbstractEnvironmentVariables createSystemVariables() {
449

450
    return EnvironmentVariables.ofSystem(this);
3✔
451
  }
452

453
  @Override
454
  public SystemInfo getSystemInfo() {
455

456
    return this.systemInfo;
3✔
457
  }
458

459
  @Override
460
  public FileAccess getFileAccess() {
461

462
    return this.fileAccess;
3✔
463
  }
464

465
  @Override
466
  public CommandletManager getCommandletManager() {
467

468
    return this.commandletManager;
3✔
469
  }
470

471
  @Override
472
  public ToolRepository getDefaultToolRepository() {
473

474
    return this.defaultToolRepository;
3✔
475
  }
476

477
  @Override
478
  public MvnRepository getMvnRepository() {
479
    if (this.mvnRepository == null) {
3✔
480
      this.mvnRepository = createMvnRepository();
4✔
481
    }
482
    return this.mvnRepository;
3✔
483
  }
484

485
  @Override
486
  public NpmRepository getNpmRepository() {
487
    if (this.npmRepository == null) {
3✔
488
      this.npmRepository = createNpmRepository();
4✔
489
    }
490
    return this.npmRepository;
3✔
491
  }
492

493
  @Override
494
  public PipRepository getPipRepository() {
495
    if (this.pipRepository == null) {
3✔
496
      this.pipRepository = createPipRepository();
4✔
497
    }
498
    return this.pipRepository;
3✔
499
  }
500

501
  @Override
502
  public CustomToolRepository getCustomToolRepository() {
503

504
    if (this.customToolRepository == null) {
3!
505
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
506
    }
507
    return this.customToolRepository;
3✔
508
  }
509

510
  @Override
511
  public Path getIdeHome() {
512

513
    return this.ideHome;
3✔
514
  }
515

516
  @Override
517
  public String getProjectName() {
518

519
    if (this.ideHome != null) {
3!
520
      return this.ideHome.getFileName().toString();
5✔
521
    }
522
    return "";
×
523
  }
524

525
  @Override
526
  public VersionIdentifier getProjectVersion() {
527

528
    if (this.ideHome != null) {
3!
529
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
530
      if (Files.exists(versionFile)) {
5✔
531
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
532
        return VersionIdentifier.of(version);
3✔
533
      }
534
    }
535
    return IdeMigrator.START_VERSION;
2✔
536
  }
537

538
  @Override
539
  public void setProjectVersion(VersionIdentifier version) {
540

541
    if (this.ideHome == null) {
3!
542
      throw new IllegalStateException("IDE_HOME not available!");
×
543
    }
544
    Objects.requireNonNull(version);
3✔
545
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
546
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
547
  }
1✔
548

549
  @Override
550
  public Path getIdeRoot() {
551

552
    return this.ideRoot;
3✔
553
  }
554

555
  @Override
556
  public Path getIdePath() {
557

558
    Path myIdeRoot = getIdeRoot();
3✔
559
    if (myIdeRoot == null) {
2✔
560
      return null;
2✔
561
    }
562
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
563
  }
564

565
  @Override
566
  public Path getCwd() {
567

568
    return this.cwd;
3✔
569
  }
570

571
  @Override
572
  public Path getTempPath() {
573

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

581
  @Override
582
  public Path getTempDownloadPath() {
583

584
    Path tmp = getTempPath();
3✔
585
    if (tmp == null) {
2!
586
      return null;
×
587
    }
588
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
589
  }
590

591
  @Override
592
  public Path getUserHome() {
593

594
    return this.userHome;
3✔
595
  }
596

597
  /**
598
   * This method should only be used for tests to mock user home.
599
   *
600
   * @param userHome the new value of {@link #getUserHome()}.
601
   */
602
  protected void setUserHome(Path userHome) {
603

604
    this.userHome = userHome;
3✔
605
    resetPrivacyMap();
2✔
606
  }
1✔
607

608
  @Override
609
  public Path getUserHomeIde() {
610

611
    return this.userHomeIde;
3✔
612
  }
613

614
  @Override
615
  public Path getSettingsPath() {
616

617
    return this.settingsPath;
3✔
618
  }
619

620
  @Override
621
  public Path getSettingsGitRepository() {
622

623
    Path settingsPath = getSettingsPath();
3✔
624
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
625
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
626
      LOG.error("Settings repository exists but is not a git repository.");
3✔
627
      return null;
2✔
628
    }
629
    return settingsPath;
2✔
630
  }
631

632
  @Override
633
  public boolean isSettingsRepositorySymlinkOrJunction() {
634

635
    Path settingsPath = getSettingsPath();
3✔
636
    if (settingsPath == null) {
2!
637
      return false;
×
638
    }
639
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
640
  }
641

642
  @Override
643
  public Path getSettingsCommitIdPath() {
644

645
    return this.settingsCommitIdPath;
3✔
646
  }
647

648
  @Override
649
  public Path getConfPath() {
650

651
    return this.confPath;
3✔
652
  }
653

654
  @Override
655
  public Path getSoftwarePath() {
656

657
    if (this.ideHome == null) {
3✔
658
      return null;
2✔
659
    }
660
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
661
  }
662

663
  @Override
664
  public Path getSoftwareExtraPath() {
665

666
    Path softwarePath = getSoftwarePath();
3✔
667
    if (softwarePath == null) {
2!
668
      return null;
×
669
    }
670
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
671
  }
672

673
  @Override
674
  public Path getSoftwareRepositoryPath() {
675

676
    Path idePath = getIdePath();
3✔
677
    if (idePath == null) {
2!
678
      return null;
×
679
    }
680
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
681
  }
682

683
  @Override
684
  public Path getPluginsPath() {
685

686
    return this.pluginsPath;
3✔
687
  }
688

689
  @Override
690
  public String getWorkspaceName() {
691

692
    return this.workspaceName;
3✔
693
  }
694

695
  @Override
696
  public Path getWorkspacesBasePath() {
697

698
    return this.workspacesBasePath;
3✔
699
  }
700

701
  @Override
702
  public Path getWorkspacePath() {
703

704
    return this.workspacePath;
3✔
705
  }
706

707
  @Override
708
  public Path getWorkspacePath(String workspace) {
709

710
    if (this.workspacesBasePath == null) {
3!
711
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
712
    }
713
    return this.workspacesBasePath.resolve(workspace);
5✔
714
  }
715

716
  @Override
717
  public Path getDownloadPath() {
718

719
    return this.downloadPath;
3✔
720
  }
721

722
  @Override
723
  public Path getUrlsPath() {
724

725
    Path idePath = getIdePath();
3✔
726
    if (idePath == null) {
2!
727
      return null;
×
728
    }
729
    return idePath.resolve(FOLDER_URLS);
4✔
730
  }
731

732
  @Override
733
  public Path getToolRepositoryPath() {
734

735
    Path idePath = getIdePath();
3✔
736
    if (idePath == null) {
2!
737
      return null;
×
738
    }
739
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
740
  }
741

742
  @Override
743
  public SystemPath getPath() {
744

745
    return this.path;
3✔
746
  }
747

748
  @Override
749
  public EnvironmentVariables getVariables() {
750

751
    if (this.variables == null) {
3✔
752
      this.variables = createVariables();
4✔
753
    }
754
    return this.variables;
3✔
755
  }
756

757
  @Override
758
  public UrlMetadata getUrls() {
759

760
    if (this.urlMetadata == null) {
3✔
761
      if (!isTest()) {
3!
762
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
763
      }
764
      this.urlMetadata = new UrlMetadata(this);
6✔
765
    }
766
    return this.urlMetadata;
3✔
767
  }
768

769
  @Override
770
  public boolean isQuietMode() {
771

772
    return this.startContext.isQuietMode();
4✔
773
  }
774

775
  @Override
776
  public boolean isBatchMode() {
777

778
    return this.startContext.isBatchMode();
4✔
779
  }
780

781
  @Override
782
  public boolean isForceMode() {
783

784
    return this.startContext.isForceMode();
4✔
785
  }
786

787
  @Override
788
  public boolean isForcePull() {
789

790
    return this.startContext.isForcePull();
4✔
791
  }
792

793
  @Override
794
  public boolean isForcePlugins() {
795

796
    return this.startContext.isForcePlugins();
4✔
797
  }
798

799
  @Override
800
  public boolean isForceRepositories() {
801

802
    return this.startContext.isForceRepositories();
4✔
803
  }
804

805
  @Override
806
  public boolean isOfflineMode() {
807

808
    return this.startContext.isOfflineMode();
4✔
809
  }
810

811
  @Override
812
  public boolean isPrivacyMode() {
813
    return this.startContext.isPrivacyMode();
4✔
814
  }
815

816
  @Override
817
  public boolean isSkipUpdatesMode() {
818

819
    return this.startContext.isSkipUpdatesMode();
4✔
820
  }
821

822
  @Override
823
  public boolean isNoColorsMode() {
824

825
    return this.startContext.isNoColorsMode();
×
826
  }
827

828
  @Override
829
  public NetworkStatus getNetworkStatus() {
830

831
    if (this.networkStatus == null) {
×
832
      this.networkStatus = new NetworkStatusImpl(this);
×
833
    }
834
    return this.networkStatus;
×
835
  }
836

837
  @Override
838
  public Locale getLocale() {
839

840
    Locale locale = this.startContext.getLocale();
4✔
841
    if (locale == null) {
2✔
842
      locale = Locale.getDefault();
2✔
843
    }
844
    return locale;
2✔
845
  }
846

847
  @Override
848
  public DirectoryMerger getWorkspaceMerger() {
849

850
    if (this.workspaceMerger == null) {
3✔
851
      this.workspaceMerger = new DirectoryMerger(this);
6✔
852
    }
853
    return this.workspaceMerger;
3✔
854
  }
855

856
  /**
857
   * @return the default execution directory in which a command process is executed.
858
   */
859
  @Override
860
  public Path getDefaultExecutionDirectory() {
861

862
    return this.defaultExecutionDirectory;
×
863
  }
864

865
  /**
866
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
867
   */
868
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
869

870
    if (defaultExecutionDirectory != null) {
×
871
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
872
    }
873
  }
×
874

875
  @Override
876
  public GitContext getGitContext() {
877

878
    return new GitContextImpl(this);
×
879
  }
880

881
  @Override
882
  public ProcessContext newProcess() {
883

884
    ProcessContext processContext = createProcessContext();
3✔
885
    if (this.defaultExecutionDirectory != null) {
3!
886
      processContext.directory(this.defaultExecutionDirectory);
×
887
    }
888
    return processContext;
2✔
889
  }
890

891
  @Override
892
  public IdeSystem getSystem() {
893

894
    if (this.system == null) {
×
895
      this.system = new IdeSystemImpl();
×
896
    }
897
    return this.system;
×
898
  }
899

900
  /**
901
   * @return a new instance of {@link ProcessContext}.
902
   * @see #newProcess()
903
   */
904
  protected ProcessContext createProcessContext() {
905

906
    return new ProcessContextImpl(this);
×
907
  }
908

909
  @Override
910
  public IdeLogLevel getLogLevelConsole() {
911

912
    return this.startContext.getLogLevelConsole();
×
913
  }
914

915
  @Override
916
  public IdeLogLevel getLogLevelLogger() {
917

918
    return this.startContext.getLogLevelLogger();
×
919
  }
920

921
  @Override
922
  public IdeLogListener getLogListener() {
923

924
    return this.startContext.getLogListener();
×
925
  }
926

927
  @Override
928
  public void logIdeHomeAndRootStatus() {
929
    if (this.ideRoot != null) {
3!
930
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
×
931
    }
932
    if (this.ideHome == null) {
3✔
933
      LOG.warn(getMessageNotInsideIdeProject());
5✔
934
    } else {
935
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
936
    }
937
  }
1✔
938

939
  @Override
940
  public String formatArgument(Object argument) {
941

942
    if (argument == null) {
2✔
943
      return null;
2✔
944
    }
945
    String result = argument.toString();
3✔
946
    if (isPrivacyMode()) {
3✔
947
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
948
        initializePrivacyMap(this.userHome, "~");
×
949
        String projectName = getProjectName();
×
950
        if (!projectName.isEmpty()) {
×
951
          this.privacyMap.put(projectName, "project");
×
952
        }
953
      }
954
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
955
        result = result.replace(entry.getKey(), entry.getValue());
×
956
      }
×
957
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
958
    }
959
    return result;
2✔
960
  }
961

962
  /**
963
   * @param path the sensitive {@link Path} to
964
   * @param replacement the replacement to mask the {@link Path} in log output.
965
   */
966
  protected void initializePrivacyMap(Path path, String replacement) {
967

968
    if (path == null) {
×
969
      return;
×
970
    }
971
    if (this.systemInfo.isWindows()) {
×
972
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
973
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
974
    } else {
975
      this.privacyMap.put(path.toString(), replacement);
×
976
    }
977
  }
×
978

979
  /**
980
   * Resets the privacy map in case fundamental values have changed.
981
   */
982
  private void resetPrivacyMap() {
983

984
    this.privacyMap.clear();
3✔
985
  }
1✔
986

987

988
  @Override
989
  public String askForInput(String message, String defaultValue) {
990

991
    while (true) {
992
      if (!message.isBlank()) {
3!
993
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
994
      }
995
      if (isBatchMode()) {
3!
996
        if (isForceMode()) {
×
997
          return defaultValue;
×
998
        } else {
999
          throw new CliAbortException();
×
1000
        }
1001
      }
1002
      String input = readLine().trim();
4✔
1003
      if (!input.isEmpty()) {
3!
1004
        return input;
2✔
1005
      } else {
1006
        if (defaultValue != null) {
×
1007
          return defaultValue;
×
1008
        }
1009
      }
1010
    }
×
1011
  }
1012

1013
  @Override
1014
  public <O> O question(O[] options, String question, Object... args) {
1015

1016
    assert (options.length > 0);
4!
1017
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1018
    return displayOptionsAndGetAnswer(options);
4✔
1019
  }
1020

1021
  private <O> O displayOptionsAndGetAnswer(O[] options) {
1022
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
1023
    int i = 0;
2✔
1024
    for (O option : options) {
16✔
1025
      i++;
1✔
1026
      String title = "" + option;
4✔
1027
      String key = computeOptionKey(title);
3✔
1028
      addMapping(mapping, key, option);
4✔
1029
      String numericKey = Integer.toString(i);
3✔
1030
      if (numericKey.equals(key)) {
4!
1031
        LOG.trace("Options should not be numeric: {}", key);
×
1032
      } else {
1033
        addMapping(mapping, numericKey, option);
4✔
1034
      }
1035
      IdeLogLevel.INTERACTION.log(LOG, "Option {}: {}", numericKey, title);
14✔
1036
    }
1037
    O option = null;
2✔
1038
    if (isBatchMode()) {
3!
1039
      if (isForceMode()) {
×
1040
        option = options[0];
×
1041
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
×
1042
      }
1043
    } else {
1044
      while (option == null) {
2✔
1045
        String answer = readLine();
3✔
1046
        option = mapping.get(answer);
4✔
1047
        if (option == null) {
2!
1048
          LOG.warn("Invalid answer: '{}' - please try again.", answer);
×
1049
        }
1050
      }
1✔
1051
    }
1052
    return option;
2✔
1053
  }
1054

1055
  private static String computeOptionKey(String option) {
1056
    String key = option;
2✔
1057
    int index = -1;
2✔
1058
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1059
      int currentIndex = key.indexOf(c);
4✔
1060
      if (currentIndex != -1) {
3✔
1061
        if ((index == -1) || (currentIndex < index)) {
3!
1062
          index = currentIndex;
2✔
1063
        }
1064
      }
1065
    }
1066
    if (index > 0) {
2✔
1067
      key = key.substring(0, index).trim();
6✔
1068
    }
1069
    return key;
2✔
1070
  }
1071

1072
  /**
1073
   * @return the input from the end-user (e.g. read from the console).
1074
   */
1075
  protected abstract String readLine();
1076

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

1079
    O duplicate = mapping.put(key, option);
5✔
1080
    if (duplicate != null) {
2!
1081
      throw new IllegalArgumentException("Duplicated option " + key);
×
1082
    }
1083
  }
1✔
1084

1085
  @Override
1086
  public Step getCurrentStep() {
1087

1088
    return this.currentStep;
×
1089
  }
1090

1091
  @Override
1092
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1093

1094
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1095
    return this.currentStep;
3✔
1096
  }
1097

1098
  /**
1099
   * Internal method to end the running {@link Step}.
1100
   *
1101
   * @param step the current {@link Step} to end.
1102
   */
1103
  public void endStep(StepImpl step) {
1104

1105
    if (step == this.currentStep) {
4!
1106
      this.currentStep = this.currentStep.getParent();
6✔
1107
    } else {
1108
      String currentStepName = "null";
×
1109
      if (this.currentStep != null) {
×
1110
        currentStepName = this.currentStep.getName();
×
1111
      }
1112
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1113
    }
1114
  }
1✔
1115

1116
  /**
1117
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1118
   *
1119
   * @param arguments the {@link CliArgument}.
1120
   * @return the return code of the execution.
1121
   */
1122
  public int run(CliArguments arguments) {
1123

1124
    CliArgument current = arguments.current();
3✔
1125
    assert (this.currentStep == null);
4!
1126
    boolean supressStepSuccess = false;
2✔
1127
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1128
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1129
    Commandlet cmd = null;
2✔
1130
    ValidationResult result = null;
2✔
1131
    try {
1132
      while (commandletIterator.hasNext()) {
3✔
1133
        cmd = commandletIterator.next();
4✔
1134
        result = applyAndRun(arguments.copy(), cmd);
6✔
1135
        if (result.isValid()) {
3!
1136
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1137
          step.success();
2✔
1138
          return ProcessResult.SUCCESS;
4✔
1139
        }
1140
      }
1141
      activateLogging(cmd);
3✔
1142
      verifyIdeMinVersion(false);
3✔
1143
      if (result != null) {
2!
1144
        LOG.error(result.getErrorMessage());
×
1145
      }
1146
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1147
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1148
      if (cmd != null) {
2!
1149
        help.commandlet.setValue(cmd);
×
1150
      }
1151
      help.run();
2✔
1152
      return 1;
4✔
1153
    } catch (Throwable t) {
1✔
1154
      activateLogging(cmd);
3✔
1155
      step.error(t, true);
4✔
1156
      if (this.logfile != null) {
3!
1157
        System.err.println("Logfile can be found at " + this.logfile); // do not use logger
×
1158
      }
1159
      throw t;
2✔
1160
    } finally {
1161
      step.close();
2✔
1162
      assert (this.currentStep == null);
4!
1163
      step.logSummary(supressStepSuccess);
3✔
1164
    }
1165
  }
1166

1167
  /**
1168
   * Ensure the logging system is initialized.
1169
   */
1170
  private void activateLogging(Commandlet cmd) {
1171

1172
    configureJavaUtilLogging(cmd);
3✔
1173
    this.startContext.activateLogging();
3✔
1174
  }
1✔
1175

1176
  /**
1177
   * Configures the logging system (JUL).
1178
   *
1179
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1180
   */
1181
  public void configureJavaUtilLogging(Commandlet cmd) {
1182

1183
    if (this.julConfigured) {
3✔
1184
      return;
1✔
1185
    }
1186
    boolean writeLogfile = isWriteLogfile(cmd);
4✔
1187
    this.startContext.setWriteLogfile(writeLogfile);
4✔
1188
    Properties properties = createJavaUtilLoggingProperties(writeLogfile, cmd);
5✔
1189
    try {
1190
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1191
      properties.store(out, null);
4✔
1192
      out.flush();
2✔
1193
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1194
      LogManager.getLogManager().readConfiguration(in);
3✔
1195
      this.julConfigured = true;
3✔
1196
      this.startContext.activateLogging();
3✔
1197
    } catch (IOException e) {
×
1198
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1199
    }
1✔
1200
  }
1✔
1201

1202
  protected boolean isWriteLogfile(Commandlet cmd) {
1203
    if (!cmd.isWriteLogFile()) {
×
1204
      return false;
×
1205
    }
1206
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1207
    return Boolean.TRUE.equals(writeLogfile);
×
1208
  }
1209

1210
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1211

1212
    Path idePath = getIdePath();
3✔
1213
    if (writeLogfile && (idePath == null)) {
2!
1214
      writeLogfile = false;
×
1215
      LOG.error("Cannot enable log-file since IDE_ROOT is undefined.");
×
1216
    }
1217
    Properties properties = new Properties();
4✔
1218
    // prevent 3rd party (e.g. java.lang.ProcessBuilder) logging into our console via JUL
1219
    // see JulLogLevel for the trick we did to workaround JUL flaws
1220
    properties.setProperty(".level", "SEVERE");
5✔
1221
    if (writeLogfile) {
2!
1222
      this.startContext.setLogLevelLogger(IdeLogLevel.TRACE);
×
1223
      String fileHandlerName = FileHandler.class.getName();
×
1224
      properties.setProperty("handlers", JulConsoleHandler.class.getName() + "," + fileHandlerName);
×
1225
      properties.setProperty(fileHandlerName + ".formatter", SimpleFormatter.class.getName());
×
1226
      properties.setProperty(fileHandlerName + ".encoding", "UTF-8");
×
1227
      this.logfile = createLogfilePath(idePath, cmd);
×
1228
      getFileAccess().mkdirs(this.logfile.getParent());
×
1229
      properties.setProperty(fileHandlerName + ".pattern", this.logfile.toString());
×
1230
    } else {
×
1231
      properties.setProperty("handlers", JulConsoleHandler.class.getName());
6✔
1232
    }
1233
    properties.setProperty(SimpleFormatter.class.getName() + ".format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL [%4$s] [%3$s] %5$s%6$s%n");
7✔
1234
    return properties;
2✔
1235
  }
1236

1237
  private Path createLogfilePath(Path idePath, Commandlet cmd) {
1238
    LocalDateTime now = LocalDateTime.now();
×
1239
    Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1240
    StringBuilder sb = new StringBuilder(32);
×
1241
    if (this.ideHome == null || ((cmd != null) && !cmd.isIdeHomeRequired())) {
×
1242
      sb.append("_ide-");
×
1243
    } else {
1244
      sb.append(this.ideHome.getFileName().toString());
×
1245
      sb.append('-');
×
1246
    }
1247
    sb.append("ide-");
×
1248
    if (cmd != null) {
×
1249
      sb.append(cmd.getName());
×
1250
      sb.append('-');
×
1251
    }
1252
    sb.append(DateTimeUtil.formatTime(now));
×
1253
    sb.append(".log");
×
1254
    return logsPath.resolve(sb.toString());
×
1255
  }
1256

1257
  @Override
1258
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1259

1260
    this.startContext.deactivateLogging(threshold);
4✔
1261
    lambda.run();
2✔
1262
    this.startContext.activateLogging();
3✔
1263
  }
1✔
1264

1265
  /**
1266
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1267
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1268
   *     {@link Commandlet} did not match and we have to try a different candidate).
1269
   */
1270
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1271

1272
    IdeLogLevel previousLogLevel = null;
2✔
1273
    cmd.reset();
2✔
1274
    ValidationResult result = apply(arguments, cmd);
5✔
1275
    if (result.isValid()) {
3!
1276
      result = cmd.validate();
3✔
1277
    }
1278
    if (result.isValid()) {
3!
1279
      LOG.debug("Running commandlet {}", cmd);
4✔
1280
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1281
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1282
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1283
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1284
      }
1285
      try {
1286
        if (cmd.isProcessableOutput()) {
3!
1287
          if (!LOG.isDebugEnabled()) {
×
1288
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1289
            previousLogLevel = this.startContext.setLogLevelConsole(IdeLogLevel.PROCESSABLE);
×
1290
          }
1291
        } else {
1292
          if (cmd.isIdeHomeRequired()) {
3!
1293
            LOG.debug(getMessageIdeHomeFound());
4✔
1294
          }
1295
          Path settingsRepository = getSettingsGitRepository();
3✔
1296
          if (settingsRepository != null) {
2!
1297
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1298
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1299
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1300
              String msg;
1301
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1302
                msg = "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1303
              } else {
1304
                msg = "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
×
1305
              }
1306
              IdeLogLevel.INTERACTION.log(LOG, msg);
×
1307
            }
1308
          }
1309
        }
1310
        boolean success = ensureLicenseAgreement(cmd);
4✔
1311
        if (!success) {
2!
1312
          return ValidationResultValid.get();
×
1313
        }
1314
        cmd.run();
2✔
1315
      } finally {
1316
        if (previousLogLevel != null) {
2!
1317
          this.startContext.setLogLevelConsole(previousLogLevel);
×
1318
        }
1319
      }
1✔
1320
    } else {
1321
      LOG.trace("Commandlet did not match");
×
1322
    }
1323
    return result;
2✔
1324
  }
1325

1326
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1327

1328
    if (isTest()) {
3!
1329
      return true; // ignore for tests
2✔
1330
    }
1331
    getFileAccess().mkdirs(this.userHomeIde);
×
1332
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1333
    if (Files.isRegularFile(licenseAgreement)) {
×
1334
      return true; // success, license already accepted
×
1335
    }
1336
    if (cmd instanceof EnvironmentCommandlet) {
×
1337
      // 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
1338
      // 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
1339
      // printing anything anymore in such case.
1340
      return false;
×
1341
    }
1342
    IdeLogLevel oldLogLevel = this.startContext.getLogLevelConsole();
×
1343
    IdeLogLevel newLogLevel = oldLogLevel;
×
1344
    if (oldLogLevel.ordinal() > IdeLogLevel.INFO.ordinal()) {
×
1345
      newLogLevel = IdeLogLevel.INFO;
×
1346
      this.startContext.setLogLevelConsole(newLogLevel);
×
1347
    }
1348
    StringBuilder sb = new StringBuilder(1180);
×
1349
    sb.append(LOGO).append("""
×
1350
        Welcome to IDEasy!
1351
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1352
        It supports automatic download and installation of arbitrary 3rd party tools.
1353
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1354
        But if explicitly configured, also commercial software that requires an additional license may be used.
1355
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1356
        You are solely responsible for all risks implied by using this software.
1357
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1358
        You will be able to find it online under the following URL:
1359
        """).append(LICENSE_URL);
×
1360
    if (this.ideRoot != null) {
×
1361
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1362
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1363
    }
1364
    LOG.info(sb.toString());
×
1365
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1366

1367
    sb.setLength(0);
×
1368
    LocalDateTime now = LocalDateTime.now();
×
1369
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1370
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1371
    try {
1372
      Files.writeString(licenseAgreement, sb);
×
1373
    } catch (Exception e) {
×
1374
      throw new RuntimeException("Failed to save license agreement!", e);
×
1375
    }
×
1376
    if (oldLogLevel != newLogLevel) {
×
1377
      this.startContext.setLogLevelConsole(oldLogLevel);
×
1378
    }
1379
    return true;
×
1380
  }
1381

1382
  @Override
1383
  public void verifyIdeMinVersion(boolean throwException) {
1384
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1385
    if (minVersion == null) {
2✔
1386
      return;
1✔
1387
    }
1388
    VersionIdentifier versionIdentifier = IdeVersion.getVersionIdentifier();
2✔
1389
    if (versionIdentifier.compareVersion(minVersion).isLess() && !IdeVersion.isUndefined()) {
7!
1390
      String message = String.format("Your version of IDEasy is currently %s\n"
13✔
1391
          + "However, this is too old as your project requires at latest version %s\n"
1392
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1393
          + "ide upgrade", versionIdentifier, minVersion);
1394
      if (throwException) {
2✔
1395
        throw new CliException(message);
5✔
1396
      } else {
1397
        LOG.warn(message);
3✔
1398
      }
1399
    }
1400
  }
1✔
1401

1402
  /**
1403
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1404
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1405
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1406
   */
1407
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1408

1409
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1410
    if (arguments.current().isStart()) {
4✔
1411
      arguments.next();
3✔
1412
    }
1413
    if (includeContextOptions) {
2✔
1414
      ContextCommandlet cc = new ContextCommandlet();
4✔
1415
      for (Property<?> property : cc.getProperties()) {
11✔
1416
        assert (property.isOption());
4!
1417
        property.apply(arguments, this, cc, collector);
7✔
1418
      }
1✔
1419
    }
1420
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1421
    CliArgument current = arguments.current();
3✔
1422
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1423
      collector.add(current.get(), null, null, null);
7✔
1424
    }
1425
    arguments.next();
3✔
1426
    while (commandletIterator.hasNext()) {
3✔
1427
      Commandlet cmd = commandletIterator.next();
4✔
1428
      if (!arguments.current().isEnd()) {
4✔
1429
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1430
      }
1431
    }
1✔
1432
    return collector.getSortedCandidates();
3✔
1433
  }
1434

1435
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1436

1437
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1438
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1439
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1440
    List<Property<?>> properties = cmd.getProperties();
3✔
1441
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1442
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1443
    for (Property<?> property : properties) {
10✔
1444
      if (property.isOption()) {
3✔
1445
        optionProperties.add(property);
4✔
1446
      }
1447
    }
1✔
1448
    CliArgument currentArgument = arguments.current();
3✔
1449
    while (!currentArgument.isEnd()) {
3✔
1450
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1451
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1452
        if (currentArgument.isCompletion()) {
3✔
1453
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1454
          while (optionIterator.hasNext()) {
3✔
1455
            Property<?> option = optionIterator.next();
4✔
1456
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1457
            if (success) {
2✔
1458
              optionIterator.remove();
2✔
1459
              arguments.next();
3✔
1460
            }
1461
          }
1✔
1462
        } else {
1✔
1463
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1464
          if (option != null) {
2✔
1465
            arguments.next();
3✔
1466
            boolean removed = optionProperties.remove(option);
4✔
1467
            if (!removed) {
2!
1468
              option = null;
×
1469
            }
1470
          }
1471
          if (option == null) {
2✔
1472
            LOG.trace("No such option was found.");
3✔
1473
            return;
1✔
1474
          }
1475
        }
1✔
1476
      } else {
1477
        if (valueIterator.hasNext()) {
3✔
1478
          Property<?> valueProperty = valueIterator.next();
4✔
1479
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1480
          if (!success) {
2✔
1481
            LOG.trace("Completion cannot match any further.");
3✔
1482
            return;
1✔
1483
          }
1484
        } else {
1✔
1485
          LOG.trace("No value left for completion.");
3✔
1486
          return;
1✔
1487
        }
1488
      }
1489
      currentArgument = arguments.current();
4✔
1490
    }
1491
  }
1✔
1492

1493
  /**
1494
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1495
   *     {@link CliArguments#copy() copy} as needed.
1496
   * @param cmd the potential {@link Commandlet} to match.
1497
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1498
   */
1499
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1500

1501
    LOG.trace("Trying to match arguments to commandlet {}", cmd.getName());
5✔
1502
    CliArgument currentArgument = arguments.current();
3✔
1503
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1504
    Property<?> property = null;
2✔
1505
    if (propertyIterator.hasNext()) {
3!
1506
      property = propertyIterator.next();
4✔
1507
    }
1508
    while (!currentArgument.isEnd()) {
3✔
1509
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1510
      Property<?> currentProperty = property;
2✔
1511
      if (!arguments.isEndOptions()) {
3!
1512
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1513
        if (option != null) {
2!
1514
          currentProperty = option;
×
1515
        }
1516
      }
1517
      if (currentProperty == null) {
2!
1518
        LOG.trace("No option or next value found");
×
1519
        ValidationState state = new ValidationState(null);
×
1520
        state.addErrorMessage("No matching property found");
×
1521
        return state;
×
1522
      }
1523
      LOG.trace("Next property candidate to match argument is {}", currentProperty);
4✔
1524
      if (currentProperty == property) {
3!
1525
        if (!property.isMultiValued()) {
3✔
1526
          if (propertyIterator.hasNext()) {
3✔
1527
            property = propertyIterator.next();
5✔
1528
          } else {
1529
            property = null;
2✔
1530
          }
1531
        }
1532
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1533
          arguments.stopSplitShortOptions();
2✔
1534
        }
1535
      }
1536
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1537
      if (!matches) {
2!
1538
        ValidationState state = new ValidationState(null);
×
1539
        state.addErrorMessage("No matching property found");
×
1540
        return state;
×
1541
      }
1542
      currentArgument = arguments.current();
3✔
1543
    }
1✔
1544
    return ValidationResultValid.get();
2✔
1545
  }
1546

1547
  @Override
1548
  public Path findBash() {
1549
    if (this.bash != null) {
3✔
1550
      return this.bash;
3✔
1551
    }
1552
    Path bashPath = findBashOnBashPath();
3✔
1553
    if (bashPath == null) {
2✔
1554
      bashPath = findBashInPath();
3✔
1555
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1556
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1557
        if (bashPath == null) {
2!
1558
          bashPath = findBashInWindowsRegistry();
3✔
1559
        }
1560
      }
1561
    }
1562
    if (bashPath == null) {
2✔
1563
      LOG.error("No bash executable could be found on your system.");
4✔
1564
    } else {
1565
      this.bash = bashPath;
3✔
1566
    }
1567
    return bashPath;
2✔
1568
  }
1569

1570
  private Path findBashOnBashPath() {
1571
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1572
    Path bash;
1573
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1574
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1575
    if (bashVariable != null) {
2✔
1576
      bash = Path.of(bashVariable);
5✔
1577
      if (Files.exists(bash)) {
5✔
1578
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1579
        return bash;
2✔
1580
      } else {
1581
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1582
        return null;
2✔
1583
      }
1584
    } else {
1585
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1586
      return null;
2✔
1587
    }
1588
  }
1589

1590
  /**
1591
   * @param path the path to check.
1592
   * @param toIgnore the String sequence which needs to be checked and ignored.
1593
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1594
   */
1595
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1596
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1597
    return !s.contains(toIgnore);
7!
1598
  }
1599

1600
  /**
1601
   * Tries to find the bash.exe within the PATH environment variable.
1602
   *
1603
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1604
   */
1605
  private Path findBashInPath() {
1606
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1607
    Path bash;
1608
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1609
    if (pathVariableName != null) {
2!
1610
      Path plainBash = Path.of(BASH);
5✔
1611
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1612
          "\\windows\\system32");
1613
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1614
      bash = bashPath.toAbsolutePath();
3✔
1615
      if (bashPath.equals(plainBash)) {
4✔
1616
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1617
        bash = null;
3✔
1618
      } else {
1619
        if (Files.exists(bashPath)) {
5!
1620
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1621
        } else {
1622
          bash = null;
×
1623
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1624
        }
1625
      }
1626
    } else {
1✔
1627
      bash = null;
×
1628
      // this should never happen...
1629
      LOG.error("PATH environment variable was not found");
×
1630
    }
1631
    return bash;
2✔
1632
  }
1633

1634
  /**
1635
   * Tries to find the bash.exe within the Windows registry.
1636
   *
1637
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1638
   */
1639
  protected Path findBashInWindowsRegistry() {
1640
    LOG.trace("Trying to find bash in Windows registry");
×
1641
    // If not found in the default location, try the registry query
1642
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1643
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1644
    for (String bashVariant : bashVariants) {
×
1645
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1646
      for (String registryKey : registryKeys) {
×
1647
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1648
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1649
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1650

1651
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1652
        if (path != null) {
×
1653
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1654
          if (Files.exists(bashPath)) {
×
1655
            LOG.debug("Found bash at: {}", bashPath);
×
1656
            return bashPath;
×
1657
          } else {
1658
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1659
            return null;
×
1660
          }
1661
        } else {
1662
          LOG.info("No bash executable could be found in the Windows registry.");
×
1663
        }
1664
      }
1665
    }
1666
    // no bash found
1667
    return null;
×
1668
  }
1669

1670
  private Path findBashOnWindowsDefaultGitPath() {
1671
    // Check if Git Bash exists in the default location
1672
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1673
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1674
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1675
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1676
      return defaultPath;
×
1677
    }
1678
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1679
    return null;
2✔
1680
  }
1681

1682
  @Override
1683
  public WindowsPathSyntax getPathSyntax() {
1684

1685
    return this.pathSyntax;
3✔
1686
  }
1687

1688
  /**
1689
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1690
   */
1691
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1692

1693
    this.pathSyntax = pathSyntax;
3✔
1694
  }
1✔
1695

1696
  /**
1697
   * @return the {@link IdeStartContextImpl}.
1698
   */
1699
  public IdeStartContextImpl getStartContext() {
1700

1701
    return startContext;
3✔
1702
  }
1703

1704
  /**
1705
   * @return the {@link WindowsHelper}.
1706
   */
1707
  public final WindowsHelper getWindowsHelper() {
1708

1709
    if (this.windowsHelper == null) {
3✔
1710
      this.windowsHelper = createWindowsHelper();
4✔
1711
    }
1712
    return this.windowsHelper;
3✔
1713
  }
1714

1715
  /**
1716
   * @return the new {@link WindowsHelper} instance.
1717
   */
1718
  protected WindowsHelper createWindowsHelper() {
1719

1720
    return new WindowsHelperImpl(this);
×
1721
  }
1722

1723
  /**
1724
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1725
   */
1726
  public void reload() {
1727

1728
    this.variables = null;
3✔
1729
    this.customToolRepository = null;
3✔
1730
  }
1✔
1731

1732
  @Override
1733
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1734

1735
    assert (Files.isDirectory(installationPath));
6!
1736
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1737
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1738
  }
1✔
1739

1740
  /*
1741
   * @param home the IDE_HOME directory.
1742
   * @param workspace the name of the active workspace folder.
1743
   */
1744
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1745

1746
  }
1747

1748
  /**
1749
   * Returns the default git path on Windows. Required to be overwritten in tests.
1750
   *
1751
   * @return default path to git on Windows.
1752
   */
1753
  public String getDefaultWindowsGitPath() {
1754
    return DEFAULT_WINDOWS_GIT_PATH;
×
1755
  }
1756

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

© 2026 Coveralls, Inc