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

devonfw / IDEasy / 19640865660

24 Nov 2025 04:05PM UTC coverage: 69.024% (+0.1%) from 68.924%
19640865660

push

github

web-flow
#1561: Fix BASH_PATH not used properly (#1577)

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

3562 of 5651 branches covered (63.03%)

Branch coverage included in aggregate %.

9262 of 12928 relevant lines covered (71.64%)

3.15 hits per line

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

49.62
cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java
1
package com.devonfw.tools.ide.git;
2

3
import java.io.IOException;
4
import java.nio.file.Files;
5
import java.nio.file.Path;
6
import java.util.ArrayList;
7
import java.util.List;
8
import java.util.Objects;
9

10
import com.devonfw.tools.ide.cli.CliException;
11
import com.devonfw.tools.ide.context.IdeContext;
12
import com.devonfw.tools.ide.os.SystemInfoImpl;
13
import com.devonfw.tools.ide.process.ProcessContext;
14
import com.devonfw.tools.ide.process.ProcessErrorHandling;
15
import com.devonfw.tools.ide.process.ProcessMode;
16
import com.devonfw.tools.ide.process.ProcessResult;
17
import com.devonfw.tools.ide.variable.IdeVariables;
18

19
/**
20
 * Implements the {@link GitContext}.
21
 */
22
public class GitContextImpl implements GitContext {
23

24
  /** @see #getContext() */
25
  protected final IdeContext context;
26
  private Path git;
27

28
  /**
29
   * @param context the {@link IdeContext context}.
30
   */
31
  public GitContextImpl(IdeContext context) {
2✔
32

33
    this.context = context;
3✔
34
  }
1✔
35

36
  @Override
37
  public void pullOrCloneIfNeeded(GitUrl gitUrl, Path repository) {
38

39
    GitOperation.PULL_OR_CLONE.executeIfNeeded(this.context, gitUrl, repository, null);
8✔
40
  }
1✔
41

42
  @Override
43
  public boolean fetchIfNeeded(Path repository) {
44

45
    return fetchIfNeeded(repository, null, null);
×
46
  }
47

48
  @Override
49
  public boolean fetchIfNeeded(Path repository, String remote, String branch) {
50

51
    return GitOperation.FETCH.executeIfNeeded(this.context, new GitUrl("https://dummy.url/repo.git", branch), repository, remote);
12✔
52
  }
53

54
  @Override
55
  public boolean isRepositoryUpdateAvailable(Path repository) {
56

57
    String localFailureMessage = String.format("Failed to get the local commit id of settings repository '%s'.", repository);
9✔
58
    String remoteFailureMessage = String.format("Failed to get the remote commit id of settings repository '%s', missing remote upstream branch?", repository);
9✔
59
    String localCommitId = runGitCommandAndGetSingleOutput(localFailureMessage, repository, ProcessMode.DEFAULT_CAPTURE, "rev-parse", "HEAD");
16✔
60
    String remoteCommitId = runGitCommandAndGetSingleOutput(remoteFailureMessage, repository, ProcessMode.DEFAULT_CAPTURE, "rev-parse", "@{u}");
16✔
61
    if ((localCommitId == null) || (remoteCommitId == null)) {
2!
62
      return false;
2✔
63
    }
64
    return !localCommitId.equals(remoteCommitId);
×
65
  }
66

67
  @Override
68
  public boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath) {
69

70
    String trackedCommitId;
71
    try {
72
      trackedCommitId = Files.readString(trackedCommitIdPath);
×
73
    } catch (IOException e) {
×
74
      return false;
×
75
    }
×
76
    String remoteFailureMessage = String.format("Failed to get the remote commit id of settings repository '%s', missing remote upstream branch?", repository);
×
77
    String remoteCommitId = runGitCommandAndGetSingleOutput(remoteFailureMessage, repository, ProcessMode.DEFAULT_CAPTURE, "rev-parse", "@{u}");
×
78
    return !trackedCommitId.equals(remoteCommitId);
×
79
  }
80

81
  @Override
82
  public void pullOrCloneAndResetIfNeeded(GitUrl gitUrl, Path repository, String remoteName) {
83

84
    pullOrCloneIfNeeded(gitUrl, repository);
4✔
85
    reset(repository, gitUrl.branch(), remoteName);
6✔
86
    cleanup(repository);
3✔
87
  }
1✔
88

89
  @Override
90
  public void pullOrClone(GitUrl gitUrl, Path repository) {
91

92
    Objects.requireNonNull(repository);
3✔
93
    Objects.requireNonNull(gitUrl);
3✔
94
    if (Files.isDirectory(repository.resolve(GIT_FOLDER))) {
7✔
95
      // checks for remotes
96
      String remote = determineRemote(repository);
4✔
97
      if (remote == null) {
2!
98
        String message = repository + " is a local git repository with no remote - if you did this for testing, you may continue...\n"
×
99
            + "Do you want to ignore the problem and continue anyhow?";
100
        this.context.askToContinue(message);
×
101
      } else {
×
102
        pull(repository);
3✔
103
      }
104
    } else {
1✔
105
      clone(gitUrl, repository);
4✔
106
    }
107
  }
1✔
108

109
  /**
110
   * Handles errors which occurred during git pull.
111
   *
112
   * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
113
   *     git will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
114
   * @param result the {@link ProcessResult} to evaluate.
115
   */
116
  private void handleErrors(Path targetRepository, ProcessResult result) {
117

118
    if (!result.isSuccessful()) {
×
119
      String message = "Failed to update git repository at " + targetRepository;
×
120
      if (this.context.isOfflineMode()) {
×
121
        this.context.warning(message);
×
122
        this.context.interaction("Continuing as we are in offline mode - results may be outdated!");
×
123
      } else {
124
        this.context.error(message);
×
125
        if (this.context.isOnline()) {
×
126
          this.context.error("See above error for details. If you have local changes, please stash or revert and retry.");
×
127
        } else {
128
          this.context.error("It seems you are offline - please ensure Internet connectivity and retry or activate offline mode (-o or --offline).");
×
129
        }
130
        this.context.askToContinue("Typically you should abort and fix the problem. Do you want to continue anyways?");
×
131
      }
132
    }
133
  }
×
134

135
  @Override
136
  public void clone(GitUrl gitUrl, Path repository) {
137

138
    GitUrlSyntax gitUrlSyntax = IdeVariables.PREFERRED_GIT_PROTOCOL.get(getContext());
6✔
139
    gitUrl = gitUrlSyntax.format(gitUrl);
4✔
140
    if (this.context.isOfflineMode()) {
4✔
141
      this.context.requireOnline("git clone of " + gitUrl, false);
×
142
    }
143
    this.context.getFileAccess().mkdirs(repository);
5✔
144
    List<String> args = new ArrayList<>(7);
5✔
145
    args.add("clone");
4✔
146
    if (this.context.isQuietMode()) {
4!
147
      args.add("-q");
×
148
    }
149
    args.add("--recursive");
4✔
150
    args.add(gitUrl.url());
5✔
151
    args.add("--config");
4✔
152
    args.add("core.autocrlf=false");
4✔
153
    args.add(".");
4✔
154
    runGitCommand(repository, args);
4✔
155
    String branch = gitUrl.branch();
3✔
156
    if (branch != null) {
2!
157
      runGitCommand(repository, "switch", branch);
×
158
    }
159
  }
1✔
160

161
  @Override
162
  public void pull(Path repository) {
163

164
    if (this.context.isOffline()) {
4!
165
      this.context.info("Skipping git pull on {} because offline", repository);
×
166
      return;
×
167
    }
168
    ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT, "--no-pager", "pull", "--quiet");
19✔
169
    if (!result.isSuccessful()) {
3!
170
      String branchName = determineCurrentBranch(repository);
×
171
      this.context.warning("Git pull on branch {} failed for repository {}.", branchName, repository);
×
172
      handleErrors(repository, result);
×
173
    }
174
  }
1✔
175

176
  @Override
177
  public void fetch(Path repository, String remote, String branch) {
178

179
    if (branch == null) {
2!
180
      branch = determineCurrentBranch(repository);
×
181
    }
182
    if (remote == null) {
2!
183
      remote = determineRemote(repository);
×
184
    }
185

186
    ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "fetch", Objects.requireNonNullElse(remote, "origin"), branch);
22✔
187

188
    if (!result.isSuccessful()) {
3!
189
      this.context.warning("Git fetch for '{}/{} failed.'.", remote, branch);
×
190
    }
191
  }
1✔
192

193
  @Override
194
  public String determineCurrentBranch(Path repository) {
195

196
    return runGitCommandAndGetSingleOutput("Failed to determine current branch of git repository", repository, "branch", "--show-current");
×
197
  }
198

199
  @Override
200
  public String determineRemote(Path repository) {
201

202
    return runGitCommandAndGetSingleOutput("Failed to determine current origin of git repository.", repository, "remote");
11✔
203
  }
204

205
  @Override
206
  public void reset(Path repository, String branchName, String remoteName) {
207

208
    if ((remoteName == null) || remoteName.isEmpty()) {
5!
209
      remoteName = DEFAULT_REMOTE;
2✔
210
    }
211
    if ((branchName == null) || branchName.isEmpty()) {
5!
212
      branchName = GitUrl.BRANCH_MASTER;
2✔
213
    }
214
    ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT, "diff-index", "--quiet", "HEAD");
19✔
215
    if (!result.isSuccessful()) {
3!
216
      // reset to origin/master
217
      this.context.warning("Git has detected modified files -- attempting to reset {} to '{}/{}'.", repository, remoteName, branchName);
18✔
218
      result = runGitCommand(repository, ProcessMode.DEFAULT, "reset", "--hard", remoteName + "/" + branchName);
21✔
219
      if (!result.isSuccessful()) {
3!
220
        this.context.warning("Git failed to reset {} to '{}/{}'.", remoteName, branchName, repository);
×
221
        handleErrors(repository, result);
×
222
      }
223
    }
224
  }
1✔
225

226
  @Override
227
  public void cleanup(Path repository) {
228

229
    // check for untracked files
230
    ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "ls-files", "--other", "--directory", "--exclude-standard");
23✔
231
    if (!result.getOut().isEmpty()) {
4✔
232
      // delete untracked files
233
      this.context.warning("Git detected untracked files in {} and is attempting a cleanup.", repository);
10✔
234
      runGitCommand(repository, "clean", "-df");
13✔
235
    }
236
  }
1✔
237

238
  @Override
239
  public String retrieveGitUrl(Path repository) {
240

241
    return runGitCommandAndGetSingleOutput("Failed to retrieve git URL for repository", repository, "config", "--get", "remote.origin.url");
×
242
  }
243

244
  IdeContext getContext() {
245

246
    return this.context;
3✔
247
  }
248

249
  @Override
250
  public Path findGitRequired() {
251

252
    Path gitPath = findGit();
×
253
    if (gitPath == null) {
×
254
      String message = "Git " + IdeContext.IS_NOT_INSTALLED_BUT_REQUIRED;
×
255
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
256
        message += IdeContext.PLEASE_DOWNLOAD_AND_INSTALL_GIT + ":\n "
×
257
            + IdeContext.WINDOWS_GIT_DOWNLOAD_URL;
258
      }
259
      throw new CliException(message);
×
260
    }
261
    return gitPath;
×
262
  }
263

264
  /**
265
   * Finds the path to the Git executable.
266
   *
267
   * @return the {@link Path} to the Git executable, or {@code null} if Git is not found.
268
   */
269
  public Path findGit() {
270
    if (this.git != null) {
×
271
      return this.git;
×
272
    }
273

274
    Path gitPath = findGitInPath(Path.of("git"));
×
275

276
    if (gitPath == null) {
×
277
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
278
        gitPath = findGitOnWindowsViaBash();
×
279
      }
280
    }
281

282
    if (gitPath != null) {
×
283
      this.git = gitPath;
×
284
      this.context.trace("Found git at: {}", gitPath);
×
285
    }
286

287
    return gitPath;
×
288
  }
289

290
  private Path findGitOnWindowsViaBash() {
291
    Path gitPath;
292
    Path bashBinary = this.context.findBashRequired();
×
293
    this.context.trace("Trying to find git path on Windows");
×
294
    if (Files.exists(bashBinary)) {
×
295
      gitPath = bashBinary.getParent().resolve("git.exe");
×
296
      if (Files.exists(gitPath)) {
×
297
        this.context.trace("Git path was extracted from bash path at: {}", gitPath);
×
298
      } else {
299
        this.context.error("Git path: {} was extracted from bash path at: {} but it does not exist", gitPath, bashBinary);
×
300
        return null;
×
301
      }
302
    } else {
303
      this.context.error("Bash path was checked at: {} but it does not exist", bashBinary);
×
304
      return null;
×
305
    }
306
    return gitPath;
×
307
  }
308

309
  private Path findGitInPath(Path gitPath) {
310
    this.context.trace("Trying to find git executable within the PATH environment variable");
×
311
    Path binaryGitPath = this.context.getPath().findBinary(gitPath);
×
312
    if (gitPath == binaryGitPath) {
×
313
      this.context.debug("No git executable could be found within the PATH environment variable");
×
314
      return null;
×
315
    }
316
    return binaryGitPath;
×
317
  }
318

319
  private void runGitCommand(Path directory, String... args) {
320

321
    ProcessResult result = runGitCommand(directory, ProcessMode.DEFAULT, args);
6✔
322
    if (!result.isSuccessful()) {
3!
323
      String command = result.getCommand();
×
324
      this.context.requireOnline(command, false);
×
325
      result.failOnError();
×
326
    }
327
  }
1✔
328

329
  private void runGitCommand(Path directory, List<String> args) {
330

331
    runGitCommand(directory, args.toArray(String[]::new));
10✔
332
  }
1✔
333

334
  private String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, String... args) {
335

336
    return runGitCommandAndGetSingleOutput(warningOnError, directory, ProcessMode.DEFAULT_CAPTURE, args);
7✔
337
  }
338

339
  private String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, ProcessMode mode, String... args) {
340
    ProcessErrorHandling errorHandling = ProcessErrorHandling.NONE;
2✔
341
    if (this.context.debug().isEnabled()) {
5!
342
      errorHandling = ProcessErrorHandling.LOG_WARNING;
2✔
343
    }
344
    ProcessResult result = runGitCommand(directory, mode, errorHandling, args);
7✔
345
    if (result.isSuccessful()) {
3!
346
      List<String> out = result.getOut();
3✔
347
      int size = out.size();
3✔
348
      if (size == 1) {
3✔
349
        return out.get(0);
5✔
350
      } else if (size == 0) {
2!
351
        warningOnError += " - No output received from " + result.getCommand();
6✔
352
      } else {
353
        warningOnError += " - Expected single line of output but received " + size + " lines from " + result.getCommand();
×
354
      }
355
    }
356
    this.context.warning(warningOnError);
4✔
357
    return null;
2✔
358
  }
359

360
  private ProcessResult runGitCommand(Path directory, ProcessMode mode, String... args) {
361

362
    return runGitCommand(directory, mode, ProcessErrorHandling.LOG_WARNING, args);
7✔
363
  }
364

365
  private ProcessResult runGitCommand(Path directory, ProcessMode mode, ProcessErrorHandling errorHandling, String... args) {
366

367
    ProcessContext processContext;
368

369
    if (this.context.isBatchMode()) {
4!
370
      processContext = this.context.newProcess().executable(findGitRequired()).withEnvVar("GIT_TERMINAL_PROMPT", "0").withEnvVar("GCM_INTERACTIVE", "never")
×
371
          .withEnvVar("GIT_ASKPASS", "echo").withEnvVar("SSH_ASKPASS", "echo").errorHandling(errorHandling).directory(directory);
×
372
    } else {
373
      processContext = this.context.newProcess().executable(findGitRequired()).errorHandling(errorHandling)
9✔
374
          .directory(directory);
2✔
375
    }
376

377
    processContext.addArgs(args);
4✔
378
    return processContext.run(mode);
4✔
379
  }
380

381
  @Override
382
  public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) {
383

384
    if ((repository == null) || (trackedCommitIdPath == null)) {
4!
385
      this.context.warning("Invalid usage of saveCurrentCommitId with null value");
×
386
      return;
×
387
    }
388
    this.context.trace("Saving commit Id of {} into {}", repository, trackedCommitIdPath);
14✔
389
    String currentCommitId = determineCurrentCommitId(repository);
4✔
390
    if (currentCommitId != null) {
2!
391
      try {
392
        Files.writeString(trackedCommitIdPath, currentCommitId);
6✔
393
      } catch (IOException e) {
×
394
        throw new IllegalStateException("Failed to save commit ID", e);
×
395
      }
1✔
396
    }
397
  }
1✔
398

399
  /**
400
   * @param repository the {@link Path} to the git repository.
401
   * @return the current commit ID of the given {@link Path repository}.
402
   */
403
  protected String determineCurrentCommitId(Path repository) {
404
    return runGitCommandAndGetSingleOutput("Failed to get current commit id.", repository, "rev-parse", FILE_HEAD);
×
405
  }
406
}
407

408

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

© 2026 Coveralls, Inc