Push: show pre-push hook output in PushResultDialog
Capture the output of the pre-push hook, if any, and show it in the
result dialog.
Bug: 580910
Change-Id: I56326da9870911e70cff4d14daada14e1438e1ca
Signed-off-by: Thomas Wolf <twolf@apache.org>
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java
index 6016d2a..87b09d5 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java
@@ -424,6 +424,9 @@
public static String PullOperation_TaskName;
/** */
+ public static String PushOperation_ForUri;
+
+ /** */
public static String PushOperation_InternalExceptionOccurredMessage;
/** */
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties
index ed7df68..9f7eee1 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties
@@ -159,6 +159,7 @@
PullOperation_DetachedHeadMessage=No local branch is currently checked out
PullOperation_PullNotConfiguredMessage=The current branch is not configured for pull
PullOperation_TaskName=Pulling {0,choice,1#1 repository|1<{0} repositories}
+PushOperation_ForUri=For URI {0}:
PushOperation_InternalExceptionOccurredMessage=An internal Exception occurred during push: {0}
PushOperation_ExceptionOccurredDuringPushOnUriMessage=An exception occurred during push on URI {0}: {1}
PushOperation_resultCancelled=Operation was cancelled.
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/op/PushOperation.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/op/PushOperation.java
index 4b36726..f4c247c 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/op/PushOperation.java
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/op/PushOperation.java
@@ -3,7 +3,7 @@
* Copyright (C) 2011, Mathias Kinzler <mathias.kinzler@sap.com>
* Copyright (C) 2012, Robin Stocker <robin@nibor.org>
* Copyright (C) 2015, Stephan Hackstedt <stephan.hackstedt@googlemail.com>
- * Copyright (C) 2016, 2022 Thomas Wolf <thomas.wolf@paranor.ch>
+ * Copyright (C) 2016, 2022 Thomas Wolf <twolf@apache.org>
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
@@ -14,9 +14,13 @@
*******************************************************************************/
package org.eclipse.egit.core.op;
+import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
+import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.text.MessageFormat;
import java.util.Collection;
import java.util.List;
@@ -39,6 +43,7 @@
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.SystemReader;
import org.eclipse.osgi.util.NLS;
/**
@@ -230,7 +235,11 @@
operationResult = new PushOperationResult();
try (Git git = new Git(localDb)) {
- if (specification != null)
+ Charset hookCharset = SystemReader.getInstance()
+ .getDefaultCharset();
+ if (specification != null) {
+ StringBuilder allHookOutputs = new StringBuilder();
+ StringBuilder allHookErrors = new StringBuilder();
for (final URIish uri : specification.getURIs()) {
if (progress.isCanceled()) {
operationResult.addOperationResult(uri,
@@ -251,13 +260,27 @@
transport.setCredentialsProvider(
credentialsProvider);
}
- PushResult result = transport.push(gitSubMonitor,
- refUpdates, out);
-
- operationResult.addOperationResult(result.getURI(),
- result);
- specification.addURIRefUpdates(result.getURI(),
- result.getRemoteUpdates());
+ try (ByteArrayOutputStream hookOutBytes = new ByteArrayOutputStream();
+ ByteArrayOutputStream hookErrBytes = new ByteArrayOutputStream();
+ PrintStream stdout = new PrintStream(hookOutBytes, true, hookCharset);
+ PrintStream stderr = new PrintStream(hookErrBytes, true, hookCharset)) {
+ transport.setHookOutputStream(stdout);
+ transport.setHookErrorStream(stderr);
+ PushResult result = transport.push(gitSubMonitor,
+ refUpdates, out);
+ stdout.flush();
+ stderr.flush();
+ addHookMessage(result.getURI(),
+ hookOutBytes.toString(hookCharset),
+ allHookOutputs);
+ addHookMessage(result.getURI(),
+ hookErrBytes.toString(hookCharset),
+ allHookErrors);
+ operationResult.addOperationResult(result.getURI(),
+ result);
+ specification.addURIRefUpdates(result.getURI(),
+ result.getRemoteUpdates());
+ }
} catch (JGitInternalException e) {
String errorMessage = e.getCause() != null
? e.getCause().getMessage() : e.getMessage();
@@ -269,10 +292,17 @@
handleException(uri, e, e.getMessage());
}
}
- else {
+ operationResult.setHookOutput(allHookOutputs.toString(),
+ allHookErrors.toString());
+ } else {
final EclipseGitProgressTransformer gitMonitor = new EclipseGitProgressTransformer(
progress.newChild(totalWork));
- try {
+ try (ByteArrayOutputStream hookOutBytes = new ByteArrayOutputStream();
+ ByteArrayOutputStream hookErrBytes = new ByteArrayOutputStream();
+ PrintStream stdout = new PrintStream(hookOutBytes, true,
+ hookCharset);
+ PrintStream stderr = new PrintStream(hookErrBytes, true,
+ hookCharset)) {
Iterable<PushResult> results = git.push()
.setRemote(remoteName)
.setDryRun(dryRun)
@@ -280,7 +310,14 @@
.setProgressMonitor(gitMonitor)
.setCredentialsProvider(credentialsProvider)
.setOutputStream(out)
+ .setHookOutputStream(stdout)
+ .setHookErrorStream(stderr)
.call();
+ stdout.flush();
+ stderr.flush();
+ operationResult.setHookOutput(
+ hookOutBytes.toString(hookCharset),
+ hookErrBytes.toString(hookCharset));
for (PushResult result : results) {
operationResult.addOperationResult(result.getURI(),
result);
@@ -301,6 +338,18 @@
}
}
+ private void addHookMessage(URIish uri, String msg, StringBuilder all) {
+ if (!msg.isEmpty()) {
+ if (all.length() > 0 && all.charAt(all.length() - 1) != '\n') {
+ all.append('\n');
+ }
+ all.append(
+ MessageFormat.format(CoreText.PushOperation_ForUri, uri));
+ all.append('\n');
+ all.append(msg);
+ }
+ }
+
private void handleException(final URIish uri, Exception e,
String userMessage) {
String uriString;
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/op/PushOperationResult.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/op/PushOperationResult.java
index 5fe23f0..5dbbaed 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/op/PushOperationResult.java
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/op/PushOperationResult.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
+ * Copyright (C) 2008, 2022 Marek Zawirski <marek.zawirski@gmail.com> and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
@@ -33,8 +33,13 @@
* @see PushOperation
*/
public class PushOperationResult {
+
private LinkedHashMap<URIish, Entry> urisEntries;
+ private String hookOut;
+
+ private String hookErr;
+
/**
* Construct empty push operation result.
*/
@@ -118,6 +123,37 @@
}
/**
+ * Sets the output of a pre-push hook.
+ *
+ * @param stdout
+ * of the pre-push hook
+ * @param stderr
+ * of the pre-push hook
+ */
+ public void setHookOutput(String stdout, String stderr) {
+ hookOut = stdout;
+ hookErr = stderr;
+ }
+
+ /**
+ * Retrieves the stdout output of a pre-push hook, if any.
+ *
+ * @return the hook's output to stdout, or an empty string
+ */
+ public String getHookStdOut() {
+ return hookOut == null ? "" : hookOut; //$NON-NLS-1$
+ }
+
+ /**
+ * Retrieves the stderr output of a pre-push hook, if any.
+ *
+ * @return the hook's output to stderr, or an empty string
+ */
+ public String getHookStdErr() {
+ return hookErr == null ? "" : hookErr; //$NON-NLS-1$
+ }
+
+ /**
* @return string being list of failed URIs with their error messages.
*/
public String getErrorStringForAllURis() {
diff --git a/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/internal/push/PushToUpstreamTest.java b/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/internal/push/PushToUpstreamTest.java
index d0803cc..9ce6c47 100644
--- a/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/internal/push/PushToUpstreamTest.java
+++ b/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/internal/push/PushToUpstreamTest.java
@@ -1,5 +1,6 @@
/*******************************************************************************
* Copyright (c) 2014, 2022 Robin Stocker <robin@nibor.org> and others.
+ *
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
@@ -12,10 +13,15 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import java.io.File;
+import java.nio.file.Files;
+import java.text.MessageFormat;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import org.eclipse.egit.core.internal.CoreText;
import org.eclipse.egit.core.op.BranchOperation;
import org.eclipse.egit.core.op.CreateLocalBranchOperation;
import org.eclipse.egit.ui.JobFamilies;
@@ -25,6 +31,7 @@
import org.eclipse.egit.ui.test.JobJoiner;
import org.eclipse.egit.ui.test.TestUtil;
import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.swtbot.swt.finder.SWTBot;
@@ -70,6 +77,46 @@
}
@Test
+ public void pushWithHook() throws Exception {
+ checkoutNewLocalBranch("foo");
+ // Existing configuration without push refspec
+ String remoteName = "origin";
+ String pushUrl = repository.getConfig().getString("remote", "push",
+ "pushurl");
+ repository.getConfig().setString("remote", remoteName, "url", pushUrl);
+ repository.getConfig().setString("remote", remoteName, "fetch",
+ "refs/heads/*:refs/remotes/origin/*");
+ File gitDir = repository.getDirectory();
+ File hookDir = new File(gitDir, "hooks");
+ assertTrue(hookDir.mkdir() || hookDir.isDirectory());
+ File hookFile = new File(hookDir, "pre-push");
+ Files.writeString(hookFile.toPath(), "#!/bin/sh\n"
+ + "echo \"1:$1 2:$2 3:$3\"\n" // to stdout
+ + "cat - 1>&2\n" // to stderr
+ + "exit 0\n");
+ if (repository.getFS().supportsExecute()) {
+ repository.getFS().setExecute(hookFile, true);
+ }
+ String headId = repository.resolve(Constants.HEAD).getName();
+ String forUri = MessageFormat.format(CoreText.PushOperation_ForUri,
+ pushUrl);
+ String expectedHookOutput = MessageFormat.format(
+ UIText.PushResultTable_PrePushHookOutput,
+ "stdout: " + forUri+ '\n'
+ + "stdout: 1:" + pushUrl + " 2:" + pushUrl + " 3:\n",
+ "stderr: " + forUri + '\n'
+ + "stderr: refs/heads/foo " + headId
+ + " refs/heads/foo "+ ObjectId.zeroId().getName() + '\n');
+ String resultText = pushToUpstream("origin", "foo", true, false);
+ assertEquals("Hook message doesn't match: " + resultText,
+ expectedHookOutput,
+ resultText.substring(0, Math.min(resultText.length(),
+ expectedHookOutput.length())));
+
+ assertBranchPushed("foo", remoteRepository);
+ }
+
+ @Test
public void pushIsDisabledWithPushDefaultNothing() throws Exception {
checkoutNewLocalBranch("foo");
repository.getConfig().setString(ConfigConstants.CONFIG_PUSH_SECTION,
@@ -194,7 +241,7 @@
pushToUpstream(remoteName, "", false, false);
}
- private void pushToUpstream(String remoteName, String branchName,
+ private String pushToUpstream(String remoteName, String branchName,
boolean expectBranchWizard, boolean expectMultipleWarning) {
SWTBotTree project = selectProject();
JobJoiner joiner = null;
@@ -218,7 +265,10 @@
}
SWTBotShell resultDialog = TestUtil
.botForShellStartingWith("Push Results");
+ String resultText = resultDialog.bot().styledText().getLines().stream()
+ .collect(Collectors.joining("\n"));
resultDialog.close();
+ return resultText;
}
private void assertPushToUpstreamDisabled() {
diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/UIText.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/UIText.java
index 1a91842..2e9e727 100644
--- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/UIText.java
+++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/UIText.java
@@ -2992,6 +2992,9 @@
public static String PushResultTable_MessageText;
/** */
+ public static String PushResultTable_PrePushHookOutput;
+
+ /** */
public static String PushResultTable_repository;
/** */
diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushResultTable.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushResultTable.java
index 4c29d83..57f0e59 100644
--- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushResultTable.java
+++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushResultTable.java
@@ -1,5 +1,6 @@
/*******************************************************************************
- * Copyright (C) 2008, 2015 Marek Zawirski <marek.zawirski@gmail.com> and others.
+ * Copyright (C) 2008, 2022 Marek Zawirski <marek.zawirski@gmail.com> and others.
+ *
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
@@ -9,8 +10,11 @@
*******************************************************************************/
package org.eclipse.egit.ui.internal.push;
+import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.eclipse.egit.core.op.PushOperationResult;
import org.eclipse.egit.ui.UIUtils;
@@ -75,6 +79,8 @@
private Repository repo;
+ private String hookResult;
+
PushResultTable(final Composite parent) {
this(parent, null);
}
@@ -216,8 +222,13 @@
return;
}
Object selected = structuredSelection.getFirstElement();
- if (selected instanceof RefUpdateElement)
- text.setText(getResult((RefUpdateElement) selected));
+ if (selected instanceof RefUpdateElement) {
+ String toShow = getResult((RefUpdateElement) selected);
+ if (!hookResult.isEmpty()) {
+ toShow = hookResult + toShow;
+ }
+ text.setText(toShow);
+ }
}
});
@@ -299,6 +310,28 @@
sashForm.setWeights(defaultValues);
}
+ private String formatHookOutput(String hookOutput, String hookError) {
+ String out = hookOutput.strip();
+ String err = hookError.strip();
+ if (out.isEmpty() && err.isEmpty()) {
+ return ""; //$NON-NLS-1$
+ }
+ if (!out.isEmpty()) {
+ out = prefixLines("stdout: ", out); //$NON-NLS-1$
+ }
+ if (!err.isEmpty()) {
+ err = prefixLines("stderr: ", err); //$NON-NLS-1$
+ }
+ return MessageFormat.format(UIText.PushResultTable_PrePushHookOutput,
+ out, err);
+ }
+
+ private String prefixLines(String prefix, String text) {
+ return Stream.of(text.split("\n")) //$NON-NLS-1$
+ .map(s -> prefix + s.stripTrailing())
+ .collect(Collectors.joining("\n", "", "\n")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
void setData(final Repository localDb, final PushOperationResult result) {
reader = localDb.newObjectReader();
repo = localDb;
@@ -311,22 +344,27 @@
return;
}
+ hookResult = formatHookOutput(result.getHookStdOut(),
+ result.getHookStdErr()).replaceAll("\n", Text.DELIMITER); //$NON-NLS-1$
final List<RefUpdateElement> results = new ArrayList<>();
- for (URIish uri : result.getURIs())
- if (result.isSuccessfulConnection(uri))
+ for (URIish uri : result.getURIs()) {
+ if (result.isSuccessfulConnection(uri)) {
for (RemoteRefUpdate update : result.getPushResult(uri)
- .getRemoteUpdates())
+ .getRemoteUpdates()) {
results.add(new RefUpdateElement(result, update, uri,
reader, repo));
-
+ }
+ }
+ }
treeViewer.setInput(results.toArray());
// select the first row of table to get the details of the first
// push result shown in the Text control
Tree table = treeViewer.getTree();
- if (table.getItemCount() > 0)
+ if (table.getItemCount() > 0) {
treeViewer.setSelection(new StructuredSelection(table.getItem(0)
.getData()));
+ }
root.layout();
}
diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/uitext.properties b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/uitext.properties
index 435ade0..c417b96 100644
--- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/uitext.properties
+++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/uitext.properties
@@ -1013,6 +1013,7 @@
PushResultDialog_label_failed=Failed pushing to {0}
PushResultDialog_ConfigureButton=C&onfigure...
PushResultTable_MessageText=Message Details
+PushResultTable_PrePushHookOutput=Output from the ''pre-push'' hook:\n{0}{1}--------\n
PushResultTable_repository=Repository
PushResultTable_statusRemoteRejected=[remote rejected]
PushResultTable_statusRejected=[rejected]