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

devonfw / IDEasy / 8111080112

01 Mar 2024 12:07PM UTC coverage: 58.254% (+1.1%) from 57.131%
8111080112

push

github

web-flow
#9: background process (#200)

1519 of 2867 branches covered (52.98%)

Branch coverage included in aggregate %.

3954 of 6528 relevant lines covered (60.57%)

2.62 hits per line

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

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

3
import java.io.IOException;
4
import java.net.URL;
5
import java.nio.file.Files;
6
import java.nio.file.Path;
7
import java.nio.file.attribute.FileTime;
8
import java.time.Duration;
9
import java.util.AbstractMap;
10
import java.util.HashMap;
11
import java.util.List;
12
import java.util.Map;
13
import java.util.Objects;
14

15
import com.devonfw.tools.ide.cli.CliException;
16
import com.devonfw.tools.ide.log.IdeLogLevel;
17
import com.devonfw.tools.ide.log.IdeSubLogger;
18
import com.devonfw.tools.ide.process.ProcessContext;
19
import com.devonfw.tools.ide.process.ProcessErrorHandling;
20
import com.devonfw.tools.ide.process.ProcessMode;
21
import com.devonfw.tools.ide.process.ProcessResult;
22

23
/**
24
 * Implements the {@link GitContext}.
25
 */
26
public class GitContextImpl implements GitContext {
27
  private static final Duration GIT_PULL_CACHE_DELAY_MILLIS = Duration.ofMillis(30 * 60 * 1000);;
4✔
28

29
  private final IdeContext context;
30

31
  private ProcessContext processContext;
32

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

38
    this.context = context;
3✔
39

40
  }
1✔
41

42
  @Override
43
  public void pullOrCloneIfNeeded(String repoUrl, Path targetRepository) {
44

45
    Path gitDirectory = targetRepository.resolve(".git");
4✔
46

47
    // Check if the .git directory exists
48
    if (Files.isDirectory(gitDirectory)) {
5!
49
      Path magicFilePath = gitDirectory.resolve("HEAD");
4✔
50
      long currentTime = System.currentTimeMillis();
2✔
51
      // Get the modification time of the magic file
52
      long fileMTime;
53
      try {
54
        fileMTime = Files.getLastModifiedTime(magicFilePath).toMillis();
6✔
55
      } catch (IOException e) {
×
56
        throw new IllegalStateException("Could not read " + magicFilePath, e);
×
57
      }
1✔
58

59
      // Check if the file modification time is older than the delta threshold
60
      if ((currentTime - fileMTime > GIT_PULL_CACHE_DELAY_MILLIS.toMillis()) || context.isForceMode()) {
11!
61
        pullOrClone(repoUrl, targetRepository);
×
62
        try {
63
          Files.setLastModifiedTime(magicFilePath, FileTime.fromMillis(currentTime));
×
64
        } catch (IOException e) {
×
65
          throw new IllegalStateException("Could not read or write in " + magicFilePath, e);
×
66
        }
×
67
      }
68
    } else {
1✔
69
      // If the .git directory does not exist, perform git clone
70
      pullOrClone(repoUrl, targetRepository);
×
71
    }
72
  }
1✔
73

74
  public void pullOrFetchAndResetIfNeeded(String repoUrl, Path targetRepository, String remoteName, String branchName) {
75

76
    pullOrCloneIfNeeded(repoUrl, targetRepository);
4✔
77

78
    if (remoteName.isEmpty()) {
3!
79
      reset(targetRepository, "origin", "master");
×
80
    } else {
81
      reset(targetRepository, remoteName, "master");
5✔
82
    }
83

84
    cleanup(targetRepository);
3✔
85
  }
1✔
86

87
  @Override
88
  public void pullOrClone(String gitRepoUrl, Path targetRepository) {
89

90
    Objects.requireNonNull(targetRepository);
3✔
91
    Objects.requireNonNull(gitRepoUrl);
3✔
92

93
    if (!gitRepoUrl.startsWith("http")) {
4!
94
      throw new IllegalArgumentException("Invalid git URL '" + gitRepoUrl + "'!");
×
95
    }
96

97
    initializeProcessContext(targetRepository);
3✔
98
    if (Files.isDirectory(targetRepository.resolve(".git"))) {
7✔
99
      // checks for remotes
100
      ProcessResult result = this.processContext.addArg("remote").run(ProcessMode.DEFAULT_CAPTURE);
7✔
101
      List<String> remotes = result.getOut();
3✔
102
      if (remotes.isEmpty()) {
3!
103
        String message = targetRepository
×
104
            + " 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
        this.processContext.errorHandling(ProcessErrorHandling.WARNING);
5✔
109

110
        if (!this.context.isOffline()) {
4!
111
          pull(targetRepository);
3✔
112
        }
113
      }
114
    } else {
1✔
115
      String branch = "";
2✔
116
      int hashIndex = gitRepoUrl.indexOf("#");
4✔
117
      if (hashIndex != -1) {
3!
118
        branch = gitRepoUrl.substring(hashIndex + 1);
×
119
        gitRepoUrl = gitRepoUrl.substring(0, hashIndex);
×
120
      }
121
      clone(new GitUrl(gitRepoUrl, branch), targetRepository);
8✔
122
      if (!branch.isEmpty()) {
3!
123
        this.processContext.addArgs("checkout", branch);
×
124
        this.processContext.run();
×
125
      }
126
    }
127
  }
1✔
128

129
  /**
130
   * Handles errors which occurred during git pull.
131
   *
132
   * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled.
133
   *        It is not the parent directory where git will by default create a sub-folder by default on clone but the *
134
   *        final folder that will contain the ".git" subfolder.
135
   * @param result the {@link ProcessResult} to evaluate.
136
   */
137
  private void handleErrors(Path targetRepository, ProcessResult result) {
138

139
    if (!result.isSuccessful()) {
×
140
      String message = "Failed to update git repository at " + targetRepository;
×
141
      if (this.context.isOffline()) {
×
142
        this.context.warning(message);
×
143
        this.context.interaction("Continuing as we are in offline mode - results may be outdated!");
×
144
      } else {
145
        this.context.error(message);
×
146
        if (this.context.isOnline()) {
×
147
          this.context
×
148
              .error("See above error for details. If you have local changes, please stash or revert and retry.");
×
149
        } else {
150
          this.context.error(
×
151
              "It seems you are offline - please ensure Internet connectivity and retry or activate offline mode (-o or --offline).");
152
        }
153
        this.context.askToContinue("Typically you should abort and fix the problem. Do you want to continue anyways?");
×
154
      }
155
    }
156
  }
×
157

158
  /**
159
   * Lazily initializes the {@link ProcessContext}.
160
   *
161
   * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled.
162
   *        It is not the parent directory where git will by default create a sub-folder by default on clone but the *
163
   *        final folder that will contain the ".git" subfolder.
164
   */
165
  private void initializeProcessContext(Path targetRepository) {
166

167
    if (this.processContext == null) {
3✔
168
      this.processContext = this.context.newProcess().directory(targetRepository).executable("git")
10✔
169
          .withEnvVar("GIT_TERMINAL_PROMPT", "0");
2✔
170
    }
171
  }
1✔
172

173
  @Override
174
  public void clone(GitUrl gitRepoUrl, Path targetRepository) {
175

176
    URL parsedUrl = gitRepoUrl.parseUrl();
3✔
177
    initializeProcessContext(targetRepository);
3✔
178
    ProcessResult result;
179
    if (!this.context.isOffline()) {
4✔
180
      this.context.getFileAccess().mkdirs(targetRepository);
5✔
181
      this.context.requireOnline("git clone of " + parsedUrl);
5✔
182
      this.processContext.addArg("clone");
5✔
183
      if (this.context.isQuietMode()) {
4!
184
        this.processContext.addArg("-q");
×
185
      }
186
      this.processContext.addArgs("--recursive", parsedUrl, "--config", "core.autocrlf=false", ".");
26✔
187
      result = this.processContext.run(ProcessMode.DEFAULT_CAPTURE);
5✔
188
      if (!result.isSuccessful()) {
3!
189
        this.context.warning("Git failed to clone {} into {}.", parsedUrl, targetRepository);
×
190
      }
191
    } else {
192
      throw new CliException("Could not clone " + parsedUrl + " to " + targetRepository + " because you are offline.");
7✔
193
    }
194
  }
1✔
195

196
  @Override
197
  public void pull(Path targetRepository) {
198

199
    initializeProcessContext(targetRepository);
3✔
200
    ProcessResult result;
201
    // pull from remote
202
    result = this.processContext.addArg("--no-pager").addArg("pull").run(ProcessMode.DEFAULT_CAPTURE);
9✔
203

204
    if (!result.isSuccessful()) {
3!
205
      Map<String, String> remoteAndBranchName = retrieveRemoteAndBranchName();
×
206
      context.warning("Git pull for {}/{} failed for repository {}.", remoteAndBranchName.get("remote"),
×
207
          remoteAndBranchName.get("branch"), targetRepository);
×
208
      handleErrors(targetRepository, result);
×
209
    }
210
  }
1✔
211

212
  private Map<String, String> retrieveRemoteAndBranchName() {
213

214
    Map<String, String> remoteAndBranchName = new HashMap<>();
×
215
    ProcessResult remoteResult = this.processContext.addArg("branch").addArg("-vv").run(ProcessMode.DEFAULT_CAPTURE);
×
216
    List<String> remotes = remoteResult.getOut();
×
217
    if (!remotes.isEmpty()) {
×
218
      for (String remote : remotes) {
×
219
        if (remote.startsWith("*")) {
×
220
          String checkedOutBranch = remote.substring(remote.indexOf("[") + 1, remote.indexOf("]"));
×
221
          remoteAndBranchName.put("remote", checkedOutBranch.substring(0, checkedOutBranch.indexOf("/")));
×
222
          // check if current repo is behind remote and omit message
223
          if (checkedOutBranch.contains(":")) {
×
224
            remoteAndBranchName.put("branch",
×
225
                checkedOutBranch.substring(checkedOutBranch.indexOf("/") + 1, checkedOutBranch.indexOf(":")));
×
226
          } else {
227
            remoteAndBranchName.put("branch", checkedOutBranch.substring(checkedOutBranch.indexOf("/") + 1));
×
228
          }
229

230
        }
231
      }
×
232
    } else {
233
      return Map.ofEntries(new AbstractMap.SimpleEntry<>("remote", "unknown"),
×
234
          new AbstractMap.SimpleEntry<>("branch", "unknown"));
235
    }
236

237
    return remoteAndBranchName;
×
238
  }
239

240
  @Override
241
  public void reset(Path targetRepository, String remoteName, String branchName) {
242

243
    initializeProcessContext(targetRepository);
3✔
244
    ProcessResult result;
245
    // check for changed files
246
    result = this.processContext.addArg("diff-index").addArg("--quiet").addArg("HEAD").run(ProcessMode.DEFAULT_CAPTURE);
11✔
247

248
    if (!result.isSuccessful()) {
3!
249
      // reset to origin/master
250
      context.warning("Git has detected modified files -- attempting to reset {} to '{}/{}'.", targetRepository,
18✔
251
          remoteName, branchName);
252
      result = this.processContext.addArg("reset").addArg("--hard").addArg(remoteName + "/" + branchName)
11✔
253
          .run(ProcessMode.DEFAULT_CAPTURE);
2✔
254

255
      if (!result.isSuccessful()) {
3!
256
        context.warning("Git failed to reset {} to '{}/{}'.", remoteName, branchName, targetRepository);
×
257
        handleErrors(targetRepository, result);
×
258
      }
259
    }
260
  }
1✔
261

262
  @Override
263
  public void cleanup(Path targetRepository) {
264

265
    initializeProcessContext(targetRepository);
3✔
266
    ProcessResult result;
267
    // check for untracked files
268
    result = this.processContext.addArg("ls-files").addArg("--other").addArg("--directory").addArg("--exclude-standard")
11✔
269
        .run(ProcessMode.DEFAULT_CAPTURE);
2✔
270

271
    if (!result.getOut().isEmpty()) {
4!
272
      // delete untracked files
273
      context.warning("Git detected untracked files in {} and is attempting a cleanup.", targetRepository);
10✔
274
      result = this.processContext.addArg("clean").addArg("-df").run(ProcessMode.DEFAULT_CAPTURE);
9✔
275

276
      if (!result.isSuccessful()) {
3!
277
        context.warning("Git failed to clean the repository {}.", targetRepository);
×
278
      }
279
    }
280
  }
1✔
281

282
  @Override
283
  public IdeSubLogger level(IdeLogLevel level) {
284

285
    return null;
×
286
  }
287
}
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