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

devonfw / IDEasy / 24842808195

23 Apr 2026 03:07PM UTC coverage: 70.629% (+0.006%) from 70.623%
24842808195

Pull #1838

github

web-flow
Merge 40409ff04 into d0e60fe23
Pull Request #1838: #1833: Missing commit id file

4329 of 6776 branches covered (63.89%)

Branch coverage included in aggregate %.

11198 of 15208 relevant lines covered (73.63%)

3.11 hits per line

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

41.44
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 org.slf4j.Logger;
11
import org.slf4j.LoggerFactory;
12

13
import com.devonfw.tools.ide.cli.CliException;
14
import com.devonfw.tools.ide.context.IdeContext;
15
import com.devonfw.tools.ide.log.IdeLogLevel;
16
import com.devonfw.tools.ide.os.SystemInfoImpl;
17
import com.devonfw.tools.ide.process.ProcessContext;
18
import com.devonfw.tools.ide.process.ProcessErrorHandling;
19
import com.devonfw.tools.ide.process.ProcessMode;
20
import com.devonfw.tools.ide.process.ProcessResult;
21
import com.devonfw.tools.ide.variable.IdeVariables;
22

23
/**
24
 * Implements the {@link GitContext}.
25
 */
26
public class GitContextImpl implements GitContext {
27

28
  private static final Logger LOG = LoggerFactory.getLogger(GitContextImpl.class);
4✔
29

30
  /** @see #getContext() */
31
  protected final IdeContext context;
32
  private Path git;
33

34
  /**
35
   * @param context the {@link IdeContext context}.
36
   */
37
  public GitContextImpl(IdeContext context) {
2✔
38

39
    this.context = context;
3✔
40
  }
1✔
41

42
  @Override
43
  public void pullOrCloneIfNeeded(GitUrl gitUrl, Path repository) {
44

45
    GitOperation.PULL_OR_CLONE.executeIfNeeded(this.context, gitUrl, repository, null);
8✔
46
  }
1✔
47

48
  @Override
49
  public boolean fetchIfNeeded(Path repository) {
50

51
    return fetchIfNeeded(repository, null, null);
×
52
  }
53

54
  @Override
55
  public boolean fetchIfNeeded(Path repository, String remote, String branch) {
56

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

60
  @Override
61
  public boolean isRepositoryUpdateAvailable(Path repository) {
62

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

73
  @Override
74
  public boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath) {
75

76
    String trackedCommitId = this.context.getFileAccess().readFileContent(trackedCommitIdPath);
×
77
    if (trackedCommitId == null) {
×
78
      return true;
×
79
    }
80

81
    String remoteFailureMessage = String.format("Failed to get the remote commit id of settings repository '%s', missing remote upstream branch?", repository);
×
82
    String remoteCommitId = runGitCommandAndGetSingleOutput(remoteFailureMessage, repository, ProcessMode.DEFAULT_CAPTURE, "rev-parse", "@{u}");
×
83
    return !trackedCommitId.equals(remoteCommitId);
×
84
  }
85

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

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

94
  @Override
95
  public void pullSafelyWithStash(Path repository) {
96
    String token = "autostash:pull:" + java.util.UUID.randomUUID();
×
97
    LOG.debug("Untracked files found. Creating temporary stash with token '{}'", token);
×
98
    ProcessResult stashRes = runGitCommand(repository, ProcessMode.DEFAULT, "--no-pager", "stash", "push", "--include-untracked", "-m", token, "--quiet");
×
99
    if (!stashRes.isSuccessful()) {
×
100
      LOG.warn("Failed to create stash before pull on {}", repository);
×
101
      handleErrors(repository, stashRes);
×
102
    }
103

104
    ProcessResult listRes = runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "--no-pager", "stash", "list");
×
105
    if (!listRes.isSuccessful()) {
×
106
      LOG.warn("Failed to list stash after creating temporary stash on {}", repository);
×
107
      handleErrors(repository, listRes);
×
108
    }
109

110
    String stashRef = findStashRefByMessage(listRes.getOut(), token);
×
111
    if (stashRef == null) {
×
112
      LOG.warn("Could not find created stash by token '{}'. Leaving stash untouched.", token);
×
113
    } else {
114
      LOG.debug("Created stash identified as '{}'", stashRef);
×
115
    }
116

117
    pull(repository);
×
118

119
    if (stashRef != null) {
×
120
      ProcessResult popRes = runGitCommand(repository, ProcessMode.DEFAULT, "--no-pager", "stash", "pop", stashRef, "--quiet");
×
121
      if (!popRes.isSuccessful()) {
×
122
        LOG.warn("Applying stash {} failed after successful pull on {}.", stashRef, repository);
×
123
        handleErrors(repository, popRes);
×
124
      } else {
125
        LOG.debug("Stash {} successfully popped after pull.", stashRef);
×
126
      }
127
    } else {
×
128
      LOG.warn("Skipping stash pop because stashRef is unknown (token '{}'). Stash remains on the stack.", token);
×
129
    }
130
  }
×
131

132
  @Override
133
  public boolean hasUntrackedFiles(Path repository) {
134
    ProcessResult status = runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "--no-pager", "status", "--porcelain", "-uall");
×
135
    if (!status.isSuccessful()) {
×
136
      handleErrors(repository, status);
×
137
      return false;
×
138
    }
139
    List<String> out = status.getOut();
×
140
    return !out.isEmpty();
×
141
  }
142

143
  private String findStashRefByMessage(List<String> stashList, String needle) {
144
    if (stashList.isEmpty()) {
×
145
      return null;
×
146
    }
147
    for (String line : stashList) {
×
148
      if (line.contains(needle)) {
×
149
        int idx = line.indexOf(':');
×
150
        if (idx > 0) {
×
151
          return line.substring(0, idx).trim();
×
152
        }
153
      }
154
    }
×
155
    return null;
×
156
  }
157

158

159
  @Override
160
  public void pullOrClone(GitUrl gitUrl, Path repository) {
161

162
    Objects.requireNonNull(repository);
3✔
163
    Objects.requireNonNull(gitUrl);
3✔
164
    if (Files.isDirectory(repository.resolve(GIT_FOLDER))) {
7✔
165
      // checks for remotes
166
      String remote = determineRemote(repository);
4✔
167
      if (remote == null) {
2!
168
        String message = repository + " is a local git repository with no remote - if you did this for testing, you may continue...\n"
×
169
            + "Do you want to ignore the problem and continue anyhow?";
170
        this.context.askToContinue(message);
×
171
      } else {
×
172
        pull(repository);
3✔
173
      }
174
    } else {
1✔
175
      clone(gitUrl, repository);
4✔
176
    }
177
  }
1✔
178

179
  /**
180
   * Handles errors which occurred during git pull.
181
   *
182
   * @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
183
   *     git will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
184
   * @param result the {@link ProcessResult} to evaluate.
185
   */
186
  private void handleErrors(Path targetRepository, ProcessResult result) {
187

188
    if (!result.isSuccessful()) {
×
189
      String message = "Failed to update git repository at " + targetRepository;
×
190
      if (this.context.isOfflineMode()) {
×
191
        LOG.warn(message);
×
192
        IdeLogLevel.INTERACTION.log(LOG, "Continuing as we are in offline mode - results may be outdated!");
×
193
      } else {
194
        LOG.error(message);
×
195
        if (this.context.isOnline()) {
×
196
          LOG.error("See above error for details. If you have local changes, please stash or revert and retry.");
×
197
        } else {
198
          LOG.error("It seems you are offline - please ensure Internet connectivity and retry or activate offline mode (-o or --offline).");
×
199
        }
200
        this.context.askToContinue("Typically you should abort and fix the problem. Do you want to continue anyways?");
×
201
      }
202
    }
203
  }
×
204

205
  @Override
206
  public void clone(GitUrl gitUrl, Path repository) {
207

208
    GitUrlSyntax gitUrlSyntax = IdeVariables.PREFERRED_GIT_PROTOCOL.get(getContext());
6✔
209
    gitUrl = gitUrlSyntax.format(gitUrl);
4✔
210
    if (this.context.isOfflineMode()) {
4✔
211
      this.context.requireOnline("git clone of " + gitUrl, false);
×
212
    }
213
    this.context.getFileAccess().mkdirs(repository);
5✔
214
    List<String> args = new ArrayList<>(7);
5✔
215
    args.add("clone");
4✔
216
    if (this.context.isQuietMode()) {
4!
217
      args.add("-q");
×
218
    }
219
    args.add("--recursive");
4✔
220
    args.add(gitUrl.url());
5✔
221
    args.add("--config");
4✔
222
    args.add("core.autocrlf=false");
4✔
223
    args.add(".");
4✔
224
    runGitCommand(repository, args);
4✔
225
    String branch = gitUrl.branch();
3✔
226
    if (branch != null) {
2✔
227
      runGitCommand(repository, "switch", branch);
13✔
228
    }
229
  }
1✔
230

231
  @Override
232
  public void pull(Path repository) {
233

234
    if (this.context.isOffline()) {
4!
235
      LOG.info("Skipping git pull on {} because offline", repository);
×
236
      return;
×
237
    }
238
    ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT, "--no-pager", "pull", "--quiet");
19✔
239
    if (!result.isSuccessful()) {
3!
240
      String branchName = determineCurrentBranch(repository);
×
241
      LOG.warn("Git pull on branch {} failed for repository {}.", branchName, repository);
×
242
      handleErrors(repository, result);
×
243
    }
244
  }
1✔
245

246
  @Override
247
  public void fetch(Path repository, String remote, String branch) {
248

249
    if (branch == null) {
2!
250
      branch = determineCurrentBranch(repository);
×
251
    }
252
    if (remote == null) {
2!
253
      remote = determineRemote(repository);
×
254
    }
255

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

258
    if (!result.isSuccessful()) {
3!
259
      LOG.warn("Git fetch for '{}/{} failed.'.", remote, branch);
×
260
    }
261
  }
1✔
262

263
  @Override
264
  public String determineCurrentBranch(Path repository) {
265

266
    return runGitCommandAndGetSingleOutput("Failed to determine current branch of git repository", repository, "branch", "--show-current");
×
267
  }
268

269
  @Override
270
  public String determineRemote(Path repository) {
271

272
    return runGitCommandAndGetSingleOutput("Failed to determine current origin of git repository.", repository, "remote");
11✔
273
  }
274

275
  @Override
276
  public void reset(Path repository, String branchName, String remoteName) {
277

278
    if ((remoteName == null) || remoteName.isEmpty()) {
5!
279
      remoteName = DEFAULT_REMOTE;
2✔
280
    }
281
    if ((branchName == null) || branchName.isEmpty()) {
5!
282
      branchName = GitUrl.BRANCH_MASTER;
2✔
283
    }
284
    ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT, "diff-index", "--quiet", "HEAD");
19✔
285
    if (!result.isSuccessful()) {
3!
286
      // reset to origin/master
287
      LOG.warn("Git has detected modified files -- attempting to reset {} to '{}/{}'.", repository, remoteName, branchName);
17✔
288
      result = runGitCommand(repository, ProcessMode.DEFAULT, "reset", "--hard", remoteName + "/" + branchName);
21✔
289
      if (!result.isSuccessful()) {
3!
290
        LOG.warn("Git failed to reset {} to '{}/{}'.", remoteName, branchName, repository);
×
291
        handleErrors(repository, result);
×
292
      }
293
    }
294
  }
1✔
295

296
  @Override
297
  public void cleanup(Path repository) {
298

299
    // check for untracked files
300
    ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "ls-files", "--other", "--directory", "--exclude-standard");
23✔
301
    if (!result.getOut().isEmpty()) {
4✔
302
      // delete untracked files
303
      LOG.warn("Git detected untracked files in {} and is attempting a cleanup.", repository);
4✔
304
      runGitCommand(repository, "clean", "-df");
13✔
305
    }
306
  }
1✔
307

308
  @Override
309
  public String retrieveGitUrl(Path repository) {
310

311
    return runGitCommandAndGetSingleOutput("Failed to retrieve git URL for repository", repository, "config", "--get", "remote.origin.url");
×
312
  }
313

314
  IdeContext getContext() {
315

316
    return this.context;
3✔
317
  }
318

319
  @Override
320
  public Path findGitRequired() {
321

322
    Path gitPath = findGit();
×
323
    if (gitPath == null) {
×
324
      String message = "Git " + IdeContext.IS_NOT_INSTALLED_BUT_REQUIRED;
×
325
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
326
        message += IdeContext.PLEASE_DOWNLOAD_AND_INSTALL_GIT + ":\n " + IdeContext.WINDOWS_GIT_DOWNLOAD_URL;
×
327
      }
328
      throw new CliException(message);
×
329
    }
330
    return gitPath;
×
331
  }
332

333
  @Override
334
  public Path findGit() {
335
    if (this.git != null) {
×
336
      return this.git;
×
337
    }
338

339
    Path gitPath = findGitInPath(Path.of("git"));
×
340

341
    if (gitPath == null) {
×
342
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
343
        gitPath = findGitOnWindowsViaBash();
×
344
      }
345
    }
346

347
    if (gitPath != null) {
×
348
      this.git = gitPath;
×
349
      LOG.trace("Found git at: {}", gitPath);
×
350
    }
351

352
    return gitPath;
×
353
  }
354

355
  @Override
356
  public boolean isGitRepo(Path directory) {
357
    return directory != null && Files.exists(directory.resolve(".git"));
13!
358
  }
359

360
  private Path findGitOnWindowsViaBash() {
361
    Path gitPath;
362
    Path bashBinary = this.context.findBashRequired();
×
363
    LOG.trace("Trying to find git path on Windows");
×
364
    if (Files.exists(bashBinary)) {
×
365
      gitPath = bashBinary.getParent().resolve("git.exe");
×
366
      if (Files.exists(gitPath)) {
×
367
        LOG.trace("Git path was extracted from bash path at: {}", gitPath);
×
368
      } else {
369
        LOG.error("Git path: {} was extracted from bash path at: {} but it does not exist", gitPath, bashBinary);
×
370
        return null;
×
371
      }
372
    } else {
373
      LOG.error("Bash path was checked at: {} but it does not exist", bashBinary);
×
374
      return null;
×
375
    }
376
    return gitPath;
×
377
  }
378

379
  private Path findGitInPath(Path gitPath) {
380
    LOG.trace("Trying to find git executable within the PATH environment variable");
×
381
    Path binaryGitPath = this.context.getPath().findBinary(gitPath);
×
382
    if (gitPath == binaryGitPath) {
×
383
      LOG.debug("No git executable could be found within the PATH environment variable");
×
384
      return null;
×
385
    }
386
    return binaryGitPath;
×
387
  }
388

389
  private void runGitCommand(Path directory, String... args) {
390

391
    ProcessResult result = runGitCommand(directory, ProcessMode.DEFAULT, args);
6✔
392
    if (!result.isSuccessful()) {
3!
393
      String command = result.getCommand();
×
394
      this.context.requireOnline(command, false);
×
395
      result.failOnError();
×
396
    }
397
  }
1✔
398

399
  private void runGitCommand(Path directory, List<String> args) {
400

401
    runGitCommand(directory, args.toArray(String[]::new));
10✔
402
  }
1✔
403

404
  private String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, String... args) {
405

406
    return runGitCommandAndGetSingleOutput(warningOnError, directory, ProcessMode.DEFAULT_CAPTURE, args);
7✔
407
  }
408

409
  private String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, ProcessMode mode, String... args) {
410
    ProcessErrorHandling errorHandling = ProcessErrorHandling.NONE;
2✔
411
    if (LOG.isDebugEnabled()) {
3!
412
      errorHandling = ProcessErrorHandling.LOG_WARNING;
2✔
413
    }
414
    ProcessResult result = runGitCommand(directory, mode, errorHandling, args);
7✔
415
    if (result.isSuccessful()) {
3!
416
      List<String> out = result.getOut();
3✔
417
      int size = out.size();
3✔
418
      if (size == 1) {
3✔
419
        return out.getFirst();
4✔
420
      } else if (size == 0) {
2!
421
        warningOnError += " - No output received from " + result.getCommand();
6✔
422
      } else {
423
        warningOnError += " - Expected single line of output but received " + size + " lines from " + result.getCommand();
×
424
      }
425
    }
426
    LOG.warn(warningOnError);
3✔
427
    return null;
2✔
428
  }
429

430
  private ProcessResult runGitCommand(Path directory, ProcessMode mode, String... args) {
431

432
    return runGitCommand(directory, mode, ProcessErrorHandling.LOG_WARNING, args);
7✔
433
  }
434

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

437
    ProcessContext processContext;
438

439
    if (this.context.isBatchMode()) {
4!
440
      processContext = this.context.newProcess().executable(findGitRequired()).withEnvVar("GIT_TERMINAL_PROMPT", "0").withEnvVar("GCM_INTERACTIVE", "never")
×
441
          .withEnvVar("GIT_ASKPASS", "echo").withEnvVar("SSH_ASKPASS", "echo").errorHandling(errorHandling).directory(directory);
×
442
    } else {
443
      processContext = this.context.newProcess().executable(findGitRequired()).errorHandling(errorHandling).directory(directory);
11✔
444
    }
445

446
    processContext.addArgs(args);
4✔
447
    return processContext.run(mode);
4✔
448
  }
449

450
  @Override
451
  public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) {
452

453
    if ((repository == null) || (trackedCommitIdPath == null)) {
4!
454
      LOG.warn("Invalid usage of saveCurrentCommitId with null value");
×
455
      return;
×
456
    }
457
    LOG.trace("Saving commit Id of {} into {}", repository, trackedCommitIdPath);
5✔
458
    String currentCommitId = determineCurrentCommitId(repository);
4✔
459
    if (currentCommitId != null) {
2!
460
      try {
461
        Files.writeString(trackedCommitIdPath, currentCommitId);
6✔
462
      } catch (IOException e) {
×
463
        throw new IllegalStateException("Failed to save commit ID", e);
×
464
      }
1✔
465
    }
466
  }
1✔
467

468
  /**
469
   * @param repository the {@link Path} to the git repository.
470
   * @return the current commit ID of the given {@link Path repository}.
471
   */
472
  protected String determineCurrentCommitId(Path repository) {
473
    return runGitCommandAndGetSingleOutput("Failed to get current commit id.", repository, "rev-parse", FILE_HEAD);
×
474
  }
475
}
476

477

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