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

devonfw / IDEasy / 22241505980

20 Feb 2026 09:16PM UTC coverage: 70.656% (+0.2%) from 70.474%
22241505980

Pull #1710

github

web-flow
Merge 04e4bdacd into 379acdc9d
Pull Request #1710: #404: allow logging via SLF4J

4121 of 6440 branches covered (63.99%)

Branch coverage included in aggregate %.

10704 of 14542 relevant lines covered (73.61%)

3.13 hits per line

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

49.81
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;
77
    try {
78
      trackedCommitId = Files.readString(trackedCommitIdPath);
×
79
    } catch (IOException e) {
×
80
      return false;
×
81
    }
×
82
    String remoteFailureMessage = String.format("Failed to get the remote commit id of settings repository '%s', missing remote upstream branch?", repository);
×
83
    String remoteCommitId = runGitCommandAndGetSingleOutput(remoteFailureMessage, repository, ProcessMode.DEFAULT_CAPTURE, "rev-parse", "@{u}");
×
84
    return !trackedCommitId.equals(remoteCommitId);
×
85
  }
86

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

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

95
  @Override
96
  public void pullOrClone(GitUrl gitUrl, Path repository) {
97

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

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

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

141
  @Override
142
  public void clone(GitUrl gitUrl, Path repository) {
143

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

167
  @Override
168
  public void pull(Path repository) {
169

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

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

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
      LOG.warn("Git fetch for '{}/{} failed.'.", remote, branch);
×
196
    }
197
  }
1✔
198

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

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
    return runGitCommandAndGetSingleOutput("Failed to determine current origin of git repository.", repository, "remote");
11✔
209
  }
210

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

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

232
  @Override
233
  public void cleanup(Path repository) {
234

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

244
  @Override
245
  public String retrieveGitUrl(Path repository) {
246

247
    return runGitCommandAndGetSingleOutput("Failed to retrieve git URL for repository", repository, "config", "--get", "remote.origin.url");
×
248
  }
249

250
  IdeContext getContext() {
251

252
    return this.context;
3✔
253
  }
254

255
  @Override
256
  public Path findGitRequired() {
257

258
    Path gitPath = findGit();
×
259
    if (gitPath == null) {
×
260
      String message = "Git " + IdeContext.IS_NOT_INSTALLED_BUT_REQUIRED;
×
261
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
262
        message += IdeContext.PLEASE_DOWNLOAD_AND_INSTALL_GIT + ":\n "
×
263
            + IdeContext.WINDOWS_GIT_DOWNLOAD_URL;
264
      }
265
      throw new CliException(message);
×
266
    }
267
    return gitPath;
×
268
  }
269

270
  @Override
271
  public Path findGit() {
272
    if (this.git != null) {
×
273
      return this.git;
×
274
    }
275

276
    Path gitPath = findGitInPath(Path.of("git"));
×
277

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

284
    if (gitPath != null) {
×
285
      this.git = gitPath;
×
286
      LOG.trace("Found git at: {}", gitPath);
×
287
    }
288

289
    return gitPath;
×
290
  }
291

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

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

321
  private void runGitCommand(Path directory, String... args) {
322

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

331
  private void runGitCommand(Path directory, List<String> args) {
332

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

336
  private String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, String... args) {
337

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

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

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

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

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

369
    ProcessContext processContext;
370

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

379
    processContext.addArgs(args);
4✔
380
    return processContext.run(mode);
4✔
381
  }
382

383
  @Override
384
  public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) {
385

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

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

410

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