From ecc2e58caaf523f5596943e9acb2beb1824ad997 Mon Sep 17 00:00:00 2001 From: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:07:35 +0200 Subject: [PATCH] #6: #277: fix git clone hangs (#278) --- .../tools/ide/context/GitContextImpl.java | 58 +++++++------------ .../tools/ide/process/ProcessContextImpl.java | 35 ++++++++--- 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java index 1203816dc..e7a435500 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java @@ -26,11 +26,11 @@ public class GitContextImpl implements GitContext { private static final Duration GIT_PULL_CACHE_DELAY_MILLIS = Duration.ofMillis(30 * 60 * 1000); - ; - private final IdeContext context; - private ProcessContext processContext; + private final ProcessContext processContext; + + private final ProcessMode PROCESS_MODE = ProcessMode.DEFAULT; /** * @param context the {@link IdeContext context}. @@ -38,7 +38,7 @@ public class GitContextImpl implements GitContext { public GitContextImpl(IdeContext context) { this.context = context; - + this.processContext = this.context.newProcess().executable("git").withEnvVar("GIT_TERMINAL_PROMPT", "0"); } @Override @@ -102,10 +102,9 @@ public void pullOrClone(String gitRepoUrl, String branch, Path targetRepository) throw new IllegalArgumentException("Invalid git URL '" + gitRepoUrl + "'!"); } - initializeProcessContext(targetRepository); if (Files.isDirectory(targetRepository.resolve(".git"))) { // checks for remotes - ProcessResult result = this.processContext.addArg("remote").run(ProcessMode.DEFAULT_CAPTURE); + ProcessResult result = this.processContext.addArg("remote").run(PROCESS_MODE); List remotes = result.getOut(); if (remotes.isEmpty()) { String message = targetRepository @@ -128,8 +127,8 @@ public void pullOrClone(String gitRepoUrl, String branch, Path targetRepository) * Handles errors which occurred during git pull. * * @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 git will by default create a sub-folder by default on clone but the * final - * folder that will contain the ".git" subfolder. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the * + * final folder that will contain the ".git" subfolder. * @param result the {@link ProcessResult} to evaluate. */ private void handleErrors(Path targetRepository, ProcessResult result) { @@ -142,8 +141,8 @@ private void handleErrors(Path targetRepository, ProcessResult result) { } else { this.context.error(message); if (this.context.isOnline()) { - this.context.error( - "See above error for details. If you have local changes, please stash or revert and retry."); + this.context + .error("See above error for details. If you have local changes, please stash or revert and retry."); } else { this.context.error( "It seems you are offline - please ensure Internet connectivity and retry or activate offline mode (-o or --offline)."); @@ -153,26 +152,11 @@ private void handleErrors(Path targetRepository, ProcessResult result) { } } - /** - * Lazily initializes the {@link ProcessContext}. - * - * @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 git will by default create a sub-folder by default on clone but the * final - * folder that will contain the ".git" subfolder. - */ - private void initializeProcessContext(Path targetRepository) { - - if (this.processContext == null) { - this.processContext = this.context.newProcess().directory(targetRepository).executable("git") - .withEnvVar("GIT_TERMINAL_PROMPT", "0"); - } - } - @Override public void clone(GitUrl gitRepoUrl, Path targetRepository) { URL parsedUrl = gitRepoUrl.parseUrl(); - initializeProcessContext(targetRepository); + this.processContext.directory(targetRepository); ProcessResult result; if (!this.context.isOffline()) { this.context.getFileAccess().mkdirs(targetRepository); @@ -182,14 +166,14 @@ public void clone(GitUrl gitRepoUrl, Path targetRepository) { this.processContext.addArg("-q"); } this.processContext.addArgs("--recursive", gitRepoUrl.url(), "--config", "core.autocrlf=false", "."); - result = this.processContext.run(ProcessMode.DEFAULT_CAPTURE); + result = this.processContext.run(PROCESS_MODE); if (!result.isSuccessful()) { this.context.warning("Git failed to clone {} into {}.", parsedUrl, targetRepository); } String branch = gitRepoUrl.branch(); if (branch != null) { this.processContext.addArgs("checkout", branch); - result = this.processContext.run(ProcessMode.DEFAULT_CAPTURE); + result = this.processContext.run(PROCESS_MODE); if (!result.isSuccessful()) { this.context.warning("Git failed to checkout to branch {}", branch); } @@ -202,10 +186,10 @@ public void clone(GitUrl gitRepoUrl, Path targetRepository) { @Override public void pull(Path targetRepository) { - initializeProcessContext(targetRepository); + this.processContext.directory(targetRepository); ProcessResult result; // pull from remote - result = this.processContext.addArg("--no-pager").addArg("pull").run(ProcessMode.DEFAULT_CAPTURE); + result = this.processContext.addArg("--no-pager").addArg("pull").run(PROCESS_MODE); if (!result.isSuccessful()) { Map remoteAndBranchName = retrieveRemoteAndBranchName(); @@ -218,7 +202,7 @@ public void pull(Path targetRepository) { private Map retrieveRemoteAndBranchName() { Map remoteAndBranchName = new HashMap<>(); - ProcessResult remoteResult = this.processContext.addArg("branch").addArg("-vv").run(ProcessMode.DEFAULT_CAPTURE); + ProcessResult remoteResult = this.processContext.addArg("branch").addArg("-vv").run(PROCESS_MODE); List remotes = remoteResult.getOut(); if (!remotes.isEmpty()) { for (String remote : remotes) { @@ -246,17 +230,17 @@ private Map retrieveRemoteAndBranchName() { @Override public void reset(Path targetRepository, String remoteName, String branchName) { - initializeProcessContext(targetRepository); + this.processContext.directory(targetRepository); ProcessResult result; // check for changed files - result = this.processContext.addArg("diff-index").addArg("--quiet").addArg("HEAD").run(ProcessMode.DEFAULT_CAPTURE); + result = this.processContext.addArg("diff-index").addArg("--quiet").addArg("HEAD").run(PROCESS_MODE); if (!result.isSuccessful()) { // reset to origin/master context.warning("Git has detected modified files -- attempting to reset {} to '{}/{}'.", targetRepository, remoteName, branchName); result = this.processContext.addArg("reset").addArg("--hard").addArg(remoteName + "/" + branchName) - .run(ProcessMode.DEFAULT_CAPTURE); + .run(PROCESS_MODE); if (!result.isSuccessful()) { context.warning("Git failed to reset {} to '{}/{}'.", remoteName, branchName, targetRepository); @@ -268,16 +252,16 @@ public void reset(Path targetRepository, String remoteName, String branchName) { @Override public void cleanup(Path targetRepository) { - initializeProcessContext(targetRepository); + this.processContext.directory(targetRepository); ProcessResult result; // check for untracked files result = this.processContext.addArg("ls-files").addArg("--other").addArg("--directory").addArg("--exclude-standard") - .run(ProcessMode.DEFAULT_CAPTURE); + .run(PROCESS_MODE); if (!result.getOut().isEmpty()) { // delete untracked files context.warning("Git detected untracked files in {} and is attempting a cleanup.", targetRepository); - result = this.processContext.addArg("clean").addArg("-df").run(ProcessMode.DEFAULT_CAPTURE); + result = this.processContext.addArg("clean").addArg("-df").run(PROCESS_MODE); if (!result.isSuccessful()) { context.warning("Git failed to clean the repository {}.", targetRepository); diff --git a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java index 2afec83ad..16ba79150 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import com.devonfw.tools.ide.cli.CliException; @@ -134,18 +135,16 @@ public ProcessResult run(ProcessMode processMode) { this.processBuilder.command(args); - Process process = this.processBuilder.start(); - List out = null; List err = null; + Process process = this.processBuilder.start(); + if (processMode == ProcessMode.DEFAULT_CAPTURE) { - try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - out = outReader.lines().collect(Collectors.toList()); - } - try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { - err = errReader.lines().collect(Collectors.toList()); - } + CompletableFuture> outFut = readInputStream(process.getInputStream()); + CompletableFuture> errFut = readInputStream(process.getErrorStream()); + out = outFut.get(); + err = errFut.get(); } int exitCode; @@ -171,6 +170,26 @@ public ProcessResult run(ProcessMode processMode) { } } + /** + * Asynchronously and parallel reads {@link InputStream input stream} and stores it in {@link CompletableFuture}. + * Inspired by: StackOverflow + * + * @param is {@link InputStream}. + * @return {@link CompletableFuture}. + */ + private static CompletableFuture> readInputStream(InputStream is) { + + return CompletableFuture.supplyAsync(() -> { + + try (InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr)) { + return br.lines().toList(); + } catch (Throwable e) { + throw new RuntimeException("There was a problem while executing the program", e); + } + }); + } + private String createCommandMessage(String interpreter, String suffix) { StringBuilder sb = new StringBuilder();