at.bitandart.zoubek.mervin.gerrit.GerritReviewRepositoryService.java Source code

Java tutorial

Introduction

Here is the source code for at.bitandart.zoubek.mervin.gerrit.GerritReviewRepositoryService.java

Source

/*******************************************************************************
 * Copyright (c) 2015, 2016 Florian Zoubek.
 * 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
 *
 * Contributors:
 *    Florian Zoubek - initial API and implementation
 *******************************************************************************/
package at.bitandart.zoubek.mervin.gerrit;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.e4.core.contexts.ContextInjectionFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.util.ECollections;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.EMFCompare;
import org.eclipse.emf.compare.scope.DefaultComparisonScope;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.URIHandler;
import org.eclipse.emf.ecore.resource.impl.ExtensibleURIConverterImpl;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.EcoreUtil.Copier;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.domain.IEditingDomainProvider;
import org.eclipse.emf.transaction.RecordingCommand;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.TreeWalk;

import com.google.common.base.Predicate;

import at.bitandart.zoubek.mervin.IReviewDescriptor;
import at.bitandart.zoubek.mervin.IReviewRepositoryService;
import at.bitandart.zoubek.mervin.exceptions.InvalidReviewException;
import at.bitandart.zoubek.mervin.exceptions.InvalidReviewRepositoryException;
import at.bitandart.zoubek.mervin.exceptions.RepositoryIOException;
import at.bitandart.zoubek.mervin.model.modelreview.Comment;
import at.bitandart.zoubek.mervin.model.modelreview.CommentLink;
import at.bitandart.zoubek.mervin.model.modelreview.DiagramPatch;
import at.bitandart.zoubek.mervin.model.modelreview.DiagramResource;
import at.bitandart.zoubek.mervin.model.modelreview.ModelPatch;
import at.bitandart.zoubek.mervin.model.modelreview.ModelResource;
import at.bitandart.zoubek.mervin.model.modelreview.ModelReview;
import at.bitandart.zoubek.mervin.model.modelreview.ModelReviewFactory;
import at.bitandart.zoubek.mervin.model.modelreview.Patch;
import at.bitandart.zoubek.mervin.model.modelreview.PatchChangeType;
import at.bitandart.zoubek.mervin.model.modelreview.PatchSet;
import at.bitandart.zoubek.mervin.model.modelreview.User;

/**
 * <p>
 * This service handles reviews stored at a remote gerrit git repository
 * referenced from a local clone of the repository. Any URI provided to this
 * service used to identify a repository must point to a local git repository
 * where the origin remote must point to the remote gerrit git repository. Also
 * the user used to access the repository must have read and write access to all
 * change refs at the server.
 * </p>
 * 
 * <p>
 * <b>Important:</b> The review id of a change is not the change id, it is the
 * internal primary key of the change used by Gerrit!.
 * </p>
 * 
 * @author Florian Zoubek
 *
 * @see GerritReviewDescriptor
 */
public class GerritReviewRepositoryService implements IReviewRepositoryService {

    /**
     * the ref pattern used to identify change refs and parse the corresponding
     * change id and patch set id
     */
    private static final String CHANGE_REF_PATTERN = "refs/changes/(?<modChangePk>\\d\\d)/(?<changePk>\\d*)/(?<patchSetId>.*)";

    @SuppressWarnings("unused")
    private static final String CHANGE_REF_PATTERN_GROUP_MOD_CHANGE_PK = "modChangePk";
    private static final String CHANGE_REF_PATTERN_GROUP_CHANGE_PK = "changePk";
    private static final String CHANGE_REF_PATTERN_GROUP_PATCH_SET_ID = "patchSetId";
    private static final String COMMENTS_BASE_REF = "refs/mervin/comments";
    private static final String COMMENTS_FILE_EXTENSION = "modelreview";
    private static final String COMMENTS_FILE_URI = "comments." + COMMENTS_FILE_EXTENSION;

    /**
     * the model review factory used by this service to create all model
     * elements
     */
    @Inject
    private ModelReviewFactory modelReviewFactory;

    @SuppressWarnings("restriction")
    @Inject
    private org.eclipse.e4.core.services.log.Logger logger;

    @Inject
    private IEclipseContext eclipseContext;

    @SuppressWarnings("restriction")
    @Override
    public List<IReviewDescriptor> getReviews(URI uri) throws InvalidReviewRepositoryException {

        List<IReviewDescriptor> changeIds = new LinkedList<>();

        try {
            // connect to the local git repository
            Git git = Git.open(new File(uri));

            try {
                // Assume that origin refers to the remote gerrit repository
                // list all remote refs from origin
                Collection<Ref> remoteRefs = git.lsRemote().setTimeout(60).call();

                Pattern changeRefPattern = Pattern.compile(CHANGE_REF_PATTERN);

                // search for change refs
                for (Ref ref : remoteRefs) {

                    Matcher matcher = changeRefPattern.matcher(ref.getName());
                    if (matcher.matches()) {
                        String changePk = matcher.group(CHANGE_REF_PATTERN_GROUP_CHANGE_PK);
                        String changeId = "<unknown>";
                        GerritReviewDescriptor reviewDescriptor;
                        try {
                            reviewDescriptor = new GerritReviewDescriptor(Integer.parseInt(changePk), changeId);
                        } catch (NumberFormatException nfe) {
                            // FIXME ignore it or throw an exception?
                            break;
                        }

                        if (!changeIds.contains(reviewDescriptor)) {
                            changeIds.add(reviewDescriptor);

                            /*
                             * the change id is present in all commit messages,
                             * so we extract it from the commit message of the
                             * current ref
                             */
                            FetchResult fetchResult = git.fetch().setRefSpecs(new RefSpec(ref.getName())).call();

                            Ref localRef = fetchResult.getAdvertisedRef(ref.getName());
                            RevWalk revWalk = new RevWalk(git.getRepository());
                            RevCommit commit = revWalk.parseCommit(localRef.getObjectId());
                            String[] paragraphs = commit.getFullMessage().split("\n");
                            String lastParagraph = paragraphs[paragraphs.length - 1];
                            Pattern pattern = Pattern.compile(".*Change-Id: (I[^ \n]*).*");
                            Matcher changeIdMatcher = pattern.matcher(lastParagraph);

                            if (changeIdMatcher.matches()) {
                                changeId = changeIdMatcher.group(1);
                                reviewDescriptor.setChangeId(changeId);
                                ;
                            } else {
                                logger.warn(MessageFormat.format(
                                        "Could not find the change id for Gerrit change with primary key {0}",
                                        changePk));
                            }
                            revWalk.close();
                        }
                    }
                }

            } catch (GitAPIException e) {
                throw new RepositoryIOException("Error during loading all remote changes", e);
            }

        } catch (IOException e) {
            throw new InvalidReviewRepositoryException("Could not open local git repository", e);
        }
        return changeIds;
    }

    @Override
    public ModelReview loadReview(URI uri, String id, User currentReviewer, IProgressMonitor monitor)
            throws InvalidReviewRepositoryException, InvalidReviewException, RepositoryIOException {
        /*
         * Fetch all refs to the patch sets for the particular change and create
         * the model instance from it
         */

        monitor.beginTask("Connecting to repository", IProgressMonitor.UNKNOWN);

        try {
            Git git = Git.open(new File(uri));
            int iId = Integer.parseInt(id);

            // First of all: fetch the patch sets
            // git fetch origin
            // +refs/changes/id%100/<cid>/*:refs/changes/id%100/<cid>/*
            // Refspec of a patchset:
            // +refs/changes/id%100/<cid>/<psId>:refs/changes/id%100/<cid>/<psId>

            monitor.beginTask("Fetching change ref", IProgressMonitor.UNKNOWN);
            git.fetch()
                    .setRefSpecs(new RefSpec(MessageFormat.format(
                            "+refs/changes/{0,number,00}/{1}/*:refs/changes/{0,number,00}/{1}/*", iId % 100, iId)))
                    .call();

            // create model instance

            ModelReview modelReview = modelReviewFactory.createModelReview();
            modelReview.setId(id);
            modelReview.setRepositoryURI(uri.toString());
            modelReview.setCurrentReviewer(currentReviewer);
            EList<PatchSet> patchSets = modelReview.getPatchSets();

            Repository repository = git.getRepository();
            Map<String, Ref> allRefs = repository.getAllRefs();
            Pattern changeRefPattern = Pattern.compile(CHANGE_REF_PATTERN);

            monitor.beginTask("Loading Patch Sets", allRefs.size());

            List<ResourceSet> resourceSets = new LinkedList<>();

            for (Entry<String, Ref> refEntry : allRefs.entrySet()) {
                Matcher matcher = changeRefPattern.matcher(refEntry.getValue().getName());
                if (matcher.matches() && matcher.group(CHANGE_REF_PATTERN_GROUP_CHANGE_PK).equals(id)) {

                    PatchSet patchSet = modelReviewFactory.createPatchSet();
                    patchSets.add(patchSet);
                    patchSet.setId(matcher.group(CHANGE_REF_PATTERN_GROUP_PATCH_SET_ID));

                    monitor.subTask("Loading Patch Set #" + patchSet.getId());

                    // load patched files
                    loadPatches(patchSet, refEntry.getValue(), git);

                    // load involved models
                    resourceSets.addAll(loadInvolvedModelsAndDiagrams(patchSet, refEntry.getValue(), git));

                    // compare the involved models
                    patchSet.setModelComparison(compareModels(patchSet));

                    // compare the involved diagrams
                    patchSet.setDiagramComparison(compareDiagrams(patchSet));
                }
                monitor.worked(1);
            }

            monitor.beginTask("Sorting Patch Sets", IProgressMonitor.UNKNOWN);

            /*
             * sort by their identifiers, numeric identifiers before string
             * identifiers (gerrit currently has only numeric patch set
             * identifiers, but to be on the save side also consider non-numeric
             * identifiers )
             */
            ECollections.sort(patchSets, new Comparator<PatchSet>() {

                @Override
                public int compare(PatchSet o1, PatchSet o2) {
                    String psId1 = o1.getId();
                    String psId2 = o2.getId();
                    Integer iPsId1 = null;
                    Integer iPsId2 = null;
                    try {
                        iPsId1 = Integer.valueOf(psId1);
                    } catch (NumberFormatException nfe) {
                    }
                    try {
                        iPsId2 = Integer.valueOf(psId2);
                    } catch (NumberFormatException nfe) {
                    }

                    if (iPsId1 != null && iPsId2 != null) {
                        // both numeric ids
                        return iPsId1.compareTo(iPsId2);
                    } else if (iPsId1 != null && iPsId2 == null) {
                        // only one is numeric, the numeric id is always less
                        // than the string id
                        return -1;
                    } else if (iPsId1 == null && iPsId2 != null) {
                        // only one is numeric, the numeric id is always less
                        // than the string id
                        return 1;
                    }

                    // fallback to string sort
                    return psId1.compareTo(psId2);
                }
            });

            monitor.beginTask("Loading Comments", IProgressMonitor.UNKNOWN);

            loadComments(repository, modelReview, resourceSets);

            monitor.done();

            return modelReview;

        } catch (IOException e) {
            throw new InvalidReviewRepositoryException("Could not open local git repository", e);
        } catch (NumberFormatException e) {
            throw new InvalidReviewException(MessageFormat.format("Invalid review id: {0}", id));
        } catch (GitAPIException e) {
            throw new RepositoryIOException("Error occured during reading from the git repository", e);
        }
    }

    /**
     * compares the old and new versions of the involved models in the specified
     * {@link PatchSet}.
     * 
     * @param patchSet
     * @return
     */
    public Comparison compareModels(PatchSet patchSet) {

        EList<ModelResource> oldInvolvedModels = patchSet.getOldInvolvedModels();
        EList<ModelResource> newInvolvedModels = patchSet.getNewInvolvedModels();
        ResourceSet oldResourceSet = new ResourceSetImpl();
        ResourceSet newResourceSet = new ResourceSetImpl();

        if (!oldInvolvedModels.isEmpty()) {
            oldResourceSet = oldInvolvedModels.get(0).getObjects().get(0).eResource().getResourceSet();
        }

        if (!newInvolvedModels.isEmpty()) {
            newResourceSet = newInvolvedModels.get(0).getObjects().get(0).eResource().getResourceSet();
        }

        EcoreUtil.resolveAll(oldResourceSet);
        EcoreUtil.resolveAll(newResourceSet);

        EMFCompare comparator = EMFCompare.builder().build();

        DefaultComparisonScope scope = new DefaultComparisonScope(newResourceSet, oldResourceSet, null);
        scope.setResourceSetContentFilter(new Predicate<Resource>() {

            @Override
            public boolean apply(Resource resource) {
                return !resource.getURI().fileExtension().equals("notation");
            }
        });
        Comparison comparison = comparator.compare(scope);
        return comparison;
    }

    /**
     * compares the old and new versions of the involved diagrams in the
     * specified {@link PatchSet}.
     * 
     * @param patchSet
     * @return
     */
    public Comparison compareDiagrams(PatchSet patchSet) {

        EList<DiagramResource> oldInvolvedDiagrams = patchSet.getOldInvolvedDiagrams();
        EList<DiagramResource> newInvolvedDiagrams = patchSet.getNewInvolvedDiagrams();
        ResourceSet oldResourceSet = new ResourceSetImpl();
        ResourceSet newResourceSet = new ResourceSetImpl();

        if (!oldInvolvedDiagrams.isEmpty()) {
            oldResourceSet = oldInvolvedDiagrams.get(0).getObjects().get(0).eResource().getResourceSet();
        }

        if (!newInvolvedDiagrams.isEmpty()) {
            newResourceSet = newInvolvedDiagrams.get(0).getObjects().get(0).eResource().getResourceSet();
        }

        EcoreUtil.resolveAll(oldResourceSet);
        EcoreUtil.resolveAll(newResourceSet);

        EMFCompare comparator = EMFCompare.builder().build();

        DefaultComparisonScope scope = new DefaultComparisonScope(newResourceSet, oldResourceSet, null);
        scope.setResourceSetContentFilter(new Predicate<Resource>() {

            @Override
            public boolean apply(Resource resource) {
                return resource.getURI().fileExtension().equals("notation");
            }
        });
        Comparison comparison = comparator.compare(scope);
        return comparison;
    }

    /**
     * loads all involved models and diagrams for the given patchSet using the
     * given {@link Git} instance from the given git {@link Ref}.
     * 
     * @param patchSet
     *            the patch set instance to store the involved models into
     * @param ref
     *            the git ref to the commit which contains the patch set.
     * @param git
     *            the git instance to use
     * @return a list containing the resource sets for the old and the new model
     *         resources.
     * @throws IOException
     */
    private List<ResourceSet> loadInvolvedModelsAndDiagrams(PatchSet patchSet, Ref ref, Git git)
            throws IOException {

        String commitHash = ref.getObjectId().name();

        RevWalk revWalk = new RevWalk(git.getRepository());
        RevCommit newCommit = revWalk.parseCommit(ref.getObjectId());
        RevCommit oldCommit = newCommit.getParent(0);
        revWalk.parseHeaders(oldCommit);
        revWalk.close();

        String parentCommitHash = oldCommit.getId().name();

        URI repoURI = git.getRepository().getDirectory().toURI();
        String authority = repoURI.getAuthority();
        String path = repoURI.getPath();
        String repoPath = (authority != null ? authority + "/" : "") + (path != null ? path : "");
        if (repoPath.endsWith("/")) {
            repoPath = repoPath.substring(0, repoPath.length() - 1);
        }

        ResourceSet newResourceSet = createGitAwareResourceSet(commitHash, repoPath,
                Collections.<ResourceSet>emptyList());
        ResourceSet oldModelResourceSet = createGitAwareResourceSet(parentCommitHash, repoPath,
                Collections.<ResourceSet>emptyList());

        for (Patch patch : patchSet.getPatches()) {
            if (patch instanceof ModelPatch || patch instanceof DiagramPatch) {
                org.eclipse.emf.common.util.URI newUri = org.eclipse.emf.common.util.URI
                        .createURI(GitURIParser.GIT_COMMIT_SCHEME + "://" + repoPath + "/" + commitHash + "/"
                                + patch.getNewPath());

                org.eclipse.emf.common.util.URI oldUri = org.eclipse.emf.common.util.URI
                        .createURI(GitURIParser.GIT_COMMIT_SCHEME + "://" + repoPath + "/" + parentCommitHash + "/"
                                + patch.getOldPath());

                if (patch.getChangeType() != PatchChangeType.DELETE) {
                    // if the patch has been deleted no new resource exists
                    Resource newResource = newResourceSet.getResource(newUri, true);
                    try {
                        applyResourceContent(newResource, patch, false);
                    } catch (IOException e) {
                        throw new IOException(
                                MessageFormat.format("Could not load resource \"{0}\" for patch set {1}",
                                        newUri.toString(), patchSet.getId()),
                                e);
                    }
                }

                if (patch.getChangeType() != PatchChangeType.ADD) {
                    // if the patch has been added no old resource exists
                    Resource oldResource = oldModelResourceSet.getResource(oldUri, true);
                    try {
                        applyResourceContent(oldResource, patch, true);
                    } catch (IOException e) {
                        throw new IOException(
                                MessageFormat.format("Could not load resource \"{0}\" for patch set {1}",
                                        oldUri.toString(), patchSet.getId()),
                                e);
                    }
                }

            }
        }

        List<ResourceSet> resourceSets = new ArrayList<>(2);
        resourceSets.add(oldModelResourceSet);
        resourceSets.add(newResourceSet);
        return resourceSets;
    }

    /**
     * applies the content of the given resource to the patch instance and also
     * update the involved models and diagrams references in the parent patch
     * set.
     * 
     * @param resource
     *            the resource to read form
     * @param patch
     *            the patch to apply the data to
     * @param old
     *            an boolean indicating if the resource contains content of the
     *            old version or the new version
     * @throws IOException
     */
    private void applyResourceContent(Resource resource, Patch patch, boolean old) throws IOException {
        PatchSet patchSet = patch.getPatchSet();
        EList<ModelResource> involvedModels = null;
        EList<DiagramResource> involvedDiagrams = null;
        if (old) {
            involvedModels = patchSet.getOldInvolvedModels();
            involvedDiagrams = patchSet.getOldInvolvedDiagrams();
        } else {
            involvedModels = patchSet.getNewInvolvedModels();
            involvedDiagrams = patchSet.getNewInvolvedDiagrams();
        }

        resource.load(null);

        if (patch instanceof ModelPatch) {

            // create a new model resource and add all objects from
            // the resource to the object list
            ModelResource modelResource = modelReviewFactory.createModelResource();
            EList<EObject> containedObjects = modelResource.getObjects();
            containedObjects.addAll(resource.getContents());

            // determine the root package if possible
            List<EPackage> rootPackages = findRootPackages(containedObjects);
            modelResource.getRootPackages().addAll(rootPackages);
            if (old) {
                ((ModelPatch) patch).setOldModelResource(modelResource);
            } else {
                ((ModelPatch) patch).setNewModelResource(modelResource);
            }

            // also update the involved models
            updateInvolvedModels(involvedModels, containedObjects);

        } else if (patch instanceof DiagramPatch) {

            // create a new diagram resource and add all objects from
            // the resource to the object list
            DiagramResource diagramResource = modelReviewFactory.createDiagramResource();
            EList<EObject> containedObjects = diagramResource.getObjects();
            containedObjects.addAll(resource.getContents());

            // determine the root package if possible
            List<EPackage> rootPackages = findRootPackages(containedObjects);
            diagramResource.getRootPackages().addAll(rootPackages);
            if (old) {
                ((DiagramPatch) patch).setOldDiagramResource(diagramResource);
            } else {
                ((DiagramPatch) patch).setNewDiagramResource(diagramResource);
            }

            // also update the involved diagrams
            updateInvolvedDiagrams(involvedDiagrams, containedObjects);
        }
    }

    /**
     * updates the given list of involved models with the given list of objects.
     * 
     * @param involvedModels
     *            the list of involved models that should be updated
     * @param objects
     *            the objects used to update the list of involved models
     */
    private void updateInvolvedModels(List<ModelResource> involvedModels, List<EObject> objects) {
        for (EObject object : objects) {
            EPackage rootPackage = findRootPackage(object);

            ModelResource modelResource = null;

            // search for already existing model resources
            for (ModelResource existingModel : involvedModels) {
                if (existingModel.getRootPackages().contains(rootPackage)) {
                    modelResource = existingModel;
                    break;
                }
            }

            if (modelResource == null) {
                // there is no existing model resource for the given root
                // package, so we have to create it
                modelResource = modelReviewFactory.createModelResource();
                modelResource.getRootPackages().add(rootPackage);
                involvedModels.add(modelResource);
            }
            modelResource.getObjects().add(object);
        }
    }

    /**
     * updates the given list of involved diagrams with the given list of
     * objects.
     * 
     * @param involvedDiagrams
     *            the list of involved diagrams that should be updated
     * @param objects
     *            the objects used to update the list of involved diagrams
     */
    private void updateInvolvedDiagrams(List<DiagramResource> involvedDiagrams, List<EObject> objects) {
        for (EObject object : objects) {
            EPackage rootPackage = findRootPackage(object);

            DiagramResource diagramResource = null;

            // search for already existing diagram resource
            for (DiagramResource existingDiagram : involvedDiagrams) {
                if (existingDiagram.getRootPackages().contains(rootPackage)) {
                    diagramResource = existingDiagram;
                    break;
                }
            }

            if (diagramResource == null) {
                // there is no existing diagram resource for the given root
                // package, so we have to create it
                diagramResource = modelReviewFactory.createDiagramResource();
                diagramResource.getRootPackages().add(rootPackage);
                involvedDiagrams.add(diagramResource);
            }
            diagramResource.getObjects().add(object);
        }
    }

    /**
     * lists all root packages of the given list of objects.
     * 
     * @param objects
     * @return a list of all root packages of the given list of objects
     */
    private static List<EPackage> findRootPackages(EList<EObject> objects) {
        List<EPackage> packages = new LinkedList<EPackage>();
        for (EObject object : objects) {
            EPackage rootPackage = findRootPackage(object);
            if (!packages.contains(rootPackage)) {
                packages.add(rootPackage);
            }
        }
        return packages;
    }

    /**
     * 
     * @param object
     * @return the root {@link EPackage} of the given object
     */
    private static EPackage findRootPackage(EObject object) {
        EPackage ePackage = object.eClass().getEPackage();
        EPackage rootPackage = null;
        while (ePackage != null) {
            rootPackage = ePackage;
            ePackage = ePackage.getESuperPackage();
        }
        return rootPackage;
    }

    /**
     * creates an resource set that resolves file
     * {@link org.eclipse.emf.common.util.URI}s to files in the given git commit
     * tree.
     * 
     * @param commitHash
     *            the commit hash
     * @param repoPath
     *            the absolute path to the git repo
     * @param fallbackSets
     *            a list of {@link ResourceSet}s that should be considered when
     *            searching for a resource with a specific URI
     * @return the resource set
     */
    private ResourceSet createGitAwareResourceSet(final String commitHash, final String repoPath,
            final List<ResourceSet> fallbackSets) {

        ResourceSet resourceSet = new ResourceSetImpl() {
            @Override
            protected Resource delegatedGetResource(org.eclipse.emf.common.util.URI uri, boolean loadOnDemand) {

                /*
                 * try to resolve the resource from one of the fallback resource
                 * sets
                 */
                for (ResourceSet resourceSet : fallbackSets) {

                    Resource resource = resourceSet.getResource(uri, false);
                    if (resource != null) {
                        return resource;
                    }
                }

                return super.delegatedGetResource(uri, loadOnDemand);
            }
        };

        resourceSet.setURIConverter(new ExtensibleURIConverterImpl() {
            @Override
            public org.eclipse.emf.common.util.URI normalize(org.eclipse.emf.common.util.URI uri) {
                org.eclipse.emf.common.util.URI normalizedURI = super.normalize(uri);
                if (uri.scheme().equals("file")) {
                    org.eclipse.emf.common.util.URI oldPrefix = org.eclipse.emf.common.util.URI
                            .createURI("file://");
                    org.eclipse.emf.common.util.URI newPrefix = org.eclipse.emf.common.util.URI
                            .createURI(GitURIParser.GIT_COMMIT_SCHEME + "://" + repoPath + "/" + commitHash + "/");
                    normalizedURI = normalizedURI.replacePrefix(oldPrefix, newPrefix);
                }
                return normalizedURI;
            }
        });
        EList<URIHandler> uriHandlers = resourceSet.getURIConverter().getURIHandlers();
        uriHandlers.add(ContextInjectionFactory.make(ReadOnlyGitCommitURIHandler.class, eclipseContext));
        /*
         * make sure the Git commit URI handler is placed before the standard
         * URI Handler which handles by default any URI (otherwise the default
         * URI handler is visited first and is used to load the resource)
         */
        uriHandlers.move(uriHandlers.size() - 1, uriHandlers.size() - 2);
        TransactionalEditingDomain.Factory.INSTANCE.createEditingDomain(resourceSet);
        return resourceSet;
    }

    /**
     * loads all patches of from the given list of {@link DiffEntry}s.
     * 
     * @param patchSet
     *            the patchSet to add the patches to.
     * @param ref
     *            the ref to the commit of the patch set.
     * @param repository
     *            the git repository instance
     * @throws RepositoryIOException
     */
    private void loadPatches(PatchSet patchSet, Ref ref, Git git) throws RepositoryIOException {

        EList<Patch> patches = patchSet.getPatches();
        Repository repository = git.getRepository();

        try {

            RevWalk revWalk = new RevWalk(repository);
            RevCommit newCommit = revWalk.parseCommit(ref.getObjectId());
            RevCommit oldCommit = newCommit.getParent(0);
            revWalk.parseHeaders(oldCommit);
            ObjectReader objectReader = repository.newObjectReader();
            revWalk.close();

            CanonicalTreeParser newTreeIterator = new CanonicalTreeParser();
            newTreeIterator.reset(objectReader, newCommit.getTree().getId());
            CanonicalTreeParser oldTreeIterator = new CanonicalTreeParser();
            oldTreeIterator.reset(objectReader, oldCommit.getTree().getId());

            List<DiffEntry> diffs = git.diff().setOldTree(oldTreeIterator).setNewTree(newTreeIterator).call();

            for (DiffEntry diff : diffs) {

                String newPath = diff.getNewPath();
                String oldPath = diff.getOldPath();
                Patch patch = null;

                /*
                 * only papyrus diagrams are supported for now, so models are in
                 * .uml files, diagrams in .notation files
                 */

                if (diff.getChangeType() != ChangeType.DELETE) {
                    if (newPath.endsWith(".uml")) {
                        patch = modelReviewFactory.createModelPatch();

                    } else if (newPath.endsWith(".notation")) {
                        patch = modelReviewFactory.createDiagramPatch();
                    } else {
                        patch = modelReviewFactory.createPatch();
                    }
                } else {
                    if (oldPath.endsWith(".uml")) {
                        patch = modelReviewFactory.createModelPatch();

                    } else if (oldPath.endsWith(".notation")) {
                        patch = modelReviewFactory.createDiagramPatch();
                    } else {
                        patch = modelReviewFactory.createPatch();
                    }
                }

                switch (diff.getChangeType()) {
                case ADD:
                    patch.setChangeType(PatchChangeType.ADD);
                    break;
                case COPY:
                    patch.setChangeType(PatchChangeType.COPY);
                    break;
                case DELETE:
                    patch.setChangeType(PatchChangeType.DELETE);
                    break;
                case MODIFY:
                    patch.setChangeType(PatchChangeType.MODIFY);
                    break;
                case RENAME:
                    patch.setChangeType(PatchChangeType.RENAME);
                    break;
                }

                patch.setNewPath(newPath);
                patch.setOldPath(oldPath);

                if (diff.getChangeType() != ChangeType.DELETE) {
                    ObjectLoader objectLoader = repository.open(diff.getNewId().toObjectId());
                    patch.setNewContent(objectLoader.getBytes());
                }
                if (diff.getChangeType() != ChangeType.ADD) {
                    ObjectLoader objectLoader = repository.open(diff.getOldId().toObjectId());
                    patch.setOldContent(objectLoader.getBytes());
                }
                patches.add(patch);

            }

        } catch (IOException e) {
            throw new RepositoryIOException(MessageFormat.format(
                    "An IO error occured during loading the patches for patch set #{0}", patchSet.getId()), e);
        } catch (GitAPIException e) {
            throw new RepositoryIOException(
                    MessageFormat.format("An JGit API error occured during loading the patches for patch set #{0}",
                            patchSet.getId()),
                    e);
        }
    }

    /**
     * loads the comments for the given {@link ModelReview} from the given
     * repository. The current reviewer should be set for the given model to
     * avoid duplicated user objects representing the same user. The resource
     * set used to load the comments tries to load resources containing
     * referenced EObjects from the given list of model resource sets before
     * creating the resource directly, in order to avoid duplicated model
     * elements. This is necessary as {@link CommentLink}s may refer to objects
     * contained in other resource sets.
     * 
     * @param repository
     *            the repository to load the comments from.
     * @param modelReview
     *            the {@link ModelReview} to load the comments for.
     * @param modelResourceSets
     *            a list of resource sets to resolve referenced model elements
     *            if they cannot be found in the comments model.
     * @throws RepositoryIOException
     */
    private void loadComments(Repository repository, final ModelReview modelReview,
            List<ResourceSet> modelResourceSets) throws RepositoryIOException {

        String commentRefName = getCommentRefName(modelReview);
        String repoPath = repository.getWorkTree().getPath();
        Ref commentRef;

        try {

            commentRef = repository.exactRef(commentRefName);
            String commitHash = commentRef.getObjectId().name();

            /* prepare the resource set and the resource for the comments */
            org.eclipse.emf.common.util.URI commentsUri = org.eclipse.emf.common.util.URI.createURI(
                    GitURIParser.GIT_COMMIT_SCHEME + "://" + repoPath + "/" + commitHash + "/" + COMMENTS_FILE_URI);
            ResourceSet resourceSet = createGitAwareResourceSet(commitHash, repoPath, modelResourceSets);
            Resource commentsResource = resourceSet.createResource(commentsUri);

            /* load the comments from the repository */
            commentsResource.load(null);
            final EList<EObject> content = commentsResource.getContents();

            /* add the loaded comments to the given model review */

            TransactionalEditingDomain editingDomain = findTransactionalEditingDomainFor(commentsResource);

            Command addCommentsCommand = new RecordingCommand(editingDomain) {

                @Override
                protected void doExecute() {

                    User reviewer = modelReview.getCurrentReviewer();

                    for (EObject object : content) {

                        if (object instanceof Comment) {

                            Comment comment = (Comment) object;
                            comment.resolvePatchSet(modelReview);
                            User author = comment.getAuthor();

                            if (isSameUser(reviewer, author)) {
                                // avoid duplicated users
                                comment.setAuthor(reviewer);
                            }

                            modelReview.getComments().add(comment);
                        }
                    }
                }

            };

            editingDomain.getCommandStack().execute(addCommentsCommand);

        } catch (IOException e) {
            throw new RepositoryIOException(MessageFormat.format(
                    "An IO error occured during loading the comments for the review with id #{0}",
                    modelReview.getId()), e);
        }
    }

    /**
     * finds the {@link TransactionalEditingDomain} for the given
     * {@link Resource} if there is one.
     * 
     * @param resource
     *            the resource to find the {@link TransactionalEditingDomain}
     *            for.
     * @return the {@link TransactionalEditingDomain} for the given resource or
     *         null if no {@link TransactionalEditingDomain} can be found.
     */
    private static TransactionalEditingDomain findTransactionalEditingDomainFor(Resource resource) {

        if (resource != null) {

            IEditingDomainProvider editingDomainProvider = (IEditingDomainProvider) EcoreUtil
                    .getExistingAdapter(resource, IEditingDomainProvider.class);
            if (editingDomainProvider != null) {

                EditingDomain editingDomain = editingDomainProvider.getEditingDomain();
                if (editingDomain instanceof TransactionalEditingDomain) {
                    return (TransactionalEditingDomain) editingDomain;
                }

            } else {
                return findTransactionalEditingDomainFor(resource.getResourceSet());
            }
        }

        return null;
    }

    /**
     * finds the {@link TransactionalEditingDomain} for the given
     * {@link ResourceSet} if there is one.
     * 
     * @param resourceSet
     *            the resource set to find the
     *            {@link TransactionalEditingDomain} for.
     * @return the {@link TransactionalEditingDomain} for the given resource set
     *         or null if no {@link TransactionalEditingDomain} can be found.
     */
    private static TransactionalEditingDomain findTransactionalEditingDomainFor(ResourceSet resourceSet) {

        IEditingDomainProvider editingDomainProvider;
        if (resourceSet instanceof IEditingDomainProvider) {

            EditingDomain editingDomain = ((IEditingDomainProvider) resourceSet).getEditingDomain();
            if (editingDomain instanceof TransactionalEditingDomain) {
                return (TransactionalEditingDomain) editingDomain;
            }

        } else if (resourceSet != null) {

            editingDomainProvider = (IEditingDomainProvider) EcoreUtil.getExistingAdapter(resourceSet,
                    IEditingDomainProvider.class);

            if (editingDomainProvider != null) {
                EditingDomain editingDomain = editingDomainProvider.getEditingDomain();
                if (editingDomain instanceof TransactionalEditingDomain) {
                    return (TransactionalEditingDomain) editingDomain;
                }
            }
        }

        return null;
    }

    /**
     * @param user1
     * @param user2
     * @return true if the given objects represent the same users, false
     *         otherwise.
     */
    private boolean isSameUser(User user1, User user2) {
        return user1 != null && user2 != null && user1.getName().equals(user2.getName());
    }

    @Override
    public void saveReview(URI uri, ModelReview modelReview, User currentReviewer, IProgressMonitor monitor)
            throws InvalidReviewRepositoryException, InvalidReviewException, RepositoryIOException {

        monitor.beginTask("Connecting to repository", IProgressMonitor.UNKNOWN);

        String repoFileURI = COMMENTS_FILE_URI;

        try {
            Git git = Git.open(new File(uri));
            Repository repository = git.getRepository();
            ObjectInserter objectInserter = repository.newObjectInserter();

            String commentRefName = getCommentRefName(modelReview);
            Ref commentRef = repository.exactRef(commentRefName);

            DirCache index = DirCache.newInCore();
            DirCacheBuilder dirCacheBuilder = index.builder();

            monitor.beginTask("Preparing commit...", IProgressMonitor.UNKNOWN);

            if (commentRef != null) {

                /*
                 * The ref already exists so we have to copy the previous
                 * RevTree to keep all already attached files
                 */

                RevWalk revWalk = new RevWalk(repository);
                RevCommit prevCommit = revWalk.parseCommit(commentRef.getObjectId());
                RevTree tree = prevCommit.getTree();

                List<String> ignoredFiles = new ArrayList<>();
                /*
                 * add file path of the new file to the ignored file paths, as
                 * we don't want any already existing old file in our new tree
                 */
                ignoredFiles.add(repoFileURI);
                buildDirCacheFromTree(tree, repository, dirCacheBuilder, ignoredFiles);

                revWalk.close();
            }

            monitor.beginTask("Writing comments file...", IProgressMonitor.UNKNOWN);

            ResourceSet resourceSet = new ResourceSetImpl();
            Resource resource = resourceSet.createResource(org.eclipse.emf.common.util.URI.createURI(repoFileURI));

            addCommentsToResource(modelReview, resource);

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            resource.save(outputStream, null);

            // insert file as object
            byte[] content = outputStream.toByteArray();
            long length = content.length;
            InputStream inputStream = new ByteArrayInputStream(content);
            ObjectId objectId = objectInserter.insert(Constants.OBJ_BLOB, length, inputStream);
            inputStream.close();

            // create tree entry
            DirCacheEntry entry = new DirCacheEntry(repoFileURI);
            entry.setFileMode(FileMode.REGULAR_FILE);
            entry.setLastModified(System.currentTimeMillis());
            entry.setLength(length);
            entry.setObjectId(objectId);
            dirCacheBuilder.add(entry);

            dirCacheBuilder.finish();

            // write new tree in database
            ObjectId indexTreeId = index.writeTree(objectInserter);

            monitor.beginTask("Commiting comments...", IProgressMonitor.UNKNOWN);

            // create commit
            CommitBuilder commitBuilder = new CommitBuilder();
            PersonIdent personIdent = new PersonIdent("Mervin", "mervin@mervin.modelreview");
            commitBuilder.setCommitter(personIdent);
            commitBuilder.setAuthor(personIdent);
            commitBuilder.setMessage(
                    MessageFormat.format("Updated comments by user \"{0}\"", currentReviewer.getName()));

            if (commentRef != null) {
                commitBuilder.setParentId(commentRef.getObjectId());
            }
            commitBuilder.setTreeId(indexTreeId);

            // commit
            ObjectId commitId = objectInserter.insert(commitBuilder);
            objectInserter.flush();

            RefUpdate refUpdate = repository.updateRef(commentRefName);
            refUpdate.setNewObjectId(commitId);
            if (commentRef != null)
                refUpdate.setExpectedOldObjectId(commentRef.getObjectId());
            else
                refUpdate.setExpectedOldObjectId(ObjectId.zeroId());

            /*
             * TODO the result handling below is copied from the CommitCommand
             * class, I don't know if this is really necessary in our case
             */
            Result result = refUpdate.forceUpdate();
            switch (result) {
            case NEW:
            case FORCED:
            case FAST_FORWARD: {
                if (repository.getRepositoryState() == RepositoryState.MERGING_RESOLVED) {
                    /*
                     * Commit was successful. Now delete the files used for
                     * merge commits
                     */
                    repository.writeMergeCommitMsg(null);
                    repository.writeMergeHeads(null);
                } else if (repository.getRepositoryState() == RepositoryState.CHERRY_PICKING_RESOLVED) {
                    repository.writeMergeCommitMsg(null);
                    repository.writeCherryPickHead(null);
                } else if (repository.getRepositoryState() == RepositoryState.REVERTING_RESOLVED) {
                    repository.writeMergeCommitMsg(null);
                    repository.writeRevertHead(null);
                }
                break;
            }
            case REJECTED:
            case LOCK_FAILURE:
                throw new RepositoryIOException("Error occured during writing to the git repository",
                        new ConcurrentRefUpdateException("Could not lock ref " + refUpdate.getRef().getName(),
                                refUpdate.getRef(), result));
            default:
                throw new RepositoryIOException("Error occured during writing to the git repository",
                        new JGitInternalException(MessageFormat.format(JGitText.get().updatingRefFailed,
                                refUpdate.getRef().getName(), commitId.toString(), result)));
            }

        } catch (IOException e) {
            throw new InvalidReviewRepositoryException("Could not open local git repository", e);
        } finally {
            monitor.done();
        }

    }

    /**
     * adds a copy of the comments and their respective authors of the given
     * model review to the given resource.
     * 
     * @param modelReview
     *            the review to copy the comments from.
     * @param resource
     *            the resource to store the copied objects to.
     */
    private void addCommentsToResource(ModelReview modelReview, Resource resource) {

        EList<Comment> originalComments = modelReview.getComments();
        Set<EObject> objectsToCopy = new HashSet<>();
        objectsToCopy.addAll(originalComments);
        objectsToCopy.addAll(modelReview.getPatchSets());

        Copier copier = new Copier();
        copier.copyAll(objectsToCopy);
        copier.copyReferences();

        List<Comment> comments = new ArrayList<>(originalComments.size());
        List<User> users = new LinkedList<>();
        for (Comment originalComment : originalComments) {

            Comment comment = (Comment) copier.get(originalComment);
            comments.add(comment);
            User author = comment.getAuthor();
            boolean skipAuthor = false;

            /*
             * replace the patchset with a reference to the patch set id, as the
             * patch set is derived from the repository in this case.
             */

            PatchSet patchset = comment.getPatchset();
            comment.setPatchSetRefId(patchset.getId());
            comment.setPatchset(null);

            for (User user : users) {
                if (isSameUser(user, author)) {
                    skipAuthor = true;
                    break;
                }
            }

            if (!skipAuthor) {
                users.add(author);
            }
        }

        resource.getContents().addAll(comments);
        resource.getContents().addAll(users);
    }

    /**
     * builds a dir cache by copying entries from an existing tree, skipping
     * files if specified.
     *
     * @param tree
     *            the tree to copy from.
     * @param repository
     *            the repository to work upon.
     * @param dirCacheBuilder
     *            the dir builder instance used to add entries to the dir cache.
     * @param ignoredFilePaths
     *            a list of file paths to ignore during copying.
     * @throws MissingObjectException
     * @throws IncorrectObjectTypeException
     * @throws CorruptObjectException
     * @throws IOException
     */
    private void buildDirCacheFromTree(RevTree tree, Repository repository, DirCacheBuilder dirCacheBuilder,
            List<String> ignoredFilePaths)
            throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException {
        // TODO improve exception handling

        TreeWalk treeWalk = new TreeWalk(repository);
        int treeId = treeWalk.addTree(tree);
        treeWalk.setRecursive(true);

        while (treeWalk.next()) {
            String path = treeWalk.getPathString();

            CanonicalTreeParser prevTreeParser = treeWalk.getTree(treeId, CanonicalTreeParser.class);
            if (prevTreeParser != null && !ignoredFilePaths.contains(path)) {
                // create a new DirCacheEntry with data from the previous commit

                final DirCacheEntry dcEntry = new DirCacheEntry(path);
                dcEntry.setObjectId(prevTreeParser.getEntryObjectId());
                dcEntry.setFileMode(prevTreeParser.getEntryFileMode());
                dirCacheBuilder.add(dcEntry);
            }
        }
        treeWalk.close();
    }

    /**
     * @param review
     *            the review to retrieve the ref name for.
     * @return the comment ref name for the given {@link ModelReview}.
     */
    private String getCommentRefName(ModelReview review) {
        return getCommentRefName(review.getId());
    }

    /**
     * @param changeId
     *            the id of the gerrit change (primary key of the gerrit
     *            change).
     * @return the comment ref name for the given changeId.
     */
    private String getCommentRefName(String changeId) {
        return COMMENTS_BASE_REF + "/" + changeId;
    }

}