/******************************************************************************* * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br> * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> * Copyright (c) 2010, Stefan Lay <stefan.lay@sap.com> * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com> * Copyright (C) 2010-2012, Matthias Sohn <matthias.sohn@sap.com> * Copyright (C) 2012, Daniel megert <daniel_megert@ch.ibm.com> * Copyright (C) 2012-2013 Robin Stocker <robin@nibor.org> * Copyright (C) 2012, François Rey <eclipse.org_@_francois_._rey_._name> * Copyright (C) 2015, IBM Corporation (Dani Megert <daniel_megert@ch.ibm.com>) * Copyright (C) 2015-2016 Thomas Wolf <thomas.wolf@paranor.ch> * Copyright (C) 2015-2017, Stefan Dirix <sdirix@eclipsesource.com> * * 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.history; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.egit.core.AdapterUtils; import org.eclipse.egit.core.project.RepositoryMapping; import org.eclipse.egit.ui.Activator; import org.eclipse.egit.ui.JobFamilies; import org.eclipse.egit.ui.UIPreferences; 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.commit.DiffDocument; import org.eclipse.egit.ui.internal.commit.DiffRegionFormatter; import org.eclipse.egit.ui.internal.commit.DiffViewer; import org.eclipse.egit.ui.internal.dialogs.HyperlinkSourceViewer; import org.eclipse.egit.ui.internal.dialogs.HyperlinkTokenScanner; import org.eclipse.egit.ui.internal.fetch.FetchHeadChangedEvent; import org.eclipse.egit.ui.internal.history.FindToolbar.StatusListener; import org.eclipse.egit.ui.internal.repository.tree.AdditionalRefNode; import org.eclipse.egit.ui.internal.repository.tree.FileNode; import org.eclipse.egit.ui.internal.repository.tree.FolderNode; import org.eclipse.egit.ui.internal.repository.tree.RefNode; import org.eclipse.egit.ui.internal.repository.tree.RepositoryTreeNode; import org.eclipse.egit.ui.internal.repository.tree.TagNode; import org.eclipse.egit.ui.internal.selection.SelectionUtils; import org.eclipse.egit.ui.internal.trace.GitTraceLocation; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ControlContribution; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IContributionItem; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.IStatusLineManager; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.bindings.keys.SWTKeySupport; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.preference.IPersistentPreferenceStore; import org.eclipse.jface.preference.PreferenceDialog; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.resource.ResourceManager; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextListener; import org.eclipse.jface.text.TextAttribute; import org.eclipse.jface.text.TextEvent; import org.eclipse.jface.text.hyperlink.IHyperlinkDetector; import org.eclipse.jface.text.presentation.IPresentationReconciler; import org.eclipse.jface.text.presentation.PresentationReconciler; import org.eclipse.jface.text.rules.DefaultDamagerRepairer; import org.eclipse.jface.text.rules.IToken; import org.eclipse.jface.text.rules.Token; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.source.SourceViewerConfiguration; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.diff.DiffConfig; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.events.ListenerHandle; import org.eclipse.jgit.events.RefsChangedEvent; import org.eclipse.jgit.events.RefsChangedListener; 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.revplot.PlotCommit; import org.eclipse.jgit.revplot.PlotWalk; import org.eclipse.jgit.revwalk.FollowFilter; import org.eclipse.jgit.revwalk.RenameCallback; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; 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.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.AndTreeFilter; import org.eclipse.jgit.treewalk.filter.OrTreeFilter; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CLabel; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Listener; import org.eclipse.team.ui.history.HistoryPage; import org.eclipse.team.ui.history.IHistoryView; import org.eclipse.ui.IActionBars; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.actions.ActionFactory; import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction; import org.eclipse.ui.dialogs.PreferencesUtil; import org.eclipse.ui.editors.text.EditorsUI; import org.eclipse.ui.part.IShowInSource; import org.eclipse.ui.part.IShowInTargetList; import org.eclipse.ui.part.ShowInContext; import org.eclipse.ui.progress.IWorkbenchSiteProgressService; import org.eclipse.ui.progress.UIJob; /** Graphical commit history viewer. */ public class GitHistoryPage extends HistoryPage implements RefsChangedListener, TableLoader, IShowInSource, IShowInTargetList { private static final int INITIAL_ITEM = -1; /** actions used in GitHistoryPage **/ private static class GitHistoryPageActions { private abstract class BooleanPrefAction extends Action implements IPropertyChangeListener, IWorkbenchAction { private final String prefName; BooleanPrefAction(final String pn, final String text) { setText(text); prefName = pn; historyPage.store.addPropertyChangeListener(this); setChecked(historyPage.store.getBoolean(prefName)); } @Override public void run() { historyPage.store.setValue(prefName, isChecked()); if (historyPage.store.needsSaving()) try { historyPage.store.save(); } catch (IOException e) { Activator.handleError(e.getMessage(), e, false); } } abstract void apply(boolean value); @Override public void propertyChange(final PropertyChangeEvent event) { if (prefName.equals(event.getProperty())) { setChecked(historyPage.store.getBoolean(prefName)); apply(isChecked()); } } @Override public void dispose() { // stop listening historyPage.store.removePropertyChangeListener(this); } } private class ShowFilterAction extends Action { private final ShowFilter filter; ShowFilterAction(ShowFilter filter, ImageDescriptor icon, String menuLabel, String toolTipText) { super(null, IAction.AS_CHECK_BOX); this.filter = filter; setImageDescriptor(icon); setText(menuLabel); setToolTipText(toolTipText); } @Override public void run() { String oldName = historyPage.getName(); String oldDescription = historyPage.getDescription(); if (!isChecked()) if (historyPage.showAllFilter == filter) { historyPage.showAllFilter = ShowFilter.SHOWALLRESOURCE; showAllResourceVersionsAction.setChecked(true); historyPage.initAndStartRevWalk(false); } if (isChecked() && historyPage.showAllFilter != filter) { historyPage.showAllFilter = filter; if (this != showAllRepoVersionsAction) showAllRepoVersionsAction.setChecked(false); if (this != showAllProjectVersionsAction) showAllProjectVersionsAction.setChecked(false); if (this != showAllFolderVersionsAction) showAllFolderVersionsAction.setChecked(false); if (this != showAllResourceVersionsAction) showAllResourceVersionsAction.setChecked(false); historyPage.initAndStartRevWalk(false); } historyPage.firePropertyChange(historyPage, P_NAME, oldName, historyPage.getName()); // even though this is currently ending nowhere (see bug // 324386), we // still create the event historyPage.firePropertyChange(historyPage, P_DESCRIPTION, oldDescription, historyPage.getDescription()); Activator.getDefault().getPreferenceStore().setValue( PREF_SHOWALLFILTER, historyPage.showAllFilter.toString()); } @Override public String toString() { return "ShowFilter[" + filter.toString() + "]"; //$NON-NLS-1$ //$NON-NLS-2$ } } List<IWorkbenchAction> actionsToDispose; BooleanPrefAction showRelativeDateAction; BooleanPrefAction showEmailAddressesAction; BooleanPrefAction showNotesAction; BooleanPrefAction showTagSequenceAction; BooleanPrefAction showBranchSequenceAction; BooleanPrefAction wrapCommentAction; BooleanPrefAction fillCommentAction; IAction findAction; IAction refreshAction; BooleanPrefAction showCommentAction; BooleanPrefAction showFilesAction; IWorkbenchAction compareModeAction; IWorkbenchAction showAllBranchesAction; IWorkbenchAction showAdditionalRefsAction; BooleanPrefAction followRenamesAction; IWorkbenchAction reuseCompareEditorAction; ShowFilterAction showAllRepoVersionsAction; ShowFilterAction showAllProjectVersionsAction; ShowFilterAction showAllFolderVersionsAction; ShowFilterAction showAllResourceVersionsAction; private GitHistoryPage historyPage; GitHistoryPageActions(GitHistoryPage historyPage) { actionsToDispose = new ArrayList<>(); this.historyPage = historyPage; createActions(); } private static String formatAccelerator(int accelerator) { return SWTKeySupport.getKeyFormatterForPlatform().format( SWTKeySupport.convertAcceleratorToKeyStroke(accelerator)); } private void createActions() { createFindToolbarAction(); createRefreshAction(); createFilterActions(); createCompareModeAction(); createReuseCompareEditorAction(); createShowAllBranchesAction(); createShowAdditionalRefsAction(); createShowCommentAction(); createShowFilesAction(); createShowRelativeDateAction(); createShowEmailAddressesAction(); createShowNotesAction(); createShowTagSequenceAction(); createShowBranchSequenceAction(); createWrapCommentAction(); createFillCommentAction(); createFollowRenamesAction(); wrapCommentAction.setEnabled(showCommentAction.isChecked()); fillCommentAction.setEnabled(showCommentAction.isChecked()); } private void createFindToolbarAction() { findAction = new Action(UIText.GitHistoryPage_FindMenuLabel, UIIcons.ELCL16_FIND) { @Override public void run() { historyPage.store.setValue( UIPreferences.RESOURCEHISTORY_SHOW_FINDTOOLBAR, isChecked()); if (historyPage.store.needsSaving()) { try { historyPage.store.save(); } catch (IOException e) { Activator.handleError(e.getMessage(), e, false); } } historyPage.searchBar.setVisible(isChecked()); } @Override public void setChecked(boolean checked) { super.setChecked(checked); int accelerator = getAccelerator(); if (checked) { setToolTipText( NLS.bind(UIText.GitHistoryPage_FindHideTooltip, formatAccelerator(accelerator))); } else { setToolTipText( NLS.bind(UIText.GitHistoryPage_FindShowTooltip, formatAccelerator(accelerator))); } } }; // TODO: how not to hard-wire this? findAction.setAccelerator(SWT.MOD1 | 'F'); findAction.setEnabled(false); // Gets enabled once we have commits boolean isChecked = historyPage.store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_FINDTOOLBAR); findAction.setChecked(isChecked); historyPage.getSite().getActionBars().setGlobalActionHandler( ActionFactory.FIND.getId(), findAction); historyPage.getSite().getActionBars().updateActionBars(); } private void createRefreshAction() { refreshAction = new Action(UIText.GitHistoryPage_RefreshMenuLabel, UIIcons.ELCL16_REFRESH) { @Override public void run() { historyPage.refresh(); } }; } private void createFilterActions() { showAllRepoVersionsAction = new ShowFilterAction( ShowFilter.SHOWALLREPO, UIIcons.REPOSITORY, UIText.GitHistoryPage_AllInRepoMenuLabel, UIText.GitHistoryPage_AllInRepoTooltip); showAllProjectVersionsAction = new ShowFilterAction( ShowFilter.SHOWALLPROJECT, UIIcons.FILTERPROJECT, UIText.GitHistoryPage_AllInProjectMenuLabel, UIText.GitHistoryPage_AllInProjectTooltip); showAllFolderVersionsAction = new ShowFilterAction( ShowFilter.SHOWALLFOLDER, UIIcons.FILTERFOLDER, UIText.GitHistoryPage_AllInParentMenuLabel, UIText.GitHistoryPage_AllInParentTooltip); showAllResourceVersionsAction = new ShowFilterAction( ShowFilter.SHOWALLRESOURCE, UIIcons.FILTERRESOURCE, UIText.GitHistoryPage_AllOfResourceMenuLabel, UIText.GitHistoryPage_AllOfResourceTooltip); showAllRepoVersionsAction .setChecked(historyPage.showAllFilter == showAllRepoVersionsAction.filter); showAllProjectVersionsAction .setChecked(historyPage.showAllFilter == showAllProjectVersionsAction.filter); showAllFolderVersionsAction .setChecked(historyPage.showAllFilter == showAllFolderVersionsAction.filter); showAllResourceVersionsAction .setChecked(historyPage.showAllFilter == showAllResourceVersionsAction.filter); } private void createCompareModeAction() { compareModeAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_COMPARE_MODE, UIText.GitHistoryPage_CompareModeMenuLabel) { @Override void apply(boolean value) { // nothing, just switch the preference } }; compareModeAction.setImageDescriptor(UIIcons.ELCL16_COMPARE_VIEW); compareModeAction.setToolTipText(UIText.GitHistoryPage_compareMode); actionsToDispose.add(compareModeAction); } private void createReuseCompareEditorAction() { reuseCompareEditorAction = new CompareUtils.ReuseCompareEditorAction(); actionsToDispose.add(reuseCompareEditorAction); } private void createShowAllBranchesAction() { showAllBranchesAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_SHOW_ALL_BRANCHES, UIText.GitHistoryPage_ShowAllBranchesMenuLabel) { @Override void apply(boolean value) { historyPage.refresh(); } }; showAllBranchesAction.setImageDescriptor(UIIcons.BRANCH); showAllBranchesAction .setToolTipText(UIText.GitHistoryPage_showAllBranches); actionsToDispose.add(showAllBranchesAction); } private void createShowAdditionalRefsAction() { showAdditionalRefsAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS, UIText.GitHistoryPage_ShowAdditionalRefsMenuLabel) { @Override void apply(boolean value) { historyPage.refresh(); } }; actionsToDispose.add(showAdditionalRefsAction); } private void createFollowRenamesAction() { followRenamesAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_FOLLOW_RENAMES, UIText.GitHistoryPage_FollowRenames) { @Override void apply(boolean follow) { historyPage.refresh(); } }; followRenamesAction.apply(followRenamesAction.isChecked()); actionsToDispose.add(followRenamesAction); } private void createShowCommentAction() { showCommentAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_SHOW_REV_COMMENT, UIText.ResourceHistory_toggleRevComment) { @Override void apply(final boolean value) { historyPage.layout(); wrapCommentAction.setEnabled(isChecked()); fillCommentAction.setEnabled(isChecked()); } }; actionsToDispose.add(showCommentAction); } private void createShowFilesAction() { showFilesAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_SHOW_REV_DETAIL, UIText.ResourceHistory_toggleRevDetail) { @Override void apply(final boolean value) { historyPage.layout(); } }; actionsToDispose.add(showFilesAction); } private void createShowRelativeDateAction() { showRelativeDateAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_SHOW_RELATIVE_DATE, UIText.ResourceHistory_toggleRelativeDate) { @Override void apply(boolean date) { // nothing, just set the Preference } }; showRelativeDateAction.apply(showRelativeDateAction.isChecked()); actionsToDispose.add(showRelativeDateAction); } private void createShowEmailAddressesAction() { showEmailAddressesAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_SHOW_EMAIL_ADDRESSES, UIText.GitHistoryPage_toggleEmailAddresses) { @Override void apply(boolean date) { // nothing, just set the Preference } }; showEmailAddressesAction.apply(showEmailAddressesAction.isChecked()); actionsToDispose.add(showEmailAddressesAction); } private void createShowNotesAction() { showNotesAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_SHOW_NOTES, UIText.ResourceHistory_toggleShowNotes) { @Override void apply(boolean value) { historyPage.refresh(); } }; showNotesAction.apply(showNotesAction.isChecked()); actionsToDispose.add(showNotesAction); } private void createShowTagSequenceAction() { showTagSequenceAction = new BooleanPrefAction( UIPreferences.HISTORY_SHOW_TAG_SEQUENCE, UIText.ResourceHistory_ShowTagSequence) { @Override void apply(boolean value) { // nothing, just set the Preference } }; showTagSequenceAction.apply(showTagSequenceAction.isChecked()); actionsToDispose.add(showTagSequenceAction); } private void createShowBranchSequenceAction() { showBranchSequenceAction = new BooleanPrefAction( UIPreferences.HISTORY_SHOW_BRANCH_SEQUENCE, UIText.ResourceHistory_ShowBranchSequence) { @Override void apply(boolean value) { // nothing, just set the Preference } }; showBranchSequenceAction .apply(showBranchSequenceAction.isChecked()); actionsToDispose.add(showBranchSequenceAction); } private void createWrapCommentAction() { wrapCommentAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_SHOW_COMMENT_WRAP, UIText.ResourceHistory_toggleCommentWrap) { @Override void apply(boolean wrap) { // nothing, just set the Preference } }; wrapCommentAction.apply(wrapCommentAction.isChecked()); actionsToDispose.add(wrapCommentAction); } private void createFillCommentAction() { fillCommentAction = new BooleanPrefAction( UIPreferences.RESOURCEHISTORY_SHOW_COMMENT_FILL, UIText.ResourceHistory_toggleCommentFill) { @Override void apply(boolean fill) { // nothing, just set the Preference } }; fillCommentAction.apply(fillCommentAction.isChecked()); actionsToDispose.add(fillCommentAction); } } /** * This class defines a couple that associates two pieces of information: * the file path, and whether it is a regular file (or a directory). */ private static class FilterPath { private String path; private boolean regularFile; public FilterPath(String path, boolean regularFile) { super(); this.path = path; this.regularFile = regularFile; } /** @return the file path */ public String getPath() { return path; } /** @return <code>true</code> if the file is a regular file, * and <code>false</code> otherwise (directory, project) */ public boolean isRegularFile() { return regularFile; } /** * In {@link FilterPath} class, equality is based on {@link #getPath * path} equality. */ @Override public boolean equals(Object obj) { if (obj == null || !(obj instanceof FilterPath)) return false; FilterPath other = (FilterPath) obj; if (path == null) return other.path == null; return path.equals(other.path); } @Override public int hashCode() { if (path != null) return path.hashCode(); return super.hashCode(); } @Override public String toString() { StringBuilder builder = new StringBuilder("Path: "); //$NON-NLS-1$ builder.append(getPath()); builder.append("regular: "); //$NON-NLS-1$ builder.append(isRegularFile()); return builder.toString(); } } private static class HistoryPageRule implements ISchedulingRule { @Override public boolean contains(ISchedulingRule rule) { return this == rule; } @Override public boolean isConflicting(ISchedulingRule rule) { return this == rule; } } private static final String POPUP_ID = "org.eclipse.egit.ui.historyPageContributions"; //$NON-NLS-1$ private static final String DESCRIPTION_PATTERN = "{0} - {1}"; //$NON-NLS-1$ private static final String NAME_PATTERN = "{0}: {1} [{2}]"; //$NON-NLS-1$ private static final String PREF_SHOWALLFILTER = "org.eclipse.egit.ui.githistorypage.showallfilter"; //$NON-NLS-1$ enum ShowFilter { SHOWALLRESOURCE, SHOWALLFOLDER, SHOWALLPROJECT, SHOWALLREPO, } private ShowFilter showAllFilter = ShowFilter.SHOWALLRESOURCE; private GitHistoryPageActions actions; /** An error text to be shown instead of the control */ private StyledText errorText; private final IPersistentPreferenceStore store = (IPersistentPreferenceStore) Activator .getDefault().getPreferenceStore(); private ListenerHandle myRefsChangedHandle; private HistoryPageInput input; private String name; private boolean trace = GitTraceLocation.HISTORYVIEW.isActive(); /** Overall composite hosting all of our controls. */ private Composite topControl; /** Overall composite hosting the controls that displays the history. */ private Composite historyControl; /** Split between {@link #graph} and {@link #revInfoSplit}. */ private SashForm graphDetailSplit; /** Split between {@link #commentViewer} and {@link #fileViewer}. */ private SashForm revInfoSplit; /** The table showing the DAG, first "paragraph", author, author date. */ private CommitGraphTable graph; /** Viewer displaying the currently selected commit of {@link #graph}. */ private CommitMessageViewer commentViewer; private DiffViewer diffViewer; /** Viewer displaying file difference implied by {@link #graph}'s commit. */ private CommitFileDiffViewer fileViewer; /** A label showing a warning icon */ private Composite warningComposite; /** A label field to display a warning */ private CLabel warningLabel; /** Our context menu manager for the entire page. */ private final MenuManager popupMgr = new MenuManager(null, POPUP_ID); /** Job that is updating our history view, if we are refreshing. */ private GenerateHistoryJob job; private final ResourceManager resources = new LocalResourceManager( JFaceResources.getResources()); /** Last HEAD */ private AnyObjectId currentHeadId; /** Last FETCH_HEAD */ private AnyObjectId currentFetchHeadId; /** Repository of the last input*/ private Repository currentRepo; private boolean currentShowAllBranches; private boolean currentShowAdditionalRefs; private boolean currentShowNotes; private boolean currentFollowRenames; /** Tracks the file names that are to be highlighted in the diff file viewer */ private Set<String> fileViewerInterestingPaths; // react on changes to the relative date preference private final IPropertyChangeListener listener = new IPropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { final String prop = event.getProperty(); if (UIPreferences.HISTORY_MAX_BRANCH_LENGTH.equals(prop) || UIPreferences.HISTORY_MAX_TAG_LENGTH.equals(prop)) graph.getTableView().refresh(); if (UIPreferences.RESOURCEHISTORY_SHOW_COMMENT_WRAP.equals(prop)) { setWrap(((Boolean) event.getNewValue()).booleanValue()); } } }; /** Tracks the selection to display the correct input when linked with editors. */ private GitHistorySelectionTracker selectionTracker; /** * List of paths we used to limit the revwalk; null if no paths. * <p> * Note that a change in this list requires that the history is redrawn */ private List<FilterPath> pathFilters; private Runnable refschangedRunnable; private final RenameTracker renameTracker = new RenameTracker(); private ScrolledComposite commentAndDiffScrolledComposite; private Composite commentAndDiffComposite; private volatile boolean resizing; private final HistoryPageRule pageSchedulingRule; /** Toolbar to find commits in the history view. */ private SearchBar searchBar; /** * Determine if the input can be shown in this viewer. * * @param object * an object that is hopefully of type ResourceList or IResource, * but may be anything (including null). * @return true if the input is a ResourceList or an IResource of type FILE, * FOLDER or PROJECT and we can show it; false otherwise. */ public static boolean canShowHistoryFor(final Object object) { if (object instanceof HistoryPageInput) return true; if (object instanceof IResource) return typeOk((IResource) object); if (object instanceof RepositoryTreeNode) return true; IResource resource = AdapterUtils.adaptToAnyResource(object); if (resource != null && typeOk(resource)) return true; return AdapterUtils.adapt(object, Repository.class) != null; } private static boolean typeOk(final IResource object) { switch (object.getType()) { case IResource.FILE: case IResource.FOLDER: case IResource.PROJECT: return true; } return false; } /** * The default constructor */ public GitHistoryPage() { trace = GitTraceLocation.HISTORYVIEW.isActive(); pageSchedulingRule = new HistoryPageRule(); if (trace) { GitTraceLocation.getTrace().traceEntry( GitTraceLocation.HISTORYVIEW.getLocation()); } } private interface ICommitsProvider { Object getSearchContext(); SWTCommit[] getCommits(); RevFlag getHighlight(); } private static class SearchBar extends ControlContribution { private IActionBars bars; private FindToolbar toolbar; private Object searchContext; private String lastText; private ObjectId lastObjectId; private Object lastSearchContext; private ICommitsProvider provider; private boolean wasVisible = false; private final CommitGraphTable graph; private final IAction openCloseToggle; /** * "Go to next/previous" from the {@link FindToolbar} sends * {@link SWT#Selection} events with the chosen {@link RevCommit} as * data. */ private final Listener selectionListener = new Listener() { @Override public void handleEvent(Event evt) { final RevCommit commit = (RevCommit) evt.data; lastObjectId = commit.getId(); graph.selectCommit(commit); } }; /** * Listener to close the search bar on ESC. (Ctrl/Cmd-F is already * handled via global retarget action.) */ private final KeyListener keyListener = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { int key = SWTKeySupport.convertEventToUnmodifiedAccelerator(e); if (key == SWT.ESC) { setVisible(false); e.doit = false; } } }; /** * Listener to display status messages from the asynchronous find. (Is * called in the UI thread.) */ private final StatusListener statusListener = new StatusListener() { @Override public void setMessage(FindToolbar originator, String text) { IStatusLineManager status = bars.getStatusLineManager(); if (status != null) { status.setMessage(text); } } }; /** * Listener to ensure that the history view is fully activated when the * user clicks into the search bar's text widget. This makes sure our * status manager gets activated and thus shows the status messages. We * don't get a focus event when the user clicks in the field; and * fiddling with the focus in a FocusListener could get hairy anyway. */ private final Listener mouseListener = new Listener() { private boolean hasFocus; private boolean hadFocusOnMouseDown; @Override public void handleEvent(Event e) { switch (e.type) { case SWT.FocusIn: toolbar.getDisplay().asyncExec(new Runnable() { @Override public void run() { hasFocus = true; } }); break; case SWT.FocusOut: hasFocus = false; break; case SWT.MouseDown: hadFocusOnMouseDown = hasFocus; break; case SWT.MouseUp: if (!hadFocusOnMouseDown) { graph.getControl().setFocus(); toolbar.setFocus(); } break; default: break; } } }; public SearchBar(String id, CommitGraphTable graph, IAction openCloseAction, IActionBars bars) { super(id); super.setVisible(false); this.graph = graph; this.openCloseToggle = openCloseAction; this.bars = bars; } private void beforeHide() { lastText = toolbar.getText(); lastSearchContext = searchContext; statusListener.setMessage(toolbar, ""); //$NON-NLS-1$ // It will be disposed by the IToolBarManager toolbar = null; openCloseToggle.setChecked(false); wasVisible = false; } @Override public void setVisible(boolean visible) { if (visible != isVisible()) { if (!visible) { beforeHide(); } super.setVisible(visible); // Update the toolbar. Will dispose our FindToolbar widget on // hide, and will create a new one (through createControl()) // on show. It'll also reposition the toolbar, if needed. // Note: just doing bars.getToolBarManager().update(true); // messes up big time (doesn't resize or re-position). bars.updateActionBars(); if (visible && toolbar != null) { openCloseToggle.setChecked(true); // If the toolbar was moved below the tabs, we now have // the wrong background. It disappears when one clicks // elsewhere. Looks like an inactive selection... No // way found to fix this but this ugly focus juggling: graph.getControl().setFocus(); toolbar.setFocus(); } else if (!visible && !graph.getControl().isDisposed()) { graph.getControl().setFocus(); } } } @Override public boolean isDynamic() { // We toggle our own visibility return true; } @Override protected Control createControl(Composite parent) { toolbar = new FindToolbar(parent); toolbar.setBackground(null); toolbar.addKeyListener(keyListener); toolbar.addListener(SWT.FocusIn, mouseListener); toolbar.addListener(SWT.FocusOut, mouseListener); toolbar.addListener(SWT.MouseDown, mouseListener); toolbar.addListener(SWT.MouseUp, mouseListener); toolbar.addListener(SWT.Modify, (e) -> lastText = toolbar.getText()); toolbar.addStatusListener(statusListener); toolbar.addSelectionListener(selectionListener); boolean hasInput = provider != null; if (hasInput) { setInput(provider); } if (lastText != null) { if (lastSearchContext != null && lastSearchContext.equals(searchContext)) { toolbar.setPreselect(lastObjectId); } toolbar.setText(lastText, hasInput); } lastSearchContext = null; lastObjectId = null; if (wasVisible) { return toolbar; } wasVisible = true; // This fixes the wrong background when Eclipse starts up with the // search bar visible. toolbar.getDisplay().asyncExec(new Runnable() { @Override public void run() { if (toolbar != null && !toolbar.isDisposed()) { // See setVisible() above. Somehow, we need this, too. graph.getControl().setFocus(); toolbar.setFocus(); } } }); return toolbar; } public void setInput(ICommitsProvider provider) { this.provider = provider; if (toolbar != null) { searchContext = provider.getSearchContext(); toolbar.setInput(provider.getHighlight(), graph.getTableView().getTable(), provider.getCommits()); } } } @Override public void createControl(final Composite parent) { trace = GitTraceLocation.HISTORYVIEW.isActive(); if (trace) GitTraceLocation.getTrace().traceEntry( GitTraceLocation.HISTORYVIEW.getLocation()); attachSelectionTracker(); historyControl = createMainPanel(parent); warningComposite = new Composite(historyControl, SWT.NONE); warningComposite.setLayout(new GridLayout(2, false)); warningComposite.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false)); warningLabel = new CLabel(warningComposite, SWT.NONE); warningLabel.setImage(PlatformUI.getWorkbench().getSharedImages() .getImage(ISharedImages.IMG_OBJS_WARN_TSK)); warningLabel .setToolTipText(UIText.GitHistoryPage_IncompleteListTooltip); Link preferencesLink = new Link(warningComposite, SWT.NONE); preferencesLink.setText(UIText.GitHistoryPage_PreferencesLink); preferencesLink.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { String preferencePageId = "org.eclipse.egit.ui.internal.preferences.HistoryPreferencePage"; //$NON-NLS-1$ PreferenceDialog dialog = PreferencesUtil .createPreferenceDialogOn(getSite().getShell(), preferencePageId, new String[] { preferencePageId }, null); dialog.open(); } }); GridDataFactory.fillDefaults().grab(true, true).applyTo(historyControl); graphDetailSplit = new SashForm(historyControl, SWT.VERTICAL); GridDataFactory.fillDefaults().grab(true, true).applyTo( graphDetailSplit); graph = new CommitGraphTable(graphDetailSplit, getSite(), popupMgr, this, resources); Activator.getDefault().getPreferenceStore() .addPropertyChangeListener(listener); revInfoSplit = new SashForm(graphDetailSplit, SWT.HORIZONTAL); commentAndDiffScrolledComposite = new ScrolledComposite(revInfoSplit, SWT.H_SCROLL | SWT.V_SCROLL); commentAndDiffScrolledComposite.setExpandHorizontal(true); commentAndDiffScrolledComposite.setExpandVertical(true); commentAndDiffComposite = new Composite(commentAndDiffScrolledComposite, SWT.NONE); commentAndDiffScrolledComposite.setContent(commentAndDiffComposite); commentAndDiffComposite.setLayout(GridLayoutFactory.fillDefaults() .create()); commentViewer = new CommitMessageViewer(commentAndDiffComposite, getPartSite()); commentViewer.getControl().setLayoutData( GridDataFactory.fillDefaults().grab(true, false).create()); commentViewer.addTextListener(new ITextListener() { @Override public void textChanged(TextEvent event) { resizeCommentAndDiffScrolledComposite(); } }); commentAndDiffComposite.setBackground(commentViewer.getControl() .getBackground()); HyperlinkSourceViewer.Configuration configuration = new HyperlinkSourceViewer.Configuration( EditorsUI.getPreferenceStore()) { @Override public int getHyperlinkStateMask(ISourceViewer sourceViewer) { return SWT.NONE; } @Override protected IHyperlinkDetector[] internalGetHyperlinkDetectors( ISourceViewer sourceViewer) { IHyperlinkDetector[] registered = super.internalGetHyperlinkDetectors( sourceViewer); // Always add our special detector for commit hyperlinks; we // want those to show always. if (registered == null) { return new IHyperlinkDetector[] { new CommitMessageViewer.KnownHyperlinksDetector() }; } else { IHyperlinkDetector[] result = new IHyperlinkDetector[registered.length + 1]; System.arraycopy(registered, 0, result, 0, registered.length); result[registered.length] = new CommitMessageViewer.KnownHyperlinksDetector(); return result; } } @Override public String[] getConfiguredContentTypes( ISourceViewer sourceViewer) { return new String[] { IDocument.DEFAULT_CONTENT_TYPE, CommitMessageViewer.HEADER_CONTENT_TYPE, CommitMessageViewer.FOOTER_CONTENT_TYPE }; } @Override public IPresentationReconciler getPresentationReconciler( ISourceViewer viewer) { PresentationReconciler reconciler = new PresentationReconciler(); reconciler.setDocumentPartitioning( getConfiguredDocumentPartitioning(viewer)); DefaultDamagerRepairer hyperlinkDamagerRepairer = new DefaultDamagerRepairer( new HyperlinkTokenScanner(this, viewer)); reconciler.setDamager(hyperlinkDamagerRepairer, IDocument.DEFAULT_CONTENT_TYPE); reconciler.setRepairer(hyperlinkDamagerRepairer, IDocument.DEFAULT_CONTENT_TYPE); TextAttribute headerDefault = new TextAttribute( PlatformUI.getWorkbench().getDisplay() .getSystemColor(SWT.COLOR_DARK_GRAY)); DefaultDamagerRepairer headerDamagerRepairer = new DefaultDamagerRepairer( new HyperlinkTokenScanner(this, viewer, headerDefault)); reconciler.setDamager(headerDamagerRepairer, CommitMessageViewer.HEADER_CONTENT_TYPE); reconciler.setRepairer(headerDamagerRepairer, CommitMessageViewer.HEADER_CONTENT_TYPE); DefaultDamagerRepairer footerDamagerRepairer = new DefaultDamagerRepairer( new FooterTokenScanner(this, viewer)); reconciler.setDamager(footerDamagerRepairer, CommitMessageViewer.FOOTER_CONTENT_TYPE); reconciler.setRepairer(footerDamagerRepairer, CommitMessageViewer.FOOTER_CONTENT_TYPE); return reconciler; } }; commentViewer.configure(configuration); diffViewer = new DiffViewer(commentAndDiffComposite, null, SWT.NONE); diffViewer.configure( new DiffViewer.Configuration(EditorsUI.getPreferenceStore())); diffViewer.getControl().setLayoutData( GridDataFactory.fillDefaults().grab(true, false).create()); setWrap(store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_COMMENT_WRAP)); commentAndDiffScrolledComposite.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { if (!resizing && commentViewer.getTextWidget() .getWordWrap()) { resizeCommentAndDiffScrolledComposite(); } } }); fileViewer = new CommitFileDiffViewer(revInfoSplit, getSite()); fileViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { ISelection selection = event.getSelection(); List<FileDiff> diffs = new ArrayList<>(); if (selection instanceof IStructuredSelection) { IStructuredSelection sel = (IStructuredSelection) selection; for (Object obj : sel.toList()) if (obj instanceof FileDiff) diffs.add((FileDiff) obj); } formatDiffs(diffs); } }); layoutSashForm(graphDetailSplit, UIPreferences.RESOURCEHISTORY_GRAPH_SPLIT); layoutSashForm(revInfoSplit, UIPreferences.RESOURCEHISTORY_REV_SPLIT); attachCommitSelectionChanged(); initActions(); getSite().setSelectionProvider(graph.getTableView()); getSite().registerContextMenu(POPUP_ID, popupMgr, graph.getTableView()); // due to the issues described in bug 322751, it makes no // sense to set a selection provider for the site here layout(); myRefsChangedHandle = Repository.getGlobalListenerList() .addRefsChangedListener(this); IToolBarManager manager = getSite().getActionBars().getToolBarManager(); searchBar = new SearchBar(GitHistoryPage.class.getName() + ".searchBar", //$NON-NLS-1$ graph, actions.findAction, getSite().getActionBars()); manager.prependToGroup("org.eclipse.team.ui.historyView", searchBar); //$NON-NLS-1$ getSite().getActionBars().updateActionBars(); if (trace) GitTraceLocation.getTrace().traceExit( GitTraceLocation.HISTORYVIEW.getLocation()); } private void layoutSashForm(final SashForm sf, final String key) { sf.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { final int[] w = sf.getWeights(); store.putValue(key, UIPreferences.intArrayToString(w)); if (store.needsSaving()) try { store.save(); } catch (IOException e1) { Activator.handleError(e1.getMessage(), e1, false); } } }); int[] weights = UIPreferences.stringToIntArray(store.getString(key), 2); if (weights == null) { // Corrupted preferences? weights = UIPreferences .stringToIntArray(store.getDefaultString(key), 2); } sf.setWeights(weights); } private Composite createMainPanel(final Composite parent) { topControl = new Composite(parent, SWT.NONE); StackLayout layout = new StackLayout(); topControl.setLayout(layout); final Composite c = new Composite(topControl, SWT.NULL); layout.topControl = c; // shown instead of the splitter if an error message was set errorText = new StyledText(topControl, SWT.NONE); // use the same font as in message viewer errorText.setFont(UIUtils .getFont(UIPreferences.THEME_CommitMessageFont)); final GridLayout parentLayout = new GridLayout(); parentLayout.marginHeight = 0; parentLayout.marginWidth = 0; parentLayout.verticalSpacing = 0; c.setLayout(parentLayout); return c; } private void layout() { final boolean showComment = store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_REV_COMMENT); final boolean showFiles = store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_REV_DETAIL); if (showComment && showFiles) { graphDetailSplit.setMaximizedControl(null); revInfoSplit.setMaximizedControl(null); } else if (showComment && !showFiles) { graphDetailSplit.setMaximizedControl(null); revInfoSplit.setMaximizedControl(commentViewer.getControl()); } else if (!showComment && showFiles) { graphDetailSplit.setMaximizedControl(null); revInfoSplit.setMaximizedControl(fileViewer.getControl()); } else if (!showComment && !showFiles) graphDetailSplit.setMaximizedControl(graph.getControl()); historyControl.layout(); } private void attachCommitSelectionChanged() { graph.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(final SelectionChangedEvent event) { final ISelection s = event.getSelection(); if (s.isEmpty() || !(s instanceof IStructuredSelection)) { commentViewer.setInput(null); fileViewer.setInput(null); return; } final IStructuredSelection sel = ((IStructuredSelection) s); if (sel.size() > 1) { commentViewer.setInput(null); fileViewer.setInput(null); return; } if (input == null) { return; } final PlotCommit<?> c = (PlotCommit<?>) sel.getFirstElement(); commentViewer.setInput(c); final PlotWalk walk = new PlotWalk(input.getRepository()); try { final RevCommit unfilteredCommit = walk.parseCommit(c); for (RevCommit parent : unfilteredCommit.getParents()) walk.parseBody(parent); fileViewer.setInput(unfilteredCommit); } catch (IOException e) { fileViewer.setInput(c); } finally { walk.dispose(); } if (input.getSingleFile() != null) fileViewer.selectFirstInterestingElement(); } }); commentViewer .addCommitNavigationListener(new CommitNavigationListener() { @Override public void showCommit(final RevCommit c) { graph.selectCommit(c); } }); } /** * Attaches the selection tracker to the workbench page containing this page. */ private void attachSelectionTracker() { if (selectionTracker == null) { selectionTracker = new GitHistorySelectionTracker(); selectionTracker.attach(getSite().getPage()); } } /** * Detaches the selection tracker from the workbench page, if necessary. */ private void detachSelectionTracker() { if (selectionTracker != null) { selectionTracker.detach(getSite().getPage()); } } private void initActions() { try { showAllFilter = ShowFilter.valueOf(Activator.getDefault() .getPreferenceStore().getString(PREF_SHOWALLFILTER)); } catch (IllegalArgumentException e) { showAllFilter = ShowFilter.SHOWALLRESOURCE; } actions = new GitHistoryPageActions(this); setupToolBar(); setupViewMenu(); } private void setupToolBar() { IToolBarManager mgr = getSite().getActionBars().getToolBarManager(); mgr.add(actions.findAction); mgr.add(new Separator()); mgr.add(actions.showAllRepoVersionsAction); mgr.add(actions.showAllProjectVersionsAction); mgr.add(actions.showAllFolderVersionsAction); mgr.add(actions.showAllResourceVersionsAction); mgr.add(new Separator()); mgr.add(actions.compareModeAction); mgr.add(actions.showAllBranchesAction); } private void setupViewMenu() { IMenuManager viewMenuMgr = getSite().getActionBars().getMenuManager(); viewMenuMgr.add(actions.refreshAction); viewMenuMgr.add(new Separator()); IMenuManager showSubMenuMgr = new MenuManager( UIText.GitHistoryPage_ShowSubMenuLabel); viewMenuMgr.add(showSubMenuMgr); showSubMenuMgr.add(actions.showAllBranchesAction); showSubMenuMgr.add(actions.showAdditionalRefsAction); showSubMenuMgr.add(actions.showNotesAction); showSubMenuMgr.add(actions.followRenamesAction); showSubMenuMgr.add(new Separator()); showSubMenuMgr.add(actions.findAction); showSubMenuMgr.add(actions.showCommentAction); showSubMenuMgr.add(actions.showFilesAction); showSubMenuMgr.add(new Separator()); showSubMenuMgr.add(actions.showRelativeDateAction); showSubMenuMgr.add(actions.showEmailAddressesAction); IMenuManager showInMessageManager = new MenuManager( UIText.GitHistoryPage_InRevisionCommentSubMenuLabel); showSubMenuMgr.add(showInMessageManager); showInMessageManager.add(actions.showBranchSequenceAction); showInMessageManager.add(actions.showTagSequenceAction); showInMessageManager.add(actions.wrapCommentAction); showInMessageManager.add(actions.fillCommentAction); IMenuManager filterSubMenuMgr = new MenuManager( UIText.GitHistoryPage_FilterSubMenuLabel); viewMenuMgr.add(filterSubMenuMgr); filterSubMenuMgr.add(actions.showAllRepoVersionsAction); filterSubMenuMgr.add(actions.showAllProjectVersionsAction); filterSubMenuMgr.add(actions.showAllFolderVersionsAction); filterSubMenuMgr.add(actions.showAllResourceVersionsAction); viewMenuMgr.add(new Separator()); viewMenuMgr.add(actions.compareModeAction); viewMenuMgr.add(actions.reuseCompareEditorAction); } @Override public void dispose() { trace = GitTraceLocation.HISTORYVIEW.isActive(); if (trace) GitTraceLocation.getTrace().traceEntry( GitTraceLocation.HISTORYVIEW.getLocation()); detachSelectionTracker(); Activator.getDefault().getPreferenceStore() .removePropertyChangeListener(listener); if (myRefsChangedHandle != null) { myRefsChangedHandle.remove(); myRefsChangedHandle = null; } resources.dispose(); // dispose of the actions (the history framework doesn't do this for us) for (IWorkbenchAction action : actions.actionsToDispose) action.dispose(); actions.actionsToDispose.clear(); releaseGenerateHistoryJob(); if (popupMgr != null) { for (final IContributionItem i : popupMgr.getItems()) if (i instanceof IWorkbenchAction) ((IWorkbenchAction) i).dispose(); for (final IContributionItem i : getSite().getActionBars() .getMenuManager().getItems()) if (i instanceof IWorkbenchAction) ((IWorkbenchAction) i).dispose(); } renameTracker.reset(null); if (job != null) { job.cancel(); job = null; } Job.getJobManager().cancel(JobFamilies.HISTORY_DIFF); super.dispose(); } @Override public void setFocus() { if (repoHasBeenRemoved(currentRepo)) clearHistoryPage(); graph.getControl().setFocus(); } private boolean repoHasBeenRemoved(final Repository repo) { return (repo != null && repo.getDirectory() != null && !repo .getDirectory().exists()); } private void clearHistoryPage() { currentRepo = null; name = ""; //$NON-NLS-1$ input = null; clearCommentViewer(); clearFileViewer(); setInput(null); } private void clearCommentViewer() { commentViewer.setRepository(null); commentViewer.setInput(null); } private void clearFileViewer() { fileViewer.setTreeWalk(null, null); fileViewer.setInput(null); } @Override public Control getControl() { return topControl; } @Override public void refresh() { if (repoHasBeenRemoved(currentRepo)) clearHistoryPage(); this.input = null; inputSet(); } /** * @param compareMode * switch compare mode button of the view on / off */ public void setCompareMode(boolean compareMode) { store.setValue(UIPreferences.RESOURCEHISTORY_COMPARE_MODE, compareMode); } /** * @return the selection provider */ public ISelectionProvider getSelectionProvider() { return graph.getTableView(); } @Override public void onRefsChanged(final RefsChangedEvent e) { if (input == null || e.getRepository() != input.getRepository()) return; if (getControl().isDisposed()) return; synchronized (this) { if (refschangedRunnable == null) { refschangedRunnable = new Runnable() { @Override public void run() { if (!getControl().isDisposed()) { if (GitTraceLocation.HISTORYVIEW.isActive()) GitTraceLocation .getTrace() .trace( GitTraceLocation.HISTORYVIEW .getLocation(), "Executing async repository changed event"); //$NON-NLS-1$ refschangedRunnable = null; initAndStartRevWalk( !(e instanceof FetchHeadChangedEvent)); } } }; getControl().getDisplay().asyncExec(refschangedRunnable); } } } /** * Returns the last, tracked selection. If no selection has been tracked, * returns the current selection in the active part. * * @return selection */ private IStructuredSelection getSelection() { if (selectionTracker != null && selectionTracker.getSelection() != null) { return selectionTracker.getSelection(); } // fallback to current selection of the active part ISelection selection = getSite().getPage().getSelection(); if (selection != null) { return SelectionUtils.getStructuredSelection(selection); } return null; } /** * <p> * Determines the * {@link SelectionUtils#getMostFittingInput(IStructuredSelection, Object) * most fitting} HistoryPageInput for the {@link #getSelection() last * selection} and the given object. Most fitting means that the input will * contain all selected resources which are contained in the same repository * as the given object. If no most fitting input can be determined, the * given object is returned as is. * </p> * <p> * This is a workaround for the limitation of the GenericHistoryView that * only forwards the first part of a selection and adapts it immediately to * an {@link IResource}. * </p> * * @param object * The object to which the HistoryPageInput is tailored * @return the most fitting history input * @see SelectionUtils#getMostFittingInput(IStructuredSelection, Object) */ private Object getMostFittingInput(Object object) { IStructuredSelection selection = getSelection(); if (selection != null && !selection.isEmpty()) { HistoryPageInput mostFittingInput = SelectionUtils .getMostFittingInput(selection, object); if (mostFittingInput != null) { return mostFittingInput; } } return object; } @Override public boolean setInput(Object object) { try { Object useAsInput = getMostFittingInput(object); // reset tracked selection after it has been used to avoid wrong behavior if (selectionTracker != null) { selectionTracker.clearSelection(); } // hide the warning text initially setWarningText(null); trace = GitTraceLocation.HISTORYVIEW.isActive(); if (trace) GitTraceLocation.getTrace().traceEntry( GitTraceLocation.HISTORYVIEW.getLocation(), useAsInput); if (useAsInput == getInput()) return true; this.input = null; return super.setInput(useAsInput); } finally { if (trace) GitTraceLocation.getTrace().traceExit( GitTraceLocation.HISTORYVIEW.getLocation()); } } @Override public boolean inputSet() { try { if (trace) GitTraceLocation.getTrace().traceEntry( GitTraceLocation.HISTORYVIEW.getLocation()); if (this.input != null) return true; Object o = super.getInput(); if (o == null) { setErrorMessage(UIText.GitHistoryPage_NoInputMessage); return false; } boolean showHead = false; boolean showRef = false; boolean showTag = false; Repository repo = null; RevCommit selection = null; Ref ref = null; if (o instanceof IResource) { RepositoryMapping mapping = RepositoryMapping .getMapping((IResource) o); if (mapping != null) { repo = mapping.getRepository(); input = new HistoryPageInput(repo, new IResource[] { (IResource) o }); showHead = true; } } else if (o instanceof RepositoryTreeNode) { RepositoryTreeNode repoNode = (RepositoryTreeNode) o; repo = repoNode.getRepository(); switch (repoNode.getType()) { case FILE: File file = ((FileNode) repoNode).getObject(); input = new HistoryPageInput(repo, new File[] { file }); showHead = true; break; case FOLDER: File folder = ((FolderNode) repoNode).getObject(); input = new HistoryPageInput(repo, new File[] { folder }); showHead = true; break; case REF: input = new HistoryPageInput(repo); ref = ((RefNode) repoNode).getObject(); showRef = true; break; case ADDITIONALREF: input = new HistoryPageInput(repo); ref = ((AdditionalRefNode) repoNode).getObject(); showRef = true; break; case TAG: input = new HistoryPageInput(repo); ref = ((TagNode) repoNode).getObject(); showTag = true; break; default: input = new HistoryPageInput(repo); showHead = true; break; } } else if (o instanceof HistoryPageInput) { input = (HistoryPageInput) o; } else { IResource resource = AdapterUtils.adaptToAnyResource(o); if (resource != null) { RepositoryMapping mapping = RepositoryMapping .getMapping(resource); if (mapping != null) { repo = mapping.getRepository(); input = new HistoryPageInput(repo, new IResource[] { resource }); } } } if (repo == null) { repo = AdapterUtils.adapt(o, Repository.class); if (repo != null) { File file = AdapterUtils.adapt(o, File.class); if (file == null) { input = new HistoryPageInput(repo); } else { input = new HistoryPageInput(repo, new File[] { file }); } } } selection = AdapterUtils.adapt(o, RevCommit.class); if (input == null) { this.name = ""; //$NON-NLS-1$ setErrorMessage(UIText.GitHistoryPage_NoInputMessage); return false; } final IResource[] inResources = input.getItems(); final File[] inFiles = input.getFileList(); this.name = calculateName(input); // disable the filters if we have a Repository as input boolean filtersActive = inResources != null || inFiles != null; actions.showAllRepoVersionsAction.setEnabled(filtersActive); actions.showAllProjectVersionsAction.setEnabled(filtersActive); // the repository itself has no notion of projects actions.showAllFolderVersionsAction.setEnabled(inResources != null); actions.showAllResourceVersionsAction.setEnabled(filtersActive); setErrorMessage(null); try { initAndStartRevWalk(false); } catch (IllegalStateException e) { Activator.handleError(e.getMessage(), e, true); return false; } if (showHead) showHead(repo); if (showRef) showRef(ref, repo); if (showTag) showTag(ref, repo); if (selection != null) graph.selectCommitStored(selection); return true; } finally { if (trace) GitTraceLocation.getTrace().traceExit( GitTraceLocation.HISTORYVIEW.getLocation()); } } private void showHead(Repository repo) { try (RevWalk rw = new RevWalk(repo)) { ObjectId head = repo.resolve(Constants.HEAD); if (head == null) return; RevCommit c = rw.parseCommit(head); graph.selectCommitStored(c); } catch (IOException e) { Activator.handleError(e.getMessage(), e, true); } } private void showRef(Ref ref, Repository repo) { try (RevWalk rw = new RevWalk(repo)) { RevCommit c = rw.parseCommit(ref.getLeaf().getObjectId()); graph.selectCommit(c); } catch (IOException e) { Activator.handleError(e.getMessage(), e, true); } } private void showTag(Ref ref, Repository repo) { try (RevWalk rw = new RevWalk(repo)) { RevCommit c = null; RevObject any = rw.parseAny(ref.getLeaf().getObjectId()); if (any instanceof RevCommit) c = (RevCommit) any; else if (any instanceof RevTag) { RevTag t = rw.parseTag(any); Object anyCommit = rw.parseAny(t.getObject()); if (anyCommit instanceof RevCommit) c = (RevCommit) anyCommit; } if (c != null) graph.selectCommit(c); } catch (IOException e) { Activator.handleError(e.getMessage(), e, true); } } private static String calculateName(HistoryPageInput in) { // we always visualize the current input in the form // <type>: <path> [<respository name>] // in order to give the user an understanding which context // menus they can expect with the current input // we show the filter hint only upon getDescription() // as it wrongly pollutes the navigation history final String repositoryName = Activator.getDefault() .getRepositoryUtil().getRepositoryName(in.getRepository()); if (in.getItems() == null && in.getFileList() == null) // plain repository, no files specified return NLS.bind(UIText.GitHistoryPage_RepositoryNamePattern, repositoryName); else if (in.getItems() != null && in.getItems().length == 1) { // single resource IResource resource = in.getItems()[0]; final String type; switch (resource.getType()) { case IResource.FILE: type = UIText.GitHistoryPage_FileType; break; case IResource.PROJECT: type = UIText.GitHistoryPage_ProjectType; break; default: type = UIText.GitHistoryPage_FolderType; break; } String path = resource.getFullPath().makeRelative().toString(); if (resource.getType() == IResource.FOLDER) path = path + '/'; return NLS.bind(NAME_PATTERN, new Object[] { type, path, repositoryName }); } else if (in.getFileList() != null && in.getFileList().length == 1) { // single file from Repository File resource = in.getFileList()[0]; String path; final String type; if (resource.isDirectory()) { type = UIText.GitHistoryPage_FolderType; path = resource.getPath() + IPath.SEPARATOR; } else { type = UIText.GitHistoryPage_FileType; path = resource.getPath(); } return NLS.bind(NAME_PATTERN, new Object[] { type, path, repositoryName }); } else { // user has selected multiple resources and then hits Team->Show in // History (the generic history view cannot deal with multiple // selection) int count = 0; StringBuilder b = new StringBuilder(); if (in.getItems() != null) { count = in.getItems().length; for (IResource res : in.getItems()) { b.append(res.getFullPath()); if (res.getType() == IResource.FOLDER) b.append('/'); // limit the total length if (b.length() > 100) { b.append("... "); //$NON-NLS-1$ break; } b.append(", "); //$NON-NLS-1$ } } if (in.getFileList() != null) { count = in.getFileList().length; for (File file : in.getFileList()) { b.append(getRepoRelativePath(in.getRepository(), file)); if (file.isDirectory()) b.append('/'); // limit the total length if (b.length() > 100) { b.append("... "); //$NON-NLS-1$ break; } b.append(", "); //$NON-NLS-1$ } } // trim off the last ", " (or " " if total length exceeded) if (b.length() > 2) b.setLength(b.length() - 2); String multiResourcePrefix = NLS.bind( UIText.GitHistoryPage_MultiResourcesType, Integer .valueOf(count)); return NLS.bind(NAME_PATTERN, new Object[] { multiResourcePrefix, b.toString(), repositoryName }); } } private static String getRepoRelativePath(Repository repo, File file) { IPath workdirPath = new Path(repo.getWorkTree().getPath()); IPath filePath = new Path(file.getPath()).setDevice(null); return filePath.removeFirstSegments(workdirPath.segmentCount()) .toString(); } /** * @param message * the message to display instead of the control */ public void setErrorMessage(final String message) { if (trace) GitTraceLocation.getTrace().traceEntry( GitTraceLocation.HISTORYVIEW.getLocation(), message); getHistoryPageSite().getShell().getDisplay().asyncExec(new Runnable() { @Override public void run() { if (topControl.isDisposed()) return; StackLayout layout = (StackLayout) topControl.getLayout(); if (message != null) { errorText.setText(message); layout.topControl = errorText; } else { errorText.setText(""); //$NON-NLS-1$ layout.topControl = historyControl; } topControl.layout(); } }); if (trace) GitTraceLocation.getTrace().traceExit( GitTraceLocation.HISTORYVIEW.getLocation()); } @Override public boolean isValidInput(final Object object) { return canShowHistoryFor(object); } @Override public Object getAdapter(final Class adapter) { return null; } @Override public String getDescription() { // this doesn't seem to be rendered anywhere, but still... String filterHint = null; switch (showAllFilter) { case SHOWALLREPO: filterHint = UIText.GitHistoryPage_AllChangesInRepoHint; break; case SHOWALLPROJECT: filterHint = UIText.GitHistoryPage_AllChangesInProjectHint; break; case SHOWALLFOLDER: filterHint = UIText.GitHistoryPage_AllChangesInFolderHint; break; case SHOWALLRESOURCE: filterHint = UIText.GitHistoryPage_AllChangesOfResourceHint; break; } return NLS.bind(DESCRIPTION_PATTERN, getName(), filterHint); } @Override public String getName() { return this.name; } /** * @return the internal input object, or <code>null</code> */ public HistoryPageInput getInputInternal() { return this.input; } void setWarningTextInUIThread(final Job j) { graph.getControl().getDisplay().asyncExec(new Runnable() { @Override public void run() { if (!graph.getControl().isDisposed() && job == j) { setWarningText(UIText.GitHistoryPage_ListIncompleteWarningMessage); } } }); } @SuppressWarnings("boxing") void showCommitList(final Job j, final SWTCommitList list, final SWTCommit[] asArray, final RevCommit toSelect, final boolean incomplete, final RevFlag highlightFlag) { if (trace) GitTraceLocation.getTrace().traceEntry( GitTraceLocation.HISTORYVIEW.getLocation(), new Object[] { list.size()}); if (job != j || graph.getControl().isDisposed()) return; graph.getControl().getDisplay().asyncExec(new Runnable() { @Override public void run() { if (!graph.getControl().isDisposed() && job == j) { graph.setInput(highlightFlag, list, asArray, input, true); if (toSelect != null) graph.selectCommit(toSelect); if (getFollowRenames()) updateInterestingPathsOfFileViewer(); if (trace) GitTraceLocation.getTrace().trace( GitTraceLocation.HISTORYVIEW.getLocation(), "Setting input to table"); //$NON-NLS-1$ final Object currentInput = getInput(); searchBar.setInput(new ICommitsProvider() { @Override public Object getSearchContext() { return currentInput; } @Override public SWTCommit[] getCommits() { return asArray; } @Override public RevFlag getHighlight() { return highlightFlag; } }); actions.findAction.setEnabled(true); if (store.getBoolean( UIPreferences.RESOURCEHISTORY_SHOW_FINDTOOLBAR)) { searchBar.setVisible(true); } if (incomplete) setWarningText(UIText.GitHistoryPage_ListIncompleteWarningMessage); else setWarningText(null); setErrorMessage(null); } } }); if (trace) GitTraceLocation.getTrace().traceExit( GitTraceLocation.HISTORYVIEW.getLocation()); } private void updateInterestingPathsOfFileViewer() { fileViewer.setInterestingPaths(fileViewerInterestingPaths); } private void setWarningText(String warning) { if (warningComposite == null || warningComposite.isDisposed()) return; GridData gd = (GridData) warningComposite.getLayoutData(); gd.exclude = warning == null; warningComposite.setVisible(!gd.exclude); if (warning != null) warningLabel.setText(warning); else warningLabel.setText(""); //$NON-NLS-1$ warningComposite.getParent().layout(true); } void initAndStartRevWalk(boolean forceNewWalk) throws IllegalStateException { try { if (trace) GitTraceLocation.getTrace().traceEntry( GitTraceLocation.HISTORYVIEW.getLocation()); if (input == null) return; Repository db = input.getRepository(); if (repoHasBeenRemoved(db)) { clearHistoryPage(); return; } AnyObjectId headId = resolveHead(db, true); if (headId == null) { graph.getTableView().setInput(new SWTCommit[0]); currentHeadId = null; currentFetchHeadId = null; return; } AnyObjectId fetchHeadId = resolveFetchHead(db); List<FilterPath> paths = buildFilterPaths(input.getItems(), input .getFileList(), db); if (forceNewWalk || shouldRedraw(db, headId, fetchHeadId, paths)) { releaseGenerateHistoryJob(); SWTWalk walk = createNewWalk(db, headId, fetchHeadId); setWalkStartPoints(walk, db, headId); setupFileViewer(walk, db, paths); setupCommentViewer(db); loadInitialHistory(walk); } else // needed for context menu and double click graph.setHistoryPageInput(input); } finally { if (trace) GitTraceLocation.getTrace().traceExit( GitTraceLocation.HISTORYVIEW.getLocation()); } } private boolean shouldRedraw(Repository db, AnyObjectId headId, AnyObjectId fetchHeadId, List<FilterPath> paths) { boolean pathChanged = pathChanged(pathFilters, paths); boolean headChanged = headId == null || !headId.equals(currentHeadId); boolean repoChanged = false; boolean allBranchesChanged = currentShowAllBranches != store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_ALL_BRANCHES); currentShowAllBranches = store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_ALL_BRANCHES); boolean additionalRefsChange = currentShowAdditionalRefs != store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS); currentShowAdditionalRefs = store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS); boolean fetchHeadChanged = currentShowAdditionalRefs && fetchHeadId != null && !fetchHeadId.equals(currentFetchHeadId); boolean showNotesChanged = currentShowNotes != store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_NOTES); currentShowNotes = store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_NOTES); boolean followRenamesChanged = currentFollowRenames != getFollowRenames(); currentFollowRenames = getFollowRenames(); if (!db.equals(currentRepo)) { repoChanged = true; currentRepo = db; } return pathChanged || headChanged || fetchHeadChanged || repoChanged || allBranchesChanged || additionalRefsChange || showNotesChanged || followRenamesChanged; } /** * @return whether following renames is currently enabled */ protected boolean getFollowRenames() { return store.getBoolean(UIPreferences.RESOURCEHISTORY_FOLLOW_RENAMES); } private AnyObjectId resolveHead(Repository db, boolean acceptNull) { AnyObjectId headId; try { headId = db.resolve(Constants.HEAD); } catch (IOException e) { throw new IllegalStateException(NLS.bind( UIText.GitHistoryPage_errorParsingHead, Activator .getDefault().getRepositoryUtil() .getRepositoryName(db)), e); } if (headId == null && !acceptNull) throw new IllegalStateException(NLS.bind( UIText.GitHistoryPage_errorParsingHead, Activator .getDefault().getRepositoryUtil() .getRepositoryName(db))); return headId; } private AnyObjectId resolveFetchHead(Repository db) { try { return db.resolve(Constants.FETCH_HEAD); } catch (IOException e) { return null; } } private ArrayList<FilterPath> buildFilterPaths(final IResource[] inResources, final File[] inFiles, final Repository db) throws IllegalStateException { final ArrayList<FilterPath> paths; if (inResources != null) { paths = new ArrayList<>(inResources.length); for (final IResource r : inResources) { final RepositoryMapping map = RepositoryMapping.getMapping(r); if (map == null) continue; if (db != map.getRepository()) throw new IllegalStateException( UIText.RepositoryAction_multiRepoSelection); if (showAllFilter == ShowFilter.SHOWALLFOLDER) { final String path; // if the resource's parent is the workspace root, we will // get nonsense from map.getRepoRelativePath(), so we // check here and use the project instead if (r.getParent() instanceof IWorkspaceRoot) path = map.getRepoRelativePath(r.getProject()); else path = map.getRepoRelativePath(r.getParent()); if (path != null && path.length() > 0) paths.add(new FilterPath(path, false)); } else if (showAllFilter == ShowFilter.SHOWALLPROJECT) { final String path = map.getRepoRelativePath(r.getProject()); if (path != null && path.length() > 0) paths.add(new FilterPath(path, false)); } else if (showAllFilter == ShowFilter.SHOWALLREPO) { // nothing } else /* if (showAllFilter == ShowFilter.SHOWALLRESOURCE) */{ final String path = map.getRepoRelativePath(r); if (path != null && path.length() > 0) paths.add(new FilterPath(path, r.getType() == IResource.FILE)); } } } else if (inFiles != null) { IPath workdirPath = new Path(db.getWorkTree().getPath()); IPath gitDirPath = new Path(db.getDirectory().getPath()); int segmentCount = workdirPath.segmentCount(); paths = new ArrayList<>(inFiles.length); for (File file : inFiles) { IPath filePath; boolean isRegularFile; if (showAllFilter == ShowFilter.SHOWALLFOLDER) { filePath = new Path(file.getParentFile().getPath()); isRegularFile = false; } else if (showAllFilter == ShowFilter.SHOWALLPROJECT || showAllFilter == ShowFilter.SHOWALLREPO) // we don't know of projects here -> treat as SHOWALLREPO continue; else /* if (showAllFilter == ShowFilter.SHOWALLRESOURCE) */{ filePath = new Path(file.getPath()); isRegularFile = file.isFile(); } if (gitDirPath.isPrefixOf(filePath)) throw new IllegalStateException( NLS .bind( UIText.GitHistoryPage_FileOrFolderPartOfGitDirMessage, filePath.toOSString())); IPath pathToAdd = filePath.removeFirstSegments(segmentCount) .setDevice(null); if (!pathToAdd.isEmpty()) paths.add(new FilterPath(pathToAdd.toString(), isRegularFile)); } } else paths = new ArrayList<>(0); return paths; } private boolean pathChanged(final List<FilterPath> o, final List<FilterPath> n) { if (o == null) return !n.isEmpty(); return !o.equals(n); } private @NonNull SWTWalk createNewWalk(Repository db, AnyObjectId headId, AnyObjectId fetchHeadId) { currentHeadId = headId; currentFetchHeadId = fetchHeadId; SWTWalk walk = new SWTWalk(db); try { if (store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS)) walk.addAdditionalRefs(db.getRefDatabase() .getAdditionalRefs()); walk.addAdditionalRefs(db.getRefDatabase() .getRefs(Constants.R_NOTES).values()); } catch (IOException e) { throw new IllegalStateException(NLS.bind( UIText.GitHistoryPage_errorReadingAdditionalRefs, Activator .getDefault().getRepositoryUtil() .getRepositoryName(db)), e); } walk.sort(RevSort.COMMIT_TIME_DESC, true); walk.sort(RevSort.BOUNDARY, true); walk.setRetainBody(false); return walk; } private void setWalkStartPoints(RevWalk walk, Repository db, AnyObjectId headId) { try { if (store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_ALL_BRANCHES)) { markStartAllRefs(walk, Constants.R_HEADS); markStartAllRefs(walk, Constants.R_REMOTES); markStartAllRefs(walk, Constants.R_TAGS); } if (store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS)) markStartAdditionalRefs(walk); if (store .getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_NOTES)) markStartAllRefs(walk, Constants.R_NOTES); else markUninteresting(walk, Constants.R_NOTES); walk.markStart(walk.parseCommit(headId)); } catch (IOException e) { throw new IllegalStateException(NLS.bind( UIText.GitHistoryPage_errorSettingStartPoints, Activator .getDefault().getRepositoryUtil() .getRepositoryName(db)), e); } } private void setupCommentViewer(Repository db) { commentViewer.setRepository(db); commentViewer.refresh(); } private TreeWalk setupFileViewer(RevWalk walk, Repository db, List<FilterPath> paths) { final TreeWalk fileWalker = createFileWalker(walk, db, paths); fileViewer.setTreeWalk(db, fileWalker); fileViewer.setInterestingPaths(fileViewerInterestingPaths); fileViewer.refresh(); return fileWalker; } private void formatDiffs(final List<FileDiff> diffs) { Job.getJobManager().cancel(JobFamilies.HISTORY_DIFF); if (diffs.isEmpty()) { if (UIUtils.isUsable(diffViewer)) { IDocument document = new Document(); diffViewer.setDocument(document); } return; } final Repository repository = fileViewer.getRepository(); Job formatJob = new Job(UIText.GitHistoryPage_FormatDiffJobName) { @Override protected IStatus run(IProgressMonitor monitor) { if (monitor.isCanceled()) { return Status.CANCEL_STATUS; } int maxLines = Activator.getDefault().getPreferenceStore() .getInt(UIPreferences.HISTORY_MAX_DIFF_LINES); final DiffDocument document = new DiffDocument(); try (DiffRegionFormatter formatter = new DiffRegionFormatter( document, document.getLength(), maxLines)) { SubMonitor progress = SubMonitor.convert(monitor, diffs.size()); for (FileDiff diff : diffs) { if (progress.isCanceled() || diff.getCommit().getParentCount() > 1 || document.getNumberOfLines() > maxLines) { break; } progress.subTask(diff.getPath()); try { formatter.write(repository, diff); } catch (IOException ignore) { // Ignored } progress.worked(1); } if (progress.isCanceled()) { return Status.CANCEL_STATUS; } document.connect(formatter); } UIJob uiJob = new UIJob(UIText.GitHistoryPage_FormatDiffJobName) { @Override public IStatus runInUIThread(IProgressMonitor uiMonitor) { if (uiMonitor.isCanceled()) { return Status.CANCEL_STATUS; } if (UIUtils.isUsable(diffViewer)) { diffViewer.setDocument(document); resizeCommentAndDiffScrolledComposite(); } return Status.OK_STATUS; } @Override public boolean belongsTo(Object family) { return JobFamilies.HISTORY_DIFF.equals(family); } }; uiJob.setRule(pageSchedulingRule); GitHistoryPage.this.schedule(uiJob); return Status.OK_STATUS; } @Override public boolean belongsTo(Object family) { return JobFamilies.HISTORY_DIFF.equals(family); } }; formatJob.setRule(pageSchedulingRule); schedule(formatJob); } private void setWrap(boolean wrap) { commentViewer.getTextWidget().setWordWrap(wrap); diffViewer.getTextWidget().setWordWrap(wrap); resizeCommentAndDiffScrolledComposite(); } private void resizeCommentAndDiffScrolledComposite() { resizing = true; long start = 0; int lines = 0; if (trace) { IDocument document = diffViewer.getDocument(); lines = document != null ? document.getNumberOfLines() : 0; System.out.println("Lines: " + lines); //$NON-NLS-1$ if (lines > 1) { new Exception("resizeCommentAndDiffScrolledComposite") //$NON-NLS-1$ .printStackTrace(System.out); } start = System.currentTimeMillis(); } Point size = commentAndDiffComposite .computeSize(SWT.DEFAULT, SWT.DEFAULT); commentAndDiffScrolledComposite.setMinSize(size); resizing = false; if (trace) { long stop = System.currentTimeMillis(); long time = stop - start; long lps = (lines * 1000) / (time + 1); System.out .println("Resize + diff: " + time + " ms, line/s: " + lps); //$NON-NLS-1$ //$NON-NLS-2$ } } private TreeWalk createFileWalker(RevWalk walk, Repository db, List<FilterPath> paths) { final TreeWalk fileWalker = new TreeWalk(db); fileWalker.setRecursive(true); fileWalker.setFilter(TreeFilter.ANY_DIFF); if (store.getBoolean(UIPreferences.RESOURCEHISTORY_FOLLOW_RENAMES) && !paths.isEmpty() && allRegularFiles(paths)) { pathFilters = paths; List<String> selectedPaths = new ArrayList<>(paths.size()); for (FilterPath filterPath : paths) selectedPaths.add(filterPath.getPath()); fileViewerInterestingPaths = new HashSet<>(selectedPaths); TreeFilter followFilter = createFollowFilterFor(selectedPaths); walk.setTreeFilter(followFilter); walk.setRevFilter(renameTracker.getFilter()); } else if (paths.size() > 0) { pathFilters = paths; List<String> stringPaths = new ArrayList<>(paths.size()); for (FilterPath p : paths) stringPaths.add(p.getPath()); walk.setTreeFilter(AndTreeFilter.create(PathFilterGroup .createFromStrings(stringPaths), TreeFilter.ANY_DIFF)); fileViewerInterestingPaths = new HashSet<>(stringPaths); } else { pathFilters = null; walk.setTreeFilter(TreeFilter.ALL); fileViewerInterestingPaths = null; } return fileWalker; } /** * Creates a filter for the given files, will make sure that renames/copies * of all files will be followed. * @param paths the list of files to follow, must not be <code>null</code> or empty * @return the ORed list of {@link FollowFilter follow filters} */ private TreeFilter createFollowFilterFor(List<String> paths) { if (paths == null || paths.isEmpty()) throw new IllegalArgumentException("paths must not be null nor empty"); //$NON-NLS-1$ DiffConfig diffConfig = currentRepo.getConfig().get(DiffConfig.KEY); List<TreeFilter> followFilters = new ArrayList<>(paths.size()); for (String path : paths) followFilters.add(createFollowFilter(path, diffConfig)); if (followFilters.size() == 1) { FollowFilter followFilter = (FollowFilter) followFilters.get(0); renameTracker.reset(followFilter.getPath()); return followFilters.get(0); } // TODO: this scenario is not supported by JGit: RewriteTreeFilter // can not handle composite TreeFilters and expects a plain // FollowFilter for rename detection. return OrTreeFilter.create(followFilters); } private FollowFilter createFollowFilter(String path, DiffConfig diffConfig) { FollowFilter followFilter = FollowFilter.create(path, diffConfig); followFilter.setRenameCallback(new RenameCallback() { @Override public void renamed(DiffEntry entry) { renameTracker.getCallback().renamed(entry); if (fileViewerInterestingPaths != null) { fileViewerInterestingPaths.add(entry.getOldPath()); fileViewerInterestingPaths.add(entry.getNewPath()); } } }); return followFilter; } /** * @return Returns <code>true</code> if <b>all</b> filterpaths refer to plain files, * or if the list is empty. * @param paths the paths to check */ private boolean allRegularFiles(List<FilterPath> paths) { for (FilterPath filterPath : paths) if (!filterPath.isRegularFile()) return false; return true; } @Override public void loadItem(int item) { if (job != null && job.loadMoreItemsThreshold() < item) { loadHistory(item); } } @Override public void loadCommit(RevCommit c) { if (job == null) return; job.setLoadHint(c); if (trace) GitTraceLocation.getTrace().trace( GitTraceLocation.HISTORYVIEW.getLocation(), "Scheduling GenerateHistoryJob"); //$NON-NLS-1$ schedule(job); } /** * Load initial history items * * @param walk * the revwalk, non null */ private void loadInitialHistory(@NonNull RevWalk walk) { job = new GenerateHistoryJob(this, graph.getControl(), walk, resources); job.setRule(pageSchedulingRule); job.setLoadHint(INITIAL_ITEM); if (trace) GitTraceLocation.getTrace().trace( GitTraceLocation.HISTORYVIEW.getLocation(), "Scheduling initial GenerateHistoryJob"); //$NON-NLS-1$ schedule(job); } /** * Load history items incrementally * * @param itemToLoad * hint for index of item that should be loaded */ private void loadHistory(final int itemToLoad) { if (job == null) { return; } job.setLoadHint(itemToLoad); if (trace) GitTraceLocation.getTrace().trace( GitTraceLocation.HISTORYVIEW.getLocation(), "Scheduling incremental GenerateHistoryJob"); //$NON-NLS-1$ schedule(job); } private IWorkbenchPartSite getPartSite() { final IWorkbenchPart part = getHistoryPageSite().getPart(); IWorkbenchPartSite site = null; if (part != null) site = part.getSite(); return site; } private void schedule(final Job j) { IWorkbenchPartSite site = getPartSite(); if (site != null) { final IWorkbenchSiteProgressService p; p = AdapterUtils.adapt(site, IWorkbenchSiteProgressService.class); if (p != null) { p.schedule(j, 0, true /* use half-busy cursor */); return; } } j.schedule(); } /** * {@link RevWalk#markStart(RevCommit)} all refs with given prefix to mark * start of graph traversal using currentWalker * @param walk the walk to be configured * * @param prefix * prefix of refs to be marked * @throws IOException * @throws MissingObjectException * @throws IncorrectObjectTypeException */ private void markStartAllRefs(RevWalk walk, String prefix) throws IOException, MissingObjectException, IncorrectObjectTypeException { for (Entry<String, Ref> refEntry : input.getRepository() .getRefDatabase().getRefs(prefix).entrySet()) { Ref ref = refEntry.getValue(); if (ref.isSymbolic()) continue; markStartRef(walk, ref); } } private void markStartAdditionalRefs(RevWalk walk) throws IOException { List<Ref> additionalRefs = input.getRepository().getRefDatabase() .getAdditionalRefs(); for (Ref ref : additionalRefs) markStartRef(walk, ref); } private void markStartRef(RevWalk walk, Ref ref) throws IOException, IncorrectObjectTypeException { try { RevObject refTarget = walk.parseAny(ref.getLeaf().getObjectId()); RevObject peeled = walk.peel(refTarget); if (peeled instanceof RevCommit) walk.markStart((RevCommit) peeled); } catch (MissingObjectException e) { // If there is a ref which points to Nirvana then we should simply // ignore this ref. We should not let a corrupt ref cause that the // history view is not filled at all } } private void markUninteresting(RevWalk walk, String prefix) throws IOException, MissingObjectException, IncorrectObjectTypeException { for (Entry<String, Ref> refEntry : input.getRepository() .getRefDatabase().getRefs(prefix).entrySet()) { Ref ref = refEntry.getValue(); if (ref.isSymbolic()) continue; Object refTarget = walk .parseAny(ref.getLeaf().getObjectId()); if (refTarget instanceof RevCommit) walk.markUninteresting((RevCommit) refTarget); } } private void releaseGenerateHistoryJob() { if (job != null) { if (job.getState() != Job.NONE) job.cancel(); job.release(); job = null; } } @Override public ShowInContext getShowInContext() { if (fileViewer != null && fileViewer.getControl().isFocusControl()) return fileViewer.getShowInContext(); else return null; } @Override public String[] getShowInTargetIds() { return new String[] { IHistoryView.VIEW_ID }; } /** * Get renamed path in given commit with initial starting path * * @param path * @param commit * @return actual path in commit */ public String getRenamedPath(String path, ObjectId commit) { return renameTracker.getPath(commit, path); } private static class FooterTokenScanner extends HyperlinkTokenScanner { private static final Pattern ITALIC_LINE = Pattern .compile("^[A-Z](?:[A-Za-z]+-)+by: "); //$NON-NLS-1$ private final IToken italicToken; public FooterTokenScanner(SourceViewerConfiguration configuration, ISourceViewer viewer) { super(configuration, viewer); Object defaults = defaultToken.getData(); TextAttribute italic; if (defaults instanceof TextAttribute) { TextAttribute defaultAttribute = (TextAttribute) defaults; int style = defaultAttribute.getStyle() ^ SWT.ITALIC; italic = new TextAttribute(defaultAttribute.getForeground(), defaultAttribute.getBackground(), style, defaultAttribute.getFont()); } else { italic = new TextAttribute(null, null, SWT.ITALIC); } italicToken = new Token(italic); } @Override protected IToken scanToken() { // If we're at a "Signed-off-by" or similar footer line, make it // italic. try { IRegion region = document .getLineInformationOfOffset(currentOffset); if (currentOffset == region.getOffset()) { String line = document.get(currentOffset, region.getLength()); Matcher m = ITALIC_LINE.matcher(line); if (m.find()) { currentOffset = Math.min(endOfRange, currentOffset + region.getLength()); return italicToken; } } } catch (BadLocationException e) { // Ignore and return null below. } return null; } } }