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

devonfw / IDEasy / 26101908636

19 May 2026 01:55PM UTC coverage: 70.982% (+0.003%) from 70.979%
26101908636

Pull #1859

github

web-flow
Merge cf4a7f717 into b4eeee25f
Pull Request #1859: #1392: Smart completions

4472 of 6964 branches covered (64.22%)

Branch coverage included in aggregate %.

11521 of 15567 relevant lines covered (74.01%)

3.14 hits per line

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

63.77
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;
×
387
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
×
388
      wks = "*".repeat(wks.length());
×
389
    }
390
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
×
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");
×
401
    if (root == null) {
×
402
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
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
  @Override
413
  public boolean isTest() {
414

415
    return false;
×
416
  }
417

418
  protected SystemPath computeSystemPath() {
419

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

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

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

439
  private EnvironmentVariables createVariables() {
440

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

449
  protected AbstractEnvironmentVariables createSystemVariables() {
450

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

454
  @Override
455
  public SystemInfo getSystemInfo() {
456

457
    return this.systemInfo;
3✔
458
  }
459

460
  @Override
461
  public FileAccess getFileAccess() {
462

463
    return this.fileAccess;
3✔
464
  }
465

466
  @Override
467
  public CommandletManager getCommandletManager() {
468

469
    return this.commandletManager;
3✔
470
  }
471

472
  @Override
473
  public ToolRepository getDefaultToolRepository() {
474

475
    return this.defaultToolRepository;
3✔
476
  }
477

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

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

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

502
  @Override
503
  public CustomToolRepository getCustomToolRepository() {
504

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

511
  @Override
512
  public Path getIdeHome() {
513

514
    return this.ideHome;
3✔
515
  }
516

517
  @Override
518
  public String getProjectName() {
519

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

526
  @Override
527
  public VersionIdentifier getProjectVersion() {
528

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

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

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

550
  @Override
551
  public Path getIdeRoot() {
552

553
    return this.ideRoot;
3✔
554
  }
555

556
  @Override
557
  public Path getIdePath() {
558

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

566
  @Override
567
  public Path getCwd() {
568

569
    return this.cwd;
3✔
570
  }
571

572
  @Override
573
  public Path getTempPath() {
574

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

582
  @Override
583
  public Path getTempDownloadPath() {
584

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

592
  @Override
593
  public Path getUserHome() {
594

595
    return this.userHome;
3✔
596
  }
597

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

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

612
  @Override
613
  public Path getUserHomeIde() {
614

615
    return this.userHomeIde;
3✔
616
  }
617

618
  @Override
619
  public Path getSettingsPath() {
620

621
    return this.settingsPath;
3✔
622
  }
623

624
  @Override
625
  public Path getSettingsGitRepository() {
626

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

636
  @Override
637
  public boolean isSettingsRepositorySymlinkOrJunction() {
638

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

646
  @Override
647
  public Path getSettingsCommitIdPath() {
648

649
    return this.settingsCommitIdPath;
3✔
650
  }
651

652
  @Override
653
  public Path getConfPath() {
654

655
    return this.confPath;
3✔
656
  }
657

658
  @Override
659
  public Path getSoftwarePath() {
660

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

667
  @Override
668
  public Path getSoftwareExtraPath() {
669

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

677
  @Override
678
  public Path getSoftwareRepositoryPath() {
679

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

687
  @Override
688
  public Path getPluginsPath() {
689

690
    return this.pluginsPath;
3✔
691
  }
692

693
  @Override
694
  public String getWorkspaceName() {
695

696
    return this.workspaceName;
3✔
697
  }
698

699
  @Override
700
  public Path getWorkspacesBasePath() {
701

702
    return this.workspacesBasePath;
3✔
703
  }
704

705
  @Override
706
  public Path getWorkspacePath() {
707

708
    return this.workspacePath;
3✔
709
  }
710

711
  @Override
712
  public Path getWorkspacePath(String workspace) {
713

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

720
  @Override
721
  public Path getDownloadPath() {
722

723
    return this.downloadPath;
3✔
724
  }
725

726
  @Override
727
  public Path getUrlsPath() {
728

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

736
  @Override
737
  public Path getToolRepositoryPath() {
738

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

746
  @Override
747
  public SystemPath getPath() {
748

749
    return this.path;
3✔
750
  }
751

752
  @Override
753
  public EnvironmentVariables getVariables() {
754

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

761
  @Override
762
  public UrlMetadata getUrls() {
763

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

773
  @Override
774
  public boolean isQuietMode() {
775

776
    return this.startContext.isQuietMode();
4✔
777
  }
778

779
  @Override
780
  public boolean isBatchMode() {
781

782
    return this.startContext.isBatchMode();
4✔
783
  }
784

785
  @Override
786
  public boolean isForceMode() {
787

788
    return this.startContext.isForceMode();
4✔
789
  }
790

791
  @Override
792
  public boolean isForcePull() {
793

794
    return this.startContext.isForcePull();
4✔
795
  }
796

797
  @Override
798
  public boolean isForcePlugins() {
799

800
    return this.startContext.isForcePlugins();
4✔
801
  }
802

803
  @Override
804
  public boolean isForceRepositories() {
805

806
    return this.startContext.isForceRepositories();
4✔
807
  }
808

809
  @Override
810
  public boolean isOfflineMode() {
811

812
    return this.startContext.isOfflineMode();
4✔
813
  }
814

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

820
  @Override
821
  public boolean isSkipUpdatesMode() {
822

823
    return this.startContext.isSkipUpdatesMode();
4✔
824
  }
825

826
  @Override
827
  public boolean isNoColorsMode() {
828

829
    return this.startContext.isNoColorsMode();
×
830
  }
831

832
  @Override
833
  public NetworkStatus getNetworkStatus() {
834

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

841
  @Override
842
  public Locale getLocale() {
843

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

851
  @Override
852
  public DirectoryMerger getWorkspaceMerger() {
853

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

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

866
    return this.defaultExecutionDirectory;
×
867
  }
868

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

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

879
  @Override
880
  public GitContext getGitContext() {
881

882
    return new GitContextImpl(this);
×
883
  }
884

885
  @Override
886
  public ProcessContext newProcess() {
887

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

895
  @Override
896
  public IdeSystem getSystem() {
897

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

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

910
    return new ProcessContextImpl(this);
×
911
  }
912

913
  @Override
914
  public IdeLogLevel getLogLevelConsole() {
915

916
    return this.startContext.getLogLevelConsole();
×
917
  }
918

919
  @Override
920
  public IdeLogLevel getLogLevelLogger() {
921

922
    return this.startContext.getLogLevelLogger();
×
923
  }
924

925
  @Override
926
  public IdeLogListener getLogListener() {
927

928
    return this.startContext.getLogListener();
×
929
  }
930

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

943
  @Override
944
  public String formatArgument(Object argument) {
945

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

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

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

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

988
    this.privacyMap.clear();
3✔
989
  }
1✔
990

991

992
  @Override
993
  public String askForInput(String message, String defaultValue) {
994

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

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

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

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

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

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

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

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

1092
  @Override
1093
  public Step getCurrentStep() {
1094

1095
    return this.currentStep;
×
1096
  }
1097

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

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

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

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

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

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

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

1175
    configureJavaUtilLogging(cmd);
3✔
1176
    this.startContext.activateLogging();
3✔
1177
  }
1✔
1178

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

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

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

1213
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1214

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

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

1260
  @Override
1261
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1262

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

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

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

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

1328

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

1351
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1352

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

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

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

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

1435
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1436
    if (arguments.current().isStart()) {
4✔
1437
      arguments.next();
3✔
1438
    }
1439
    if (includeContextOptions) {
2✔
1440
      ContextCommandlet cc = new ContextCommandlet();
4✔
1441
      for (Property<?> property : cc.getProperties()) {
11✔
1442
        assert (property.isOption());
4!
1443
        property.apply(arguments, this, cc, collector);
7✔
1444
      }
1✔
1445
    }
1446

1447
    this.commandletManager.collectCompletionCandidates(arguments, collector);
5✔
1448

1449
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1450

1451
    while (commandletIterator.hasNext()) {
3✔
1452
      Commandlet cmd = commandletIterator.next();
4✔
1453
      if (!arguments.current().isEnd()) {
4!
1454
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1455
      }
1456
    }
1✔
1457
    return collector.getSortedCandidates();
3✔
1458
  }
1459

1460
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1461

1462
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1463
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1464
    Property<?> lastValueProperty = null;
2✔
1465
    List<Property<?>> properties = cmd.getProperties();
3✔
1466
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1467
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1468
    for (Property<?> property : properties) {
10✔
1469
      if (property.isOption()) {
3✔
1470
        optionProperties.add(property);
4✔
1471
      }
1472
    }
1✔
1473
    CliArgument currentArgument = arguments.current();
3✔
1474
    while (!currentArgument.isEnd()) {
3✔
1475
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1476
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1477
        if (currentArgument.isCompletion()) {
3✔
1478
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1479
          while (optionIterator.hasNext()) {
3✔
1480
            Property<?> option = optionIterator.next();
4✔
1481
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1482
            if (success) {
2✔
1483
              optionIterator.remove();
2✔
1484
            }
1485
          }
1✔
1486
        } else {
1✔
1487
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1488
          if (option != null) {
2✔
1489
            optionProperties.remove(option);
4✔
1490
          }
1491
        }
1✔
1492
      } else {
1493
        if (currentArgument.isCompletion() && currentArgument.get().isEmpty()
8✔
1494
            && !arguments.isEndOptions()) {
2✔
1495
          for (Property<?> option : optionProperties) {
10✔
1496
            option.apply(arguments, this, cmd, collector);
7✔
1497
          }
1✔
1498
        }
1499

1500
        Property<?> valueProperty = null;
2✔
1501
        if (valueIterator.hasNext()) {
3✔
1502
          lastValueProperty = valueIterator.next();
4✔
1503
          valueProperty = lastValueProperty;
3✔
1504
        } else if (lastValueProperty != null && lastValueProperty.isMultiValued()) {
5!
1505
          valueProperty = lastValueProperty;
2✔
1506
        }
1507

1508
        if (valueProperty != null) {
2✔
1509
          valueProperty.apply(arguments, this, cmd, collector);
7✔
1510
        }
1511
      }
1512

1513
      arguments.next();
3✔
1514
      currentArgument = arguments.current();
4✔
1515
    }
1516
  }
1✔
1517

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

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

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

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

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

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

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

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

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

1708
  @Override
1709
  public WindowsPathSyntax getPathSyntax() {
1710

1711
    return this.pathSyntax;
3✔
1712
  }
1713

1714
  /**
1715
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1716
   */
1717
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1718

1719
    this.pathSyntax = pathSyntax;
3✔
1720
  }
1✔
1721

1722
  /**
1723
   * @return the {@link IdeStartContextImpl}.
1724
   */
1725
  public IdeStartContextImpl getStartContext() {
1726

1727
    return startContext;
3✔
1728
  }
1729

1730
  /**
1731
   * @return the {@link WindowsHelper}.
1732
   */
1733
  public final WindowsHelper getWindowsHelper() {
1734

1735
    if (this.windowsHelper == null) {
3✔
1736
      this.windowsHelper = createWindowsHelper();
4✔
1737
    }
1738
    return this.windowsHelper;
3✔
1739
  }
1740

1741
  /**
1742
   * @return the new {@link WindowsHelper} instance.
1743
   */
1744
  protected WindowsHelper createWindowsHelper() {
1745

1746
    return new WindowsHelperImpl(this);
×
1747
  }
1748

1749
  /**
1750
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1751
   */
1752
  public void reload() {
1753

1754
    this.variables = null;
3✔
1755
    this.customToolRepository = null;
3✔
1756
  }
1✔
1757

1758
  @Override
1759
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1760

1761
    assert (Files.isDirectory(installationPath));
6!
1762
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1763
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1764
  }
1✔
1765

1766
  /*
1767
   * @param home the IDE_HOME directory.
1768
   * @param workspace the name of the active workspace folder.
1769
   */
1770
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1771

1772
  }
1773

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

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