blob: d5984ffc17cd0d71205a1d2f7b044ebd3885c777 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}