org.eclipse.emf.compare.egit.ui.internal.merge.ModelGitMergeEditorInput.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.emf.compare.egit.ui.internal.merge.ModelGitMergeEditorInput.java

Source

/*******************************************************************************
 * Copyright (C) 2010, 2014 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.emf.compare.egit.ui.internal.merge;

//CHECKSTYLE:OFF
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.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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.ICompareInput;
import org.eclipse.compare.structuremergeviewer.IDiffContainer;
import org.eclipse.compare.structuremergeviewer.IDiffElement;
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.mapping.RemoteResourceMappingContext;
import org.eclipse.core.resources.mapping.ResourceMapping;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.egit.core.internal.CompareCoreUtils;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffData;
import org.eclipse.egit.core.internal.job.RuleUtil;
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.core.project.RepositoryMapping;
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.emf.compare.egit.internal.merge.DirCacheResourceVariantTreeProvider;
import org.eclipse.emf.compare.egit.internal.merge.GitResourceVariantTreeProvider;
import org.eclipse.emf.compare.egit.internal.merge.GitResourceVariantTreeSubscriber;
import org.eclipse.emf.compare.egit.internal.merge.LogicalModels;
import org.eclipse.emf.compare.egit.ui.internal.EMFCompareEGitUIMessages;
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.swt.widgets.Display;
import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.team.core.mapping.ISynchronizationContext;
import org.eclipse.team.core.mapping.ISynchronizationScopeManager;
import org.eclipse.team.core.subscribers.Subscriber;
import org.eclipse.team.core.subscribers.SubscriberMergeContext;
import org.eclipse.team.core.subscribers.SubscriberResourceMappingContext;
import org.eclipse.team.core.subscribers.SubscriberScopeManager;
import org.eclipse.team.ui.mapping.ISynchronizationCompareAdapter;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE.SharedImages;

/**
 * A Git-specific {@link CompareEditorInput}. This class is a copy from
 * org.eclipse.egit.ui.internal.merge.GitMergeEditorInput.
 * 
 * @author <a href="mailto:mathias.kinzler@sap.com">Mathias Kinzler</a>
 */
@SuppressWarnings("restriction")
public class ModelGitMergeEditorInput 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 ModelGitMergeEditorInput(boolean useWorkspace, IPath... locations) {
        super(new CompareConfiguration());
        this.useWorkspace = useWorkspace;
        this.locations = locations;
        CompareConfiguration config = getCompareConfiguration();
        config.setLeftEditable(true);
    }

    @Override
    public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
        if ((adapter == IFile.class || adapter == IResource.class) && isUIThread()) {
            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);
    }

    private static boolean isUIThread() {
        return Display.getCurrent() != null;
    }

    @Override
    protected Object prepareInput(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
        monitor.beginTask(UIText.GitMergeEditorInput_CheckingResourcesTaskName, IProgressMonitor.UNKNOWN);

        // Make sure all resources belong to the same repository
        final Map<Repository, Collection<String>> pathsByRepository = ResourceUtil
                .splitPathsByRepository(Arrays.asList(locations));
        if (pathsByRepository.size() != 1) {
            throw new InvocationTargetException(
                    new IllegalStateException(UIText.RepositoryAction_multiRepoSelection));
        }
        final Repository repository = pathsByRepository.keySet().iterator().next();
        final List<String> filterPaths = new ArrayList<String>(pathsByRepository.get(repository));

        checkCanceled(monitor);

        // The merge drivers have done their job of putting the necessary
        // information in the index
        // Read that info and provide it to the file-specific comparators
        RevWalk rw = null;
        try {
            rw = new RevWalk(repository);

            // get the "right" side (MERGE_HEAD for merge, ORIG_HEAD for rebase)
            final RevCommit rightCommit = getRightCommit(rw, repository);

            // we need the HEAD, also to determine the common ancestor
            final RevCommit headCommit = getLeftCommit(rw, repository);

            // try to obtain the common ancestor
            RevCommit ancestorCommit = getCommonAncestor(rw, rightCommit, headCommit);

            checkCanceled(monitor);

            // set the labels
            setLabels(repository, rightCommit, headCommit, ancestorCommit);

            final ICompareInput input = prepareCompareInput(repository, filterPaths, monitor);
            if (input != null) {
                return input;
            }

            checkCanceled(monitor);
            return buildDiffContainer(repository, headCommit, ancestorCommit, filterPaths, rw, monitor);
        } catch (IOException e) {
            throw new InvocationTargetException(e);
        } finally {
            if (rw != null) {
                rw.close();
                rw.dispose();
            }
            repository.close();
            monitor.done();
        }
    }

    private void checkCanceled(IProgressMonitor monitor) throws InterruptedException {
        if (monitor.isCanceled()) {
            throw new InterruptedException();
        }
    }

    /**
     * Even if there is a single file involved in this operation, it may have a custom comparator or merger
     * defined. This will be found through its specific ISynchronizationCompareAdapter.
     * <p>
     * If there are multiple files involved, we need them all to be part of the same logical model. Otherwise,
     * we can't be sure that multiple ISynchronizationCompareAdapter aren't interested in the different files,
     * and thus cannot show a valid 'aggregate' compare editor.
     * </p>
     * <p>
     * Then again, even if the multiple files involved are all part of single-file models (i.e. none of them
     * is a part of a larger logical model, and we have as many models involved as there are files), we cannot
     * show them all within the same compare editor input. Comparing the files and determining conflicts is
     * the job of the ISynchronizationCompareAdapter(s), <u>not</u> ours. If we cannot reliably find the
     * appropriate compare adapter, we should not try and compare the files ourselves. The user will have to
     * manually open the merge tool on each individual logical model.
     * </p>
     * <p>
     * Since we cannot determine the logical model of a file that is not in the workspace, this will fall back
     * to the 'old' merge tool (we compute the diffs ourselves without consideration for the file type) iff
     * there are <u>only</u> such files in the locations set.
     * </p>
     *
     * @param repository
     *            Repository within which the compared files are located.
     * @param filterPaths
     *            repository-relative paths of the resources we are comparing.
     * @param monitor
     *            Monitor on which to report progress to the user.
     * @return The useable compare input.
     * @throws InvocationTargetException
     * @throws InterruptedException
     */
    private ICompareInput prepareCompareInput(Repository repository, List<String> filterPaths,
            IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
        try {
            final GitResourceVariantTreeProvider variantTreeProvider = new DirCacheResourceVariantTreeProvider(
                    repository, useWorkspace);
            final Subscriber subscriber = new GitResourceVariantTreeSubscriber(variantTreeProvider);
            checkCanceled(monitor);

            final Set<IProject> projects = new LinkedHashSet<IProject>();
            for (IResource root : subscriber.roots()) {
                projects.add(root.getProject());
            }

            // Compute the set of IResources involved in this operation.
            // This will be cut short if we find that at least one resource is
            // in the workspace and at least one resource is not.
            final Set<IResource> resourcesInOperation = new LinkedHashSet<IResource>();
            boolean outOfWS = false;
            for (IPath path : locations) {
                boolean foundMatchInWS = false;
                final Iterator<IProject> projectIterator = projects.iterator();
                while (!foundMatchInWS && projectIterator.hasNext()) {
                    final IProject project = projectIterator.next();
                    final IPath projectLocation = project.getLocation();
                    if (projectLocation.equals(path)) {
                        resourcesInOperation.addAll(getConflictingFilesFrom(project));
                        foundMatchInWS = true;
                    } else if (project.getLocation().isPrefixOf(path)) {
                        final IResource resource = ResourceUtil.getResourceForLocation(path, false);
                        if (resource instanceof IContainer) {
                            resourcesInOperation.addAll(getConflictingFilesFrom((IContainer) resource));
                        } else {
                            resourcesInOperation.add(resource);
                        }
                        foundMatchInWS = true;
                    }
                }
                if (!foundMatchInWS) {
                    if (!resourcesInOperation.isEmpty()) {
                        // no need to go any further : we have both files in the
                        // workspace and files outside of it
                        break;
                    } else {
                        // for now, all paths are out of the ws
                        outOfWS = true;
                    }
                } else if (outOfWS) {
                    // There was a match in the workspace for this one
                    // yet at least one path before that was out of the ws
                    break;
                }
            }

            checkCanceled(monitor);

            if (!resourcesInOperation.isEmpty() && outOfWS) {
                // At least one resource is in the workspace while at least one
                // is out of it.
                // We cannot reliably tell whether they are related enough to be
                // in the same compare editor.
                throw new InvocationTargetException(new IllegalStateException(
                        EMFCompareEGitUIMessages.getString("GitMergeEditorInput_OutOfWSResources"))); //$NON-NLS-1$
            } else if (resourcesInOperation.isEmpty()) {
                // All resources are out of the workspace.
                // Fall back to the workspace-unaware "prepareDiffInput"
            } else {
                final RemoteResourceMappingContext remoteMappingContext = new SubscriberResourceMappingContext(
                        subscriber, true);
                // Make sure that all of the compared resources are either
                // - all part of the same model, or
                // - not part of any model.
                Set<IResource> model = null;
                for (IResource comparedResource : resourcesInOperation) {
                    model = LogicalModels.discoverModel(comparedResource, remoteMappingContext);
                    if (model.isEmpty()) {
                        // not part of any model... carry on
                    } else {
                        if (!model.containsAll(resourcesInOperation)) {
                            // These resources belong to multiple different
                            // models.
                            // The merge tool needs to be launched manually on
                            // each distinct logical model.
                            throw new RuntimeException(
                                    EMFCompareEGitUIMessages.getString("GitMergeEditorInput_MultipleModels")); //$NON-NLS-1$
                        } else {
                            // No use going further : we know these resource all
                            // belong to the same model.
                            break;
                        }
                    }
                }

                final ISynchronizationCompareAdapter compareAdapter = LogicalModels.findAdapter(model,
                        ISynchronizationCompareAdapter.class);
                if (compareAdapter != null) {
                    final Set<ResourceMapping> allMappings = LogicalModels.getResourceMappings(model,
                            remoteMappingContext);

                    checkCanceled(monitor);

                    final ISynchronizationContext synchronizationContext = prepareSynchronizationContext(repository,
                            subscriber, allMappings, remoteMappingContext);
                    final Object modelObject = allMappings.iterator().next().getModelObject();
                    if (compareAdapter.hasCompareInput(synchronizationContext, modelObject)) {
                        return compareAdapter.asCompareInput(synchronizationContext, modelObject);
                    } else {
                        // This compare adapter does not know about our model
                        // object
                    }
                } else {
                    // There isn't a specific compare adapter for this logical
                    // model. Fall back to default.
                }
            }
        } catch (IOException e) {
            throw new InvocationTargetException(e);
        } catch (CoreException e) {
            throw new InvocationTargetException(e);
        }
        return null;
    }

    private Set<IResource> getConflictingFilesFrom(IContainer container) throws IOException {
        final Set<IResource> conflictingResources = new LinkedHashSet<IResource>();
        final RepositoryMapping mapping = RepositoryMapping.getMapping(container);
        if (mapping == null) {
            return conflictingResources;
        }
        final IndexDiffCacheEntry indexDiffCacheEntry = org.eclipse.egit.core.Activator.getDefault()
                .getIndexDiffCache().getIndexDiffCacheEntry(mapping.getRepository());
        if (indexDiffCacheEntry == null) {
            return conflictingResources;
        }
        final IndexDiffData indexDiffData = indexDiffCacheEntry.getIndexDiff();
        if (indexDiffData != null) {
            final IPath containerPath = container.getLocation();
            final File workTree = mapping.getWorkTree();
            if (workTree != null) {
                final IPath workDirPrefix = new Path(workTree.getCanonicalPath());
                for (String conflicting : indexDiffData.getConflicting()) {
                    final IPath resourcePath = workDirPrefix.append(conflicting);
                    if (containerPath.isPrefixOf(resourcePath)) {
                        final IPath containerRelativePath = resourcePath
                                .removeFirstSegments(containerPath.segmentCount());
                        conflictingResources.add(container.getFile(containerRelativePath));
                    }
                }
            }
        }
        return conflictingResources;
    }

    private ISynchronizationContext prepareSynchronizationContext(final Repository repository,
            Subscriber subscriber, Set<ResourceMapping> allModelMappings,
            RemoteResourceMappingContext mappingContext)
            throws CoreException, OperationCanceledException, InterruptedException {
        final ResourceMapping[] mappings = allModelMappings.toArray(new ResourceMapping[allModelMappings.size()]);

        final ISynchronizationScopeManager manager = new SubscriberScopeManager(subscriber.getName(), mappings,
                subscriber, mappingContext, true) {
            @Override
            public ISchedulingRule getSchedulingRule() {
                return RuleUtil.getRule(repository);
            }
        };
        manager.initialize(new NullProgressMonitor());

        final ISynchronizationContext context = new GitSynchronizationContext(subscriber, manager);
        // Wait for the asynchronous scope expanding to end (started from the
        // initialization of our synchronization context)
        Job.getJobManager().join(context, new NullProgressMonitor());

        return context;
    }

    private RevCommit getRightCommit(RevWalk revWalk, Repository repository) throws InvocationTargetException {
        try {
            String target;
            if (repository.getRepositoryState().equals(RepositoryState.MERGING)) {
                target = Constants.MERGE_HEAD;
            } else if (repository.getRepositoryState().equals(RepositoryState.CHERRY_PICKING)) {
                target = Constants.CHERRY_PICK_HEAD;
            } else if (repository.getRepositoryState().equals(RepositoryState.REBASING_INTERACTIVE)) {
                target = readFile(repository.getDirectory(),
                        RebaseCommand.REBASE_MERGE + File.separatorChar + RebaseCommand.STOPPED_SHA);
            } else {
                target = Constants.ORIG_HEAD;
            }
            ObjectId mergeHead = repository.resolve(target);
            if (mergeHead == null) {
                throw new IOException(NLS.bind(UIText.ValidationUtils_CanNotResolveRefMessage, target));
            }
            return revWalk.parseCommit(mergeHead);
        } catch (IOException e) {
            throw new InvocationTargetException(e);
        }
    }

    private RevCommit getLeftCommit(RevWalk revWalk, Repository repository) throws InvocationTargetException {
        try {
            ObjectId head = repository.resolve(Constants.HEAD);
            if (head == null) {
                throw new IOException(NLS.bind(UIText.ValidationUtils_CanNotResolveRefMessage, Constants.HEAD));
            }
            return revWalk.parseCommit(head);
        } catch (IOException e) {
            throw new InvocationTargetException(e);
        }
    }

    private RevCommit getCommonAncestor(RevWalk revWalk, RevCommit rightCommit, RevCommit leftCommit) {
        List<RevCommit> startPoints = new ArrayList<RevCommit>();
        revWalk.setRevFilter(RevFilter.MERGE_BASE);
        startPoints.add(rightCommit);
        startPoints.add(leftCommit);
        try {
            revWalk.markStart(startPoints);
            return revWalk.next();
        } catch (Exception e) {
            return null;
        }
    }

    private void setLabels(Repository repository, RevCommit rightCommit, RevCommit leftCommit,
            RevCommit ancestorCommit) throws InvocationTargetException {
        CompareConfiguration config = getCompareConfiguration();
        config.setRightLabel(NLS.bind(LABELPATTERN, rightCommit.getShortMessage(),
                CompareUtils.truncatedRevision(rightCommit.name())));

        if (!useWorkspace) {
            config.setLeftLabel(NLS.bind(LABELPATTERN, leftCommit.getShortMessage(),
                    CompareUtils.truncatedRevision(leftCommit.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
        final String fullBranch;
        try {
            fullBranch = repository.getFullBranch();
        } catch (IOException e) {
            throw new InvocationTargetException(e);
        }
        setTitle(NLS.bind(UIText.GitMergeEditorInput_MergeEditorTitle,
                new Object[] { Activator.getDefault().getRepositoryUtil().getRepositoryName(repository),
                        rightCommit.getShortMessage(), fullBranch }));
    }

    @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, false);
                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.close();
        }
    }

    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);
    }

    private static class GitSynchronizationContext extends SubscriberMergeContext {
        public GitSynchronizationContext(Subscriber subscriber, ISynchronizationScopeManager scopeManager) {
            super(subscriber, scopeManager);
            initialize();
        }

        public void markAsMerged(IDiff node, boolean inSyncHint, IProgressMonitor monitor) throws CoreException {
            // Won't be used as a merging context
        }

        public void reject(IDiff diff, IProgressMonitor monitor) throws CoreException {
            // Won't be used as a merging context
        }

        @Override
        protected void makeInSync(IDiff diff, IProgressMonitor monitor) throws CoreException {
            // Won't be used as a merging context
        }
    }
}
// CHECKSTYLE:ON