From 0c3d009cad131956891ee5892a5a0abfa6d5959b Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Mon, 24 Oct 2022 12:44:14 +0200 Subject: [PATCH] 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 --- .../org/eclipse/egit/core/internal/CoreText.java | 3 + .../eclipse/egit/core/internal/coretext.properties | 1 + .../org/eclipse/egit/core/op/PushOperation.java | 71 ++++++++++++++++++---- .../eclipse/egit/core/op/PushOperationResult.java | 38 +++++++++++- .../egit/ui/internal/push/PushToUpstreamTest.java | 52 +++++++++++++++- .../src/org/eclipse/egit/ui/internal/UIText.java | 3 + .../egit/ui/internal/push/PushResultTable.java | 54 +++++++++++++--- .../org/eclipse/egit/ui/internal/uitext.properties | 1 + 8 files changed, 202 insertions(+), 21 deletions(-) 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 6016d2a39..87b09d530 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 class CoreText extends NLS { 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 ed7df68b1..9f7eee178 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 @@ ProjectUtil_taskCheckingDirectory=Checking: {0} 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 4b3672604..f4c247c58 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 * Copyright (C) 2012, Robin Stocker * Copyright (C) 2015, Stephan Hackstedt - * Copyright (C) 2016, 2022 Thomas Wolf + * Copyright (C) 2016, 2022 Thomas Wolf * * 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; 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 @@ public class PushOperation { 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 @@ public class PushOperation { 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 @@ public class PushOperation { 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 results = git.push() .setRemote(remoteName) .setDryRun(dryRun) @@ -280,7 +310,14 @@ public class PushOperation { .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 @@ public class PushOperation { } } + 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 5fe23f00c..5dbbaedaf 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 + * Copyright (C) 2008, 2022 Marek Zawirski 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 @@ import org.eclipse.jgit.transport.URIish; * @see PushOperation */ public class PushOperationResult { + private LinkedHashMap urisEntries; + private String hookOut; + + private String hookErr; + /** * Construct empty push operation result. */ @@ -118,6 +123,37 @@ public class PushOperationResult { } /** + * 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 d0803cc4c..9ce6c4795 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 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 @@ package org.eclipse.egit.ui.internal.push; 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.ContextMenuHelper; 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 @@ public class PushToUpstreamTest extends LocalRepositoryTestCase { } @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 @@ public class PushToUpstreamTest extends LocalRepositoryTestCase { 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 @@ public class PushToUpstreamTest extends LocalRepositoryTestCase { } 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 1a91842a5..2e9e7276d 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 class UIText extends NLS { 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 4c29d8385..57f0e59b5 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 and others. + * Copyright (C) 2008, 2022 Marek Zawirski 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 @@ class PushResultTable { private Repository repo; + private String hookResult; + PushResultTable(final Composite parent) { this(parent, null); } @@ -216,8 +222,13 @@ class PushResultTable { 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 @@ class PushResultTable { 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 @@ class PushResultTable { return; } + hookResult = formatHookOutput(result.getHookStdOut(), + result.getHookStdErr()).replaceAll("\n", Text.DELIMITER); //$NON-NLS-1$ final List 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 435ade00a..c417b9671 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=Pushed to {0} 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] -- 2.11.4.GIT