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

devonfw / IDEasy / 19632300454

24 Nov 2025 11:12AM UTC coverage: 69.047% (+0.1%) from 68.924%
19632300454

Pull #1577

github

web-flow
Merge 80cf4c4df into ffcb5d97f
Pull Request #1577: #1561: Fix BASH_PATH not used properly

3562 of 5647 branches covered (63.08%)

Branch coverage included in aggregate %.

9269 of 12936 relevant lines covered (71.65%)

3.14 hits per line

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

51.48
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.common.SystemPath;
12
import com.devonfw.tools.ide.context.IdeContext;
13
import com.devonfw.tools.ide.os.SystemInfoImpl;
14
import com.devonfw.tools.ide.process.ProcessContext;
15
import com.devonfw.tools.ide.process.ProcessErrorHandling;
16
import com.devonfw.tools.ide.process.ProcessMode;
17
import com.devonfw.tools.ide.process.ProcessResult;
18
import com.devonfw.tools.ide.variable.IdeVariables;
19

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

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

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

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

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

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

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

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

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

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

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

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

69
  @Override
70
  public boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath) {
71

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

84
  @Override
85
  public void pullOrCloneAndResetIfNeeded(GitUrl gitUrl, Path repository, String remoteName) {
86

87
    pullOrCloneIfNeeded(gitUrl, repository);
4✔
88
    reset(repository, gitUrl.branch(), remoteName);
6✔
89
    cleanup(repository);
3✔
90
  }
1✔
91

92
  @Override
93
  public void pullOrClone(GitUrl gitUrl, Path repository) {
94

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

112
  /**
113
   * Handles errors which occurred during git pull.
114
   *
115
   * @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
116
   *     git will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
117
   * @param result the {@link ProcessResult} to evaluate.
118
   */
119
  private void handleErrors(Path targetRepository, ProcessResult result) {
120

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

138
  @Override
139
  public void clone(GitUrl gitUrl, Path repository) {
140

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

165
  @Override
166
  public void pull(Path repository) {
167

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

181
  @Override
182
  public void fetch(Path repository, String remote, String branch) {
183

184
    findGitRequired();
3✔
185
    if (branch == null) {
2!
186
      branch = determineCurrentBranch(repository);
×
187
    }
188
    if (remote == null) {
2!
189
      remote = determineRemote(repository);
×
190
    }
191

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

194
    if (!result.isSuccessful()) {
3!
195
      this.context.warning("Git fetch for '{}/{} failed.'.", remote, branch);
×
196
    }
197
  }
1✔
198

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

202
    findGitRequired();
×
203
    return runGitCommandAndGetSingleOutput("Failed to determine current branch of git repository", repository, "branch", "--show-current");
×
204
  }
205

206
  @Override
207
  public String determineRemote(Path repository) {
208

209
    findGitRequired();
3✔
210
    return runGitCommandAndGetSingleOutput("Failed to determine current origin of git repository.", repository, "remote");
11✔
211
  }
212

213
  @Override
214
  public void reset(Path repository, String branchName, String remoteName) {
215

216
    findGitRequired();
3✔
217
    if ((remoteName == null) || remoteName.isEmpty()) {
5!
218
      remoteName = DEFAULT_REMOTE;
2✔
219
    }
220
    if ((branchName == null) || branchName.isEmpty()) {
5!
221
      branchName = GitUrl.BRANCH_MASTER;
2✔
222
    }
223
    ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT, "diff-index", "--quiet", "HEAD");
19✔
224
    if (!result.isSuccessful()) {
3!
225
      // reset to origin/master
226
      this.context.warning("Git has detected modified files -- attempting to reset {} to '{}/{}'.", repository, remoteName, branchName);
18✔
227
      result = runGitCommand(repository, ProcessMode.DEFAULT, "reset", "--hard", remoteName + "/" + branchName);
21✔
228
      if (!result.isSuccessful()) {
3!
229
        this.context.warning("Git failed to reset {} to '{}/{}'.", remoteName, branchName, repository);
×
230
        handleErrors(repository, result);
×
231
      }
232
    }
233
  }
1✔
234

235
  @Override
236
  public void cleanup(Path repository) {
237

238
    findGitRequired();
3✔
239
    // check for untracked files
240
    ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "ls-files", "--other", "--directory", "--exclude-standard");
23✔
241
    if (!result.getOut().isEmpty()) {
4✔
242
      // delete untracked files
243
      this.context.warning("Git detected untracked files in {} and is attempting a cleanup.", repository);
10✔
244
      runGitCommand(repository, "clean", "-df");
13✔
245
    }
246
  }
1✔
247

248
  @Override
249
  public String retrieveGitUrl(Path repository) {
250

251
    findGitRequired();
×
252
    return runGitCommandAndGetSingleOutput("Failed to retrieve git URL for repository", repository, "config", "--get", "remote.origin.url");
×
253
  }
254

255
  IdeContext getContext() {
256

257
    return this.context;
3✔
258
  }
259

260
  @Override
261
  public Path findGitRequired() {
262

263
    Path gitPath = findGit();
×
264
    if (gitPath == null) {
×
265
      String message = "Git is not installed on your computer but required by IDEasy";
×
266
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
267
        message += " Please download and install git:\n"
×
268
            + "https://git-scm.com/download/";
269
      }
270
      throw new CliException(message);
×
271
    }
272
    return gitPath;
×
273
  }
274

275
  /**
276
   * Finds the path to the Git executable.
277
   *
278
   * @return the {@link Path} to the Git executable, or {@code null} if Git is not found.
279
   */
280
  public Path findGit() {
281
    if (this.git != null) {
×
282
      return this.git;
×
283
    }
284
    Path bashBinary = this.context.findBashRequired();
×
285
    Path gitPath = Path.of("git");
×
286
    if (SystemInfoImpl.INSTANCE.isWindows()) {
×
287
      if (Files.exists(bashBinary)) {
×
288
        gitPath = bashBinary.getParent().resolve("git.exe");
×
289
        if (Files.exists(gitPath)) {
×
290
          this.context.trace("Git path was extracted from bash path at: {}", gitPath);
×
291
          gitPath = findGit();
×
292
          SystemPath systemPath = this.context.getPath();
×
293
          systemPath.setPath("git", gitPath);
×
294
        } else {
×
295
          this.context.error("Git path: {} was extracted from bash path at: {} but it does not exist", gitPath, bashBinary);
×
296
          return null;
×
297
        }
298
      } else {
299
        this.context.error("Bash path was checked at: {} but it does not exist", bashBinary);
×
300
        return null;
×
301
      }
302
    } else {
303
      Path binaryGitPath = this.context.getPath().findBinary(gitPath);
×
304
      if (gitPath == binaryGitPath) {
×
305
        this.context.error("Not git executable was found on your system");
×
306
        return null;
×
307
      } else {
308
        gitPath = binaryGitPath;
×
309
      }
310
    }
311
    this.git = gitPath;
×
312
    this.context.trace("Found git at: {}", gitPath);
×
313
    return gitPath;
×
314
  }
315

316
  private void runGitCommand(Path directory, String... args) {
317

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

326
  private void runGitCommand(Path directory, List<String> args) {
327

328
    runGitCommand(directory, args.toArray(String[]::new));
10✔
329
  }
1✔
330

331
  private String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, String... args) {
332

333
    return runGitCommandAndGetSingleOutput(warningOnError, directory, ProcessMode.DEFAULT_CAPTURE, args);
7✔
334
  }
335

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

357
  private ProcessResult runGitCommand(Path directory, ProcessMode mode, String... args) {
358

359
    return runGitCommand(directory, mode, ProcessErrorHandling.LOG_WARNING, args);
7✔
360
  }
361

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

364
    ProcessContext processContext;
365

366
    if (this.context.isBatchMode()) {
4!
367
      processContext = this.context.newProcess().executable("git").withEnvVar("GIT_TERMINAL_PROMPT", "0").withEnvVar("GCM_INTERACTIVE", "never")
×
368
          .withEnvVar("GIT_ASKPASS", "echo").withEnvVar("SSH_ASKPASS", "echo").errorHandling(errorHandling).directory(directory);
×
369
    } else {
370
      processContext = this.context.newProcess().executable("git").errorHandling(errorHandling)
8✔
371
          .directory(directory);
2✔
372
    }
373

374
    processContext.addArgs(args);
4✔
375
    return processContext.run(mode);
4✔
376
  }
377

378
  @Override
379
  public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) {
380

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

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

405

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