blob: 53868e93dcd946d573dac7694f89decbf8dc523a [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2010, 2021 Mathias Kinzler <mathias.kinzler@sap.com> and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.emf.compare.egit.ui.internal.merge;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareEditorInput;
import org.eclipse.compare.CompareViewerPane;
import org.eclipse.compare.IResourceProvider;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.contentmergeviewer.ContentMergeViewer;
import org.eclipse.compare.structuremergeviewer.DiffContainer;
import org.eclipse.compare.structuremergeviewer.DiffNode;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.compare.structuremergeviewer.IDiffContainer;
import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.CoreException;
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.Job;
import org.eclipse.egit.core.info.GitInfo;
import org.eclipse.egit.core.internal.efs.HiddenResources;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCache;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
import org.eclipse.egit.core.internal.util.ResourceUtil;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.merge.CompareEditorInputViewerAction;
import org.eclipse.egit.ui.internal.merge.CompareWithEachOtherAction;
import org.eclipse.egit.ui.internal.merge.GitDiffTreeViewer;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.commands.ActionHandler;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.team.internal.ui.synchronize.EditableSharedDocumentAdapter.ISharedDocumentAdapterListener;
import org.eclipse.team.internal.ui.synchronize.LocalResourceTypedElement;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.IHandlerActivation;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.ide.IDE.SharedImages;
import org.eclipse.ui.part.IShowInSource;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.services.IServiceLocator;
/**
* A Git-specific {@link CompareEditorInput}.
* <p>
* This class is a copy from {@link org.eclipse.egit.ui.internal.merge.AbstractGitCompareEditorInput}, except
* for the following:
* <ul>
* <li>{@link #buildInput(IProgressMonitor)} returns an Object instead of a DiffContainer</li>
* <li>{@link #inputBuilt(Object)} takes an Object instead of a DiffContainer</li>
* </p>
*/
@SuppressWarnings("restriction")
public abstract class AbstractGitCompareEditorInput extends CompareEditorInput {
private static final Comparator<String> CMP = (left, right) -> {
String l = left.startsWith("/") ? left.substring(1) : left; //$NON-NLS-1$
String r = right.startsWith("/") ? right.substring(1) : right; //$NON-NLS-1$
return l.replace('/', '\001').compareToIgnoreCase(r.replace('/', '\001'));
};
private static final Image FOLDER_IMAGE = PlatformUI.getWorkbench().getSharedImages()
.getImage(ISharedImages.IMG_OBJ_FOLDER);
private static final Image PROJECT_IMAGE = PlatformUI.getWorkbench().getSharedImages()
.getImage(SharedImages.IMG_OBJ_PROJECT);
private final IPath[] locations;
private List<IFile> toDelete;
private Map<String, IHandlerActivation> activations = new HashMap<>();
private Repository repository;
private Collection<String> gitPaths;
private boolean initialized;
/**
* Creates a new {@link AbstractGitCompareEditorInput}. Note that if the repository is null and no
* locations are given, initPaths will throw an exception.
*
* @param repository
* to operate in; if {@code null} will be determined from the {@code locations}
* @param locations
* absolute file system locations of the files/folders to restrict the operation to
*/
protected AbstractGitCompareEditorInput(Repository repository, IPath... locations) {
super(new CompareConfiguration());
this.repository = repository;
this.locations = locations;
}
@Override
public <T> T getAdapter(Class<T> adapter) {
if ((adapter == IFile.class || adapter == IResource.class) && !isMultiFile()) {
ITypedElement element = getElement();
IResource resource = getResource(element);
if (resource != null && adapter.isInstance(resource) && resource.exists()) {
return adapter.cast(resource);
}
} else if (adapter == IShowInSource.class && isMultiFile()) {
DiffNode node = getNode();
if (node instanceof FolderNode) {
FolderNode folder = (FolderNode)node;
IContainer container = folder.getContainer();
if (container != null) {
return adapter.cast(new ShowInContext(this, new StructuredSelection(container)));
}
IPath path = folder.getPath();
if (path != null) {
if (path.isAbsolute()) {
return adapter.cast(
getShowInSource(new ShowInContext(this, new StructuredSelection(path))));
} else {
GitInfo info = getGitInfo(path);
if (info != null) {
return adapter.cast(
getShowInSource(new ShowInContext(this, new StructuredSelection(info))));
}
}
}
} else if (node != null) {
ITypedElement element = node.getLeft();
IResource resource = getResource(element);
if (resource instanceof IFile && resource.exists()) {
return adapter.cast(
getShowInSource(new ShowInContext(this, new StructuredSelection(resource))));
}
GitInfo info = Adapters.adapt(element, GitInfo.class);
if (info != null && info.getRepository() != null) {
IPath path = Path.fromPortableString(info.getGitPath());
if (!repository.isBare()) {
File f = new File(repository.getWorkTree(), path.toOSString());
if (f.exists()) {
return adapter.cast(getShowInSource(new ShowInContext(this,
new StructuredSelection(Path.fromOSString(f.getAbsolutePath())))));
}
}
// The repository is bare, or the path does not exist in the
// working tree. The history page can deal with these paths,
// so at least "Show in->History" should work.
return adapter
.cast(getShowInSource(new ShowInContext(this, new StructuredSelection(info))));
}
}
return adapter.cast(getShowInSource(null));
} else if (adapter == Repository.class) {
return adapter.cast(adapter);
}
return super.getAdapter(adapter);
}
@Override
public Object getSelectedEdition() {
if (isUIThread()) {
return super.getSelectedEdition();
} else {
Object[] item = {null };
PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
item[0] = super.getSelectedEdition();
});
return item[0];
}
}
private boolean isMultiFile() {
Object input = getCompareResult();
if (input instanceof IDiffContainer) {
IDiffElement[] children = ((IDiffContainer)input).getChildren();
if (children.length != 1) {
return true;
}
if (children[0] instanceof IDiffContainer && ((IDiffContainer)children[0]).hasChildren()) {
return true;
}
}
return false;
}
private ITypedElement getElement() {
Object selectedEdition = getSelectedEdition();
if (selectedEdition instanceof DiffNode) {
DiffNode diffNode = (DiffNode)selectedEdition;
return diffNode.getLeft();
}
return null;
}
private DiffNode getNode() {
Object selectedEdition = getSelectedEdition();
if (selectedEdition instanceof DiffNode) {
return (DiffNode)selectedEdition;
}
return null;
}
private IResource getResource(ITypedElement element) {
if (element != null) {
IResource resource = null;
if (element instanceof HiddenResourceTypedElement) {
resource = ((HiddenResourceTypedElement)element).getRealFile();
} else if (element instanceof IResourceProvider) {
resource = ((IResourceProvider)element).getResource();
}
return resource;
}
return null;
}
/**
* Hook method for subclasses to provide a {@link GitInfo} accessor for an item at the given path. The
* default implementation always returns {@code null}.
*
* @param path
* to get a {@link GitInfo} for
* @return the {@link GitInfo} accessor, or {@code null} if none can be determined
*/
protected GitInfo getGitInfo(IPath path) {
return null;
}
private IShowInSource getShowInSource(ShowInContext context) {
return () -> context;
}
@Override
protected void contentsCreated() {
super.contentsCreated();
// select the first conflict
getNavigator().selectChange(true);
}
@Override
public Viewer createDiffViewer(Composite parent) {
GitDiffTreeViewer viewer = new GitDiffTreeViewer(parent, getContainer(), getCompareConfiguration());
viewer.setComparator(new ViewerComparator(CMP) {
@Override
public int category(Object element) {
if (element instanceof FolderNode) {
return 0;
} else {
return 1;
}
}
});
IAction compareAction = new CompareWithEachOtherAction(viewer,
UIText.GitMergeEditorInput_CompareWithEachOtherMenuLabel, UIIcons.ELCL16_COMPARE_VIEW);
viewer.setActions(Collections.singleton(compareAction));
registerAction(compareAction, CompareWithEachOtherAction.COMMAND_ID);
return viewer;
}
@Override
public Viewer findContentViewer(Viewer oldViewer, ICompareInput input, Composite parent) {
Viewer newViewer = super.findContentViewer(oldViewer, input, parent);
ToolBarManager manager = CompareViewerPane.getToolBarManager(parent);
if (manager != null) {
initActions(manager, newViewer, input);
}
return newViewer;
}
/**
* Something that can get or create an action to be added to the toolbar of a content merge viewer.
*/
@FunctionalInterface
protected interface ActionSupplier {
/**
* Obtains an action.
*
* @param create
* whether to create an action if none was created yet
* @return the action, or {@code null} if none created
*/
public CompareEditorInputViewerAction get(boolean create);
}
/**
* Hook for subclasses to provide toolbar actions.
*
* @param manager
* to add the action to
* @param newViewer
* for the action
* @param input
* for the viewer
*/
protected void initActions(ToolBarManager manager, Viewer newViewer, ICompareInput input) {
// Nothing.
}
/**
* Manages the action with the given id. If none exists in the toolbar but it is applicable, create one
* through the supplier. Otherwise set the enablement according to whether it is applicable. The action is
* registered with the {@link IHandlerService} as a command.
*
* @param manager
* to add the action to
* @param viewer
* for the action
* @param isApplicable
* {@code true} if the action applies
* @param id
* of the action; also used as a command id
* @param supplier
* to create or get the action
*/
protected void setAction(ToolBarManager manager, Viewer viewer, boolean isApplicable, String id,
ActionSupplier supplier) {
IContributionItem item = manager.find(id);
if (item != null) {
if (item instanceof ActionContributionItem) {
IAction action = ((ActionContributionItem)item).getAction();
if (action instanceof CompareEditorInputViewerAction) {
((CompareEditorInputViewerAction)action)
.setViewer(isApplicable ? (ContentMergeViewer)viewer : null);
action.setEnabled(isApplicable);
if (item.isVisible() != isApplicable) {
item.setVisible(isApplicable);
manager.update(true);
}
}
}
} else if (isApplicable) {
CompareEditorInputViewerAction action = supplier.get(true);
action.setViewer((ContentMergeViewer)viewer);
action.setEnabled(true);
manager.insert(0, new ActionContributionItem(action));
manager.update(true);
registerAction(action, id);
} else {
// Neither present nor applicable: disable it if it exists
CompareEditorInputViewerAction action = supplier.get(false);
if (action != null) {
action.setEnabled(false);
}
}
}
private void registerAction(IAction action, String commandId) {
if (activations.containsKey(commandId)) {
return;
}
action.setActionDefinitionId(commandId);
IServiceLocator locator = getContainer().getServiceLocator();
if (locator != null) {
IHandlerService handlers = locator.getService(IHandlerService.class);
if (handlers != null) {
activations.put(commandId, handlers.activateHandler(commandId, new ActionHandler(action)));
}
}
}
/**
* Hook for subclasses to dispose actions, if needed. Invoked during {@link #handleDispose()}.
*/
protected void disposeActions() {
// Nothing
}
@Override
protected void handleDispose() {
super.handleDispose();
// We do NOT dispose the images, as these are shared.
activations.values().forEach(a -> a.getHandlerService().deactivateHandler(a));
activations.clear();
disposeActions();
// We need to remove the temporary resources. A CompareEditorInput is
// supposed to be the very last thing that is disposed in a compare
// viewer, but this is not always true. If content merge viewers add
// additional widgets, for instance for the Java structure comparison,
// we're suddenly no longer the last item to be disposed. The various
// viewers (left, right, structure, and so on) are all disposed when
// their widgets are disposed. Widget disposal happens recursively
// top-down on the UI thread, so an asyncExec should be safe here to
// ensure that we remove the files only once everything else has been
// disposed of. If we delete temporary resources before all viewers had
// disconnected the Document, some might not disconnect because
// SharedDocumentAdapter.getDocumentKey() returns null if the file has
// been deleted. If this happens the framework will find that still
// connected document the next time this resource is opened and show
// that instead of the true resource contents. This is wrong and is very
// annoying if this cached document is dirty: one can open only this
// dirty version from then on, until the next restart of Eclipse.
PlatformUI.getWorkbench().getDisplay().asyncExec(this::cleanUp);
}
private void cleanUp() {
if (toDelete == null || toDelete.isEmpty()) {
return;
}
List<IFile> toClean = toDelete;
toDelete = null;
// Don't clean up if the workbench is shutting down; we would exit with
// unsaved workspace changes. Instead, EGit core cleans the project on
// start.
Job job = new Job(UIText.GitMergeEditorInput_ResourceCleanupJobName) {
@Override
public boolean shouldSchedule() {
return super.shouldSchedule() && !PlatformUI.getWorkbench().isClosing();
}
@Override
public boolean shouldRun() {
return super.shouldRun() && !PlatformUI.getWorkbench().isClosing();
}
@Override
protected IStatus run(IProgressMonitor monitor) {
IWorkspaceRunnable remove = m -> {
SubMonitor progress = SubMonitor.convert(m, toClean.size());
for (IFile tmp : toClean) {
if (PlatformUI.getWorkbench().isClosing()) {
return;
}
try {
tmp.delete(true, progress.newChild(1));
} catch (CoreException e) {
// Ignore
}
}
};
try {
ResourcesPlugin.getWorkspace().run(remove, null, IWorkspace.AVOID_UPDATE, monitor);
} catch (CoreException e) {
return e.getStatus();
}
return Status.OK_STATUS;
}
};
job.setSystem(true);
job.setUser(false);
job.schedule();
}
private static boolean isUIThread() {
return Display.getCurrent() != null;
}
/**
* Creates a {@link HiddenResourceTypedElement}.
*
* @param uri
* to link to
* @param repo
* {@link Repository} the item is in
* @param gitPath
* within the repository
* @param name
* for the hidden resource
* @param file
* original file, if any
* @param encoding
* to use
* @return a {@link HiddenResourceTypedElement}
* @throws IOException
* on errors
*/
protected LocalResourceTypedElement createWithHiddenResource(URI uri, Repository repo, String gitPath,
String name, IFile file, Charset encoding) throws IOException {
IFile tmp = createHiddenResource(uri, name, encoding);
return new HiddenResourceTypedElement(repo, gitPath, tmp, file);
}
/**
* Creates a hidden resource that will be removed when this {@link AbstractGitCompareEditorInput} is
* disposed.
*
* @param uri
* to link to
* @param name
* for the resource
* @param encoding
* to use
* @return the hidden resource
* @throws IOException
* on errors
*/
protected IFile createHiddenResource(URI uri, String name, Charset encoding) throws IOException {
try {
IFile tmp = HiddenResources.INSTANCE.createFile(uri, name, encoding, null);
if (toDelete == null) {
toDelete = new ArrayList<>();
}
toDelete.add(tmp);
return tmp;
} catch (CoreException e) {
throw new IOException(e.getMessage(), e);
}
}
/**
* Constructs diff nodes for folders connecting the file to the root.
*
* @param root
* to connect to
* @param repositoryPath
* full absolute path of the git repository working tree
* @param file
* to determine a {@link IProject} from
* @param location
* full absolute path of the file
* @return the folder node to attach a new {@link DiffNode} for the file to, already attached to root
*/
protected IDiffContainer getFileParent(IDiffContainer root, IPath repositoryPath, IFile file,
IPath location) {
int projectSegment = -1;
String projectName = null;
if (file != null) {
IProject project = file.getProject();
IPath projectLocation = project.getLocation();
if (projectLocation != null) {
IPath projectPath = project.getLocation().makeRelativeTo(repositoryPath);
projectSegment = projectPath.segmentCount() - 1;
projectName = project.getName();
}
}
IPath path = location.makeRelativeTo(repositoryPath);
int pathLength = path.segmentCount() - 1;
IDiffContainer child = root;
for (int i = 0; i < pathLength; i++) {
if (i == projectSegment) {
child = getOrCreateChild(child, projectName, true);
} else {
child = getOrCreateChild(child, path.segment(i), false);
}
}
if (child != root) {
IContainer container = file != null ? file.getParent() : null;
path = location.removeLastSegments(1);
IDiffContainer folder = child;
while (folder != root) {
if (folder instanceof FolderNode) {
if (container != null) {
((FolderNode)folder).setContainer(container);
if (container.isLinked()) {
container = null;
} else {
container = container.getParent();
if (container.getType() == IResource.ROOT) {
container = null;
}
}
} else if (path != null) {
((FolderNode)folder).setPath(path);
}
if (path != null && pathLength > 0) {
path = path.removeLastSegments(1);
pathLength--;
} else {
break;
}
}
folder = folder.getParent();
}
}
return child;
}
/**
* Constructs diff nodes for folders connecting the file to the root.
*
* @param root
* to connect to
* @param gitPath
* git path (relative to the repository root)
* @return the folder node to attach a new {@link DiffNode} for the file to, already attached to root
*/
protected IDiffContainer getFileParent(IDiffContainer root, String gitPath) {
IDiffContainer child = root;
IPath path = Path.fromPortableString(gitPath);
int pathLength = path.segmentCount() - 1;
for (int i = 0; i < pathLength; i++) {
child = getOrCreateChild(child, path.segment(i), false);
}
if (child != root) {
if (!repository.isBare()) {
path = Path.fromOSString(repository.getWorkTree().getAbsolutePath()).append(path);
}
path = path.removeLastSegments(1);
IDiffContainer folder = child;
while (folder != root) {
if (folder instanceof FolderNode) {
if (pathLength > 0) {
((FolderNode)folder).setPath(path);
path = path.removeLastSegments(1);
pathLength--;
} else {
break;
}
}
folder = folder.getParent();
}
}
return child;
}
private DiffNode getOrCreateChild(IDiffContainer parent, final String name, final boolean projectMode) {
for (IDiffElement child : parent.getChildren()) {
if (child.getName().equals(name)) {
return ((DiffNode)child);
}
}
return new FolderNode(parent, name, projectMode ? PROJECT_IMAGE : FOLDER_IMAGE);
}
private void collapse(DiffContainer top) {
IDiffElement[] children = top.getChildren();
boolean isRoot = top.getParent() == null;
if (!isRoot) {
while (children != null && children.length == 1) {
IDiffElement singleChild = children[0];
if (singleChild instanceof FolderNode) {
FolderNode node = (FolderNode)singleChild;
top.remove(singleChild);
top.getParent().add(singleChild);
node.setName(top.getName() + '/' + singleChild.getName());
((DiffContainer)top.getParent()).remove(top);
children = node.getChildren();
top = node;
} else {
// Hit a leaf.
return;
}
}
}
if (children != null && (isRoot || children.length > 1)) {
for (IDiffElement node : children) {
if (node instanceof FolderNode) {
collapse((DiffContainer)node);
}
}
}
}
@Override
public boolean canRunAsJob() {
return true;
}
@Override
protected final Object prepareInput(IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException {
monitor.beginTask(UIText.GitMergeEditorInput_CheckingResourcesTaskName, IProgressMonitor.UNKNOWN);
try {
initPaths();
if (monitor.isCanceled()) {
throw new InterruptedException();
}
Object result = buildInput(monitor);
if (result instanceof DiffContainer) {
collapse((DiffContainer)result);
}
inputBuilt(result);
return result;
} finally {
monitor.done();
}
}
/**
* Build the {@link DiffNode}s and return the root node.
*
* @param monitor
* for cancellation and progress reporting
* @return the root diff node
* @throws InvocationTargetException
* on errors
* @throws InterruptedException
* on cancellation
* @see CompareEditorInput#prepareInput(IProgressMonitor monitor)
*/
protected abstract Object buildInput(IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException;
/**
* Hook for subclasses called once the full compare result has been built.
* <p>
* This default implementation does nothing.
* </p>
*
* @param root
* that will be returned as compare result
*/
protected void inputBuilt(Object root) {
// Nothing
}
private void initPaths() throws InvocationTargetException {
if (initialized) {
return;
}
initialized = true;
if (repository == null || locations != null && locations.length > 0) {
Map<Repository, Collection<String>> pathsByRepository = ResourceUtil
.splitPathsByRepository(Arrays.asList(locations));
if (pathsByRepository.size() != 1) {
throw new InvocationTargetException(
new IllegalStateException(UIText.RepositoryAction_multiRepoSelection));
}
Entry<Repository, Collection<String>> entry = pathsByRepository.entrySet().iterator().next();
Repository repo = entry.getKey();
if (repository != null && !repo.getDirectory().equals(repository.getDirectory())) {
throw new InvocationTargetException(new IllegalStateException("Paths not in repo " //$NON-NLS-1$
+ repository.getDirectory()));
}
if (repository == null) {
repository = repo;
}
gitPaths = new ArrayList<>(entry.getValue());
}
}
/**
* Retrieves the repository.
*
* @return the {@link Repository}
*/
protected Repository getRepository() {
return repository;
}
/**
* Retrieves the git paths to filter the comparison by.
*
* @return the paths, or an empty collection if all paths in the repository shall be compared
*/
protected Collection<String> getFilterPaths() {
if (gitPaths == null) {
return Collections.emptyList();
}
return gitPaths;
}
@Override
public int hashCode() {
return Arrays.hashCode(locations);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
AbstractGitCompareEditorInput other = (AbstractGitCompareEditorInput)obj;
return Arrays.equals(locations, other.locations);
}
/**
* {@link AbstractGitCompareEditorInput} is not a {@code SaveableCompareEditorInput}. Editable
* {@link ITypedElement}s must handle saving on being flushed. Attaching a {@code LocalResourceSaver} to a
* {@link LocalResourceTypedElement} achieves that, and also refreshes as needed when hidden resources are
* used.
*/
protected static class LocalResourceSaver implements ISharedDocumentAdapterListener {
LocalResourceTypedElement element;
/**
* Creates a new {@link LocalResourceSaver} for the given {@link LocalResourceTypedElement}.
*
* @param element
* to handle saving of
*/
public LocalResourceSaver(LocalResourceTypedElement element) {
this.element = element;
}
/**
* Saves the element; invoked via {@link #handleDocumentFlushed()}.
*
* @throws CoreException
* on errors
*/
protected void save() throws CoreException {
element.saveDocument(true, null);
refreshIndexDiff();
}
private void refreshIndexDiff() {
IResource resource = element.getResource();
if (resource != null && HiddenResources.INSTANCE.isHiddenProject(resource.getProject())) {
String gitPath = null;
Repository repository = null;
URI uri = resource.getLocationURI();
if (EFS.SCHEME_FILE.equals(uri.getScheme())) {
IPath location = new Path(uri.getSchemeSpecificPart());
repository = ResourceUtil.getRepository(location);
if (repository != null) {
location = ResourceUtil.getRepositoryRelativePath(location, repository);
if (location != null) {
gitPath = location.toPortableString();
}
}
} else {
repository = HiddenResources.INSTANCE.getRepository(uri);
if (repository != null) {
gitPath = HiddenResources.INSTANCE.getGitPath(uri);
}
}
if (gitPath != null && repository != null) {
IndexDiffCacheEntry indexDiffCacheForRepository = IndexDiffCache.INSTANCE
.getIndexDiffCacheEntry(repository);
if (indexDiffCacheForRepository != null) {
indexDiffCacheForRepository.refreshFiles(Collections.singletonList(gitPath));
}
}
}
}
@Override
public void handleDocumentConnected() {
// Nothing
}
@Override
public void handleDocumentDisconnected() {
// Nothing
}
@Override
public void handleDocumentFlushed() {
try {
save();
} catch (CoreException e) {
Activator.handleStatus(e.getStatus(), true);
}
}
@Override
public void handleDocumentDeleted() {
// Nothing
}
@Override
public void handleDocumentSaved() {
// Nothing
}
}
/**
* A {@link LocalResourceTypedElement} for a hidden resource, which may correspond to a real
* {@link IFile}.
*/
protected static class HiddenResourceTypedElement extends LocalResourceTypedElement implements GitInfo {
private final IFile realFile;
private final Repository repository;
private final String gitPath;
private HiddenResourceTypedElement(Repository repository, String gitPath, IFile file,
IFile realFile) {
super(file);
this.realFile = realFile;
this.repository = repository;
this.gitPath = gitPath;
}
/**
* Retrieves the real file for this {@link HiddenResourceTypedElement}.
*
* @return the {@link IFile}, or {@code null} if none
*/
public IFile getRealFile() {
return realFile;
}
@Override
public boolean equals(Object obj) {
// local fields not considered
return super.equals(obj);
}
@Override
public int hashCode() {
// local fields not considered
return super.hashCode();
}
@Override
public Repository getRepository() {
return repository;
}
@Override
public String getGitPath() {
return gitPath;
}
@Override
public Source getSource() {
return Source.WORKING_TREE;
}
@Override
public AnyObjectId getCommitId() {
return null;
}
}
private static class FolderNode extends DiffNode {
private final Image image;
private String name;
private IContainer container;
private IPath path;
FolderNode(IDiffContainer parent, String name, Image image) {
super(parent, Differencer.NO_CHANGE);
this.name = name;
this.image = image;
}
@Override
public String getType() {
return ITypedElement.FOLDER_TYPE;
}
@Override
public String getName() {
return name;
}
void setName(String name) {
// Be careful when calling this. Changing the name of a node changes
// the hash code of this node and of all its children! Call only
// before the node's hashCode is needed.
this.name = name;
}
@Override
public Image getImage() {
return image;
}
void setContainer(IContainer container) {
this.container = container;
}
IContainer getContainer() {
return container;
}
void setPath(IPath path) {
this.path = path;
}
IPath getPath() {
return path;
}
@Override
public boolean equals(Object other) {
// Ignore my own fields. Super implementation includes getName().
return super.equals(other);
}
@Override
public int hashCode() {
// Ignore my own fields. Super implementation includes getName().
return super.hashCode();
}
}
}