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

devonfw / IDEasy / 24787861383

22 Apr 2026 03:43PM UTC coverage: 70.614% (-0.04%) from 70.654%
24787861383

push

github

web-flow
#1815: Suppress update notification while updating (#1816)

4324 of 6768 branches covered (63.89%)

Branch coverage included in aggregate %.

11192 of 15205 relevant lines covered (73.61%)

3.11 hits per line

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

65.09
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.UpdateCommandlet;
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
    if (options.length == 1) {
4✔
1038
      mapping.put("", options[0]);
7✔
1039
    }
1040
    O option = null;
2✔
1041
    if (isBatchMode()) {
3!
1042
      if (isForceMode()) {
×
1043
        option = options[0];
×
1044
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
×
1045
      }
1046
    } else {
1047
      while (option == null) {
2✔
1048
        String answer = readLine();
3✔
1049
        option = mapping.get(answer);
4✔
1050
        if (option == null) {
2!
1051
          LOG.warn("Invalid answer: '{}' - please try again.", answer);
×
1052
        }
1053
      }
1✔
1054
    }
1055
    return option;
2✔
1056
  }
1057

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

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

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

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

1088
  @Override
1089
  public Step getCurrentStep() {
1090

1091
    return this.currentStep;
×
1092
  }
1093

1094
  @Override
1095
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1096

1097
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1098
    return this.currentStep;
3✔
1099
  }
1100

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1271
    IdeLogLevel previousLogLevel = null;
2✔
1272
    cmd.reset();
2✔
1273
    ValidationResult result = apply(arguments, cmd);
5✔
1274
    if (result.isValid()) {
3!
1275
      result = cmd.validate();
3✔
1276
    }
1277
    if (result.isValid()) {
3!
1278
      LOG.debug("Running commandlet {}", cmd);
4✔
1279
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1280
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1281
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1282
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1283
      }
1284
      try {
1285
        if (cmd.isProcessableOutput()) {
3!
1286
          if (!LOG.isDebugEnabled()) {
×
1287
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1288
            previousLogLevel = this.startContext.setLogLevelConsole(IdeLogLevel.PROCESSABLE);
×
1289
          }
1290
        } else {
1291
          if (cmd.isIdeHomeRequired()) {
3!
1292
            LOG.debug(getMessageIdeHomeFound());
4✔
1293
          }
1294
          Path settingsRepository = getSettingsGitRepository();
3✔
1295
          if (settingsRepository != null) {
2!
1296
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1297
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1298
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1299

1300
              // Inform the user that an update is available. The update message is suppressed if we are already running the update
1301
              String msg = determineSettingsUpdateMessage(cmd);
×
1302
              if (msg != null) {
×
1303
                IdeLogLevel.INTERACTION.log(LOG, msg);
×
1304
              }
1305
            }
1306
          }
1307
        }
1308
        boolean success = ensureLicenseAgreement(cmd);
4✔
1309
        if (!success) {
2!
1310
          return ValidationResultValid.get();
×
1311
        }
1312
        cmd.run();
2✔
1313
      } finally {
1314
        if (previousLogLevel != null) {
2!
1315
          this.startContext.setLogLevelConsole(previousLogLevel);
×
1316
        }
1317
      }
1✔
1318
    } else {
1319
      LOG.trace("Commandlet did not match");
×
1320
    }
1321
    return result;
2✔
1322
  }
1323

1324

1325
  /**
1326
   * When an update is available for the settings repository, we log a message to the console, reminding the user to run {@code ide update}.
1327
   * This method determines the correct message to log, depending on whether the settings repository is a symlink/junction, or not.
1328
   * Should the user already be running the appropriate {@code ide update} command, the message is suppressed to avoid confusion.
1329
   *
1330
   * @param cmd the {@link Commandlet}.
1331
   * @return {@code msg} to log to the console. {@code null} if the message is suppressed.
1332
   */
1333
  private String determineSettingsUpdateMessage(Commandlet cmd) {
1334
    if (isSettingsRepositorySymlinkOrJunction()) {
×
1335
      if ((cmd instanceof UpdateCommandlet) && isForceMode()) {
×
1336
        return null;
×
1337
      }
1338
      return "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1339
    } else {
1340
      if (cmd instanceof UpdateCommandlet) {
×
1341
        return null;
×
1342
      }
1343
      return "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
×
1344
    }
1345
  }
1346

1347
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1348

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

1389
    sb.setLength(0);
×
1390
    LocalDateTime now = LocalDateTime.now();
×
1391
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1392
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1393
    try {
1394
      Files.writeString(licenseAgreement, sb);
×
1395
    } catch (Exception e) {
×
1396
      throw new RuntimeException("Failed to save license agreement!", e);
×
1397
    }
×
1398
    if (oldLogLevel != newLogLevel) {
×
1399
      this.startContext.setLogLevelConsole(oldLogLevel);
×
1400
    }
1401
    return true;
×
1402
  }
1403

1404
  @Override
1405
  public void verifyIdeMinVersion(boolean throwException) {
1406
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1407
    if (minVersion == null) {
2✔
1408
      return;
1✔
1409
    }
1410
    VersionIdentifier versionIdentifier = IdeVersion.getVersionIdentifier();
2✔
1411
    if (versionIdentifier.compareVersion(minVersion).isLess() && !IdeVersion.isUndefined()) {
7!
1412
      String message = String.format("Your version of IDEasy is currently %s\n"
13✔
1413
          + "However, this is too old as your project requires at latest version %s\n"
1414
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1415
          + "ide upgrade", versionIdentifier, minVersion);
1416
      if (throwException) {
2✔
1417
        throw new CliException(message);
5✔
1418
      } else {
1419
        LOG.warn(message);
3✔
1420
      }
1421
    }
1422
  }
1✔
1423

1424
  /**
1425
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1426
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1427
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1428
   */
1429
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1430

1431
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1432
    if (arguments.current().isStart()) {
4✔
1433
      arguments.next();
3✔
1434
    }
1435
    if (includeContextOptions) {
2✔
1436
      ContextCommandlet cc = new ContextCommandlet();
4✔
1437
      for (Property<?> property : cc.getProperties()) {
11✔
1438
        assert (property.isOption());
4!
1439
        property.apply(arguments, this, cc, collector);
7✔
1440
      }
1✔
1441
    }
1442
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1443
    CliArgument current = arguments.current();
3✔
1444
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1445
      collector.add(current.get(), null, null, null);
7✔
1446
    }
1447
    arguments.next();
3✔
1448
    while (commandletIterator.hasNext()) {
3✔
1449
      Commandlet cmd = commandletIterator.next();
4✔
1450
      if (!arguments.current().isEnd()) {
4✔
1451
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1452
      }
1453
    }
1✔
1454
    return collector.getSortedCandidates();
3✔
1455
  }
1456

1457
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1458

1459
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1460
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1461
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1462
    List<Property<?>> properties = cmd.getProperties();
3✔
1463
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1464
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1465
    for (Property<?> property : properties) {
10✔
1466
      if (property.isOption()) {
3✔
1467
        optionProperties.add(property);
4✔
1468
      }
1469
    }
1✔
1470
    CliArgument currentArgument = arguments.current();
3✔
1471
    while (!currentArgument.isEnd()) {
3✔
1472
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1473
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1474
        if (currentArgument.isCompletion()) {
3✔
1475
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1476
          while (optionIterator.hasNext()) {
3✔
1477
            Property<?> option = optionIterator.next();
4✔
1478
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1479
            if (success) {
2✔
1480
              optionIterator.remove();
2✔
1481
              arguments.next();
3✔
1482
            }
1483
          }
1✔
1484
        } else {
1✔
1485
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1486
          if (option != null) {
2✔
1487
            arguments.next();
3✔
1488
            boolean removed = optionProperties.remove(option);
4✔
1489
            if (!removed) {
2!
1490
              option = null;
×
1491
            }
1492
          }
1493
          if (option == null) {
2✔
1494
            LOG.trace("No such option was found.");
3✔
1495
            return;
1✔
1496
          }
1497
        }
1✔
1498
      } else {
1499
        if (valueIterator.hasNext()) {
3✔
1500
          Property<?> valueProperty = valueIterator.next();
4✔
1501
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1502
          if (!success) {
2✔
1503
            LOG.trace("Completion cannot match any further.");
3✔
1504
            return;
1✔
1505
          }
1506
        } else {
1✔
1507
          LOG.trace("No value left for completion.");
3✔
1508
          return;
1✔
1509
        }
1510
      }
1511
      currentArgument = arguments.current();
4✔
1512
    }
1513
  }
1✔
1514

1515
  /**
1516
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1517
   *     {@link CliArguments#copy() copy} as needed.
1518
   * @param cmd the potential {@link Commandlet} to match.
1519
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1520
   */
1521
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1522

1523
    LOG.trace("Trying to match arguments to commandlet {}", cmd.getName());
5✔
1524
    CliArgument currentArgument = arguments.current();
3✔
1525
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1526
    Property<?> property = null;
2✔
1527
    if (propertyIterator.hasNext()) {
3!
1528
      property = propertyIterator.next();
4✔
1529
    }
1530
    while (!currentArgument.isEnd()) {
3✔
1531
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1532
      Property<?> currentProperty = property;
2✔
1533
      if (!arguments.isEndOptions()) {
3!
1534
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1535
        if (option != null) {
2!
1536
          currentProperty = option;
×
1537
        }
1538
      }
1539
      if (currentProperty == null) {
2!
1540
        LOG.trace("No option or next value found");
×
1541
        ValidationState state = new ValidationState(null);
×
1542
        state.addErrorMessage("No matching property found");
×
1543
        return state;
×
1544
      }
1545
      LOG.trace("Next property candidate to match argument is {}", currentProperty);
4✔
1546
      if (currentProperty == property) {
3!
1547
        if (!property.isMultiValued()) {
3✔
1548
          if (propertyIterator.hasNext()) {
3✔
1549
            property = propertyIterator.next();
5✔
1550
          } else {
1551
            property = null;
2✔
1552
          }
1553
        }
1554
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1555
          arguments.stopSplitShortOptions();
2✔
1556
        }
1557
      }
1558
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1559
      if (!matches) {
2!
1560
        ValidationState state = new ValidationState(null);
×
1561
        state.addErrorMessage("No matching property found");
×
1562
        return state;
×
1563
      }
1564
      currentArgument = arguments.current();
3✔
1565
    }
1✔
1566
    return ValidationResultValid.get();
2✔
1567
  }
1568

1569
  @Override
1570
  public Path findBash() {
1571
    if (this.bash != null) {
3✔
1572
      return this.bash;
3✔
1573
    }
1574
    Path bashPath = findBashOnBashPath();
3✔
1575
    if (bashPath == null) {
2✔
1576
      bashPath = findBashInPath();
3✔
1577
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1578
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1579
        if (bashPath == null) {
2!
1580
          bashPath = findBashInWindowsRegistry();
3✔
1581
        }
1582
      }
1583
    }
1584
    if (bashPath == null) {
2✔
1585
      LOG.error("No bash executable could be found on your system.");
4✔
1586
    } else {
1587
      this.bash = bashPath;
3✔
1588
    }
1589
    return bashPath;
2✔
1590
  }
1591

1592
  private Path findBashOnBashPath() {
1593
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1594
    Path bash;
1595
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1596
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1597
    if (bashVariable != null) {
2✔
1598
      bash = Path.of(bashVariable);
5✔
1599
      if (Files.exists(bash)) {
5✔
1600
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1601
        return bash;
2✔
1602
      } else {
1603
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1604
        return null;
2✔
1605
      }
1606
    } else {
1607
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1608
      return null;
2✔
1609
    }
1610
  }
1611

1612
  /**
1613
   * @param path the path to check.
1614
   * @param toIgnore the String sequence which needs to be checked and ignored.
1615
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1616
   */
1617
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1618
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1619
    return !s.contains(toIgnore);
7!
1620
  }
1621

1622
  /**
1623
   * Tries to find the bash.exe within the PATH environment variable.
1624
   *
1625
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1626
   */
1627
  private Path findBashInPath() {
1628
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1629
    Path bash;
1630
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1631
    if (pathVariableName != null) {
2!
1632
      Path plainBash = Path.of(BASH);
5✔
1633
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1634
          "\\windows\\system32");
1635
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1636
      bash = bashPath.toAbsolutePath();
3✔
1637
      if (bashPath.equals(plainBash)) {
4✔
1638
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1639
        bash = null;
3✔
1640
      } else {
1641
        if (Files.exists(bashPath)) {
5!
1642
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1643
        } else {
1644
          bash = null;
×
1645
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1646
        }
1647
      }
1648
    } else {
1✔
1649
      bash = null;
×
1650
      // this should never happen...
1651
      LOG.error("PATH environment variable was not found");
×
1652
    }
1653
    return bash;
2✔
1654
  }
1655

1656
  /**
1657
   * Tries to find the bash.exe within the Windows registry.
1658
   *
1659
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1660
   */
1661
  protected Path findBashInWindowsRegistry() {
1662
    LOG.trace("Trying to find bash in Windows registry");
×
1663
    // If not found in the default location, try the registry query
1664
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1665
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1666
    for (String bashVariant : bashVariants) {
×
1667
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1668
      for (String registryKey : registryKeys) {
×
1669
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1670
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1671
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1672

1673
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1674
        if (path != null) {
×
1675
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1676
          if (Files.exists(bashPath)) {
×
1677
            LOG.debug("Found bash at: {}", bashPath);
×
1678
            return bashPath;
×
1679
          } else {
1680
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1681
            return null;
×
1682
          }
1683
        } else {
1684
          LOG.info("No bash executable could be found in the Windows registry.");
×
1685
        }
1686
      }
1687
    }
1688
    // no bash found
1689
    return null;
×
1690
  }
1691

1692
  private Path findBashOnWindowsDefaultGitPath() {
1693
    // Check if Git Bash exists in the default location
1694
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1695
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1696
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1697
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1698
      return defaultPath;
×
1699
    }
1700
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1701
    return null;
2✔
1702
  }
1703

1704
  @Override
1705
  public WindowsPathSyntax getPathSyntax() {
1706

1707
    return this.pathSyntax;
3✔
1708
  }
1709

1710
  /**
1711
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1712
   */
1713
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1714

1715
    this.pathSyntax = pathSyntax;
3✔
1716
  }
1✔
1717

1718
  /**
1719
   * @return the {@link IdeStartContextImpl}.
1720
   */
1721
  public IdeStartContextImpl getStartContext() {
1722

1723
    return startContext;
3✔
1724
  }
1725

1726
  /**
1727
   * @return the {@link WindowsHelper}.
1728
   */
1729
  public final WindowsHelper getWindowsHelper() {
1730

1731
    if (this.windowsHelper == null) {
3✔
1732
      this.windowsHelper = createWindowsHelper();
4✔
1733
    }
1734
    return this.windowsHelper;
3✔
1735
  }
1736

1737
  /**
1738
   * @return the new {@link WindowsHelper} instance.
1739
   */
1740
  protected WindowsHelper createWindowsHelper() {
1741

1742
    return new WindowsHelperImpl(this);
×
1743
  }
1744

1745
  /**
1746
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1747
   */
1748
  public void reload() {
1749

1750
    this.variables = null;
3✔
1751
    this.customToolRepository = null;
3✔
1752
  }
1✔
1753

1754
  @Override
1755
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1756

1757
    assert (Files.isDirectory(installationPath));
6!
1758
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1759
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1760
  }
1✔
1761

1762
  /*
1763
   * @param home the IDE_HOME directory.
1764
   * @param workspace the name of the active workspace folder.
1765
   */
1766
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1767

1768
  }
1769

1770
  /**
1771
   * Returns the default git path on Windows. Required to be overwritten in tests.
1772
   *
1773
   * @return default path to git on Windows.
1774
   */
1775
  public String getDefaultWindowsGitPath() {
1776
    return DEFAULT_WINDOWS_GIT_PATH;
×
1777
  }
1778

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