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

devonfw / IDEasy / 19536215420

20 Nov 2025 12:04PM UTC coverage: 69.094% (+0.2%) from 68.924%
19536215420

Pull #1577

github

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

3558 of 5639 branches covered (63.1%)

Branch coverage included in aggregate %.

9261 of 12914 relevant lines covered (71.71%)

3.15 hits per line

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

58.59
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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

208
    verifyGitInstalled();
2✔
209
    return runGitCommandAndGetSingleOutput("Failed to determine current origin of git repository.", repository, "remote");
11✔
210
  }
211

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

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

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

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

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

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

254
  IdeContext getContext() {
255

256
    return this.context;
3✔
257
  }
258

259
  @Override
260
  public void verifyGitInstalled() {
261

262
    String bashBinary = this.context.findBashRequired();
4✔
263
    String message = "Git is not installed on your computer but required by IDEasy. Please download and install git:\n"
2✔
264
        + "https://git-scm.com/download/";
265

266
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
267
      Path bashPath = Path.of(bashBinary);
×
268
      if (Files.exists(bashPath)) {
×
269
        Path gitPath = bashPath.getParent().resolve("git.exe");
×
270
        if (Files.exists(gitPath)) {
×
271
          this.context.trace("Git path was extracted from bash path at: {}", gitPath);
×
272
          SystemPath systemPath = this.context.getPath();
×
273
          systemPath.setPath("git", gitPath);
×
274
        } else {
×
275
          this.context.debug("Git path: {} was extracted from bash path at: {} but it does not exist", gitPath, bashPath);
×
276
          throw new CliException(message);
×
277
        }
278
      } else {
×
279
        this.context.debug("Bash path was checked at: {} but it does not exist", bashPath);
×
280
        throw new CliException(message);
×
281
      }
282
    } else {
×
283
      Path git = Path.of("git");
5✔
284
      Path binaryGitPath = this.context.getPath().findBinary(git);
6✔
285
      if (git == binaryGitPath) {
3!
286
        throw new CliException(message);
×
287
      }
288
    }
289

290
    this.context.trace("Using Bash at: {}", bashBinary);
10✔
291
    this.context.trace("Git is installed");
4✔
292
  }
1✔
293

294
  private void runGitCommand(Path directory, String... args) {
295

296
    ProcessResult result = runGitCommand(directory, ProcessMode.DEFAULT, args);
6✔
297
    if (!result.isSuccessful()) {
3!
298
      String command = result.getCommand();
×
299
      this.context.requireOnline(command, false);
×
300
      result.failOnError();
×
301
    }
302
  }
1✔
303

304
  private void runGitCommand(Path directory, List<String> args) {
305

306
    runGitCommand(directory, args.toArray(String[]::new));
10✔
307
  }
1✔
308

309
  private String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, String... args) {
310

311
    return runGitCommandAndGetSingleOutput(warningOnError, directory, ProcessMode.DEFAULT_CAPTURE, args);
7✔
312
  }
313

314
  private String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, ProcessMode mode, String... args) {
315
    ProcessErrorHandling errorHandling = ProcessErrorHandling.NONE;
2✔
316
    if (this.context.debug().isEnabled()) {
5!
317
      errorHandling = ProcessErrorHandling.LOG_WARNING;
2✔
318
    }
319
    ProcessResult result = runGitCommand(directory, mode, errorHandling, args);
7✔
320
    if (result.isSuccessful()) {
3!
321
      List<String> out = result.getOut();
3✔
322
      int size = out.size();
3✔
323
      if (size == 1) {
3✔
324
        return out.get(0);
5✔
325
      } else if (size == 0) {
2!
326
        warningOnError += " - No output received from " + result.getCommand();
6✔
327
      } else {
328
        warningOnError += " - Expected single line of output but received " + size + " lines from " + result.getCommand();
×
329
      }
330
    }
331
    this.context.warning(warningOnError);
4✔
332
    return null;
2✔
333
  }
334

335
  private ProcessResult runGitCommand(Path directory, ProcessMode mode, String... args) {
336

337
    return runGitCommand(directory, mode, ProcessErrorHandling.LOG_WARNING, args);
7✔
338
  }
339

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

342
    ProcessContext processContext;
343

344
    if (this.context.isBatchMode()) {
4!
345
      processContext = this.context.newProcess().executable("git").withEnvVar("GIT_TERMINAL_PROMPT", "0").withEnvVar("GCM_INTERACTIVE", "never")
×
346
          .withEnvVar("GIT_ASKPASS", "echo").withEnvVar("SSH_ASKPASS", "echo").errorHandling(errorHandling).directory(directory);
×
347
    } else {
348
      processContext = this.context.newProcess().executable("git").errorHandling(errorHandling)
8✔
349
          .directory(directory);
2✔
350
    }
351

352
    processContext.addArgs(args);
4✔
353
    return processContext.run(mode);
4✔
354
  }
355

356
  @Override
357
  public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) {
358

359
    if ((repository == null) || (trackedCommitIdPath == null)) {
4!
360
      this.context.warning("Invalid usage of saveCurrentCommitId with null value");
×
361
      return;
×
362
    }
363
    this.context.trace("Saving commit Id of {} into {}", repository, trackedCommitIdPath);
14✔
364
    String currentCommitId = determineCurrentCommitId(repository);
4✔
365
    if (currentCommitId != null) {
2!
366
      try {
367
        Files.writeString(trackedCommitIdPath, currentCommitId);
6✔
368
      } catch (IOException e) {
×
369
        throw new IllegalStateException("Failed to save commit ID", e);
×
370
      }
1✔
371
    }
372
  }
1✔
373

374
  /**
375
   * @param repository the {@link Path} to the git repository.
376
   * @return the current commit ID of the given {@link Path repository}.
377
   */
378
  protected String determineCurrentCommitId(Path repository) {
379
    return runGitCommandAndGetSingleOutput("Failed to get current commit id.", repository, "rev-parse", FILE_HEAD);
×
380
  }
381
}
382

383

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