/*******************************************************************************
* Copyright (C) 2010, 2013 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 v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.eclipse.egit.ui.internal.merge;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareEditorInput;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.structuremergeviewer.DiffNode;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.compare.structuremergeviewer.IDiffContainer;
import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.egit.core.internal.CompareCoreUtils;
import org.eclipse.egit.core.internal.storage.GitFileRevision;
import org.eclipse.egit.core.internal.storage.WorkingTreeFileRevision;
import org.eclipse.egit.core.internal.util.ResourceUtil;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.internal.CompareUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.revision.EditableRevision;
import org.eclipse.egit.ui.internal.revision.FileRevisionTypedElement;
import org.eclipse.egit.ui.internal.revision.GitCompareFileRevisionEditorInput.EmptyTypedElement;
import org.eclipse.egit.ui.internal.revision.LocalFileRevision;
import org.eclipse.egit.ui.internal.revision.LocationEditableRevision;
import org.eclipse.egit.ui.internal.revision.ResourceEditableRevision;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jgit.api.RebaseCommand;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
import org.eclipse.jgit.treewalk.filter.OrTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.graphics.Image;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE.SharedImages;
/**
* A Git-specific {@link CompareEditorInput}
*/
public class GitMergeEditorInput extends CompareEditorInput {
private static final String LABELPATTERN = "{0} - {1}"; //$NON-NLS-1$
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 boolean useWorkspace;
private final IPath[] locations;
/**
* @param useWorkspace
* if <code>true</code>, use the workspace content (i.e. the
* Git-merged version) as "left" content, otherwise use HEAD
* (i.e. the previous, non-merged version)
* @param locations
* as selected by the user
*/
public GitMergeEditorInput(boolean useWorkspace, IPath... locations) {
super(new CompareConfiguration());
this.useWorkspace = useWorkspace;
this.locations = locations;
CompareConfiguration config = getCompareConfiguration();
config.setLeftEditable(true);
}
@Override
public Object getAdapter(Class adapter) {
if (adapter == IFile.class || adapter == IResource.class) {
Object selectedEdition = getSelectedEdition();
if (selectedEdition instanceof DiffNode) {
DiffNode diffNode = (DiffNode) selectedEdition;
ITypedElement element = diffNode.getLeft();
if (element instanceof ResourceEditableRevision) {
ResourceEditableRevision resourceRevision = (ResourceEditableRevision) element;
return resourceRevision.getFile();
}
}
}
return super.getAdapter(adapter);
}
@Override
protected Object prepareInput(IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException {
// make sure all resources belong to the same repository
RevWalk rw = null;
try {
monitor.beginTask(
UIText.GitMergeEditorInput_CheckingResourcesTaskName,
IProgressMonitor.UNKNOWN);
Map<Repository, Collection<String>> pathsByRepository = ResourceUtil
.splitPathsByRepository(Arrays.asList(locations));
if (pathsByRepository.size() != 1) {
throw new InvocationTargetException(
new IllegalStateException(
UIText.RepositoryAction_multiRepoSelection));
}
Repository repo = pathsByRepository.keySet().iterator().next();
List<String> filterPaths = new ArrayList<String>(
pathsByRepository.get(repo));
if (monitor.isCanceled())
throw new InterruptedException();
rw = new RevWalk(repo);
// get the "right" side (MERGE_HEAD for merge, ORIG_HEAD for rebase)
final RevCommit rightCommit;
try {
String target;
if (repo.getRepositoryState().equals(RepositoryState.MERGING))
target = Constants.MERGE_HEAD;
else if (repo.getRepositoryState().equals(RepositoryState.CHERRY_PICKING))
target = Constants.CHERRY_PICK_HEAD;
else if (repo.getRepositoryState().equals(
RepositoryState.REBASING_INTERACTIVE))
target = readFile(repo.getDirectory(),
RebaseCommand.REBASE_MERGE + File.separatorChar
+ RebaseCommand.STOPPED_SHA);
else
target = Constants.ORIG_HEAD;
ObjectId mergeHead = repo.resolve(target);
if (mergeHead == null)
throw new IOException(NLS.bind(
UIText.ValidationUtils_CanNotResolveRefMessage,
target));
rightCommit = rw.parseCommit(mergeHead);
} catch (IOException e) {
throw new InvocationTargetException(e);
}
// we need the HEAD, also to determine the common
// ancestor
final RevCommit headCommit;
try {
ObjectId head = repo.resolve(Constants.HEAD);
if (head == null)
throw new IOException(NLS.bind(
UIText.ValidationUtils_CanNotResolveRefMessage,
Constants.HEAD));
headCommit = rw.parseCommit(head);
} catch (IOException e) {
throw new InvocationTargetException(e);
}
final String fullBranch;
try {
fullBranch = repo.getFullBranch();
} catch (IOException e) {
throw new InvocationTargetException(e);
}
// try to obtain the common ancestor
List<RevCommit> startPoints = new ArrayList<RevCommit>();
rw.setRevFilter(RevFilter.MERGE_BASE);
startPoints.add(rightCommit);
startPoints.add(headCommit);
RevCommit ancestorCommit;
try {
rw.markStart(startPoints);
ancestorCommit = rw.next();
} catch (Exception e) {
ancestorCommit = null;
}
if (monitor.isCanceled())
throw new InterruptedException();
// set the labels
CompareConfiguration config = getCompareConfiguration();
config.setRightLabel(NLS.bind(LABELPATTERN, rightCommit
.getShortMessage(), CompareUtils.truncatedRevision(rightCommit.name())));
if (!useWorkspace)
config.setLeftLabel(NLS.bind(LABELPATTERN, headCommit
.getShortMessage(), CompareUtils.truncatedRevision(headCommit.name())));
else
config.setLeftLabel(UIText.GitMergeEditorInput_WorkspaceHeader);
if (ancestorCommit != null)
config.setAncestorLabel(NLS.bind(LABELPATTERN, ancestorCommit
.getShortMessage(), CompareUtils.truncatedRevision(ancestorCommit.name())));
// set title and icon
setTitle(NLS.bind(UIText.GitMergeEditorInput_MergeEditorTitle,
new Object[] {
Activator.getDefault().getRepositoryUtil()
.getRepositoryName(repo),
rightCommit.getShortMessage(), fullBranch }));
// build the nodes
try {
return buildDiffContainer(repo, headCommit,
ancestorCommit, filterPaths, rw, monitor);
} catch (IOException e) {
throw new InvocationTargetException(e);
}
} finally {
if (rw != null)
rw.dispose();
monitor.done();
}
}
@Override
protected void contentsCreated() {
super.contentsCreated();
// select the first conflict
getNavigator().selectChange(true);
}
@Override
protected void handleDispose() {
super.handleDispose();
// we do NOT dispose the images, as these are shared
}
private IDiffContainer buildDiffContainer(Repository repository,
RevCommit headCommit,
RevCommit ancestorCommit, List<String> filterPaths, RevWalk rw,
IProgressMonitor monitor) throws IOException, InterruptedException {
monitor.setTaskName(UIText.GitMergeEditorInput_CalculatingDiffTaskName);
IDiffContainer result = new DiffNode(Differencer.CONFLICTING);
TreeWalk tw = new TreeWalk(repository);
try {
int dirCacheIndex = tw.addTree(new DirCacheIterator(repository
.readDirCache()));
int fileTreeIndex = tw.addTree(new FileTreeIterator(repository));
int repositoryTreeIndex = tw.addTree(rw.parseTree(repository
.resolve(Constants.HEAD)));
// skip ignored resources
NotIgnoredFilter notIgnoredFilter = new NotIgnoredFilter(
fileTreeIndex);
// filter by selected resources
if (filterPaths.size() > 1) {
List<TreeFilter> suffixFilters = new ArrayList<TreeFilter>();
for (String filterPath : filterPaths)
suffixFilters.add(PathFilter.create(filterPath));
TreeFilter otf = OrTreeFilter.create(suffixFilters);
tw.setFilter(AndTreeFilter.create(otf, notIgnoredFilter));
} else if (filterPaths.size() > 0) {
String path = filterPaths.get(0);
if (path.length() == 0)
tw.setFilter(notIgnoredFilter);
else
tw.setFilter(AndTreeFilter.create(PathFilter.create(path),
notIgnoredFilter));
} else
tw.setFilter(notIgnoredFilter);
tw.setRecursive(true);
while (tw.next()) {
if (monitor.isCanceled())
throw new InterruptedException();
String gitPath = tw.getPathString();
monitor.setTaskName(gitPath);
FileTreeIterator fit = tw.getTree(fileTreeIndex,
FileTreeIterator.class);
if (fit == null)
continue;
DirCacheIterator dit = tw.getTree(dirCacheIndex,
DirCacheIterator.class);
final DirCacheEntry dirCacheEntry = dit == null ? null : dit
.getDirCacheEntry();
boolean conflicting = dirCacheEntry != null
&& dirCacheEntry.getStage() > 0;
AbstractTreeIterator rt = tw.getTree(repositoryTreeIndex,
AbstractTreeIterator.class);
// compare local file against HEAD to see if it was modified
boolean modified = rt != null
&& !fit.getEntryObjectId()
.equals(rt.getEntryObjectId());
// if this is neither conflicting nor changed, we skip it
if (!conflicting && !modified)
continue;
ITypedElement right;
if (conflicting) {
GitFileRevision revision = GitFileRevision.inIndex(
repository, gitPath, DirCacheEntry.STAGE_3);
String encoding = CompareCoreUtils.getResourceEncoding(
repository, gitPath);
right = new FileRevisionTypedElement(revision, encoding);
} else
right = CompareUtils.getFileRevisionTypedElement(gitPath,
headCommit, repository);
// can this really happen?
if (right instanceof EmptyTypedElement)
continue;
IFileRevision rev;
// if the file is not conflicting (as it was auto-merged)
// we will show the auto-merged (local) version
Path repositoryPath = new Path(repository.getWorkTree()
.getAbsolutePath());
IPath location = repositoryPath
.append(fit.getEntryPathString());
IFile file = ResourceUtil.getFileForLocation(location);
if (!conflicting || useWorkspace) {
if (file != null)
rev = new LocalFileRevision(file);
else
rev = new WorkingTreeFileRevision(location.toFile());
} else {
rev = GitFileRevision.inIndex(repository, gitPath,
DirCacheEntry.STAGE_2);
}
IRunnableContext runnableContext = getContainer();
if (runnableContext == null)
runnableContext = PlatformUI.getWorkbench().getProgressService();
EditableRevision leftEditable;
if (file != null)
leftEditable = new ResourceEditableRevision(rev, file,
runnableContext);
else
leftEditable = new LocationEditableRevision(rev, location,
runnableContext);
// make sure we don't need a round trip later
try {
leftEditable.cacheContents(monitor);
} catch (CoreException e) {
throw new IOException(e.getMessage());
}
int kind = Differencer.NO_CHANGE;
if (conflicting)
kind = Differencer.CONFLICTING;
else if (modified)
kind = Differencer.PSEUDO_CONFLICT;
IDiffContainer fileParent = getFileParent(result,
repositoryPath, file, location);
ITypedElement anc;
if (ancestorCommit != null)
anc = CompareUtils.getFileRevisionTypedElement(gitPath,
ancestorCommit, repository);
else
anc = null;
// we get an ugly black icon if we have an EmptyTypedElement
// instead of null
if (anc instanceof EmptyTypedElement)
anc = null;
// create the node as child
new DiffNode(fileParent, kind, anc, leftEditable, right);
}
return result;
} finally {
tw.release();
}
}
private 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);
IDiffContainer child = root;
for (int i = 0; i < path.segmentCount() - 1; i++) {
if (i == projectSegment)
child = getOrCreateChild(child, projectName, true);
else
child = getOrCreateChild(child, path.segment(i), false);
}
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);
}
}
DiffNode child = new DiffNode(parent, Differencer.NO_CHANGE) {
@Override
public String getName() {
return name;
}
@Override
public Image getImage() {
if (projectMode)
return PROJECT_IMAGE;
else
return FOLDER_IMAGE;
}
};
return child;
}
private String readFile(File directory, String fileName) throws IOException {
byte[] content = IO.readFully(new File(directory, fileName));
// strip off the last LF
int end = content.length;
while (0 < end && content[end - 1] == '\n')
end--;
return RawParseUtils.decode(content, 0, end);
}
}