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

devonfw / IDEasy / 12085044164

29 Nov 2024 12:48PM UTC coverage: 67.107% (+0.08%) from 67.031%
12085044164

push

github

web-flow
#587:  check for git installation (#769)

2505 of 4080 branches covered (61.4%)

Branch coverage included in aggregate %.

6533 of 9388 relevant lines covered (69.59%)

3.07 hits per line

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

53.6
cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java
1
package com.devonfw.tools.ide.context;
2

3
import java.nio.file.Files;
4
import java.nio.file.Path;
5
import java.util.List;
6
import java.util.Objects;
7

8
import com.devonfw.tools.ide.cli.CliOfflineException;
9
import com.devonfw.tools.ide.process.ProcessContext;
10
import com.devonfw.tools.ide.process.ProcessErrorHandling;
11
import com.devonfw.tools.ide.process.ProcessMode;
12
import com.devonfw.tools.ide.process.ProcessResult;
13
import com.devonfw.tools.ide.variable.IdeVariables;
14

15
/**
16
 * Implements the {@link GitContext}.
17
 */
18
public class GitContextImpl implements GitContext {
19

20
  private final IdeContext context;
21

22
  private final ProcessContext processContext;
23

24
  private static final ProcessMode PROCESS_MODE = ProcessMode.DEFAULT;
2✔
25
  private static final ProcessMode PROCESS_MODE_FOR_FETCH = ProcessMode.DEFAULT_CAPTURE;
3✔
26

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

32
    this.context = context;
3✔
33
    this.processContext = this.context.newProcess().executable("git").withEnvVar("GIT_TERMINAL_PROMPT", "0").errorHandling(ProcessErrorHandling.LOG_WARNING);
12✔
34
  }
1✔
35

36
  @Override
37
  public void pullOrCloneIfNeeded(String repoUrl, String branch, Path targetRepository) {
38

39
    GitOperation.PULL_OR_CLONE.executeIfNeeded(this.context, repoUrl, targetRepository, null, branch);
9✔
40
  }
1✔
41

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

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

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

51
    return GitOperation.FETCH.executeIfNeeded(this.context, null, targetRepository, remote, branch);
9✔
52
  }
53

54
  @Override
55
  public boolean isRepositoryUpdateAvailable(Path repository) {
56
    verifyGitInstalled();
2✔
57
    ProcessResult result = this.processContext.directory(repository).addArg("rev-parse").addArg("HEAD").run(PROCESS_MODE_FOR_FETCH);
11✔
58
    if (!result.isSuccessful()) {
3!
59
      this.context.warning("Failed to get the local commit hash.");
×
60
      return false;
×
61
    }
62
    String localCommitHash = result.getOut().stream().findFirst().orElse("").trim();
9✔
63
    // get remote commit code
64
    result = this.processContext.addArg("rev-parse").addArg("@{u}").run(PROCESS_MODE_FOR_FETCH);
9✔
65
    if (!result.isSuccessful()) {
3!
66
      this.context.warning("Failed to get the remote commit hash.");
×
67
      return false;
×
68
    }
69
    String remote_commit_code = result.getOut().stream().findFirst().orElse("").trim();
9✔
70
    return !localCommitHash.equals(remote_commit_code);
6!
71
  }
72

73
  @Override
74
  public void pullOrCloneAndResetIfNeeded(String repoUrl, Path repository, String branch, String remoteName) {
75

76
    pullOrCloneIfNeeded(repoUrl, branch, repository);
5✔
77

78
    reset(repository, "master", remoteName);
5✔
79

80
    cleanup(repository);
3✔
81
  }
1✔
82

83
  @Override
84
  public void pullOrClone(String gitRepoUrl, Path repository) {
85

86
    pullOrClone(gitRepoUrl, repository, null);
5✔
87
  }
1✔
88

89
  @Override
90
  public void pullOrClone(String gitRepoUrl, Path repository, String branch) {
91

92
    Objects.requireNonNull(repository);
3✔
93
    Objects.requireNonNull(gitRepoUrl);
3✔
94
    if (!gitRepoUrl.startsWith("http")) {
4!
95
      throw new IllegalArgumentException("Invalid git URL '" + gitRepoUrl + "'!");
×
96
    }
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(new GitUrl(gitRepoUrl, branch), repository);
8✔
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.isOffline()) {
×
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 gitRepoUrl, Path targetRepository) {
140
    verifyGitInstalled();
2✔
141
    GitUrlSyntax gitUrlSyntax = IdeVariables.PREFERRED_GIT_PROTOCOL.get(getContext());
6✔
142
    gitRepoUrl = gitUrlSyntax.format(gitRepoUrl);
4✔
143
    this.processContext.directory(targetRepository);
5✔
144
    ProcessResult result;
145
    if (!this.context.isOffline()) {
4✔
146
      this.context.getFileAccess().mkdirs(targetRepository);
5✔
147
      this.context.requireOnline("git clone of " + gitRepoUrl.url());
6✔
148
      this.processContext.addArg("clone");
5✔
149
      if (this.context.isQuietMode()) {
4!
150
        this.processContext.addArg("-q");
×
151
      }
152
      this.processContext.addArgs("--recursive", gitRepoUrl.url(), "--config", "core.autocrlf=false", ".");
27✔
153
      result = this.processContext.run(PROCESS_MODE);
5✔
154
      if (!result.isSuccessful()) {
3!
155
        this.context.warning("Git failed to clone {} into {}.", gitRepoUrl.url(), targetRepository);
×
156
      }
157
      String branch = gitRepoUrl.branch();
3✔
158
      if (branch != null) {
2!
159
        this.processContext.addArgs("checkout", branch);
×
160
        result = this.processContext.run(PROCESS_MODE);
×
161
        if (!result.isSuccessful()) {
×
162
          this.context.warning("Git failed to checkout to branch {}", branch);
×
163
        }
164
      }
165
    } else {
1✔
166
      throw CliOfflineException.ofClone(gitRepoUrl.parseUrl(), targetRepository);
5✔
167
    }
168
  }
1✔
169

170
  @Override
171
  public void pull(Path repository) {
172
    verifyGitInstalled();
2✔
173
    if (this.context.isOffline()) {
4!
174
      this.context.info("Skipping git pull on {} because offline", repository);
×
175
      return;
×
176
    }
177
    ProcessResult result = this.processContext.directory(repository).addArg("--no-pager").addArg("pull").addArg("--quiet").run(PROCESS_MODE);
13✔
178
    if (!result.isSuccessful()) {
3!
179
      String branchName = determineCurrentBranch(repository);
×
180
      this.context.warning("Git pull on branch {} failed for repository {}.", branchName, repository);
×
181
      handleErrors(repository, result);
×
182
    }
183
  }
1✔
184

185
  @Override
186
  public void fetch(Path targetRepository, String remote, String branch) {
187
    verifyGitInstalled();
2✔
188
    if (branch == null) {
2!
189
      branch = determineCurrentBranch(targetRepository);
×
190
    }
191
    if (remote == null) {
2!
192
      remote = determineRemote(targetRepository);
×
193
    }
194
    ProcessResult result = this.processContext.directory(targetRepository).addArg("fetch").addArg(remote).addArg(branch).run(PROCESS_MODE_FOR_FETCH);
13✔
195

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

201
  @Override
202
  public String determineCurrentBranch(Path repository) {
203
    verifyGitInstalled();
×
204
    ProcessResult remoteResult = this.processContext.directory(repository).addArg("branch").addArg("--show-current").run(ProcessMode.DEFAULT_CAPTURE);
×
205
    if (remoteResult.isSuccessful()) {
×
206
      List<String> remotes = remoteResult.getOut();
×
207
      if (!remotes.isEmpty()) {
×
208
        assert (remotes.size() == 1);
×
209
        return remotes.get(0);
×
210
      }
211
    } else {
×
212
      this.context.warning("Failed to determine current branch of git repository {}", repository);
×
213
    }
214
    return null;
×
215
  }
216

217
  @Override
218
  public String determineRemote(Path repository) {
219
    verifyGitInstalled();
2✔
220
    ProcessResult remoteResult = this.processContext.directory(repository).addArg("remote").run(ProcessMode.DEFAULT_CAPTURE);
9✔
221
    if (remoteResult.isSuccessful()) {
3!
222
      List<String> remotes = remoteResult.getOut();
3✔
223
      if (!remotes.isEmpty()) {
3!
224
        assert (remotes.size() == 1);
5!
225
        return remotes.get(0);
5✔
226
      }
227
    } else {
×
228
      this.context.warning("Failed to determine current origin of git repository {}", repository);
×
229
    }
230
    return null;
×
231
  }
232

233
  @Override
234
  public void reset(Path targetRepository, String branchName, String remoteName) {
235
    verifyGitInstalled();
2✔
236
    if ((remoteName == null) || remoteName.isEmpty()) {
5!
237
      remoteName = DEFAULT_REMOTE;
×
238
    }
239
    this.processContext.directory(targetRepository);
5✔
240
    ProcessResult result;
241
    // check for changed files
242
    result = this.processContext.addArg("diff-index").addArg("--quiet").addArg("HEAD").run(PROCESS_MODE);
11✔
243

244
    if (!result.isSuccessful()) {
3!
245
      // reset to origin/master
246
      this.context.warning("Git has detected modified files -- attempting to reset {} to '{}/{}'.", targetRepository, remoteName, branchName);
18✔
247
      result = this.processContext.addArg("reset").addArg("--hard").addArg(remoteName + "/" + branchName).run(PROCESS_MODE);
13✔
248

249
      if (!result.isSuccessful()) {
3!
250
        this.context.warning("Git failed to reset {} to '{}/{}'.", remoteName, branchName, targetRepository);
×
251
        handleErrors(targetRepository, result);
×
252
      }
253
    }
254
  }
1✔
255

256
  @Override
257
  public void cleanup(Path targetRepository) {
258
    verifyGitInstalled();
2✔
259
    this.processContext.directory(targetRepository);
5✔
260
    ProcessResult result;
261
    // check for untracked files
262
    result = this.processContext.addArg("ls-files").addArg("--other").addArg("--directory").addArg("--exclude-standard").run(ProcessMode.DEFAULT_CAPTURE);
13✔
263

264
    if (!result.getOut().isEmpty()) {
4!
265
      // delete untracked files
266
      this.context.warning("Git detected untracked files in {} and is attempting a cleanup.", targetRepository);
10✔
267
      result = this.processContext.addArg("clean").addArg("-df").run(PROCESS_MODE);
9✔
268

269
      if (!result.isSuccessful()) {
3!
270
        this.context.warning("Git failed to clean the repository {}.", targetRepository);
×
271
      }
272
    }
273
  }
1✔
274

275
  @Override
276
  public String retrieveGitUrl(Path repository) {
277
    verifyGitInstalled();
×
278
    this.processContext.directory(repository);
×
279
    ProcessResult result;
280
    result = this.processContext.addArgs("-C", repository, "remote", "-v").run(ProcessMode.DEFAULT_CAPTURE);
×
281
    for (String line : result.getOut()) {
×
282
      if (line.contains("(fetch)")) {
×
283
        return line.split("\\s+")[1]; // Extract the URL from the line
×
284
      }
285
    }
×
286

287
    this.context.error("Failed to retrieve git URL for repository: {}", repository);
×
288
    return null;
×
289
  }
290

291
  IdeContext getContext() {
292

293
    return this.context;
3✔
294
  }
295

296
  /**
297
   * Checks if there is a git installation and throws an exception if there is none
298
   */
299
  private void verifyGitInstalled() {
300
    this.context.findBashRequired();
4✔
301
    Path git = Path.of("git");
5✔
302
    Path binaryGitPath = this.context.getPath().findBinary(git);
6✔
303
    if (git == binaryGitPath) {
3!
304
      String message = "Could not find a git installation. We highly recommend installing git since most of our actions require git to work properly!";
×
305
      throw new IllegalStateException(message);
×
306
    }
307
    this.context.trace("Git is installed");
4✔
308
  }
1✔
309
}
310

311

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

© 2025 Coveralls, Inc