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

devonfw / IDEasy / 24239455949

10 Apr 2026 10:52AM UTC coverage: 70.478% (+0.01%) from 70.466%
24239455949

Pull #1813

github

web-flow
Merge 601588651 into 6f34d3c1f
Pull Request #1813: #1270: Load global user settings outside project

4265 of 6690 branches covered (63.75%)

Branch coverage included in aggregate %.

11071 of 15070 relevant lines covered (73.46%)

3.1 hits per line

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

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

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

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

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

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

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

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

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

105
  private final IdeStartContextImpl startContext;
106

107
  private Path ideHome;
108

109
  private final Path ideRoot;
110

111
  private Path confPath;
112

113
  protected Path settingsPath;
114

115
  private Path settingsCommitIdPath;
116

117
  protected Path pluginsPath;
118

119
  private Path workspacePath;
120

121
  private Path workspacesBasePath;
122

123
  private String workspaceName;
124

125
  private Path cwd;
126

127
  private Path downloadPath;
128

129
  private Path userHome;
130

131
  private Path userHomeIde;
132

133
  private SystemPath path;
134

135
  private WindowsPathSyntax pathSyntax;
136

137
  private final SystemInfo systemInfo;
138

139
  private EnvironmentVariables variables;
140

141
  private final FileAccess fileAccess;
142

143
  protected CommandletManager commandletManager;
144

145
  protected ToolRepository defaultToolRepository;
146

147
  private CustomToolRepository customToolRepository;
148

149
  private MvnRepository mvnRepository;
150

151
  private NpmRepository npmRepository;
152

153
  private PipRepository pipRepository;
154

155
  private DirectoryMerger workspaceMerger;
156

157
  protected UrlMetadata urlMetadata;
158

159
  protected Path defaultExecutionDirectory;
160

161
  private StepImpl currentStep;
162

163
  private NetworkStatus networkStatus;
164

165
  protected IdeSystem system;
166

167
  private WindowsHelper windowsHelper;
168

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

171
  private Path bash;
172

173
  private boolean julConfigured;
174

175
  private Path logfile;
176

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

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

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

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

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

234
  /**
235
   * 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
236
   * add additional validation or boundary checks.
237
   *
238
   * @param workingDirectory the starting directory for the search.
239
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
240
   */
241
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
242

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

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

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

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

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

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

293
  private Path findIdeRoot(Path ideHomePath) {
294

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

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

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

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

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

383
  private String getMessageIdeHomeFound() {
384

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

392
  private String getMessageNotInsideIdeProject() {
393

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

397
  private String getMessageIdeRootNotFound() {
398

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

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

413
    return false;
×
414
  }
415

416
  protected SystemPath computeSystemPath() {
417

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

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

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

437
  private EnvironmentVariables createVariables() {
438

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

447
  protected AbstractEnvironmentVariables createSystemVariables() {
448

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

452
  @Override
453
  public SystemInfo getSystemInfo() {
454

455
    return this.systemInfo;
3✔
456
  }
457

458
  @Override
459
  public FileAccess getFileAccess() {
460

461
    return this.fileAccess;
3✔
462
  }
463

464
  @Override
465
  public CommandletManager getCommandletManager() {
466

467
    return this.commandletManager;
3✔
468
  }
469

470
  @Override
471
  public ToolRepository getDefaultToolRepository() {
472

473
    return this.defaultToolRepository;
3✔
474
  }
475

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

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

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

500
  @Override
501
  public CustomToolRepository getCustomToolRepository() {
502

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

509
  @Override
510
  public Path getIdeHome() {
511

512
    return this.ideHome;
3✔
513
  }
514

515
  @Override
516
  public String getProjectName() {
517

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

524
  @Override
525
  public VersionIdentifier getProjectVersion() {
526

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

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

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

548
  @Override
549
  public Path getIdeRoot() {
550

551
    return this.ideRoot;
3✔
552
  }
553

554
  @Override
555
  public Path getIdePath() {
556

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

564
  @Override
565
  public Path getCwd() {
566

567
    return this.cwd;
3✔
568
  }
569

570
  @Override
571
  public Path getTempPath() {
572

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

580
  @Override
581
  public Path getTempDownloadPath() {
582

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

590
  @Override
591
  public Path getUserHome() {
592

593
    return this.userHome;
3✔
594
  }
595

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

603
    this.userHome = userHome;
3✔
604
    this.userHomeIde = userHome.resolve(FOLDER_DOT_IDE);
5✔
605
    this.downloadPath = userHome.resolve("Downloads/ide");
5✔
606
    this.variables = null;
3✔
607
    resetPrivacyMap();
2✔
608
  }
1✔
609

610
  @Override
611
  public Path getUserHomeIde() {
612

613
    return this.userHomeIde;
3✔
614
  }
615

616
  @Override
617
  public Path getSettingsPath() {
618

619
    return this.settingsPath;
3✔
620
  }
621

622
  @Override
623
  public Path getSettingsGitRepository() {
624

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

634
  @Override
635
  public boolean isSettingsRepositorySymlinkOrJunction() {
636

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

644
  @Override
645
  public Path getSettingsCommitIdPath() {
646

647
    return this.settingsCommitIdPath;
3✔
648
  }
649

650
  @Override
651
  public Path getConfPath() {
652

653
    return this.confPath;
3✔
654
  }
655

656
  @Override
657
  public Path getSoftwarePath() {
658

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

665
  @Override
666
  public Path getSoftwareExtraPath() {
667

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

675
  @Override
676
  public Path getSoftwareRepositoryPath() {
677

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

685
  @Override
686
  public Path getPluginsPath() {
687

688
    return this.pluginsPath;
3✔
689
  }
690

691
  @Override
692
  public String getWorkspaceName() {
693

694
    return this.workspaceName;
3✔
695
  }
696

697
  @Override
698
  public Path getWorkspacesBasePath() {
699

700
    return this.workspacesBasePath;
3✔
701
  }
702

703
  @Override
704
  public Path getWorkspacePath() {
705

706
    return this.workspacePath;
3✔
707
  }
708

709
  @Override
710
  public Path getWorkspacePath(String workspace) {
711

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

718
  @Override
719
  public Path getDownloadPath() {
720

721
    return this.downloadPath;
3✔
722
  }
723

724
  @Override
725
  public Path getUrlsPath() {
726

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

734
  @Override
735
  public Path getToolRepositoryPath() {
736

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

744
  @Override
745
  public SystemPath getPath() {
746

747
    return this.path;
3✔
748
  }
749

750
  @Override
751
  public EnvironmentVariables getVariables() {
752

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

759
  @Override
760
  public UrlMetadata getUrls() {
761

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

771
  @Override
772
  public boolean isQuietMode() {
773

774
    return this.startContext.isQuietMode();
4✔
775
  }
776

777
  @Override
778
  public boolean isBatchMode() {
779

780
    return this.startContext.isBatchMode();
4✔
781
  }
782

783
  @Override
784
  public boolean isForceMode() {
785

786
    return this.startContext.isForceMode();
4✔
787
  }
788

789
  @Override
790
  public boolean isForcePull() {
791

792
    return this.startContext.isForcePull();
4✔
793
  }
794

795
  @Override
796
  public boolean isForcePlugins() {
797

798
    return this.startContext.isForcePlugins();
4✔
799
  }
800

801
  @Override
802
  public boolean isForceRepositories() {
803

804
    return this.startContext.isForceRepositories();
4✔
805
  }
806

807
  @Override
808
  public boolean isOfflineMode() {
809

810
    return this.startContext.isOfflineMode();
4✔
811
  }
812

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

818
  @Override
819
  public boolean isSkipUpdatesMode() {
820

821
    return this.startContext.isSkipUpdatesMode();
4✔
822
  }
823

824
  @Override
825
  public boolean isNoColorsMode() {
826

827
    return this.startContext.isNoColorsMode();
×
828
  }
829

830
  @Override
831
  public NetworkStatus getNetworkStatus() {
832

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

839
  @Override
840
  public Locale getLocale() {
841

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

849
  @Override
850
  public DirectoryMerger getWorkspaceMerger() {
851

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

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

864
    return this.defaultExecutionDirectory;
×
865
  }
866

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

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

877
  @Override
878
  public GitContext getGitContext() {
879

880
    return new GitContextImpl(this);
×
881
  }
882

883
  @Override
884
  public ProcessContext newProcess() {
885

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

893
  @Override
894
  public IdeSystem getSystem() {
895

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

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

908
    return new ProcessContextImpl(this);
×
909
  }
910

911
  @Override
912
  public IdeLogLevel getLogLevelConsole() {
913

914
    return this.startContext.getLogLevelConsole();
×
915
  }
916

917
  @Override
918
  public IdeLogLevel getLogLevelLogger() {
919

920
    return this.startContext.getLogLevelLogger();
×
921
  }
922

923
  @Override
924
  public IdeLogListener getLogListener() {
925

926
    return this.startContext.getLogListener();
×
927
  }
928

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

941
  @Override
942
  public String formatArgument(Object argument) {
943

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

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

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

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

986
    this.privacyMap.clear();
3✔
987
  }
1✔
988

989

990
  @Override
991
  public String askForInput(String message, String defaultValue) {
992

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

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

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

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

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

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

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

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

1090
  @Override
1091
  public Step getCurrentStep() {
1092

1093
    return this.currentStep;
×
1094
  }
1095

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1327
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1328

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

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

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

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

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

1437
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1438

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

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

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

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

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

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

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

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

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

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

1684
  @Override
1685
  public WindowsPathSyntax getPathSyntax() {
1686

1687
    return this.pathSyntax;
3✔
1688
  }
1689

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

1695
    this.pathSyntax = pathSyntax;
3✔
1696
  }
1✔
1697

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

1703
    return startContext;
3✔
1704
  }
1705

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

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

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

1722
    return new WindowsHelperImpl(this);
×
1723
  }
1724

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

1730
    this.variables = null;
3✔
1731
    this.customToolRepository = null;
3✔
1732
  }
1✔
1733

1734
  @Override
1735
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1736

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

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

1748
  }
1749

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

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