Merge branch '552698-git' (fixes #552698)
diff --git a/features/org.hawk.git.feature/feature.xml b/features/org.hawk.git.feature/feature.xml
index 2acd80f..4f4c31d 100644
--- a/features/org.hawk.git.feature/feature.xml
+++ b/features/org.hawk.git.feature/feature.xml
@@ -3,20 +3,28 @@
       id="org.hawk.git.feature"
       label="Hawk Git Feature"
       version="1.2.0.qualifier"
-      provider-name="University of York">
+      provider-name="Eclipse.org">
 
    <description>
-     Provides the ability for Hawk to index local Git working copies.
+      Provides the ability for Hawk to index local Git working copies.
    </description>
 
    <copyright>
-     Copyright (C) 2015-2018 Universit of York, Aston University.
+      Copyright (C) 2015-2019 University of York, Aston University.
    </copyright>
 
    <license url="https://www.eclipse.org/legal/epl-2.0/">
       Eclipse Public License v2.0 with GPLv3.0 Secondary License
    </license>
 
+   <requires>
+      <import plugin="org.hawk.core"/>
+      <import plugin="org.slf4j.api" version="1.7.2" match="greaterOrEqual"/>
+      <import plugin="org.eclipse.jgit" version="5.0.0" match="greaterOrEqual"/>
+      <import plugin="org.apache.httpcomponents.httpclient" version="4.3.6" match="greaterOrEqual"/>
+      <import plugin="org.apache.httpcomponents.httpcore" version="4.3.3" match="greaterOrEqual"/>
+   </requires>
+
    <plugin
          id="org.hawk.git"
          download-size="0"
@@ -24,4 +32,11 @@
          version="0.0.0"
          unpack="false"/>
 
+   <plugin
+         id="org.hawk.jgit"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
 </feature>
diff --git a/plugins-server/org.hawk.service.server.logback/src/logback.xml b/plugins-server/org.hawk.service.server.logback/src/logback.xml
index e848a29..fd53756 100644
--- a/plugins-server/org.hawk.service.server.logback/src/logback.xml
+++ b/plugins-server/org.hawk.service.server.logback/src/logback.xml
@@ -43,7 +43,7 @@
 		<appender-ref ref="FILE" />
 	</logger>
 
-	<root level="debug">
+	<root level="info">
 		<appender-ref ref="STDOUT" />
 	</root>
 
diff --git a/plugins-server/org.hawk.service.servlet/src/org/hawk/service/servlet/config/ConfigFileParser.java b/plugins-server/org.hawk.service.servlet/src/org/hawk/service/servlet/config/ConfigFileParser.java
index 039a17c..2d66608 100644
--- a/plugins-server/org.hawk.service.servlet/src/org/hawk/service/servlet/config/ConfigFileParser.java
+++ b/plugins-server/org.hawk.service.servlet/src/org/hawk/service/servlet/config/ConfigFileParser.java
@@ -209,15 +209,10 @@
 			config.setFactory(hawkElement.hasAttribute(FACTORY) ? hawkElement.getAttribute(FACTORY) : null);

 

 			readDelay(hawkElement.getElementsByTagName(DELAY), config);

-

 			readPlugins(hawkElement.getElementsByTagName(PLUGINS), config);

-

 			readMetamodels(hawkElement.getElementsByTagName(METAMODELS), config);

-

 			readRepositories(hawkElement.getElementsByTagName(REPOSITORIES), config);

-

 			readIndexedAttributes(hawkElement.getElementsByTagName(INDEXED_ATTRIBUTES), config);

-

 			readDerivedAttributes(hawkElement.getElementsByTagName(DERIVED_ATTRIBUTES), config);

 		}

 	}

@@ -415,8 +410,8 @@
 				RepositoryParameters params = new RepositoryParameters(

 						repoElement.getAttribute(TYPE),

 						repoElement.getAttribute(LOCATION),

-						repoElement.getAttribute(USER),

-						repoElement.getAttribute(PASS),

+						repoElement.hasAttribute(USER) ? repoElement.getAttribute(USER) : null,

+						repoElement.hasAttribute(PASS) ? repoElement.getAttribute(PASS) : null,

 						Boolean.valueOf(repoElement.getAttribute(FROZEN)));

 

 				config.getRepositories().add(params);

@@ -483,7 +478,7 @@
 	}

 

 	private void createAndAddAttribute(Document document, Element element, String tagName, String value) {

-		if(value != null) {

+		if (value != null) {

 			Attr attr = document.createAttribute(tagName);

 			attr.setValue(value);

 			element.setAttributeNode(attr);

diff --git a/plugins/org.hawk.core/src/org/hawk/core/IVcsManager.java b/plugins/org.hawk.core/src/org/hawk/core/IVcsManager.java
index c337c8a..cd7ce7b 100644
--- a/plugins/org.hawk.core/src/org/hawk/core/IVcsManager.java
+++ b/plugins/org.hawk.core/src/org/hawk/core/IVcsManager.java
@@ -21,7 +21,6 @@
 import java.io.File;
 import java.util.Collection;
 
-import org.hawk.core.IHawkPlugin.Category;
 import org.hawk.core.model.IHawkObject;
 
 public interface IVcsManager extends IHawkPlugin {
@@ -37,12 +36,20 @@
 	String getFirstRevision() throws Exception;
 
 	/**
-	 * Returns the set of changed items from this revision.
+	 * Returns the set of changed items after this revision.
 	 */
 	Collection<VcsCommitItem> getDelta(String startRevision) throws Exception;
 
 	/**
-	 * Returns the set of changed items between these revisions.
+	 * Returns the set of changed items strictly after <code>startRevision</code>
+	 * until <code>endRevision</code> (included).
+	 *
+	 * @param startRevision Oldest revision in the range (excluded). Some special
+	 *                      values are allowed: both <code>null</code> and strings
+	 *                      with negative integers (e.g. "-3") mean "before the
+	 *                      first revision". This allows Hawk to fetch the changes
+	 *                      introduced by the very first revision.
+	 * @param endRevision   Newest revision in the range (included).
 	 */
 	VcsRepositoryDelta getDelta(String startRevision, String endRevision) throws Exception;
 
@@ -57,16 +64,16 @@
 	 *
 	 * Returns <code>null</code> if the file could not be found.
 	 *
-	 * @param revision
-	 *            Identifier of the desired revision of the specified file.
-	 *            Implementations that do not support retrieving past versions of
-	 *            the file may ignore this argument.
-	 * @param path
-	 *            Path within the repository where the file is stored.
-	 * @param optionalTemp
-	 *            If the file is not available as-is in the local filesystem (e.g.
-	 *            needs to be retrieved over the network), this argument provides a
-	 *            location where the implementation can temporarily save it.
+	 * @param revision     Identifier of the desired revision of the specified file.
+	 *                     Implementations that do not support retrieving past
+	 *                     versions of the file may ignore this argument.
+	 * @param path         Path within the repository where the file is stored.
+	 * @param optionalTemp If the file is not available as-is in the local
+	 *                     filesystem (e.g. needs to be retrieved over the network),
+	 *                     this argument provides a location where the
+	 *                     implementation can temporarily save it.
+	 * @return {@link File} object with the contents of the path, or
+	 *         <code>null</code> if the file could not be found.
 	 */
 	File importFile(String revision, String path, File optionalTemp);
 
diff --git a/plugins/org.hawk.core/src/org/hawk/core/runtime/BaseModelIndexer.java b/plugins/org.hawk.core/src/org/hawk/core/runtime/BaseModelIndexer.java
index 2b3b922..4bbb6f4 100644
--- a/plugins/org.hawk.core/src/org/hawk/core/runtime/BaseModelIndexer.java
+++ b/plugins/org.hawk.core/src/org/hawk/core/runtime/BaseModelIndexer.java
@@ -936,12 +936,16 @@
 
 	@Override
 	public boolean addGraphChangeListener(IGraphChangeListener changeListener) {
-		return listener.add(changeListener);
+		final boolean add = listener.add(changeListener);
+		changeListener.setModelIndexer(this);
+		return add;
 	}
 
 	@Override
 	public boolean removeGraphChangeListener(IGraphChangeListener changeListener) {
-		return listener.remove(changeListener);
+		final boolean remove = listener.remove(changeListener);
+		changeListener.setModelIndexer(null);
+		return remove;
 	}
 
 	@Override
diff --git a/plugins/org.hawk.graph.syncValidationListener/src/org/hawk/graph/syncValidationListener/SyncValidationListener.java b/plugins/org.hawk.graph.syncValidationListener/src/org/hawk/graph/syncValidationListener/SyncValidationListener.java
index 3010d2c..254421b 100644
--- a/plugins/org.hawk.graph.syncValidationListener/src/org/hawk/graph/syncValidationListener/SyncValidationListener.java
+++ b/plugins/org.hawk.graph.syncValidationListener/src/org/hawk/graph/syncValidationListener/SyncValidationListener.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2011-2016 The University of York.

+ * Copyright (c) 2011-2019 The University of York, Aston University.

  * 

  * This program and the accompanying materials are made available under the

  * terms of the Eclipse Public License 2.0 which is available at

@@ -13,6 +13,7 @@
  *

  * Contributors:

  *     Konstantinos Barmpis - initial API and implementation

+ *     Antonio Garcia-Dominguez - collect errors in list instead of printing them

  ******************************************************************************/

 package org.hawk.graph.syncValidationListener;

 

@@ -20,10 +21,12 @@
 import java.net.URI;

 import java.net.URISyntaxException;

 import java.net.URLDecoder;

+import java.util.ArrayList;

 import java.util.Arrays;

 import java.util.HashMap;

 import java.util.HashSet;

 import java.util.Iterator;

+import java.util.List;

 import java.util.Map;

 import java.util.Map.Entry;

 import java.util.Set;

@@ -50,8 +53,24 @@
 

 public class SyncValidationListener implements IGraphChangeListener {

 

+	public class ValidationError {

+		private final String message;

+

+		public ValidationError(String message) {

+			this.message = message;

+		}

+

+		public String getMessage() {

+			return message;

+		}

+	}

+

 	private ModelIndexerImpl hawk;

-	private int removedProxies, totalErrors, deleted, malformed, singletonCount, totalGraphSize, totalResourceSizes;

+

+	// stats

+	private List<ValidationError> errors = new ArrayList<>();

+	private int removedProxies, deleted, malformed, singletonCount, totalGraphSize, totalResourceSizes;

+

 	private Set<String> changed = new HashSet<>();

 

 	private IGraphNodeIndex singletonIndex;

@@ -65,8 +84,10 @@
 	public void setModelIndexer(IModelIndexer hawk) {

 		this.hawk = (ModelIndexerImpl) hawk;

 

-		System.err.println("SyncValidationListener: hawk.setSyncMetricsEnabled(true) called, performance will suffer!");

-		hawk.setSyncMetricsEnabled(true);

+		if (this.hawk != null) {

+			System.err.println("SyncValidationListener: hawk.setSyncMetricsEnabled(true) called, performance will suffer!");

+			hawk.setSyncMetricsEnabled(true);

+		}

 	}

 

 	@Override

@@ -82,7 +103,9 @@
 	@Override

 	public void synchroniseEnd() {

 		try {

-			validateChanges();

+			if (hawk != null) {

+				validateChanges();

+			}

 		} catch (URISyntaxException e) {

 			e.printStackTrace();

 		}

@@ -91,11 +114,17 @@
 		changed.clear();

 	}

 

+	public List<ValidationError> getErrors() {

+		return errors;

+	}

+

 	public int getTotalErrors() {

-		return totalErrors;

+		return errors.size();

 	}

 

 	private void validateChanges() throws URISyntaxException {

+		assert this.hawk != null : "validateChanges() should only be called if the indexer has been set";

+

 		System.err.println("sync metrics:");

 		System.err.println("interesting\t" + hawk.getInterestingFiles());

 		System.err.println("deleted\t\t" + hawk.getDeletedFiles());

@@ -107,8 +136,8 @@
 

 		System.err.println("validating changes...");

 

+		errors.clear();

 		removedProxies = 0;

-		totalErrors = 0;

 		malformed = 0;

 		singletonCount = 0;

 		totalResourceSizes = 0;

@@ -124,19 +153,20 @@
 		}

 

 		System.err.println("changed resource size: " + totalResourceSizes);

-

 		System.err.println("relevant graph size: "

 				+ totalGraphSize

 				+ (singletonCount > 0 ? (" + singleton count: " + singletonCount) : ""));

 

 		if (totalGraphSize + singletonCount != totalResourceSizes) {

-			totalErrors++;

+			errors.add(new ValidationError(

+				String.format("Mismatched resource size:  %d + %d != %d", totalGraphSize, singletonCount, totalResourceSizes)

+			));

 		}

 

 		System.err.println("validated changes... "

-				+ (totalErrors == 0 ? "true"

-						: ((totalErrors == malformed) + " (with "

-								+ totalErrors + " total and "

+				+ (errors.isEmpty() ? "true"

+						: ((errors.size() == malformed) + " (with "

+								+ errors.size() + " total and "

 								+ malformed + " malformed errors)"))

 				+ (removedProxies == 0 ? "" : " [" + removedProxies

 						+ "] unresolved hawk proxies matched"));

@@ -168,33 +198,36 @@
 				file = repository + GraphModelUpdater.FILEINDEX_REPO_SEPARATOR + c.getPath();

 				filenode = graph.getFileIndex().get("id", file).getSingle();

 			} catch (Exception ee) {

-				System.err.println("expected file " + file

-						+ " but it did not exist (maybe metamodel not registered, if so expect +1 errors)");

-				totalErrors++;

+				errors.add(new ValidationError(

+					String.format(

+						"Expected file %s but it did not exist "

+						+ "(maybe metamodel not registered, if so expect +1 errors)",

+						file)

+				));

 				return;

 			}

 

 			// cache model elements in current resource

-			Map<String, IHawkObject> eobjectCache = new HashMap<>();

+			Map<String, IHawkObject> eObjectCache = new HashMap<>();

 			// cache of malformed object identifiers (to ignore references)

 			Set<String> malformedObjectCache = new HashSet<>();

-			cacheModelElements(c, r, eobjectCache, malformedObjectCache);

+			cacheModelElements(c, r, eObjectCache, malformedObjectCache);

 

 			// go through all nodes in graph from the file the resource is in

 			for (IGraphEdge instanceEdge : filenode.getIncomingWithType(ModelElementNode.EDGE_LABEL_FILE)) {

 				final IGraphNode instance = instanceEdge.getStartNode();

 				totalGraphSize++;

 

-				final IHawkObject eobject = eobjectCache.get(instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));

+				final IHawkObject eobject = eObjectCache.get(instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));

 

 				// if a node cannot be found in the model cache

 				if (eobject == null) {

-					System.err.println("error in validating: graph contains node with identifier:"

-							+ instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY) + " but resource does not!");

 					// this triggers when a malformed model has 2 identical identifiers

-					totalErrors++;

+					errors.add(new ValidationError(

+						String.format("Graph contains node with identifier: %s but resource does not!", instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY))

+					));

 				} else {

-					eobjectCache.remove(instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));

+					eObjectCache.remove(instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));

 

 					if (!malformedObjectCache.contains(eobject.getUri())) {

 						compareAttributes(instance, eobject);

@@ -204,10 +237,10 @@
 			}

 

 			// if there are model elements not found in nodes

-			if (eobjectCache.size() > 0) {

-				System.err.println("error in validating: the following objects were not found in the graph:");

-				System.err.println(eobjectCache.keySet());

-				totalErrors++;

+			if (eObjectCache.size() > 0) {

+				errors.add(new ValidationError(

+					String.format("The following objects were not found in the graph:\n%s", eObjectCache.keySet())

+				));

 			}

 

 			t.success();

@@ -273,21 +306,25 @@
 			filterFragmentBasedReferences(noderefvaluesclone, modelrefvaluesclone);

 

 			if (noderefvaluesclone.size() > 0) {

-				System.err.println("error in validating: reference " + modelRefName + " of node: "

-						+ instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY) + "\nlocated: "

-						+ instance.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE).iterator().next().getEndNode()

-								.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));

-				System.err.println(noderefvaluesclone);

-				System.err.println("the above ids were found in the graph but not the model");

-				totalErrors++;

+				final IGraphNode fileNode = instance.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE).iterator().next().getEndNode();

+				errors.add(new ValidationError(

+					String.format(

+						"Reference %s of node: %s\n"

+						+ "Located: %s\n%s\n"

+						+ "The above IDs were found in the graph but not the model",

+						modelRefName, instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY),

+						fileNode, noderefvaluesclone)

+				));

 			}

 

 			if (modelrefvaluesclone.size() > 0) {

-				System.err.println("error in validating: reference " + modelRefName + " of model element: "

-						+ eobject.getUriFragment() + "\nlocated: " + eobject.getUri());

-				System.err.println(modelrefvaluesclone);

-				System.err.println("the above ids were found in the model but not the graph");

-				totalErrors++;

+				errors.add(new ValidationError(

+					String.format("Reference %s of model element: %s\n"

+							+ "Located: %s\n%s\n"

+							+ "The above IDs were found in the model but not the graph",

+							modelRefName, eobject.getUriFragment(),

+							eobject.getUri(), modelrefvaluesclone)

+				));

 			}

 

 			nodereferences.remove(modelRefName);

@@ -295,10 +332,10 @@
 		}

 

 		if (nodereferences.size() > 0) {

-			System.err.println("error in validating: references " + nodereferences.keySet()

-					+ " had targets in the graph but not in the model: ");

-			System.err.println(nodereferences);

-			totalErrors++;

+			errors.add(new ValidationError(String.format(

+				"References %s had targets in the graph but not in the model:\n%s",

+				nodereferences.keySet(), nodereferences)

+			));

 		}

 	}

 

@@ -396,35 +433,35 @@
 		for (String propertykey : node.getPropertyKeys()) {

 			if (!propertykey.equals(IModelIndexer.SIGNATURE_PROPERTY)

 					&& !propertykey.equals(IModelIndexer.IDENTIFIER_PROPERTY)

-					&& !propertykey.startsWith(GraphModelUpdater.PROXY_REFERENCE_PREFIX)) {

+					&& !propertykey.startsWith(GraphModelUpdater.PROXY_REFERENCE_PREFIX)

+					&& !propertykey.equals(GraphModelInserter.LAST_DERIVED_TSTAMP_NODEPROP)) {

 

 				Object dbattr = node.getProperty(propertykey);

 				Object attr = modelAttributes.get(propertykey);

 

 				if (!flattenedStringEquals(dbattr, attr)) {

-					totalErrors++;

-					System.err.println("error in validating, attribute: " + propertykey + " has values:");

-					final String cla1 = dbattr != null ? dbattr.getClass().toString() : "null attr";

-					System.err.println(String.format("database:\t%s JAVATYPE: %s IN NODE: %s WITH ID: %s",

-						(dbattr instanceof Object[] ? (Arrays.asList((Object[]) dbattr)) : dbattr),

-						cla1, node.getId(), node.getProperty(IModelIndexer.IDENTIFIER_PROPERTY)));

+					final String dbJavaType = dbattr != null ? dbattr.getClass().toString() : "null attr";

+					final Object dbValue = dbattr instanceof Object[] ? (Arrays.asList((Object[]) dbattr)) : dbattr;

+					final String modelJavaType = attr != null ? attr.getClass().toString() : "null attr";

+					final Object modelValue = attr instanceof Object[] ? (Arrays.asList((Object[]) attr)) : attr;

 

-					String cla2 = attr != null ? attr.getClass().toString() : "null attr";

-					System.err.println(String.format("model:\t\t%s JAVATYPE: %s IN ELEMENT WITH ID %s",

-						(attr instanceof Object[] ? (Arrays.asList((Object[]) attr)) : attr),

-						cla2, modelElement.getUriFragment()));

+					errors.add(new ValidationError(String.format(

+						"Attribute %s has mismatched values:\n"

+							+ " * database:\t%s JAVATYPE: %s IN NODE: %s WITH ID: %s\n"

+							+ " * model:\t\t%s JAVATYPE: %s IN ELEMENT WITH ID %s",

+						propertykey,

+						dbValue, dbJavaType, node.getId(), node.getProperty(IModelIndexer.IDENTIFIER_PROPERTY),

+						modelValue, modelJavaType, modelElement.getUriFragment()

+					)));

 				}

 

 				modelAttributes.remove(propertykey);

 			}

 		}

 		if (modelAttributes.size() > 0) {

-			System.err.println(String.format(

-				"error in validating, the following attributes were "

-				+ "not found in the graph node %s: %s",

-				node.getId(), modelAttributes.keySet()));

-

-			totalErrors++;

+			errors.add(new ValidationError(String.format(

+				"The following attributes were not found in the graph node %s: %s", node.getId(), modelAttributes.keySet()

+			)));

 		}

 	}

 

diff --git a/plugins/org.hawk.greycat/src/org/hawk/greycat/GreycatNode.java b/plugins/org.hawk.greycat/src/org/hawk/greycat/GreycatNode.java
index aaf411f..19f85bb 100644
--- a/plugins/org.hawk.greycat/src/org/hawk/greycat/GreycatNode.java
+++ b/plugins/org.hawk.greycat/src/org/hawk/greycat/GreycatNode.java
@@ -939,10 +939,14 @@
 			final Node n = rn.get();
 
 			CompletableFuture<long[]> result = new CompletableFuture<>();
-			n.timepoints(Constants.BEGINNING_OF_TIME,
-				getTime() > Constants.BEGINNING_OF_TIME ? getTime() - 1 : getTime(),
-				(value) -> result.complete(value)
-			);
+
+			/*
+			 * While the GreyCat javadocs say that both ends of the timepoints range are
+			 * inclusive, the TimeAwareBackendTest#nextPrev method showed that the end of
+			 * the range is exclusive.
+			 */
+			n.timepoints(Constants.BEGINNING_OF_TIME, getTime(),
+				(value) -> result.complete(value));
 
 			/*
 			 * We assume timepoints(...) produces elements from newest to oldest. The
diff --git a/plugins/org.hawk.http/pom-plain.xml b/plugins/org.hawk.http/pom-plain.xml
index 71429a1..04a3d17 100644
--- a/plugins/org.hawk.http/pom-plain.xml
+++ b/plugins/org.hawk.http/pom-plain.xml
@@ -23,7 +23,6 @@
     <dependency>
 	    <groupId>org.apache.httpcomponents</groupId>
 	    <artifactId>httpclient</artifactId>
-	    <version>4.3.6</version>
     </dependency>
   </dependencies>
 </project>
diff --git a/plugins/org.hawk.jgit/.classpath b/plugins/org.hawk.jgit/.classpath
new file mode 100644
index 0000000..6e16b23
--- /dev/null
+++ b/plugins/org.hawk.jgit/.classpath
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<classpath>

+	<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>

+	<classpathentry kind="src" path="src"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>

+	<classpathentry kind="output" path="bin"/>

+</classpath>

diff --git a/plugins/org.hawk.jgit/.project b/plugins/org.hawk.jgit/.project
new file mode 100644
index 0000000..629d3d5
--- /dev/null
+++ b/plugins/org.hawk.jgit/.project
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<projectDescription>

+	<name>org.hawk.jgit</name>

+	<comment></comment>

+	<projects>

+	</projects>

+	<buildSpec>

+		<buildCommand>

+			<name>org.eclipse.jdt.core.javabuilder</name>

+			<arguments>

+			</arguments>

+		</buildCommand>

+		<buildCommand>

+			<name>org.eclipse.pde.ManifestBuilder</name>

+			<arguments>

+			</arguments>

+		</buildCommand>

+		<buildCommand>

+			<name>org.eclipse.pde.SchemaBuilder</name>

+			<arguments>

+			</arguments>

+		</buildCommand>

+	</buildSpec>

+	<natures>

+		<nature>org.eclipse.pde.PluginNature</nature>

+		<nature>org.eclipse.jdt.core.javanature</nature>

+	</natures>

+</projectDescription>

diff --git a/plugins/org.hawk.jgit/META-INF/MANIFEST.MF b/plugins/org.hawk.jgit/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..c6be03a
--- /dev/null
+++ b/plugins/org.hawk.jgit/META-INF/MANIFEST.MF
@@ -0,0 +1,13 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Hawk JGit Plugin
+Bundle-SymbolicName: org.hawk.jgit;singleton:=true
+Bundle-Version: 1.2.0.qualifier
+Bundle-Vendor: Eclipse.org
+Require-Bundle: org.hawk.core,
+ org.slf4j.api;bundle-version="1.7.2",
+ org.eclipse.jgit;bundle-version="5.0.0",
+ org.apache.httpcomponents.httpclient;bundle-version="4.3.6",
+ org.apache.httpcomponents.httpcore;bundle-version="4.3.3"
+Bundle-RequiredExecutionEnvironment: JavaSE-1.7
+Export-Package: org.hawk.git
diff --git a/plugins/org.hawk.jgit/build.properties b/plugins/org.hawk.jgit/build.properties
new file mode 100644
index 0000000..6f20375
--- /dev/null
+++ b/plugins/org.hawk.jgit/build.properties
@@ -0,0 +1,5 @@
+source.. = src/

+output.. = bin/

+bin.includes = META-INF/,\

+               .,\

+               plugin.xml

diff --git a/plugins/org.hawk.jgit/plugin.xml b/plugins/org.hawk.jgit/plugin.xml
new file mode 100644
index 0000000..73387e4
--- /dev/null
+++ b/plugins/org.hawk.jgit/plugin.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<?eclipse version="3.4"?>

+<plugin>

+   <extension

+         point="org.hawk.core.VCSExtensionPoint">

+           

+               <VCS

+            VCSManager="org.hawk.git.JGitRepository">

+      </VCS>

+ </extension>

+

+</plugin>

diff --git a/plugins/org.hawk.jgit/pom-plain.xml b/plugins/org.hawk.jgit/pom-plain.xml
new file mode 100644
index 0000000..a648701
--- /dev/null
+++ b/plugins/org.hawk.jgit/pom-plain.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.eclipse.hawk</groupId>
+    <artifactId>mondo-hawk-plain</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <relativePath>../../pom-plain.xml</relativePath>
+  </parent>
+
+  <groupId>org.eclipse.hawk</groupId>
+  <artifactId>org.eclipse.hawk.jgit</artifactId>
+  <version>1.2.0-SNAPSHOT</version>
+  <packaging>jar</packaging>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>org.eclipse.hawk.core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jgit</groupId>
+      <artifactId>org.eclipse.jgit</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/plugins/org.hawk.jgit/src/org/hawk/git/JGitRepository.java b/plugins/org.hawk.jgit/src/org/hawk/git/JGitRepository.java
new file mode 100644
index 0000000..f683696
--- /dev/null
+++ b/plugins/org.hawk.jgit/src/org/hawk/git/JGitRepository.java
@@ -0,0 +1,511 @@
+/*******************************************************************************

+ * Copyright (c) 2011-2019 The University of York, Aston University, and others

+ * 

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * This Source Code may also be made available under the following Secondary

+ * Licenses when the conditions for such availability set forth in the Eclipse

+ * Public License, v. 2.0 are satisfied: GNU General Public License, version 3.

+ *

+ * SPDX-License-Identifier: EPL-2.0 OR GPL-3.0

+ *

+ * Contributors:

+ *     Konstantinos Barmpis - initial API and implementation

+ *     Antonio Garcia-Dominguez - use Java 7 Path instead of File+string processing

+ *     Horacio Hoyos Rodriguez - Add proper Git support (with code review from Antonio)

+ ******************************************************************************/

+package org.hawk.git;

+

+import java.io.File;

+import java.io.FileOutputStream;

+import java.io.IOException;

+import java.net.URI;

+import java.net.URISyntaxException;

+import java.nio.file.Path;

+import java.nio.file.Paths;

+import java.time.Duration;

+import java.time.Instant;

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.Date;

+import java.util.Iterator;

+import java.util.List;

+import java.util.ListIterator;

+

+import org.apache.http.NameValuePair;

+import org.apache.http.client.utils.URLEncodedUtils;

+import org.eclipse.jgit.api.Git;

+import org.eclipse.jgit.diff.DiffEntry;

+import org.eclipse.jgit.diff.DiffFormatter;

+import org.eclipse.jgit.errors.AmbiguousObjectException;

+import org.eclipse.jgit.errors.IncorrectObjectTypeException;

+import org.eclipse.jgit.errors.MissingObjectException;

+import org.eclipse.jgit.errors.RevisionSyntaxException;

+import org.eclipse.jgit.lib.ObjectId;

+import org.eclipse.jgit.lib.ObjectLoader;

+import org.eclipse.jgit.lib.ObjectReader;

+import org.eclipse.jgit.lib.PersonIdent;

+import org.eclipse.jgit.lib.Repository;

+import org.eclipse.jgit.revwalk.RevCommit;

+import org.eclipse.jgit.revwalk.RevSort;

+import org.eclipse.jgit.revwalk.RevWalk;

+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;

+import org.eclipse.jgit.treewalk.TreeWalk;

+import org.eclipse.jgit.util.io.NullOutputStream;

+import org.hawk.core.ICredentialsStore;

+import org.hawk.core.IModelIndexer;

+import org.hawk.core.IVcsManager;

+import org.hawk.core.VcsChangeType;

+import org.hawk.core.VcsCommit;

+import org.hawk.core.VcsCommitItem;

+import org.hawk.core.VcsRepositoryDelta;

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+

+/**

+ * <p>

+ * An implementation of {@link IVcsManager} that supports Git repositories.

+ * </p>

+ * <p>

+ * This implementation relies on JGit to provide its functionality. The Git tree

+ * and history is used to provide version information and to generate file

+ * contents.

+ * </p>

+ */

+public class JGitRepository implements IVcsManager {

+	

+	/**

+	 * Thrown if a revision cannot be found in the repository.

+	 */

+	public class UnableToFindRevisionException extends Exception {

+		private static final long serialVersionUID = -7277359120689923918L;

+

+		public UnableToFindRevisionException(String message, Throwable cause) {

+			super(message, cause);

+		}

+

+	}

+	

+	/**

+	 * Thrown if a delta is requested for revisions that do not belong to the same ancestry.

+	 */

+	public class UnmergedAncestryException extends Exception {

+		private static final long serialVersionUID = -1805916162822065083L;

+

+		public UnmergedAncestryException(String message) {

+			super(message);

+		}

+	}

+

+	/**

+	 * Name of the query parameter that can be used to specify an alternate branch.

+	 */

+	static final String BRANCH_QPARAM = "branch";

+

+	private static final Logger LOG = LoggerFactory.getLogger(JGitRepository.class);

+

+	private String branch;

+

+	private Repository repository;

+	private String firstRevision;

+	private Path rootLocation;

+	private boolean active;

+	private boolean isFrozen;

+

+	@Override

+	public String getCurrentRevision() throws Exception {

+		try {

+			final ObjectId oid = repository.resolve(branch);

+			return ObjectId.toString(oid);

+		} catch (RevisionSyntaxException | AmbiguousObjectException | IncorrectObjectTypeException e) {

+			throw new IllegalStateException("Unexpected exception", e);

+		} catch (IOException e) {

+			throw new UnableToFindRevisionException("Unable to resolve the tip of branch " + branch, e);

+		}

+	}

+

+	@Override

+	public synchronized String getFirstRevision() throws Exception {

+		if (firstRevision == null) {

+			repository.scanForRepoChanges();

+			try (RevWalk walk = new RevWalk(repository)) {

+				walk.markStart(walk.parseCommit(repository.resolve(branch)));

+				walk.sort(RevSort.COMMIT_TIME_DESC, true);

+				walk.sort(RevSort.REVERSE, true);

+				RevCommit commit = walk.next();

+				firstRevision = ObjectId.toString(commit.getId());

+			} catch (IOException e) {

+				throw new UnableToFindRevisionException("Unable to resolve the first revision", e);

+			}

+		}

+		return firstRevision;

+	}

+

+	@Override

+	public List<VcsCommitItem> getDelta(String startRevision) throws Exception {

+		if (startRevision != null && startRevision.startsWith("-")) {

+			startRevision = null;

+		}

+		return getDelta(startRevision, getCurrentRevision()).getCompactedCommitItems();

+	}

+

+	@Override

+	public VcsRepositoryDelta getDelta(String startRevision, String endRevision) throws Exception {

+		if (startRevision == null) {

+			// nothing to do here!

+		} else if (startRevision.startsWith("-")) {

+			startRevision = null;

+		} else {

+			checkRangeAncestry(startRevision, endRevision);

+		}

+

+		VcsRepositoryDelta delta = new VcsRepositoryDelta();

+		delta.setManager(this);

+		try (Git git = Git.open(rootLocation.toFile())) {

+			Iterable<RevCommit> revs;

+			if (startRevision == null) {

+				revs = git.log().add(ObjectId.fromString(endRevision)).call();

+			} else {

+				revs = git.log().addRange(

+					ObjectId.fromString(startRevision),

+					ObjectId.fromString(endRevision)

+				).call();

+			}

+

+			// Generate deltas between revisions

+			List<VcsCommit> commits = new ArrayList<>();

+			RevCommit previous = null;

+			for (RevCommit rev : revs) {

+				if (previous == null && rev.getParentCount() > 0) {

+					previous = rev.getParent(0);

+				}

+

+				diff(rev, previous).stream()

+					.findFirst().ifPresent(c -> commits.add(c.getCommit()));

+

+				previous = rev;

+			}

+

+			/*

+			 * 'git log' returns revisions from newest to oldest, but the Hawk core expects

+			 * them from oldest to newest.

+			 */

+			for (ListIterator<VcsCommit> itCommit = commits.listIterator(commits.size()); itCommit.hasPrevious(); ) {

+				final VcsCommit commit = itCommit.previous();

+				commit.setDelta(delta);

+				delta.getCommits().add(commit);

+			}

+		}

+

+		return delta;

+	}

+

+	private void checkRangeAncestry(String startRevision, String endRevision)

+			throws UnmergedAncestryException, UnableToFindRevisionException {

+		try (RevWalk walk = new RevWalk(repository)) {

+			RevCommit startCommit = walk.parseCommit(repository.resolve(startRevision));

+			RevCommit endCommit = walk.parseCommit(repository.resolve(endRevision));

+			if (!walk.isMergedInto(startCommit, endCommit)) {

+				throw new UnmergedAncestryException(

+						String.format("Revision %s (end) is in a branch not merged "

+						+ "into the branch that contains revision %s (start).", endRevision, startRevision));

+			}

+		} catch (IOException e) {

+			throw new UnableToFindRevisionException("Unable to resolve the delta of revision", e);

+		}

+	}

+	

+

+	@Override

+	public File importFile(String revision, String path, File optionalTemp) {

+		if (path.startsWith("/")) {

+			// Strip out starting / from the paths produced by diff(), if present

+			path = path.substring(1);

+		}

+

+		// Do a TreeWalk over the tree of the commit pointed by the provided revision

+		try (RevWalk revWalk = new RevWalk(repository)) {

+			try (TreeWalk treeWalk = TreeWalk.forPath(

+					repository,

+					path,

+					revWalk.parseCommit(ObjectId.fromString(revision)).getTree())) {

+				if (treeWalk == null) {

+					LOG.warn("Could not find path '{}' on revision {} of the Git repository at '{}'", path, revision, rootLocation);

+					return null;

+				}

+

+				ObjectId blobId = treeWalk.getObjectId(0);

+				try (ObjectReader objectReader = repository.newObjectReader()) {

+					ObjectLoader objectLoader = objectReader.open(blobId);

+					byte[] bytes = objectLoader.getBytes();

+					try (FileOutputStream fOS = new FileOutputStream(optionalTemp)) {

+						fOS.write(bytes);

+					}

+					catch (Exception e) {

+						LOG.error("There was an error writing the contents of the file in the repository into the provided file.", e);

+					}

+				}

+			}

+			catch (Exception e) {

+				LOG.error("There was an error traversing the Git tree to retrieve the file contents.", e);

+			}

+		} catch (Exception e) {

+			LOG.error("There was an error accessing the Git repository to retrieve the file contents.", e);

+		}

+		return optionalTemp;

+	}

+	

+	@Override

+	public boolean isActive() {

+		return active;

+	}

+	

+	/**

+	 * Prepares this manager to be run. Always invoked before {@link #run()}.

+	 *

+	 * @param vcsloc Path or <code>file://</code> URL to the root folder of the

+	 *               repository. If using a <code>file://</code> URL, the branch to

+	 *               be indexed can be specified via <code>?branch=BRANCH</code>.

+	 * @param indexer Hawk indexer that will manage this VCS.

+	 */

+	@Override

+	public void init(String vcsloc, IModelIndexer indexer) throws Exception {

+		// Accept both regular paths and file:// URIs

+		Path path;

+		try {

+			final URI uri = new URI(vcsloc);

+

+			/*

+			 * This is needed to remove the query part cleanly (Paths.get will complain otherwise).

+			 */

+			path = Paths.get(uri.resolve("."));

+

+			List<NameValuePair> pairs = URLEncodedUtils.parse(uri, "UTF-8");

+			for (NameValuePair pair : pairs) {

+				if (BRANCH_QPARAM.equals(pair.getName())) {

+					branch = pair.getValue();

+				}

+			}

+		} catch (URISyntaxException | IllegalArgumentException ex) {

+			path = Paths.get(vcsloc);

+		}

+

+		File canonicalFile;

+		try {

+			canonicalFile = path.toFile().getCanonicalFile();

+		} catch (IOException e) {

+			throw new IllegalArgumentException(String.format("Unable to access '%s' as a local folder", vcsloc));

+		}

+		if (!canonicalFile.isDirectory()) {

+			throw new IllegalArgumentException(String.format(

+				"The location pointed to by %s is not a folder", vcsloc));

+		}

+

+		rootLocation = canonicalFile.toPath();

+

+		// Assess if the vcsloc is actually a git repo

+		FileRepositoryBuilder builder = new FileRepositoryBuilder().addCeilingDirectory(canonicalFile.getParentFile())

+				.findGitDir(canonicalFile);

+		if (builder.getGitDir() == null) {

+			throw new IllegalArgumentException(

+					String.format("The location pointed to by %s doesn't appear to be a valid Git "

+							+ "repository (.git folder not found or invalid).", vcsloc));

+		}

+

+		repository = builder.setMustExist(true).build();

+		if (branch == null) {

+			/*

+			 * If the user has not specified a branch by using

+			 * <code>file:///path/to/root?branch=BRANCH</code>, then we will use the

+			 * currently checked out branch.

+			 */

+			branch = repository.getFullBranch();

+		}

+	}

+	

+	@Override

+	public void run() {

+		active = true;

+	}

+	

+	@Override

+	public void shutdown() {

+		rootLocation = null;

+		repository.close();

+		active = false;

+	}

+	

+	@Override

+	public String getLocation() {

+		return rootLocation.toString();

+	}

+	

+	/**

+	 * Git repositories do <b>NOT</b> support authentication. Calling any of the authentication related

+	 * methods will throw an {@link UnsupportedOperationException}

+	 * 

+	 * @see #getUsername()

+	 * @see #getPassword()

+	 * @see #setCredentials(String, String, ICredentialsStore)

+	 */

+	@Override

+	public boolean isAuthSupported() {

+		return false;

+	}

+	

+	/**

+	 * Git repositories do <b>NOT</b> support authentication. This method always throws {@link UnsupportedOperationException}

+	 * @see #isAuthSupported()

+	 */

+	@Override

+	public String getUsername() {

+		throw new UnsupportedOperationException("Git repository does not support authentication.");

+	}

+

+	/**

+	 * Git repositories do <b>NOT</b> support authentication. This method always throws {@link UnsupportedOperationException}

+	 * @see #isAuthSupported()

+	 */

+	@Override

+	public String getPassword() {

+		throw new UnsupportedOperationException("Git repository does not support authentication.");

+	}

+	

+	/**

+	 * Git repositories do <b>NOT</b> support authentication. This method always throws {@link UnsupportedOperationException}

+	 * @see #isAuthSupported()

+	 */

+	@Override

+	public void setCredentials(String username, String password, ICredentialsStore credStore) {

+		throw new UnsupportedOperationException("Git repository does not support authentication.");

+	}

+	

+	

+	@Override

+	public String getHumanReadableName() {

+		return "Git Repository (JGit-based)";

+	}

+

+	@Override

+	public boolean isPathLocationAccepted() {

+		return true;

+	}

+

+	@Override

+	public boolean isURLLocationAccepted() {

+		return false;

+	}

+	

+	@Override

+	public String getRepositoryPath(String rawPath) {

+		return rootLocation.relativize(Paths.get(rawPath)).toString();

+	}

+	

+	@Override

+	public boolean isFrozen() {

+		return isFrozen;

+	}

+

+	@Override

+	public void setFrozen(boolean f) {

+		isFrozen = f;

+	}

+	

+	/**

+	 * Create the set of VcsCommitItems between the two commits.

+	 * <p>

+	 * The previous commit can be <b>null</b> in which case the changes will be reported against an 

+	 * empty tree. This is useful when getting the differences of the first commit.

+	 *  

+	 * @param current			the current commit

+	 * @param previous			the previous commit (can be null)

+	 * @return a list of VcsCommitItems

+	 * @throws IOException if there is an error accessing the git information

+	 */

+	private Collection<VcsCommitItem> diff(RevCommit current, RevCommit previous) throws IOException {

+		Collection<VcsCommitItem> result = new ArrayList<VcsCommitItem>();

+		try (DiffFormatter diffFmt = new DiffFormatter(NullOutputStream.INSTANCE)) {			

+			diffFmt.setRepository(repository);

+			VcsCommit commit = asVcsCommit(current);

+			for (DiffEntry diff : diffFmt.scan(

+					previous == null ? null : previous.getTree(),

+					current.getTree())) {

+				VcsCommitItem item = new VcsCommitItem();

+				switch (diff.getChangeType()) {

+				case ADD:

+					item.setChangeType(VcsChangeType.ADDED);

+					item.setPath("/" + diff.getNewPath());

+					break;

+				case DELETE:

+					item.setChangeType(VcsChangeType.DELETED);

+					item.setPath("/" + diff.getOldPath());

+					break;

+				case MODIFY:

+					item.setChangeType(VcsChangeType.UPDATED);

+					item.setPath("/" + diff.getNewPath());

+					break;

+				case RENAME:

+					item.setChangeType(VcsChangeType.REPLACED);

+					item.setPath("/" + diff.getNewPath());

+					break;

+				case COPY:

+					item.setChangeType(VcsChangeType.UNKNOWN);

+				default:

+					break;

+				}

+				item.setCommit(commit);

+				commit.getItems().add(item);

+				result.add(item);

+			}

+			return result;

+		}

+	}

+

+	private VcsCommit asVcsCommit(RevCommit current) throws MissingObjectException, IncorrectObjectTypeException, IOException {

+		final PersonIdent authorIdent = current.getAuthorIdent();

+

+		/*

+		 * Git's commit timestamps have a 1 second resolution, meaning that if we commit

+		 * very quickly in sequence (within the same second), the temporal graph may

+		 * actually lose versions.

+		 *

+		 * As a workaround, we can count how many commits before the current one have

+		 * the same timestamp, and add that number of milliseconds to the VcsCommit

+		 * timestamp. This means we'd only run afoul of this limitation if we managed to

+		 * commit more than 1000 times in a second (which is normally very unlikely).

+		 */

+		Date commitDate = authorIdent.getWhen();

+		if (current.getParentCount() > 0) {

+			try (RevWalk walk = new RevWalk(repository)) {

+				walk.markStart(walk.parseCommit(current.getId()));

+

+				int nCommitsSameTimestamp = 0;

+				final Iterator<RevCommit> itCommit = walk.iterator();

+				while (itCommit.hasNext()) {

+					RevCommit ancestor = itCommit.next();

+					if (ancestor.getAuthorIdent().getWhen().equals(commitDate)) {

+						++nCommitsSameTimestamp;

+					} else {

+						break;

+					}

+				}

+

+				if (nCommitsSameTimestamp > 0) {

+					Instant newInstant = commitDate.toInstant().plus(Duration.ofMillis(nCommitsSameTimestamp));

+					commitDate = Date.from(newInstant);

+				}

+			}

+		}

+

+		VcsCommit commit = new VcsCommit();

+		commit.setAuthor(authorIdent.getName());

+		commit.setJavaDate(commitDate);

+		commit.setMessage(current.getFullMessage());

+		commit.setRevision(current.getName());

+

+		return commit;

+	}

+	

+}

diff --git a/plugins/org.hawk.svn/src/org/hawk/svn/SvnManager.java b/plugins/org.hawk.svn/src/org/hawk/svn/SvnManager.java
index 0a3e68b..a7cbc99 100644
--- a/plugins/org.hawk.svn/src/org/hawk/svn/SvnManager.java
+++ b/plugins/org.hawk.svn/src/org/hawk/svn/SvnManager.java
@@ -280,7 +280,7 @@
 	@Override
 	public Collection<VcsCommitItem> getDelta(String startRevision) throws Exception {
 		if (Long.valueOf(startRevision) < 0)
-			return getDelta(getFirstRevision(), getCurrentRevision()).getCompactedCommitItems();
+			return getDelta(null, getCurrentRevision()).getCompactedCommitItems();
 		else
 			return getDelta(startRevision, getCurrentRevision()).getCompactedCommitItems();
 	}
diff --git a/pom-plain.xml b/pom-plain.xml
index 1bfa932..833a320 100644
--- a/pom-plain.xml
+++ b/pom-plain.xml
@@ -20,6 +20,7 @@
     <module>plugins/org.hawk.git/pom-plain.xml</module>
     <module>plugins/org.hawk.graph/pom-plain.xml</module>
     <module>plugins/org.hawk.http/pom-plain.xml</module>
+    <module>plugins/org.hawk.jgit/pom-plain.xml</module>
     <module>plugins/org.hawk.localfolder/pom-plain.xml</module>
     <module>plugins/org.hawk.modelio.exml/pom-plain.xml</module>
     <module>plugins/org.hawk.orientdb/pom-plain.xml</module>
@@ -53,6 +54,16 @@
 	      <artifactId>slf4j-api</artifactId>
 	      <version>1.7.7</version>
       </dependency>
+      <dependency>
+        <groupId>org.eclipse.jgit</groupId>
+        <artifactId>org.eclipse.jgit</artifactId>
+        <version>5.5.0.201909110433-r</version>
+      </dependency>
+      <dependency>
+	      <groupId>org.apache.httpcomponents</groupId>
+	      <artifactId>httpclient</artifactId>
+	      <version>4.3.6</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
diff --git a/pom.xml b/pom.xml
index e4cb921..91da5dd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,7 @@
     <module>plugins/org.hawk.graph</module>
     <module>plugins/org.hawk.greycat</module>
     <module>plugins/org.hawk.http</module>
+    <module>plugins/org.hawk.jgit</module>
     <module>plugins/org.hawk.localfolder</module>
     <module>plugins/org.hawk.manifest</module>
     <module>plugins/org.hawk.modelio.exml.ecoregen</module>
@@ -102,6 +103,7 @@
     <module>tests/org.hawk.backend.tests</module>
     <module>tests/org.hawk.emf.tests</module>
     <module>tests/org.hawk.integration.tests</module>
+    <module>tests/org.hawk.jgit.tests</module>
     <module>tests/org.hawk.localfolder.tests</module>
     <module>tests/org.hawk.manifest.tests</module>
     <module>tests/org.hawk.modelio.exml.tests</module>
diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/ModelIndexingTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/ModelIndexingTest.java
index 6620e24..6592c96 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/ModelIndexingTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/ModelIndexingTest.java
@@ -172,26 +172,26 @@
 		db.delete();
 	}
 
-	protected void waitForSync() throws Throwable {
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				// nothing to do
-				return null;
-			}
-		});
+	/**
+	 * Simple version which just waits for the indexing to happen.
+	 */
+	protected void scheduleAndWait() throws Throwable {
+		scheduleAndWait(() -> null);
 	}
 
-	protected void waitForSync(final Callable<?> r) throws Throwable {
+	/**
+	 * Schedules a piece of code on the same thread as Hawk's indexing, and waits
+	 * for it to be run. Times out with a test failure after 10 minutes.
+	 */
+	protected void scheduleAndWait(final Callable<?> r) throws Throwable {
 		final Semaphore sem = new Semaphore(0);
-		final SyncEndListener changeListener = new SyncEndListener(r, sem);
-		indexer.addGraphChangeListener(changeListener);
+		final ScheduledTask<?> task = new ScheduledTask<>(r, sem);
+		indexer.scheduleTask(task, 0);
 		if (!sem.tryAcquire(600, TimeUnit.SECONDS)) {
 			fail("Synchronization timed out");
 		} else {
-			indexer.removeGraphChangeListener(changeListener);
-			if (changeListener.getThrowable() != null) {
-				throw changeListener.getThrowable();
+			if (task.getThrowable() != null) {
+				throw task.getThrowable();
 			}
 		}
 	}
diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/SyncEndListener.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/ScheduledTask.java
similarity index 71%
rename from tests/org.hawk.integration.tests/src/org/hawk/integration/tests/SyncEndListener.java
rename to tests/org.hawk.integration.tests/src/org/hawk/integration/tests/ScheduledTask.java
index 2dd1fa4..831007d 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/SyncEndListener.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/ScheduledTask.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2015-2017 The University of York, Aston University.
+ * Copyright (c) 2019 The University of York, Aston University.
  * 
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License 2.0 which is available at
@@ -19,32 +19,28 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.Semaphore;
 
-import org.hawk.core.util.GraphChangeAdapter;
-
-/**
- * Simple listener that allows for blocking another thread until a
- * synchronisation has been completed.
- */
-public class SyncEndListener extends GraphChangeAdapter {
-	private final Callable<?> r;
+public class ScheduledTask<T> implements Callable<T> {
+	private final Callable<T> r;
 	private final Semaphore sem;
 	private Throwable ex = null;
 
-	public SyncEndListener(Callable<?> r, Semaphore sem) {
-		this.r = r;
+	public ScheduledTask(Callable<T> wrapped, Semaphore sem) {
+		this.r = wrapped;
 		this.sem = sem;
 	}
 
 	@Override
-	public void synchroniseEnd() {
+	public T call() throws Exception {
+		T result = null;
 		try {
 			if (r != null) {
-				r.call();
+				result = r.call();
 			}
 		} catch (Throwable e) {
 			ex = e;
 		}
 		sem.release();
+		return result;
 	}
 
 	public Throwable getThrowable() {
diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/bpmn/ModelVersioningTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/bpmn/ModelVersioningTest.java
index 7e9198d..d6b5b93 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/bpmn/ModelVersioningTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/bpmn/ModelVersioningTest.java
@@ -62,12 +62,9 @@
 		Files.copy(new File("resources/models/" + baseModel).toPath(), modelPath);

 		requestFolderIndex(modelFolder.getRoot());

 

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-				return null;

-			}

+		scheduleAndWait(() -> {

+			assertEquals(0, syncValidation.getListener().getTotalErrors());

+			return null;

 		});

 	}

 

@@ -85,7 +82,7 @@
 		for (int i = 1; i <= 8; i++) {

 			replaceWith("bpmn/v" + i + "-B.2.0.bpmn");

 			indexer.requestImmediateSync();

-			waitForSync(noErrors);

+			scheduleAndWait(noErrors);

 		}

 	}

 

diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/CountInstancesTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/CountInstancesTest.java
index 4781a53..35587b1 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/CountInstancesTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/CountInstancesTest.java
@@ -22,7 +22,6 @@
 import java.io.File;

 import java.util.Arrays;

 import java.util.Collection;

-import java.util.concurrent.Callable;

 

 import org.hawk.backend.tests.BackendTestSuite;

 import org.hawk.backend.tests.factories.IGraphDatabaseFactory;

@@ -58,16 +57,13 @@
 			new File("resources/metamodels/Tree.ecore"));

 		requestFolderIndex(new File("resources/models/tree"));

 

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-				assertEquals(2, eol("return Tree.all.size;"));

-				assertEquals("t3", eol("return Tree.all.selectOne(t|t.label='t9000').eContainer.label;"));

-				assertEquals(0, eol("return Tree.all.selectOne(t|t.label='t3').eContainers.size;"));

-				assertEquals(1, eol("return Tree.all.selectOne(t|t.label='t9000').eContainers.size;"));

-				return null;

-			}

+		scheduleAndWait(() -> {

+			assertEquals(0, syncValidation.getListener().getTotalErrors());

+			assertEquals(2, eol("return Tree.all.size;"));

+			assertEquals("t3", eol("return Tree.all.selectOne(t|t.label='t9000').eContainer.label;"));

+			assertEquals(0, eol("return Tree.all.selectOne(t|t.label='t3').eContainers.size;"));

+			assertEquals(1, eol("return Tree.all.selectOne(t|t.label='t9000').eContainers.size;"));

+			return null;

 		});

 	}

 

@@ -78,27 +74,23 @@
 			new File("resources/metamodels/Tree.ecore"));

 		requestFolderIndex(new File("resources/models/tree-xres"));

 

-		waitForSync(new Callable<Object>() {

-			@SuppressWarnings("unchecked")

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-

-				// Test for bug #56: select(t:Type|xyz) does not work

-				assertEquals(3, eol("return Model.allContents.select(t:Tree|true).size;"));

-

-				final Collection<String> labels = (Collection<String>) eol("return Model.allContents.collect(t:Tree|t.label);");

-				assertEquals(3, labels.size());

-				for (String e : Arrays.asList("xyz", "root", "abc")) {

-					assertTrue(labels.contains(e));

-				}

-

-				assertEquals(3, eol("return Tree.all.size;"));

-				assertEquals(2, eol("return Tree.all.selectOne(t|t.label='root').children.size;"));

-				assertEquals("root", eol("return Tree.all.selectOne(t|t.label='xyz').eContainer.label;"));

-				assertEquals("root", eol("return Tree.all.selectOne(t|t.label='abc').eContainer.label;"));

-				return null;

+		scheduleAndWait(() -> {

+			assertEquals(0, syncValidation.getListener().getTotalErrors());

+		

+			// Test for bug #56: select(t:Type|xyz) does not work

+			assertEquals(3, eol("return Model.allContents.select(t:Tree|true).size;"));

+		

+			final Collection<String> labels = (Collection<String>) eol("return Model.allContents.collect(t:Tree|t.label);");

+			assertEquals(3, labels.size());

+			for (String e : Arrays.asList("xyz", "root", "abc")) {

+				assertTrue(labels.contains(e));

 			}

+		

+			assertEquals(3, eol("return Tree.all.size;"));

+			assertEquals(2, eol("return Tree.all.selectOne(t|t.label='root').children.size;"));

+			assertEquals("root", eol("return Tree.all.selectOne(t|t.label='xyz').eContainer.label;"));

+			assertEquals("root", eol("return Tree.all.selectOne(t|t.label='abc').eContainer.label;"));

+			return null;

 		});

 	}

 

@@ -109,18 +101,15 @@
 			new File("resources/metamodels/JDTAST.ecore"));

 

 		requestFolderIndex(new File("resources/models/set0"));

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-				assertEquals(1, eol("return IJavaProject.all.size;"));

+		scheduleAndWait(() -> {

+			assertEquals(0, syncValidation.getListener().getTotalErrors());

+			assertEquals(1, eol("return IJavaProject.all.size;"));

+		

+			final int reportedSize = (Integer) eol("return TypeDeclaration.all.size;");

+			final Collection<?> actualList = (Collection<?>) eol("return TypeDeclaration.all;");

+			assertEquals(reportedSize, actualList.size());

 

-				final int reportedSize = (Integer) eol("return TypeDeclaration.all.size;");

-				final Collection<?> actualList = (Collection<?>) eol("return TypeDeclaration.all;");

-				assertEquals(reportedSize, actualList.size());

-

-				return null;

-			}

+			return null;

 		});

 	}

 

@@ -138,16 +127,13 @@
 		});

 		vcs.run();

 		indexer.addVCSManager(vcs, true);

-		

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-				assertEquals(1, eol("return Tree.all.size;"));

-				assertEquals(1, eol("return Model.getAllOf('Tree', 'Tree', '/tree with spaces/space tree.model').size;"));

-				assertEquals(1, eol("return Model.getAllOf('Tree', 'Tree', '/tree%20with%20spaces/space%20tree.model').size;"));

-				return null;

-			}

+

+		scheduleAndWait(() -> {

+			assertEquals(0, syncValidation.getListener().getTotalErrors());

+			assertEquals(1, eol("return Tree.all.size;"));

+			assertEquals(1, eol("return Model.getAllOf('Tree', 'Tree', '/tree with spaces/space tree.model').size;"));

+			assertEquals(1, eol("return Model.getAllOf('Tree', 'Tree', '/tree%20with%20spaces/space%20tree.model').size;"));

+			return null;

 		});

 	}

 }

diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/DerivedFeatureTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/DerivedFeatureTest.java
index cab640b..8e68f77 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/DerivedFeatureTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/DerivedFeatureTest.java
@@ -27,7 +27,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.concurrent.Callable;
 
 import org.hawk.backend.tests.BackendTestSuite;
 import org.hawk.backend.tests.factories.IGraphDatabaseFactory;
@@ -53,61 +52,60 @@
 				new File("resources/metamodels/Tree.ecore"));
 		requestFolderIndex(new File("resources/models/tree-dedges"));
 
-		waitForSync(new Callable<Object>() {
-			@SuppressWarnings("unchecked")
-			@Override
-			public Object call() throws Exception {
-				indexer.addDerivedAttribute("Tree", "Tree", "descendants", "dummy", true, true, false,
-						EOLQueryEngine.TYPE, "return self.closure(c|c.children).flatten;");
+		scheduleAndWait(() -> {
+			indexer.addDerivedAttribute("Tree", "Tree", "descendants", "dummy", true, true, false, EOLQueryEngine.TYPE,
+					"return self.closure(c|c.children).flatten;");
 
-				// Forward derived edges
-				{
-					final Map<String, Integer> expected = new HashMap<>();
-					expected.put("t3", 3);
-					expected.put("t4", 1);
-					expected.put("t5", 0);
-					expected.put("t6", 0);
+			// Forward derived edges
+			{
+				final Map<String, Integer> expected = new HashMap<>();
+				expected.put("t3", 3);
+				expected.put("t4", 1);
+				expected.put("t5", 0);
+				expected.put("t6", 0);
 
-					for (Entry<String, Integer> entry : expected.entrySet()) {
-						final Map<String, Object> context = Collections.singletonMap(EOLQueryEngine.PROPERTY_ARGUMENTS,
-								(Object) Collections.singletonMap("nodeLabel", entry.getKey()));
+				for (Entry<String, Integer> entry : expected.entrySet()) {
+					final Map<String, Object> context = Collections.singletonMap(EOLQueryEngine.PROPERTY_ARGUMENTS,
+							(Object) Collections.singletonMap("nodeLabel", entry.getKey()));
 
-						Object result = eol("return Tree.all.selectOne(t|t.label=nodeLabel).descendants.size;", context);
-						assertEquals(
-							String.format("%s should have %d descendants", entry.getKey(), entry.getValue()),
+					Object result = eol("return Tree.all.selectOne(t|t.label=nodeLabel).descendants.size;", context);
+					assertEquals(String.format("%s should have %d descendants", entry.getKey(), entry.getValue()),
 							entry.getValue(), result);
-					}
 				}
-
-				// Reverse derived edges
-				{
-					final Map<String, Integer> reverseExpected = new HashMap<>();
-					reverseExpected.put("t3", 0);
-					reverseExpected.put("t4", 1);
-					reverseExpected.put("t5", 1);
-					reverseExpected.put("t6", 2);
-
-					for (Entry<String, Integer> entry : reverseExpected.entrySet()) {
-						final Map<String, Object> context = Collections.singletonMap(EOLQueryEngine.PROPERTY_ARGUMENTS,
-								(Object) Collections.singletonMap("nodeLabel", entry.getKey()));
-
-						Object result = eol("return Tree.all.selectOne(t|t.label=nodeLabel).revRefNav_descendants.size;", context);
-						assertEquals(
-							String.format("%s should have %d ancestors", entry.getKey(), entry.getValue()),
-							entry.getValue(), result);
-					}
-				}
-
-				// Reverse reference navigation for the edges returns the element nodes and not just the derived value nodes
-				List<String> expected = Arrays.asList("t3", "t4");
-				Collection<String> result = (Collection<String>) eol("return Tree.all.selectOne(t|t.label='t6').revRefNav_descendants.label.flatten;");
-				assertEquals(expected.size(), result.size());
-				for (String e: expected) {
-					assertTrue(result.contains(e));
-				}
-
-				return null;
 			}
+
+			// Reverse derived edges
+			{
+				final Map<String, Integer> reverseExpected = new HashMap<>();
+				reverseExpected.put("t3", 0);
+				reverseExpected.put("t4", 1);
+				reverseExpected.put("t5", 1);
+				reverseExpected.put("t6", 2);
+
+				for (Entry<String, Integer> entry : reverseExpected.entrySet()) {
+					final Map<String, Object> context = Collections.singletonMap(EOLQueryEngine.PROPERTY_ARGUMENTS,
+							(Object) Collections.singletonMap("nodeLabel", entry.getKey()));
+
+					Object result = eol("return Tree.all.selectOne(t|t.label=nodeLabel).revRefNav_descendants.size;",
+							context);
+					assertEquals(String.format("%s should have %d ancestors", entry.getKey(), entry.getValue()),
+							entry.getValue(), result);
+				}
+			}
+
+			// Reverse reference navigation for the edges returns the element nodes and not
+			// just the derived value nodes
+			List<String> expected = Arrays.asList("t3", "t4");
+			@SuppressWarnings("unchecked")
+			Collection<String> result = (Collection<String>) eol(
+					"return Tree.all.selectOne(t|t.label='t6').revRefNav_descendants.label.flatten;");
+
+			assertEquals(expected.size(), result.size());
+			for (String e : expected) {
+				assertTrue(result.contains(e));
+			}
+
+			return null;
 		});
 	}
 
@@ -117,40 +115,38 @@
 				new File("resources/metamodels/Tree.ecore"));
 		requestFolderIndex(new File("resources/models/tree-dedges"));
 
-		waitForSync(new Callable<Object>() {
-			@Override
-			public Object call() throws Exception {
-				indexer.addDerivedAttribute("Tree", "Tree", "maxDescendant", "dummy", false, true, false,
-						EOLQueryEngine.TYPE, "return self.closure(c|c.children).flatten.sortBy(t|t.label).last;");
+		scheduleAndWait(() -> {
+			indexer.addDerivedAttribute("Tree", "Tree", "maxDescendant", "dummy", false, true, false,
+					EOLQueryEngine.TYPE, "return self.closure(c|c.children).flatten.sortBy(t|t.label).last;");
 
-				// Forward derived edge
-				{
-					// TODO: isMany is not honored on queries - need to add first
-					Object result = eol("return Tree.all.selectOne(t|t.label='t3').maxDescendant.first.label;");
-					assertEquals("t6", result);
-				}
-
-				// Reverse derived edges
-				{
-					final Map<String, Integer> reverseExpected = new HashMap<>();
-					reverseExpected.put("t3", 0);
-					reverseExpected.put("t4", 0);
-					reverseExpected.put("t5", 0);
-					reverseExpected.put("t6", 2); // t6 is the descendant of t3 and t4 with the last label in dictionary order
-
-					for (Entry<String, Integer> entry : reverseExpected.entrySet()) {
-						final Map<String, Object> context = Collections.singletonMap(EOLQueryEngine.PROPERTY_ARGUMENTS,
-								(Object) Collections.singletonMap("nodeLabel", entry.getKey()));
-
-						Object result = eol("return Tree.all.selectOne(t|t.label=nodeLabel).revRefNav_maxDescendant.size;", context);
-						assertEquals(
-							String.format("%s should have %d maxDescendant reverse refs", entry.getKey(), entry.getValue()),
-							entry.getValue(), result);
-					}
-				}
-
-				return null;
+			// Forward derived edge
+			{
+				// TODO: isMany is not honored on queries - need to add first
+				Object result = eol("return Tree.all.selectOne(t|t.label='t3').maxDescendant.first.label;");
+				assertEquals("t6", result);
 			}
+
+			// Reverse derived edges
+			{
+				final Map<String, Integer> reverseExpected = new HashMap<>();
+				reverseExpected.put("t3", 0);
+				reverseExpected.put("t4", 0);
+				reverseExpected.put("t5", 0);
+				reverseExpected.put("t6", 2); // t6 is the descendant of t3 and t4 with the last label in dictionary
+												// order
+
+				for (Entry<String, Integer> entry : reverseExpected.entrySet()) {
+					final Map<String, Object> context = Collections.singletonMap(EOLQueryEngine.PROPERTY_ARGUMENTS,
+							(Object) Collections.singletonMap("nodeLabel", entry.getKey()));
+
+					Object result = eol("return Tree.all.selectOne(t|t.label=nodeLabel).revRefNav_maxDescendant.size;",
+							context);
+					assertEquals(String.format("%s should have %d maxDescendant reverse refs", entry.getKey(),
+							entry.getValue()), entry.getValue(), result);
+				}
+			}
+
+			return null;
 		});
 	}
 
@@ -162,14 +158,11 @@
 				EOLQueryEngine.TYPE, "return self.xrefs.size;");
 		requestFolderIndex(new File("resources/models/scopedQuery"));
 
-		waitForSync(new Callable<Object>() {
-			@Override
-			public Object call() throws Exception {
-				assertEquals(1, eol("return Element.all.selectOne(e|e.id=0).nRefs;"));
-				assertEquals(3, eol("return Element.all.selectOne(e|e.id=1).nRefs;"));
-				assertEquals(3, eol("return Element.all.selectOne(e|e.id=23).nRefs;"));
-				return null;
-			}
+		scheduleAndWait(() -> {
+			assertEquals(1, eol("return Element.all.selectOne(e|e.id=0).nRefs;"));
+			assertEquals(3, eol("return Element.all.selectOne(e|e.id=1).nRefs;"));
+			assertEquals(3, eol("return Element.all.selectOne(e|e.id=23).nRefs;"));
+			return null;
 		});
 		
 	}
diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/DerivedFromMetaPropertiesTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/DerivedFromMetaPropertiesTest.java
index 113a991..1b869c8 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/DerivedFromMetaPropertiesTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/DerivedFromMetaPropertiesTest.java
@@ -21,7 +21,6 @@
 import java.io.File;
 import java.util.Arrays;
 import java.util.HashSet;
-import java.util.concurrent.Callable;
 
 import org.eclipse.emf.common.util.TreeIterator;
 import org.eclipse.emf.common.util.URI;
@@ -94,12 +93,9 @@
 
 		requestFolderIndex(modelFolder.getRoot());
 
-		waitForSync(new Callable<Object>() {
-			@Override
-			public Object call() throws Exception {
-				assertEquals(0, syncValidation.getListener().getTotalErrors());
-				return null;
-			}
+		scheduleAndWait(() -> {
+			assertEquals(0, syncValidation.getListener().getTotalErrors());
+			return null;
 		});
 	}
 
@@ -122,7 +118,7 @@
 		tRoot.eResource().save(null);
 		indexer.requestImmediateSync();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(new HashSet<>(Arrays.asList("changed", "childA")),
 				eol("return Tree.all.selectOne(t|t.label='childAA').allContainers.collect(c|c.label).asSet;"));
 			assertEquals(new HashSet<>(Arrays.asList("childA", "childAA", "childB")),
diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/EnumSupportTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/EnumSupportTest.java
index 0422768..42251fa 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/EnumSupportTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/EnumSupportTest.java
@@ -46,7 +46,7 @@
 				new File("resources/metamodels/ColoredTree.ecore"));
 		requestFolderIndex(new File("resources/models/enum-support"));
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(1, eol("return Tree.all.select(t|t.color = Color#red).size;"));
 			return null;
 		});
diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/ScopedQueryTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/ScopedQueryTest.java
index b20545b..4101c81 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/ScopedQueryTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/ScopedQueryTest.java
@@ -68,17 +68,17 @@
 
 	@Test
 	public void listFiles() throws Throwable {
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			try (IGraphTransaction tx = db.beginTransaction()) {
 				GraphWrapper gw = new GraphWrapper(db);
-
+		
 				Function<String, Supplier<Integer>> query = (String path) -> () -> (Integer) gw
 						.getFileNodes(Collections.singleton("*"), Collections.singleton(path)).size();
-
+		
 				assertEquals(3, (int) query.apply("*").get());
 				assertEquals(2, (int) query.apply("/subfolder/*").get());
 				assertEquals(1, (int) query.apply("/subfolder/subfolder/*").get());
-
+		
 				return null;
 			}
 		});
@@ -86,33 +86,33 @@
 
 	@Test
 	public void instanceCounts() throws Throwable {
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(0, syncValidation.getListener().getTotalErrors());
 			assertEquals("With no context, it should return all six elements", 6, eol("return Element.all.size;"));
 			assertEquals("With file context '*', it should return all six elements", 6, eol("return Element.all.size;", fc("*")));
 			assertEquals("With file context '/root.model', it should return only the two root elements",
 					new HashSet<>(Arrays.asList(0, 1)), eol("return Element.all.id.asSet;", fc("/root.model")));
-
+		
 			assertEquals("With file context '/subfolder/*', it should return four elements", 4,
 					eol("return Element.all.size;",	fc("/subfolder/*")));
 			assertEquals("With file context '/subfolder/subfolder/*', it should return only two elements", 2,
 					eol("return Element.all.size;",	fc("/subfolder/subfolder/*")));
-
+		
 			return null;
 		});
 	}
 
 	@Test
 	public void instanceCountsAllOf() throws Throwable {
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(0, syncValidation.getListener().getTotalErrors());
-
+		
 			try (IGraphTransaction tx = db.beginTransaction()) {
 				assertEquals("With no context, it should return all six elements of the exact type", 6,
 						queryEngine.getAllOf("Element", ModelElementNode.EDGE_LABEL_OFTYPE).size());
 				assertEquals("With no context, it should return all six elements of the exact type or subtypes", 6,
 						queryEngine.getAllOf("Element", ModelElementNode.EDGE_LABEL_OFKIND).size());
-
+		
 				assertEquals("With file context '*', it should return all six elements", 6,
 						queryEngine.getAllOf(MM_URI, "Element", "*").size());
 				assertEquals("With file context '/root.model', it should return only the two root elements", 2,
@@ -123,54 +123,54 @@
 						queryEngine.getAllOf(MM_URI, "Element", "/subfolder/subfolder/*").size());
 				tx.success();
 			}
-
+		
 			return null;
 		});
 	}
 	
 	@Test
 	public void forwardRefs() throws Throwable {
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(0, syncValidation.getListener().getTotalErrors());
-
+		
 			assertForwardRefs(null, 0, 1);
 			assertForwardRefs(null, 1, 3);
-
+		
 			assertForwardRefs(null, 12, 1);
 			assertForwardRefs("/subfolder/*", 12, 0);
 			assertForwardRefs(null, 15, 2);
 			assertForwardRefs("/subfolder/*", 15, 1);
-
+		
 			assertForwardRefs(null, 23, 3);
 			assertForwardRefs("/subfolder/*", 23, 2);
 			assertForwardRefs("/subfolder/subfolder/*", 23, 1);
 			assertForwardRefs(null, 27, 1);
-
+		
 			return null;
 		});
 	}
 
 	@Test
 	public void reverseRefs() throws Throwable {
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(0, syncValidation.getListener().getTotalErrors());
-
+		
 			assertReverseRefs(null, 0, 3);
 			assertReverseRefs(null, 1, 1);
-
+		
 			assertReverseRefs(null, 12, 1);
 			assertReverseRefs("/subfolder/*", 12, 0);
 			assertReverseRefs(null, 15, 3);
 			assertReverseRefs("/subfolder/*", 15, 2);
-
+		
 			assertReverseRefs(null, 23, 2);
 			assertReverseRefs("/subfolder/*", 23, 1);
 			assertReverseRefs("/subfolder/subfolder/*", 23, 1);
-
+		
 			assertReverseRefs(null, 27, 1);
 			assertReverseRefs("/subfolder/*", 27, 1);
 			assertReverseRefs("/subfolder/subfolder/*", 27, 1);
-
+		
 			return null;
 		});
 	}
diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/SubtreeContextTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/SubtreeContextTest.java
index 44c9136..8314342 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/SubtreeContextTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/SubtreeContextTest.java
@@ -18,14 +18,17 @@
 

 import static org.junit.Assert.assertEquals;

 import static org.junit.Assert.assertTrue;

+import static org.junit.Assert.fail;

 import static org.junit.Assume.assumeFalse;

 

 import java.io.File;

 import java.io.IOException;

+import java.util.ArrayList;

 import java.util.HashMap;

+import java.util.List;

 import java.util.Map;

 import java.util.Random;

-import java.util.concurrent.Callable;

+import java.util.stream.Collectors;

 

 import org.apache.commons.io.FileUtils;

 import org.apache.commons.io.filefilter.TrueFileFilter;

@@ -42,6 +45,7 @@
 import org.hawk.core.query.IQueryEngine;

 import org.hawk.epsilon.emc.EOLQueryEngine;

 import org.hawk.graph.syncValidationListener.SyncValidationListener;

+import org.hawk.graph.syncValidationListener.SyncValidationListener.ValidationError;

 import org.hawk.integration.tests.ModelIndexingTest;

 import org.junit.Before;

 import org.junit.Rule;

@@ -174,24 +178,27 @@
 

 	protected void prepareFragmented() throws Exception, Throwable {

 		requestFolderIndex(folderFragmented);

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-				return null;

-			}

+		scheduleAndWait(() -> {

+			assertNoValidationErrors("There should be no validation errors on the fragmented model");

+			return null;

 		});

 	}

 

+	private void assertNoValidationErrors(String message) {

+		List<ValidationError> errors = syncValidation.getListener().getErrors();

+		if (!errors.isEmpty()) {

+			List<String> messages = errors.stream()

+				.map(e -> e.getMessage())

+				.collect(Collectors.toList());

+			fail(message + ":\n" + String.join("\n\n", messages));

+		}

+	}

+

 	protected void prepareOriginal() throws Exception, Throwable {

 		requestFolderIndex(folderOriginal);

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-

-				return null;

-			}

+		scheduleAndWait(() -> {

+			assertNoValidationErrors("There should be no validation errors on the original model");

+			return null;

 		});

 	}

 

@@ -298,21 +305,25 @@
 			"IPackageFragmentRoot.all.size should be > than BinaryPackageFragmentRoot.all.size: checking %d > %d", ipkgCount, binpkgCount),

 			ipkgCount > binpkgCount);

 

-		Random rnd = new Random(42);

+		List<String> pathsTouched = new ArrayList<>();

+		final Random rnd = new Random(42);

 		for (File f : FileUtils.listFiles(folderFragmented, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE)) {

 			if (rnd.nextDouble() < 0.2) {

 				FileUtils.touch(f);

+				pathsTouched.add(f.getName());

 			}

 		}

 		indexer.requestImmediateSync();

-		waitForSync(() -> {

+		scheduleAndWait(() -> {

 			final int ipkgCount2 = (int) eol("return IPackageFragmentRoot.all.size;", ctx(

 				IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,

 				IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi",

 				IQueryEngine.PROPERTY_SUBTREE_DERIVEDALLOF, "true"

 			));

-			assertEquals(ipkgCount, ipkgCount2);

-

+			assertEquals(

+				"IPackageFragmentRoot.all.size should stay the same after touching " + pathsTouched,

+				ipkgCount, ipkgCount2);

+		

 			return null;

 		});

 	}

diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/TreeUpdateTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/TreeUpdateTest.java
index 6ed5ff4..4d8ef49 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/TreeUpdateTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/emf/TreeUpdateTest.java
@@ -23,7 +23,6 @@
 import java.nio.file.Files;

 import java.nio.file.Path;

 import java.nio.file.StandardCopyOption;

-import java.util.concurrent.Callable;

 

 import org.hawk.backend.tests.BackendTestSuite;

 import org.hawk.backend.tests.factories.IGraphDatabaseFactory;

@@ -69,12 +68,9 @@
 

 		requestFolderIndex(modelFolder.getRoot());

 

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-				return null;

-			}

+		scheduleAndWait(() -> {

+			assertEquals(0, syncValidation.getListener().getTotalErrors());

+			return null;

 		});

 	}

 

@@ -91,16 +87,13 @@
 		prepare("tree/tree.model");

 		replaceWith("changed-trees/add-child.model");

 		indexer.requestImmediateSync();

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-				try (IGraphTransaction tx = db.beginTransaction()) {

-					assertEquals(3, queryEngine.getAllOf("Tree", ModelElementNode.EDGE_LABEL_OFTYPE).size());

-					tx.success();

-				}

-				return null;

+		scheduleAndWait(() -> {

+			assertEquals(0, syncValidation.getListener().getTotalErrors());

+			try (IGraphTransaction tx = db.beginTransaction()) {

+				assertEquals(3, queryEngine.getAllOf("Tree", ModelElementNode.EDGE_LABEL_OFTYPE).size());

+				tx.success();

 			}

+			return null;

 		});

 	}

 

@@ -109,16 +102,13 @@
 		prepare("tree/tree.model");

 		replaceWith("changed-trees/remove-child.model");

 		indexer.requestImmediateSync();

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-				try (IGraphTransaction tx = db.beginTransaction()) {

-					assertEquals(1, queryEngine.getAllOf("Tree", ModelElementNode.EDGE_LABEL_OFTYPE).size());

-					tx.success();

-				}

-				return null;

+		scheduleAndWait(() -> {

+			assertEquals(0, syncValidation.getListener().getTotalErrors());

+			try (IGraphTransaction tx = db.beginTransaction()) {

+				assertEquals(1, queryEngine.getAllOf("Tree", ModelElementNode.EDGE_LABEL_OFTYPE).size());

+				tx.success();

 			}

+			return null;

 		});

 	}

 

@@ -127,14 +117,11 @@
 		prepare("tree/tree.model");

 		replaceWith("changed-trees/rename-child.model");

 		indexer.requestImmediateSync();

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-				assertEquals(2, eol("return Tree.all.size;"));

-				assertEquals(1, eol("return Tree.all.select(t|t.label='t90001').size;"));

-				return null;

-			}

+		scheduleAndWait(() -> {

+			assertEquals(0, syncValidation.getListener().getTotalErrors());

+			assertEquals(2, eol("return Tree.all.size;"));

+			assertEquals(1, eol("return Tree.all.select(t|t.label='t90001').size;"));

+			return null;

 		});

 	}

 

@@ -143,18 +130,14 @@
 		prepare("tree/tree.model");

 		replaceWith("changed-trees/rename-root.model");

 		indexer.requestImmediateSync();

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, syncValidation.getListener().getTotalErrors());

-				try (IGraphTransaction tx = db.beginTransaction()) {

-					assertEquals(2, queryEngine.getAllOf("Tree", ModelElementNode.EDGE_LABEL_OFTYPE).size());

-					assertEquals(1, queryEngine.query(indexer,

-							"return Tree.all.select(t|t.label='t40').size;", null));

-					tx.success();

-				}

-				return null;

+		scheduleAndWait(() -> {

+			assertEquals(0, syncValidation.getListener().getTotalErrors());

+			try (IGraphTransaction tx = db.beginTransaction()) {

+				assertEquals(2, queryEngine.getAllOf("Tree", ModelElementNode.EDGE_LABEL_OFTYPE).size());

+				assertEquals(1, queryEngine.query(indexer, "return Tree.all.select(t|t.label='t40').size;", null));

+				tx.success();

 			}

+			return null;

 		});

 	}

 

diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/manifests/ManifestIndexQueryTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/manifests/ManifestIndexQueryTest.java
index 85c00d8..01e0f7c 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/manifests/ManifestIndexQueryTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/manifests/ManifestIndexQueryTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertEquals;
 
 import java.io.File;
-import java.util.concurrent.Callable;
 
 import org.hawk.backend.tests.BackendTestSuite;
 import org.hawk.backend.tests.factories.IGraphDatabaseFactory;
@@ -44,13 +43,10 @@
 	@Test
 	public void indexDuplicateDependencies() throws Throwable {
 		requestFolderIndex(new File("resources/models/dupdep"));
-		waitForSync(new Callable<Object>() {
-			@Override
-			public Object call() throws Exception {
-				Object result = eol("return ManifestRequires.all.bundle.size;");
-				assertEquals(2, (int) result);
-				return null;
-			}
+		scheduleAndWait(() -> {
+			Object result = eol("return ManifestRequires.all.bundle.size;");
+			assertEquals(2, (int) result);
+			return null;
 		});
 	}
 
diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/modelio/ModelioMetamodelPopulationTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/modelio/ModelioMetamodelPopulationTest.java
index 634c24a..729eeae 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/modelio/ModelioMetamodelPopulationTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/modelio/ModelioMetamodelPopulationTest.java
@@ -23,7 +23,6 @@
 import java.util.Collection;

 import java.util.HashSet;

 import java.util.Set;

-import java.util.concurrent.Callable;

 

 import org.hawk.backend.tests.BackendTestSuite;

 import org.hawk.backend.tests.factories.IGraphDatabaseFactory;

@@ -102,13 +101,10 @@
 	public void zoo() throws Throwable {

 		requestFolderIndex(new File("resources/models/zoo"));

 

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				assertEquals(0, validationListener.getListener().getTotalErrors());

-				assertEquals(6, eol("return Class.all.size;"));

-				return null;

-			}

+		scheduleAndWait(() -> {

+			assertEquals(0, validationListener.getListener().getTotalErrors());

+			assertEquals(6, eol("return Class.all.size;"));

+			return null;

 		});

 	}

 }

diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/modelio/ModelioProxyResolutionTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/modelio/ModelioProxyResolutionTest.java
index 7dbdd32..4011a8b 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/modelio/ModelioProxyResolutionTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/modelio/ModelioProxyResolutionTest.java
@@ -31,7 +31,6 @@
 import java.util.Map;

 import java.util.NoSuchElementException;

 import java.util.Set;

-import java.util.concurrent.Callable;

 

 import org.apache.commons.io.FileUtils;

 import org.hawk.backend.tests.BackendTestSuite;

@@ -93,13 +92,13 @@
 	public void elephantResolve() throws Throwable {

 		copyClasses("Elephant");

 		requestFolderIndex(tempFolder.getRoot());

-		waitForSync(() -> {

+		scheduleAndWait(() -> {

 			// The file should have exactly three unique IDs that others can refer to

 			try (IGraphTransaction tx = db.beginTransaction()) {

 				final IGraphNodeIndex fragmentIndex = db

 					.getOrCreateNodeIndex(GraphModelBatchInjector.FRAGMENT_DICT_NAME);

 				assertEquals(3, fragmentIndex.query("id", "*").size());

-

+		

 				/*

 				 * We should have two lists with unresolved refs: one to the supertype, and one

 				 * to the package.

@@ -107,30 +106,30 @@
 				final GraphModelInserter inserter = updater.createInserter();

 				List<ProxyReferenceList> lists = inserter.getProxyReferenceLists(indexer.getGraph());

 				assertEquals(2, lists.size());

-

+		

 				final Set<String> unresolved = collectUnresolvedFragments(lists);

 				assertEquals(new HashSet<>(Arrays.asList(PACKAGE_FRAGMENT, ANIMAL_FRAGMENT)), unresolved);

 				tx.success();

 			}

-

+		

 			return null;

 		});

 

 		copyClasses("Animal", "Area");

 		indexer.requestImmediateSync();

-		waitForSync(() -> {

+		scheduleAndWait(() -> {

 			// We should only be missing the package and the string datatype

 			List<ProxyReferenceList> lists = updater.createInserter().getProxyReferenceLists(indexer.getGraph());

-

+		

 			// Three lists come from Animal, Area, Elephant, which are missing the package.

 			// Two lists come from Animal.name and Elephant.name, which are missing datatype. 

 			assertEquals(5, lists.size());

- 

+		

 			final Set<String> unresolved = collectUnresolvedFragments(lists);

 			assertEquals(new HashSet<>(Arrays.asList(

 				STRING_FRAGMENT, PACKAGE_FRAGMENT

 			)), unresolved);

-

+		

 			return null;

 		});

 	}

@@ -139,15 +138,12 @@
 	public void zoo() throws Throwable {

 		requestFolderIndex(new File("resources/models/zoo"));

 

-		waitForSync(new Callable<Object>() {

-			@Override

-			public Object call() throws Exception {

-				// We should only be missing the string datatype

-				List<ProxyReferenceList> lists = updater.createInserter().getProxyReferenceLists(indexer.getGraph());

-				final Set<String> unresolved = collectUnresolvedFragments(lists);

-				assertEquals(Collections.emptySet(), unresolved);

-				return null;

-			}

+		scheduleAndWait(() -> {

+			// We should only be missing the string datatype

+			List<ProxyReferenceList> lists = updater.createInserter().getProxyReferenceLists(indexer.getGraph());

+			final Set<String> unresolved = collectUnresolvedFragments(lists);

+			assertEquals(Collections.emptySet(), unresolved);

+			return null;

 		});

 	}

 

diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/AbstractUMLIndexingTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/AbstractUMLIndexingTest.java
index 6eb604d..9ac9ea7 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/AbstractUMLIndexingTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/AbstractUMLIndexingTest.java
@@ -67,7 +67,7 @@
 		vcs.init(null, indexer);
 		vcs.run();
 		indexer.addVCSManager(vcs, true);
-		waitForSync();
+		scheduleAndWait();
 	}
 
 }
\ No newline at end of file
diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/UMLIndexingTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/UMLIndexingTest.java
index 6f2877d..67f21a4 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/UMLIndexingTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/UMLIndexingTest.java
@@ -23,7 +23,6 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Map;
-import java.util.concurrent.Callable;
 
 import org.apache.commons.io.FileUtils;
 import org.eclipse.emf.common.util.URI;
@@ -57,43 +56,38 @@
 	@Test
 	public void zoo() throws Throwable {
 		requestFolderIndex(new File(BASE_DIRECTORY, "zoo"));
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				assertEquals(1, eol("return Model.types.select(t|t.name='Profile').size;"));
-				assertEquals(4, eol("return Class.all.size;",
+		scheduleAndWait(() -> {
+			assertEquals(1, eol("return Model.types.select(t|t.name='Profile').size;"));
+			assertEquals(4, eol("return Class.all.size;",
 					Collections.singletonMap(CEOLQueryEngine.PROPERTY_FILECONTEXT, "*model.uml")));
 
-				try (IGraphTransaction tx = db.beginTransaction()) {
-					GraphNodeWrapper attr = (GraphNodeWrapper) eol(
-							"return Class.all.selectOne(c|c.name='Animal').ownedAttribute.selectOne(a|a.name='age');");
+			try (IGraphTransaction tx = db.beginTransaction()) {
+				GraphNodeWrapper attr = (GraphNodeWrapper) eol(
+						"return Class.all.selectOne(c|c.name='Animal').ownedAttribute.selectOne(a|a.name='age');");
 
-					// Check cross-reference to UML predefined library
-					final IGraphNode node = attr.getNode();
-					final Iterable<IGraphEdge> itOutgoing = node.getOutgoingWithType("type");
-					int size = 0;
-					for (@SuppressWarnings("unused") IGraphEdge e : itOutgoing) {
-						size++;
-					}
-					assertEquals(1, size);
-
-					tx.success();
+				// Check cross-reference to UML predefined library
+				final IGraphNode node = attr.getNode();
+				final Iterable<IGraphEdge> itOutgoing = node.getOutgoingWithType("type");
+				int size = 0;
+				for (@SuppressWarnings("unused")
+				IGraphEdge e : itOutgoing) {
+					size++;
 				}
+				assertEquals(1, size);
 
-				return null;
+				tx.success();
 			}
+
+			return null;
 		});
 	}
 
 	@Test
 	public void stereotypeAsModel() throws Throwable {
 		requestFolderIndex(new File(BASE_DIRECTORY, "simpleProfile"));
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				assertEquals(1, eol("return Stereotype.all.select(s|s.name='special').size;"));
-				return null;
-			}
+		scheduleAndWait(() -> {
+			assertEquals(1, eol("return Stereotype.all.select(s|s.name='special').size;"));
+			return null;
 		});
 	}
 
@@ -127,25 +121,20 @@
 	public void customProfileV4() throws Throwable {
 		indexer.registerMetamodels(new File(BASE_DIRECTORY, "simpleProfile/model.profile.uml"));
 		requestFolderIndex(new File(BASE_DIRECTORY, "simpleProfileApplication"));
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				// Check that we support Papyrus profile versioning
-				Map<String, Object> ctx = Collections.singletonMap(
-					EOLQueryEngine.PROPERTY_DEFAULTNAMESPACES,
+		scheduleAndWait(() -> {
+			// Check that we support Papyrus profile versioning
+			Map<String, Object> ctx = Collections.singletonMap(EOLQueryEngine.PROPERTY_DEFAULTNAMESPACES,
 					SIMPLE_PROFILE_NSURI_PREFIX + "/0.0.4");
 
-				assertEquals(1, eol("return special.all.size;", ctx));
-				assertEquals(9001, eol("return special.all.first.amount;", ctx));
-				assertEquals("Example", eol("return special.all.first.base_Class.name;", ctx));
+			assertEquals(1, eol("return special.all.size;", ctx));
+			assertEquals(9001, eol("return special.all.first.amount;", ctx));
+			assertEquals("Example", eol("return special.all.first.base_Class.name;", ctx));
 
-				// profileApplication is mapped as an ofType edge
-				assertEquals(1, eol("return RootElementApplication.all.size;", ctx));
-				assertEquals("Example", eol(
-					"return RootElementApplication.all.packagedElement.flatten.first.name;", ctx));
+			// profileApplication is mapped as an ofType edge
+			assertEquals(1, eol("return RootElementApplication.all.size;", ctx));
+			assertEquals("Example", eol("return RootElementApplication.all.packagedElement.flatten.first.name;", ctx));
 
-				return null;
-			}
+			return null;
 		});
 	}
 
@@ -153,20 +142,17 @@
 	public void customProfileV5() throws Throwable {
 		indexer.registerMetamodels(new File(BASE_DIRECTORY, "simpleProfile/model.profile.uml"));
 		requestFolderIndex(new File(BASE_DIRECTORY, "simpleProfileApplicationNewVersion"));
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				// Check that we support Papyrus profile versioning
-				Map<String, Object> ctx = Collections.singletonMap(
-					EOLQueryEngine.PROPERTY_DEFAULTNAMESPACES,
-					SIMPLE_PROFILE_NSURI_PREFIX + "/0.0.5");
-
-				assertEquals(1, eol("return special.all.size;", ctx));
-				assertEquals(9002, eol("return special.all.first.amount;", ctx));
-				assertEquals("example", eol("return special.all.first.name;", ctx));
-				assertEquals("Example", eol("return special.all.first.base_Class.name;", ctx));
-				return null;
-			}
+		scheduleAndWait(() -> {
+			// Check that we support Papyrus profile versioning
+			Map<String, Object> ctx = Collections.singletonMap(
+				EOLQueryEngine.PROPERTY_DEFAULTNAMESPACES,
+				SIMPLE_PROFILE_NSURI_PREFIX + "/0.0.5");
+		
+			assertEquals(1, eol("return special.all.size;", ctx));
+			assertEquals(9002, eol("return special.all.first.amount;", ctx));
+			assertEquals("example", eol("return special.all.first.name;", ctx));
+			assertEquals("Example", eol("return special.all.first.base_Class.name;", ctx));
+			return null;
 		});
 	}
 
@@ -185,7 +171,7 @@
 		final Map<String, Object> ctxNewVersion = Collections.singletonMap(
 				EOLQueryEngine.PROPERTY_DEFAULTNAMESPACES,
 				SIMPLE_PROFILE_NSURI_PREFIX + "/0.0.5");
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(1, eol("return special.all.size;", ctxOldVersion));
 			assertEquals(0, eol("return special.all.size;", ctxNewVersion));
 			return null;
@@ -194,7 +180,7 @@
 		final File newFile = new File(BASE_DIRECTORY, "simpleProfileApplicationNewVersion/model.uml").getCanonicalFile();
 		copyResource(destURI, newFile);
 		indexer.requestImmediateSync();
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(0, eol("return special.all.size;", ctxOldVersion));
 			assertEquals(1, eol("return special.all.size;", ctxNewVersion));
 			return null;
@@ -215,7 +201,7 @@
 		final Map<String, Object> ctxNewVersion = Collections.singletonMap(
 				EOLQueryEngine.PROPERTY_DEFAULTNAMESPACES,
 				SIMPLE_PROFILE_NSURI_PREFIX + "/0.0.5");
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(1, eol("return special.all.size;", ctxOldVersion));
 			assertEquals(0, eol("return special.all.size;", ctxNewVersion));
 			return null;
@@ -223,7 +209,7 @@
 
 		FileUtils.copyFile(new File(BASE_DIRECTORY, "simpleProfileApplicationNewVersion/model.uml"), new File(modelFolder.getRoot(), "simpleProfileApplication/model.uml"));
 		indexer.requestImmediateSync();
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(0, eol("return special.all.size;", ctxOldVersion));
 			assertEquals(1, eol("return special.all.size;", ctxNewVersion));
 			return null;
@@ -233,7 +219,7 @@
 	@Test
 	public void localfolderCrosslinks() throws Throwable {
 		requestFolderIndex(new File(BASE_DIRECTORY, "crossfile-refs"));
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			Map<String, Object> ctx = Collections.singletonMap(CEOLQueryEngine.PROPERTY_FILECONTEXT, "*model.uml");
 			assertEquals(3, eol("return Class.all.size;", ctx));
 			assertEquals(new HashSet<>(Arrays.asList("Class1", "Class3")),
diff --git a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/UMLWorkspaceIndexingTest.java b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/UMLWorkspaceIndexingTest.java
index 9bb7e79..24e6be9 100644
--- a/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/UMLWorkspaceIndexingTest.java
+++ b/tests/org.hawk.integration.tests/src/org/hawk/integration/tests/uml/UMLWorkspaceIndexingTest.java
@@ -44,7 +44,7 @@
 
 		try {
 			requestWorkspaceIndex();
-			waitForSync(() -> {
+			scheduleAndWait(() -> {
 				assertEquals(3, eolWorkspace("return Class.all.size;"));
 				assertEquals(new HashSet<>(Arrays.asList("Class1", "Class3")), eolWorkspace(
 						"return Class.all.selectOne(c|c.name='Class2').generalization.general.name.flatten.asSet;"));
diff --git a/tests/org.hawk.jgit.tests/.classpath b/tests/org.hawk.jgit.tests/.classpath
new file mode 100644
index 0000000..1747f41
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<classpath>

+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>

+	<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>

+	<classpathentry kind="src" path="src"/>

+	<classpathentry kind="src" path="resources"/>

+	<classpathentry kind="output" path="bin"/>

+</classpath>

diff --git a/tests/org.hawk.jgit.tests/.project b/tests/org.hawk.jgit.tests/.project
new file mode 100644
index 0000000..c7feccf
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/.project
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>org.hawk.jgit.tests</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.pde.ManifestBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.pde.SchemaBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.pde.PluginNature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/tests/org.hawk.jgit.tests/.settings/org.eclipse.jdt.core.prefs b/tests/org.hawk.jgit.tests/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..0c68a61
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,7 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/tests/org.hawk.jgit.tests/META-INF/MANIFEST.MF b/tests/org.hawk.jgit.tests/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..9d4a0bc
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/META-INF/MANIFEST.MF
@@ -0,0 +1,12 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Tests for Hawk Local Folder VCS
+Bundle-SymbolicName: org.hawk.jgit.tests
+Bundle-Version: 1.0.0.qualifier
+Fragment-Host: org.hawk.jgit;bundle-version="1.2.0"
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Require-Bundle: org.junit;bundle-version="4.12.0",
+ org.mockito;bundle-version="2.13.0",
+ net.bytebuddy.byte-buddy;bundle-version="1.7.9",
+ org.objenesis;bundle-version="2.6.0"
+Automatic-Module-Name: org.hawk.git.tests
diff --git a/tests/org.hawk.jgit.tests/Run Git Tests.launch b/tests/org.hawk.jgit.tests/Run Git Tests.launch
new file mode 100644
index 0000000..5d06aad
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/Run Git Tests.launch
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.pde.ui.JunitLaunchConfig">
+<booleanAttribute key="append.args" value="true"/>
+<stringAttribute key="application" value="org.eclipse.pde.junit.runtime.coretestapplication"/>
+<booleanAttribute key="askclear" value="false"/>
+<booleanAttribute key="automaticAdd" value="true"/>
+<booleanAttribute key="automaticValidate" value="true"/>
+<stringAttribute key="bootstrap" value=""/>
+<stringAttribute key="checked" value="[NONE]"/>
+<booleanAttribute key="clearConfig" value="true"/>
+<booleanAttribute key="clearws" value="true"/>
+<booleanAttribute key="clearwslog" value="false"/>
+<stringAttribute key="configLocation" value="${workspace_loc}/.metadata/.plugins/org.eclipse.pde.core/pde-junit"/>
+<booleanAttribute key="default" value="false"/>
+<stringAttribute key="deselected_workspace_plugins" value="com.fasterxml.jackson_2.9.6,com.github.peterwippermann.junit4.parameterizedsuite_1.1.0,org.apache.commons.io_1.3.2,org.hawk.backend.tests,org.hawk.bpmn,org.hawk.emf,org.hawk.emf.tests,org.hawk.emfresource,org.hawk.epsilon,org.hawk.examples.docgen,org.hawk.git,org.hawk.graph,org.hawk.graph.sampleListener,org.hawk.graph.syncValidationListener,org.hawk.greycat,org.hawk.http,org.hawk.integration.tests,org.hawk.localfolder,org.hawk.localfolder.tests,org.hawk.manifest,org.hawk.manifest.tests,org.hawk.modelio.exml,org.hawk.modelio.exml.ecoregen,org.hawk.modelio.exml.tests,org.hawk.neo4j-v2,org.hawk.neo4j-v2.dependencies,org.hawk.orientdb,org.hawk.osgiserver,org.hawk.service.api,org.hawk.service.api.dt,org.hawk.service.artemis,org.hawk.service.artemis.server,org.hawk.service.cli,org.hawk.service.cli.product,org.hawk.service.clients.cli.application,org.hawk.service.emc,org.hawk.service.emc.dt,org.hawk.service.emf,org.hawk.service.emf.dt,org.hawk.service.emf.tests,org.hawk.service.remote.thrift,org.hawk.service.remote.thrift.timeaware,org.hawk.service.server.application,org.hawk.service.server.cli,org.hawk.service.server.gzip,org.hawk.service.server.product,org.hawk.service.server.productdefs,org.hawk.service.server.users.cli,org.hawk.service.server.users.servlet,org.hawk.service.servlet,org.hawk.service.servlet.tests,org.hawk.svn,org.hawk.svn.tests,org.hawk.timeaware,org.hawk.timeaware.tests,org.hawk.ui.emc.dt2,org.hawk.ui.emfresource,org.hawk.ui2,org.hawk.uml,org.hawk.uml.tests,org.hawk.workspace,org.hawk.workspace.tests,uk.ac.aston.hawk.mongodb,uk.ac.aston.hawk.mongodb.drones,uk.ac.aston.hawk.mongodb.drones.integrationtest,uk.ac.aston.hawk.mongodb.drones.parser,uk.ac.aston.hawk.mongodb.drones.parser.tests,uk.ac.aston.hawk.mongodb.drones.tests,uk.ac.aston.hawk.mongodb.tests,uk.ac.aston.log2repo.drone,uk.ac.aston.log2repo.drone.hawk,uk.ac.aston.log2repo.rdm,uk.ac.aston.log2repo.rdm.hawk,uk.ac.aston.logmodel"/>
+<booleanAttribute key="includeOptional" value="true"/>
+<stringAttribute key="location" value="${workspace_loc}/../junit-workspace"/>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/org.hawk.jgit.tests/src/org/hawk/git/JGitRepositoryTest.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value=""/>
+<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
+<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
+<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
+<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="org.hawk.git.JGitRepositoryTest"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-os ${target.os} -ws ${target.ws} -arch ${target.arch} -nl ${target.nl} -consoleLog"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="org.hawk.jgit.tests"/>
+<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.pde.ui.workbenchClasspathProvider"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea"/>
+<stringAttribute key="pde.version" value="3.3"/>
+<stringAttribute key="product" value="org.eclipse.platform.ide"/>
+<booleanAttribute key="run_in_ui_thread" value="true"/>
+<stringAttribute key="selected_target_plugins" value="ch.qos.logback.classic@default:default,ch.qos.logback.core@default:default,ch.qos.logback.slf4j@default:false,com.ibm.icu@default:default,com.jcraft.jsch*0.1.55.v20190404-1902@default:default,com.jcraft.jzlib@default:default,javaewah@default:default,javax.annotation@default:default,javax.inject@default:default,javax.xml@default:default,net.bytebuddy.byte-buddy-agent@default:default,net.bytebuddy.byte-buddy@default:default,org.apache.batik.constants@default:default,org.apache.batik.css*1.9.1.v20180313-1559@default:default,org.apache.batik.i18n@default:default,org.apache.batik.util*1.9.1.v20180227-1645@default:default,org.apache.commons.codec*1.9.0.v20170208-1614@default:default,org.apache.commons.io@default:default,org.apache.commons.jxpath@default:default,org.apache.commons.logging@default:default,org.apache.felix.gogo.runtime@default:default,org.apache.felix.scr@1:true,org.apache.httpcomponents.httpclient*4.5.2.v20170210-0925@default:default,org.apache.httpcomponents.httpcore*4.4.6.v20170210-0925@default:default,org.apache.log4j@default:default,org.apache.xmlgraphics@default:default,org.bouncycastle.bcpg@default:default,org.bouncycastle.bcprov@default:default,org.eclipse.ant.core@default:default,org.eclipse.compare.core@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filesystem.linux.x86_64@default:false,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.resources@default:default,org.eclipse.core.runtime@default:true,org.eclipse.core.variables@default:default,org.eclipse.e4.core.commands@default:default,org.eclipse.e4.core.contexts@default:default,org.eclipse.e4.core.di.annotations@default:default,org.eclipse.e4.core.di.extensions.supplier@default:default,org.eclipse.e4.core.di.extensions@default:default,org.eclipse.e4.core.di@default:default,org.eclipse.e4.core.services@default:default,org.eclipse.e4.emf.xpath@default:default,org.eclipse.e4.ui.bindings@default:default,org.eclipse.e4.ui.css.core@default:default,org.eclipse.e4.ui.css.swt.theme@default:default,org.eclipse.e4.ui.css.swt@default:default,org.eclipse.e4.ui.di@default:default,org.eclipse.e4.ui.model.workbench@default:default,org.eclipse.e4.ui.services@default:default,org.eclipse.e4.ui.swt.gtk@default:false,org.eclipse.e4.ui.widgets@default:default,org.eclipse.e4.ui.workbench.addons.swt@default:default,org.eclipse.e4.ui.workbench.renderers.swt@default:default,org.eclipse.e4.ui.workbench.swt@default:default,org.eclipse.e4.ui.workbench3@default:default,org.eclipse.e4.ui.workbench@default:default,org.eclipse.emf.common@default:default,org.eclipse.emf.ecore.change@default:default,org.eclipse.emf.ecore.xmi@default:default,org.eclipse.emf.ecore@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.bidi@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.event@default:default,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.region@default:false,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.supplement@default:default,org.eclipse.equinox.transforms.hook@default:false,org.eclipse.equinox.weaving.hook@default:false,org.eclipse.help@default:default,org.eclipse.jetty.osgi-servlet-api@default:default,org.eclipse.jetty.osgi.alpn.fragment@default:false,org.eclipse.jface.databinding@default:default,org.eclipse.jface@default:default,org.eclipse.jgit@default:default,org.eclipse.osgi.compatibility.state@default:false,org.eclipse.osgi.services@default:default,org.eclipse.osgi.util@default:default,org.eclipse.osgi@-1:true,org.eclipse.swt.gtk.linux.x86_64@default:false,org.eclipse.swt@default:default,org.eclipse.team.core@default:default,org.eclipse.ui.trace@default:default,org.eclipse.ui.workbench@default:default,org.eclipse.ui@default:default,org.eclipse.xtext.logging@default:false,org.hamcrest.core@default:default,org.junit@default:default,org.mockito@default:default,org.objenesis@default:default,org.slf4j.api@default:default,org.slf4j.impl.log4j12@default:false,org.w3c.css.sac@default:default,org.w3c.dom.events@default:default,org.w3c.dom.smil@default:default,org.w3c.dom.svg@default:default"/>
+<stringAttribute key="selected_workspace_plugins" value="org.hawk.core.dependencies@default:default,org.hawk.core@default:default,org.hawk.jgit.tests@default:false,org.hawk.jgit@default:default,org.hawk.service.server.logback@default:false"/>
+<booleanAttribute key="show_selected_only" value="false"/>
+<booleanAttribute key="tracing" value="false"/>
+<booleanAttribute key="useCustomFeatures" value="false"/>
+<booleanAttribute key="useDefaultConfig" value="true"/>
+<booleanAttribute key="useDefaultConfigArea" value="false"/>
+<booleanAttribute key="useProduct" value="false"/>
+</launchConfiguration>
diff --git a/tests/org.hawk.jgit.tests/build.properties b/tests/org.hawk.jgit.tests/build.properties
new file mode 100644
index 0000000..f11210d
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/build.properties
@@ -0,0 +1,5 @@
+source.. = src/,\
+           resources/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
diff --git a/tests/org.hawk.jgit.tests/resources/v0/one.txt b/tests/org.hawk.jgit.tests/resources/v0/one.txt
new file mode 100644
index 0000000..43dd47e
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/resources/v0/one.txt
@@ -0,0 +1 @@
+one
\ No newline at end of file
diff --git a/tests/org.hawk.jgit.tests/resources/v1/one.txt b/tests/org.hawk.jgit.tests/resources/v1/one.txt
new file mode 100644
index 0000000..43dd47e
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/resources/v1/one.txt
@@ -0,0 +1 @@
+one
\ No newline at end of file
diff --git a/tests/org.hawk.jgit.tests/resources/v1/two.txt b/tests/org.hawk.jgit.tests/resources/v1/two.txt
new file mode 100644
index 0000000..eeacbdb
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/resources/v1/two.txt
@@ -0,0 +1 @@
+two

diff --git a/tests/org.hawk.jgit.tests/resources/v2/one.txt b/tests/org.hawk.jgit.tests/resources/v2/one.txt
new file mode 100644
index 0000000..7c0d23c
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/resources/v2/one.txt
@@ -0,0 +1,2 @@
+one

+one
\ No newline at end of file
diff --git a/tests/org.hawk.jgit.tests/resources/v2/two.txt b/tests/org.hawk.jgit.tests/resources/v2/two.txt
new file mode 100644
index 0000000..eeacbdb
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/resources/v2/two.txt
@@ -0,0 +1 @@
+two

diff --git a/tests/org.hawk.jgit.tests/resources/v3/two.txt b/tests/org.hawk.jgit.tests/resources/v3/two.txt
new file mode 100644
index 0000000..9ce97cd
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/resources/v3/two.txt
@@ -0,0 +1,2 @@
+two

+two

diff --git a/tests/org.hawk.jgit.tests/src/org/hawk/git/JGitRepositoryTest.java b/tests/org.hawk.jgit.tests/src/org/hawk/git/JGitRepositoryTest.java
new file mode 100644
index 0000000..2dd7b9b
--- /dev/null
+++ b/tests/org.hawk.jgit.tests/src/org/hawk/git/JGitRepositoryTest.java
@@ -0,0 +1,390 @@
+/*******************************************************************************
+ * Copyright (c) 2018-2019 Aston University, and others
+ * 
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License, v. 2.0 are satisfied: GNU General Public License, version 3.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-3.0
+ *
+ * Contributors:
+ *     Antonio Garcia-Dominguez - initial API and implementation
+ *     Horacio Hoyos Rodriguez - Add proper Git support
+ ******************************************************************************/
+package org.hawk.git;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.api.AddCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.RmCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.NoFilepatternException;
+import org.hawk.core.IModelIndexer;
+import org.hawk.core.VcsChangeType;
+import org.hawk.core.VcsCommitItem;
+import org.hawk.core.VcsRepositoryDelta;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+public class JGitRepositoryTest {
+
+	private static final String NULL_REV = "0000000000000000000000000000000000000000";
+
+	private enum Version {
+		v0("Initial Commit", "one.txt"), v1("Adds file two", "one.txt", "two.txt"),
+		v2("Fixes file one", "one.txt", "two.txt"), v3("Fixes file two, deletes file one", "two.txt");
+
+		private final String[] allFiles = new String[] { "one.txt", "two.txt" };
+		private final String msg;
+		private final Set<String> names;
+
+		Version(String msg, String... files) {
+			this.msg = msg;
+			this.names = new HashSet<>(Arrays.asList(files));
+		}
+
+		public void commit(TemporaryFolder folder, Map<String, File> files) throws IOException {
+			updateFiles(folder, files);
+			commitChanges(folder, files);
+		}
+
+		private void commitChanges(TemporaryFolder folder, Map<String, File> files) {
+			try (org.eclipse.jgit.api.Git repo = org.eclipse.jgit.api.Git.open(folder.getRoot())) {
+				try {
+					AddCommand add = repo.add();
+					RmCommand rm = repo.rm();
+					for (String name : allFiles) {
+						if (names.contains(name)) {
+							add.addFilepattern(name);
+						} else {
+							rm.addFilepattern(name);
+						}
+					}
+					// Some commands can be empty
+					try {
+						add.call();
+					} catch (NoFilepatternException e) {
+						/* Ignore */ }
+					try {
+						rm.call();
+					} catch (NoFilepatternException e) {
+						/* Ignore */ }
+					repo.commit().setMessage(msg).call();
+				} catch (GitAPIException e) {
+					throw new IllegalStateException("Failed to commit");
+				}
+			} catch (IOException e2) {
+				throw new IllegalStateException("Failed to commit");
+			}
+		}
+
+		private void updateFiles(TemporaryFolder folder, Map<String, File> files)
+				throws IOException, FileNotFoundException {
+			File tempFile;
+			for (String name : allFiles) {
+				tempFile = files.get(name);
+				if (names.contains(name)) {
+					if (tempFile != null) {
+						// Replace it
+						tempFile.delete();
+					}
+					tempFile = folder.newFile(name);
+					copy(name, tempFile);
+					files.put(name, tempFile);
+				} else {
+					if (tempFile != null) {
+						tempFile.delete();
+						files.remove(name);
+					}
+				}
+			}
+		}
+
+		private void copy(String name, File tempFile) throws FileNotFoundException, IOException {
+			InputStream in = JGitRepositoryTest.class.getResourceAsStream(String.format("/%s/%s", this.name(), name));
+			try (OutputStream out = new FileOutputStream(tempFile)) {
+				byte[] buf = new byte[1024];
+				int length;
+				while ((length = in.read(buf)) > 0) {
+					out.write(buf, 0, length);
+				}
+			}
+		}
+
+	}
+
+	@Rule
+	public TemporaryFolder folder = new TemporaryFolder();
+
+	@Rule
+	public ExpectedException thrown = ExpectedException.none();
+
+	private final Map<String, File> files = new HashMap<>();
+
+	private JGitRepository vcs;
+	private IModelIndexer indexer;
+
+	@Before
+	public void setup() throws Exception {
+		org.eclipse.jgit.api.Git.init().setDirectory(folder.getRoot()).call();
+
+		indexer = mock(IModelIndexer.class);
+		vcs = new JGitRepository();
+		vcs.init(folder.getRoot().getCanonicalPath(), indexer);
+		vcs.run();
+	}
+
+	@After
+	public void clean() throws Exception {
+		vcs.shutdown();
+		files.clear();
+	}
+
+	@Test
+	public void currentRevisionWithoutCommits() throws Exception {
+		String currRev = vcs.getCurrentRevision();
+		// Git repo with no commits has revision 0
+		assertEquals(NULL_REV, currRev);
+	}
+
+	@Test
+	public void currentRevisionOneCommit() throws Exception {
+		Version.v0.commit(folder, files);
+		String currRev = vcs.getCurrentRevision();
+		// If there are commits there should be a hash
+		assertNotEquals(NULL_REV, currRev);
+	}
+
+	@Test
+	public void currentRevisionTwoCommits() throws Exception {
+		Version.v0.commit(folder, files);
+		String v0Rev = vcs.getCurrentRevision();
+		System.out.println(v0Rev);
+		assertNotEquals(NULL_REV, v0Rev);
+		Version.v1.commit(folder, files);
+		String v1Rev = vcs.getCurrentRevision();
+		System.out.println(v1Rev);
+		assertNotEquals(NULL_REV, v1Rev);
+		assertNotEquals(v0Rev, v1Rev);
+	}
+
+	@Test
+	public void firstRevisionIsConstant() throws Exception {
+		Version.v0.commit(folder, files);
+		String rev0 = vcs.getCurrentRevision();
+		assertEquals(rev0, vcs.getFirstRevision());
+		Version.v1.commit(folder, files);
+		assertEquals(rev0, vcs.getFirstRevision());
+		Version.v2.commit(folder, files);
+		assertEquals(rev0, vcs.getFirstRevision());
+		Version.v3.commit(folder, files);
+		assertEquals(rev0, vcs.getFirstRevision());
+	}
+
+	@Test
+	public void delta1ArgWithNullRevision() throws Exception {
+		Version.v0.commit(folder, files);
+		List<VcsCommitItem> delta = vcs.getDelta(null);
+
+		assertEquals("1-arg delta with null starts from first revision", 1, delta.size());
+		assertEquals(VcsChangeType.ADDED, delta.get(0).getChangeType());
+	}
+
+	@Test
+	public void delta1ArgWithCurrent() throws Exception {
+		Version.v0.commit(folder, files);
+		Collection<VcsCommitItem> delta = vcs.getDelta(vcs.getCurrentRevision());
+		assertEquals(0, delta.size());
+	}
+
+	@Test
+	public void delta1ArgWithTwoCommits() throws Exception {
+		Version.v0.commit(folder, files);
+		String rev0 = vcs.getCurrentRevision();
+		Version.v1.commit(folder, files);
+		List<VcsCommitItem> delta = vcs.getDelta(rev0);
+		assertEquals(1, delta.size());
+		assertSame(vcs, delta.get(0).getCommit().getDelta().getManager());
+		assertEquals(1L, delta.stream()
+				.filter(i -> i.getChangeType().equals(VcsChangeType.ADDED) && i.getPath().equals("/two.txt")).count());
+	}
+
+	@Test
+	public void delta1ArgWithUpdatedFile() throws Exception {
+		Version.v0.commit(folder, files);
+		Version.v1.commit(folder, files);
+		String rev1 = vcs.getCurrentRevision();
+		Version.v2.commit(folder, files);
+		Collection<VcsCommitItem> delta = vcs.getDelta(rev1);
+		assertEquals(1, delta.size());
+		assertEquals(1L,
+				delta.stream()
+						.filter(i -> i.getChangeType().equals(VcsChangeType.UPDATED) && i.getPath().equals("/one.txt"))
+						.count());
+	}
+
+	@Test
+	public void delta1ArgTwoChanges() throws Exception {
+		Version.v0.commit(folder, files);
+		Version.v1.commit(folder, files);
+		Version.v2.commit(folder, files);
+		final String rev2 = vcs.getCurrentRevision();
+		Version.v3.commit(folder, files);
+
+		Collection<VcsCommitItem> delta = vcs.getDelta(rev2);
+		assertEquals(2, delta.size());
+		assertEquals(1L,
+				delta.stream()
+						.filter(i -> i.getChangeType().equals(VcsChangeType.DELETED) && i.getPath().equals("/one.txt"))
+						.count());
+		assertEquals(1L,
+				delta.stream()
+						.filter(i -> i.getChangeType().equals(VcsChangeType.UPDATED) && i.getPath().equals("/two.txt"))
+						.count());
+	}
+
+	@Test
+	public void delta2ArgAfterFirstOfTwo() throws Exception {
+		Version.v0.commit(folder, files);
+		Version.v1.commit(folder, files);
+		VcsRepositoryDelta delta = vcs.getDelta(vcs.getFirstRevision(), vcs.getCurrentRevision());
+		assertEquals(1, delta.getCommits().size());
+		Collection<VcsCommitItem> commits = delta.getCommits().get(0).getItems();
+		assertEquals(1, commits.size());
+		assertEquals(1L, commits.stream()
+				.filter(i -> i.getChangeType().equals(VcsChangeType.ADDED) && i.getPath().equals("/two.txt")).count());
+	}
+
+	@Test
+	public void delta2ArgAfterFirstOfThree() throws Exception {
+		Version.v0.commit(folder, files);
+		Version.v1.commit(folder, files);
+		Version.v2.commit(folder, files);
+		Version.v3.commit(folder, files);
+
+		VcsRepositoryDelta delta = vcs.getDelta(vcs.getFirstRevision(), vcs.getCurrentRevision());
+		assertEquals(3, delta.getCommits().size());
+		Collection<VcsCommitItem> items = delta.getCompactedCommitItems();
+		assertEquals(2, items.size());
+		assertEquals(1L,
+				items.stream()
+					.filter(i -> i.getChangeType().equals(VcsChangeType.UPDATED) && i.getPath().equals("/two.txt"))
+					.count());
+		assertEquals(1L,
+				items.stream()
+					.filter(i -> i.getChangeType().equals(VcsChangeType.DELETED) && i.getPath().equals("/one.txt"))
+					.count());
+	}
+
+	@Test
+	public void importFile() throws Exception {
+		Version.v0.commit(folder, files);
+		String rev0 = vcs.getCurrentRevision();
+		Version.v1.commit(folder, files);
+		String rev1 = vcs.getCurrentRevision();
+		Version.v2.commit(folder, files);
+		String rev2 = vcs.getCurrentRevision();
+		Version.v3.commit(folder, files);
+		String rev3 = vcs.getCurrentRevision();
+		
+		File temp = vcs.importFile(rev0, "one.txt", folder.newFile());
+		List<String> lines = Files.lines(temp.toPath()).collect(Collectors.toList());
+		assertEquals(1, lines.size());
+		assertEquals("one", lines.get(0));
+
+		// Since JGitRepository produces paths starting with "/", importFile should be happy with them too
+		temp = vcs.importFile(rev0, "/one.txt", folder.newFile());
+		lines = Files.lines(temp.toPath()).collect(Collectors.toList());
+		assertEquals("/one.txt should work just as well as one.txt (number of lines)", 1, lines.size());
+		assertEquals("/one.txt should work just as well as one.txt (line content)", "one", lines.get(0));
+		
+		assertNull("two.txt does not exist in first revision", vcs.importFile(rev0, "two.txt", folder.newFile()));
+		
+		temp = vcs.importFile(rev1, "two.txt", folder.newFile());
+		lines = Files.lines(temp.toPath()).collect(Collectors.toList());
+		assertEquals(1, lines.size());
+		assertEquals("two", lines.get(0));
+		
+		temp = vcs.importFile(rev2, "one.txt", folder.newFile());
+		lines = Files.lines(temp.toPath()).collect(Collectors.toList());
+		assertEquals(2, lines.size());
+		assertEquals("one", lines.get(0));
+		assertEquals("one", lines.get(1));
+		
+		temp = vcs.importFile(rev3, "two.txt", folder.newFile());
+		lines = Files.lines(temp.toPath()).collect(Collectors.toList());
+		assertEquals(2, lines.size());
+		assertEquals("two", lines.get(0));
+		assertEquals("two", lines.get(1));
+		
+		assertNull("one.txt was deleted in fourth revision", vcs.importFile(rev3, "one.txt", folder.newFile()));
+	}
+
+	@Test
+	public void gitHonorsCurrentBranch() throws Exception {
+		Version.v0.commit(folder, files);
+		String rev0 = vcs.getCurrentRevision();
+		Version.v1.commit(folder, files);
+		String rev1 = vcs.getCurrentRevision();
+
+		final String newBranchName = "newbranch";
+		try (Git repo = Git.open(folder.getRoot())) {
+			repo.checkout().setName(newBranchName).setCreateBranch(true).call();
+			Version.v2.commit(folder, files);
+		}
+
+		assertEquals(rev1, vcs.getCurrentRevision());
+		VcsRepositoryDelta delta = vcs.getDelta(null, vcs.getCurrentRevision());
+		assertEquals(2, delta.getCommits().size());
+		assertEquals(rev0, delta.getCommits().get(0).getRevision());
+		assertEquals(rev1, delta.getCommits().get(1).getRevision());
+
+		delta = vcs.getDelta(rev0, vcs.getCurrentRevision());
+		assertEquals(1, delta.getCommits().size());
+		assertEquals(rev1, delta.getCommits().get(0).getRevision());
+
+		// Now try creating a VCS pointing at the new branch
+		
+		final JGitRepository repoNewBranch = new JGitRepository();
+		final IModelIndexer indexer = mock(IModelIndexer.class);
+		repoNewBranch.init(String.format(
+				"%s?%s=%s",
+				folder.getRoot().toURI().toString(),
+				JGitRepository.BRANCH_QPARAM,
+				newBranchName), indexer);
+		repoNewBranch.run();
+
+		assertEquals(3, repoNewBranch.getDelta(null,
+			repoNewBranch.getCurrentRevision()).getCommits().size());
+	}
+	
+}
diff --git a/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_1.xml b/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_1.xml
index 3ba7c7b..28c5dee 100644
--- a/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_1.xml
+++ b/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_1.xml
@@ -9,6 +9,7 @@
         <plugin name="org.hawk.modelio.exml.listeners.ModelioGraphChangeListener"/>
         <plugin name="org.hawk.modelio.exml.metamodel.ModelioMetaModelResourceFactory"/>
         <plugin name="org.hawk.modelio.exml.model.ModelioModelResourceFactory"/>
+        <plugin name="org.hawk.epsilon.emc.EOLQueryEngine"/>
     </plugins>
     <metamodels>
         <metamodel
@@ -33,6 +34,6 @@
     <repositories>
         <repository frozen="false"
             location="file:///home/antonio/Documents/mondo-hawk/tests/org.hawk.service.servlet.tests/resources/Zoo/"
-            pass="" type="org.hawk.localfolder.LocalFolder" user=""/>
+            type="org.hawk.localfolder.LocalFolder" />
     </repositories>
 </hawk>
diff --git a/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_2.xml b/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_2.xml
index 3178377..20c1ca8 100644
--- a/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_2.xml
+++ b/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_2.xml
@@ -6,6 +6,7 @@
         <plugin name="org.hawk.modelio.exml.metamodel.ModelioMetaModelResourceFactory"/>
         <plugin name="org.hawk.modelio.exml.model.ModelioModelResourceFactory"/>
         <plugin name="org.hawk.graph.updater.GraphModelUpdater"/>
+        <plugin name="org.hawk.epsilon.emc.EOLQueryEngine"/>
     </plugins>
     <metamodels>
         <metamodel
@@ -30,6 +31,6 @@
     <repositories>
         <repository frozen="false"
             location="file:///home/antonio/Documents/mondo-hawk/tests/org.hawk.service.servlet.tests/resources/Zoo/"
-            pass="" type="org.hawk.localfolder.LocalFolder" user=""/>
+            type="org.hawk.localfolder.LocalFolder"/>
     </repositories>
 </hawk>
diff --git a/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_3.xml b/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_3.xml
index b651d64..cf5f0f7 100644
--- a/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_3.xml
+++ b/tests/org.hawk.service.servlet.tests/resources/ServerConfig/instance_3.xml
@@ -9,6 +9,7 @@
         <plugin name="org.hawk.modelio.exml.listeners.ModelioGraphChangeListener"/>
         <plugin name="org.hawk.modelio.exml.metamodel.ModelioMetaModelResourceFactory"/>
         <plugin name="org.hawk.modelio.exml.model.ModelioModelResourceFactory"/>
+        <plugin name="org.hawk.epsilon.emc.EOLQueryEngine"/>
     </plugins>
     <metamodels>
         <metamodel
@@ -32,7 +33,7 @@
     </indexedAttributes>
     <repositories>
         <repository frozen="false"
-            location="file:///C:/Desktop/Hawk/Zoo/" pass=""
-            type="org.hawk.localfolder.LocalFolder" user=""/>
+            location="file:///C:/Desktop/Hawk/Zoo/"
+            type="org.hawk.localfolder.LocalFolder" />
     </repositories>
 </hawk>
diff --git a/tests/org.hawk.service.servlet.tests/src/org/hawk/service/servlet/config/ConfigFileParserTest.java b/tests/org.hawk.service.servlet.tests/src/org/hawk/service/servlet/config/ConfigFileParserTest.java
index 0c5a258..384e13e 100644
--- a/tests/org.hawk.service.servlet.tests/src/org/hawk/service/servlet/config/ConfigFileParserTest.java
+++ b/tests/org.hawk.service.servlet.tests/src/org/hawk/service/servlet/config/ConfigFileParserTest.java
@@ -1,12 +1,5 @@
-package org.hawk.service.servlet.config;

-import static org.junit.Assert.*;

-

-import java.io.File;

-

-import org.junit.Test;

-

 /*******************************************************************************

- * Copyright (c) 2017 Aston University

+ * Copyright (c) 2017-2019 Aston University

  * 

  * This program and the accompanying materials are made available under the

  * terms of the Eclipse Public License 2.0 which is available at

@@ -20,26 +13,54 @@
  *

  * Contributors:

  *     Orjuwan Al-Wadeai - Test For ConfigFileParser

+ *     Antonio Garcia-Dominguez - Use a temporary file

  ******************************************************************************/

 

+package org.hawk.service.servlet.config;

+

+import static org.junit.Assert.assertEquals;

+import static org.junit.Assert.assertNull;

+import static org.junit.Assert.assertTrue;

+

+import java.io.File;

+import java.io.IOException;

+

+import org.junit.Before;

+import org.junit.Rule;

+import org.junit.Test;

+import org.junit.rules.TemporaryFolder;

+

+import com.google.common.io.Files;

+

 public class ConfigFileParserTest {

 	private static final String SERVERCONFIG_PATH = "resources/ServerConfig/";

-	ConfigFileParser parser;

-	HawkInstanceConfig config;

-	final String xmlFilePath = SERVERCONFIG_PATH + "instance_3.xml";

-	final String xsdFilePath = SERVERCONFIG_PATH + "HawkServerConfigurationSchema.xsd";

+	private static final String XML_PATH = SERVERCONFIG_PATH + "instance_3.xml";

+

+	@Rule

+	public TemporaryFolder folder = new TemporaryFolder();

+

+	private ConfigFileParser parser;

+	private HawkInstanceConfig config;

+	private File fConfig;

+

+	@Before

+	public void setup() throws IOException {

+		File fOriginal = new File(XML_PATH);

+		fConfig = folder.newFile(fOriginal.getName());

+		Files.copy(fOriginal, fConfig);

+	}

 

 	@Test

 	public void testParseFile() {

 		parseXml();

-		

-		// config config values

+

+		// config values

 		assertEquals("Instance Name:", "instance_3", config.getName());

 		assertEquals("Instance Backend:", "org.hawk.orientdb.OrientDatabase", config.getBackend());

 		assertEquals("Instance Delay Max:", 512000, config.getDelayMax());

 		assertEquals("Instance Delay Min:", 5000, config.getDelayMin());

-		

-		assertEquals("Instance Number of Plugins:", 7, config.getPlugins().size());

+

+		assertEquals("Instance Number of Plugins:", 8, config.getPlugins().size());

 		assertTrue(config.getPlugins().contains("org.hawk.emf.metamodel.EMFMetaModelResourceFactory"));

 		assertTrue(config.getPlugins().contains("org.hawk.emf.model.EMFModelResourceFactory"));

 		assertTrue(config.getPlugins().contains("org.hawk.graph.syncValidationListener.SyncValidationListener"));

@@ -47,53 +68,49 @@
 		assertTrue(config.getPlugins().contains("org.hawk.modelio.exml.listeners.ModelioGraphChangeListener"));

 		assertTrue(config.getPlugins().contains("org.hawk.modelio.exml.metamodel.ModelioMetaModelResourceFactory"));

 		assertTrue(config.getPlugins().contains("org.hawk.modelio.exml.model.ModelioModelResourceFactory"));

-		

+		assertTrue(config.getPlugins().contains("org.hawk.epsilon.emc.EOLQueryEngine"));

+

 		assertEquals("Instance Number of Metamodels:", 1, config.getMetamodels().size());

-		assertEquals("C:/resources/metamodel/metamodel_descriptor.xml",config.getMetamodels().get(0).getLocation());

-		assertEquals("",config.getMetamodels().get(0).getUri());

-		

+		assertEquals("C:/resources/metamodel/metamodel_descriptor.xml", config.getMetamodels().get(0).getLocation());

+		assertEquals("", config.getMetamodels().get(0).getUri());

+

 		assertEquals("Instance Number of Derived Attributes:", 1, config.getDerivedAttributes().size());

 		assertEquals("modelio://Modeliosoft.Standard/2.0.00", config.getDerivedAttributes().get(0).getMetamodelUri());

 		assertEquals("Class", config.getDerivedAttributes().get(0).getTypeName());

 		assertEquals("ownedOperationCount", config.getDerivedAttributes().get(0).getAttributeName());

 		assertEquals("Integer", config.getDerivedAttributes().get(0).getAttributeType());

-		

+

 		assertEquals(false, config.getDerivedAttributes().get(0).isMany());

 		assertEquals(false, config.getDerivedAttributes().get(0).isOrdered());

 		assertEquals(false, config.getDerivedAttributes().get(0).isUnique());

-		

-		assertEquals("EOLQueryEngine", config.getDerivedAttributes().get(0).getDerivationLanguage());

-		assertEquals("\n\t\t\t\t\t\treturn self.OwnedOperation.size;\n\t\t\t\t\t", config.getDerivedAttributes().get(0).getDerivationLogic());

 

-		

-	

+		assertEquals("EOLQueryEngine", config.getDerivedAttributes().get(0).getDerivationLanguage());

+		assertEquals("\n\t\t\t\t\t\treturn self.OwnedOperation.size;\n\t\t\t\t\t",

+				config.getDerivedAttributes().get(0).getDerivationLogic());

+

 		assertEquals("Instance Number of Indexed Attributes:", 1, config.getIndexedAttributes().size());

 		assertEquals("modelio://Modeliosoft.Standard/2.0.00", config.getIndexedAttributes().get(0).getMetamodelUri());

 		assertEquals("Class", config.getIndexedAttributes().get(0).getTypeName());

 		assertEquals("Name", config.getIndexedAttributes().get(0).getAttributeName());

-		

+

 		assertEquals("Instance Number of :Repositories", 1, config.getRepositories().size());

 		assertEquals("file:///C:/Desktop/Hawk/Zoo/", config.getRepositories().get(0).getLocation());

 		assertEquals("org.hawk.localfolder.LocalFolder", config.getRepositories().get(0).getType());

-		assertEquals("", config.getRepositories().get(0).getUser());

-		assertEquals("", config.getRepositories().get(0).getPass());

+		assertNull(config.getRepositories().get(0).getUser());

+		assertNull(config.getRepositories().get(0).getPass());

 		assertEquals(false, config.getRepositories().get(0).isFrozen());

 	}

 

-

 	private void parseXml() {

 		parser = new ConfigFileParser();

-		config = parser.parse(new File(xmlFilePath));

+		config = parser.parse(fConfig);

 	}

 

-

 	@Test

 	public void testSaveConfigToFile() {

-		parseXml();	

+		parseXml();

 		parser.saveConfigAsXml(config);

 		testParseFile();

 	}

 

-

 }

-

diff --git a/tests/org.hawk.service.servlet.tests/src/org/hawk/service/servlet/config/HawkServerConfiguratorTest.java b/tests/org.hawk.service.servlet.tests/src/org/hawk/service/servlet/config/HawkServerConfiguratorTest.java
index 01e4393..c758913 100644
--- a/tests/org.hawk.service.servlet.tests/src/org/hawk/service/servlet/config/HawkServerConfiguratorTest.java
+++ b/tests/org.hawk.service.servlet.tests/src/org/hawk/service/servlet/config/HawkServerConfiguratorTest.java
@@ -45,50 +45,48 @@
 public class HawkServerConfiguratorTest {

 	private static final String SERVERCONFIG_PATH = "resources/ServerConfig/";

 

-	private static final String metamodelDescriptor = "resources/metamodel/metamodel_descriptor.xml";

-	private static final String modelsZoo = "resources/Zoo";

+	private static final String MMDESC_PATH = "resources/metamodel/metamodel_descriptor.xml";

+	private static final String ZOO_PATH = "resources/Zoo";

 

-	static final String xmlFileName_1 = "instance_1.xml";

-	static final String xmlFileName_2 = "instance_2.xml";

+	private static final String INSTANCE1_PATH = "instance_1.xml";

+	private static final String INSTANCE2_PATH = "instance_2.xml";

 

-	static HManager manager;

-	static File configurationFolder;

-

+	private static HManager manager;

+	private static File configurationFolder;

 	private static HawkThriftIface hawkIface;

-

 	private static HawkServerConfigurator serverConfigurator;

-

 	private boolean finishedRetrievingInstanceInfo;

-	

-	Collection<IVcsManager> repos;

-	Collection<IndexedAttributeParameters> derivedAttributes;

-	Collection<IndexedAttributeParameters> indexedAttributes;

-	List<String> metamodels;

-	

+

+	private Collection<IVcsManager> repos;

+	private Collection<IndexedAttributeParameters> derivedAttributes;

+	private Collection<IndexedAttributeParameters> indexedAttributes;

+	private List<String> metamodels;

+

 	@BeforeClass

-	static public void setup() {

+	public static void setup() throws IOException {

 		initConfigurationFolder();

-		setupAndCopyPathsInXmlFile(xmlFileName_1);

-		setupAndCopyPathsInXmlFile(xmlFileName_2);

+		setupAndCopyPathsInXmlFile(INSTANCE1_PATH);

+		setupAndCopyPathsInXmlFile(INSTANCE2_PATH);

+

 		manager = HManager.getInstance();

 		hawkIface = new HawkThriftIface(ThriftProtocol.TUPLE, null, null);

 		serverConfigurator = new HawkServerConfigurator(hawkIface);

-		serverConfigurator.loadHawkServerConfigurations();		

+		serverConfigurator.loadHawkServerConfigurations();

 	}

 

 	@Test

-	public void testHawkServerConfigurator_instance1() throws Exception {		

-		testhawkInstance(xmlFileName_1);

+	public void testHawkServerConfigurator_instance1() throws Exception {

+		testhawkInstance(INSTANCE1_PATH);

 	}

 

 	@Test

 	public void testHawkServerConfigurator_instance2() throws Exception {

-		testhawkInstance(xmlFileName_2);

+		testhawkInstance(INSTANCE2_PATH);

 	}

 

 	private void testhawkInstance(String xmlFileName) throws Exception {

 		final ConfigFileParser parser = new ConfigFileParser();

-		final HawkInstanceConfig config = parser.parse(new File(SERVERCONFIG_PATH, xmlFileName));

+		final HawkInstanceConfig config = parser.parse(new File(configurationFolder, xmlFileName));

 		final HModel instance = manager.getHawkByName(config.getName());

 

 		assertTrue(instance.isRunning());

@@ -100,7 +98,7 @@
 		finishedRetrievingInstanceInfo = false;

 		instance.configurePolling(0, 0);

 

-		instance.getHawk().getModelIndexer().scheduleTask(new Callable<Void>(){

+		instance.getHawk().getModelIndexer().scheduleTask(new Callable<Void>() {

 			@Override

 			public Void call() {

 				metamodels = instance.getRegisteredMetamodels();

@@ -110,27 +108,28 @@
 				finishedRetrievingInstanceInfo = true;

 				return null;

 			}

-		}, 0);;

-		

-		while(!finishedRetrievingInstanceInfo) {

+		}, 0);

+		;

+

+		while (!finishedRetrievingInstanceInfo) {

 			try {

 				Thread.sleep(2000);

 			} catch (InterruptedException e) {

 				e.printStackTrace();

 			}

 		}

-		

-		// repositories

-		assertTrue(compareRepositories(config.getRepositories(), repos));

 

-		//		 metamodels

+		// repositories

+		compareRepositories(config.getRepositories(), repos);

+

+		// metamodels

 		assertTrue(metamodels.contains("modelio://ModelioMetaPackage"));

 		assertTrue(metamodels.contains("modelio://Modeliosoft.Archimate/1.0.03"));

 		assertTrue(metamodels.contains("modelio://Modeliosoft.Analyst/2.0.00"));

 		assertTrue(metamodels.contains("modelio://Modeliosoft.modelio.kernel/1.0.00"));

 		assertTrue(metamodels.contains("modelio://Modeliosoft.Standard/2.0.00"));

 		assertTrue(metamodels.contains("modelio://Modeliosoft.Infrastructure/2.1.00"));

-		

+

 		// derived attributes

 		assertTrue(derivedAttributes.size() > 0);

 		assertTrue(derivedAttributes.contains(config.getDerivedAttributes().get(0)));

@@ -139,93 +138,59 @@
 		assertEquals(indexedAttributes.size(), config.getIndexedAttributes().size());

 		assertTrue(indexedAttributes.contains(config.getIndexedAttributes().get(0)));

 	}

-	

-	private boolean compareRepositories(List<RepositoryParameters> repositories, Collection<IVcsManager> collection) {

 

-		if(repositories.size() != collection.size()) {

-			return false;

-		}

+	private void compareRepositories(List<RepositoryParameters> repositories, Collection<IVcsManager> collection) {

+		assertEquals(repositories.size(), collection.size());

 

-		boolean allEqual = true;

 		for (IVcsManager vcsManager : collection) {

-			boolean FoundAndEqual = false;

-			for(RepositoryParameters repos : repositories) {

-				if( repos.getType().equals(vcsManager.getType()) && 

-						repos.getLocation().equals(vcsManager.getLocation()) &&

-						repos.isFrozen() == vcsManager.isFrozen())

-				{

-					FoundAndEqual = true;

-					if(vcsManager.getUsername() != null) {

-						FoundAndEqual = repos.getUser().equals(vcsManager.getUsername()); 

-					} else {

-						FoundAndEqual = (repos.getUser().isEmpty() || repos.getUser() == null);

-					}

+			boolean foundAndEqual = false;

 

-					if(vcsManager.getPassword() != null) {

-						FoundAndEqual = repos.getUser().equals(vcsManager.getPassword()); 

-					} else {

-						FoundAndEqual = (repos.getPass().isEmpty() || repos.getPass() == null);

-					}

-

-					break;

-				}

+			for (RepositoryParameters repos : repositories) {

+				foundAndEqual = foundAndEqual ||

+					(vcsManager.getType().equals(repos.getType())

+					 && vcsManager.getLocation().equals(repos.getLocation())

+					 && vcsManager.isFrozen() == repos.isFrozen()

+					 && (vcsManager.getUsername() == null

+							? repos.getUser() == null

+							: vcsManager.getUsername().equals(repos.getUser()))

+					 && (vcsManager.getPassword() == null

+							? repos.getPass() == null

+							: vcsManager.getPassword().equals(repos.getPass())));

 			}

 

-			if(!FoundAndEqual) {

-				allEqual = false;

-				break;

-			}

+			assertTrue("Should be able to find repo " + vcsManager.getLocation(), foundAndEqual);

 		}

-

-		return allEqual;

 	}

 

-	static public void setupAndCopyPathsInXmlFile(String xmlFileName) {

+	private static void setupAndCopyPathsInXmlFile(String xmlFileName) throws IOException {

+		final File fOriginal = new File(SERVERCONFIG_PATH, xmlFileName);

+		final File fCopy = new File(configurationFolder, xmlFileName);

+		Files.copy(fOriginal, fCopy);

+

 		ConfigFileParser parser = new ConfigFileParser();

-		HawkInstanceConfig config = parser.parse(new File(SERVERCONFIG_PATH, xmlFileName));

-		File location = new File(metamodelDescriptor);

+		HawkInstanceConfig config = parser.parse(fCopy);

+		File location = new File(MMDESC_PATH);

 		config.getMetamodels().get(0).setLocation(location.getAbsolutePath());

-		config.getRepositories().get(0).setLocation(getLocalFilePath(modelsZoo));

+		config.getRepositories().get(0).setLocation(getLocalFilePath(ZOO_PATH));

 		parser.saveConfigAsXml(config);

-

-		// copy

-		try {

-			Files.copy(new File(SERVERCONFIG_PATH, xmlFileName), new File(configurationFolder, xmlFileName));

-		} catch (IOException e) {

-			e.printStackTrace();

-		}

 	}

 

-	static public String getLocalFilePath(String fileLocation) {

+	private static String getLocalFilePath(String fileLocation) throws IOException {

 		String repositoryURI = fileLocation;

+		Path path = Paths.get(fileLocation);

 

-		try {

-			Path path;

-			path = Paths.get(fileLocation);

+		File canonicalFile = null;

+		canonicalFile = path.toFile().getCanonicalFile();

 

-			File canonicalFile = null;

-			canonicalFile = path.toFile().getCanonicalFile();

-

-			Path rootLocation = canonicalFile.toPath();

-			repositoryURI = rootLocation.toUri().toString();

-

-		} catch (IOException e) {

-			e.printStackTrace();

-		}

-

+		Path rootLocation = canonicalFile.toPath();

+		repositoryURI = rootLocation.toUri().toString();

 

 		return repositoryURI;

 	}

 

-	static public void initConfigurationFolder() {

-		try {

-			URL installURL = Platform.getConfigurationLocation().getURL();

-			String path = FileLocator.toFileURL(installURL).getPath();

-

-			configurationFolder = new File(path);

-		} catch (IOException e) {

-			e.printStackTrace();

-		}

+	private static void initConfigurationFolder() throws IOException {

+		final URL installURL = Platform.getConfigurationLocation().getURL();

+		final String path = FileLocator.toFileURL(installURL).getPath();

+		configurationFolder = new File(path);

 	}

 }

-

diff --git a/tests/org.hawk.timeaware.tests/META-INF/MANIFEST.MF b/tests/org.hawk.timeaware.tests/META-INF/MANIFEST.MF
index 50451b8..9a98213 100644
--- a/tests/org.hawk.timeaware.tests/META-INF/MANIFEST.MF
+++ b/tests/org.hawk.timeaware.tests/META-INF/MANIFEST.MF
@@ -27,5 +27,7 @@
  org.hawk.service.api;bundle-version="1.2.0",
  javax.servlet;bundle-version="3.1.0",
  org.hawk.service.artemis.server;bundle-version="1.2.0",
- org.apache.thrift;bundle-version="0.9.3"
+ org.apache.thrift;bundle-version="0.9.3",
+ org.hawk.jgit;bundle-version="1.2.0",
+ org.eclipse.jgit;bundle-version="5.5.0"
 Bundle-ActivationPolicy: lazy
diff --git a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/AbstractTimeAwareModelIndexingTest.java b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/AbstractTimeAwareModelIndexingTest.java
index 6dc585a..eae0a44 100644
--- a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/AbstractTimeAwareModelIndexingTest.java
+++ b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/AbstractTimeAwareModelIndexingTest.java
@@ -16,8 +16,6 @@
  ******************************************************************************/
 package org.hawk.timeaware.tests;
 
-import static org.junit.Assert.assertEquals;
-
 import java.io.File;
 import java.util.Collections;
 import java.util.Map;
diff --git a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/DerivedAttributeHistoryTest.java b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/DerivedAttributeHistoryTest.java
index 92f5306..193aaa1 100644
--- a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/DerivedAttributeHistoryTest.java
+++ b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/DerivedAttributeHistoryTest.java
@@ -23,7 +23,6 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
-import java.util.concurrent.Callable;
 
 import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.resource.Resource;
@@ -76,23 +75,19 @@
 	public void computedForAllVersions() throws Throwable {
 		oneDerivedAttribute();
 		requestSVNIndex(svnRepository);
+		scheduleAndWait(() -> {
+			// .all works on revision 0
+			assertTrue((boolean)
+				timeAwareEOL("return Tree.latest.all.first.always(v|v.Important.isDefined());"));
+			assertTrue((boolean)
+					timeAwareEOL("return Tree.latest.all.first.earliest.Important;"));
+			assertFalse((boolean)
+					timeAwareEOL("return Tree.latest.all.first.latest.Important;"));
 		
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				// .all works on revision 0
-				assertTrue((boolean)
-					timeAwareEOL("return Tree.latest.all.first.always(v|v.Important.isDefined());"));
-				assertTrue((boolean)
-						timeAwareEOL("return Tree.latest.all.first.earliest.Important;"));
-				assertFalse((boolean)
-						timeAwareEOL("return Tree.latest.all.first.latest.Important;"));
-
-				assertEquals(1,
-					timeAwareEOL("return Tree.latest.all.first.whenAnnotated('Important').versions.size;"));
-
-				return null;
-			}
+			assertEquals(1,
+				timeAwareEOL("return Tree.latest.all.first.whenAnnotated('Important').versions.size;"));
+		
+			return null;
 		});
 	}
 
@@ -100,21 +95,18 @@
 	public void whenComposability() throws Throwable {
 		twoDerivedAttributeRepository();
 
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				// whenAnnotated
-				assertEquals(2,
-					timeAwareEOL("return Tree.earliest.next.all.first.whenAnnotated('Important').versions.size;"));
-				assertEquals(2,
-						timeAwareEOL("return Tree.earliest.next.all.first.whenAnnotated('HasChildren').versions.size;"));
-				assertEquals(1,
-						timeAwareEOL("return Tree.earliest.next.all.first.whenAnnotated('Important').whenAnnotated('HasChildren').versions.size;"));
-				assertEquals(1,
-						timeAwareEOL("return Tree.earliest.next.all.first.whenAnnotated('HasChildren').whenAnnotated('Important').versions.size;"));
-
-				return null;
-			}
+		scheduleAndWait(() -> {
+			// whenAnnotated
+			assertEquals(2,
+				timeAwareEOL("return Tree.earliest.next.all.first.whenAnnotated('Important').versions.size;"));
+			assertEquals(2,
+					timeAwareEOL("return Tree.earliest.next.all.first.whenAnnotated('HasChildren').versions.size;"));
+			assertEquals(1,
+					timeAwareEOL("return Tree.earliest.next.all.first.whenAnnotated('Important').whenAnnotated('HasChildren').versions.size;"));
+			assertEquals(1,
+					timeAwareEOL("return Tree.earliest.next.all.first.whenAnnotated('HasChildren').whenAnnotated('Important').versions.size;"));
+		
+			return null;
 		});
 	}
 
@@ -122,15 +114,12 @@
 	public void sinceAnnotated() throws Throwable {
 		twoDerivedAttributeRepository();
 
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				assertEquals(4, timeAwareEOL("return Tree.earliest.next.all.first.earliest.sinceAnnotated('Important').versions.size;"));
-				assertEquals(1, timeAwareEOL("return Tree.earliest.next.all.first.earliest.next.sinceAnnotated('Important').versions.size;"));
-				assertEquals(2, timeAwareEOL("return Tree.earliest.next.all.first.earliest.sinceAnnotated('HasChildren').versions.size;"));
-
-				return null;
-			}
+		scheduleAndWait(() -> {
+			assertEquals(4, timeAwareEOL("return Tree.earliest.next.all.first.earliest.sinceAnnotated('Important').versions.size;"));
+			assertEquals(1, timeAwareEOL("return Tree.earliest.next.all.first.earliest.next.sinceAnnotated('Important').versions.size;"));
+			assertEquals(2, timeAwareEOL("return Tree.earliest.next.all.first.earliest.sinceAnnotated('HasChildren').versions.size;"));
+		
+			return null;
 		});
 	}
 
@@ -138,15 +127,12 @@
 	public void afterAnnotated() throws Throwable {
 		twoDerivedAttributeRepository();
 
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				assertEquals(3, timeAwareEOL("return Tree.earliest.next.all.first.earliest.afterAnnotated('Important').versions.size;"));
-				assertFalse((boolean) timeAwareEOL("return Tree.earliest.next.all.first.earliest.next.afterAnnotated('Important').isDefined();"));
-				assertEquals(1, timeAwareEOL("return Tree.earliest.next.all.first.earliest.afterAnnotated('HasChildren').versions.size;"));
-
-				return null;
-			}
+		scheduleAndWait(() -> {
+			assertEquals(3, timeAwareEOL("return Tree.earliest.next.all.first.earliest.afterAnnotated('Important').versions.size;"));
+			assertFalse((boolean) timeAwareEOL("return Tree.earliest.next.all.first.earliest.next.afterAnnotated('Important').isDefined();"));
+			assertEquals(1, timeAwareEOL("return Tree.earliest.next.all.first.earliest.afterAnnotated('HasChildren').versions.size;"));
+		
+			return null;
 		});
 	}
 
@@ -154,18 +140,15 @@
 	public void untilAnnotated() throws Throwable {
 		twoDerivedAttributeRepository();
 
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				assertEquals(1, timeAwareEOL("return Tree.earliest.next.all.first.earliest.untilAnnotated('Important').versions.size;"));
-				assertEquals(3, timeAwareEOL("return Tree.earliest.next.all.first.earliest.next.sinceThen.untilAnnotated('Important').versions.size;"));
-				assertEquals(3, timeAwareEOL("return Tree.earliest.next.all.first.earliest.untilAnnotated('HasChildren').versions.size;"));
-
-				assertEquals(1, timeAwareEOL("return Tree.earliest.next.all.first.earliest.untilAnnotated('HasChildren').untilAnnotated('Important').versions.size;"));
-				assertFalse((boolean) timeAwareEOL("return Tree.earliest.next.all.first.earliest.untilAnnotated('Important').untilAnnotated('HasChildren').isDefined();"));
-
-				return null;
-			}
+		scheduleAndWait(() -> {
+			assertEquals(1, timeAwareEOL("return Tree.earliest.next.all.first.earliest.untilAnnotated('Important').versions.size;"));
+			assertEquals(3, timeAwareEOL("return Tree.earliest.next.all.first.earliest.next.sinceThen.untilAnnotated('Important').versions.size;"));
+			assertEquals(3, timeAwareEOL("return Tree.earliest.next.all.first.earliest.untilAnnotated('HasChildren').versions.size;"));
+		
+			assertEquals(1, timeAwareEOL("return Tree.earliest.next.all.first.earliest.untilAnnotated('HasChildren').untilAnnotated('Important').versions.size;"));
+			assertFalse((boolean) timeAwareEOL("return Tree.earliest.next.all.first.earliest.untilAnnotated('Important').untilAnnotated('HasChildren').isDefined();"));
+		
+			return null;
 		});
 	}
 
@@ -173,14 +156,11 @@
 	public void beforeAnnotated() throws Throwable {
 		twoDerivedAttributeRepository();
 
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				assertFalse((boolean) timeAwareEOL("return Tree.earliest.next.all.first.earliest.beforeAnnotated('Important').isDefined();"));
-				assertEquals(2, timeAwareEOL("return Tree.earliest.next.all.first.earliest.next.sinceThen.beforeAnnotated('Important').versions.size;"));
-				assertEquals(2, timeAwareEOL("return Tree.earliest.next.all.first.earliest.beforeAnnotated('HasChildren').versions.size;"));
-				return null;
-			}
+		scheduleAndWait(() -> {
+			assertFalse((boolean) timeAwareEOL("return Tree.earliest.next.all.first.earliest.beforeAnnotated('Important').isDefined();"));
+			assertEquals(2, timeAwareEOL("return Tree.earliest.next.all.first.earliest.next.sinceThen.beforeAnnotated('Important').versions.size;"));
+			assertEquals(2, timeAwareEOL("return Tree.earliest.next.all.first.earliest.beforeAnnotated('HasChildren').versions.size;"));
+			return null;
 		});
 	}
 
@@ -197,7 +177,7 @@
 
 		parentLabelRepository();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals("Root2", timeAwareEOL("return Tree.latest.all.selectOne(l|l.latest.label='Child').latest.parentLabel;"));
 			assertEquals(3, timeAwareEOL("return Tree.latest.all.selectOne(l|l.latest.label='Child').versions.size;"));
 			return null;
@@ -217,7 +197,7 @@
 
 		parentLabelRepository();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals("Root2", timeAwareEOL("return Tree.latest.all.selectOne(l|l.latest.label='Child').latest.parentLabel;"));
 			assertEquals(3, timeAwareEOL("return Tree.latest.all.selectOne(l|l.latest.label='Child').versions.size;"));
 			return null;
diff --git a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/FileContextTimeAwareEOLQueryEngineTest.java b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/FileContextTimeAwareEOLQueryEngineTest.java
index ac3a518..71b934a 100644
--- a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/FileContextTimeAwareEOLQueryEngineTest.java
+++ b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/FileContextTimeAwareEOLQueryEngineTest.java
@@ -20,7 +20,6 @@
 
 import java.io.File;
 import java.util.Arrays;
-import java.util.concurrent.Callable;
 
 import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.resource.Resource;
@@ -34,7 +33,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
-import org.tmatesoft.svn.core.SVNCommitInfo;
 
 /**
  * Tests for time-aware queries that use additional context parameters, e.g.
@@ -81,7 +79,7 @@
 		rTreeA.getContents().add(a1);
 		rTreeA.save(null);
 		svnRepositoryA.add(fTreeA);
-		final SVNCommitInfo cInfoA1 = svnRepositoryA.commit("first in A");
+		svnRepositoryA.commit("first in A");
 
 		// Now do the same at least one second later, in the second SVN repository
 		final long startMillis = System.currentTimeMillis();
@@ -95,7 +93,7 @@
 		rTreeB.getContents().add(b1);
 		rTreeB.save(null);
 		svnRepositoryB.add(fTreeB);
-		final SVNCommitInfo cInfoB1 = svnRepositoryB.commit("first in B");
+		svnRepositoryB.commit("first in B");
 
 		// Run the SVN indexing now
 		requestSVNIndex();
@@ -114,9 +112,9 @@
 
 	private void requestSVNIndex() throws Throwable {
 		requestSVNIndex(svnRepositoryA);
-		waitForSync();
+		scheduleAndWait();
 
 		requestSVNIndex(svnRepositoryB);
-		waitForSync();
+		scheduleAndWait();
 	}
 }
diff --git a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/GitNodeHistoryTest.java b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/GitNodeHistoryTest.java
new file mode 100644
index 0000000..7416996
--- /dev/null
+++ b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/GitNodeHistoryTest.java
@@ -0,0 +1,154 @@
+/*******************************************************************************
+ * Copyright (c) 2018-2019 Aston University.
+ * 
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License, v. 2.0 are satisfied: GNU General Public License, version 3.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-3.0
+ *
+ * Contributors:
+ *     Antonio Garcia-Dominguez - initial API and implementation
+ ******************************************************************************/
+package org.hawk.timeaware.tests;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.util.Arrays;
+
+import org.eclipse.emf.common.util.URI;
+import org.eclipse.emf.ecore.resource.Resource;
+import org.eclipse.jgit.api.Git;
+import org.hawk.backend.tests.BackendTestSuite;
+import org.hawk.backend.tests.factories.IGraphDatabaseFactory;
+import org.hawk.git.JGitRepository;
+import org.hawk.integration.tests.emf.EMFModelSupportFactory;
+import org.hawk.integration.tests.mm.Tree.Tree;
+import org.hawk.integration.tests.mm.Tree.TreeFactory;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests for the time-aware indexing of model element nodes, using Subversion.
+ */
+@RunWith(Parameterized.class)
+public class GitNodeHistoryTest extends AbstractTimeAwareModelIndexingTest {
+
+	@Rule
+	public TemporaryFolder folder = new TemporaryFolder();
+
+	@Parameters(name = "{0}")
+    public static Iterable<Object[]> params() {
+    	Object[][] baseParams = BackendTestSuite.timeAwareBackends();
+
+    	Object[][] params = new Object[baseParams.length][];
+    	for (int i = 0; i < baseParams.length; i++) {
+    		params[i] = new Object[] { baseParams[i][0], new EMFModelSupportFactory() };
+    	}
+
+    	return Arrays.asList(params);
+    }
+
+	public GitNodeHistoryTest(IGraphDatabaseFactory dbFactory, IModelSupportFactory modelSupportFactory) {
+		super(dbFactory, modelSupportFactory);
+	}
+
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+		Git.init().setDirectory(folder.getRoot()).call();
+	}
+
+	@Override
+	protected void setUpMetamodels() throws Exception {
+		indexer.registerMetamodels(new File(TREE_MM_PATH));
+	}
+
+	@Test
+	public void defaultBranch() throws Throwable {
+		final File fTree = new File(folder.getRoot(), "root.xmi");
+		final Resource rTree = rsTree.createResource(URI.createFileURI(fTree.getAbsolutePath()));
+
+		final Tree tRoot = TreeFactory.eINSTANCE.createTree();
+		tRoot.setLabel("root");
+		rTree.getContents().add(tRoot);
+		rTree.save(null);
+
+		try (Git git = Git.open(folder.getRoot())) {
+			git.add().addFilepattern(fTree.getName()).call();
+			git.commit().setMessage("first commit").call();
+
+			final Tree tChild = TreeFactory.eINSTANCE.createTree();
+			tChild.setLabel("child");
+			tRoot.getChildren().add(tChild);
+			rTree.save(null);
+			git.commit().setAll(true).setMessage("second commit").call();
+		}
+
+		JGitRepository vcs = new JGitRepository();
+		vcs.init(folder.getRoot().getAbsolutePath(), indexer);
+		vcs.run();
+		indexer.addVCSManager(vcs, true);
+
+		scheduleAndWait(() -> {
+			// 3 versions: beginning of time + 2 commits
+			assertEquals(3, timeAwareEOL("return Tree.versions.size;"));
+			assertEquals(2, timeAwareEOL("return Tree.latest.all.size;"));
+			assertEquals(1, timeAwareEOL("return Tree.latest.prev.all.size;"));
+			assertEquals(0, timeAwareEOL("return Tree.latest.prev.prev.all.size;"));
+			return null;
+		});
+	}
+
+	@Test
+	public void nonDefaultBranch() throws Throwable {
+		final File fTree = new File(folder.getRoot(), "root.xmi");
+		final Resource rTree = rsTree.createResource(URI.createFileURI(fTree.getAbsolutePath()));
+
+		final Tree tRoot = TreeFactory.eINSTANCE.createTree();
+		tRoot.setLabel("root");
+		rTree.getContents().add(tRoot);
+		rTree.save(null);
+
+		try (Git git = Git.open(folder.getRoot())) {
+			git.add().addFilepattern(fTree.getName()).call();
+			git.commit().setMessage("first commit").call();
+
+			final Tree tChild = TreeFactory.eINSTANCE.createTree();
+			tChild.setLabel("child");
+			tRoot.getChildren().add(tChild);
+			rTree.save(null);
+			git.commit().setAll(true).setMessage("second commit").call();
+
+			git.checkout().setName("newbranch").setCreateBranch(true).call();
+			tChild.setLabel("changed");
+			rTree.save(null);
+			git.commit().setAll(true).setMessage("third commit in new branch").call();
+
+			git.checkout().setName("master").call();
+		}
+
+		JGitRepository vcs = new JGitRepository();
+		vcs.init(folder.getRoot().toURI().toString() + "?branch=newbranch", indexer);
+		vcs.run();
+		indexer.addVCSManager(vcs, true);
+
+		scheduleAndWait(() -> {
+			// 3 versions: beginning of time + 2 commits
+			assertEquals(1, timeAwareEOL("return Tree.latest.all.select(t|t.label='child').size;"));
+			assertEquals("changed", timeAwareEOL("return Tree.latest.all.selectOne(t|t.label='child').latest.label;"));
+			assertEquals(2, timeAwareEOL("return Tree.latest.all.selectOne(t|t.label='child').versions.size;"));
+			return null;
+		});
+	}
+	
+}
diff --git a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/NodeHistoryTest.java b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/SubversionNodeHistoryTest.java
similarity index 89%
rename from tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/NodeHistoryTest.java
rename to tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/SubversionNodeHistoryTest.java
index a9cb661..0638c68 100644
--- a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/NodeHistoryTest.java
+++ b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/SubversionNodeHistoryTest.java
@@ -24,16 +24,13 @@
 
 import java.io.File;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.Callable;
 
 import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.resource.Resource;
 import org.hawk.backend.tests.BackendTestSuite;
 import org.hawk.backend.tests.factories.IGraphDatabaseFactory;
 import org.hawk.core.graph.timeaware.ITimeAwareGraphNode;
-import org.hawk.epsilon.emc.EOLQueryEngine;
 import org.hawk.epsilon.emc.wrappers.GraphNodeWrapper;
 import org.hawk.integration.tests.emf.EMFModelSupportFactory;
 import org.hawk.svn.tests.rules.TemporarySVNRepository;
@@ -46,26 +43,26 @@
 import org.junit.runners.Parameterized.Parameters;
 
 /**
- * Tests for the time-aware indexing of model element nodes.
+ * Tests for the time-aware indexing of model element nodes, using Subversion.
  */
 @RunWith(Parameterized.class)
-public class NodeHistoryTest extends AbstractTimeAwareModelIndexingTest {
+public class SubversionNodeHistoryTest extends AbstractTimeAwareModelIndexingTest {
 	@Rule
 	public TemporarySVNRepository svnRepository = new TemporarySVNRepository();
 
 	@Parameters(name = "{0}")
-    public static Iterable<Object[]> params() {
-    	Object[][] baseParams = BackendTestSuite.timeAwareBackends();
+	public static Iterable<Object[]> params() {
+		Object[][] baseParams = BackendTestSuite.timeAwareBackends();
 
-    	Object[][] params = new Object[baseParams.length][];
-    	for (int i = 0; i < baseParams.length; i++) {
-    		params[i] = new Object[] { baseParams[i][0], new EMFModelSupportFactory() };
-    	}
+		Object[][] params = new Object[baseParams.length][];
+		for (int i = 0; i < baseParams.length; i++) {
+			params[i] = new Object[] { baseParams[i][0], new EMFModelSupportFactory() };
+		}
 
-    	return Arrays.asList(params);
-    }
+		return Arrays.asList(params);
+	}
 
-	public NodeHistoryTest(IGraphDatabaseFactory dbFactory, IModelSupportFactory modelSupportFactory) {
+	public SubversionNodeHistoryTest(IGraphDatabaseFactory dbFactory, IModelSupportFactory modelSupportFactory) {
 		super(dbFactory, modelSupportFactory);
 	}
 
@@ -77,7 +74,7 @@
 	@Test
 	public void travelToMissingTimepointReturnsNull() throws Throwable {
 		twoCommitTree();
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			GraphNodeWrapper gnw = (GraphNodeWrapper) timeAwareEOL("return Tree.latest.prev.all.first;");
 			assertNotNull(gnw.getNode());
 			assertNull(((ITimeAwareGraphNode) gnw.getNode()).travelInTime(ITimeAwareGraphNode.NO_SUCH_INSTANT));
@@ -88,22 +85,20 @@
 	@Test
 	public void createDeleteNode() throws Throwable {
 		twoCommitTree();
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				// .all works on revision 0
-				assertEquals(0, timeAwareEOL("return Tree.all.size;"));
+		scheduleAndWait(() -> {
+			// .all works on revision 0
+			assertEquals(0, timeAwareEOL("return Tree.all.size;"));
 
-				// We also deleted everything in the latest revision
-				assertEquals(0, timeAwareEOL("return Tree.latest.all.size;"));
+			// We also deleted everything in the latest revision
+			assertEquals(0, timeAwareEOL("return Tree.latest.all.size;"));
 
-				// .created can return instances that have been created from a certain moment in time (even if not alive anymore)
-				assertEquals(1, timeAwareEOL("return Tree.latest.prev.size;"));
+			// .created can return instances that have been created from a certain moment in
+			// time (even if not alive anymore)
+			assertEquals(1, timeAwareEOL("return Tree.latest.prev.size;"));
 
-				assertEquals("xy", timeAwareEOL("return Tree.latest.prev.all.first.label;"));
+			assertEquals("xy", timeAwareEOL("return Tree.latest.prev.all.first.label;"));
 
-				return null;
-			}
+			return null;
 		});
 	}
 
@@ -127,30 +122,24 @@
 	@Test
 	public void countInstancesFromModelTypes() throws Throwable {
 		twoCommitTree();
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').all.size;"));
-				assertEquals(1, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.all.size;"));
-				assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.prev.all.size;"));
-				assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').earliest.all.size;"));
-				return null;
-			}
+		scheduleAndWait(() -> {
+			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').all.size;"));
+			assertEquals(1, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.all.size;"));
+			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.prev.all.size;"));
+			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').earliest.all.size;"));
+			return null;
 		});
 	}
 
 	@Test
 	public void countInstancesFromModel() throws Throwable {
 		twoCommitTree();
-		waitForSync(new Callable<Object>(){
-			@Override
-			public Object call() throws Exception {
-				assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').all.size;"));
-				assertEquals(1, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.all.size;"));
-				assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.prev.all.size;"));
-				assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').earliest.all.size;"));
-				return null;
-			}
+		scheduleAndWait(() -> {
+			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').all.size;"));
+			assertEquals(1, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.all.size;"));
+			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.prev.all.size;"));
+			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').earliest.all.size;"));
+			return null;
 		});
 	}
 
@@ -158,7 +147,7 @@
 	@Test
 	public void countInstancesTimeline() throws Throwable {
 		twoCommitTree();
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			List<List<Object>> results = (List<List<Object>>) timelineEOL("return Tree.all.size;");
 			assertEquals(0, results.get(0).get(1));
 			assertEquals(0, (int) timeAwareEOL("return Model.allInstancesAt(t).size;", "t", results.get(0).get(0)));
@@ -193,7 +182,7 @@
 		svnRepository.commit("First commit");
 		requestSVNIndex();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(0, timeAwareEOL("return Model.allInstances.collect(t|t.label).size;"));
 			assertEquals(0, timeAwareEOL("return Model.allInstances.size;"));
 			assertEquals(1, timeAwareEOL("return Model.allInstancesNow.size;"));
@@ -227,7 +216,7 @@
 	@Test
 	public void rangesAreBothInclusive() throws Throwable {
 		keepAddingChildren();
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			GraphNodeWrapper gnw = (GraphNodeWrapper) timeAwareEOL(
 				"return Tree.latest.all.selectOne(t|t.latest.label = 'Root');"
 			);
@@ -260,7 +249,7 @@
 	@Test
 	public void alwaysTrue() throws Throwable {
 		keepAddingChildren();
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertTrue((boolean) timeAwareEOL(
 				"return Tree.latest.all.selectOne(t|t.label='Root').always(v|v.label = 'Root');"
 			));
@@ -299,7 +288,7 @@
 	@Test
 	public void after() throws Throwable {
 		keepAddingChildren();
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(".after is an open left range, i.e. excludes matching version", 2, (int) timeAwareEOL(
 				"return Tree.latest.all.selectOne(t|t.label='Root').earliest.after(v|v.children.size > 0).children.size;"
 			));
@@ -320,7 +309,7 @@
 		svnRepository.commit("Added fourth child");
 		indexer.requestImmediateSync();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(".until is a closed end range, i.e. includes matching version", 2, (int) timeAwareEOL(
 				"return Tree.earliest.next.all.selectOne(t|t.label='Root').until(v|v.children.size > 1).latest.children.size;"
 			));
@@ -344,7 +333,7 @@
 		svnRepository.commit("Added fourth child");
 		indexer.requestImmediateSync();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(".before is a open end range, i.e. excludes matching version", 1, (int) timeAwareEOL(
 				"return Tree.earliest.next.all.selectOne(t|t.label='Root').before(v|v.children.size > 1).latest.children.size;"
 			));
@@ -366,7 +355,7 @@
 	public void sinceThen() throws Throwable {
 		keepAddingChildren();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertFalse("Type node - Without .sinceThen, always uses all versions", (boolean) timeAwareEOL(
 				"return Tree.earliest.next.always(v|v.all.size > 0);"
 			));
@@ -389,7 +378,7 @@
 	public void afterThen() throws Throwable {
 		keepAddingChildren();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertFalse("Type node - Without .afterThen, always uses all versions", (boolean) timeAwareEOL(
 				"return Tree.earliest.next.always(v|v.all.size > 1);"
 			));
@@ -412,7 +401,7 @@
 	public void untilThen() throws Throwable {
 		keepAddingChildren();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertTrue("Positive combination of .sinceThen + .untilThen", (boolean) timeAwareEOL(
 				"return Tree.earliest.next.sinceThen.latest.prev.untilThen.always(v|v.all.size > 0 and v.all.size < 4);"
 			));
@@ -427,7 +416,7 @@
 	public void beforeThen() throws Throwable {
 		keepAddingChildren();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertTrue("Positive combination of .afterThen + .beforeThen", (boolean) timeAwareEOL(
 				"return Tree.earliest.afterThen.latest.beforeThen.always(v|v.all.size > 0 and v.all.size < 4);"
 			));
@@ -446,7 +435,7 @@
 		svnRepository.commit("Removed third child");
 		indexer.requestImmediateSync();
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals(".when with all versions", 5, (int) timeAwareEOL(
 				"return Tree.earliest.next.all.selectOne(t|t.label='Root').when(v|v.children.size >= 0).versions.size;"
 			));
@@ -528,7 +517,7 @@
 
 		// END OF TEST SETUP
 
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertEquals("There are three versions with an empty root node", 3, (int)timeAwareEOL(
 				"return Tree.latest.all.selectOne(t|not t.eContainer.isDefined())"
 				+ ".earliest.when(v|v.children.isEmpty).versions.size;"
@@ -578,7 +567,7 @@
 		svnRepository.commit("Changed label");
 		indexer.requestImmediateSync();
 		
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			assertFalse((boolean) timeAwareEOL(
 				"return Tree.latest.all.selectOne(t|t.latest.label='SomethingElse').always(v|v.label = 'Root');"
 			));
diff --git a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/ThriftEncodingTest.java b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/ThriftEncodingTest.java
index c8b8412..a68e7dd 100644
--- a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/ThriftEncodingTest.java
+++ b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/ThriftEncodingTest.java
@@ -71,19 +71,20 @@
 		indexer.registerMetamodels(new File(TREE_MM_PATH));
 	}
 
+	@SuppressWarnings("unchecked")
 	@Test
 	public void encodeLatestVersion() throws Throwable {
 		keepAddingChildren();
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			Collection<GraphNodeWrapper> ret = (Collection<GraphNodeWrapper>) timeAwareEOL("return Model.allInstancesNow;");
 			HawkModelElementEncoder enc = new HawkModelElementEncoder(new GraphWrapper(indexer.getGraph()));
 			enc.setUseContainment(false);
-
+		
 			List<ModelElement> encoded = new ArrayList<>(ret.size());
 			for (GraphNodeWrapper gnw : ret) {
 				encoded.add(enc.encode(gnw.getNode()));
 			}
-
+		
 			assertEquals(4, encoded.size());
 			return null;
 		});
@@ -92,11 +93,11 @@
 	@Test
 	public void encodeLatestVersionRoot() throws Throwable {
 		keepAddingChildren();
-		waitForSync(() -> {
+		scheduleAndWait(() -> {
 			GraphNodeWrapper ret = (GraphNodeWrapper) timeAwareEOL("return Model.allInstancesNow.selectOne(t|t.eContainer.isUndefined());");
 			HawkModelElementEncoder enc = new HawkModelElementEncoder(new GraphWrapper(indexer.getGraph()));
 			ModelElement encoded = enc.encode(ret.getNode());
-
+		
 			assertNotNull(encoded);
 			assertEquals("Root", encoded.getAttributes().get(0).value.getVString());
 			assertEquals(3, encoded.getContainers().get(0).elements.size());
diff --git a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/TimeAwareBackendTest.java b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/TimeAwareBackendTest.java
index 0f9e5a0..cf8db01 100644
--- a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/TimeAwareBackendTest.java
+++ b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/TimeAwareBackendTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
 import java.util.NoSuchElementException;
@@ -231,6 +232,36 @@
 		createDeleteHeavyEdge();
 	}
 
+	@Test
+	public void nextPrev() throws Exception {
+		ITimeAwareGraphNode n;
+		try (IGraphTransaction tx = db.beginTransaction()) {
+			taDB.setTime(9);
+			n = taDB.createNode(Collections.singletonMap("x", 1), "example");
+			tx.success();
+		}
+		try (IGraphTransaction tx = db.beginTransaction()) {
+			n.travelInTime(10).setProperty("x", 3);
+			tx.success();
+		}
+		try (IGraphTransaction tx = db.beginTransaction()) {
+			n.travelInTime(11).setProperty("x", 5);
+			tx.success();
+		}
+
+		try (IGraphTransaction tx = db.beginTransaction()) {
+			assertEquals(Arrays.asList(11L, 10L, 9L), n.getAllInstants());
+			
+			assertEquals(10, n.travelInTime(9).getNextInstant());
+			assertEquals(11, n.travelInTime(10).getNextInstant());
+			assertEquals(ITimeAwareGraphNode.NO_SUCH_INSTANT, n.travelInTime(11).getNextInstant());
+
+			assertEquals(ITimeAwareGraphNode.NO_SUCH_INSTANT, n.travelInTime(9).getPreviousInstant());
+			assertEquals(9, n.travelInTime(10).getPreviousInstant());
+			assertEquals(10, n.travelInTime(11).getPreviousInstant());
+		}
+	}
+
 	private void createDeleteHeavyEdge() throws Exception {
 		Object heavyEdgeId;
 		try (IGraphTransaction tx = db.beginTransaction()) {
diff --git a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/VCSManagerIndexTest.java b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/VCSManagerIndexTest.java
index 8d1a0cb..d16ea47 100644
--- a/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/VCSManagerIndexTest.java
+++ b/tests/org.hawk.timeaware.tests/src/org/hawk/timeaware/tests/VCSManagerIndexTest.java
@@ -52,10 +52,10 @@
 	private VCSManagerIndex idx;
 
 	@Parameters(name = "{0}")
-    public static Object[] params() {
-    	return BackendTestSuite.params();
-    }
-	
+	public static Object[][] params() {
+		return BackendTestSuite.timeAwareBackends();
+	}
+
 	public VCSManagerIndexTest(IGraphDatabaseFactory factory) {
 		this.dbFactory = factory;
 	}