/******************************************************************************* * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br> * Copyright (C) 2013, Robin Stocker <robin@nibor.org> * Copyright (C) 2016, Daniel Megert <daniel_megert@ch.ibm.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.revision; import java.io.File; import java.lang.reflect.InvocationTargetException; import org.eclipse.compare.CompareConfiguration; import org.eclipse.compare.CompareEditorInput; import org.eclipse.compare.ICompareContainer; import org.eclipse.compare.IEditableContent; import org.eclipse.compare.IResourceProvider; import org.eclipse.compare.ITypedElement; import org.eclipse.compare.structuremergeviewer.DiffNode; import org.eclipse.compare.structuremergeviewer.Differencer; import org.eclipse.compare.structuremergeviewer.ICompareInput; import org.eclipse.compare.structuremergeviewer.IDiffElement; import org.eclipse.compare.structuremergeviewer.IStructureComparator; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFileState; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IStorage; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.egit.core.Activator; import org.eclipse.egit.core.AdapterUtils; import org.eclipse.egit.core.internal.storage.IndexFileRevision; import org.eclipse.egit.core.internal.storage.OpenWorkspaceVersionEnabled; import org.eclipse.egit.ui.internal.CommonUtils; import org.eclipse.egit.ui.internal.CompareUtils; import org.eclipse.egit.ui.internal.EgitUiEditorUtils; import org.eclipse.egit.ui.internal.UIText; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.graphics.Image; import org.eclipse.team.core.history.IFileRevision; import org.eclipse.team.internal.ui.synchronize.EditableSharedDocumentAdapter.ISharedDocumentAdapterListener; import org.eclipse.team.internal.ui.synchronize.LocalResourceSaveableComparison; import org.eclipse.team.internal.ui.synchronize.LocalResourceTypedElement; import org.eclipse.team.ui.synchronize.SaveableCompareEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.ISaveablesLifecycleListener; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.Saveable; import org.eclipse.ui.SaveablesLifecycleEvent; /** * The input provider for the compare editor when working on resources * under Git control. */ @SuppressWarnings("restriction") public class GitCompareFileRevisionEditorInput extends SaveableCompareEditorInput { private ITypedElement left; private ITypedElement right; private ITypedElement ancestor; /** * Creates a new CompareFileRevisionEditorInput. * @param left * @param right * @param page */ public GitCompareFileRevisionEditorInput(ITypedElement left, ITypedElement right, IWorkbenchPage page) { super(new CompareConfiguration(), page); this.left = left; this.right = right; } /** * Creates a new CompareFileRevisionEditorInput. * @param left * @param right * @param ancestor * @param page */ public GitCompareFileRevisionEditorInput(ITypedElement left, ITypedElement right, ITypedElement ancestor, IWorkbenchPage page) { super(new CompareConfiguration(), page); this.left = left; this.right = right; this.ancestor = ancestor; } FileRevisionTypedElement getRightRevision() { if (right instanceof FileRevisionTypedElement) { return (FileRevisionTypedElement) right; } return null; } FileRevisionTypedElement getLeftRevision() { if (left instanceof FileRevisionTypedElement) { return (FileRevisionTypedElement) left; } return null; } FileRevisionTypedElement getAncestorRevision() { if (ancestor instanceof FileRevisionTypedElement) return (FileRevisionTypedElement) ancestor; return null; } private static void ensureContentsCached(FileRevisionTypedElement left, FileRevisionTypedElement right, FileRevisionTypedElement ancestor, IProgressMonitor monitor) { if (left != null) { try { left.cacheContents(monitor); } catch (CoreException e) { Activator.logError(e.getMessage(), e); } } if (right != null) { try { right.cacheContents(monitor); } catch (CoreException e) { Activator.logError(e.getMessage(), e); } } if (ancestor != null) { try { ancestor.cacheContents(monitor); } catch (CoreException e) { Activator.logError(e.getMessage(), e); } } } private boolean isLeftEditable(ICompareInput input) { Object tmpLeft = input.getLeft(); return isEditable(tmpLeft); } private boolean isRightEditable(ICompareInput input) { Object tmpRight = input.getRight(); return isEditable(tmpRight); } private boolean isEditable(Object object) { if (object instanceof IEditableContent) { return ((IEditableContent) object).isEditable(); } return false; } private IResource getResource() { if (left instanceof IResourceProvider) { IResourceProvider resourceProvider = (IResourceProvider) left; return resourceProvider.getResource(); } return null; } private ICompareInput createCompareInput() { return compare(left, right, ancestor); } private DiffNode compare(ITypedElement actLeft, ITypedElement actRight, ITypedElement actAncestor) { if (actLeft.getType().equals(ITypedElement.FOLDER_TYPE)) { // return new MyDiffContainer(null, left,right); DiffNode diffNode = new DiffNode(null, Differencer.CHANGE, actAncestor, actLeft, actRight); ITypedElement[] lc = (ITypedElement[])((IStructureComparator)actLeft).getChildren(); ITypedElement[] rc = (ITypedElement[])((IStructureComparator)actRight).getChildren(); ITypedElement[] ac = null; if (actAncestor != null) ac = (ITypedElement[]) ((IStructureComparator) actAncestor) .getChildren(); int li=0; int ri=0; while (li<lc.length && ri<rc.length) { ITypedElement ln = lc[li]; ITypedElement rn = rc[ri]; ITypedElement an = null; if (ac != null) an = ac[ri]; int compareTo = ln.getName().compareTo(rn.getName()); // TODO: Git ordering! if (compareTo == 0) { if (!ln.equals(rn)) diffNode.add(compare(ln,rn, an)); ++li; ++ri; } else if (compareTo < 0) { DiffNode childDiffNode = new DiffNode(Differencer.ADDITION, an, ln, null); diffNode.add(childDiffNode); if (ln.getType().equals(ITypedElement.FOLDER_TYPE)) { ITypedElement[] children = (ITypedElement[])((IStructureComparator)ln).getChildren(); if(children != null && children.length > 0) { for (ITypedElement child : children) { childDiffNode.add(addDirectoryFiles(child, Differencer.ADDITION)); } } } ++li; } else { DiffNode childDiffNode = new DiffNode(Differencer.DELETION, an, null, rn); diffNode.add(childDiffNode); if (rn.getType().equals(ITypedElement.FOLDER_TYPE)) { ITypedElement[] children = (ITypedElement[])((IStructureComparator)rn).getChildren(); if(children != null && children.length > 0) { for (ITypedElement child : children) { childDiffNode.add(addDirectoryFiles(child, Differencer.DELETION)); } } } ++ri; } } while (li<lc.length) { ITypedElement ln = lc[li]; ITypedElement an = null; if (ac != null) an= ac[li]; DiffNode childDiffNode = new DiffNode(Differencer.ADDITION, an, ln, null); diffNode.add(childDiffNode); if (ln.getType().equals(ITypedElement.FOLDER_TYPE)) { ITypedElement[] children = (ITypedElement[])((IStructureComparator)ln).getChildren(); if(children != null && children.length > 0) { for (ITypedElement child : children) { childDiffNode.add(addDirectoryFiles(child, Differencer.ADDITION)); } } } ++li; } while (ri<rc.length) { ITypedElement rn = rc[ri]; ITypedElement an = null; if (ac != null) an = ac[ri]; DiffNode childDiffNode = new DiffNode(Differencer.DELETION, an, null, rn); diffNode.add(childDiffNode); if (rn.getType().equals(ITypedElement.FOLDER_TYPE)) { ITypedElement[] children = (ITypedElement[])((IStructureComparator)rn).getChildren(); if(children != null && children.length > 0) { for (ITypedElement child : children) { childDiffNode.add(addDirectoryFiles(child, Differencer.DELETION)); } } } ++ri; } return diffNode; } else { if (actAncestor != null) return new DiffNode(Differencer.CONFLICTING, actAncestor, actLeft, actRight); else return new DiffNode(actLeft, actRight); } } private DiffNode addDirectoryFiles(ITypedElement elem, int diffType) { ITypedElement l = null; ITypedElement r = null; if (diffType == Differencer.DELETION) { r = elem; } else { l = elem; } if (elem.getType().equals(ITypedElement.FOLDER_TYPE)) { DiffNode diffNode = null; diffNode = new DiffNode(null,Differencer.CHANGE,null,l,r); ITypedElement[] children = (ITypedElement[])((IStructureComparator)elem).getChildren(); for (ITypedElement child : children) { diffNode.add(addDirectoryFiles(child, diffType)); } return diffNode; } else { return new DiffNode(diffType, null, l, r); } } private void initLabels(ICompareInput input) { CompareConfiguration cc = getCompareConfiguration(); if (getLeftRevision() != null) { String leftLabel = getFileRevisionLabel(getLeftRevision()); cc.setLeftLabel(leftLabel); } else if (getResource() != null) { String label = NLS.bind(UIText.GitCompareFileRevisionEditorInput_LocalLabel, new Object[]{ input.getLeft().getName() }); cc.setLeftLabel(label); } else { cc.setLeftLabel(left.getName()); } if (getRightRevision() != null) { String rightLabel = getFileRevisionLabel(getRightRevision()); cc.setRightLabel(rightLabel); } else { cc.setRightLabel(right.getName()); } if (getAncestorRevision() != null) { String ancestorLabel = getFileRevisionLabel(getAncestorRevision()); cc.setAncestorLabel(ancestorLabel); } } private String getFileRevisionLabel(FileRevisionTypedElement element) { Object fileObject = element.getFileRevision(); if (fileObject instanceof LocalFileRevision){ return NLS.bind(UIText.GitCompareFileRevisionEditorInput_LocalHistoryLabel, new Object[]{element.getName(), element.getTimestamp()}); } else if (fileObject instanceof IndexFileRevision) { if (isEditable(element)) return NLS.bind( UIText.GitCompareFileRevisionEditorInput_IndexEditableLabel, element.getName()); else return NLS.bind( UIText.GitCompareFileRevisionEditorInput_IndexLabel, element.getName()); } else { return NLS.bind(UIText.GitCompareFileRevisionEditorInput_RevisionLabel, new Object[]{element.getName(), CompareUtils.truncatedRevision(element.getContentIdentifier()), element.getAuthor()}); } } /* (non-Javadoc) * @see org.eclipse.compare.CompareEditorInput#getToolTipText() */ @Override public String getToolTipText() { Object[] titleObject = new Object[3]; titleObject[0] = getLongName(left); titleObject[1] = CompareUtils.truncatedRevision(getContentIdentifier(getLeftRevision())); titleObject[2] = CompareUtils.truncatedRevision(getContentIdentifier(getRightRevision())); return NLS.bind(UIText.GitCompareFileRevisionEditorInput_CompareTooltip, titleObject); } /* (non-Javadoc) * @see org.eclipse.compare.CompareEditorInput#getTitle() */ @Override public String getTitle() { Object[] titleObject = new Object[3]; titleObject[0] = getShortName(left); titleObject[1] = CompareUtils.truncatedRevision(getContentIdentifier(getLeftRevision())); titleObject[2] = CompareUtils.truncatedRevision(getContentIdentifier(getRightRevision())); return NLS.bind(UIText.GitCompareFileRevisionEditorInput_CompareTooltip, titleObject); } /* (non-Javadoc) * @see org.eclipse.compare.CompareEditorInput#getAdapter(java.lang.Class) */ @Override public Object getAdapter(Class adapter) { if (adapter == IFile.class || adapter == IResource.class) { return getResource(); } return super.getAdapter(adapter); } private String getShortName(ITypedElement element) { if (element instanceof FileRevisionTypedElement){ FileRevisionTypedElement fileRevisionElement = (FileRevisionTypedElement) element; return fileRevisionElement.getName(); } else if (element instanceof LocalResourceTypedElement){ LocalResourceTypedElement typedContent = (LocalResourceTypedElement) element; return typedContent.getResource().getName(); } return element.getName(); } private String getLongName(ITypedElement element) { if (element instanceof FileRevisionTypedElement){ FileRevisionTypedElement fileRevisionElement = (FileRevisionTypedElement) element; return fileRevisionElement.getPath(); } else if (element instanceof LocalResourceTypedElement){ LocalResourceTypedElement typedContent = (LocalResourceTypedElement) element; return typedContent.getResource().getFullPath().toString(); } return element.getName(); } private String getContentIdentifier(ITypedElement element){ if (element instanceof FileRevisionTypedElement){ FileRevisionTypedElement fileRevisionElement = (FileRevisionTypedElement) element; Object fileObject = fileRevisionElement.getFileRevision(); if (fileObject instanceof LocalFileRevision){ try { IStorage storage = ((LocalFileRevision) fileObject).getStorage(new NullProgressMonitor()); if (AdapterUtils.adapt(storage, IFileState.class) != null) { //local revision return UIText.GitCompareFileRevisionEditorInput_LocalRevision; } else if (AdapterUtils.adapt(storage, IFile.class) != null) { //current revision return UIText.GitCompareFileRevisionEditorInput_CurrentRevision; } } catch (CoreException e) { Activator .logError( UIText.GitCompareFileRevisionEditorInput_contentIdentifier, e); } } else { return fileRevisionElement.getContentIdentifier(); } } return UIText.GitCompareFileRevisionEditorInput_CurrentTitle; } @Override protected void fireInputChange() { // have the diff node notify its listeners of a change ((NotifiableDiffNode) getCompareResult()).fireChange(); } @Override protected ICompareInput prepareCompareInput(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { ICompareInput input = createCompareInput(); getCompareConfiguration().setLeftEditable(isLeftEditable(input)); getCompareConfiguration().setRightEditable(isRightEditable(input)); ensureContentsCached(getLeftRevision(), getRightRevision(), getAncestorRevision(), monitor); initLabels(input); setTitle(NLS.bind(UIText.GitCompareFileRevisionEditorInput_CompareInputTitle, new String[] { input.getName() })); // The compare editor (Structure Compare) will show the diff filenames // with their project relative path. So, no need to also show directory entries. DiffNode flatDiffNode = new NotifiableDiffNode(null, ancestor != null ? Differencer.CONFLICTING : Differencer.CHANGE, ancestor, left, right); flatDiffView(flatDiffNode, (DiffNode) input); return flatDiffNode; } @Override protected Saveable createSaveable() { // copied from // org.eclipse.team.ui.synchronize.SaveableCompareEditorInput.createSaveable() Object compareResult = getCompareResult(); Assert.isNotNull(compareResult, "This method cannot be called until after prepareInput is called"); //$NON-NLS-1$ return new InternalResourceSaveableComparison( (ICompareInput) compareResult, this); } private void flatDiffView(DiffNode rootNode, DiffNode currentNode) { if(currentNode != null) { IDiffElement[] dElems = currentNode.getChildren(); if(dElems != null) { for(IDiffElement dElem : dElems) { DiffNode dNode = (DiffNode) dElem; if(dNode.getChildren() != null && dNode.getChildren().length > 0) { flatDiffView(rootNode, dNode); } else { rootNode.add(dNode); } } } } } @Override public void registerContextMenu(MenuManager menu, final ISelectionProvider selectionProvider) { super.registerContextMenu(menu, selectionProvider); registerOpenWorkspaceVersion(menu, selectionProvider); } private void registerOpenWorkspaceVersion(MenuManager menu, final ISelectionProvider selectionProvider) { FileRevisionTypedElement leftRevision = getLeftRevision(); if (leftRevision != null) { IFileRevision fileRevision = leftRevision.getFileRevision(); if (fileRevision instanceof OpenWorkspaceVersionEnabled) { OpenWorkspaceVersionEnabled workspaceVersion = (OpenWorkspaceVersionEnabled) fileRevision; final File workspaceFile = new File(workspaceVersion .getRepository().getWorkTree(), workspaceVersion.getGitPath()); if (workspaceFile.exists()) menu.addMenuListener(new IMenuListener() { @Override public void menuAboutToShow(IMenuManager manager) { Action action = new OpenWorkspaceVersionAction( UIText.CommitFileDiffViewer_OpenWorkingTreeVersionInEditorMenuLabel, selectionProvider, workspaceFile); manager.insertAfter("file", action); //$NON-NLS-1$ } }); } } } private static class OpenWorkspaceVersionAction extends Action { private final ISelectionProvider selectionProvider; private final File workspaceFile; private OpenWorkspaceVersionAction(String text, ISelectionProvider selectionProvider, File workspaceFile) { super(text); this.selectionProvider = selectionProvider; this.workspaceFile = workspaceFile; } @Override public void run() { int selectedLine = 0; if (selectionProvider.getSelection() instanceof ITextSelection) { ITextSelection selection = (ITextSelection) selectionProvider .getSelection(); selectedLine = selection.getStartLine(); } IWorkbenchWindow window = PlatformUI.getWorkbench() .getActiveWorkbenchWindow(); IWorkbenchPage page = window.getActivePage(); IEditorPart editor = EgitUiEditorUtils.openEditor(workspaceFile, page); EgitUiEditorUtils.revealLine(editor, selectedLine); } } /** * ITypedElement without content. May be used to indicate that a file is not * available. */ public static class EmptyTypedElement implements ITypedElement{ private String name; /** * @param name the name used for display */ public EmptyTypedElement(String name) { this.name = name; } @Override public Image getImage() { return null; } @Override public String getName() { return name; } @Override public String getType() { return ITypedElement.UNKNOWN_TYPE; } } // copy of // org.eclipse.team.ui.synchronize.SaveableCompareEditorInput.InternalResourceSaveableComparison // we need to copy this private class to prevent NPE in // org.eclipse.team.internal.ui.synchronize.LocalResourceSaveableComparison.propertyChange(PropertyChangeEvent) // when moving and saving partial changes when comparing version from git // index with HEAD private class InternalResourceSaveableComparison extends LocalResourceSaveableComparison implements ISharedDocumentAdapterListener { private LocalResourceTypedElement lrte; private boolean connected = false; public InternalResourceSaveableComparison(ICompareInput input, CompareEditorInput editorInput) { super(input, editorInput, left); ITypedElement element = left; if (element instanceof LocalResourceTypedElement) { lrte = (LocalResourceTypedElement) element; if (lrte.isConnected()) { registerSaveable(true); } else { lrte.setSharedDocumentListener(this); } } } @Override protected void fireInputChange() { GitCompareFileRevisionEditorInput.this.fireInputChange(); } @Override public void dispose() { super.dispose(); if (lrte != null) lrte.setSharedDocumentListener(null); } @Override public void handleDocumentConnected() { if (connected) return; connected = true; registerSaveable(false); if (lrte != null) lrte.setSharedDocumentListener(null); } private void registerSaveable(boolean init) { ICompareContainer container = getContainer(); IWorkbenchPart part = container.getWorkbenchPart(); if (part != null) { ISaveablesLifecycleListener lifecycleListener = getSaveablesLifecycleListener(part); // Remove this saveable from the lifecycle listener if (!init) lifecycleListener .handleLifecycleEvent(new SaveablesLifecycleEvent( part, SaveablesLifecycleEvent.POST_CLOSE, new Saveable[] { this }, false)); // Now fix the hashing so it uses the connected document initializeHashing(); // Finally, add this saveable back to the listener lifecycleListener .handleLifecycleEvent(new SaveablesLifecycleEvent(part, SaveablesLifecycleEvent.POST_OPEN, new Saveable[] { this }, false)); } } private ISaveablesLifecycleListener getSaveablesLifecycleListener( IWorkbenchPart part) { ISaveablesLifecycleListener listener = AdapterUtils.adapt(part, ISaveablesLifecycleListener.class); if (listener == null) listener = CommonUtils.getService(part.getSite(), ISaveablesLifecycleListener.class); return listener; } @Override public void handleDocumentDeleted() { // Ignore } @Override public void handleDocumentDisconnected() { // Ignore } @Override public void handleDocumentFlushed() { // Ignore } @Override public void handleDocumentSaved() { // Ignore } @Override public void doSave(IProgressMonitor monitor) throws CoreException { // SaveableComparison unconditionally resets the dirty flag to // false, but LocalResourceSaveableComparison's performSave may not // actually save: if the file has been changed outside the compare // editor, it displays a dialog that the user may cancel. if (isDirty()) { performSave(monitor); // LocalResourecSaveableComparison does already reset the dirty // flag if it did save. } } } }