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

devonfw / IDEasy / 13440922988

20 Feb 2025 05:16PM UTC coverage: 67.945% (-0.01%) from 67.959%
13440922988

push

github

web-flow
#1053: fix setup #1044: setup rewritten in Java (#1055)

Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>

2992 of 4849 branches covered (61.7%)

Branch coverage included in aggregate %.

7765 of 10983 relevant lines covered (70.7%)

3.08 hits per line

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

59.75
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
  /**
142
   * The constructor.
143
   *
144
   * @param startContext the {@link IdeLogger}.
145
   * @param workingDirectory the optional {@link Path} to current working directory.
146
   */
147
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
148

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

186
    // detection completed, initializing variables
187
    this.ideRoot = findIdeRoot(currentDir);
5✔
188

189
    setCwd(workingDirectory, workspace, currentDir);
5✔
190

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

200
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
201
    this.mavenRepository = new MavenRepository(this);
6✔
202
  }
1✔
203

204
  private Path findIdeRoot(Path ideHomePath) {
205

206
    Path ideRootPath = null;
2✔
207
    if (ideHomePath != null) {
2✔
208
      ideRootPath = ideHomePath.getParent();
4✔
209
    } else if (!isTest()) {
3!
210
      ideRootPath = getIdeRootPathFromEnv();
×
211
    }
212
    return ideRootPath;
2✔
213
  }
214

215
  /**
216
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
217
   */
218
  protected Path getIdeRootPathFromEnv() {
219

220
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
221
    if (root != null) {
×
222
      Path rootPath = Path.of(root);
×
223
      if (Files.isDirectory(rootPath)) {
×
224
        return rootPath;
×
225
      }
226
    }
227
    return null;
×
228
  }
229

230
  @Override
231
  public void setCwd(Path userDir, String workspace, Path ideHome) {
232

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

261
    this.path = computeSystemPath();
4✔
262
  }
1✔
263

264
  private String getMessageIdeHomeFound() {
265

266
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
267
  }
268

269
  private String getMessageIdeHomeNotFound() {
270

271
    return "You are not inside an IDE installation: " + this.cwd;
5✔
272
  }
273

274
  private String getMessageIdeRootNotFound() {
275

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

284
  /**
285
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
286
   */
287
  public boolean isTest() {
288

289
    return false;
×
290
  }
291

292
  protected SystemPath computeSystemPath() {
293

294
    return new SystemPath(this);
×
295
  }
296

297
  private boolean isIdeHome(Path dir) {
298

299
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
300
      return false;
2✔
301
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
302
      return false;
×
303
    }
304
    return true;
2✔
305
  }
306

307
  private EnvironmentVariables createVariables() {
308

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

317
  protected AbstractEnvironmentVariables createSystemVariables() {
318

319
    return EnvironmentVariables.ofSystem(this);
3✔
320
  }
321

322
  @Override
323
  public SystemInfo getSystemInfo() {
324

325
    return this.systemInfo;
3✔
326
  }
327

328
  @Override
329
  public FileAccess getFileAccess() {
330

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

336
  @Override
337
  public CommandletManager getCommandletManager() {
338

339
    return this.commandletManager;
3✔
340
  }
341

342
  @Override
343
  public ToolRepository getDefaultToolRepository() {
344

345
    return this.defaultToolRepository;
3✔
346
  }
347

348
  @Override
349
  public MavenRepository getMavenToolRepository() {
350

351
    return this.mavenRepository;
3✔
352
  }
353

354
  @Override
355
  public CustomToolRepository getCustomToolRepository() {
356

357
    if (this.customToolRepository == null) {
3!
358
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
359
    }
360
    return this.customToolRepository;
3✔
361
  }
362

363
  @Override
364
  public Path getIdeHome() {
365

366
    return this.ideHome;
3✔
367
  }
368

369
  @Override
370
  public String getProjectName() {
371

372
    if (this.ideHome != null) {
3!
373
      return this.ideHome.getFileName().toString();
5✔
374
    }
375
    return "";
×
376
  }
377

378
  @Override
379
  public VersionIdentifier getProjectVersion() {
380

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

391
  @Override
392
  public void setProjectVersion(VersionIdentifier version) {
393

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

402
  @Override
403
  public Path getIdeRoot() {
404

405
    return this.ideRoot;
3✔
406
  }
407

408
  @Override
409
  public Path getIdePath() {
410

411
    Path myIdeRoot = getIdeRoot();
3✔
412
    if (myIdeRoot == null) {
2!
413
      return null;
×
414
    }
415
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
416
  }
417

418
  @Override
419
  public Path getCwd() {
420

421
    return this.cwd;
3✔
422
  }
423

424
  @Override
425
  public Path getTempPath() {
426

427
    Path idePath = getIdePath();
3✔
428
    if (idePath == null) {
2!
429
      return null;
×
430
    }
431
    return idePath.resolve("tmp");
4✔
432
  }
433

434
  @Override
435
  public Path getTempDownloadPath() {
436

437
    Path tmp = getTempPath();
3✔
438
    if (tmp == null) {
2!
439
      return null;
×
440
    }
441
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
442
  }
443

444
  @Override
445
  public Path getUserHome() {
446

447
    return this.userHome;
3✔
448
  }
449

450
  @Override
451
  public Path getUserHomeIde() {
452

453
    return this.userHomeIde;
3✔
454
  }
455

456
  @Override
457
  public Path getSettingsPath() {
458

459
    return this.settingsPath;
3✔
460
  }
461

462
  @Override
463
  public Path getSettingsGitRepository() {
464

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

474
  @Override
475
  public boolean isSettingsRepositorySymlinkOrJunction() {
476

477
    Path settingsPath = getSettingsPath();
3✔
478
    if (settingsPath == null) {
2!
479
      return false;
×
480
    }
481
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
482
  }
483

484
  @Override
485
  public Path getSettingsCommitIdPath() {
486

487
    return this.settingsCommitIdPath;
3✔
488
  }
489

490
  @Override
491
  public Path getConfPath() {
492

493
    return this.confPath;
3✔
494
  }
495

496
  @Override
497
  public Path getSoftwarePath() {
498

499
    if (this.ideHome == null) {
3✔
500
      return null;
2✔
501
    }
502
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
503
  }
504

505
  @Override
506
  public Path getSoftwareExtraPath() {
507

508
    Path softwarePath = getSoftwarePath();
3✔
509
    if (softwarePath == null) {
2!
510
      return null;
×
511
    }
512
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
513
  }
514

515
  @Override
516
  public Path getSoftwareRepositoryPath() {
517

518
    Path idePath = getIdePath();
3✔
519
    if (idePath == null) {
2!
520
      return null;
×
521
    }
522
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
523
  }
524

525
  @Override
526
  public Path getPluginsPath() {
527

528
    return this.pluginsPath;
3✔
529
  }
530

531
  @Override
532
  public String getWorkspaceName() {
533

534
    return this.workspaceName;
3✔
535
  }
536

537
  @Override
538
  public Path getWorkspacePath() {
539

540
    return this.workspacePath;
3✔
541
  }
542

543
  @Override
544
  public Path getDownloadPath() {
545

546
    return this.downloadPath;
3✔
547
  }
548

549
  @Override
550
  public Path getUrlsPath() {
551

552
    Path idePath = getIdePath();
3✔
553
    if (idePath == null) {
2!
554
      return null;
×
555
    }
556
    return idePath.resolve(FOLDER_URLS);
4✔
557
  }
558

559
  @Override
560
  public Path getToolRepositoryPath() {
561

562
    Path idePath = getIdePath();
3✔
563
    if (idePath == null) {
2!
564
      return null;
×
565
    }
566
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
567
  }
568

569
  @Override
570
  public SystemPath getPath() {
571

572
    return this.path;
3✔
573
  }
574

575
  @Override
576
  public EnvironmentVariables getVariables() {
577

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

584
  @Override
585
  public UrlMetadata getUrls() {
586

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

596
  @Override
597
  public boolean isQuietMode() {
598

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

602
  @Override
603
  public boolean isBatchMode() {
604

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

608
  @Override
609
  public boolean isForceMode() {
610

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

614
  @Override
615
  public boolean isOfflineMode() {
616

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

620
  @Override
621
  public boolean isSkipUpdatesMode() {
622

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

626
  @Override
627
  public boolean isOnline() {
628

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

648
  private void configureNetworkProxy() {
649

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

656
  @Override
657
  public Locale getLocale() {
658

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

666
  @Override
667
  public DirectoryMerger getWorkspaceMerger() {
668

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

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

681
    return this.defaultExecutionDirectory;
×
682
  }
683

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

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

694
  @Override
695
  public GitContext getGitContext() {
696

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

700
  @Override
701
  public ProcessContext newProcess() {
702

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

710
  @Override
711
  public IdeSystem getSystem() {
712

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

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

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

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

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

734
  @Override
735
  public void logIdeHomeAndRootStatus() {
736

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

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

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

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

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

773
    return input;
2✔
774
  }
775

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

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

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

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

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

827
  @Override
828
  public Step getCurrentStep() {
829

830
    return this.currentStep;
×
831
  }
832

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

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

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

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

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

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

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

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

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

970
  private boolean ensureLicenseAgreement(Commandlet cmd) {
971

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

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

1031
  private void verifyIdeRoot() {
1032

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

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

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

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

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

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

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

1191
  @Override
1192
  public String findBash() {
1193

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

1199
    return bash;
2✔
1200
  }
1201

1202
  private String findBashOnWindows() {
1203

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

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

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

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

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

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

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

1253
  @Override
1254
  public WindowsPathSyntax getPathSyntax() {
1255

1256
    return this.pathSyntax;
3✔
1257
  }
1258

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

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

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

1272
    return startContext;
3✔
1273
  }
1274

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

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

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

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

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

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