/******************************************************************************* * Copyright (c) 2011, 2016 GitHub Inc. 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: * Kevin Sawicki (GitHub Inc.) - initial API and implementation * Thomas Wolf <thomas.wolf@paranor.ch> - preference-based date formatting *******************************************************************************/ package org.eclipse.egit.ui.internal.commit; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.PlatformObject; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.egit.core.AdapterUtils; import org.eclipse.egit.ui.Activator; import org.eclipse.egit.ui.UIPreferences; import org.eclipse.egit.ui.UIUtils; import org.eclipse.egit.ui.internal.GitLabelProvider; import org.eclipse.egit.ui.internal.PreferenceBasedDateFormatter; import org.eclipse.egit.ui.internal.UIIcons; import org.eclipse.egit.ui.internal.UIText; import org.eclipse.egit.ui.internal.dialogs.SpellcheckableMessageArea; import org.eclipse.egit.ui.internal.history.CommitFileDiffViewer; import org.eclipse.egit.ui.internal.history.FileDiff; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.jgit.annotations.NonNull; 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.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalkUtils; import org.eclipse.jgit.util.GitDateFormatter; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CLabel; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.PartInitException; import org.eclipse.ui.forms.IFormColors; import org.eclipse.ui.forms.IManagedForm; import org.eclipse.ui.forms.editor.FormEditor; import org.eclipse.ui.forms.editor.FormPage; import org.eclipse.ui.forms.events.ExpansionAdapter; import org.eclipse.ui.forms.events.ExpansionEvent; import org.eclipse.ui.forms.events.HyperlinkAdapter; import org.eclipse.ui.forms.events.HyperlinkEvent; import org.eclipse.ui.forms.widgets.AbstractHyperlink; import org.eclipse.ui.forms.widgets.ExpandableComposite; import org.eclipse.ui.forms.widgets.FormToolkit; import org.eclipse.ui.forms.widgets.Hyperlink; import org.eclipse.ui.forms.widgets.ScrolledForm; import org.eclipse.ui.forms.widgets.Section; import org.eclipse.ui.part.IShowInSource; import org.eclipse.ui.part.ShowInContext; /** * Commit editor page class displaying author, committer, parent commits, * message, and file information in form sections. */ public class CommitEditorPage extends FormPage implements ISchedulingRule, IShowInSource { private static final String SIGNED_OFF_BY = "Signed-off-by: {0} <{1}>"; //$NON-NLS-1$ /** * Abbreviated length of parent id links displayed */ public static final int PARENT_LENGTH = 20; private LocalResourceManager resources = new LocalResourceManager( JFaceResources.getResources()); private Composite tagLabelArea; private Section branchSection; private TableViewer branchViewer; private Section diffSection; private CommitFileDiffViewer diffViewer; private FocusTracker focusTracker = new FocusTracker(); /** * Create commit editor page * * @param editor */ public CommitEditorPage(FormEditor editor) { this(editor, "commitPage", UIText.CommitEditorPage_Title); //$NON-NLS-1$ } /** * Create commit editor page * * @param editor * @param id * @param title */ public CommitEditorPage(FormEditor editor, String id, String title) { super(editor, id, title); } /** * Add the given {@link Control} to this form's focus tracking. * * @param control * to add to focus tracking */ protected void addToFocusTracking(@NonNull Control control) { focusTracker.addToFocusTracking(control); } private void addSectionTextToFocusTracking(@NonNull Section composite) { for (Control control : composite.getChildren()) { if (control instanceof AbstractHyperlink) { addToFocusTracking(control); } } } private void hookExpansionGrabbing(final Section section) { section.addExpansionListener(new ExpansionAdapter() { @Override public void expansionStateChanged(ExpansionEvent e) { ((GridData) section.getLayoutData()).grabExcessVerticalSpace = e .getState(); getManagedForm().getForm().getBody().layout(true, true); } }); } private Image getImage(ImageDescriptor descriptor) { return (Image) this.resources.get(descriptor); } Section createSection(Composite parent, FormToolkit toolkit, String title, int span) { Section section = toolkit.createSection(parent, ExpandableComposite.TITLE_BAR | ExpandableComposite.TWISTIE | ExpandableComposite.EXPANDED); GridDataFactory.fillDefaults().span(span, 1).grab(true, true) .applyTo(section); section.setText(title); addSectionTextToFocusTracking(section); return section; } Composite createSectionClient(Section parent, FormToolkit toolkit) { Composite client = toolkit.createComposite(parent); GridLayoutFactory.fillDefaults().extendedMargins(2, 2, 2, 2) .applyTo(client); return client; } private boolean isSignedOffBy(PersonIdent person) { RevCommit commit = getCommit().getRevCommit(); return commit.getFullMessage().indexOf(getSignedOffByLine(person)) != -1; } private String getSignedOffByLine(PersonIdent person) { return MessageFormat.format(SIGNED_OFF_BY, person.getName(), person.getEmailAddress()); } private void setPerson(Text text, PersonIdent person, boolean isAuthor) { PreferenceBasedDateFormatter formatter = PreferenceBasedDateFormatter .create(); boolean isRelative = formatter .getFormat() == GitDateFormatter.Format.RELATIVE; String textTemplate = null; if (isAuthor) { textTemplate = isRelative ? UIText.CommitEditorPage_LabelAuthorRelative : UIText.CommitEditorPage_LabelAuthor; } else { textTemplate = isRelative ? UIText.CommitEditorPage_LabelCommitterRelative : UIText.CommitEditorPage_LabelCommitter; } text.setText(MessageFormat.format(textTemplate, person.getName(), person.getEmailAddress(), formatter.formatDate(person))); } private Composite createUserArea(Composite parent, FormToolkit toolkit, PersonIdent person, boolean author) { Composite userArea = toolkit.createComposite(parent); GridLayoutFactory.fillDefaults().spacing(2, 2).numColumns(3) .applyTo(userArea); Label userLabel = toolkit.createLabel(userArea, null); userLabel.setImage(getImage(author ? UIIcons.ELCL16_AUTHOR : UIIcons.ELCL16_COMMITTER)); if (author) userLabel.setToolTipText(UIText.CommitEditorPage_TooltipAuthor); else userLabel.setToolTipText(UIText.CommitEditorPage_TooltipCommitter); boolean signedOff = isSignedOffBy(person); final Text userText = new Text(userArea, SWT.FLAT | SWT.READ_ONLY); addToFocusTracking(userText); setPerson(userText, person, author); toolkit.adapt(userText, false, false); userText.setData(FormToolkit.KEY_DRAW_BORDER, Boolean.FALSE); IPropertyChangeListener uiPrefsListener = (event) -> { String property = event.getProperty(); if (UIPreferences.DATE_FORMAT.equals(property) || UIPreferences.DATE_FORMAT_CHOICE.equals(property)) { setPerson(userText, person, author); userArea.layout(); } }; Activator.getDefault().getPreferenceStore().addPropertyChangeListener(uiPrefsListener); userText.addDisposeListener((e) -> { Activator.getDefault().getPreferenceStore() .removePropertyChangeListener(uiPrefsListener); }); GridDataFactory.fillDefaults().span(signedOff ? 1 : 2, 1) .applyTo(userText); if (signedOff) { Label signedOffLabel = toolkit.createLabel(userArea, null); signedOffLabel.setImage(getImage(UIIcons.SIGNED_OFF)); if (author) signedOffLabel .setToolTipText(UIText.CommitEditorPage_TooltipSignedOffByAuthor); else signedOffLabel .setToolTipText(UIText.CommitEditorPage_TooltipSignedOffByCommitter); } return userArea; } void updateSectionClient(Section section, Composite client, FormToolkit toolkit) { hookExpansionGrabbing(section); toolkit.paintBordersFor(client); section.setClient(client); } private void createHeaderArea(Composite parent, FormToolkit toolkit, int span) { RevCommit commit = getCommit().getRevCommit(); Composite top = toolkit.createComposite(parent); GridDataFactory.fillDefaults().grab(true, false).span(span, 1) .applyTo(top); GridLayoutFactory.fillDefaults().numColumns(2).applyTo(top); Composite userArea = toolkit.createComposite(top); GridLayoutFactory.fillDefaults().spacing(2, 2).numColumns(1) .applyTo(userArea); GridDataFactory.fillDefaults().grab(true, false).applyTo(userArea); PersonIdent author = commit.getAuthorIdent(); if (author != null) createUserArea(userArea, toolkit, author, true); PersonIdent committer = commit.getCommitterIdent(); if (committer != null && !committer.equals(author)) createUserArea(userArea, toolkit, committer, false); int count = commit.getParentCount(); if (count > 0) createParentsArea(top, toolkit, commit); createTagsArea(userArea, toolkit, 2); } private void createParentsArea(Composite parent, FormToolkit toolkit, RevCommit commit) { Composite parents = toolkit.createComposite(parent); GridLayoutFactory.fillDefaults().spacing(2, 2).numColumns(2) .applyTo(parents); GridDataFactory.fillDefaults().grab(false, false).applyTo(parents); for (int i = 0; i < commit.getParentCount(); i++) { final RevCommit parentCommit = commit.getParent(i); toolkit.createLabel(parents, getParentCommitLabel(i)) .setForeground( toolkit.getColors().getColor(IFormColors.TB_TOGGLE)); final Hyperlink link = toolkit .createHyperlink(parents, parentCommit.abbreviate(PARENT_LENGTH).name(), SWT.NONE); link.addHyperlinkListener(new HyperlinkAdapter() { @Override public void linkActivated(HyperlinkEvent e) { try { CommitEditor.open(new RepositoryCommit(getCommit() .getRepository(), parentCommit)); if ((e.getStateMask() & SWT.MOD1) != 0) getEditor().close(false); } catch (PartInitException e1) { Activator.logError( "Error opening commit editor", e1);//$NON-NLS-1$ } } }); addToFocusTracking(link); } } @SuppressWarnings("unused") String getParentCommitLabel(int i) { return UIText.CommitEditorPage_LabelParent; } private List<Ref> getTags() { Repository repository = getCommit().getRepository(); List<Ref> tags = new ArrayList<>(repository.getTags().values()); Collections.sort(tags, new Comparator<Ref>() { @Override public int compare(Ref r1, Ref r2) { return Repository.shortenRefName(r1.getName()) .compareToIgnoreCase( Repository.shortenRefName(r2.getName())); } }); return tags; } void createTagsArea(Composite parent, FormToolkit toolkit, int span) { Composite tagArea = toolkit.createComposite(parent); GridLayoutFactory.fillDefaults().numColumns(2).equalWidth(false) .applyTo(tagArea); GridDataFactory.fillDefaults().span(span, 1).grab(true, false) .applyTo(tagArea); toolkit.createLabel(tagArea, UIText.CommitEditorPage_LabelTags) .setForeground( toolkit.getColors().getColor(IFormColors.TB_TOGGLE)); tagLabelArea = toolkit.createComposite(tagArea); GridDataFactory.fillDefaults().grab(true, true).applyTo(tagLabelArea); GridLayoutFactory.fillDefaults().spacing(1, 1).applyTo(tagLabelArea); } void fillDiffs(FileDiff[] diffs) { diffViewer.setInput(diffs); diffSection.setText(getDiffSectionTitle(Integer.valueOf(diffs.length))); setSectionExpanded(diffSection, diffs.length != 0); } static void setSectionExpanded(Section section, boolean expanded) { section.setExpanded(expanded); ((GridData) section.getLayoutData()).grabExcessVerticalSpace = expanded; } String getDiffSectionTitle(Integer numChanges) { return MessageFormat.format(UIText.CommitEditorPage_SectionFiles, numChanges); } void fillTags(FormToolkit toolkit, List<Ref> tags) { for (Control child : tagLabelArea.getChildren()) child.dispose(); // Hide "Tags" area if no tags to show ((GridData) tagLabelArea.getParent().getLayoutData()).exclude = tags .isEmpty(); GridLayoutFactory.fillDefaults().spacing(1, 1).numColumns(tags.size()) .applyTo(tagLabelArea); for (Ref tag : tags) { ObjectId id = tag.getPeeledObjectId(); boolean annotated = id != null; if (id == null) id = tag.getObjectId(); CLabel tagLabel = new CLabel(tagLabelArea, SWT.NONE); toolkit.adapt(tagLabel, false, false); if (annotated) tagLabel.setImage(getImage(UIIcons.TAG_ANNOTATED)); else tagLabel.setImage(getImage(UIIcons.TAG)); tagLabel.setText(Repository.shortenRefName(tag.getName())); } } private void createMessageArea(Composite parent, FormToolkit toolkit, int span) { Section messageSection = createSection(parent, toolkit, UIText.CommitEditorPage_SectionMessage, span); Composite messageArea = createSectionClient(messageSection, toolkit); RevCommit commit = getCommit().getRevCommit(); String message = commit.getFullMessage(); SpellcheckableMessageArea textContent = new SpellcheckableMessageArea( messageArea, message, true, toolkit.getBorderStyle()) { @Override protected IAdaptable getDefaultTarget() { return new PlatformObject() { @Override public Object getAdapter(Class adapter) { return Platform.getAdapterManager().getAdapter( getEditorInput(), adapter); } }; } @Override protected void createMarginPainter() { // Disabled intentionally } }; if ((toolkit.getBorderStyle() & SWT.BORDER) == 0) textContent.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TEXT_BORDER); StyledText textWidget = textContent.getTextWidget(); Point size = textWidget.computeSize(SWT.DEFAULT, SWT.DEFAULT); int yHint = size.y > 80 ? 80 : SWT.DEFAULT; GridDataFactory.fillDefaults().hint(SWT.DEFAULT, yHint).minSize(1, 20) .grab(true, true).applyTo(textContent); addToFocusTracking(textWidget); updateSectionClient(messageSection, messageArea, toolkit); } private void createBranchesArea(Composite parent, FormToolkit toolkit, int span) { branchSection = createSection(parent, toolkit, UIText.CommitEditorPage_SectionBranchesEmpty, span); Composite branchesArea = createSectionClient(branchSection, toolkit); branchViewer = new TableViewer(toolkit.createTable(branchesArea, SWT.V_SCROLL | SWT.H_SCROLL)); Control control = branchViewer.getControl(); control.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER); GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, 50) .applyTo(control); addToFocusTracking(control); branchViewer.setComparator(new ViewerComparator()); branchViewer.setLabelProvider(new GitLabelProvider() { @Override public String getText(Object element) { return Repository.shortenRefName(super.getText(element)); } }); branchViewer.setContentProvider(ArrayContentProvider.getInstance()); updateSectionClient(branchSection, branchesArea, toolkit); } private void fillBranches(List<Ref> result) { branchViewer.setInput(result); branchSection.setText(MessageFormat.format( UIText.CommitEditorPage_SectionBranches, Integer.valueOf(result.size()))); } void createDiffArea(Composite parent, FormToolkit toolkit, int span) { diffSection = createSection(parent, toolkit, UIText.CommitEditorPage_SectionFilesEmpty, span); Composite filesArea = createSectionClient(diffSection, toolkit); diffViewer = new CommitFileDiffViewer(filesArea, getSite(), SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION | toolkit.getBorderStyle()); Control control = diffViewer.getControl(); control.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER); GridDataFactory.fillDefaults().grab(true, true).applyTo(control); addToFocusTracking(control); diffViewer.setContentProvider(ArrayContentProvider.getInstance()); diffViewer.setTreeWalk(getCommit().getRepository(), null); updateSectionClient(diffSection, filesArea, toolkit); } RepositoryCommit getCommit() { return AdapterUtils.adapt(getEditor(), RepositoryCommit.class); } @Override protected void createFormContent(IManagedForm managedForm) { managedForm.addPart(new FocusManagerFormPart(focusTracker) { @Override public void setDefaultFocus() { getManagedForm().getForm().setFocus(); } }); Composite body = managedForm.getForm().getBody(); body.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { resources.dispose(); } }); FillLayout bodyLayout = new FillLayout(); bodyLayout.marginHeight = 5; bodyLayout.marginWidth = 5; body.setLayout(bodyLayout); FormToolkit toolkit = managedForm.getToolkit(); Composite displayArea = new Composite(body, toolkit.getOrientation()) { @Override public boolean setFocus() { Control control = focusTracker.getLastFocusControl(); if (control != null && control.forceFocus()) { return true; } return super.setFocus(); } }; toolkit.adapt(displayArea); GridLayoutFactory.fillDefaults().numColumns(2).applyTo(displayArea); createHeaderArea(displayArea, toolkit, 2); createMessageArea(displayArea, toolkit, 2); createChangesArea(displayArea, toolkit); loadSections(); } void createChangesArea(Composite displayArea, FormToolkit toolkit) { createDiffArea(displayArea, toolkit, 1); createBranchesArea(displayArea, toolkit, 1); } private List<Ref> loadTags() { RepositoryCommit repoCommit = getCommit(); RevCommit commit = repoCommit.getRevCommit(); Repository repository = repoCommit.getRepository(); List<Ref> tags = new ArrayList<>(); for (Ref tag : getTags()) { tag = repository.peel(tag); ObjectId id = tag.getPeeledObjectId(); if (id == null) id = tag.getObjectId(); if (!commit.equals(id)) continue; tags.add(tag); } return tags; } private List<Ref> loadBranches() { Repository repository = getCommit().getRepository(); RevCommit commit = getCommit().getRevCommit(); try (RevWalk revWalk = new RevWalk(repository)) { Map<String, Ref> refsMap = new HashMap<>(); refsMap.putAll(repository.getRefDatabase().getRefs( Constants.R_HEADS)); refsMap.putAll(repository.getRefDatabase().getRefs( Constants.R_REMOTES)); return RevWalkUtils.findBranchesReachableFrom(commit, revWalk, refsMap.values()); } catch (IOException e) { Activator.handleError(e.getMessage(), e, false); return Collections.emptyList(); } } void loadSections() { RepositoryCommit commit = getCommit(); Job refreshJob = new Job(MessageFormat.format( UIText.CommitEditorPage_JobName, commit.getRevCommit().name())) { @Override protected IStatus run(IProgressMonitor monitor) { final List<Ref> tags = loadTags(); final List<Ref> branches = loadBranches(); final FileDiff[] diffs = getCommit().getDiffs(); final ScrolledForm form = getManagedForm().getForm(); if (UIUtils.isUsable(form)) form.getDisplay().syncExec(new Runnable() { @Override public void run() { if (!UIUtils.isUsable(form)) return; fillTags(getManagedForm().getToolkit(), tags); fillDiffs(diffs); fillBranches(branches); form.reflow(true); form.layout(true, true); } }); return Status.OK_STATUS; } }; refreshJob.setRule(this); refreshJob.schedule(); } /** * Refresh the editor page */ public void refresh() { loadSections(); } @Override public void dispose() { focusTracker.dispose(); super.dispose(); } @Override public boolean contains(ISchedulingRule rule) { return rule == this; } @Override public boolean isConflicting(ISchedulingRule rule) { return rule == this; } @Override public ShowInContext getShowInContext() { if (diffViewer != null && diffViewer.getControl().isFocusControl()) { return diffViewer.getShowInContext(); } return null; } }