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

devonfw / IDEasy / 13327588889

14 Feb 2025 10:44AM UTC coverage: 67.947% (-0.5%) from 68.469%
13327588889

Pull #1021

github

web-flow
Merge d03159bfe into 52609dacb
Pull Request #1021: #786: support ide upgrade to automatically update to the latest version of IDEasy

2964 of 4791 branches covered (61.87%)

Branch coverage included in aggregate %.

7688 of 10886 relevant lines covered (70.62%)

3.07 hits per line

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

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

3
import java.io.BufferedReader;
4
import java.io.InputStreamReader;
5
import java.net.URL;
6
import java.net.URLConnection;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.time.LocalDateTime;
10
import java.util.ArrayList;
11
import java.util.HashMap;
12
import java.util.Iterator;
13
import java.util.List;
14
import java.util.Locale;
15
import java.util.Map;
16
import java.util.Objects;
17

18
import com.devonfw.tools.ide.cli.CliAbortException;
19
import com.devonfw.tools.ide.cli.CliArgument;
20
import com.devonfw.tools.ide.cli.CliArguments;
21
import com.devonfw.tools.ide.cli.CliException;
22
import com.devonfw.tools.ide.commandlet.Commandlet;
23
import com.devonfw.tools.ide.commandlet.CommandletManager;
24
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
25
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
26
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
27
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
28
import com.devonfw.tools.ide.common.SystemPath;
29
import com.devonfw.tools.ide.completion.CompletionCandidate;
30
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
31
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
32
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
33
import com.devonfw.tools.ide.environment.EnvironmentVariables;
34
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
35
import com.devonfw.tools.ide.environment.IdeSystem;
36
import com.devonfw.tools.ide.environment.IdeSystemImpl;
37
import com.devonfw.tools.ide.git.GitContext;
38
import com.devonfw.tools.ide.git.GitContextImpl;
39
import com.devonfw.tools.ide.git.GitUrl;
40
import com.devonfw.tools.ide.io.FileAccess;
41
import com.devonfw.tools.ide.io.FileAccessImpl;
42
import com.devonfw.tools.ide.log.IdeLogLevel;
43
import com.devonfw.tools.ide.log.IdeLogger;
44
import com.devonfw.tools.ide.log.IdeSubLogger;
45
import com.devonfw.tools.ide.merge.DirectoryMerger;
46
import com.devonfw.tools.ide.migration.IdeMigrator;
47
import com.devonfw.tools.ide.network.NetworkProxy;
48
import com.devonfw.tools.ide.os.SystemInfo;
49
import com.devonfw.tools.ide.os.SystemInfoImpl;
50
import com.devonfw.tools.ide.os.WindowsHelper;
51
import com.devonfw.tools.ide.os.WindowsHelperImpl;
52
import com.devonfw.tools.ide.os.WindowsPathSyntax;
53
import com.devonfw.tools.ide.process.ProcessContext;
54
import com.devonfw.tools.ide.process.ProcessContextImpl;
55
import com.devonfw.tools.ide.process.ProcessResult;
56
import com.devonfw.tools.ide.property.Property;
57
import com.devonfw.tools.ide.step.Step;
58
import com.devonfw.tools.ide.step.StepImpl;
59
import com.devonfw.tools.ide.tool.repository.CustomToolRepository;
60
import com.devonfw.tools.ide.tool.repository.CustomToolRepositoryImpl;
61
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
62
import com.devonfw.tools.ide.tool.repository.MavenRepository;
63
import com.devonfw.tools.ide.tool.repository.ToolRepository;
64
import com.devonfw.tools.ide.url.model.UrlMetadata;
65
import com.devonfw.tools.ide.util.DateTimeUtil;
66
import com.devonfw.tools.ide.validation.ValidationResult;
67
import com.devonfw.tools.ide.validation.ValidationResultValid;
68
import com.devonfw.tools.ide.validation.ValidationState;
69
import com.devonfw.tools.ide.variable.IdeVariables;
70
import com.devonfw.tools.ide.version.VersionIdentifier;
71

72
/**
73
 * Abstract base implementation of {@link IdeContext}.
74
 */
75
public abstract class AbstractIdeContext implements IdeContext {
76

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

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

81
  private final IdeStartContextImpl startContext;
82

83
  private Path ideHome;
84

85
  private final Path ideRoot;
86

87
  private final Path idePath;
88

89
  private Path confPath;
90

91
  protected Path settingsPath;
92

93
  private Path settingsCommitIdPath;
94

95
  private Path softwarePath;
96

97
  private Path softwareExtraPath;
98

99
  private final Path softwareRepositoryPath;
100

101
  protected Path pluginsPath;
102

103
  private Path workspacePath;
104

105
  private String workspaceName;
106

107
  protected Path urlsPath;
108

109
  private final Path tempPath;
110

111
  private final Path tempDownloadPath;
112

113
  private Path cwd;
114

115
  private Path downloadPath;
116

117
  private final Path toolRepositoryPath;
118

119
  protected Path userHome;
120

121
  private Path userHomeIde;
122

123
  private SystemPath path;
124

125
  private WindowsPathSyntax pathSyntax;
126

127
  private final SystemInfo systemInfo;
128

129
  private EnvironmentVariables variables;
130

131
  private final FileAccess fileAccess;
132

133
  protected CommandletManager commandletManager;
134

135
  protected ToolRepository defaultToolRepository;
136

137
  private CustomToolRepository customToolRepository;
138

139
  private MavenRepository mavenRepository;
140

141
  private DirectoryMerger workspaceMerger;
142

143
  protected UrlMetadata urlMetadata;
144

145
  protected Path defaultExecutionDirectory;
146

147
  private StepImpl currentStep;
148

149
  protected Boolean online;
150

151
  protected IdeSystem system;
152

153
  private NetworkProxy networkProxy;
154

155
  private WindowsHelper windowsHelper;
156

157
  /**
158
   * The constructor.
159
   *
160
   * @param startContext the {@link IdeLogger}.
161
   * @param workingDirectory the optional {@link Path} to current working directory.
162
   */
163
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
164

165
    super();
2✔
166
    this.startContext = startContext;
3✔
167
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
168
    this.commandletManager = new CommandletManagerImpl(this);
6✔
169
    this.fileAccess = new FileAccessImpl(this);
6✔
170
    String workspace = WORKSPACE_MAIN;
2✔
171
    if (workingDirectory == null) {
2!
172
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
173
    } else {
174
      workingDirectory = workingDirectory.toAbsolutePath();
3✔
175
    }
176
    this.cwd = workingDirectory;
3✔
177
    // detect IDE_HOME and WORKSPACE
178
    Path currentDir = workingDirectory;
2✔
179
    String name1 = "";
2✔
180
    String name2 = "";
2✔
181
    Path ideRootPath = getIdeRootPathFromEnv();
3✔
182
    while (currentDir != null) {
2✔
183
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
184
      if (isIdeHome(currentDir)) {
4✔
185
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
186
          workspace = name2;
3✔
187
        }
188
        break;
189
      }
190
      name2 = name1;
2✔
191
      int nameCount = currentDir.getNameCount();
3✔
192
      if (nameCount >= 1) {
3✔
193
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
194
      }
195
      currentDir = currentDir.getParent();
3✔
196
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
197
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
198
        currentDir = null;
×
199
      }
200
    }
1✔
201

202
    // detection completed, initializing variables
203
    this.ideRoot = findIdeRoot(currentDir);
5✔
204

205
    setCwd(workingDirectory, workspace, currentDir);
5✔
206

207
    if (this.ideRoot == null) {
3✔
208
      this.idePath = null;
3✔
209
      this.toolRepositoryPath = null;
3✔
210
      this.urlsPath = null;
3✔
211
      this.tempPath = null;
3✔
212
      this.tempDownloadPath = null;
3✔
213
      this.softwareRepositoryPath = null;
4✔
214
    } else {
215
      this.idePath = this.ideRoot.resolve(FOLDER_UNDERSCORE_IDE);
6✔
216
      this.toolRepositoryPath = this.idePath.resolve("software");
6✔
217
      this.urlsPath = this.idePath.resolve("urls");
6✔
218
      this.tempPath = this.idePath.resolve("tmp");
6✔
219
      this.tempDownloadPath = this.tempPath.resolve(FOLDER_DOWNLOADS);
6✔
220
      this.softwareRepositoryPath = this.idePath.resolve(FOLDER_SOFTWARE);
6✔
221
      if (Files.isDirectory(this.tempPath)) {
7✔
222
        // TODO delete all files older than 1 day here...
223
      } else {
224
        this.fileAccess.mkdirs(this.tempDownloadPath);
5✔
225
      }
226
    }
227

228
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
229
    this.mavenRepository = new MavenRepository(this);
6✔
230
  }
1✔
231

232
  private Path findIdeRoot(Path ideHomePath) {
233

234
    Path ideRootPath = null;
2✔
235
    if (ideHomePath != null) {
2✔
236
      ideRootPath = ideHomePath.getParent();
4✔
237
    } else if (!isTest()) {
3!
238
      ideRootPath = getIdeRootPathFromEnv();
×
239
    }
240
    return ideRootPath;
2✔
241
  }
242

243
  /**
244
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
245
   */
246
  protected Path getIdeRootPathFromEnv() {
247

248
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
249
    if (root != null) {
×
250
      Path rootPath = Path.of(root);
×
251
      if (Files.isDirectory(rootPath)) {
×
252
        return rootPath;
×
253
      }
254
    }
255
    return null;
×
256
  }
257

258
  @Override
259
  public void setCwd(Path userDir, String workspace, Path ideHome) {
260

261
    this.cwd = userDir;
3✔
262
    this.workspaceName = workspace;
3✔
263
    this.ideHome = ideHome;
3✔
264
    if (ideHome == null) {
2✔
265
      this.workspacePath = null;
3✔
266
      this.confPath = null;
3✔
267
      this.settingsPath = null;
3✔
268
      this.softwarePath = null;
3✔
269
      this.softwareExtraPath = null;
3✔
270
      this.pluginsPath = null;
4✔
271
    } else {
272
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
273
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
274
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
275
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
276
      this.softwarePath = this.ideHome.resolve(FOLDER_SOFTWARE);
6✔
277
      this.softwareExtraPath = this.softwarePath.resolve(FOLDER_EXTRA);
6✔
278
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
279
    }
280
    if (isTest()) {
3!
281
      // only for testing...
282
      if (this.ideHome == null) {
3✔
283
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
284
      } else {
285
        this.userHome = this.ideHome.resolve("home");
7✔
286
      }
287
    } else {
288
      this.userHome = Path.of(getSystem().getProperty("user.home"));
×
289
    }
290
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
291
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
292

293
    this.path = computeSystemPath();
4✔
294
  }
1✔
295

296
  private String getMessageIdeHomeFound() {
297

298
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
299
  }
300

301
  private String getMessageIdeHomeNotFound() {
302

303
    return "You are not inside an IDE installation: " + this.cwd;
5✔
304
  }
305

306
  private String getMessageIdeRootNotFound() {
307

308
    String root = getSystem().getEnv("IDE_ROOT");
5✔
309
    if (root == null) {
2!
310
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
311
    } else {
312
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
313
    }
314
  }
315

316
  /**
317
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
318
   */
319
  public boolean isTest() {
320

321
    return false;
×
322
  }
323

324
  protected SystemPath computeSystemPath() {
325

326
    return new SystemPath(this);
×
327
  }
328

329
  private boolean isIdeHome(Path dir) {
330

331
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
332
      return false;
2✔
333
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
334
      return false;
×
335
    }
336
    return true;
2✔
337
  }
338

339
  private EnvironmentVariables createVariables() {
340

341
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
342
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
343
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
344
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
345
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
346
    return conf.resolved();
3✔
347
  }
348

349
  protected AbstractEnvironmentVariables createSystemVariables() {
350

351
    return EnvironmentVariables.ofSystem(this);
3✔
352
  }
353

354
  @Override
355
  public SystemInfo getSystemInfo() {
356

357
    return this.systemInfo;
3✔
358
  }
359

360
  @Override
361
  public FileAccess getFileAccess() {
362

363
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
364
    configureNetworkProxy();
2✔
365
    return this.fileAccess;
3✔
366
  }
367

368
  @Override
369
  public CommandletManager getCommandletManager() {
370

371
    return this.commandletManager;
3✔
372
  }
373

374
  @Override
375
  public ToolRepository getDefaultToolRepository() {
376

377
    return this.defaultToolRepository;
3✔
378
  }
379

380
  @Override
381
  public MavenRepository getMavenToolRepository() {
382

383
    return this.mavenRepository;
3✔
384
  }
385

386
  @Override
387
  public CustomToolRepository getCustomToolRepository() {
388

389
    if (this.customToolRepository == null) {
3!
390
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
391
    }
392
    return this.customToolRepository;
3✔
393
  }
394

395
  @Override
396
  public Path getIdeHome() {
397

398
    return this.ideHome;
3✔
399
  }
400

401
  @Override
402
  public String getProjectName() {
403

404
    if (this.ideHome != null) {
3!
405
      return this.ideHome.getFileName().toString();
5✔
406
    }
407
    return "";
×
408
  }
409

410
  @Override
411
  public VersionIdentifier getProjectVersion() {
412

413
    if (this.ideHome != null) {
3!
414
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
415
      if (Files.exists(versionFile)) {
5✔
416
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
417
        return VersionIdentifier.of(version);
3✔
418
      }
419
    }
420
    return IdeMigrator.START_VERSION;
2✔
421
  }
422

423
  @Override
424
  public void setProjectVersion(VersionIdentifier version) {
425

426
    if (this.ideHome == null) {
3!
427
      throw new IllegalStateException("IDE_HOME not available!");
×
428
    }
429
    Objects.requireNonNull(version);
3✔
430
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
431
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
432
  }
1✔
433

434
  @Override
435
  public Path getIdeRoot() {
436

437
    return this.ideRoot;
3✔
438
  }
439

440
  @Override
441
  public Path getIdePath() {
442

443
    return this.idePath;
×
444
  }
445

446
  @Override
447
  public Path getCwd() {
448

449
    return this.cwd;
3✔
450
  }
451

452
  @Override
453
  public Path getTempPath() {
454

455
    return this.tempPath;
3✔
456
  }
457

458
  @Override
459
  public Path getTempDownloadPath() {
460

461
    return this.tempDownloadPath;
3✔
462
  }
463

464
  @Override
465
  public Path getUserHome() {
466

467
    return this.userHome;
3✔
468
  }
469

470
  @Override
471
  public Path getUserHomeIde() {
472

473
    return this.userHomeIde;
3✔
474
  }
475

476
  @Override
477
  public Path getSettingsPath() {
478

479
    return this.settingsPath;
3✔
480
  }
481

482
  @Override
483
  public Path getSettingsGitRepository() {
484

485
    Path settingsPath = getSettingsPath();
3✔
486
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
487
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
488
      error("Settings repository exists but is not a git repository.");
3✔
489
      return null;
2✔
490
    }
491
    return settingsPath;
2✔
492
  }
493

494
  @Override
495
  public boolean isSettingsRepositorySymlinkOrJunction() {
496

497
    Path settingsPath = getSettingsPath();
3✔
498
    if (settingsPath == null) {
2!
499
      return false;
×
500
    }
501
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
502
  }
503

504
  @Override
505
  public Path getSettingsCommitIdPath() {
506

507
    return this.settingsCommitIdPath;
3✔
508
  }
509

510
  @Override
511
  public Path getConfPath() {
512

513
    return this.confPath;
3✔
514
  }
515

516
  @Override
517
  public Path getSoftwarePath() {
518

519
    return this.softwarePath;
3✔
520
  }
521

522
  @Override
523
  public Path getSoftwareExtraPath() {
524

525
    return this.softwareExtraPath;
3✔
526
  }
527

528
  @Override
529
  public Path getSoftwareRepositoryPath() {
530

531
    return this.softwareRepositoryPath;
3✔
532
  }
533

534
  @Override
535
  public Path getPluginsPath() {
536

537
    return this.pluginsPath;
3✔
538
  }
539

540
  @Override
541
  public String getWorkspaceName() {
542

543
    return this.workspaceName;
3✔
544
  }
545

546
  @Override
547
  public Path getWorkspacePath() {
548

549
    return this.workspacePath;
3✔
550
  }
551

552
  @Override
553
  public Path getDownloadPath() {
554

555
    return this.downloadPath;
3✔
556
  }
557

558
  @Override
559
  public Path getUrlsPath() {
560

561
    return this.urlsPath;
3✔
562
  }
563

564
  @Override
565
  public Path getToolRepositoryPath() {
566

567
    return this.toolRepositoryPath;
3✔
568
  }
569

570
  @Override
571
  public SystemPath getPath() {
572

573
    return this.path;
3✔
574
  }
575

576
  @Override
577
  public EnvironmentVariables getVariables() {
578

579
    if (this.variables == null) {
3✔
580
      this.variables = createVariables();
4✔
581
    }
582
    return this.variables;
3✔
583
  }
584

585
  @Override
586
  public UrlMetadata getUrls() {
587

588
    if (this.urlMetadata == null) {
3✔
589
      if (!isTest()) {
3!
590
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, this.urlsPath, null);
×
591
      }
592
      this.urlMetadata = new UrlMetadata(this);
6✔
593
    }
594
    return this.urlMetadata;
3✔
595
  }
596

597
  @Override
598
  public boolean isQuietMode() {
599

600
    return this.startContext.isQuietMode();
4✔
601
  }
602

603
  @Override
604
  public boolean isBatchMode() {
605

606
    return this.startContext.isBatchMode();
×
607
  }
608

609
  @Override
610
  public boolean isForceMode() {
611

612
    return this.startContext.isForceMode();
4✔
613
  }
614

615
  @Override
616
  public boolean isOfflineMode() {
617

618
    return this.startContext.isOfflineMode();
4✔
619
  }
620

621
  @Override
622
  public boolean isSkipUpdatesMode() {
623

624
    return this.startContext.isSkipUpdatesMode();
×
625
  }
626

627
  @Override
628
  public boolean isOnline() {
629

630
    if (this.online == null) {
3✔
631
      configureNetworkProxy();
2✔
632
      // we currently assume we have only a CLI process that runs shortly
633
      // therefore we run this check only once to save resources when this method is called many times
634
      try {
635
        int timeout = 1000;
2✔
636
        //open a connection to github.com and try to retrieve data
637
        //getContent fails if there is no connection
638
        URLConnection connection = new URL("https://www.github.com").openConnection();
6✔
639
        connection.setConnectTimeout(timeout);
3✔
640
        connection.getContent();
3✔
641
        this.online = Boolean.TRUE;
3✔
642
      } catch (Exception ignored) {
×
643
        this.online = Boolean.FALSE;
×
644
      }
1✔
645
    }
646
    return this.online.booleanValue();
4✔
647
  }
648

649
  private void configureNetworkProxy() {
650

651
    if (this.networkProxy == null) {
3✔
652
      this.networkProxy = new NetworkProxy(this);
6✔
653
      this.networkProxy.configure();
3✔
654
    }
655
  }
1✔
656

657
  @Override
658
  public Locale getLocale() {
659

660
    Locale locale = this.startContext.getLocale();
4✔
661
    if (locale == null) {
2!
662
      locale = Locale.getDefault();
×
663
    }
664
    return locale;
2✔
665
  }
666

667
  @Override
668
  public DirectoryMerger getWorkspaceMerger() {
669

670
    if (this.workspaceMerger == null) {
3✔
671
      this.workspaceMerger = new DirectoryMerger(this);
6✔
672
    }
673
    return this.workspaceMerger;
3✔
674
  }
675

676
  /**
677
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
678
   */
679
  @Override
680
  public Path getDefaultExecutionDirectory() {
681

682
    return this.defaultExecutionDirectory;
×
683
  }
684

685
  /**
686
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
687
   */
688
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
689

690
    if (defaultExecutionDirectory != null) {
×
691
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
692
    }
693
  }
×
694

695
  @Override
696
  public GitContext getGitContext() {
697

698
    return new GitContextImpl(this);
×
699
  }
700

701
  @Override
702
  public ProcessContext newProcess() {
703

704
    ProcessContext processContext = createProcessContext();
3✔
705
    if (this.defaultExecutionDirectory != null) {
3!
706
      processContext.directory(this.defaultExecutionDirectory);
×
707
    }
708
    return processContext;
2✔
709
  }
710

711
  @Override
712
  public IdeSystem getSystem() {
713

714
    if (this.system == null) {
×
715
      this.system = new IdeSystemImpl(this);
×
716
    }
717
    return this.system;
×
718
  }
719

720
  /**
721
   * @return a new instance of {@link ProcessContext}.
722
   * @see #newProcess()
723
   */
724
  protected ProcessContext createProcessContext() {
725

726
    return new ProcessContextImpl(this);
×
727
  }
728

729
  @Override
730
  public IdeSubLogger level(IdeLogLevel level) {
731

732
    return this.startContext.level(level);
5✔
733
  }
734

735
  @Override
736
  public void logIdeHomeAndRootStatus() {
737

738
    if (this.ideRoot != null) {
3!
739
      success("IDE_ROOT is set to {}", this.ideRoot);
×
740
    }
741
    if (this.ideHome == null) {
3!
742
      warning(getMessageIdeHomeNotFound());
5✔
743
    } else {
744
      success("IDE_HOME is set to {}", this.ideHome);
×
745
    }
746
  }
1✔
747

748
  @Override
749
  public String askForInput(String message, String defaultValue) {
750

751
    if (!message.isBlank()) {
×
752
      info(message);
×
753
    }
754
    if (isBatchMode()) {
×
755
      if (isForceMode()) {
×
756
        return defaultValue;
×
757
      } else {
758
        throw new CliAbortException();
×
759
      }
760
    }
761
    String input = readLine().trim();
×
762
    return input.isEmpty() ? defaultValue : input;
×
763
  }
764

765
  @Override
766
  public String askForInput(String message) {
767

768
    String input;
769
    do {
770
      info(message);
3✔
771
      input = readLine().trim();
4✔
772
    } while (input.isEmpty());
3!
773

774
    return input;
2✔
775
  }
776

777
  @SuppressWarnings("unchecked")
778
  @Override
779
  public <O> O question(String question, O... options) {
780

781
    assert (options.length >= 2);
×
782
    interaction(question);
×
783
    Map<String, O> mapping = new HashMap<>(options.length);
×
784
    int i = 0;
×
785
    for (O option : options) {
×
786
      i++;
×
787
      String key = "" + option;
×
788
      addMapping(mapping, key, option);
×
789
      String numericKey = Integer.toString(i);
×
790
      if (numericKey.equals(key)) {
×
791
        trace("Options should not be numeric: " + key);
×
792
      } else {
793
        addMapping(mapping, numericKey, option);
×
794
      }
795
      interaction("Option " + numericKey + ": " + key);
×
796
    }
797
    O option = null;
×
798
    if (isBatchMode()) {
×
799
      if (isForceMode()) {
×
800
        option = options[0];
×
801
        interaction("" + option);
×
802
      }
803
    } else {
804
      while (option == null) {
×
805
        String answer = readLine();
×
806
        option = mapping.get(answer);
×
807
        if (option == null) {
×
808
          warning("Invalid answer: '" + answer + "' - please try again.");
×
809
        }
810
      }
×
811
    }
812
    return option;
×
813
  }
814

815
  /**
816
   * @return the input from the end-user (e.g. read from the console).
817
   */
818
  protected abstract String readLine();
819

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

822
    O duplicate = mapping.put(key, option);
×
823
    if (duplicate != null) {
×
824
      throw new IllegalArgumentException("Duplicated option " + key);
×
825
    }
826
  }
×
827

828
  @Override
829
  public Step getCurrentStep() {
830

831
    return this.currentStep;
×
832
  }
833

834
  @Override
835
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
836

837
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
838
    return this.currentStep;
3✔
839
  }
840

841
  /**
842
   * Internal method to end the running {@link Step}.
843
   *
844
   * @param step the current {@link Step} to end.
845
   */
846
  public void endStep(StepImpl step) {
847

848
    if (step == this.currentStep) {
4!
849
      this.currentStep = this.currentStep.getParent();
6✔
850
    } else {
851
      String currentStepName = "null";
×
852
      if (this.currentStep != null) {
×
853
        currentStepName = this.currentStep.getName();
×
854
      }
855
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
856
    }
857
  }
1✔
858

859
  /**
860
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
861
   *
862
   * @param arguments the {@link CliArgument}.
863
   * @return the return code of the execution.
864
   */
865
  public int run(CliArguments arguments) {
866

867
    CliArgument current = arguments.current();
3✔
868
    assert (this.currentStep == null);
4!
869
    boolean supressStepSuccess = false;
2✔
870
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
871
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
872
    Commandlet cmd = null;
2✔
873
    ValidationResult result = null;
2✔
874
    try {
875
      while (commandletIterator.hasNext()) {
3!
876
        cmd = commandletIterator.next();
4✔
877
        result = applyAndRun(arguments.copy(), cmd);
6✔
878
        if (result.isValid()) {
3!
879
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
880
          step.success();
2✔
881
          return ProcessResult.SUCCESS;
4✔
882
        }
883
      }
884
      this.startContext.activateLogging();
×
885
      if (result != null) {
×
886
        error(result.getErrorMessage());
×
887
      }
888
      step.error("Invalid arguments: {}", current.getArgs());
×
889
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
×
890
      if (cmd != null) {
×
891
        help.commandlet.setValue(cmd);
×
892
      }
893
      help.run();
×
894
      return 1;
×
895
    } catch (Throwable t) {
1✔
896
      this.startContext.activateLogging();
3✔
897
      step.error(t, true);
4✔
898
      throw t;
2✔
899
    } finally {
900
      step.close();
2✔
901
      assert (this.currentStep == null);
4!
902
      step.logSummary(supressStepSuccess);
3✔
903
    }
904
  }
905

906
  /**
907
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
908
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
909
   *     {@link Commandlet} did not match and we have to try a different candidate).
910
   */
911
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
912

913
    IdeLogLevel previousLogLevel = null;
2✔
914
    cmd.reset();
2✔
915
    ValidationResult result = apply(arguments, cmd);
5✔
916
    if (result.isValid()) {
3!
917
      result = cmd.validate();
3✔
918
    }
919
    if (result.isValid()) {
3!
920
      debug("Running commandlet {}", cmd);
9✔
921
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
922
        throw new CliException(getMessageIdeHomeNotFound(), ProcessResult.NO_IDE_HOME);
×
923
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6✔
924
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
925
      }
926
      try {
927
        if (cmd.isProcessableOutput()) {
3!
928
          if (!debug().isEnabled()) {
×
929
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
930
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
931
          }
932
          this.startContext.activateLogging();
×
933
        } else {
934
          this.startContext.activateLogging();
3✔
935
          verifyIdeRoot();
2✔
936
          if (cmd.isIdeHomeRequired()) {
3✔
937
            debug(getMessageIdeHomeFound());
4✔
938
          }
939
          Path settingsRepository = getSettingsGitRepository();
3✔
940
          if (settingsRepository != null) {
2!
941
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
942
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
943
                    settingsRepository, getSettingsCommitIdPath()))) {
×
944
              if (isSettingsRepositorySymlinkOrJunction()) {
×
945
                interaction(
×
946
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
947

948
              } else {
949
                interaction(
×
950
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
951
              }
952
            }
953
          }
954
        }
955
        boolean success = ensureLicenseAgreement(cmd);
4✔
956
        if (!success) {
2!
957
          return ValidationResultValid.get();
×
958
        }
959
        cmd.run();
2✔
960
      } finally {
961
        if (previousLogLevel != null) {
2!
962
          this.startContext.setLogLevel(previousLogLevel);
×
963
        }
964
      }
1✔
965
    } else {
966
      trace("Commandlet did not match");
×
967
    }
968
    return result;
2✔
969
  }
970

971
  private boolean ensureLicenseAgreement(Commandlet cmd) {
972

973
    if (isTest()) {
3!
974
      return true; // ignore for tests
2✔
975
    }
976
    getFileAccess().mkdirs(this.userHomeIde);
×
977
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
978
    if (Files.isRegularFile(licenseAgreement)) {
×
979
      return true; // success, license already accepted
×
980
    }
981
    if (cmd instanceof EnvironmentCommandlet) {
×
982
      // 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
983
      // 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
984
      // printing anything anymore in such case.
985
      return false;
×
986
    }
987
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
988
    if (logLevelInfoDisabled) {
×
989
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
990
    }
991
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
992
    if (logLevelInteractionDisabled) {
×
993
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
994
    }
995
    StringBuilder sb = new StringBuilder(1180);
×
996
    sb.append(LOGO).append("""
×
997
        Welcome to IDEasy!
998
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
999
        It supports automatic download and installation of arbitrary 3rd party tools.
1000
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1001
        But if explicitly configured, also commercial software that requires an additional license may be used.
1002
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1003
        You are solely responsible for all risks implied by using this software.
1004
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1005
        You will be able to find it online under the following URL:
1006
        """).append(LICENSE_URL);
×
1007
    if (this.ideRoot != null) {
×
1008
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1009
          append(this.idePath.resolve("IDEasy.pdf").toString()).append("\n");
×
1010
    }
1011
    info(sb.toString());
×
1012
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1013

1014
    sb.setLength(0);
×
1015
    LocalDateTime now = LocalDateTime.now();
×
1016
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1017
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1018
    try {
1019
      Files.writeString(licenseAgreement, sb);
×
1020
    } catch (Exception e) {
×
1021
      throw new RuntimeException("Failed to save license agreement!", e);
×
1022
    }
×
1023
    if (logLevelInfoDisabled) {
×
1024
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1025
    }
1026
    if (logLevelInteractionDisabled) {
×
1027
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1028
    }
1029
    return true;
×
1030
  }
1031

1032
  private void verifyIdeRoot() {
1033

1034
    if (!isTest()) {
3!
1035
      if (this.ideRoot == null) {
×
1036
        warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again.");
×
1037
      } else if (this.ideHome != null) {
×
1038
        Path ideRootPath = getIdeRootPathFromEnv();
×
1039
        if (!this.ideRoot.equals(ideRootPath)) {
×
1040
          warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath,
×
1041
              this.ideHome.getFileName(), this.ideRoot);
×
1042
        }
1043
      }
1044
    }
1045
  }
1✔
1046

1047
  /**
1048
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1049
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1050
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1051
   */
1052
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1053

1054
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1055
    if (arguments.current().isStart()) {
4✔
1056
      arguments.next();
3✔
1057
    }
1058
    if (includeContextOptions) {
2✔
1059
      ContextCommandlet cc = new ContextCommandlet();
4✔
1060
      for (Property<?> property : cc.getProperties()) {
11✔
1061
        assert (property.isOption());
4!
1062
        property.apply(arguments, this, cc, collector);
7✔
1063
      }
1✔
1064
    }
1065
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1066
    CliArgument current = arguments.current();
3✔
1067
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1068
      collector.add(current.get(), null, null, null);
7✔
1069
    }
1070
    arguments.next();
3✔
1071
    while (commandletIterator.hasNext()) {
3✔
1072
      Commandlet cmd = commandletIterator.next();
4✔
1073
      if (!arguments.current().isEnd()) {
4✔
1074
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1075
      }
1076
    }
1✔
1077
    return collector.getSortedCandidates();
3✔
1078
  }
1079

1080
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1081

1082
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1083
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1084
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1085
    List<Property<?>> properties = cmd.getProperties();
3✔
1086
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1087
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1088
    for (Property<?> property : properties) {
10✔
1089
      if (property.isOption()) {
3✔
1090
        optionProperties.add(property);
4✔
1091
      }
1092
    }
1✔
1093
    CliArgument currentArgument = arguments.current();
3✔
1094
    while (!currentArgument.isEnd()) {
3✔
1095
      trace("Trying to match argument '{}'", currentArgument);
9✔
1096
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1097
        if (currentArgument.isCompletion()) {
3✔
1098
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1099
          while (optionIterator.hasNext()) {
3✔
1100
            Property<?> option = optionIterator.next();
4✔
1101
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1102
            if (success) {
2✔
1103
              optionIterator.remove();
2✔
1104
              arguments.next();
3✔
1105
            }
1106
          }
1✔
1107
        } else {
1✔
1108
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1109
          if (option != null) {
2✔
1110
            arguments.next();
3✔
1111
            boolean removed = optionProperties.remove(option);
4✔
1112
            if (!removed) {
2!
1113
              option = null;
×
1114
            }
1115
          }
1116
          if (option == null) {
2✔
1117
            trace("No such option was found.");
3✔
1118
            return;
1✔
1119
          }
1120
        }
1✔
1121
      } else {
1122
        if (valueIterator.hasNext()) {
3✔
1123
          Property<?> valueProperty = valueIterator.next();
4✔
1124
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1125
          if (!success) {
2✔
1126
            trace("Completion cannot match any further.");
3✔
1127
            return;
1✔
1128
          }
1129
        } else {
1✔
1130
          trace("No value left for completion.");
3✔
1131
          return;
1✔
1132
        }
1133
      }
1134
      currentArgument = arguments.current();
4✔
1135
    }
1136
  }
1✔
1137

1138
  /**
1139
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1140
   *     {@link CliArguments#copy() copy} as needed.
1141
   * @param cmd the potential {@link Commandlet} to match.
1142
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1143
   */
1144
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1145

1146
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1147
    CliArgument currentArgument = arguments.current();
3✔
1148
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1149
    Property<?> property = null;
2✔
1150
    if (propertyIterator.hasNext()) {
3!
1151
      property = propertyIterator.next();
4✔
1152
    }
1153
    while (!currentArgument.isEnd()) {
3✔
1154
      trace("Trying to match argument '{}'", currentArgument);
9✔
1155
      Property<?> currentProperty = property;
2✔
1156
      if (!arguments.isEndOptions()) {
3!
1157
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1158
        if (option != null) {
2!
1159
          currentProperty = option;
×
1160
        }
1161
      }
1162
      if (currentProperty == null) {
2!
1163
        trace("No option or next value found");
×
1164
        ValidationState state = new ValidationState(null);
×
1165
        state.addErrorMessage("No matching property found");
×
1166
        return state;
×
1167
      }
1168
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1169
      if (currentProperty == property) {
3!
1170
        if (!property.isMultiValued()) {
3✔
1171
          if (propertyIterator.hasNext()) {
3✔
1172
            property = propertyIterator.next();
5✔
1173
          } else {
1174
            property = null;
2✔
1175
          }
1176
        }
1177
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1178
          arguments.stopSplitShortOptions();
2✔
1179
        }
1180
      }
1181
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1182
      if (!matches) {
2!
1183
        ValidationState state = new ValidationState(null);
×
1184
        state.addErrorMessage("No matching property found");
×
1185
        return state;
×
1186
      }
1187
      currentArgument = arguments.current();
3✔
1188
    }
1✔
1189
    return ValidationResultValid.get();
2✔
1190
  }
1191

1192
  @Override
1193
  public String findBash() {
1194

1195
    String bash = "bash";
2✔
1196
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1197
      bash = findBashOnWindows();
×
1198
    }
1199

1200
    return bash;
2✔
1201
  }
1202

1203
  private String findBashOnWindows() {
1204

1205
    // Check if Git Bash exists in the default location
1206
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1207
    if (Files.exists(defaultPath)) {
×
1208
      return defaultPath.toString();
×
1209
    }
1210

1211
    // If not found in the default location, try the registry query
1212
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1213
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1214
    String regQueryResult;
1215
    for (String bashVariant : bashVariants) {
×
1216
      for (String registryKey : registryKeys) {
×
1217
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1218
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1219

1220
        try {
1221
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1222
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1223
            StringBuilder output = new StringBuilder();
×
1224
            String line;
1225

1226
            while ((line = reader.readLine()) != null) {
×
1227
              output.append(line);
×
1228
            }
1229

1230
            int exitCode = process.waitFor();
×
1231
            if (exitCode != 0) {
×
1232
              return null;
×
1233
            }
1234

1235
            regQueryResult = output.toString();
×
1236
            if (regQueryResult != null) {
×
1237
              int index = regQueryResult.indexOf("REG_SZ");
×
1238
              if (index != -1) {
×
1239
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1240
                return path + "\\bin\\bash.exe";
×
1241
              }
1242
            }
1243

1244
          }
×
1245
        } catch (Exception e) {
×
1246
          return null;
×
1247
        }
×
1248
      }
1249
    }
1250
    // no bash found
1251
    return null;
×
1252
  }
1253

1254
  @Override
1255
  public WindowsPathSyntax getPathSyntax() {
1256

1257
    return this.pathSyntax;
3✔
1258
  }
1259

1260
  /**
1261
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1262
   */
1263
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1264

1265
    this.pathSyntax = pathSyntax;
3✔
1266
  }
1✔
1267

1268
  /**
1269
   * @return the {@link IdeStartContextImpl}.
1270
   */
1271
  public IdeStartContextImpl getStartContext() {
1272

1273
    return startContext;
3✔
1274
  }
1275

1276
  /**
1277
   * @return the {@link WindowsHelper}.
1278
   */
1279
  public final WindowsHelper getWindowsHelper() {
1280

1281
    if (this.windowsHelper == null) {
3✔
1282
      this.windowsHelper = createWindowsHelper();
4✔
1283
    }
1284
    return this.windowsHelper;
3✔
1285
  }
1286

1287
  /**
1288
   * @return the new {@link WindowsHelper} instance.
1289
   */
1290
  protected WindowsHelper createWindowsHelper() {
1291

1292
    return new WindowsHelperImpl(this);
×
1293
  }
1294

1295
  /**
1296
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1297
   */
1298
  public void reload() {
1299

1300
    this.variables = null;
3✔
1301
    this.customToolRepository = null;
3✔
1302
  }
1✔
1303
}
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

© 2025 Coveralls, Inc