Merge "RefDirectory: remove ref lock file for following ref dir removal" into stable-4.4
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
index 6d07d34..aebbafe 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
@@ -48,6 +48,11 @@
 
 package org.eclipse.jgit.lib;
 
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -64,6 +69,7 @@
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -782,10 +788,31 @@ public void testIncludeEmptyValue() throws ConfigInvalidException {
 
 	@Test
 	public void testIncludeValuePathNotFound() throws ConfigInvalidException {
+		// we do not expect an exception, included path not found are ignored
 		String notFound = "/not/found";
-		expectedEx.expect(ConfigInvalidException.class);
-		expectedEx.expectMessage(notFound);
-		parse("[include]\npath=" + notFound + "\n");
+		Config parsed = parse("[include]\npath=" + notFound + "\n");
+		assertEquals(1, parsed.getSections().size());
+		assertEquals(notFound, parsed.getString("include", null, "path"));
+	}
+
+	@Test
+	public void testIncludeValuePathWithTilde() throws ConfigInvalidException {
+		// we do not expect an exception, included path not supported are
+		// ignored
+		String notSupported = "~/someFile";
+		Config parsed = parse("[include]\npath=" + notSupported + "\n");
+		assertEquals(1, parsed.getSections().size());
+		assertEquals(notSupported, parsed.getString("include", null, "path"));
+	}
+
+	@Test
+	public void testIncludeValuePathRelative() throws ConfigInvalidException {
+		// we do not expect an exception, included path not supported are
+		// ignored
+		String notSupported = "someRelativeFile";
+		Config parsed = parse("[include]\npath=" + notSupported + "\n");
+		assertEquals(1, parsed.getSections().size());
+		assertEquals(notSupported, parsed.getString("include", null, "path"));
 	}
 
 	@Test
@@ -849,4 +876,93 @@ private static Config parse(final String content, Config baseConfig)
 		c.fromText(content);
 		return c;
 	}
+
+	@Test
+	public void testTimeUnit() throws ConfigInvalidException {
+		assertEquals(0, parseTime("0", MILLISECONDS));
+		assertEquals(2, parseTime("2ms", MILLISECONDS));
+		assertEquals(200, parseTime("200 milliseconds", MILLISECONDS));
+
+		assertEquals(0, parseTime("0s", SECONDS));
+		assertEquals(2, parseTime("2s", SECONDS));
+		assertEquals(231, parseTime("231sec", SECONDS));
+		assertEquals(1, parseTime("1second", SECONDS));
+		assertEquals(300, parseTime("300 seconds", SECONDS));
+
+		assertEquals(2, parseTime("2m", MINUTES));
+		assertEquals(2, parseTime("2min", MINUTES));
+		assertEquals(1, parseTime("1 minute", MINUTES));
+		assertEquals(10, parseTime("10 minutes", MINUTES));
+
+		assertEquals(5, parseTime("5h", HOURS));
+		assertEquals(5, parseTime("5hr", HOURS));
+		assertEquals(1, parseTime("1hour", HOURS));
+		assertEquals(48, parseTime("48hours", HOURS));
+
+		assertEquals(5, parseTime("5 h", HOURS));
+		assertEquals(5, parseTime("5 hr", HOURS));
+		assertEquals(1, parseTime("1 hour", HOURS));
+		assertEquals(48, parseTime("48 hours", HOURS));
+		assertEquals(48, parseTime("48 \t \r hours", HOURS));
+
+		assertEquals(4, parseTime("4d", DAYS));
+		assertEquals(1, parseTime("1day", DAYS));
+		assertEquals(14, parseTime("14days", DAYS));
+
+		assertEquals(7, parseTime("1w", DAYS));
+		assertEquals(7, parseTime("1week", DAYS));
+		assertEquals(14, parseTime("2w", DAYS));
+		assertEquals(14, parseTime("2weeks", DAYS));
+
+		assertEquals(30, parseTime("1mon", DAYS));
+		assertEquals(30, parseTime("1month", DAYS));
+		assertEquals(60, parseTime("2mon", DAYS));
+		assertEquals(60, parseTime("2months", DAYS));
+
+		assertEquals(365, parseTime("1y", DAYS));
+		assertEquals(365, parseTime("1year", DAYS));
+		assertEquals(365 * 2, parseTime("2years", DAYS));
+	}
+
+	private long parseTime(String value, TimeUnit unit)
+			throws ConfigInvalidException {
+		Config c = parse("[a]\na=" + value + "\n");
+		return c.getTimeUnit("a", null, "a", 0, unit);
+	}
+
+	@Test
+	public void testTimeUnitDefaultValue() throws ConfigInvalidException {
+		// value not present
+		assertEquals(20, parse("[a]\na=0\n").getTimeUnit("a", null, "b", 20,
+				MILLISECONDS));
+		// value is empty
+		assertEquals(20, parse("[a]\na=\" \"\n").getTimeUnit("a", null, "a", 20,
+				MILLISECONDS));
+
+		// value is not numeric
+		assertEquals(20, parse("[a]\na=test\n").getTimeUnit("a", null, "a", 20,
+				MILLISECONDS));
+	}
+
+	@Test
+	public void testTimeUnitInvalid() throws ConfigInvalidException {
+		expectedEx.expect(IllegalArgumentException.class);
+		expectedEx
+				.expectMessage("Invalid time unit value: a.a=1 monttthhh");
+		parseTime("1 monttthhh", DAYS);
+	}
+
+	@Test
+	public void testTimeUnitInvalidWithSection() throws ConfigInvalidException {
+		Config c = parse("[a \"b\"]\na=1 monttthhh\n");
+		expectedEx.expect(IllegalArgumentException.class);
+		expectedEx.expectMessage("Invalid time unit value: a.b.a=1 monttthhh");
+		c.getTimeUnit("a", "b", "a", 0, DAYS);
+	}
+
+	@Test
+	public void testTimeUnitNegative() throws ConfigInvalidException {
+		expectedEx.expect(IllegalArgumentException.class);
+		parseTime("-1", MILLISECONDS);
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheConfigTest.java
new file mode 100644
index 0000000..52cc9fb
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheConfigTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016 Ericsson
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.lib;
+
+import static org.eclipse.jgit.lib.RepositoryCacheConfig.AUTO_CLEANUP_DELAY;
+import static org.eclipse.jgit.lib.RepositoryCacheConfig.NO_CLEANUP;
+import static org.junit.Assert.assertEquals;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RepositoryCacheConfigTest {
+
+	private RepositoryCacheConfig config;
+
+	@Before
+	public void setUp() {
+		config = new RepositoryCacheConfig();
+	}
+
+	@Test
+	public void testDefaultValues() {
+		assertEquals(TimeUnit.HOURS.toMillis(1), config.getExpireAfter());
+		assertEquals(config.getExpireAfter() / 10, config.getCleanupDelay());
+	}
+
+	@Test
+	public void testCleanupDelay() {
+		config.setCleanupDelay(TimeUnit.HOURS.toMillis(1));
+		assertEquals(TimeUnit.HOURS.toMillis(1), config.getCleanupDelay());
+	}
+
+	@Test
+	public void testAutoCleanupDelay() {
+		config.setExpireAfter(TimeUnit.MINUTES.toMillis(20));
+		config.setCleanupDelay(AUTO_CLEANUP_DELAY);
+		assertEquals(TimeUnit.MINUTES.toMillis(20), config.getExpireAfter());
+		assertEquals(config.getExpireAfter() / 10, config.getCleanupDelay());
+	}
+
+	@Test
+	public void testAutoCleanupDelayShouldBeMax10minutes() {
+		config.setExpireAfter(TimeUnit.HOURS.toMillis(10));
+		assertEquals(TimeUnit.HOURS.toMillis(10), config.getExpireAfter());
+		assertEquals(TimeUnit.MINUTES.toMillis(10), config.getCleanupDelay());
+	}
+
+	@Test
+	public void testDisabledCleanupDelay() {
+		config.setCleanupDelay(NO_CLEANUP);
+		assertEquals(NO_CLEANUP, config.getCleanupDelay());
+	}
+
+	@Test
+	public void testFromConfig() throws ConfigInvalidException {
+		Config otherConfig = new Config();
+		otherConfig.fromText("[core]\nrepositoryCacheExpireAfter=1000\n"
+				+ "repositoryCacheCleanupDelay=500");
+		config.fromConfig(otherConfig);
+		assertEquals(1000, config.getExpireAfter());
+		assertEquals(500, config.getCleanupDelay());
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java
index a1cec2d..6bea320 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java
@@ -195,17 +195,81 @@ public void testRepositoryUsageCountWithRegisteredRepository() {
 		assertEquals(0, ((Repository) db).useCnt.get());
 	}
 
-	public void testRepositoryUnregisteringWhenClosing() throws Exception {
+	@Test
+	public void testRepositoryNotUnregisteringWhenClosing() throws Exception {
 		FileKey loc = FileKey.exact(db.getDirectory(), db.getFS());
 		Repository d2 = RepositoryCache.open(loc);
 		assertEquals(1, d2.useCnt.get());
 		assertThat(RepositoryCache.getRegisteredKeys(),
 				hasItem(FileKey.exact(db.getDirectory(), db.getFS())));
 		assertEquals(1, RepositoryCache.getRegisteredKeys().size());
-
 		d2.close();
-
 		assertEquals(0, d2.useCnt.get());
-		assertEquals(0, RepositoryCache.getRegisteredKeys().size());
+		assertEquals(1, RepositoryCache.getRegisteredKeys().size());
+		assertTrue(RepositoryCache.isCached(d2));
+	}
+
+	@Test
+	public void testRepositoryUnregisteringWhenExpired() throws Exception {
+		Repository repoA = createBareRepository();
+		Repository repoB = createBareRepository();
+		Repository repoC = createBareRepository();
+		RepositoryCache.register(repoA);
+		RepositoryCache.register(repoB);
+		RepositoryCache.register(repoC);
+
+		assertEquals(3, RepositoryCache.getRegisteredKeys().size());
+		assertTrue(RepositoryCache.isCached(repoA));
+		assertTrue(RepositoryCache.isCached(repoB));
+		assertTrue(RepositoryCache.isCached(repoC));
+
+		// fake that repoA was closed more than 1 hour ago (default expiration
+		// time)
+		repoA.close();
+		repoA.closedAt.set(System.currentTimeMillis() - 65 * 60 * 1000);
+		// close repoB but this one will not be expired
+		repoB.close();
+
+		assertEquals(3, RepositoryCache.getRegisteredKeys().size());
+		assertTrue(RepositoryCache.isCached(repoA));
+		assertTrue(RepositoryCache.isCached(repoB));
+		assertTrue(RepositoryCache.isCached(repoC));
+
+		RepositoryCache.clearExpired();
+
+		assertEquals(2, RepositoryCache.getRegisteredKeys().size());
+		assertFalse(RepositoryCache.isCached(repoA));
+		assertTrue(RepositoryCache.isCached(repoB));
+		assertTrue(RepositoryCache.isCached(repoC));
+	}
+
+	@Test
+	public void testReconfigure() throws InterruptedException {
+		RepositoryCache.register(db);
+		assertTrue(RepositoryCache.isCached(db));
+		db.close();
+		assertTrue(RepositoryCache.isCached(db));
+
+		// Actually, we would only need to validate that
+		// WorkQueue.getExecutor().scheduleWithFixedDelay is called with proper
+		// values but since we do not have a mock library, we test
+		// reconfiguration from a black box perspective. I.e. reconfigure
+		// expireAfter and cleanupDelay to 1 ms and wait until the Repository
+		// is evicted to prove that reconfiguration worked.
+		RepositoryCacheConfig config = new RepositoryCacheConfig();
+		config.setExpireAfter(1);
+		config.setCleanupDelay(1);
+		config.install();
+
+		// Instead of using a fixed waiting time, start with small and increase:
+		// sleep 1, 2, 4, 8, 16, ..., 1024 ms
+		// This wait will time out after 2048 ms
+		for (int i = 0; i <= 10; i++) {
+			Thread.sleep(1 << i);
+			if (!RepositoryCache.isCached(db)) {
+				return;
+			}
+		}
+		fail("Repository should have been evicted from cache");
 	}
 }
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index c0dbc77..bacfb33 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -1,5 +1,13 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <component id="org.eclipse.jgit" version="2">
+    <resource path="META-INF/MANIFEST.MF">
+        <filter id="924844039">
+            <message_arguments>
+                <message_argument value="4.4.1"/>
+                <message_argument value="4.4.0"/>
+            </message_arguments>
+        </filter>
+    </resource>
     <resource path="src/org/eclipse/jgit/attributes/AttributesNode.java" type="org.eclipse.jgit.attributes.AttributesNode">
         <filter comment="moved to new AttributesManager" id="338792546">
             <message_arguments>
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index b71e48b..21fbaa4 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -185,6 +185,7 @@
 corruptObjectTruncatedInName=truncated in name
 corruptObjectTruncatedInObjectId=truncated in object id
 corruptObjectZeroId=entry points to null SHA-1
+corruptUseCnt=close() called when useCnt is already zero
 couldNotCheckOutBecauseOfConflicts=Could not check out because of conflicts
 couldNotDeleteLockFileShouldNotHappen=Could not delete lock file. Should not happen
 couldNotDeleteTemporaryIndexFileShouldNotHappen=Could not delete temporary index file. Should not happen
@@ -339,7 +340,6 @@
 invalidIdLength=Invalid id length {0}; should be {1}
 invalidIgnoreParamSubmodule=Found invalid ignore param for submodule {0}.
 invalidIgnoreRule=Exception caught while parsing ignore rule ''{0}''.
-invalidIncludedPathInConfigFile=Invalid included path in config file: {0}
 invalidIntegerValue=Invalid integer value: {0}.{1}={2}
 invalidKey=Invalid key: {0}
 invalidLineInConfigFile=Invalid line in config file
@@ -361,6 +361,8 @@
 invalidStageForPath=Invalid stage {0} for path {1}
 invalidTagOption=Invalid tag option: {0}
 invalidTimeout=Invalid timeout: {0}
+invalidTimeUnitValue2=Invalid time unit value: {0}.{1}={2}
+invalidTimeUnitValue3=Invalid time unit value: {0}.{1}.{2}={3}
 invalidURL=Invalid URL {0}
 invalidWildcards=Invalid wildcards {0}
 invalidRefSpec=Invalid refspec {0}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 7c10178..b7ef085 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -245,6 +245,7 @@ public static JGitText get() {
 	/***/ public String corruptObjectTruncatedInObjectId;
 	/***/ public String corruptObjectZeroId;
 	/***/ public String corruptPack;
+	/***/ public String corruptUseCnt;
 	/***/ public String couldNotCheckOutBecauseOfConflicts;
 	/***/ public String couldNotDeleteLockFileShouldNotHappen;
 	/***/ public String couldNotDeleteTemporaryIndexFileShouldNotHappen;
@@ -398,7 +399,6 @@ public static JGitText get() {
 	/***/ public String invalidIdLength;
 	/***/ public String invalidIgnoreParamSubmodule;
 	/***/ public String invalidIgnoreRule;
-	/***/ public String invalidIncludedPathInConfigFile;
 	/***/ public String invalidIntegerValue;
 	/***/ public String invalidKey;
 	/***/ public String invalidLineInConfigFile;
@@ -419,6 +419,8 @@ public static JGitText get() {
 	/***/ public String invalidStageForPath;
 	/***/ public String invalidTagOption;
 	/***/ public String invalidTimeout;
+	/***/ public String invalidTimeUnitValue2;
+	/***/ public String invalidTimeUnitValue3;
 	/***/ public String invalidURL;
 	/***/ public String invalidWildcards;
 	/***/ public String invalidRefSpec;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java
index 39856c0..a3859ab 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java
@@ -43,55 +43,11 @@
 
 package org.eclipse.jgit.lib;
 
-import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
 
 /** ProgressMonitor that batches update events. */
 public abstract class BatchingProgressMonitor implements ProgressMonitor {
-	private static final ScheduledThreadPoolExecutor alarmQueue;
-
-	static final Object alarmQueueKiller;
-
-	static {
-		// To support garbage collection, start our thread but
-		// swap out the thread factory. When our class is GC'd
-		// the alarmQueueKiller will finalize and ask the executor
-		// to shutdown, ending the worker.
-		//
-		int threads = 1;
-		alarmQueue = new ScheduledThreadPoolExecutor(threads,
-				new ThreadFactory() {
-					private final ThreadFactory baseFactory = Executors
-							.defaultThreadFactory();
-
-					public Thread newThread(Runnable taskBody) {
-						Thread thr = baseFactory.newThread(taskBody);
-						thr.setName("JGit-AlarmQueue"); //$NON-NLS-1$
-						thr.setDaemon(true);
-						return thr;
-					}
-				});
-		alarmQueue.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
-		alarmQueue.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
-		alarmQueue.prestartAllCoreThreads();
-
-		// Now that the threads are running, its critical to swap out
-		// our own thread factory for one that isn't in the ClassLoader.
-		// This allows the class to GC.
-		//
-		alarmQueue.setThreadFactory(Executors.defaultThreadFactory());
-
-		alarmQueueKiller = new Object() {
-			@Override
-			protected void finalize() {
-				alarmQueue.shutdownNow();
-			}
-		};
-	}
-
 	private long delayStartTime;
 
 	private TimeUnit delayStartUnit = TimeUnit.MILLISECONDS;
@@ -219,7 +175,7 @@ private static class Task implements Runnable {
 
 		void delay(long time, TimeUnit unit) {
 			display = false;
-			timerFuture = alarmQueue.schedule(this, time, unit);
+			timerFuture = WorkQueue.getExecutor().schedule(this, time, unit);
 		}
 
 		public void run() {
@@ -254,7 +210,8 @@ void update(BatchingProgressMonitor pm, int completed) {
 
 		private void restartTimer() {
 			display = false;
-			timerFuture = alarmQueue.schedule(this, 1, TimeUnit.SECONDS);
+			timerFuture = WorkQueue.getExecutor().schedule(this, 1,
+					TimeUnit.SECONDS);
 		}
 
 		void end(BatchingProgressMonitor pm) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
index b8eba3a..1e1d147 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
@@ -52,13 +52,17 @@
 package org.eclipse.jgit.lib;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.events.ConfigChangedEvent;
@@ -79,7 +83,7 @@ public class Config {
 	private static final long KiB = 1024;
 	private static final long MiB = 1024 * KiB;
 	private static final long GiB = 1024 * MiB;
-	private static final int MAX_DEPTH = 999;
+	private static final int MAX_DEPTH = 10;
 
 	/** the change listeners */
 	private final ListenerList listeners = new ListenerList();
@@ -490,6 +494,123 @@ public String getString(final String section, String subsection,
 	}
 
 	/**
+	 * Parse a numerical time unit, such as "1 minute", from the configuration.
+	 *
+	 * @param section
+	 *            section the key is in.
+	 * @param subsection
+	 *            subsection the key is in, or null if not in a subsection.
+	 * @param name
+	 *            the key name.
+	 * @param defaultValue
+	 *            default value to return if no value was present.
+	 * @param wantUnit
+	 *            the units of {@code defaultValue} and the return value, as
+	 *            well as the units to assume if the value does not contain an
+	 *            indication of the units.
+	 * @return the value, or {@code defaultValue} if not set, expressed in
+	 *         {@code units}.
+	 * @since 4.4
+	 */
+	public long getTimeUnit(String section, String subsection, String name,
+			long defaultValue, TimeUnit wantUnit) {
+		String valueString = getString(section, subsection, name);
+
+		if (valueString == null) {
+			return defaultValue;
+		}
+
+		String s = valueString.trim();
+		if (s.length() == 0) {
+			return defaultValue;
+		}
+
+		if (s.startsWith("-")/* negative */) { //$NON-NLS-1$
+			throw notTimeUnit(section, subsection, name, valueString);
+		}
+
+		Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$") //$NON-NLS-1$
+				.matcher(valueString);
+		if (!m.matches()) {
+			return defaultValue;
+		}
+
+		String digits = m.group(1);
+		String unitName = m.group(2).trim();
+
+		TimeUnit inputUnit;
+		int inputMul;
+
+		if (unitName.isEmpty()) {
+			inputUnit = wantUnit;
+			inputMul = 1;
+
+		} else if (match(unitName, "ms", "milliseconds")) { //$NON-NLS-1$ //$NON-NLS-2$
+			inputUnit = TimeUnit.MILLISECONDS;
+			inputMul = 1;
+
+		} else if (match(unitName, "s", "sec", "second", "seconds")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+			inputUnit = TimeUnit.SECONDS;
+			inputMul = 1;
+
+		} else if (match(unitName, "m", "min", "minute", "minutes")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+			inputUnit = TimeUnit.MINUTES;
+			inputMul = 1;
+
+		} else if (match(unitName, "h", "hr", "hour", "hours")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+			inputUnit = TimeUnit.HOURS;
+			inputMul = 1;
+
+		} else if (match(unitName, "d", "day", "days")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+			inputUnit = TimeUnit.DAYS;
+			inputMul = 1;
+
+		} else if (match(unitName, "w", "week", "weeks")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+			inputUnit = TimeUnit.DAYS;
+			inputMul = 7;
+
+		} else if (match(unitName, "mon", "month", "months")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+			inputUnit = TimeUnit.DAYS;
+			inputMul = 30;
+
+		} else if (match(unitName, "y", "year", "years")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+			inputUnit = TimeUnit.DAYS;
+			inputMul = 365;
+
+		} else {
+			throw notTimeUnit(section, subsection, name, valueString);
+		}
+
+		try {
+			return wantUnit.convert(Long.parseLong(digits) * inputMul,
+					inputUnit);
+		} catch (NumberFormatException nfe) {
+			throw notTimeUnit(section, subsection, unitName, valueString);
+		}
+	}
+
+	private static boolean match(final String a, final String... cases) {
+		for (final String b : cases) {
+			if (b != null && b.equalsIgnoreCase(a)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private IllegalArgumentException notTimeUnit(String section,
+			String subsection, String name, String valueString) {
+		if (subsection != null) {
+			return new IllegalArgumentException(
+					MessageFormat.format(JGitText.get().invalidTimeUnitValue3,
+							section, subsection, name, valueString));
+		}
+		return new IllegalArgumentException(
+				MessageFormat.format(JGitText.get().invalidTimeUnitValue2,
+						section, name, valueString));
+	}
+
+	/**
 	 * @param section
 	 *            section to search for.
 	 * @return set of all subsections of specified section within this
@@ -1127,9 +1248,15 @@ private void addIncludedConfig(final List<ConfigLine> newEntries,
 				decoded = RawParseUtils.decode(bytes);
 			}
 			newEntries.addAll(fromTextRecurse(decoded, depth + 1));
+		} catch (FileNotFoundException fnfe) {
+			if (path.exists()) {
+				throw new ConfigInvalidException(MessageFormat
+						.format(JGitText.get().cannotReadFile, path), fnfe);
+			}
 		} catch (IOException ioe) {
-			throw new ConfigInvalidException(MessageFormat.format(
-					JGitText.get().invalidIncludedPathInConfigFile, path));
+			throw new ConfigInvalidException(
+					MessageFormat.format(JGitText.get().cannotReadFile, path),
+					ioe);
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
index 5703ddd..7ec2499 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
@@ -63,6 +63,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
@@ -93,6 +94,8 @@
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.SystemReader;
 import org.eclipse.jgit.util.io.SafeBufferedOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Represents a Git repository.
@@ -103,6 +106,8 @@
  * This class is thread-safe.
  */
 public abstract class Repository implements AutoCloseable {
+	private static Logger LOG = LoggerFactory.getLogger(Repository.class);
+
 	private static final ListenerList globalListeners = new ListenerList();
 
 	/** @return the global listener list observing all events in this JVM. */
@@ -113,6 +118,8 @@ public static ListenerList getGlobalListenerList() {
 	/** Use counter */
 	final AtomicInteger useCnt = new AtomicInteger(1);
 
+	final AtomicLong closedAt = new AtomicLong();
+
 	/** Metadata directory holding the repository's critical files. */
 	private final File gitDir;
 
@@ -863,9 +870,24 @@ public void incrementOpen() {
 
 	/** Decrement the use count, and maybe close resources. */
 	public void close() {
-		if (useCnt.decrementAndGet() == 0) {
-			doClose();
-			RepositoryCache.unregister(this);
+		int newCount = useCnt.decrementAndGet();
+		if (newCount == 0) {
+			if (RepositoryCache.isCached(this)) {
+				closedAt.set(System.currentTimeMillis());
+			} else {
+				doClose();
+			}
+		} else if (newCount == -1) {
+			// should not happen, only log when useCnt became negative to
+			// minimize number of log entries
+			LOG.warn(JGitText.get().corruptUseCnt);
+			if (LOG.isDebugEnabled()) {
+				IllegalStateException e = new IllegalStateException();
+				LOG.debug("", e); //$NON-NLS-1$
+			}
+			if (RepositoryCache.isCached(this)) {
+				closedAt.set(System.currentTimeMillis());
+			}
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
index 22b5fcd..29ef084 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
@@ -52,17 +52,26 @@
 import java.util.Iterator;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** Cache of active {@link Repository} instances. */
 public class RepositoryCache {
 	private static final RepositoryCache cache = new RepositoryCache();
 
+	private final static Logger LOG = LoggerFactory
+			.getLogger(RepositoryCache.class);
+
 	/**
 	 * Open an existing repository, reusing a cached instance if possible.
 	 * <p>
@@ -138,10 +147,10 @@ public static void register(final Repository db) {
 	 * @param db
 	 *            repository to unregister.
 	 */
-	public static void close(final Repository db) {
+	public static void close(@NonNull final Repository db) {
 		if (db.getDirectory() != null) {
 			FileKey key = FileKey.exact(db.getDirectory(), db.getFS());
-			cache.unregisterAndCloseRepository(key);
+			cache.unregisterAndCloseRepository(key, db);
 		}
 	}
 
@@ -187,20 +196,69 @@ public static Collection<Key> getRegisteredKeys() {
 		return cache.getKeys();
 	}
 
+	static boolean isCached(@NonNull Repository repo) {
+		File gitDir = repo.getDirectory();
+		if (gitDir == null) {
+			return false;
+		}
+		FileKey key = new FileKey(gitDir, repo.getFS());
+		Reference<Repository> repoRef = cache.cacheMap.get(key);
+		return repoRef != null && repoRef.get() == repo;
+	}
+
 	/** Unregister all repositories from the cache. */
 	public static void clear() {
 		cache.clearAll();
 	}
 
+	static void clearExpired() {
+		cache.clearAllExpired();
+	}
+
+	static void reconfigure(RepositoryCacheConfig repositoryCacheConfig) {
+		cache.configureEviction(repositoryCacheConfig);
+	}
+
 	private final ConcurrentHashMap<Key, Reference<Repository>> cacheMap;
 
 	private final Lock[] openLocks;
 
+	private ScheduledFuture<?> cleanupTask;
+
+	private volatile long expireAfter;
+
 	private RepositoryCache() {
 		cacheMap = new ConcurrentHashMap<Key, Reference<Repository>>();
 		openLocks = new Lock[4];
-		for (int i = 0; i < openLocks.length; i++)
+		for (int i = 0; i < openLocks.length; i++) {
 			openLocks[i] = new Lock();
+		}
+		configureEviction(new RepositoryCacheConfig());
+	}
+
+	private void configureEviction(
+			RepositoryCacheConfig repositoryCacheConfig) {
+		expireAfter = repositoryCacheConfig.getExpireAfter();
+		ScheduledThreadPoolExecutor scheduler = WorkQueue.getExecutor();
+		synchronized (scheduler) {
+			if (cleanupTask != null) {
+				cleanupTask.cancel(false);
+			}
+			long delay = repositoryCacheConfig.getCleanupDelay();
+			if (delay == RepositoryCacheConfig.NO_CLEANUP) {
+				return;
+			}
+			cleanupTask = scheduler.scheduleWithFixedDelay(new Runnable() {
+				@Override
+				public void run() {
+					try {
+						cache.clearAllExpired();
+					} catch (Throwable e) {
+						LOG.error(e.getMessage(), e);
+					}
+				}
+			}, delay, delay, TimeUnit.MILLISECONDS);
+		}
 	}
 
 	@SuppressWarnings("resource")
@@ -239,10 +297,20 @@ private Repository unregisterRepository(final Key location) {
 		return oldRef != null ? oldRef.get() : null;
 	}
 
-	private void unregisterAndCloseRepository(final Key location) {
-		Repository oldDb = unregisterRepository(location);
-		if (oldDb != null) {
-			oldDb.close();
+	private boolean isExpired(Repository db) {
+		return db != null && db.useCnt.get() == 0
+			&& (System.currentTimeMillis() - db.closedAt.get() > expireAfter);
+	}
+
+	private void unregisterAndCloseRepository(final Key location,
+			Repository db) {
+		synchronized (lockFor(location)) {
+			if (isExpired(db)) {
+				Repository oldDb = unregisterRepository(location);
+				if (oldDb != null) {
+					oldDb.close();
+				}
+			}
 		}
 	}
 
@@ -250,6 +318,15 @@ private Collection<Key> getKeys() {
 		return new ArrayList<Key>(cacheMap.keySet());
 	}
 
+	private void clearAllExpired() {
+		for (Reference<Repository> ref : cacheMap.values()) {
+			Repository db = ref.get();
+			if (isExpired(db)) {
+				RepositoryCache.close(db);
+			}
+		}
+	}
+
 	private void clearAll() {
 		for (int stage = 0; stage < 2; stage++) {
 			for (Iterator<Map.Entry<Key, Reference<Repository>>> i = cacheMap
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCacheConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCacheConfig.java
new file mode 100644
index 0000000..428dea3
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCacheConfig.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2016 Ericsson
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.lib;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Configuration parameters for JVM-wide repository cache used by JGit.
+ *
+ * @since 4.4
+ */
+public class RepositoryCacheConfig {
+
+	/**
+	 * Set cleanupDelayMillis to this value in order to switch off time-based
+	 * cache eviction. The JVM can still expire cache entries when heap memory
+	 * runs low.
+	 */
+	public static final long NO_CLEANUP = 0;
+
+	/**
+	 * Set cleanupDelayMillis to this value in order to auto-set it to minimum
+	 * of 1/10 of expireAfterMillis and 10 minutes
+	 */
+	public static final long AUTO_CLEANUP_DELAY = -1;
+
+	private long expireAfterMillis;
+
+	private long cleanupDelayMillis;
+
+	/** Create a default configuration. */
+	public RepositoryCacheConfig() {
+		expireAfterMillis = TimeUnit.HOURS.toMillis(1);
+		cleanupDelayMillis = AUTO_CLEANUP_DELAY;
+	}
+
+	/**
+	 * @return the time an unused repository should expired and be evicted from
+	 *         the RepositoryCache in milliseconds. <b>Default is 1 hour.</b>
+	 */
+	public long getExpireAfter() {
+		return expireAfterMillis;
+	}
+
+	/**
+	 * @param expireAfterMillis
+	 *            the time an unused repository should expired and be evicted
+	 *            from the RepositoryCache in milliseconds.
+	 */
+	public void setExpireAfter(long expireAfterMillis) {
+		this.expireAfterMillis = expireAfterMillis;
+	}
+
+	/**
+	 * @return the delay between the periodic cleanup of expired repository in
+	 *         milliseconds. <b>Default is minimum of 1/10 of expireAfterMillis
+	 *         and 10 minutes</b>
+	 */
+	public long getCleanupDelay() {
+		if (cleanupDelayMillis < 0) {
+			return Math.min(expireAfterMillis / 10,
+					TimeUnit.MINUTES.toMillis(10));
+		}
+		return cleanupDelayMillis;
+	}
+
+	/**
+	 * @param cleanupDelayMillis
+	 *            the delay between the periodic cleanup of expired repository
+	 *            in milliseconds. Set it to {@link #AUTO_CLEANUP_DELAY} to
+	 *            automatically derive cleanup delay from expireAfterMillis.
+	 *            <p>
+	 *            Set it to {@link #NO_CLEANUP} in order to switch off cache
+	 *            expiration.
+	 *            <p>
+	 *            If cache expiration is switched off the JVM still can evict
+	 *            cache entries when the JVM is running low on available heap
+	 *            memory.
+	 */
+	public void setCleanupDelay(long cleanupDelayMillis) {
+		this.cleanupDelayMillis = cleanupDelayMillis;
+	}
+
+	/**
+	 * Update properties by setting fields from the configuration.
+	 * <p>
+	 * If a property is not defined in the configuration, then it is left
+	 * unmodified.
+	 *
+	 * @param config
+	 *            configuration to read properties from.
+	 * @return {@code this}.
+	 */
+	public RepositoryCacheConfig fromConfig(Config config) {
+		setExpireAfter(
+				config.getTimeUnit("core", null, "repositoryCacheExpireAfter", //$NON-NLS-1$//$NON-NLS-2$
+						getExpireAfter(), TimeUnit.MILLISECONDS));
+		setCleanupDelay(
+				config.getTimeUnit("core", null, "repositoryCacheCleanupDelay", //$NON-NLS-1$ //$NON-NLS-2$
+						AUTO_CLEANUP_DELAY, TimeUnit.MILLISECONDS));
+		return this;
+	}
+
+	/**
+	 * Install this configuration as the live settings.
+	 * <p>
+	 * The new configuration is applied immediately.
+	 */
+	public void install() {
+		RepositoryCache.reconfigure(this);
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkQueue.java
new file mode 100644
index 0000000..9735d19
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkQueue.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2008-2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.lib;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Simple work queue to run tasks in the background
+ */
+class WorkQueue {
+	private static final ScheduledThreadPoolExecutor executor;
+
+	static final Object executorKiller;
+
+	static {
+		// To support garbage collection, start our thread but
+		// swap out the thread factory. When our class is GC'd
+		// the executorKiller will finalize and ask the executor
+		// to shutdown, ending the worker.
+		//
+		int threads = 1;
+		executor = new ScheduledThreadPoolExecutor(threads,
+				new ThreadFactory() {
+					private final ThreadFactory baseFactory = Executors
+							.defaultThreadFactory();
+
+					public Thread newThread(Runnable taskBody) {
+						Thread thr = baseFactory.newThread(taskBody);
+						thr.setName("JGit-WorkQueue"); //$NON-NLS-1$
+						thr.setDaemon(true);
+						return thr;
+					}
+				});
+		executor.setRemoveOnCancelPolicy(true);
+		executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
+		executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+		executor.prestartAllCoreThreads();
+
+		// Now that the threads are running, its critical to swap out
+		// our own thread factory for one that isn't in the ClassLoader.
+		// This allows the class to GC.
+		//
+		executor.setThreadFactory(Executors.defaultThreadFactory());
+
+		executorKiller = new Object() {
+			@Override
+			protected void finalize() {
+				executor.shutdownNow();
+			}
+		};
+	}
+
+	static ScheduledThreadPoolExecutor getExecutor() {
+		return executor;
+	}
+}
diff --git a/pom.xml b/pom.xml
index 17995b6..81c04ce 100644
--- a/pom.xml
+++ b/pom.xml
@@ -188,9 +188,10 @@
 
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
-
     <maven.build.timestamp.format>yyyyMMddHHmm</maven.build.timestamp.format>
     <bundle-manifest>${project.build.directory}/META-INF/MANIFEST.MF</bundle-manifest>
+    <!-- set JDK_HOME to JAVA_HOME path of JDK7 installation in order to compile against JDK 7 class library -->
+    <JDK_HOME>${JAVA_HOME}</JDK_HOME>
 
     <jgit-last-release-version>4.2.0.201601211800-r</jgit-last-release-version>
     <jsch-version>0.1.53</jsch-version>
@@ -264,6 +265,9 @@
             <encoding>UTF-8</encoding>
             <source>1.7</source>
             <target>1.7</target>
+            <compilerArguments>
+              <bootclasspath>${JDK_HOME}${file.separator}jre${file.separator}lib${file.separator}rt.jar${path.separator}${JDK_HOME}${file.separator}jre${file.separator}lib${file.separator}jsse.jar${path.separator}${JDK_HOME}${file.separator}jre${file.separator}lib${file.separator}jce.jar</bootclasspath>
+            </compilerArguments>
           </configuration>
         </plugin>