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

devonfw / IDEasy / 29848115811

21 Jul 2026 04:20PM UTC coverage: 72.514% (+0.03%) from 72.484%
29848115811

push

github

web-flow
#2100: Resolve python versions via uv instead of ide-urls (#2152)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>
Co-authored-by: Philipp Hoang <115173117+Caylipp@users.noreply.github.com>

4973 of 7584 branches covered (65.57%)

Branch coverage included in aggregate %.

12824 of 16959 relevant lines covered (75.62%)

3.2 hits per line

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

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

93
/**
94
 * Abstract base implementation of {@link IdeContext}.
95
 */
96
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
97

98
  static final Logger LOG = LoggerFactory.getLogger(AbstractIdeContext.class);
3✔
99

100
  /** The default shell bash (Bourne Again SHell). */
101
  public static final String BASH = "bash";
102

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

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

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

109
  private static final String OPTION_DETAILS_START = "([";
110

111
  private final IdeStartContextImpl startContext;
112

113
  private Path ideHome;
114

115
  private Path ideRoot;
116

117
  private Path confPath;
118

119
  protected Path settingsPath;
120

121
  private Path settingsCommitIdPath;
122

123
  protected Path pluginsPath;
124

125
  private Path workspacePath;
126

127
  private Path workspacesBasePath;
128

129
  private String workspaceName;
130

131
  private Path cwd;
132

133
  private Path downloadPath;
134

135
  private Path userHome;
136

137
  private Path userHomeIde;
138

139
  private SystemPath path;
140

141
  private WindowsPathSyntax pathSyntax;
142

143
  private final SystemInfo systemInfo;
144

145
  private EnvironmentVariables variables;
146

147
  private final FileAccess fileAccess;
148

149
  protected CommandletManager commandletManager;
150

151
  protected ToolRepository defaultToolRepository;
152

153
  private CustomToolRepository customToolRepository;
154

155
  private MvnRepository mvnRepository;
156

157
  private NpmRepository npmRepository;
158

159
  private PipRepository pipRepository;
160

161
  private UvRepository uvRepository;
162

163
  private PythonRepository pythonRepository;
164

165
  private DirectoryMerger workspaceMerger;
166

167
  protected UrlMetadata urlMetadata;
168

169
  protected Path defaultExecutionDirectory;
170

171
  private StepImpl currentStep;
172

173
  private NetworkStatus networkStatus;
174

175
  protected IdeSystem system;
176

177
  private WindowsHelper windowsHelper;
178

179
  private final Map<String, String> privacyMap;
180

181
  private Path bash;
182

183
  private boolean julConfigured;
184

185
  private Path logfile;
186

187
  private CliSuggester cliSuggester;
188

189
  /**
190
   * The constructor.
191
   *
192
   * @param startContext the {@link IdeStartContextImpl}.
193
   * @param workingDirectory the optional {@link Path} to current working directory.
194
   */
195
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
196

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

230
    // detection completed, initializing variables
231
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
232

233
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
234

235
    if (this.ideRoot != null) {
3✔
236
      Path tempDownloadPath = getTempDownloadPath();
3✔
237
      if (Files.isDirectory(tempDownloadPath)) {
6✔
238
        // TODO delete all files older than 1 day here...
239
      } else {
240
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
241
      }
242
    }
243
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
244
  }
1✔
245

246
  /**
247
   * 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
248
   * add additional validation or boundary checks.
249
   *
250
   * @param workingDirectory the starting directory for the search.
251
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
252
   */
253
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
254

255
    Path currentDir = workingDirectory;
2✔
256
    String name1 = "";
2✔
257
    String name2 = "";
2✔
258
    String workspace = WORKSPACE_MAIN;
2✔
259
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
260

261
    while (currentDir != null) {
2✔
262
      LOG.trace("Looking for IDE_HOME in {}", currentDir);
4✔
263
      if (isIdeHome(currentDir)) {
4✔
264
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
265
          workspace = name2;
3✔
266
        }
267
        break;
268
      }
269
      name2 = name1;
2✔
270
      int nameCount = currentDir.getNameCount();
3✔
271
      if (nameCount >= 1) {
3✔
272
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
273
      }
274
      currentDir = currentDir.getParent();
3✔
275
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
276
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
277
        currentDir = null;
×
278
      }
279
    }
1✔
280

281
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
282
  }
283

284
  /**
285
   * @return a new {@link MvnRepository}
286
   */
287
  protected MvnRepository createMvnRepository() {
288
    return new MvnRepository(this);
5✔
289
  }
290

291
  /**
292
   * @return a new {@link NpmRepository}
293
   */
294
  protected NpmRepository createNpmRepository() {
295
    return new NpmRepository(this);
×
296
  }
297

298
  /**
299
   * @return a new {@link PipRepository}
300
   */
301
  protected PipRepository createPipRepository() {
302
    return new PipRepository(this);
×
303
  }
304

305
  /**
306
   * @return a new {@link UvRepository}
307
   */
308
  protected UvRepository createUvRepository() {
309
    return new UvRepository(this);
×
310
  }
311

312
  /**
313
   * @return a new {@link PythonRepository}
314
   */
315
  protected PythonRepository createPythonRepository() {
316
    return new PythonRepository(this);
5✔
317
  }
318

319
  private Path findIdeRoot(Path ideHomePath) {
320

321
    Path ideRootPath = null;
2✔
322
    if (ideHomePath != null) {
2✔
323
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
324
      ideRootPath = ideHomePath.getParent();
3✔
325
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
2!
326
        LOG.warn(
×
327
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
328
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
329
            ideRootPathFromEnv,
330
            ideHomePath.getFileName(), ideRootPath);
×
331
      }
332
    } else if (!isTest()) {
4!
333
      ideRootPath = getIdeRootPathFromEnv(true);
×
334
    }
335
    return ideRootPath;
2✔
336
  }
337

338
  /**
339
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
340
   */
341
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
342

343
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
344
    if (root != null) {
×
345
      Path rootPath = Path.of(root);
×
346
      if (Files.isDirectory(rootPath)) {
×
347
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
348
        if (withSanityCheck) {
×
349
          int nameCount = rootPath.getNameCount();
×
350
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
351
          int delta = absoluteNameCount - nameCount;
×
352
          if (delta >= 0) {
×
353
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
354
              String rootName = rootPath.getName(nameIndex).toString();
×
355
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
356
              if (!rootName.equals(absoluteRootName)) {
×
357
                LOG.warn("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
358
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
359
                break;
×
360
              }
361
            }
362
          } else {
363
            LOG.warn("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
364
                absoluteRootPath);
365
          }
366
        }
367
        return absoluteRootPath;
×
368
      } else if (withSanityCheck) {
×
369
        LOG.warn("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
370
      }
371
    }
372
    return null;
×
373
  }
374

375
  @Override
376
  public void setCwd(Path userDir, String workspace, Path ideHome) {
377

378
    this.cwd = userDir;
3✔
379
    this.workspaceName = workspace;
3✔
380
    this.ideHome = ideHome;
3✔
381
    if (ideHome == null) {
2✔
382
      this.workspacesBasePath = null;
3✔
383
      this.workspacePath = null;
3✔
384
      this.confPath = null;
3✔
385
      this.settingsPath = null;
3✔
386
      this.pluginsPath = null;
4✔
387
    } else {
388
      this.workspacesBasePath = this.ideHome.resolve(FOLDER_WORKSPACES);
6✔
389
      this.workspacePath = this.workspacesBasePath.resolve(this.workspaceName);
7✔
390
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
391
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
392
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
393
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
394
    }
395
    if (isTest()) {
3!
396
      // only for testing...
397
      if (this.ideHome == null) {
3✔
398
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
399
      } else {
400
        this.userHome = this.ideHome.resolve("home");
6✔
401
      }
402
    }
403
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
404
    this.downloadPath = computeDownloadPath(this.userHome);
6✔
405
    resetPrivacyMap();
2✔
406
    this.path = computeSystemPath();
4✔
407
  }
1✔
408

409
  /**
410
   * On macOS, {@code ~/Downloads} is protected by the OS (TCC) and the CLI may not be allowed to delete it, so we put the cache under {@code ~/Library/Caches}
411
   * instead. Tests still use {@code ~/Downloads/ide} so existing fixtures keep working.
412
   */
413
  private Path computeDownloadPath(Path home) {
414

415
    if (!isTest() && this.systemInfo.isMac()) {
3!
416
      return home.resolve("Library/Caches/IDEasy/downloads");
×
417
    }
418
    return home.resolve("Downloads/ide");
4✔
419
  }
420

421
  private String getMessageIdeHomeFound() {
422

423
    String wks = this.workspaceName;
3✔
424
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
425
      wks = "*".repeat(wks.length());
×
426
    }
427
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
428
  }
429

430
  private String getMessageNotInsideIdeProject() {
431

432
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
433
  }
434

435
  private String getMessageIdeRootNotFound() {
436

437
    String root = getSystem().getEnv("IDE_ROOT");
5✔
438
    if (root == null) {
2!
439
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
440
    } else {
441
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
442
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
443
    }
444
  }
445

446
  /**
447
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
448
   */
449
  public boolean isTest() {
450

451
    return false;
×
452
  }
453

454
  protected SystemPath computeSystemPath() {
455

456
    return new SystemPath(this);
×
457
  }
458

459
  /**
460
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
461
   *
462
   * @param dir the directory to check.
463
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
464
   */
465
  protected boolean isIdeHome(Path dir) {
466

467
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
468
      return false;
2✔
469
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
470
      return false;
×
471
    }
472
    return true;
2✔
473
  }
474

475
  private EnvironmentVariables createVariables() {
476

477
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
478
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
479
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
480
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
481
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
482
    return conf.resolved();
3✔
483
  }
484

485
  protected AbstractEnvironmentVariables createSystemVariables() {
486

487
    return EnvironmentVariables.ofSystem(this);
3✔
488
  }
489

490
  @Override
491
  public SystemInfo getSystemInfo() {
492

493
    return this.systemInfo;
3✔
494
  }
495

496
  @Override
497
  public FileAccess getFileAccess() {
498

499
    return this.fileAccess;
3✔
500
  }
501

502
  @Override
503
  public CommandletManager getCommandletManager() {
504

505
    return this.commandletManager;
3✔
506
  }
507

508
  @Override
509
  public ToolRepository getDefaultToolRepository() {
510

511
    return this.defaultToolRepository;
3✔
512
  }
513

514
  @Override
515
  public MvnRepository getMvnRepository() {
516
    if (this.mvnRepository == null) {
3✔
517
      this.mvnRepository = createMvnRepository();
4✔
518
    }
519
    return this.mvnRepository;
3✔
520
  }
521

522
  @Override
523
  public NpmRepository getNpmRepository() {
524
    if (this.npmRepository == null) {
3✔
525
      this.npmRepository = createNpmRepository();
4✔
526
    }
527
    return this.npmRepository;
3✔
528
  }
529

530
  @Override
531
  public PipRepository getPipRepository() {
532
    if (this.pipRepository == null) {
3✔
533
      this.pipRepository = createPipRepository();
4✔
534
    }
535
    return this.pipRepository;
3✔
536
  }
537

538
  @Override
539
  public UvRepository getUvRepository() {
540
    if (this.uvRepository == null) {
3✔
541
      this.uvRepository = createUvRepository();
4✔
542
    }
543
    return this.uvRepository;
3✔
544
  }
545

546
  @Override
547
  public PythonRepository getPythonRepository() {
548
    if (this.pythonRepository == null) {
3✔
549
      this.pythonRepository = createPythonRepository();
4✔
550
    }
551
    return this.pythonRepository;
3✔
552
  }
553

554
  @Override
555
  public CustomToolRepository getCustomToolRepository() {
556

557
    if (this.customToolRepository == null) {
3✔
558
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
559
    }
560
    return this.customToolRepository;
3✔
561
  }
562

563
  @Override
564
  public Path getIdeHome() {
565

566
    return this.ideHome;
3✔
567
  }
568

569
  @Override
570
  public String getProjectName() {
571

572
    if (this.ideHome != null) {
3!
573
      return this.ideHome.getFileName().toString();
5✔
574
    }
575
    return "";
×
576
  }
577

578
  @Override
579
  public VersionIdentifier getProjectVersion() {
580

581
    if (this.ideHome != null) {
3!
582
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
583
      if (Files.exists(versionFile)) {
5✔
584
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
585
        return VersionIdentifier.of(version);
3✔
586
      }
587
    }
588
    return IdeMigrator.START_VERSION;
2✔
589
  }
590

591
  @Override
592
  public void setProjectVersion(VersionIdentifier version) {
593

594
    if (this.ideHome == null) {
3!
595
      throw new IllegalStateException("IDE_HOME not available!");
×
596
    }
597
    Objects.requireNonNull(version);
3✔
598
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
599
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
600
  }
1✔
601

602
  @Override
603
  public Path getIdeRoot() {
604

605
    return this.ideRoot;
3✔
606
  }
607

608
  @Override
609
  public void setIdeRoot(Path ideRoot) {
610

611
    this.ideRoot = ideRoot;
3✔
612
  }
1✔
613

614
  @Override
615
  public Path getIdePath() {
616

617
    Path myIdeRoot = getIdeRoot();
3✔
618
    if (myIdeRoot == null) {
2✔
619
      return null;
2✔
620
    }
621
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
622
  }
623

624
  @Override
625
  public Path getCwd() {
626

627
    return this.cwd;
3✔
628
  }
629

630
  @Override
631
  public Path getTempPath() {
632

633
    Path idePath = getIdePath();
3✔
634
    if (idePath == null) {
2!
635
      return null;
×
636
    }
637
    return idePath.resolve("tmp");
4✔
638
  }
639

640
  @Override
641
  public Path getTempDownloadPath() {
642

643
    Path tmp = getTempPath();
3✔
644
    if (tmp == null) {
2!
645
      return null;
×
646
    }
647
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
648
  }
649

650
  @Override
651
  public Path getUserHome() {
652

653
    return this.userHome;
3✔
654
  }
655

656
  /**
657
   * This method should only be used for tests to mock user home.
658
   *
659
   * @param userHome the new value of {@link #getUserHome()}.
660
   */
661
  protected void setUserHome(Path userHome) {
662

663
    this.userHome = userHome;
3✔
664
    this.userHomeIde = userHome.resolve(FOLDER_DOT_IDE);
5✔
665
    this.downloadPath = computeDownloadPath(userHome);
5✔
666
    this.variables = null;
3✔
667
    resetPrivacyMap();
2✔
668
  }
1✔
669

670
  @Override
671
  public Path getUserHomeIde() {
672

673
    return this.userHomeIde;
3✔
674
  }
675

676
  @Override
677
  public Path getSettingsPath() {
678

679
    return this.settingsPath;
3✔
680
  }
681

682
  @Override
683
  public Path getSettingsGitRepository() {
684

685
    Path settingsPath = getSettingsPath();
3✔
686
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
687
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsCodeRepository()) {
12!
688
      LOG.error("Settings repository exists but is not a git repository.");
3✔
689
      return null;
2✔
690
    }
691
    return settingsPath;
2✔
692
  }
693

694
  @Override
695
  public boolean isSettingsCodeRepository() {
696

697
    Path settingsPath = getSettingsPath();
3✔
698
    if (settingsPath != null) {
2!
699
      boolean settingsIsLink = Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
700
      if (settingsIsLink) {
2!
701
        Path realPath = getFileAccess().toRealPath(this.settingsPath);
×
702
        if (realPath != null) {
×
703
          return getGitContext().isGitRepo(realPath.getParent());
×
704
        }
705
        return true;
×
706
      }
707
    }
708
    return false;
2✔
709
  }
710

711
  @Override
712
  public Path getSettingsCommitIdPath() {
713

714
    return this.settingsCommitIdPath;
3✔
715
  }
716

717
  @Override
718
  public Path getConfPath() {
719

720
    return this.confPath;
3✔
721
  }
722

723
  @Override
724
  public Path getSoftwarePath() {
725

726
    if (this.ideHome == null) {
3✔
727
      return null;
2✔
728
    }
729
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
730
  }
731

732
  @Override
733
  public Path getSoftwareExtraPath() {
734

735
    Path softwarePath = getSoftwarePath();
3✔
736
    if (softwarePath == null) {
2✔
737
      return null;
2✔
738
    }
739
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
740
  }
741

742
  @Override
743
  public Path getSoftwareRepositoryPath() {
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 Path getPluginsPath() {
754

755
    return this.pluginsPath;
3✔
756
  }
757

758
  @Override
759
  public String getWorkspaceName() {
760

761
    return this.workspaceName;
3✔
762
  }
763

764
  @Override
765
  public Path getWorkspacesBasePath() {
766

767
    return this.workspacesBasePath;
3✔
768
  }
769

770
  @Override
771
  public Path getWorkspacePath() {
772

773
    return this.workspacePath;
3✔
774
  }
775

776
  @Override
777
  public Path getWorkspacePath(String workspace) {
778

779
    if (this.workspacesBasePath == null) {
3!
780
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
781
    }
782
    return this.workspacesBasePath.resolve(workspace);
5✔
783
  }
784

785
  @Override
786
  public Path getDownloadPath() {
787

788
    return this.downloadPath;
3✔
789
  }
790

791
  @Override
792
  public Path getUrlsPath() {
793

794
    Path idePath = getIdePath();
3✔
795
    if (idePath == null) {
2!
796
      return null;
×
797
    }
798
    return idePath.resolve(FOLDER_URLS);
4✔
799
  }
800

801
  @Override
802
  public Path getToolRepositoryPath() {
803

804
    Path idePath = getIdePath();
3✔
805
    if (idePath == null) {
2!
806
      return null;
×
807
    }
808
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
809
  }
810

811
  @Override
812
  public SystemPath getPath() {
813

814
    return this.path;
3✔
815
  }
816

817
  @Override
818
  public EnvironmentVariables getVariables() {
819

820
    if (this.variables == null) {
3✔
821
      this.variables = createVariables();
4✔
822
    }
823
    return this.variables;
3✔
824
  }
825

826
  @Override
827
  public UrlMetadata getUrls() {
828

829
    if (this.urlMetadata == null) {
3✔
830
      if (!isTest()) {
3!
831
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
832
      }
833
      this.urlMetadata = new UrlMetadata(this);
6✔
834
    }
835
    return this.urlMetadata;
3✔
836
  }
837

838
  @Override
839
  public boolean isQuietMode() {
840

841
    return this.startContext.isQuietMode();
4✔
842
  }
843

844
  @Override
845
  public boolean isBatchMode() {
846

847
    return this.startContext.isBatchMode();
4✔
848
  }
849

850
  @Override
851
  public boolean isForceMode() {
852

853
    return this.startContext.isForceMode();
4✔
854
  }
855

856
  @Override
857
  public boolean isForcePull() {
858

859
    return this.startContext.isForcePull();
4✔
860
  }
861

862
  @Override
863
  public boolean isForcePlugins() {
864

865
    return this.startContext.isForcePlugins();
4✔
866
  }
867

868
  @Override
869
  public boolean isForceRepositories() {
870

871
    return this.startContext.isForceRepositories();
4✔
872
  }
873

874
  @Override
875
  public boolean isOfflineMode() {
876

877
    return this.startContext.isOfflineMode();
4✔
878
  }
879

880
  @Override
881
  public boolean isPrivacyMode() {
882
    return this.startContext.isPrivacyMode();
4✔
883
  }
884

885
  @Override
886
  public boolean isSkipUpdatesMode() {
887

888
    return this.startContext.isSkipUpdatesMode();
4✔
889
  }
890

891
  @Override
892
  public boolean isNoColorsMode() {
893

894
    return this.startContext.isNoColorsMode();
×
895
  }
896

897
  @Override
898
  public NetworkStatus getNetworkStatus() {
899

900
    if (this.networkStatus == null) {
×
901
      this.networkStatus = new NetworkStatusImpl(this);
×
902
    }
903
    return this.networkStatus;
×
904
  }
905

906
  @Override
907
  public Locale getLocale() {
908

909
    Locale locale = this.startContext.getLocale();
4✔
910
    if (locale == null) {
2✔
911
      locale = Locale.getDefault();
2✔
912
    }
913
    return locale;
2✔
914
  }
915

916
  @Override
917
  public DirectoryMerger getWorkspaceMerger() {
918

919
    if (this.workspaceMerger == null) {
3✔
920
      this.workspaceMerger = new DirectoryMerger(this);
6✔
921
    }
922
    return this.workspaceMerger;
3✔
923
  }
924

925
  /**
926
   * @return the default execution directory in which a command process is executed.
927
   */
928
  @Override
929
  public Path getDefaultExecutionDirectory() {
930

931
    return this.defaultExecutionDirectory;
×
932
  }
933

934
  /**
935
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
936
   */
937
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
938

939
    if (defaultExecutionDirectory != null) {
×
940
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
941
    }
942
  }
×
943

944
  @Override
945
  public GitContext getGitContext() {
946

947
    return new GitContextImpl(this);
×
948
  }
949

950
  @Override
951
  public ProcessContext newProcess() {
952

953
    ProcessContext processContext = createProcessContext();
3✔
954
    if (this.defaultExecutionDirectory != null) {
3!
955
      processContext.directory(this.defaultExecutionDirectory);
×
956
    }
957
    return processContext;
2✔
958
  }
959

960
  @Override
961
  public IdeSystem getSystem() {
962

963
    if (this.system == null) {
×
964
      this.system = new IdeSystemImpl();
×
965
    }
966
    return this.system;
×
967
  }
968

969
  /**
970
   * @return a new instance of {@link ProcessContext}.
971
   * @see #newProcess()
972
   */
973
  protected ProcessContext createProcessContext() {
974

975
    return new ProcessContextImpl(this);
×
976
  }
977

978
  @Override
979
  public IdeLogLevel getLogLevelConsole() {
980

981
    return this.startContext.getLogLevelConsole();
4✔
982
  }
983

984
  @Override
985
  public IdeLogLevel getLogLevelLogger() {
986

987
    return this.startContext.getLogLevelLogger();
×
988
  }
989

990
  @Override
991
  public IdeLogListener getLogListener() {
992

993
    return this.startContext.getLogListener();
×
994
  }
995

996
  @Override
997
  public void logIdeHomeAndRootStatus() {
998
    if (this.ideRoot != null) {
3✔
999
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
11✔
1000
    }
1001
    if (this.ideHome == null) {
3✔
1002
      LOG.warn(getMessageNotInsideIdeProject());
5✔
1003
    } else {
1004
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
1005
    }
1006
  }
1✔
1007

1008
  @Override
1009
  public String formatArgument(Object argument) {
1010

1011
    if (argument == null) {
2✔
1012
      return null;
2✔
1013
    }
1014
    String result = argument.toString();
3✔
1015
    if (isPrivacyMode()) {
3✔
1016
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
7!
1017
        initializePrivacyMap(this.userHome, "~");
5✔
1018
        String projectName = getProjectName();
3✔
1019
        if (!projectName.isEmpty()) {
3!
1020
          this.privacyMap.put(projectName, "project");
6✔
1021
        }
1022
      }
1023
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
12✔
1024
        result = result.replace(entry.getKey(), entry.getValue());
9✔
1025
      }
1✔
1026
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
1027
    }
1028
    return result;
2✔
1029
  }
1030

1031
  /**
1032
   * @param path the sensitive {@link Path} to
1033
   * @param replacement the replacement to mask the {@link Path} in log output.
1034
   */
1035
  protected void initializePrivacyMap(Path path, String replacement) {
1036

1037
    if (path == null) {
2!
1038
      return;
×
1039
    }
1040
    if (this.systemInfo.isWindows()) {
4!
1041
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
1042
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
1043
    } else {
1044
      this.privacyMap.put(path.toString(), replacement);
7✔
1045
    }
1046
  }
1✔
1047

1048
  /**
1049
   * Resets the privacy map in case fundamental values have changed.
1050
   */
1051
  private void resetPrivacyMap() {
1052

1053
    this.privacyMap.clear();
3✔
1054
  }
1✔
1055

1056

1057
  @Override
1058
  public String askForInput(String message, String defaultValue) {
1059

1060
    while (true) {
1061
      if (!message.isBlank()) {
3!
1062
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
1063
      }
1064
      if (isBatchMode()) {
3!
1065
        if (isForceMode()) {
×
1066
          return defaultValue;
×
1067
        } else {
1068
          throw new CliAbortException();
×
1069
        }
1070
      }
1071
      String input = readLine().trim();
4✔
1072
      if (!input.isEmpty()) {
3!
1073
        return input;
2✔
1074
      } else {
1075
        if (defaultValue != null) {
×
1076
          return defaultValue;
×
1077
        }
1078
      }
1079
    }
×
1080
  }
1081

1082
  @Override
1083
  public <O> O question(O[] options, String question, Object... args) {
1084

1085
    assert (options.length > 0);
4!
1086
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1087
    LOG.warn(question, args);
4✔
1088
    return displayOptionsAndGetAnswer(options);
4✔
1089
  }
1090

1091
  private <O> O displayOptionsAndGetAnswer(O[] options) {
1092
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
1093
    int i = 0;
2✔
1094
    for (O option : options) {
16✔
1095
      i++;
1✔
1096
      String title = "" + option;
4✔
1097
      String key = computeOptionKey(title);
3✔
1098
      addMapping(mapping, key, option);
4✔
1099
      String numericKey = Integer.toString(i);
3✔
1100
      if (numericKey.equals(key)) {
4!
1101
        LOG.trace("Options should not be numeric: {}", key);
×
1102
      } else {
1103
        addMapping(mapping, numericKey, option);
4✔
1104
      }
1105
      IdeLogLevel.INTERACTION.log(LOG, "Option {}: {}", numericKey, title);
14✔
1106
    }
1107
    if (options.length == 1) {
4✔
1108
      mapping.put("", options[0]);
7✔
1109
    }
1110
    O option = null;
2✔
1111
    if (isBatchMode()) {
3!
1112
      if (isForceMode()) {
×
1113
        option = options[0];
×
1114
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
×
1115
      }
1116
    } else {
1117
      while (option == null) {
2✔
1118
        String answer = readLine();
3✔
1119
        option = mapping.get(answer);
4✔
1120
        if (option == null) {
2!
1121
          LOG.warn("Invalid answer: '{}' - please try again.", answer);
×
1122
        }
1123
      }
1✔
1124
    }
1125
    return option;
2✔
1126
  }
1127

1128
  private static String computeOptionKey(String option) {
1129
    String key = option;
2✔
1130
    int index = -1;
2✔
1131
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1132
      int currentIndex = key.indexOf(c);
4✔
1133
      if (currentIndex != -1) {
3✔
1134
        if ((index == -1) || (currentIndex < index)) {
3!
1135
          index = currentIndex;
2✔
1136
        }
1137
      }
1138
    }
1139
    if (index > 0) {
2✔
1140
      key = key.substring(0, index).trim();
6✔
1141
    }
1142
    return key;
2✔
1143
  }
1144

1145
  /**
1146
   * @return the input from the end-user (e.g. read from the console).
1147
   */
1148
  protected abstract String readLine();
1149

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

1152
    O duplicate = mapping.put(key, option);
5✔
1153
    if (duplicate != null) {
2!
1154
      throw new IllegalArgumentException("Duplicated option " + key);
×
1155
    }
1156
  }
1✔
1157

1158
  @Override
1159
  public Step getCurrentStep() {
1160

1161
    return this.currentStep;
×
1162
  }
1163

1164
  @Override
1165
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1166

1167
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1168
    return this.currentStep;
3✔
1169
  }
1170

1171
  /**
1172
   * Internal method to end the running {@link Step}.
1173
   *
1174
   * @param step the current {@link Step} to end.
1175
   */
1176
  public void endStep(StepImpl step) {
1177

1178
    if (step == this.currentStep) {
4!
1179
      this.currentStep = this.currentStep.getParent();
6✔
1180
    } else {
1181
      String currentStepName = "null";
×
1182
      if (this.currentStep != null) {
×
1183
        currentStepName = this.currentStep.getName();
×
1184
      }
1185
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1186
    }
1187
  }
1✔
1188

1189
  /**
1190
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1191
   *
1192
   * @param arguments the {@link CliArgument}.
1193
   * @return the return code of the execution.
1194
   */
1195
  public int run(CliArguments arguments) {
1196

1197
    CliArgument current = arguments.current();
3✔
1198
    if (current.isStart()) {
3✔
1199
      arguments.next();
3✔
1200
      current = arguments.current();
3✔
1201
    }
1202
    assert (this.currentStep == null);
4!
1203
    boolean supressStepSuccess = false;
2✔
1204
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1205
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1206
    Commandlet cmd = null;
2✔
1207
    ValidationResult result = null;
2✔
1208
    try {
1209
      while (commandletIterator.hasNext()) {
3✔
1210
        cmd = commandletIterator.next();
4✔
1211
        result = applyAndRun(arguments.copy(), cmd);
6✔
1212
        if (result.isValid()) {
3✔
1213
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1214
          step.success();
2✔
1215
          return ProcessResult.SUCCESS;
4✔
1216
        }
1217
      }
1218
      activateLogging(cmd);
3✔
1219
      String commandKey = current.getKey();
3✔
1220

1221
      if (commandKey == null || commandKey.isBlank()) {
5!
1222
        return 0;
×
1223
      }
1224
      Commandlet commandletByName = this.commandletManager.getCommandlet(commandKey);
5✔
1225
      // Missing commandlet
1226
      if (commandletByName == null) {
2✔
1227
        if (getCliSuggester().isMissingCommandletHandled(commandKey, step)) {
6!
1228
          return 1;
4✔
1229
        }
1230
        return 0;
×
1231
      }
1232
      // Missing project context
1233
      if (getCliSuggester().isMissingProjectContextHandled(commandletByName, step)) {
6✔
1234
        return 1;
4✔
1235
      }
1236
      // Only validate options/arguments if same commandlet and proper type
1237
      if (cmd != commandletByName || !(result instanceof ValidationState validationState)) {
10!
1238
        return 0;
×
1239
      }
1240
      // Invalid option
1241
      if (getCliSuggester().isInvalidOptionHandled(validationState, commandletByName, step)) {
7✔
1242
        return 1;
4✔
1243
      }
1244
      // Invalid argument
1245
      if (getCliSuggester().isInvalidArgumentHandled(validationState, commandletByName)) {
6!
1246
        return 1;
4✔
1247
      }
1248
      LOG.error(result.getErrorMessage());
×
1249
      step.error("Invalid arguments: {}", current.getArgs());
×
1250
      IdeLogLevel.INTERACTION.log(LOG, "For additional details run ide help {}", cmd == null ? "" : cmd.getName());
×
1251
      return 1;
×
1252
    } catch (Throwable t) {
1✔
1253
      activateLogging(cmd);
3✔
1254
      step.error(t, true);
4✔
1255
      if (this.logfile != null) {
3!
1256
        // point the user to the logfile directly (does not make sense via logger)
1257
        System.err.println("Logfile can be found at " + this.logfile); // checkstyle:ignore SystemOut
×
1258
      }
1259
      throw t;
2✔
1260
    } finally {
1261
      step.close();
2✔
1262
      assert (this.currentStep == null);
4!
1263
      step.logSummary(supressStepSuccess);
3✔
1264
    }
1265
  }
1266

1267

1268
  /**
1269
   * @return the {@link CliSuggester} for CLI suggestions.
1270
   */
1271
  private CliSuggester getCliSuggester() {
1272
    if (this.cliSuggester == null) {
3✔
1273
      this.cliSuggester = new CliSuggester(this);
6✔
1274
    }
1275
    return this.cliSuggester;
3✔
1276
  }
1277

1278
  /**
1279
   * Ensure the logging system is initialized.
1280
   */
1281
  private void activateLogging(Commandlet cmd) {
1282

1283
    configureJavaUtilLogging(cmd);
3✔
1284
    this.startContext.activateLogging();
3✔
1285
  }
1✔
1286

1287
  /**
1288
   * Configures the logging system (JUL).
1289
   *
1290
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1291
   */
1292
  public void configureJavaUtilLogging(Commandlet cmd) {
1293

1294
    if (this.julConfigured) {
3✔
1295
      return;
1✔
1296
    }
1297
    boolean writeLogfile = isWriteLogfile(cmd);
4✔
1298
    this.startContext.setWriteLogfile(writeLogfile);
4✔
1299
    Properties properties = createJavaUtilLoggingProperties(writeLogfile, cmd);
5✔
1300
    try {
1301
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1302
      properties.store(out, null);
4✔
1303
      out.flush();
2✔
1304
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1305
      LogManager.getLogManager().readConfiguration(in);
3✔
1306
      this.julConfigured = true;
3✔
1307
      this.startContext.activateLogging();
3✔
1308
    } catch (IOException e) {
×
1309
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1310
    }
1✔
1311
  }
1✔
1312

1313
  protected boolean isWriteLogfile(Commandlet cmd) {
1314
    if ((cmd == null) || !cmd.isWriteLogFile()) {
×
1315
      return false;
×
1316
    }
1317
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1318
    return Boolean.TRUE.equals(writeLogfile);
×
1319
  }
1320

1321
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1322

1323
    Path idePath = getIdePath();
3✔
1324
    if (writeLogfile && (idePath == null)) {
2!
1325
      writeLogfile = false;
×
1326
      LOG.error("Cannot enable log-file since IDE_ROOT is undefined.");
×
1327
    }
1328
    Properties properties = new Properties();
4✔
1329
    // prevent 3rd party (e.g. java.lang.ProcessBuilder) logging into our console via JUL
1330
    // see JulLogLevel for the trick we did to workaround JUL flaws
1331
    properties.setProperty(".level", "SEVERE");
5✔
1332
    if (writeLogfile) {
2!
1333
      this.startContext.setLogLevelLogger(IdeLogLevel.TRACE);
×
1334
      String fileHandlerName = FileHandler.class.getName();
×
1335
      properties.setProperty("handlers", JulConsoleHandler.class.getName() + "," + fileHandlerName);
×
1336
      properties.setProperty(fileHandlerName + ".formatter", SimpleFormatter.class.getName());
×
1337
      properties.setProperty(fileHandlerName + ".encoding", "UTF-8");
×
1338
      this.logfile = createLogfilePath(idePath, cmd);
×
1339
      getFileAccess().mkdirs(this.logfile.getParent());
×
1340
      properties.setProperty(fileHandlerName + ".pattern", this.logfile.toString());
×
1341
    } else {
×
1342
      properties.setProperty("handlers", JulConsoleHandler.class.getName());
6✔
1343
    }
1344
    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✔
1345
    return properties;
2✔
1346
  }
1347

1348
  private Path createLogfilePath(Path idePath, Commandlet cmd) {
1349
    LocalDateTime now = LocalDateTime.now();
×
1350
    Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1351
    StringBuilder sb = new StringBuilder(32);
×
1352
    if (this.ideHome == null || ((cmd != null) && !cmd.isIdeHomeRequired())) {
×
1353
      sb.append("_ide-");
×
1354
    } else {
1355
      sb.append(this.ideHome.getFileName().toString());
×
1356
      sb.append('-');
×
1357
    }
1358
    sb.append("ide-");
×
1359
    if (cmd != null) {
×
1360
      sb.append(cmd.getName());
×
1361
      sb.append('-');
×
1362
    }
1363
    sb.append(DateTimeUtil.formatTime(now));
×
1364
    sb.append(".log");
×
1365
    return logsPath.resolve(sb.toString());
×
1366
  }
1367

1368
  @Override
1369
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1370

1371
    this.startContext.deactivateLogging(threshold);
4✔
1372
    lambda.run();
2✔
1373
    this.startContext.activateLogging();
3✔
1374
  }
1✔
1375

1376
  /**
1377
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1378
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1379
   *     {@link Commandlet} did not match and we have to try a different candidate).
1380
   */
1381
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1382

1383
    IdeLogLevel previousLogLevel = null;
2✔
1384
    cmd.reset();
2✔
1385
    ValidationResult result = apply(arguments, cmd);
5✔
1386
    if (result.isValid()) {
3✔
1387
      result = cmd.validate();
3✔
1388
    }
1389
    if (result.isValid()) {
3✔
1390
      LOG.debug("Running commandlet {}", cmd);
4✔
1391
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1392
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1393
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6✔
1394
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1395
      }
1396
      try {
1397
        if (cmd.isProcessableOutput()) {
3✔
1398
          if (!isLogLevelEnabled(IdeLogLevel.DEBUG)) {
4!
1399
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1400
            previousLogLevel = this.startContext.setLogLevelConsole(IdeLogLevel.PROCESSABLE);
×
1401
          }
1402
        } else {
1403
          if (cmd.isIdeHomeRequired()) {
3✔
1404
            LOG.debug(getMessageIdeHomeFound());
4✔
1405
          }
1406
          if (!(cmd instanceof UpgradeCommandlet)) {
3!
1407
            verifyIdeMinVersion(false);
3✔
1408
          }
1409
          Path settingsRepository = getSettingsGitRepository();
3✔
1410
          if (settingsRepository != null) {
2!
1411
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
8✔
1412
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
4!
1413
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1414

1415
              // Inform the user that an update is available. The update message is suppressed if we are already running the update
1416
              String msg = determineSettingsUpdateMessage(cmd);
4✔
1417
              if (msg != null) {
2!
1418
                IdeLogLevel.INTERACTION.log(LOG, msg);
4✔
1419
              }
1420
            }
1421
          }
1422
        }
1423
        boolean success = ensureLicenseAgreement(cmd);
4✔
1424
        if (!success) {
2!
1425
          return ValidationResultValid.get();
×
1426
        }
1427
        cmd.run();
2✔
1428
      } finally {
1429
        if (previousLogLevel != null) {
2!
1430
          this.startContext.setLogLevelConsole(previousLogLevel);
×
1431
        }
1432
      }
1✔
1433
    } else {
1434
      LOG.trace("Commandlet did not match");
3✔
1435
    }
1436
    return result;
2✔
1437
  }
1438

1439

1440
  /**
1441
   * 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
1442
   * 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
1443
   * appropriate {@code ide update} command, the message is suppressed to avoid confusion.
1444
   *
1445
   * @param cmd the {@link Commandlet}.
1446
   * @return {@code msg} to log to the console. {@code null} if the message is suppressed.
1447
   */
1448
  private String determineSettingsUpdateMessage(Commandlet cmd) {
1449
    boolean update = cmd instanceof UpdateCommandlet;
3✔
1450
    if (isSettingsCodeRepository()) {
3!
1451
      if (update && (isForceMode() || isForcePull())) {
×
1452
        return null;
×
1453
      }
1454
      return "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1455
    } else {
1456
      if (update) {
2!
1457
        return null;
×
1458
      }
1459
      return "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
2✔
1460
    }
1461
  }
1462

1463
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1464

1465
    if (isTest()) {
3!
1466
      return true; // ignore for tests
2✔
1467
    }
1468
    getFileAccess().mkdirs(this.userHomeIde);
×
1469
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1470
    if (Files.isRegularFile(licenseAgreement)) {
×
1471
      return true; // success, license already accepted
×
1472
    }
1473
    if (cmd instanceof EnvironmentCommandlet) {
×
1474
      // 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
1475
      // 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
1476
      // printing anything anymore in such case.
1477
      return false;
×
1478
    }
1479
    activateLogging(cmd);
×
1480
    IdeLogLevel oldLogLevel = this.startContext.getLogLevelConsole();
×
1481
    IdeLogLevel newLogLevel = oldLogLevel;
×
1482
    if (oldLogLevel.ordinal() > IdeLogLevel.INFO.ordinal()) {
×
1483
      newLogLevel = IdeLogLevel.INFO;
×
1484
      this.startContext.setLogLevelConsole(newLogLevel);
×
1485
    }
1486
    StringBuilder sb = new StringBuilder(1180);
×
1487
    sb.append(LOGO).append("""
×
1488
        Welcome to IDEasy!
1489
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1490
        It supports automatic download and installation of arbitrary 3rd party tools.
1491
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1492
        But if explicitly configured, also commercial software that requires an additional license may be used.
1493
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1494
        You are solely responsible for all risks implied by using this software.
1495
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1496
        You will be able to find it online under the following URL:
1497
        """).append(LICENSE_URL);
×
1498
    if (this.ideRoot != null) {
×
1499
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1500
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1501
    }
1502
    LOG.info(sb.toString());
×
1503
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1504

1505
    sb.setLength(0);
×
1506
    LocalDateTime now = LocalDateTime.now();
×
1507
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1508
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1509
    try {
1510
      Files.writeString(licenseAgreement, sb);
×
1511
    } catch (Exception e) {
×
1512
      throw new RuntimeException("Failed to save license agreement!", e);
×
1513
    }
×
1514
    if (oldLogLevel != newLogLevel) {
×
1515
      this.startContext.setLogLevelConsole(oldLogLevel);
×
1516
    }
1517
    return true;
×
1518
  }
1519

1520
  @Override
1521
  public void verifyIdeMinVersion(boolean throwException) {
1522
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1523
    if (minVersion == null) {
2✔
1524
      return;
1✔
1525
    }
1526
    VersionIdentifier versionIdentifier = IdeVersion.getVersionIdentifier();
2✔
1527
    if (versionIdentifier.compareVersion(minVersion).isLess() && !IdeVersion.isUndefined()) {
7!
1528
      String warning = String.format("Your version of IDEasy is currently %s\n"
13✔
1529
          + "However, this is too old as your project requires at latest version %s", versionIdentifier, minVersion);
1530
      String interaction = "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
2✔
1531
          + "ide upgrade";
1532
      if (throwException) {
2✔
1533
        throw new CliException(warning + "\n" + interaction);
7✔
1534
      } else {
1535
        LOG.warn(warning);
3✔
1536
        IdeLogLevel.INTERACTION.log(LOG, interaction);
4✔
1537
      }
1538
    }
1539
  }
1✔
1540

1541
  /**
1542
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1543
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1544
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1545
   */
1546
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1547

1548
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1549
    if (arguments.current().isStart()) {
4✔
1550
      arguments.next();
3✔
1551
    }
1552
    if (includeContextOptions) {
2✔
1553
      ContextCommandlet cc = new ContextCommandlet();
4✔
1554
      for (Property<?> property : cc.getProperties()) {
11✔
1555
        assert (property.isOption());
4!
1556
        property.apply(arguments, this, cc, collector);
7✔
1557
      }
1✔
1558
    }
1559
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1560
    CliArgument current = arguments.current();
3✔
1561
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1562
      collector.add(current.get(), null, null, null);
7✔
1563
    }
1564
    arguments.next();
3✔
1565
    while (commandletIterator.hasNext()) {
3✔
1566
      Commandlet cmd = commandletIterator.next();
4✔
1567
      if (!arguments.current().isEnd()) {
4✔
1568
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1569
      }
1570
    }
1✔
1571
    return collector.getSortedCandidates();
3✔
1572
  }
1573

1574
  /**
1575
   * Gets the next value property and applies its implicit end-options behavior if required.
1576
   *
1577
   * @param valueIterator the iterator over the commandlet value properties
1578
   * @param arguments the CLI arguments whose option parsing state may be updated
1579
   * @return the next value property or {@code null} if no further value property exists
1580
   */
1581
  private Property<?> nextValueProperty(Iterator<Property<?>> valueIterator, CliArguments arguments) {
1582

1583
    if (!valueIterator.hasNext()) {
3✔
1584
      return null;
2✔
1585
    }
1586

1587
    Property<?> valueProperty = valueIterator.next();
4✔
1588
    if (valueProperty.isEndOptions()) {
3✔
1589
      // Tool argument properties should accept values starting with "-" so stop option parsing here
1590
      arguments.endOptions();
2✔
1591
    }
1592
    return valueProperty;
2✔
1593
  }
1594

1595
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1596

1597
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1598
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1599
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1600
    Property<?> currentValueProperty = nextValueProperty(valueIterator, arguments);
5✔
1601
    List<Property<?>> properties = cmd.getProperties();
3✔
1602
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1603
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1604
    for (Property<?> property : properties) {
10✔
1605
      if (property.isOption()) {
3✔
1606
        optionProperties.add(property);
4✔
1607
      }
1608
    }
1✔
1609
    CliArgument currentArgument = arguments.current();
3✔
1610
    while (!currentArgument.isEnd()) {
3✔
1611
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1612
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6✔
1613
        if (currentArgument.isCompletion()) {
3✔
1614
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1615
          while (optionIterator.hasNext()) {
3✔
1616
            Property<?> option = optionIterator.next();
4✔
1617
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1618
            if (success) {
2✔
1619
              optionIterator.remove();
2✔
1620
              arguments.next();
3✔
1621
            }
1622
          }
1✔
1623
        } else {
1✔
1624
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1625
          if (option != null) {
2✔
1626
            arguments.next();
3✔
1627
            boolean removed = optionProperties.remove(option);
4✔
1628
            if (!removed) {
2!
1629
              option = null;
×
1630
            }
1631
          }
1632
          if (option == null) {
2✔
1633
            LOG.trace("No such option was found.");
3✔
1634
            return;
1✔
1635
          }
1636
        }
1✔
1637
      } else {
1638
        if (currentValueProperty != null) {
2✔
1639
          boolean success = currentValueProperty.apply(arguments, this, cmd, collector);
7✔
1640
          if (!success) {
2✔
1641
            LOG.trace("Completion cannot match any further.");
3✔
1642
            return;
1✔
1643
          }
1644
          if (!currentValueProperty.isMultiValued()) {
3✔
1645
            currentValueProperty = nextValueProperty(valueIterator, arguments);
5✔
1646
          }
1647
        } else {
1✔
1648
          LOG.trace("No value left for completion.");
3✔
1649
          return;
1✔
1650
        }
1651
      }
1652
      currentArgument = arguments.current();
4✔
1653
    }
1654
  }
1✔
1655

1656
  /**
1657
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1658
   *     {@link CliArguments#copy() copy} as needed.
1659
   * @param cmd the potential {@link Commandlet} to match.
1660
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1661
   */
1662
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1663

1664
    LOG.trace("Trying to match arguments to commandlet {}", cmd.getName());
5✔
1665
    CliArgument currentArgument = arguments.current();
3✔
1666
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1667
    Property<?> property = null;
2✔
1668
    if (propertyIterator.hasNext()) {
3!
1669
      property = propertyIterator.next();
4✔
1670
    }
1671
    while (!currentArgument.isEnd()) {
3✔
1672
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1673
      Property<?> currentProperty = property;
2✔
1674
      if (!arguments.isEndOptions()) {
3✔
1675
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1676
        if (option != null) {
2✔
1677
          currentProperty = option;
3✔
1678
        } else {
1679
          boolean allowDashedValue = (property != null) && property.isValue()
6!
1680
              && (property.isMultiValued() || "-".equals(currentArgument.get()));
11!
1681
          boolean allowKeywordOption = (currentProperty instanceof KeywordProperty keywordProperty) && keywordProperty.matches(currentArgument.getKey());
15!
1682
          if (!allowDashedValue && !allowKeywordOption && currentArgument.isOption()) {
7✔
1683
            ValidationState state = new ValidationState(null);
5✔
1684
            state.addInvalidOption(currentArgument.getKey());
4✔
1685
            state.addErrorMessage("Invalid option \"" + currentArgument.getKey() + "\"");
5✔
1686
            return state;
2✔
1687
          }
1688
        }
1689
      }
1690
      if (currentProperty == null) {
2!
1691
        LOG.trace("No option or next value found");
×
1692
        ValidationState state = new ValidationState(null);
×
1693
        state.addErrorMessage("No matching property found");
×
1694
        return state;
×
1695
      }
1696
      LOG.trace("Next property candidate to match argument is {}", currentProperty);
4✔
1697
      if (currentProperty == property) {
3✔
1698
        if (!property.isMultiValued()) {
3✔
1699
          if (propertyIterator.hasNext()) {
3✔
1700
            property = propertyIterator.next();
5✔
1701
          } else {
1702
            property = null;
2✔
1703
          }
1704
        }
1705
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1706
          arguments.endOptions();
2✔
1707
        }
1708
      }
1709
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1710
      if (!matches) {
2✔
1711
        String invalidValue = currentProperty.getLastInvalidValue();
3✔
1712
        if (invalidValue != null) {
2!
1713
          ValidationState state = new ValidationState(null);
5✔
1714
          state.addInvalidArgument(invalidValue, currentProperty.getNameOrAlias());
5✔
1715
          state.addErrorMessage(
4✔
1716
              "Invalid CLI argument '" + invalidValue + "' for property '" + currentProperty.getNameOrAlias() + "' of commandlet '" + cmd.getName() + "'");
4✔
1717
          currentProperty.clearLastInvalidValue();
2✔
1718
          return state;
2✔
1719
        }
1720
        ValidationState state = new ValidationState(null);
×
1721
        state.addErrorMessage("No matching property found");
×
1722
        return state;
×
1723
      }
1724
      currentArgument = arguments.current();
3✔
1725
    }
1✔
1726
    return ValidationResultValid.get();
2✔
1727
  }
1728

1729
  @Override
1730
  public Path findBash() {
1731
    if (this.bash != null) {
3✔
1732
      return this.bash;
3✔
1733
    }
1734
    Path bashPath = findBashOnBashPath();
3✔
1735
    if (bashPath == null) {
2✔
1736
      bashPath = findBashInPath();
3✔
1737
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1738
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1739
        if (bashPath == null) {
2!
1740
          bashPath = findBashInWindowsRegistry();
3✔
1741
        }
1742
      }
1743
    }
1744
    if (bashPath == null) {
2✔
1745
      LOG.error("No bash executable could be found on your system.");
4✔
1746
    } else {
1747
      this.bash = bashPath;
3✔
1748
    }
1749
    return bashPath;
2✔
1750
  }
1751

1752
  private Path findBashOnBashPath() {
1753
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1754
    Path bash;
1755
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1756
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1757
    if (bashVariable != null) {
2✔
1758
      bash = Path.of(bashVariable);
5✔
1759
      if (Files.exists(bash)) {
5✔
1760
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1761
        return bash;
2✔
1762
      } else {
1763
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1764
        return null;
2✔
1765
      }
1766
    } else {
1767
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1768
      return null;
2✔
1769
    }
1770
  }
1771

1772
  /**
1773
   * @param path the path to check.
1774
   * @param toIgnore the String sequence which needs to be checked and ignored.
1775
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1776
   */
1777
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1778
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1779
    return !s.contains(toIgnore);
7!
1780
  }
1781

1782
  /**
1783
   * Tries to find the bash.exe within the PATH environment variable.
1784
   *
1785
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1786
   */
1787
  private Path findBashInPath() {
1788
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1789
    Path bash;
1790
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1791
    if (pathVariableName != null) {
2!
1792
      Path plainBash = Path.of(BASH);
5✔
1793
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1794
          "\\windows\\system32");
1795
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1796
      bash = bashPath.toAbsolutePath();
3✔
1797
      if (bashPath.equals(plainBash)) {
4✔
1798
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1799
        bash = null;
3✔
1800
      } else {
1801
        if (Files.exists(bashPath)) {
5!
1802
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1803
        } else {
1804
          bash = null;
×
1805
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1806
        }
1807
      }
1808
    } else {
1✔
1809
      bash = null;
×
1810
      // this should never happen...
1811
      LOG.error("PATH environment variable was not found");
×
1812
    }
1813
    return bash;
2✔
1814
  }
1815

1816
  /**
1817
   * Tries to find the bash.exe within the Windows registry.
1818
   *
1819
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1820
   */
1821
  protected Path findBashInWindowsRegistry() {
1822
    LOG.trace("Trying to find bash in Windows registry");
×
1823
    // If not found in the default location, try the registry query
1824
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1825
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1826
    for (String bashVariant : bashVariants) {
×
1827
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1828
      for (String registryKey : registryKeys) {
×
1829
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1830
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1831
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1832

1833
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1834
        if (path != null) {
×
1835
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1836
          if (Files.exists(bashPath)) {
×
1837
            LOG.debug("Found bash at: {}", bashPath);
×
1838
            return bashPath;
×
1839
          } else {
1840
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1841
            return null;
×
1842
          }
1843
        } else {
1844
          LOG.info("No bash executable could be found in the Windows registry.");
×
1845
        }
1846
      }
1847
    }
1848
    // no bash found
1849
    return null;
×
1850
  }
1851

1852
  private Path findBashOnWindowsDefaultGitPath() {
1853
    // Check if Git Bash exists in the default location
1854
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1855
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1856
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1857
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1858
      return defaultPath;
×
1859
    }
1860
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1861
    return null;
2✔
1862
  }
1863

1864
  @Override
1865
  public WindowsPathSyntax getPathSyntax() {
1866

1867
    return this.pathSyntax;
3✔
1868
  }
1869

1870
  /**
1871
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1872
   */
1873
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1874

1875
    this.pathSyntax = pathSyntax;
3✔
1876
  }
1✔
1877

1878
  /**
1879
   * @return the {@link IdeStartContextImpl}.
1880
   */
1881
  public IdeStartContextImpl getStartContext() {
1882

1883
    return startContext;
3✔
1884
  }
1885

1886
  /**
1887
   * @return the {@link WindowsHelper}.
1888
   */
1889
  public final WindowsHelper getWindowsHelper() {
1890

1891
    if (this.windowsHelper == null) {
3✔
1892
      this.windowsHelper = createWindowsHelper();
4✔
1893
    }
1894
    return this.windowsHelper;
3✔
1895
  }
1896

1897
  /**
1898
   * @return the new {@link WindowsHelper} instance.
1899
   */
1900
  protected WindowsHelper createWindowsHelper() {
1901

1902
    return new WindowsHelperImpl(this);
×
1903
  }
1904

1905
  /**
1906
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1907
   */
1908
  public void reload() {
1909

1910
    this.variables = null;
3✔
1911
    this.customToolRepository = null;
3✔
1912
  }
1✔
1913

1914
  @Override
1915
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1916

1917
    assert (Files.isDirectory(installationPath));
6!
1918
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1919
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1920
  }
1✔
1921

1922
  /*
1923
   * @param home the IDE_HOME directory.
1924
   * @param workspace the name of the active workspace folder.
1925
   */
1926
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1927

1928
  }
1929

1930
  /**
1931
   * Returns the default git path on Windows. Required to be overwritten in tests.
1932
   *
1933
   * @return default path to git on Windows.
1934
   */
1935
  public String getDefaultWindowsGitPath() {
1936
    return DEFAULT_WINDOWS_GIT_PATH;
×
1937
  }
1938

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

© 2026 Coveralls, Inc