blob: 276bbce5fb92eab101d1cf597bee81c436247edb [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2008, Google Inc.
* Copyright (C) 2014, Obeo
*
* 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.egit.core;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Arrays;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.team.IMoveDeleteHook;
import org.eclipse.core.resources.team.IResourceTree;
import org.eclipse.core.runtime.Assert;
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.egit.core.internal.CoreText;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCache;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffData;
import org.eclipse.egit.core.project.GitProjectData;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.LockFailedException;
import org.eclipse.team.core.RepositoryProvider;
import org.eclipse.team.core.TeamException;
class GitMoveDeleteHook implements IMoveDeleteHook {
private static final boolean I_AM_DONE = true;
private static final boolean FINISH_FOR_ME = false;
private final GitProjectData data;
GitMoveDeleteHook(final GitProjectData d) {
Assert.isNotNull(d);
data = d;
}
@Override
public boolean deleteFile(final IResourceTree tree, final IFile file,
final int updateFlags, final IProgressMonitor monitor) {
if (!org.eclipse.egit.core.Activator.autoStageDeletion()) {
return false;
}
// Linked resources are not files, hence not tracked by git
if (file.isLinked())
return false;
final boolean force = (updateFlags & IResource.FORCE) == IResource.FORCE;
if (!force && !tree.isSynchronized(file, IResource.DEPTH_ZERO))
return false;
final RepositoryMapping map = RepositoryMapping.getMapping(file);
if (map == null)
return false;
String repoRelativePath = map.getRepoRelativePath(file);
IndexDiffCache indexDiffCache = IndexDiffCache.INSTANCE;
IndexDiffCacheEntry indexDiffCacheEntry = indexDiffCache
.getIndexDiffCacheEntry(map.getRepository());
if (indexDiffCacheEntry == null) {
return false;
}
IndexDiffData indexDiff = indexDiffCacheEntry.getIndexDiff();
if (indexDiff != null) {
if (indexDiff.getUntracked().contains(repoRelativePath))
return false;
if (indexDiff.getIgnoredNotInIndex().contains(repoRelativePath))
return false;
}
if (!file.exists())
return false;
if (file.isDerived())
return false;
DirCache dirc = null;
try {
dirc = map.getRepository().lockDirCache();
final int first = dirc.findEntry(repoRelativePath);
if (first < 0) {
dirc.unlock();
return false;
}
final DirCacheBuilder edit = dirc.builder();
if (first > 0)
edit.keep(0, first);
final int next = dirc.nextEntry(first);
if (next < dirc.getEntryCount())
edit.keep(next, dirc.getEntryCount() - next);
if (!edit.commit())
tree.failed(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
0, CoreText.MoveDeleteHook_operationError, null));
tree.standardDeleteFile(file, updateFlags, monitor);
} catch (LockFailedException e) {
// FIXME The index is currently locked. This notably happens during
// rebase operations. auto-staging deletions should be queued... and
// the queued job will have to double-check whether the file has
// truly been deleted or if it was only deleted to be replaced by
// another version.
// This hook only exists to automatically add changes to the index.
// If the index is currently locked, do not accept the
// responsibility of deleting the file, return false to tell the
// workspace it can continue with the standard deletion. The user
// will have to stage the deletion later on _if_ this was truly
// needed, which won't happen for calls triggered by merge
// operations from the merge strategies.
Activator.logWarning(MessageFormat.format(
CoreText.MoveDeleteHook_cannotAutoStageDeletion,
file.getLocation()), null);
return FINISH_FOR_ME;
} catch (IOException e) {
tree.failed(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0,
CoreText.MoveDeleteHook_operationError, e));
} finally {
if (dirc != null)
dirc.unlock();
}
return true;
}
@Override
public boolean deleteFolder(final IResourceTree tree, final IFolder folder,
final int updateFlags, final IProgressMonitor monitor) {
// Deleting a GIT repository which is in use is a pretty bad idea. To
// delete disconnect the team provider first.
//
if (data.isProtected(folder)) {
return cannotModifyRepository(tree);
} else {
return FINISH_FOR_ME;
}
}
@Override
public boolean deleteProject(final IResourceTree tree,
final IProject project, final int updateFlags,
final IProgressMonitor monitor) {
// TODO: Note that eclipse thinks folders are real, while
// Git does not care.
return FINISH_FOR_ME;
}
@Override
public boolean moveFile(final IResourceTree tree, final IFile srcf,
final IFile dstf, final int updateFlags,
final IProgressMonitor monitor) {
final boolean force = (updateFlags & IResource.FORCE) == IResource.FORCE;
if (!force && !tree.isSynchronized(srcf, IResource.DEPTH_ZERO))
return false;
final RepositoryMapping srcm = RepositoryMapping.getMapping(srcf);
if (srcm == null)
return false;
final RepositoryMapping dstm = RepositoryMapping.getMapping(dstf);
DirCache sCache = null;
try {
sCache = srcm.getRepository().lockDirCache();
final String sPath = srcm.getRepoRelativePath(srcf);
final DirCacheEntry sEnt = sCache.getEntry(sPath);
if (sEnt == null)
return FINISH_FOR_ME;
if (!sEnt.isMerged()) {
tree.failed(new Status(IStatus.WARNING, Activator.PLUGIN_ID,
CoreText.MoveDeleteHook_unmergedFileError));
return I_AM_DONE;
}
if (org.eclipse.egit.core.Activator.autoStageMoves()) {
final DirCacheEditor sEdit = sCache.editor();
sEdit.add(new DirCacheEditor.DeletePath(sEnt));
if (dstm != null
&& dstm.getRepository() == srcm.getRepository()) {
final String dPath = srcm.getRepoRelativePath(dstf);
sEdit.add(new DirCacheEditor.PathEdit(dPath) {
@Override
public void apply(final DirCacheEntry dEnt) {
dEnt.copyMetaData(sEnt);
}
});
}
if (!sEdit.commit()) {
tree.failed(new Status(IStatus.ERROR,
Activator.PLUGIN_ID, 0,
CoreText.MoveDeleteHook_operationError, null));
}
}
tree.standardMoveFile(srcf, dstf, updateFlags, monitor);
} catch (IOException e) {
tree.failed(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0,
CoreText.MoveDeleteHook_operationError, e));
} finally {
if (sCache != null)
sCache.unlock();
}
return I_AM_DONE;
}
@Override
public boolean moveFolder(final IResourceTree tree, final IFolder srcf,
final IFolder dstf, final int updateFlags,
final IProgressMonitor monitor) {
final boolean force = (updateFlags & IResource.FORCE) == IResource.FORCE;
if (!force && !tree.isSynchronized(srcf, IResource.DEPTH_ZERO))
return false;
final RepositoryMapping srcm = RepositoryMapping.getMapping(srcf);
if (srcm == null)
return false;
final RepositoryMapping dstm = RepositoryMapping.getMapping(dstf);
try {
final String sPath = srcm.getRepoRelativePath(srcf);
if (dstm != null && dstm.getRepository() == srcm.getRepository()) {
MoveResult result = null;
if (org.eclipse.egit.core.Activator.autoStageMoves()) {
final String dPath = srcm.getRepoRelativePath(dstf) + "/"; //$NON-NLS-1$
result = moveIndexContent(dPath, srcm, sPath);
} else {
result = checkUnmergedPaths(srcm, sPath);
}
switch (result) {
case SUCCESS:
break;
case FAILED:
tree.failed(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
0, CoreText.MoveDeleteHook_operationError, null));
return I_AM_DONE;
case UNTRACKED:
// we are not responsible for moving untracked files
return FINISH_FOR_ME;
case UNMERGED:
tree.failed(new Status(IStatus.WARNING, Activator.PLUGIN_ID,
CoreText.MoveDeleteHook_unmergedFileInFolderError));
return I_AM_DONE;
}
}
tree.standardMoveFolder(srcf, dstf, updateFlags, monitor);
} catch (IOException e) {
tree.failed(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0,
CoreText.MoveDeleteHook_operationError, e));
}
return true;
}
private void mapProject(final IProject source,
final IProjectDescription description,
final IProgressMonitor monitor, IPath gitDir) throws CoreException,
TeamException {
IProject destination = source.getWorkspace().getRoot()
.getProject(description.getName());
RepositoryMapping repositoryMapping = RepositoryMapping.create(destination, gitDir.toFile());
if (repositoryMapping != null) {
GitProjectData projectData = new GitProjectData(destination);
projectData.setRepositoryMappings(Arrays.asList(repositoryMapping));
projectData.store();
GitProjectData.add(destination, projectData);
RepositoryProvider.map(destination, GitProvider.class.getName());
destination.refreshLocal(IResource.DEPTH_INFINITE, monitor);
}
}
private boolean unmapProject(final IResourceTree tree, final IProject source) {
// The Repository mapping does not support moving
// projects, so just disconnect/reconnect for now
try {
RepositoryProvider.unmap(source);
} catch (TeamException e) {
tree.failed(new Status(IStatus.ERROR, Activator
.PLUGIN_ID, 0,
CoreText.MoveDeleteHook_operationError, e));
return true; // Do not let Eclipse complete the operation
}
return false;
}
@Override
public boolean moveProject(final IResourceTree tree, final IProject source,
final IProjectDescription description, final int updateFlags,
final IProgressMonitor monitor) {
final RepositoryMapping srcm = RepositoryMapping.getMapping(source);
if (srcm == null)
return false;
IPath newLocation = null;
if (description.getLocationURI() != null)
newLocation = URIUtil.toPath(description.getLocationURI());
else
newLocation = source.getWorkspace().getRoot().getLocation()
.append(description.getName());
IPath sourceLocation = source.getLocation();
// Prevent a serious error.
if (sourceLocation.isPrefixOf(newLocation)
&& sourceLocation.segmentCount() != newLocation.segmentCount()
&& !"true".equals(System.getProperty("egit.assume_307140_fixed"))) { //$NON-NLS-1$//$NON-NLS-2$
// Graceful handling of bug, i.e. refuse to destroy your code
tree.failed(new Status(
IStatus.ERROR,
Activator.PLUGIN_ID,
0,
"Cannot move project. See https://bugs.eclipse.org/bugs/show_bug.cgi?id=307140 (not resolved in 3.7)", //$NON-NLS-1$
null));
return true;
}
File newLocationFile = newLocation.toFile();
// check if new location is below the same repository
Path workTree = new Path(srcm.getRepository().getWorkTree().getAbsolutePath());
int matchingFirstSegments = workTree.matchingFirstSegments(newLocation);
if (matchingFirstSegments == workTree.segmentCount()) {
return moveProjectHelperMoveOnlyProject(tree, source, description, updateFlags,
monitor, srcm, newLocationFile);
} else {
int dstAboveSrcRepo = newLocation.matchingFirstSegments(srcm.getGitDirAbsolutePath());
int srcAboveSrcRepo = sourceLocation.matchingFirstSegments(srcm.getGitDirAbsolutePath());
if (dstAboveSrcRepo > 0 && srcAboveSrcRepo > 0) {
return moveProjectHelperMoveRepo(tree, source, description, updateFlags, monitor,
srcm, newLocation, sourceLocation);
} else {
return FINISH_FOR_ME;
}
}
}
private boolean moveProjectHelperMoveOnlyProject(final IResourceTree tree,
final IProject source, final IProjectDescription description,
final int updateFlags, final IProgressMonitor monitor,
final RepositoryMapping srcm, File newLocationFile) {
final String sPath = srcm.getRepoRelativePath(source);
final String absoluteWorkTreePath = srcm.getRepository().getWorkTree().getAbsolutePath();
final String newLocationAbsolutePath = newLocationFile.getAbsolutePath();
final String dPath;
if (newLocationAbsolutePath.equals(absoluteWorkTreePath))
dPath = ""; //$NON-NLS-1$
else
dPath = new Path(
newLocationAbsolutePath.substring(absoluteWorkTreePath
.length() + 1) + "/").toPortableString(); //$NON-NLS-1$
try {
IPath gitDir = srcm.getGitDirAbsolutePath();
if (unmapProject(tree, source))
return true;
monitor.worked(100);
MoveResult result = null;
if (org.eclipse.egit.core.Activator.autoStageMoves()) {
result = moveIndexContent(dPath, srcm, sPath);
} else {
result = checkUnmergedPaths(srcm, sPath);
}
switch (result) {
case SUCCESS:
break;
case FAILED:
tree.failed(new Status(IStatus.ERROR, Activator
.PLUGIN_ID, 0,
CoreText.MoveDeleteHook_operationError, null));
break;
case UNTRACKED:
// we are not responsible for moving untracked files
return FINISH_FOR_ME;
case UNMERGED:
tree.failed(new Status(IStatus.WARNING, Activator.PLUGIN_ID,
CoreText.MoveDeleteHook_unmergedFileInFolderError));
return I_AM_DONE;
}
tree.standardMoveProject(source, description, updateFlags,
monitor);
// Reconnect
mapProject(
source.getWorkspace().getRoot()
.getProject(description.getName()),
description, monitor, gitDir);
} catch (IOException | CoreException e) {
tree.failed(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
0, CoreText.MoveDeleteHook_operationError, e));
}
return true;
}
private boolean moveProjectHelperMoveRepo(final IResourceTree tree, final IProject source,
final IProjectDescription description, final int updateFlags,
final IProgressMonitor monitor, final RepositoryMapping srcm,
IPath newLocation, IPath sourceLocation) {
// Moving repo, we need to unplug the previous location and
// Re-plug it again with the new location.
IPath gitDir = srcm.getGitDirAbsolutePath();
if (unmapProject(tree, source)) {
return true; // Error information in tree
}
monitor.worked(100);
if (gitDir == null) {
return true; // mapping on deleted container with relative path
}
IPath relativeGitDir = gitDir.makeRelativeTo(sourceLocation);
tree.standardMoveProject(source, description, updateFlags,
monitor);
IPath newGitDir = newLocation.append(relativeGitDir);
// Reconnect
try {
mapProject(source, description, monitor, newGitDir);
} catch (CoreException e) {
tree.failed(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
0, CoreText.MoveDeleteHook_operationError, e));
}
return true; // We're done with the move
}
enum MoveResult { SUCCESS, FAILED, UNTRACKED, UNMERGED }
private MoveResult moveIndexContent(String dPath,
final RepositoryMapping srcm, final String sPath) throws IOException {
final DirCache sCache = srcm.getRepository().lockDirCache();
try {
final DirCacheEntry[] sEnt = sCache.getEntriesWithin(sPath);
if (sEnt.length == 0) {
sCache.unlock();
return MoveResult.UNTRACKED;
}
final DirCacheEditor sEdit = sCache.editor();
sEdit.add(new DirCacheEditor.DeleteTree(sPath));
final int sPathLen = sPath.length() == 0 ? sPath.length() : sPath
.length() + 1;
for (final DirCacheEntry se : sEnt) {
if (!se.isMerged())
return MoveResult.UNMERGED;
final String p = se.getPathString().substring(sPathLen);
sEdit.add(new DirCacheEditor.PathEdit(dPath + p) {
@Override
public void apply(final DirCacheEntry dEnt) {
dEnt.copyMetaData(se);
}
});
}
if (sEdit.commit())
return MoveResult.SUCCESS;
else
return MoveResult.FAILED;
} finally {
if (sCache != null)
sCache.unlock();
}
}
private MoveResult checkUnmergedPaths(final RepositoryMapping srcm,
final String sPath) throws IOException {
final DirCache sCache = srcm.getRepository().lockDirCache();
try {
final DirCacheEntry[] sEnt = sCache.getEntriesWithin(sPath);
if (sEnt.length == 0) {
sCache.unlock();
return MoveResult.UNTRACKED;
}
for (final DirCacheEntry se : sEnt) {
if (!se.isMerged()) {
return MoveResult.UNMERGED;
}
}
return MoveResult.SUCCESS;
} finally {
if (sCache != null) {
sCache.unlock();
}
}
}
private boolean cannotModifyRepository(final IResourceTree tree) {
tree.failed(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0,
CoreText.MoveDeleteHook_cannotModifyFolder, null));
return I_AM_DONE;
}
}