From 708aa2d18f58c7a9655045b6fe5d0483fde89e6e Mon Sep 17 00:00:00 2001 From: Dariusz Luksza Date: Sun, 11 Apr 2010 22:25:44 +0200 Subject: Add tagging UI support Implements UI for unsigned and annotated tags. This implementation allows the user to select which commit object should be associated with created tag. List of all commits is available in the 'Advanced' section of the dialog. There is a special combo widget that can suggest commits based on match of typed-in text with commit SHA-1 or part of first line of existing commit messages. User can also edit/overwrite tag message or associated commit of an existing tag by selecting it from the list of existing tags and marking the 'Force replace existing tag' check box. By default tag is created for current HEAD, this can be changed in the 'Advanced' section. The tag dialog is available in project's context menu ('Team->Tag...'). [ms]: rebased to master and resolved conflict CQ: 4073 Bug: 311262 Change-Id: Icb30655845d1e1198e59992a10148421ab9f9bd5 Signed-off-by: Dariusz Luksza Signed-off-by: Matthias Sohn --- .../src/org/eclipse/egit/ui/UIText.java | 72 +++ .../src/org/eclipse/egit/ui/UIUtils.java | 25 + .../src/org/eclipse/egit/ui/internal/SWTUtils.java | 7 +- .../eclipse/egit/ui/internal/ValidationUtils.java | 57 +++ .../egit/ui/internal/actions/TagAction.java | 183 +++++++ .../components/RepositorySelectionPage.java | 15 +- .../ui/internal/dialogs/BranchSelectionDialog.java | 27 +- .../egit/ui/internal/dialogs/CommitCombo.java | 223 +++++++++ .../egit/ui/internal/dialogs/CreateTagDialog.java | 553 +++++++++++++++++++++ .../src/org/eclipse/egit/ui/uitext.properties | 28 ++ 10 files changed, 1152 insertions(+), 38 deletions(-) create mode 100644 org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/ValidationUtils.java create mode 100644 org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/actions/TagAction.java create mode 100644 org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitCombo.java create mode 100644 org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CreateTagDialog.java (limited to 'org.eclipse.egit.ui/src/org/eclipse') diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIText.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIText.java index e10cebd56f..575d8fcb9e 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIText.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIText.java @@ -1962,6 +1962,78 @@ public class UIText extends NLS { /** */ public static String Update_update; + /** */ + public static String TagAction_cannotCheckout; + + /** */ + public static String TagAction_cannotGetBranchName; + + /** */ + public static String TagAction_repositoryState; + + /** */ + public static String TagAction_errorCreatingTag; + + /** */ + public static String TagAction_unableToCreateTag; + + /** */ + public static String TagAction_errorDuringTagging; + + /** */ + public static String TagAction_errorWhileGettingRevCommits; + + /** */ + public static String TagAction_unableToResolveHeadObjectId; + + /** */ + public static String TagAction_errorWhileMappingRevTag; + + /** */ + public static String TagAction_creating; + + /** */ + public static String TagAction_taggingFailed; + + /** */ + public static String CreateTagDialog_tagName; + + /** */ + public static String CreateTagDialog_tagMessage; + + /** */ + public static String CreateTagDialog_questionNewTagTitle; + + /** */ + public static String CreateTagDialog_overwriteTag; + + /** */ + public static String CreateTagDialog_overwriteTagToolTip; + + /** */ + public static String CreateTagDialog_existingTags; + + /** */ + public static String CreateTagDialog_advanced; + + /** */ + public static String CreateTagDialog_advancedToolTip; + + /** */ + public static String CreateTagDialog_advancedMessage; + + /** */ + public static String CreateTagDialog_tagNameToolTip; + + /** */ + public static String CreateTagDialog_clearButton; + + /** */ + public static String CreateTagDialog_clearButtonTooltip; + + /** */ + public static String CommitCombo_showSuggestedCommits; + static { initializeMessages("org.eclipse.egit.ui.uitext", UIText.class); //$NON-NLS-1$ } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIUtils.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIUtils.java index 58e3672a25..f811992493 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIUtils.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIUtils.java @@ -10,8 +10,12 @@ *******************************************************************************/ package org.eclipse.egit.ui; +import org.eclipse.jface.fieldassist.ControlDecoration; +import org.eclipse.jface.fieldassist.FieldDecorationRegistry; import org.eclipse.jface.resource.FontRegistry; +import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.widgets.Control; import org.eclipse.ui.PlatformUI; /** @@ -37,4 +41,25 @@ public class UIUtils { .getFontRegistry().getBold(id); } + /** + * Adds little bulb decoration to given control. Bulb will appear in top left + * corner of control after giving focus for this control. + * + * After clicking on bulb image text from tooltip will appear. + * + * @param control instance of {@link Control} object with should be decorated + * @param tooltip text value which should appear after clicking on bulb image. + */ + public static void addBulbDecorator(final Control control, final String tooltip) { + ControlDecoration dec = new ControlDecoration(control, SWT.TOP | SWT.LEFT); + + dec.setImage(FieldDecorationRegistry.getDefault().getFieldDecoration( + FieldDecorationRegistry.DEC_CONTENT_PROPOSAL).getImage()); + + dec.setShowOnlyOnFocus(true); + dec.setShowHover(true); + + dec.setDescriptionText(tooltip); + } + } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/SWTUtils.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/SWTUtils.java index 976bc06879..7b2912017a 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/SWTUtils.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/SWTUtils.java @@ -19,7 +19,12 @@ import org.eclipse.swt.graphics.FontMetrics; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; -import org.eclipse.swt.widgets.*; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; import org.eclipse.ui.dialogs.PreferenceLinkArea; import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer; diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/ValidationUtils.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/ValidationUtils.java new file mode 100644 index 0000000000..5f17754fef --- /dev/null +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/ValidationUtils.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (C) 2010, Dariusz Luksza + * + * 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 + *******************************************************************************/ + +package org.eclipse.egit.ui.internal; + +import java.io.IOException; + +import org.eclipse.egit.ui.Activator; +import org.eclipse.egit.ui.UIText; +import org.eclipse.jface.dialogs.IInputValidator; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.osgi.util.NLS; + +/** + * A collection of validators + */ +public class ValidationUtils { + + /** + * Creates and returns input validator for refNames + * + * @param repo + * @param refPrefix + * @return input validator for refNames + */ + public static IInputValidator getRefNameInputValidator(final Repository repo, final String refPrefix) { + return new IInputValidator() { + public String isValid(String newText) { + if (newText.length() == 0) { + // nothing entered, just don't let the user proceed, + // no need to prompt them with an error message + return ""; //$NON-NLS-1$ + } + + String testFor = refPrefix + newText; + try { + if (repo.resolve(testFor) != null) + return UIText.BranchSelectionDialog_ErrorAlreadyExists; + } catch (IOException e1) { + Activator.logError(NLS.bind( + UIText.BranchSelectionDialog_ErrorCouldNotResolve, testFor), e1); + return e1.getMessage(); + } + if (!Repository.isValidRefName(testFor)) + return UIText.BranchSelectionDialog_ErrorInvalidRefName; + return null; + } + }; + } + +} diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/actions/TagAction.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/actions/TagAction.java new file mode 100644 index 0000000000..ef6d198315 --- /dev/null +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/actions/TagAction.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * Copyright (C) 2010, Dariusz Luksza + * + * 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 + *******************************************************************************/ +package org.eclipse.egit.ui.internal.actions; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +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.core.op.TagOperation; +import org.eclipse.egit.ui.Activator; +import org.eclipse.egit.ui.UIText; +import org.eclipse.egit.ui.internal.ValidationUtils; +import org.eclipse.egit.ui.internal.decorators.GitLightweightDecorator; +import org.eclipse.egit.ui.internal.dialogs.CreateTagDialog; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Tag; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.osgi.util.NLS; + +/** + * An action for creating tag. + * + * @see TagOperation + */ +public class TagAction extends RepositoryAction { + + private Repository repo; + + @Override + public boolean isEnabled() { + return getRepository(false) != null; + } + + @Override + protected void execute(IAction action) throws InvocationTargetException, + InterruptedException { + repo = getRepository(true); + if (repo == null) + return; + + if (!repo.getRepositoryState().canCheckout()) { + MessageDialog.openError(getShell(), + UIText.TagAction_cannotCheckout, NLS.bind( + UIText.TagAction_repositoryState, repo + .getRepositoryState().getDescription())); + return; + } + + String currentBranchName; + try { + currentBranchName = repo.getBranch(); + } catch (IOException e) { + throw new InvocationTargetException(e, + UIText.TagAction_cannotGetBranchName); + } + + CreateTagDialog dialog = new CreateTagDialog(getShell(), + ValidationUtils + .getRefNameInputValidator(repo, Constants.R_TAGS), + currentBranchName); + + // get and set commits + RevWalk revCommits = getRevCommits(); + dialog.setRevCommitList(revCommits); + + // get and set existing tags + List tags = getRevTags(); + dialog.setExistingTags(tags); + + if (dialog.open() != IDialogConstants.OK_ID) + return; + + final Tag tag = new Tag(repo); + PersonIdent personIdent = new PersonIdent(repo); + String tagName = dialog.getTagName(); + + tag.setTag(tagName); + tag.setTagger(personIdent); + tag.setMessage(dialog.getTagMessage()); + + ObjectId tagCommit = getTagCommit(dialog.getTagCommit()); + tag.setObjId(tagCommit); + + String tagJobName = NLS.bind(UIText.TagAction_creating, tagName); + final boolean shouldMoveTag = dialog.shouldOverWriteTag(); + + Job tagJob = new Job(tagJobName) { + protected IStatus run(IProgressMonitor monitor) { + try { + new TagOperation(repo, tag, shouldMoveTag).execute(monitor); + } catch (CoreException e) { + return Activator.createErrorStatus( + UIText.TagAction_taggingFailed, e); + } finally { + GitLightweightDecorator.refresh(); + } + + return Status.OK_STATUS; + } + + }; + + tagJob.setUser(true); + tagJob.schedule(); + } + + private List getRevTags() { + Collection revTags = repo.getTags().values(); + List tags = new ArrayList(); + for (Ref ref : revTags) { + try { + Tag tag = repo.mapTag(ref.getName()); + tags.add(tag); + } catch (IOException e) { + ErrorDialog.openError(getShell(), + UIText.TagAction_errorDuringTagging, NLS.bind( + UIText.TagAction_errorWhileMappingRevTag, ref + .getName()), new Status(IStatus.ERROR, + Activator.getPluginId(), e.getMessage(), e)); + } + } + return tags; + } + + private RevWalk getRevCommits() { + RevWalk revWalk = new RevWalk(repo); + revWalk.sort(RevSort.COMMIT_TIME_DESC, true); + revWalk.sort(RevSort.BOUNDARY, true); + + try { + AnyObjectId headId = repo.resolve(Constants.HEAD); + if (headId != null) + revWalk.markStart(revWalk.parseCommit(headId)); + } catch (IOException e) { + ErrorDialog.openError(getShell(), + UIText.TagAction_errorDuringTagging, + UIText.TagAction_errorWhileGettingRevCommits, new Status( + IStatus.ERROR, Activator.getPluginId(), e + .getMessage(), e)); + } + + return revWalk; + } + + private ObjectId getTagCommit(ObjectId objectId) + throws InvocationTargetException { + ObjectId result = null; + if (objectId == null) { + try { + result = repo.resolve(Constants.HEAD); + } catch (IOException e) { + throw new InvocationTargetException(e, + UIText.TagAction_unableToResolveHeadObjectId); + } + } else { + result = objectId; + } + return result; + } +} diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RepositorySelectionPage.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RepositorySelectionPage.java index d45e0fc040..694e55e85b 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RepositorySelectionPage.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RepositorySelectionPage.java @@ -24,9 +24,8 @@ import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.egit.ui.Activator; import org.eclipse.egit.ui.UIText; +import org.eclipse.egit.ui.UIUtils; import org.eclipse.jface.fieldassist.ContentProposalAdapter; -import org.eclipse.jface.fieldassist.ControlDecoration; -import org.eclipse.jface.fieldassist.FieldDecorationRegistry; import org.eclipse.jface.fieldassist.IContentProposal; import org.eclipse.jface.fieldassist.IContentProposalProvider; import org.eclipse.jface.fieldassist.TextContentAdapter; @@ -847,17 +846,7 @@ public class RepositorySelectionPage extends BaseWizardPage { private void addContentProposalToUriText(Text uriTextField) { - ControlDecoration dec = new ControlDecoration(uriTextField, SWT.TOP - | SWT.LEFT); - - dec.setImage(FieldDecorationRegistry.getDefault().getFieldDecoration( - FieldDecorationRegistry.DEC_CONTENT_PROPOSAL).getImage()); - - dec.setShowOnlyOnFocus(true); - dec.setShowHover(true); - - dec - .setDescriptionText(UIText.RepositorySelectionPage_ShowPreviousURIs_HoverText); + UIUtils.addBulbDecorator(uriTextField, UIText.RepositorySelectionPage_ShowPreviousURIs_HoverText); IContentProposalProvider cp = new IContentProposalProvider() { diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/BranchSelectionDialog.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/BranchSelectionDialog.java index 07723587d8..171d2c6c4d 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/BranchSelectionDialog.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/BranchSelectionDialog.java @@ -1,4 +1,4 @@ -/******************************************************************************* +/******************************************************************************* * Copyright (C) 2007, Dave Watson * Copyright (C) 2007, Robin Rosenberg * Copyright (C) 2007, Robin Rosenberg @@ -22,9 +22,9 @@ import org.eclipse.egit.ui.internal.repository.RepositoriesViewContentProvider; import org.eclipse.egit.ui.internal.repository.RepositoriesViewLabelProvider; import org.eclipse.egit.ui.internal.repository.RepositoryTreeNode; import org.eclipse.egit.ui.internal.repository.RepositoryTreeNode.RepositoryTreeNodeType; +import org.eclipse.egit.ui.internal.ValidationUtils; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogConstants; -import org.eclipse.jface.dialogs.IInputValidator; import org.eclipse.jface.dialogs.InputDialog; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.layout.GridDataFactory; @@ -308,28 +308,7 @@ public class BranchSelectionDialog extends Dialog { getShell(), UIText.BranchSelectionDialog_QuestionNewBranchTitle, prompt, - null, new IInputValidator() { - public String isValid(String newText) { - if (newText.length() == 0) { - // nothing entered, just don't let the user proceed, - // no need to prompt them with an error message - return ""; //$NON-NLS-1$ - } - - String testFor = refPrefix + newText; - try { - if (repo.resolve(testFor) != null) - return UIText.BranchSelectionDialog_ErrorAlreadyExists; - } catch (IOException e1) { - Activator.logError(NLS.bind( - UIText.BranchSelectionDialog_ErrorCouldNotResolve, testFor), e1); - return e1.getMessage(); - } - if (!Repository.isValidRefName(testFor)) - return UIText.BranchSelectionDialog_ErrorInvalidRefName; - return null; - } - }); + null, ValidationUtils.getRefNameInputValidator(repo, refPrefix)); labelDialog.setBlockOnOpen(true); return labelDialog; } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitCombo.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitCombo.java new file mode 100644 index 0000000000..bfd7a74e96 --- /dev/null +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitCombo.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright (C) 2010, Dariusz Luksza + * + * 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 + *******************************************************************************/ +package org.eclipse.egit.ui.internal.dialogs; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.egit.ui.UIText; +import org.eclipse.egit.ui.UIUtils; +import org.eclipse.jface.fieldassist.ComboContentAdapter; +import org.eclipse.jface.fieldassist.ContentProposalAdapter; +import org.eclipse.jface.fieldassist.IContentProposal; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.FocusAdapter; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; + +/** + * This is an extended version of {@link Combo} widget with is specialized in + * displaying commits and simplifying selection process. + * + * It is integrated with {@link ContentProposalAdapter} that helps select + * preferred tag by user. To get activate proposal provider simply just start + * writing commit SHA-1 or part of commit's message first line + */ +public class CommitCombo extends Composite { + + private final List commits; + + private final Combo combo; + + private class ComboCommitEnt { + + private final String message; + + private final ObjectId objectId; + + public ComboCommitEnt(ObjectId objecId, String message) { + this.objectId = objecId; + this.message = message; + } + + } + + private class CommitContentProposalProvider implements + IContentProposalProvider { + + public IContentProposal[] getProposals(String contents, int position) { + List list = new ArrayList(); + Pattern pattern = Pattern.compile(contents, + Pattern.CASE_INSENSITIVE); + for (int i = 0; i < commits.size(); i++) { + String message = commits.get(i).message; + if (message.length() >= contents.length() + && pattern.matcher(message).find()) { + list.add(makeContentProposal(message)); + } + } + return list.toArray(new IContentProposal[] {}); + } + + /* + * Make an IContentProposal for showing the specified String. + */ + private IContentProposal makeContentProposal(final String proposal) { + return new IContentProposal() { + public String getContent() { + return proposal; + } + + public String getDescription() { + return null; + } + + public String getLabel() { + return null; + } + + public int getCursorPosition() { + return proposal.length(); + } + }; + } + } + + /** + * Constructs a new instance of this class given its parent and a style + * value describing its behavior and appearance. + * + * @param parent + * a widget which will be the parent of the new instance (cannot + * be null) + * @param style + * the SWT style bits + */ + public CommitCombo(Composite parent, int style) { + super(parent, style); + + combo = new Combo(this, SWT.DROP_DOWN); + commits = new ArrayList(); + + setLayout(GridLayoutFactory.swtDefaults().create()); + setLayoutData(GridDataFactory.fillDefaults().create()); + + GridData totalLabelData = new GridData(); + totalLabelData.horizontalAlignment = SWT.FILL; + totalLabelData.grabExcessHorizontalSpace = true; + combo.setLayoutData(totalLabelData); + combo.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + if (null == getValue()) + combo.setText(""); //$NON-NLS-1$ + } + }); + + UIUtils + .addBulbDecorator(combo, + UIText.CommitCombo_showSuggestedCommits); + + ContentProposalAdapter adapter = new ContentProposalAdapter(combo, + new ComboContentAdapter(), new CommitContentProposalProvider(), + null, null); + adapter.setPropagateKeys(true); + adapter + .setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); + } + + /** + * Add a {@link RevCommit} to widget. + * + * @param revCommit + */ + public void add(RevCommit revCommit) { + Assert.isNotNull(revCommit); + checkWidget(); + + String shortSha1 = revCommit.getName().substring(0, 8); + String message = shortSha1 + ": " + revCommit.getShortMessage(); //$NON-NLS-1$ + combo.add(message); + commits.add(new ComboCommitEnt(revCommit.getId(), message)); + } + + /** + * Returns value of SHA-1 for selected commit. + * + * @param index + * index of item in check box + * @return SHA-1 of selected commit + */ + public ObjectId getItem(int index) { + checkWidget(); + + if (!(0 <= index && index < commits.size())) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + return commits.get(index).objectId; + } + + /** + * @return index of selected element + */ + public int getSelectedIndex() { + return combo.getSelectionIndex(); + } + + /** + * @return SHA-1 of selected commit + */ + public ObjectId getValue() { + int selectionIndex = combo.getSelectionIndex(); + return -1 != selectionIndex ? getItem(selectionIndex) : null; + } + + /** + * Selects the item with is associated with given objectId + * + * @param objectId + */ + public void setSelectedElement(ObjectId objectId) { + if (objectId == null) { + return; + } + + for (int i = 0; i < commits.size(); i++) + if (objectId.equals(commits.get(i).objectId)) { + combo.select(i); + break; + } + } + + @Override + public void setEnabled(boolean enabled) { + combo.setEnabled(enabled); + super.setEnabled(enabled); + } + + /** + * Sets the selection in the receiver's text field to an empty selection + * starting just before the first character. If the text field is editable, + * this has the effect of placing the i-beam at the start of the text. + */ + public void clearSelection() { + combo.clearSelection(); + combo.setText(""); //$NON-NLS-1$ + } + +} diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CreateTagDialog.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CreateTagDialog.java new file mode 100644 index 0000000000..6bc60318c2 --- /dev/null +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CreateTagDialog.java @@ -0,0 +1,553 @@ +/******************************************************************************* + * Copyright (C) 2010, Dariusz Luksza + * + * 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 + *******************************************************************************/ +package org.eclipse.egit.ui.internal.dialogs; + +import java.util.List; +import java.util.regex.Pattern; + +import org.eclipse.egit.ui.UIText; +import org.eclipse.egit.ui.UIUtils; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.IInputValidator; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.resource.StringConverter; +import org.eclipse.jface.viewers.ColumnWeightData; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.LabelProvider; +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.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Tag; +import org.eclipse.jgit.revwalk.RevCommit; +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.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.forms.events.ExpansionAdapter; +import org.eclipse.ui.forms.events.ExpansionEvent; +import org.eclipse.ui.forms.widgets.ExpandableComposite; +import org.eclipse.ui.model.IWorkbenchAdapter; +import org.eclipse.ui.model.WorkbenchContentProvider; +import org.eclipse.ui.model.WorkbenchLabelProvider; + +/** + * Dialog for creating and editing tags. + * + */ +public class CreateTagDialog extends Dialog { + + /** + * Button id for a "Clear" button (value 22). + */ + public static final int CLEAR_ID = 22; + + private String tagName; + + private String tagMessage; + + private ObjectId tagCommit; + + private boolean overwriteTag; + + private RevWalk revCommits; + + private List existingTags; + + private Tag tag; + + private Text tagNameText; + + private Text tagMessageText; + + private Text tagNameErrorText; + + private Button overwriteButton; + + private TableViewer tagViewer; + + private CommitCombo commitCombo; + + private Pattern tagNamePattern; + + private final String branchName; + + private final IInputValidator tagNameValidator; + + class TagInputList extends LabelProvider implements IWorkbenchAdapter { + + private final List tagList; + + public TagInputList(List tagList) { + this.tagList = tagList; + } + + public Object[] getChildren(Object o) { + return tagList.toArray(new Object[] {}); + } + + public ImageDescriptor getImageDescriptor(Object object) { + return null; + } + + public String getLabel(Object o) { + if (o instanceof Tag) + return ((Tag) o).getTag(); + + return null; + } + + public Object getParent(Object o) { + return null; + } + + public Object getAdapter(Class adapter) { + if (adapter == IWorkbenchAdapter.class) + return this; + + return null; + } + } + + class TagLabelProvider extends WorkbenchLabelProvider implements + ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + return ((Tag) element).getTag(); + } + + } + + /** + * Construct dialog to creating or editing tag. + * + * @param parent + * @param tagNameValidator + * @param branchName + */ + public CreateTagDialog(Shell parent, IInputValidator tagNameValidator, + String branchName) { + super(parent); + this.tagNameValidator = tagNameValidator; + this.branchName = branchName; + } + + /** + * @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 true if tag should be forced to update, + * false if tag should be created + */ + public boolean shouldOverWriteTag() { + return overwriteTag; + } + + /** + * Sets list of already existing tags. This list will be loaded in + * Details section of this dialog. + * + * @param existingTags + */ + public void setExistingTags(List existingTags) { + this.existingTags = existingTags; + } + + /** + * Sets list of existing commits. This list will be loaded in + * {@link CommitCombo} widget in Advanced section of this + * dialog. + * + * @param revCommits + */ + public void setRevCommitList(RevWalk revCommits) { + this.revCommits = revCommits; + } + + /** + * Data from tag argument will be set in this dialog box. + * + * @param tag + */ + public void setTag(Tag tag) { + this.tag = tag; + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + + if (branchName != null) { + newShell.setText(NLS.bind( + UIText.CreateTagDialog_questionNewTagTitle, branchName)); + } + + newShell.setMinimumSize(703, 345); + } + + @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()); + + super.createButtonsForButtonBar(parent); + + validateInput(); + } + + @Override + protected Control createDialogArea(final Composite parent) { + initializeDialogUnits(parent); + + 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 }); + if (tag != null) { + setTagImpl(); + } + + applyDialogFont(parent); + return composite; + } + + @Override + protected void buttonPressed(int buttonId) { + switch (buttonId) { + case CLEAR_ID: + tagNameText.setText(""); //$NON-NLS-1$ + tagMessageText.setText(""); //$NON-NLS-1$ + commitCombo.clearSelection(); + + commitCombo.setEnabled(true); + tagNameText.setEnabled(true); + tagMessageText.setEnabled(true); + overwriteButton.setEnabled(false); + overwriteButton.setSelection(false); + break; + case IDialogConstants.OK_ID: + // read and store data from widgets + tagName = tagNameText.getText(); + tagCommit = commitCombo.getValue(); + tagMessage = tagMessageText.getText(); + overwriteTag = overwriteButton.getSelection(); + //$FALL-THROUGH$ continue propagating OK button action + default: + 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); + label.setLayoutData(data); + label.setFont(left.getFont()); + + tagNameText = new Text(left, SWT.SINGLE | SWT.BORDER); + tagNameText.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL + | GridData.HORIZONTAL_ALIGN_FILL)); + tagNameText.addModifyListener(new ModifyListener() { + + public void modifyText(ModifyEvent e) { + String textValue = Pattern.quote(tagNameText.getText()); + tagNamePattern = Pattern.compile(textValue, + Pattern.CASE_INSENSITIVE); + tagViewer.refresh(); + validateInput(); + } + }); + + UIUtils.addBulbDecorator(tagNameText, + UIText.CreateTagDialog_tagNameToolTip); + + tagNameErrorText = new Text(left, SWT.READ_ONLY | SWT.WRAP); + tagNameErrorText.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL + | GridData.HORIZONTAL_ALIGN_FILL)); + tagNameErrorText.setBackground(tagNameErrorText.getDisplay() + .getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); + + new Label(left, SWT.WRAP).setText(UIText.CreateTagDialog_tagMessage); + + tagMessageText = new Text(left, SWT.MULTI | SWT.BORDER | SWT.V_SCROLL); + tagMessageText.setLayoutData(GridDataFactory.fillDefaults().minSize(50, + 50).grab(true, true).create()); + + // key listener taken from CommitDialog.createDialogArea() allow to + // commit with ctrl-enter + tagMessageText.addKeyListener(new KeyAdapter() { + public void keyPressed(KeyEvent arg0) { + if (arg0.keyCode == SWT.CR + && (arg0.stateMask & SWT.CONTROL) > 0) { + Control button = getButton(IDialogConstants.OK_ID); + // fire OK action only when button is enabled + if (button != null && button.isEnabled()) + buttonPressed(IDialogConstants.OK_ID); + } else if (arg0.keyCode == SWT.TAB + && (arg0.stateMask & SWT.SHIFT) == 0) { + arg0.doit = false; + tagMessageText.traverse(SWT.TRAVERSE_TAB_NEXT); + } + 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) { + boolean state = overwriteButton.getSelection(); + tagNameText.setEnabled(state); + commitCombo.setEnabled(state); + tagMessageText.setEnabled(state); + validateInput(); + } + }); + + createAdvancedSection(left); + } + + private void createAdvancedSection(final Composite composite) { + 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).create()); + + for (RevCommit revCommit : revCommits) + commitCombo.add(revCommit); + + advanced.setClient(advancedComposite); + advanced.addExpansionListener(new ExpansionAdapter() { + public void expansionStateChanged(ExpansionEvent e) { + composite.layout(); + } + }); + } + + private void createExistingTagsSection(Composite parent) { + Composite right = new Composite(parent, SWT.NORMAL); + right.setLayout(GridLayoutFactory.swtDefaults().create()); + right.setLayoutData(GridLayoutFactory.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(new WorkbenchContentProvider()); + tagViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + fillTagDialog(); + } + }); + + tagViewer.setInput(new TagInputList(existingTags)); + tagViewer.addFilter(new ViewerFilter() { + + @Override + public boolean select(Viewer viewer, Object parentElement, + Object element) { + Tag tag = (Tag) element; + + if (tagNamePattern != null) + return tagNamePattern.matcher(tag.getTag()).find(); + else + return true; + } + }); + + applyDialogFont(parent); + } + + private void validateInput() { + // don't validate if dialog is disposed + if (getShell() == null) { + return; + } + + // validate tag name + String tagNameMessage = tagNameValidator.isValid(tagNameText.getText()); + setTagNameError(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())); + + button.setEnabled(containsTagNameAndMessage || shouldOverwriteTag); + } + } + + private void fillTagDialog() { + IStructuredSelection selection = (IStructuredSelection) tagViewer + .getSelection(); + Object firstSelected = selection.getFirstElement(); + + if (firstSelected instanceof Tag) { + tag = (Tag) firstSelected; + + if (!overwriteButton.isEnabled()) { + String tagMessageValue = tag.getMessage(); + // don't enable OK button if we are dealing with un-annotated + // tag because JGit doesn't support them + if (tagMessageValue != null + && tagMessageValue.trim().length() != 0) + overwriteButton.setEnabled(true); + + tagNameText.setEnabled(false); + commitCombo.setEnabled(false); + tagMessageText.setEnabled(false); + } + + setTagImpl(); + } + } + + private void setTagImpl() { + tagNameText.setText(tag.getTag()); + commitCombo.setSelectedElement(tag.getObjId()); + + // handle un-annotated tags + String message = tag.getMessage(); + tagMessageText.setText(null != message ? message : ""); //$NON-NLS-1$ + } + + private void setTagNameError(String tagNameMessage) { + // copied form + // org.eclipse.jface.dialogs.InputDialog.setErrorMessage(String) + if (tagNameErrorText != null && !tagNameErrorText.isDisposed()) { + tagNameErrorText + .setText(tagNameMessage == null ? " \n " : tagNameMessage); //$NON-NLS-1$ + // Disable the error message text control if there is no error, or + // no error text (empty or whitespace only). Hide it also to avoid + // color change. + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=130281 + boolean hasError = tagNameMessage != null + && (StringConverter.removeWhiteSpaces(tagNameMessage)) + .length() > 0; + tagNameErrorText.setEnabled(hasError); + tagNameErrorText.setVisible(hasError); + tagNameErrorText.getParent().update(); + } + } + +} diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/uitext.properties b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/uitext.properties index 3d26a4c08f..232a28330a 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/uitext.properties +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/uitext.properties @@ -514,6 +514,7 @@ BranchSelectionDialog_NewBranch=&New branch BranchSelectionDialog_OkCheckout=&Checkout BranchSelectionDialog_OkReset=&Reset BranchSelectionDialog_QuestionNewBranchMessage=Enter name of the new branch. It will branch from the selected branch {0}. {1} will be prepended to the name you type +BranchSelectionDialog_QuestionNewBranchNameMessage=Enter new name of the {0} branch. refs/heads/ will be prepended to the name you type BranchSelectionDialog_QuestionNewBranchNameMessage=Enter new name of the {0} branch. {1} will be prepended to the name you type BranchSelectionDialog_QuestionNewBranchTitle=New branch BranchSelectionDialog_QuestionNewBranchNameTitle=Rename branch @@ -699,3 +700,30 @@ UIIcons_errorDeterminingIconBase=Can't determine icon base. UIIcons_errorLoadingPluginImage=Can't load plugin image. Untrack_untrack=Untrack Update_update=Update + +TagAction_cannotCheckout=Cannot checkout now +TagAction_cannotGetBranchName=Cannot get actual branch name +TagAction_repositoryState=Cannot checkout repository because it is in state: {0} +TagAction_errorCreatingTag=Error while creating tag {0} +TagAction_unableToCreateTag=Unable to create tag {0} +TagAction_errorDuringTagging=Error during tagging +TagAction_errorWhileMappingRevTag=Unable to get information about {0} tag. +TagAction_errorWhileGettingRevCommits=An error occurred while getting list of commits. +TagAction_unableToResolveHeadObjectId=Unable to resolve object id associated with current HEAD. +TagAction_creating=Creating {0} tag. +TagAction_taggingFailed=Tagging failed + +CreateTagDialog_tagName=Tag &name*: +CreateTagDialog_tagMessage=Tag &message*: +CreateTagDialog_questionNewTagTitle=Create new tag on branch "{0}" +CreateTagDialog_overwriteTag=Force &replace existing tag +CreateTagDialog_overwriteTagToolTip=Select this option if you want to change message or commit associated with already existing tag. +CreateTagDialog_existingTags=Existing tags: +CreateTagDialog_advanced=&Advanced +CreateTagDialog_advancedToolTip=In the advanced section you may choose the commit to be tagged. +CreateTagDialog_advancedMessage=Choose commit that should be associated with this tag. +CreateTagDialog_tagNameToolTip=Start typing tag name to filter list of existing tags. +CreateTagDialog_clearButton=C&lear +CreateTagDialog_clearButtonTooltip=Clear all dialog fields. + +CommitCombo_showSuggestedCommits=Start typing SHA-1 of existing commit or part of first line in commit message to see suggested commits. -- cgit v1.2.3