/*******************************************************************************
* Copyright (C) 2010, 2013 Dariusz Luksza <dariusz@luksza.org> and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Mathias Kinzler (SAP AG) - improve UI responsiveness
*******************************************************************************/
package org.eclipse.egit.ui.internal.dialogs;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.JobFamilies;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.CompareUtils;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.ValidationUtils;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableLayout;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.forms.events.ExpansionAdapter;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.model.WorkbenchLabelProvider;
/**
* Dialog for creating and editing tags.
*/
public class CreateTagDialog extends TitleAreaDialog {
private static final int MAX_COMMIT_COUNT = 1000;
/**
* Button id for a "Clear" button.
*/
private static final int CLEAR_ID = 22;
/**
* Button id for "Create Tag and Start Push..." button
*/
private static final int CREATE_AND_START_PUSH_ID = 23;
private String tagName;
private String tagMessage;
private ObjectId tagCommit;
private boolean shouldStartPushWizard = false;
private boolean overwriteTag;
/** Tag object in case an existing annotated tag was entered */
private RevTag existingTag;
private Repository repo;
private Text tagNameText;
private SpellcheckableMessageArea tagMessageText;
private Button overwriteButton;
private TableViewer tagViewer;
private CommitCombo commitCombo;
private Pattern tagNamePattern;
private final String branchName;
private final ObjectId commitId;
private final IInputValidator tagNameValidator;
private final RevWalk rw;
private static class TagLabelProvider extends WorkbenchLabelProvider implements
ITableLabelProvider {
private final Image IMG_TAG;
private final Image IMG_LIGHTTAG;
private TagLabelProvider() {
IMG_TAG = UIIcons.TAG_ANNOTATED.createImage();
IMG_LIGHTTAG = UIIcons.TAG.createImage();
}
@Override
public Image getColumnImage(Object element, int columnIndex) {
// initially, we just display a single String ("Loading...")
if (element instanceof String)
return null;
else if (element instanceof Ref)
return IMG_LIGHTTAG;
else
return IMG_TAG;
}
@Override
public String getColumnText(Object element, int columnIndex) {
// initially, we just display a single String ("Loading...")
if (element instanceof String)
return (String) element;
else if (element instanceof Ref)
return ((Ref) element).getName().substring(10);
else
return ((RevTag) element).getTagName();
}
@Override
public void dispose() {
IMG_TAG.dispose();
IMG_LIGHTTAG.dispose();
super.dispose();
}
}
/**
* Construct dialog to creating or editing tag.
*
* @param parent
* @param branchName
* @param repo
*/
public CreateTagDialog(Shell parent, String branchName, Repository repo) {
super(parent);
this.tagNameValidator = ValidationUtils.getRefNameInputValidator(repo,
Constants.R_TAGS, false);
this.branchName = branchName;
this.commitId = null;
this.repo = repo;
this.rw = new RevWalk(repo);
setHelpAvailable(false);
}
/**
* Construct dialog to creating or editing tag.
*
* @param parent
* @param commitId
* @param repo
*/
public CreateTagDialog(Shell parent, ObjectId commitId, Repository repo) {
super(parent);
this.tagNameValidator = ValidationUtils.getRefNameInputValidator(repo,
Constants.R_TAGS, false);
this.branchName = null;
this.commitId = commitId;
this.repo = repo;
this.rw = new RevWalk(repo);
setHelpAvailable(false);
}
/**
* @return {@link ObjectId} of commit with new or edited tag should be
* associated with
*/
public ObjectId getTagCommit() {
return tagCommit;
}
/**
* @return message for created or edited tag.
*/
public String getTagMessage() {
return tagMessage;
}
/**
* @return name of new tag
*/
public String getTagName() {
return tagName;
}
/**
* Indicates does tag should be forced to update (overwritten) or created.
*
* @return <code>true</code> if tag should be forced to update,
* <code>false</code> if tag should be created
*/
public boolean shouldOverWriteTag() {
return overwriteTag;
}
/**
* @return true if the user wants to start the push wizard after creating
* the tag, false otherwise
*/
public boolean shouldStartPushWizard() {
return shouldStartPushWizard;
}
@Override
protected void configureShell(Shell newShell) {
super.configureShell(newShell);
newShell.setText(UIText.CreateTagDialog_NewTag);
}
private String getTitle() {
String title = ""; //$NON-NLS-1$
if (branchName != null) {
title = NLS.bind(UIText.CreateTagDialog_questionNewTagTitle,
branchName);
} else if (commitId != null) {
title = NLS.bind(UIText.CreateTagDialog_CreateTagOnCommitTitle,
CompareUtils.truncatedRevision(commitId.getName()));
}
return title;
}
@Override
protected void createButtonsForButtonBar(Composite parent) {
parent.setLayout(GridLayoutFactory.swtDefaults().create());
parent.setLayoutData(GridDataFactory.fillDefaults().grab(true, false)
.create());
Button clearButton = createButton(parent, CLEAR_ID,
UIText.CreateTagDialog_clearButton, false);
clearButton.setToolTipText(UIText.CreateTagDialog_clearButtonTooltip);
setButtonLayoutData(clearButton);
Composite margin = new Composite(parent, SWT.NONE);
margin.setLayoutData(GridDataFactory.fillDefaults().grab(true, false)
.create());
Button createTagAndStartPushButton = createButton(parent,
CREATE_AND_START_PUSH_ID, UIText.CreateTagDialog_CreateTagAndStartPushButton, false);
createTagAndStartPushButton
.setToolTipText(UIText.CreateTagDialog_CreateTagAndStartPushToolTip);
setButtonLayoutData(createTagAndStartPushButton);
super.createButtonsForButtonBar(parent);
getButton(OK).setText(UIText.CreateTagDialog_CreateTagButton);
validateInput();
}
@Override
public void create() {
super.create();
// start a job that fills the tag list lazily
Job job = new Job(UIText.CreateTagDialog_GetTagJobName) {
@Override
public boolean belongsTo(Object family) {
if (JobFamilies.FILL_TAG_LIST.equals(family))
return true;
return super.belongsTo(family);
}
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
final List<Object> tags = getRevTags();
PlatformUI.getWorkbench().getDisplay()
.asyncExec(new Runnable() {
@Override
public void run() {
if (!tagViewer.getTable().isDisposed()) {
tagViewer.setInput(tags);
tagViewer.getTable().setEnabled(true);
}
}
});
} catch (IOException e) {
setErrorMessage(UIText.CreateTagDialog_ExceptionRetrievingTagsMessage);
return Activator.createErrorStatus(e.getMessage(), e);
}
return Status.OK_STATUS;
}
};
job.setSystem(true);
job.schedule();
}
@Override
public boolean close() {
rw.dispose();
return super.close();
}
@Override
protected Control createDialogArea(final Composite parent) {
initializeDialogUnits(parent);
setTitle(getTitle());
setMessage(UIText.CreateTagDialog_Message);
Composite composite = (Composite) super.createDialogArea(parent);
final SashForm mainForm = new SashForm(composite, SWT.HORIZONTAL
| SWT.FILL);
mainForm.setLayoutData(GridDataFactory.fillDefaults().grab(true, true)
.create());
createLeftSection(mainForm);
createExistingTagsSection(mainForm);
mainForm.setWeights(new int[] { 70, 30 });
applyDialogFont(parent);
return composite;
}
@Override
protected void buttonPressed(int buttonId) {
if (buttonId == CLEAR_ID) {
tagNameText.setText(""); //$NON-NLS-1$
tagMessageText.setText(""); //$NON-NLS-1$
if (commitCombo != null) {
commitCombo.clearSelection();
}
tagMessageText.getTextWidget().setEditable(true);
overwriteButton.setEnabled(false);
overwriteButton.setSelection(false);
} else if (buttonId == IDialogConstants.OK_ID
|| buttonId == CREATE_AND_START_PUSH_ID) {
shouldStartPushWizard = (buttonId == CREATE_AND_START_PUSH_ID);
// read and store data from widgets
tagName = tagNameText.getText();
if (commitCombo != null)
tagCommit = commitCombo.getValue();
tagMessage = tagMessageText.getCommitMessage();
overwriteTag = overwriteButton.getSelection();
okPressed();
} else {
super.buttonPressed(buttonId);
}
}
@Override
protected boolean isResizable() {
return true;
}
private void createLeftSection(SashForm mainForm) {
Composite left = new Composite(mainForm, SWT.RESIZE);
left.setLayout(GridLayoutFactory.swtDefaults().margins(10, 5).create());
left.setLayoutData(GridDataFactory.fillDefaults().grab(true, true)
.create());
Label label = new Label(left, SWT.WRAP);
label.setText(UIText.CreateTagDialog_tagName);
GridData data = new GridData(GridData.GRAB_HORIZONTAL
| GridData.HORIZONTAL_ALIGN_FILL
| GridData.VERTICAL_ALIGN_CENTER);
data.widthHint = convertHorizontalDLUsToPixels(IDialogConstants.MINIMUM_MESSAGE_AREA_WIDTH / 2);
label.setLayoutData(data);
label.setFont(left.getFont());
tagNameText = new Text(left, SWT.SINGLE | SWT.BORDER | SWT.SEARCH
| SWT.ICON_CANCEL);
tagNameText.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL
| GridData.HORIZONTAL_ALIGN_FILL));
tagNameText.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
String tagNameValue = tagNameText.getText();
tagNamePattern = Pattern.compile(Pattern.quote(tagNameValue),
Pattern.CASE_INSENSITIVE);
tagViewer.refresh();
// Only parse/set tag once (otherwise it would be set twice when
// selecting from the existing tags)
if (existingTag == null
|| !tagNameValue.equals(existingTag.getTagName()))
setExistingTagFromText(tagNameValue);
validateInput();
}
});
UIUtils.addBulbDecorator(tagNameText,
UIText.CreateTagDialog_tagNameToolTip);
new Label(left, SWT.WRAP).setText(UIText.CreateTagDialog_tagMessage);
tagMessageText = new SpellcheckableMessageArea(left, tagMessage);
Point size = tagMessageText.getTextWidget().getSize();
tagMessageText.setLayoutData(GridDataFactory.fillDefaults().hint(size)
.grab(true, true).create());
// allow to tag with ctrl-enter
tagMessageText.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (UIUtils.isSubmitKeyEvent(e)) {
Control button = getButton(IDialogConstants.OK_ID);
// fire OK action only when button is enabled
if (button != null && button.isEnabled())
buttonPressed(IDialogConstants.OK_ID);
}
}
});
tagMessageText.getTextWidget().addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
validateInput();
}
});
overwriteButton = new Button(left, SWT.CHECK);
overwriteButton.setEnabled(false);
overwriteButton.setText(UIText.CreateTagDialog_overwriteTag);
overwriteButton
.setToolTipText(UIText.CreateTagDialog_overwriteTagToolTip);
overwriteButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
validateInput();
}
});
createAdvancedSection(left);
}
private void createAdvancedSection(final Composite composite) {
if (commitId != null)
return;
ExpandableComposite advanced = new ExpandableComposite(composite,
ExpandableComposite.TREE_NODE
| ExpandableComposite.CLIENT_INDENT);
advanced.setText(UIText.CreateTagDialog_advanced);
advanced.setToolTipText(UIText.CreateTagDialog_advancedToolTip);
advanced.setLayoutData(GridDataFactory.fillDefaults().grab(true, false)
.create());
Composite advancedComposite = new Composite(advanced, SWT.WRAP);
advancedComposite.setLayout(GridLayoutFactory.swtDefaults().create());
advancedComposite.setLayoutData(GridDataFactory.fillDefaults()
.grab(true, true).create());
Label advancedLabel = new Label(advancedComposite, SWT.WRAP);
advancedLabel.setText(UIText.CreateTagDialog_advancedMessage);
advancedLabel.setLayoutData(GridDataFactory.fillDefaults()
.grab(true, false).create());
commitCombo = new CommitCombo(advancedComposite, SWT.NORMAL);
commitCombo.setLayoutData(GridDataFactory.fillDefaults()
.grab(true, false).hint(300, SWT.DEFAULT).create());
advanced.setClient(advancedComposite);
advanced.addExpansionListener(new ExpansionAdapter() {
@Override
public void expansionStateChanged(ExpansionEvent e) {
// fill the Combo lazily to improve UI responsiveness
if (((Boolean) e.data).booleanValue()
&& commitCombo.getItemCount() == 0) {
final Collection<RevCommit> commits = new ArrayList<>();
try {
PlatformUI.getWorkbench().getProgressService()
.busyCursorWhile(new IRunnableWithProgress() {
@Override
public void run(IProgressMonitor monitor)
throws InvocationTargetException,
InterruptedException {
getRevCommits(commits);
}
});
} catch (InvocationTargetException e1) {
Activator.logError(e1.getMessage(), e1);
} catch (InterruptedException e1) {
// ignore here
}
for (RevCommit revCommit : commits)
commitCombo.add(revCommit);
// Set combo selection if a tag is selected
if (existingTag != null)
commitCombo.setSelectedElement(existingTag.getObject());
}
composite.layout(true);
composite.getShell().pack();
}
});
}
private void createExistingTagsSection(Composite parent) {
Composite right = new Composite(parent, SWT.NORMAL);
right.setLayout(GridLayoutFactory.swtDefaults().create());
right.setLayoutData(GridDataFactory.fillDefaults().create());
new Label(right, SWT.WRAP).setText(UIText.CreateTagDialog_existingTags);
Table table = new Table(right, SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER
| SWT.SINGLE);
table.setLayoutData(GridDataFactory.fillDefaults().grab(true, true)
.hint(80, 100).create());
TableLayout layout = new TableLayout();
layout.addColumnData(new ColumnWeightData(100, 20));
table.setLayout(layout);
tagViewer = new TableViewer(table);
tagViewer.setLabelProvider(new TagLabelProvider());
tagViewer.setContentProvider(ArrayContentProvider.getInstance());
tagViewer.addSelectionChangedListener(new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
fillTagDialog(event.getSelection());
}
});
tagViewer.addFilter(new ViewerFilter() {
@Override
public boolean select(Viewer viewer, Object parentElement,
Object element) {
if (tagNamePattern == null)
return true;
String name;
if (element instanceof String)
return true;
else if (element instanceof Ref) {
Ref t = (Ref) element;
name = t.getName().substring(10);
} else if (element instanceof RevTag) {
RevTag t = (RevTag) element;
name = t.getTagName();
} else
return true;
return tagNamePattern.matcher(name).find();
}
});
// let's set the table inactive initially and display a "Loading..."
// message and fill the list asynchronously during create() in order to
// improve UI responsiveness
tagViewer
.setInput(new String[] { UIText.CreateTagDialog_LoadingMessageText });
tagViewer.getTable().setEnabled(false);
applyDialogFont(parent);
}
private void validateInput() {
// don't do anything if dialog is disposed
if (getShell() == null) {
return;
}
// validate tag name
String tagNameMessage = tagNameValidator.isValid(tagNameText.getText());
setErrorMessage(tagNameMessage);
String tagMessageVal = tagMessageText.getText().trim();
Control button = getButton(IDialogConstants.OK_ID);
if (button != null) {
boolean containsTagNameAndMessage = (tagNameMessage == null || tagMessageVal
.length() == 0) && tagMessageVal.length() != 0;
boolean shouldOverwriteTag = (overwriteButton.getSelection() && Repository
.isValidRefName(Constants.R_TAGS + tagNameText.getText()));
boolean enabled = containsTagNameAndMessage || shouldOverwriteTag;
button.setEnabled(enabled);
Button createTagAndStartPush = getButton(CREATE_AND_START_PUSH_ID);
if (createTagAndStartPush != null)
createTagAndStartPush.setEnabled(enabled);
}
boolean existingTagSelected = existingTag != null;
if (existingTagSelected && !overwriteButton.getSelection())
tagMessageText.getTextWidget().setEditable(false);
else
tagMessageText.getTextWidget().setEditable(true);
overwriteButton.setEnabled(existingTagSelected);
if (!existingTagSelected)
overwriteButton.setSelection(false);
}
private void fillTagDialog(ISelection actSelection) {
IStructuredSelection selection = (IStructuredSelection) actSelection;
Object firstSelected = selection.getFirstElement();
setExistingTag(firstSelected);
}
private void setExistingTagFromText(String tagName) {
try {
ObjectId tagObjectId = repo.resolve(Constants.R_TAGS + tagName);
if (tagObjectId != null) {
try (RevWalk revWalk = new RevWalk(repo)) {
RevObject tagObject = revWalk.parseAny(tagObjectId);
setExistingTag(tagObject);
}
return;
}
} catch (IOException e) {
// ignore
} catch (RevisionSyntaxException e) {
// ignore
}
setNoExistingTag();
}
private void setNoExistingTag() {
existingTag = null;
}
private void setExistingTag(Object tagObject) {
if (tagObject instanceof RevTag)
existingTag = (RevTag) tagObject;
else {
setNoExistingTag();
setErrorMessage(UIText.CreateTagDialog_LightweightTagMessage);
return;
}
if (!tagNameText.getText().equals(existingTag.getTagName()))
tagNameText.setText(existingTag.getTagName());
if (commitCombo != null)
commitCombo.setSelectedElement(existingTag.getObject());
// handle un-annotated tags
String message = existingTag.getFullMessage();
tagMessageText.setText(null != message ? message : ""); //$NON-NLS-1$
}
private void getRevCommits(Collection<RevCommit> commits) {
try (final RevWalk revWalk = new RevWalk(repo)) {
revWalk.sort(RevSort.COMMIT_TIME_DESC, true);
revWalk.sort(RevSort.BOUNDARY, true);
AnyObjectId headId = repo.resolve(Constants.HEAD);
if (headId != null)
revWalk.markStart(revWalk.parseCommit(headId));
// do the walk to get the commits
long count = 0;
RevCommit commit;
while ((commit = revWalk.next()) != null
&& count < MAX_COMMIT_COUNT) {
commits.add(commit);
count++;
}
} catch (IOException e) {
Activator.logError(UIText.TagAction_errorWhileGettingRevCommits, e);
setErrorMessage(UIText.TagAction_errorWhileGettingRevCommits);
}
}
/**
* @return the annotated tags
* @throws IOException
*/
private List<Object> getRevTags() throws IOException {
List<Object> result = new ArrayList<>();
Collection<Ref> refs = repo.getRefDatabase().getRefs(Constants.R_TAGS)
.values();
for (Ref ref : refs) {
RevObject any;
try {
any = rw.parseAny(repo.resolve(ref.getName()));
} catch (IOException e) {
Activator.handleError(e.getMessage(), e, true);
break;
}
if (any instanceof RevTag)
result.add(any);
else
result.add(ref);
}
return result;
}
}