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

devonfw / IDEasy / 26475099762

26 May 2026 09:04PM UTC coverage: 71.132% (+0.02%) from 71.115%
26475099762

push

github

web-flow
#1788: Created a commandlet to simulate the behaviour of ln -s (#1847)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

4471 of 6952 branches covered (64.31%)

Branch coverage included in aggregate %.

11577 of 15609 relevant lines covered (74.17%)

3.14 hits per line

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

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

3
import static com.devonfw.tools.ide.variable.IdeVariables.IDE_MIN_VERSION;
4

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

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

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

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

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

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

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

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

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

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

106
  private final IdeStartContextImpl startContext;
107

108
  private Path ideHome;
109

110
  private final Path ideRoot;
111

112
  private Path confPath;
113

114
  protected Path settingsPath;
115

116
  private Path settingsCommitIdPath;
117

118
  protected Path pluginsPath;
119

120
  private Path workspacePath;
121

122
  private Path workspacesBasePath;
123

124
  private String workspaceName;
125

126
  private Path cwd;
127

128
  private Path downloadPath;
129

130
  private Path userHome;
131

132
  private Path userHomeIde;
133

134
  private SystemPath path;
135

136
  private WindowsPathSyntax pathSyntax;
137

138
  private final SystemInfo systemInfo;
139

140
  private EnvironmentVariables variables;
141

142
  private final FileAccess fileAccess;
143

144
  protected CommandletManager commandletManager;
145

146
  protected ToolRepository defaultToolRepository;
147

148
  private CustomToolRepository customToolRepository;
149

150
  private MvnRepository mvnRepository;
151

152
  private NpmRepository npmRepository;
153

154
  private PipRepository pipRepository;
155

156
  private DirectoryMerger workspaceMerger;
157

158
  protected UrlMetadata urlMetadata;
159

160
  protected Path defaultExecutionDirectory;
161

162
  private StepImpl currentStep;
163

164
  private NetworkStatus networkStatus;
165

166
  protected IdeSystem system;
167

168
  private WindowsHelper windowsHelper;
169

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

172
  private Path bash;
173

174
  private boolean julConfigured;
175

176
  private Path logfile;
177

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

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

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

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

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

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

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

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

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

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

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

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

294
  private Path findIdeRoot(Path ideHomePath) {
295

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

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

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

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

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

384
  private String getMessageIdeHomeFound() {
385

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

393
  private String getMessageNotInsideIdeProject() {
394

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

398
  private String getMessageIdeRootNotFound() {
399

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

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

414
    return false;
×
415
  }
416

417
  protected SystemPath computeSystemPath() {
418

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

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

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

438
  private EnvironmentVariables createVariables() {
439

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

448
  protected AbstractEnvironmentVariables createSystemVariables() {
449

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

453
  @Override
454
  public SystemInfo getSystemInfo() {
455

456
    return this.systemInfo;
3✔
457
  }
458

459
  @Override
460
  public FileAccess getFileAccess() {
461

462
    return this.fileAccess;
3✔
463
  }
464

465
  @Override
466
  public CommandletManager getCommandletManager() {
467

468
    return this.commandletManager;
3✔
469
  }
470

471
  @Override
472
  public ToolRepository getDefaultToolRepository() {
473

474
    return this.defaultToolRepository;
3✔
475
  }
476

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

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

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

501
  @Override
502
  public CustomToolRepository getCustomToolRepository() {
503

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

510
  @Override
511
  public Path getIdeHome() {
512

513
    return this.ideHome;
3✔
514
  }
515

516
  @Override
517
  public String getProjectName() {
518

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

525
  @Override
526
  public VersionIdentifier getProjectVersion() {
527

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

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

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

549
  @Override
550
  public Path getIdeRoot() {
551

552
    return this.ideRoot;
3✔
553
  }
554

555
  @Override
556
  public Path getIdePath() {
557

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

565
  @Override
566
  public Path getCwd() {
567

568
    return this.cwd;
3✔
569
  }
570

571
  @Override
572
  public Path getTempPath() {
573

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

581
  @Override
582
  public Path getTempDownloadPath() {
583

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

591
  @Override
592
  public Path getUserHome() {
593

594
    return this.userHome;
3✔
595
  }
596

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

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

611
  @Override
612
  public Path getUserHomeIde() {
613

614
    return this.userHomeIde;
3✔
615
  }
616

617
  @Override
618
  public Path getSettingsPath() {
619

620
    return this.settingsPath;
3✔
621
  }
622

623
  @Override
624
  public Path getSettingsGitRepository() {
625

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

635
  @Override
636
  public boolean isSettingsCodeRepository() {
637

638
    Path settingsPath = getSettingsPath();
3✔
639
    if (settingsPath != null) {
2!
640
      boolean settingsIsLink = Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
641
      if (settingsIsLink) {
2!
642
        Path realPath = getFileAccess().toRealPath(this.settingsPath);
×
643
        if (realPath != null) {
×
644
          return getGitContext().isGitRepo(realPath.getParent());
×
645
        }
646
        return true;
×
647
      }
648
    }
649
    return false;
2✔
650
  }
651

652
  @Override
653
  public Path getSettingsCommitIdPath() {
654

655
    return this.settingsCommitIdPath;
3✔
656
  }
657

658
  @Override
659
  public Path getConfPath() {
660

661
    return this.confPath;
3✔
662
  }
663

664
  @Override
665
  public Path getSoftwarePath() {
666

667
    if (this.ideHome == null) {
3✔
668
      return null;
2✔
669
    }
670
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
671
  }
672

673
  @Override
674
  public Path getSoftwareExtraPath() {
675

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

683
  @Override
684
  public Path getSoftwareRepositoryPath() {
685

686
    Path idePath = getIdePath();
3✔
687
    if (idePath == null) {
2!
688
      return null;
×
689
    }
690
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
691
  }
692

693
  @Override
694
  public Path getPluginsPath() {
695

696
    return this.pluginsPath;
3✔
697
  }
698

699
  @Override
700
  public String getWorkspaceName() {
701

702
    return this.workspaceName;
3✔
703
  }
704

705
  @Override
706
  public Path getWorkspacesBasePath() {
707

708
    return this.workspacesBasePath;
3✔
709
  }
710

711
  @Override
712
  public Path getWorkspacePath() {
713

714
    return this.workspacePath;
3✔
715
  }
716

717
  @Override
718
  public Path getWorkspacePath(String workspace) {
719

720
    if (this.workspacesBasePath == null) {
3!
721
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
722
    }
723
    return this.workspacesBasePath.resolve(workspace);
5✔
724
  }
725

726
  @Override
727
  public Path getDownloadPath() {
728

729
    return this.downloadPath;
3✔
730
  }
731

732
  @Override
733
  public Path getUrlsPath() {
734

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

742
  @Override
743
  public Path getToolRepositoryPath() {
744

745
    Path idePath = getIdePath();
3✔
746
    if (idePath == null) {
2!
747
      return null;
×
748
    }
749
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
750
  }
751

752
  @Override
753
  public SystemPath getPath() {
754

755
    return this.path;
3✔
756
  }
757

758
  @Override
759
  public EnvironmentVariables getVariables() {
760

761
    if (this.variables == null) {
3✔
762
      this.variables = createVariables();
4✔
763
    }
764
    return this.variables;
3✔
765
  }
766

767
  @Override
768
  public UrlMetadata getUrls() {
769

770
    if (this.urlMetadata == null) {
3✔
771
      if (!isTest()) {
3!
772
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
773
      }
774
      this.urlMetadata = new UrlMetadata(this);
6✔
775
    }
776
    return this.urlMetadata;
3✔
777
  }
778

779
  @Override
780
  public boolean isQuietMode() {
781

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

785
  @Override
786
  public boolean isBatchMode() {
787

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

791
  @Override
792
  public boolean isForceMode() {
793

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

797
  @Override
798
  public boolean isForcePull() {
799

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

803
  @Override
804
  public boolean isForcePlugins() {
805

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

809
  @Override
810
  public boolean isForceRepositories() {
811

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

815
  @Override
816
  public boolean isOfflineMode() {
817

818
    return this.startContext.isOfflineMode();
4✔
819
  }
820

821
  @Override
822
  public boolean isPrivacyMode() {
823
    return this.startContext.isPrivacyMode();
4✔
824
  }
825

826
  @Override
827
  public boolean isSkipUpdatesMode() {
828

829
    return this.startContext.isSkipUpdatesMode();
4✔
830
  }
831

832
  @Override
833
  public boolean isNoColorsMode() {
834

835
    return this.startContext.isNoColorsMode();
×
836
  }
837

838
  @Override
839
  public NetworkStatus getNetworkStatus() {
840

841
    if (this.networkStatus == null) {
×
842
      this.networkStatus = new NetworkStatusImpl(this);
×
843
    }
844
    return this.networkStatus;
×
845
  }
846

847
  @Override
848
  public Locale getLocale() {
849

850
    Locale locale = this.startContext.getLocale();
4✔
851
    if (locale == null) {
2✔
852
      locale = Locale.getDefault();
2✔
853
    }
854
    return locale;
2✔
855
  }
856

857
  @Override
858
  public DirectoryMerger getWorkspaceMerger() {
859

860
    if (this.workspaceMerger == null) {
3✔
861
      this.workspaceMerger = new DirectoryMerger(this);
6✔
862
    }
863
    return this.workspaceMerger;
3✔
864
  }
865

866
  /**
867
   * @return the default execution directory in which a command process is executed.
868
   */
869
  @Override
870
  public Path getDefaultExecutionDirectory() {
871

872
    return this.defaultExecutionDirectory;
×
873
  }
874

875
  /**
876
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
877
   */
878
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
879

880
    if (defaultExecutionDirectory != null) {
×
881
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
882
    }
883
  }
×
884

885
  @Override
886
  public GitContext getGitContext() {
887

888
    return new GitContextImpl(this);
×
889
  }
890

891
  @Override
892
  public ProcessContext newProcess() {
893

894
    ProcessContext processContext = createProcessContext();
3✔
895
    if (this.defaultExecutionDirectory != null) {
3!
896
      processContext.directory(this.defaultExecutionDirectory);
×
897
    }
898
    return processContext;
2✔
899
  }
900

901
  @Override
902
  public IdeSystem getSystem() {
903

904
    if (this.system == null) {
×
905
      this.system = new IdeSystemImpl();
×
906
    }
907
    return this.system;
×
908
  }
909

910
  /**
911
   * @return a new instance of {@link ProcessContext}.
912
   * @see #newProcess()
913
   */
914
  protected ProcessContext createProcessContext() {
915

916
    return new ProcessContextImpl(this);
×
917
  }
918

919
  @Override
920
  public IdeLogLevel getLogLevelConsole() {
921

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

925
  @Override
926
  public IdeLogLevel getLogLevelLogger() {
927

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

931
  @Override
932
  public IdeLogListener getLogListener() {
933

934
    return this.startContext.getLogListener();
×
935
  }
936

937
  @Override
938
  public void logIdeHomeAndRootStatus() {
939
    if (this.ideRoot != null) {
3!
940
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
×
941
    }
942
    if (this.ideHome == null) {
3✔
943
      LOG.warn(getMessageNotInsideIdeProject());
5✔
944
    } else {
945
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
946
    }
947
  }
1✔
948

949
  @Override
950
  public String formatArgument(Object argument) {
951

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

972
  /**
973
   * @param path the sensitive {@link Path} to
974
   * @param replacement the replacement to mask the {@link Path} in log output.
975
   */
976
  protected void initializePrivacyMap(Path path, String replacement) {
977

978
    if (path == null) {
×
979
      return;
×
980
    }
981
    if (this.systemInfo.isWindows()) {
×
982
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
983
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
984
    } else {
985
      this.privacyMap.put(path.toString(), replacement);
×
986
    }
987
  }
×
988

989
  /**
990
   * Resets the privacy map in case fundamental values have changed.
991
   */
992
  private void resetPrivacyMap() {
993

994
    this.privacyMap.clear();
3✔
995
  }
1✔
996

997

998
  @Override
999
  public String askForInput(String message, String defaultValue) {
1000

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

1023
  @Override
1024
  public <O> O question(O[] options, String question, Object... args) {
1025

1026
    assert (options.length > 0);
4!
1027
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1028
    return displayOptionsAndGetAnswer(options);
4✔
1029
  }
1030

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

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

1085
  /**
1086
   * @return the input from the end-user (e.g. read from the console).
1087
   */
1088
  protected abstract String readLine();
1089

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

1092
    O duplicate = mapping.put(key, option);
5✔
1093
    if (duplicate != null) {
2!
1094
      throw new IllegalArgumentException("Duplicated option " + key);
×
1095
    }
1096
  }
1✔
1097

1098
  @Override
1099
  public Step getCurrentStep() {
1100

1101
    return this.currentStep;
×
1102
  }
1103

1104
  @Override
1105
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1106

1107
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1108
    return this.currentStep;
3✔
1109
  }
1110

1111
  /**
1112
   * Internal method to end the running {@link Step}.
1113
   *
1114
   * @param step the current {@link Step} to end.
1115
   */
1116
  public void endStep(StepImpl step) {
1117

1118
    if (step == this.currentStep) {
4!
1119
      this.currentStep = this.currentStep.getParent();
6✔
1120
    } else {
1121
      String currentStepName = "null";
×
1122
      if (this.currentStep != null) {
×
1123
        currentStepName = this.currentStep.getName();
×
1124
      }
1125
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1126
    }
1127
  }
1✔
1128

1129
  /**
1130
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1131
   *
1132
   * @param arguments the {@link CliArgument}.
1133
   * @return the return code of the execution.
1134
   */
1135
  public int run(CliArguments arguments) {
1136

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

1176
  /**
1177
   * Ensure the logging system is initialized.
1178
   */
1179
  private void activateLogging(Commandlet cmd) {
1180

1181
    configureJavaUtilLogging(cmd);
3✔
1182
    this.startContext.activateLogging();
3✔
1183
  }
1✔
1184

1185
  /**
1186
   * Configures the logging system (JUL).
1187
   *
1188
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1189
   */
1190
  public void configureJavaUtilLogging(Commandlet cmd) {
1191

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

1211
  protected boolean isWriteLogfile(Commandlet cmd) {
1212
    if ((cmd == null) || !cmd.isWriteLogFile()) {
×
1213
      return false;
×
1214
    }
1215
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1216
    return Boolean.TRUE.equals(writeLogfile);
×
1217
  }
1218

1219
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1220

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

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

1266
  @Override
1267
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1268

1269
    this.startContext.deactivateLogging(threshold);
4✔
1270
    lambda.run();
2✔
1271
    this.startContext.activateLogging();
3✔
1272
  }
1✔
1273

1274
  /**
1275
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1276
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1277
   *     {@link Commandlet} did not match and we have to try a different candidate).
1278
   */
1279
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1280

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

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

1334

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

1358
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1359

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

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

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

1435
  /**
1436
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1437
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1438
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1439
   */
1440
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1441

1442
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1443
    if (arguments.current().isStart()) {
4✔
1444
      arguments.next();
3✔
1445
    }
1446
    if (includeContextOptions) {
2✔
1447
      ContextCommandlet cc = new ContextCommandlet();
4✔
1448
      for (Property<?> property : cc.getProperties()) {
11✔
1449
        assert (property.isOption());
4!
1450
        property.apply(arguments, this, cc, collector);
7✔
1451
      }
1✔
1452
    }
1453
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1454
    CliArgument current = arguments.current();
3✔
1455
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1456
      collector.add(current.get(), null, null, null);
7✔
1457
    }
1458
    arguments.next();
3✔
1459
    while (commandletIterator.hasNext()) {
3✔
1460
      Commandlet cmd = commandletIterator.next();
4✔
1461
      if (!arguments.current().isEnd()) {
4✔
1462
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1463
      }
1464
    }
1✔
1465
    return collector.getSortedCandidates();
3✔
1466
  }
1467

1468
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1469

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

1526
  /**
1527
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1528
   *     {@link CliArguments#copy() copy} as needed.
1529
   * @param cmd the potential {@link Commandlet} to match.
1530
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1531
   */
1532
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1533

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

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

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

1623
  /**
1624
   * @param path the path to check.
1625
   * @param toIgnore the String sequence which needs to be checked and ignored.
1626
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1627
   */
1628
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1629
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1630
    return !s.contains(toIgnore);
7!
1631
  }
1632

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

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

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

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

1715
  @Override
1716
  public WindowsPathSyntax getPathSyntax() {
1717

1718
    return this.pathSyntax;
3✔
1719
  }
1720

1721
  /**
1722
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1723
   */
1724
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1725

1726
    this.pathSyntax = pathSyntax;
3✔
1727
  }
1✔
1728

1729
  /**
1730
   * @return the {@link IdeStartContextImpl}.
1731
   */
1732
  public IdeStartContextImpl getStartContext() {
1733

1734
    return startContext;
3✔
1735
  }
1736

1737
  /**
1738
   * @return the {@link WindowsHelper}.
1739
   */
1740
  public final WindowsHelper getWindowsHelper() {
1741

1742
    if (this.windowsHelper == null) {
3✔
1743
      this.windowsHelper = createWindowsHelper();
4✔
1744
    }
1745
    return this.windowsHelper;
3✔
1746
  }
1747

1748
  /**
1749
   * @return the new {@link WindowsHelper} instance.
1750
   */
1751
  protected WindowsHelper createWindowsHelper() {
1752

1753
    return new WindowsHelperImpl(this);
×
1754
  }
1755

1756
  /**
1757
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1758
   */
1759
  public void reload() {
1760

1761
    this.variables = null;
3✔
1762
    this.customToolRepository = null;
3✔
1763
  }
1✔
1764

1765
  @Override
1766
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1767

1768
    assert (Files.isDirectory(installationPath));
6!
1769
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1770
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1771
  }
1✔
1772

1773
  /*
1774
   * @param home the IDE_HOME directory.
1775
   * @param workspace the name of the active workspace folder.
1776
   */
1777
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1778

1779
  }
1780

1781
  /**
1782
   * Returns the default git path on Windows. Required to be overwritten in tests.
1783
   *
1784
   * @return default path to git on Windows.
1785
   */
1786
  public String getDefaultWindowsGitPath() {
1787
    return DEFAULT_WINDOWS_GIT_PATH;
×
1788
  }
1789

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