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

devonfw / IDEasy / 13788808942

11 Mar 2025 01:08PM UTC coverage: 66.998% (-1.6%) from 68.619%
13788808942

Pull #1017

github

web-flow
Merge 2597874a1 into 2cf68279f
Pull Request #1017: #404: enhance logging with custom slf4j bridge

3034 of 4951 branches covered (61.28%)

Branch coverage included in aggregate %.

7831 of 11266 relevant lines covered (69.51%)

3.03 hits per line

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

60.29
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 Path confPath;
88

89
  protected Path settingsPath;
90

91
  private Path settingsCommitIdPath;
92

93
  protected Path pluginsPath;
94

95
  private Path workspacePath;
96

97
  private String workspaceName;
98

99
  private Path cwd;
100

101
  private Path downloadPath;
102

103
  protected Path userHome;
104

105
  private Path userHomeIde;
106

107
  private SystemPath path;
108

109
  private WindowsPathSyntax pathSyntax;
110

111
  private final SystemInfo systemInfo;
112

113
  private EnvironmentVariables variables;
114

115
  private final FileAccess fileAccess;
116

117
  protected CommandletManager commandletManager;
118

119
  protected ToolRepository defaultToolRepository;
120

121
  private CustomToolRepository customToolRepository;
122

123
  private MavenRepository mavenRepository;
124

125
  private DirectoryMerger workspaceMerger;
126

127
  protected UrlMetadata urlMetadata;
128

129
  protected Path defaultExecutionDirectory;
130

131
  private StepImpl currentStep;
132

133
  protected Boolean online;
134

135
  protected IdeSystem system;
136

137
  private NetworkProxy networkProxy;
138

139
  private WindowsHelper windowsHelper;
140

141
  /** Context used for logging */
142
  public static IdeContext loggingContext;
143

144
  /**
145
   * The constructor.
146
   *
147
   * @param startContext the {@link IdeLogger}.
148
   * @param workingDirectory the optional {@link Path} to current working directory.
149
   */
150
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
151

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

189
    // detection completed, initializing variables
190
    this.ideRoot = findIdeRoot(currentDir);
5✔
191

192
    setCwd(workingDirectory, workspace, currentDir);
5✔
193

194
    if (this.ideRoot != null) {
3✔
195
      Path tempDownloadPath = getTempDownloadPath();
3✔
196
      if (Files.isDirectory(tempDownloadPath)) {
6✔
197
        // TODO delete all files older than 1 day here...
198
      } else {
199
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
200
      }
201
    }
202

203
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
204
    loggingContext = this;
2✔
205
    this.mavenRepository = new MavenRepository(this);
6✔
206
  }
1✔
207

208
  private Path findIdeRoot(Path ideHomePath) {
209

210
    Path ideRootPath = null;
2✔
211
    if (ideHomePath != null) {
2✔
212
      ideRootPath = ideHomePath.getParent();
4✔
213
    } else if (!isTest()) {
3!
214
      ideRootPath = getIdeRootPathFromEnv();
×
215
    }
216
    return ideRootPath;
2✔
217
  }
218

219
  /**
220
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
221
   */
222
  protected Path getIdeRootPathFromEnv() {
223

224
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
225
    if (root != null) {
×
226
      Path rootPath = Path.of(root);
×
227
      if (Files.isDirectory(rootPath)) {
×
228
        return rootPath;
×
229
      }
230
    }
231
    return null;
×
232
  }
233

234
  @Override
235
  public void setCwd(Path userDir, String workspace, Path ideHome) {
236

237
    this.cwd = userDir;
3✔
238
    this.workspaceName = workspace;
3✔
239
    this.ideHome = ideHome;
3✔
240
    if (ideHome == null) {
2✔
241
      this.workspacePath = null;
3✔
242
      this.confPath = null;
3✔
243
      this.settingsPath = null;
3✔
244
      this.pluginsPath = null;
4✔
245
    } else {
246
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
247
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
248
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
249
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
250
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
251
    }
252
    if (isTest()) {
3!
253
      // only for testing...
254
      if (this.ideHome == null) {
3✔
255
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
256
      } else {
257
        this.userHome = this.ideHome.resolve("home");
7✔
258
      }
259
    } else {
260
      this.userHome = Path.of(getSystem().getProperty("user.home"));
×
261
    }
262
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
263
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
264

265
    this.path = computeSystemPath();
4✔
266
  }
1✔
267

268
  private String getMessageIdeHomeFound() {
269

270
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
271
  }
272

273
  private String getMessageNotInsideIdeProject() {
274

275
    return "You are not inside an IDE project: " + this.cwd;
5✔
276
  }
277

278
  private String getMessageIdeRootNotFound() {
279

280
    String root = getSystem().getEnv("IDE_ROOT");
5✔
281
    if (root == null) {
2!
282
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
283
    } else {
284
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
285
    }
286
  }
287

288
  /**
289
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
290
   */
291
  public boolean isTest() {
292

293
    return false;
×
294
  }
295

296
  protected SystemPath computeSystemPath() {
297

298
    return new SystemPath(this);
×
299
  }
300

301
  private boolean isIdeHome(Path dir) {
302

303
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
304
      return false;
2✔
305
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
306
      return false;
×
307
    }
308
    return true;
2✔
309
  }
310

311
  private EnvironmentVariables createVariables() {
312

313
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
314
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
315
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
316
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
317
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
318
    return conf.resolved();
3✔
319
  }
320

321
  protected AbstractEnvironmentVariables createSystemVariables() {
322

323
    return EnvironmentVariables.ofSystem(this);
3✔
324
  }
325

326
  @Override
327
  public SystemInfo getSystemInfo() {
328

329
    return this.systemInfo;
3✔
330
  }
331

332
  @Override
333
  public FileAccess getFileAccess() {
334

335
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
336
    configureNetworkProxy();
2✔
337
    return this.fileAccess;
3✔
338
  }
339

340
  @Override
341
  public CommandletManager getCommandletManager() {
342

343
    return this.commandletManager;
3✔
344
  }
345

346
  @Override
347
  public ToolRepository getDefaultToolRepository() {
348

349
    return this.defaultToolRepository;
3✔
350
  }
351

352
  @Override
353
  public MavenRepository getMavenToolRepository() {
354

355
    return this.mavenRepository;
3✔
356
  }
357

358
  @Override
359
  public CustomToolRepository getCustomToolRepository() {
360

361
    if (this.customToolRepository == null) {
3!
362
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
363
    }
364
    return this.customToolRepository;
3✔
365
  }
366

367
  @Override
368
  public Path getIdeHome() {
369

370
    return this.ideHome;
3✔
371
  }
372

373
  @Override
374
  public String getProjectName() {
375

376
    if (this.ideHome != null) {
3!
377
      return this.ideHome.getFileName().toString();
5✔
378
    }
379
    return "";
×
380
  }
381

382
  @Override
383
  public VersionIdentifier getProjectVersion() {
384

385
    if (this.ideHome != null) {
3!
386
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
387
      if (Files.exists(versionFile)) {
5✔
388
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
389
        return VersionIdentifier.of(version);
3✔
390
      }
391
    }
392
    return IdeMigrator.START_VERSION;
2✔
393
  }
394

395
  @Override
396
  public void setProjectVersion(VersionIdentifier version) {
397

398
    if (this.ideHome == null) {
3!
399
      throw new IllegalStateException("IDE_HOME not available!");
×
400
    }
401
    Objects.requireNonNull(version);
3✔
402
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
403
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
404
  }
1✔
405

406
  @Override
407
  public Path getIdeRoot() {
408

409
    return this.ideRoot;
3✔
410
  }
411

412
  @Override
413
  public Path getIdePath() {
414

415
    Path myIdeRoot = getIdeRoot();
3✔
416
    if (myIdeRoot == null) {
2!
417
      return null;
×
418
  }
419
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
420
  }
421

422
  @Override
423
  public Path getCwd() {
424

425
    return this.cwd;
3✔
426
  }
427

428
  @Override
429
  public Path getTempPath() {
430

431
    Path idePath = getIdePath();
3✔
432
    if (idePath == null) {
2!
433
      return null;
×
434
  }
435
    return idePath.resolve("tmp");
4✔
436
  }
437

438
  @Override
439
  public Path getTempDownloadPath() {
440

441
    Path tmp = getTempPath();
3✔
442
    if (tmp == null) {
2!
443
      return null;
×
444
  }
445
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
446
  }
447

448
  @Override
449
  public Path getUserHome() {
450

451
    return this.userHome;
3✔
452
  }
453

454
  @Override
455
  public Path getUserHomeIde() {
456

457
    return this.userHomeIde;
3✔
458
  }
459

460
  @Override
461
  public Path getSettingsPath() {
462

463
    return this.settingsPath;
3✔
464
  }
465

466
  @Override
467
  public Path getSettingsGitRepository() {
468

469
    Path settingsPath = getSettingsPath();
3✔
470
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
471
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
472
      error("Settings repository exists but is not a git repository.");
3✔
473
      return null;
2✔
474
    }
475
    return settingsPath;
2✔
476
  }
477

478
  @Override
479
  public boolean isSettingsRepositorySymlinkOrJunction() {
480

481
    Path settingsPath = getSettingsPath();
3✔
482
    if (settingsPath == null) {
2!
483
      return false;
×
484
    }
485
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
486
  }
487

488
  @Override
489
  public Path getSettingsCommitIdPath() {
490

491
    return this.settingsCommitIdPath;
3✔
492
  }
493

494
  @Override
495
  public Path getConfPath() {
496

497
    return this.confPath;
3✔
498
  }
499

500
  @Override
501
  public Path getSoftwarePath() {
502

503
    if (this.ideHome == null) {
3✔
504
      return null;
2✔
505
  }
506
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
507
  }
508

509
  @Override
510
  public Path getSoftwareExtraPath() {
511

512
    Path softwarePath = getSoftwarePath();
3✔
513
    if (softwarePath == null) {
2!
514
      return null;
×
515
  }
516
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
517
  }
518

519
  @Override
520
  public Path getSoftwareRepositoryPath() {
521

522
    Path idePath = getIdePath();
3✔
523
    if (idePath == null) {
2!
524
      return null;
×
525
  }
526
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
527
  }
528

529
  @Override
530
  public Path getPluginsPath() {
531

532
    return this.pluginsPath;
3✔
533
  }
534

535
  @Override
536
  public String getWorkspaceName() {
537

538
    return this.workspaceName;
3✔
539
  }
540

541
  @Override
542
  public Path getWorkspacePath() {
543

544
    return this.workspacePath;
3✔
545
  }
546

547
  @Override
548
  public Path getDownloadPath() {
549

550
    return this.downloadPath;
3✔
551
  }
552

553
  @Override
554
  public Path getUrlsPath() {
555

556
    Path idePath = getIdePath();
3✔
557
    if (idePath == null) {
2!
558
      return null;
×
559
  }
560
    return idePath.resolve(FOLDER_URLS);
4✔
561
  }
562

563
  @Override
564
  public Path getToolRepositoryPath() {
565

566
    Path idePath = getIdePath();
3✔
567
    if (idePath == null) {
2!
568
      return null;
×
569
  }
570
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
571
  }
572

573
  @Override
574
  public SystemPath getPath() {
575

576
    return this.path;
3✔
577
  }
578

579
  @Override
580
  public EnvironmentVariables getVariables() {
581

582
    if (this.variables == null) {
3✔
583
      this.variables = createVariables();
4✔
584
    }
585
    return this.variables;
3✔
586
  }
587

588
  @Override
589
  public UrlMetadata getUrls() {
590

591
    if (this.urlMetadata == null) {
3✔
592
      if (!isTest()) {
3!
593
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
594
      }
595
      this.urlMetadata = new UrlMetadata(this);
6✔
596
    }
597
    return this.urlMetadata;
3✔
598
  }
599

600
  @Override
601
  public boolean isQuietMode() {
602

603
    return this.startContext.isQuietMode();
4✔
604
  }
605

606
  @Override
607
  public boolean isBatchMode() {
608

609
    return this.startContext.isBatchMode();
×
610
  }
611

612
  @Override
613
  public boolean isForceMode() {
614

615
    return this.startContext.isForceMode();
4✔
616
  }
617

618
  @Override
619
  public boolean isOfflineMode() {
620

621
    return this.startContext.isOfflineMode();
4✔
622
  }
623

624
  @Override
625
  public boolean isSkipUpdatesMode() {
626

627
    return this.startContext.isSkipUpdatesMode();
4✔
628
  }
629

630
  @Override
631
  public boolean isOnline() {
632

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

652
  private void configureNetworkProxy() {
653

654
    if (this.networkProxy == null) {
3✔
655
      this.networkProxy = new NetworkProxy(this);
6✔
656
      this.networkProxy.configure();
3✔
657
    }
658
  }
1✔
659

660
  @Override
661
  public Locale getLocale() {
662

663
    Locale locale = this.startContext.getLocale();
4✔
664
    if (locale == null) {
2✔
665
      locale = Locale.getDefault();
2✔
666
    }
667
    return locale;
2✔
668
  }
669

670
  @Override
671
  public DirectoryMerger getWorkspaceMerger() {
672

673
    if (this.workspaceMerger == null) {
3✔
674
      this.workspaceMerger = new DirectoryMerger(this);
6✔
675
    }
676
    return this.workspaceMerger;
3✔
677
  }
678

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

685
    return this.defaultExecutionDirectory;
×
686
  }
687

688
  /**
689
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
690
   */
691
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
692

693
    if (defaultExecutionDirectory != null) {
×
694
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
695
    }
696
  }
×
697

698
  @Override
699
  public GitContext getGitContext() {
700

701
    return new GitContextImpl(this);
×
702
  }
703

704
  @Override
705
  public ProcessContext newProcess() {
706

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

714
  @Override
715
  public IdeSystem getSystem() {
716

717
    if (this.system == null) {
×
718
      this.system = new IdeSystemImpl(this);
×
719
    }
720
    return this.system;
×
721
  }
722

723
  /**
724
   * @return a new instance of {@link ProcessContext}.
725
   * @see #newProcess()
726
   */
727
  protected ProcessContext createProcessContext() {
728

729
    return new ProcessContextImpl(this);
5✔
730
  }
731

732
  @Override
733
  public IdeSubLogger level(IdeLogLevel level) {
734

735
    return this.startContext.level(level);
5✔
736
  }
737

738
  @Override
739
  public void logIdeHomeAndRootStatus() {
740

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

751
  @Override
752
  public String askForInput(String message, String defaultValue) {
753

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

768
  @Override
769
  public String askForInput(String message) {
770

771
    String input;
772
    do {
773
      info(message);
3✔
774
      input = readLine().trim();
4✔
775
    } while (input.isEmpty());
3!
776

777
    return input;
2✔
778
  }
779

780
  @SuppressWarnings("unchecked")
781
  @Override
782
  public <O> O question(String question, O... options) {
783

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

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

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

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

831
  @Override
832
  public Step getCurrentStep() {
833

834
    return this.currentStep;
×
835
  }
836

837
  @Override
838
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
839

840
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
841
    return this.currentStep;
3✔
842
  }
843

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

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

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

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

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

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

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

974
  private boolean ensureLicenseAgreement(Commandlet cmd) {
975

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

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

1035
  private void verifyIdeRoot() {
1036

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

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

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

1083
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1084

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

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

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

1195
  @Override
1196
  public String findBash() {
1197

1198
    String bash = "bash";
2✔
1199
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1200
      bash = findBashOnWindows();
×
1201
    }
1202

1203
    return bash;
2✔
1204
  }
1205

1206
  private String findBashOnWindows() {
1207

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

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

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

1229
            while ((line = reader.readLine()) != null) {
×
1230
              output.append(line);
×
1231
            }
1232

1233
            int exitCode = process.waitFor();
×
1234
            if (exitCode != 0) {
×
1235
              return null;
×
1236
            }
1237

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

1247
          }
×
1248
        } catch (Exception e) {
×
1249
          return null;
×
1250
        }
×
1251
      }
1252
    }
1253
    // no bash found
1254
    return null;
×
1255
  }
1256

1257
  @Override
1258
  public WindowsPathSyntax getPathSyntax() {
1259

1260
    return this.pathSyntax;
3✔
1261
  }
1262

1263
  /**
1264
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1265
   */
1266
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1267

1268
    this.pathSyntax = pathSyntax;
3✔
1269
  }
1✔
1270

1271
  /**
1272
   * @return the {@link IdeStartContextImpl}.
1273
   */
1274
  public IdeStartContextImpl getStartContext() {
1275

1276
    return startContext;
3✔
1277
  }
1278

1279
  /**
1280
   * @return the {@link WindowsHelper}.
1281
   */
1282
  public final WindowsHelper getWindowsHelper() {
1283

1284
    if (this.windowsHelper == null) {
3✔
1285
      this.windowsHelper = createWindowsHelper();
4✔
1286
    }
1287
    return this.windowsHelper;
3✔
1288
  }
1289

1290
  /**
1291
   * @return the new {@link WindowsHelper} instance.
1292
   */
1293
  protected WindowsHelper createWindowsHelper() {
1294

1295
    return new WindowsHelperImpl(this);
×
1296
  }
1297

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

1303
    this.variables = null;
3✔
1304
    this.customToolRepository = null;
3✔
1305
  }
1✔
1306
}
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