blob: 57f0e59b5bdb4c20831672c623d92fa3b47d8643 [file] [log] [blame]
/*******************************************************************************
* 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
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
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;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.WorkbenchStyledLabelProvider;
import org.eclipse.egit.ui.internal.commit.RepositoryCommit;
import org.eclipse.egit.ui.internal.commit.RepositoryCommitOpener;
import org.eclipse.egit.ui.internal.dialogs.SpellcheckableMessageArea;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
import org.eclipse.jface.viewers.IElementComparer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.model.IWorkbenchAdapter3;
/**
* Table displaying push operation results.
*/
class PushResultTable {
private static final String EMPTY_STRING = ""; //$NON-NLS-1$
private static final String SPACE = " "; //$NON-NLS-1$
private static final String SASH_WEIGHTS_SETTING = "sashWeights"; //$NON-NLS-1$
private final TreeViewer treeViewer;
private final SashForm root;
private final Image deleteImage;
private ObjectReader reader;
private Repository repo;
private String hookResult;
PushResultTable(final Composite parent) {
this(parent, null);
}
PushResultTable(final Composite parent,
final IDialogSettings dialogSettings) {
root = new SashForm(parent, SWT.VERTICAL);
Composite treeContainer = new Composite(root, SWT.NONE);
GridLayoutFactory.swtDefaults().numColumns(2).applyTo(treeContainer);
treeViewer = new TreeViewer(treeContainer);
treeViewer.setAutoExpandLevel(2);
addToolbar(treeContainer);
ColumnViewerToolTipSupport.enableFor(treeViewer);
final Tree table = treeViewer.getTree();
GridDataFactory.fillDefaults().grab(true, true).applyTo(table);
deleteImage = UIIcons.ELCL16_DELETE.createImage();
UIUtils.hookDisposal(root, deleteImage);
root.addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
saveDialogSettings(root, dialogSettings);
if (reader != null)
reader.close();
}
});
treeViewer.setComparer(new IElementComparer() {
// we need this to keep refresh() working while having custom
// equals() in PushOperationResult
@Override
public boolean equals(Object a, Object b) {
return a == b;
}
@Override
public int hashCode(Object element) {
return element.hashCode();
}
});
final IStyledLabelProvider styleProvider = new WorkbenchStyledLabelProvider() {
@Override
public StyledString getStyledText(Object element) {
if (element instanceof IWorkbenchAdapter3)
return ((IWorkbenchAdapter3) element).getStyledText(element);
return super.getStyledText(element);
}
};
treeViewer.setComparator(new ViewerComparator() {
@Override
public int compare(Viewer viewer, Object e1, Object e2) {
if (e1 instanceof RefUpdateElement
&& e2 instanceof RefUpdateElement) {
RefUpdateElement r1 = (RefUpdateElement) e1;
RefUpdateElement r2 = (RefUpdateElement) e2;
// Put rejected refs first
if (r1.isRejected() && !r2.isRejected())
return -1;
if (!r1.isRejected() && r2.isRejected())
return 1;
// Put new refs next
if (r1.isAdd() && !r2.isAdd())
return -1;
if (!r1.isAdd() && r2.isAdd())
return 1;
// Put branches before tags
if (!r1.isTag() && r2.isTag())
return -1;
if (r1.isTag() && !r2.isTag())
return 1;
Status s1 = r1.getStatus();
Status s2 = r2.getStatus();
// Put up to date refs last
if (s1 != Status.UP_TO_DATE && s2 == Status.UP_TO_DATE)
return -1;
if (s1 == Status.UP_TO_DATE && s2 != Status.UP_TO_DATE)
return 1;
String ref1 = r1.getDstRefName();
String ref2 = r2.getDstRefName();
if (ref1 != null && ref2 != null)
return ref1.compareToIgnoreCase(ref2);
}
// Don't alter commit ordering
if (e1 instanceof RepositoryCommit
&& e2 instanceof RepositoryCommit)
return 0;
return super.compare(viewer, e1, e2);
}
});
treeViewer.setLabelProvider(new DelegatingStyledCellLabelProvider(
styleProvider));
treeViewer.setContentProvider(new RefUpdateContentProvider());
// detail message
Group messageGroup = new Group(root, SWT.NONE);
messageGroup.setText(UIText.PushResultTable_MessageText);
GridLayoutFactory.swtDefaults().applyTo(messageGroup);
GridDataFactory.fillDefaults().grab(true, false).span(2, 1)
.applyTo(messageGroup);
final SpellcheckableMessageArea text = new SpellcheckableMessageArea(
messageGroup, EMPTY_STRING, true, SWT.BORDER) {
@Override
protected void createMarginPainter() {
// Disabled intentionally
}
};
GridDataFactory.fillDefaults().grab(true, true).applyTo(text);
treeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
ISelection selection = event.getSelection();
if (!(selection instanceof IStructuredSelection)) {
text.setText(EMPTY_STRING);
return;
}
IStructuredSelection structuredSelection = (IStructuredSelection) selection;
if (structuredSelection.size() != 1) {
text.setText(EMPTY_STRING);
return;
}
Object selected = structuredSelection.getFirstElement();
if (selected instanceof RefUpdateElement) {
String toShow = getResult((RefUpdateElement) selected);
if (!hookResult.isEmpty()) {
toShow = hookResult + toShow;
}
text.setText(toShow);
}
}
});
initializeSashWeights(root, new int[] { 3, 2 }, dialogSettings);
RepositoryCommitOpener.setup(treeViewer);
}
private void addToolbar(Composite parent) {
ToolBar toolbar = new ToolBar(parent, SWT.VERTICAL);
GridDataFactory.fillDefaults().grab(false, true).applyTo(toolbar);
UIUtils.addExpansionItems(toolbar, treeViewer);
}
private String getResult(RefUpdateElement element) {
StringBuilder result = new StringBuilder(EMPTY_STRING);
PushOperationResult pushOperationResult = element
.getPushOperationResult();
final URIish uri = element.getUri();
result.append(UIText.PushResultTable_repository);
result.append(SPACE);
result.append(uri.toString());
result.append(Text.DELIMITER);
result.append(Text.DELIMITER);
String message = element.getRemoteRefUpdate().getMessage();
if (message != null)
result.append(message).append(Text.DELIMITER);
StringBuilder messagesBuffer = new StringBuilder(pushOperationResult
.getPushResult(uri).getMessages());
trim(messagesBuffer);
if (messagesBuffer.length() > 0)
result.append(messagesBuffer).append(Text.DELIMITER);
trim(result);
return result.toString();
}
private static void trim(StringBuilder s) {
// remove leading line breaks
while (s.length() > 0 && (s.charAt(0) == '\n' || s.charAt(0) == '\r'))
s.deleteCharAt(0);
// remove trailing line breaks
while (s.length() > 0
&& (s.charAt(s.length() - 1) == '\n' || s
.charAt(s.length() - 1) == '\r'))
s.deleteCharAt(s.length() - 1);
}
private static void saveDialogSettings(SashForm sashForm,
IDialogSettings dialogSettings) {
if (dialogSettings != null) {
int[] weights = sashForm.getWeights();
String[] weightStrings = new String[weights.length];
for (int i = 0; i < weights.length; i++) {
weightStrings[i] = String.valueOf(weights[i]);
}
dialogSettings.put(SASH_WEIGHTS_SETTING, weightStrings);
}
}
private static void initializeSashWeights(SashForm sashForm,
int[] defaultValues, IDialogSettings dialogSettings) {
if (dialogSettings != null) {
String[] weightStrings = dialogSettings
.getArray(SASH_WEIGHTS_SETTING);
if (weightStrings != null
&& weightStrings.length == defaultValues.length) {
try {
int[] weights = new int[weightStrings.length];
for (int i = 0; i < weights.length; i++) {
weights[i] = Integer.parseInt(weightStrings[i]);
}
sashForm.setWeights(weights);
return;
} catch (NumberFormatException ignore) { // bad settings
dialogSettings.put(SASH_WEIGHTS_SETTING, (String[]) null);
}
}
}
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;
// Set empty result for a while.
treeViewer.setInput(null);
if (result == null) {
root.layout();
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 (RemoteRefUpdate update : result.getPushResult(uri)
.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) {
treeViewer.setSelection(new StructuredSelection(table.getItem(0)
.getData()));
}
root.layout();
}
Control getControl() {
return root;
}
}