Merge branch 'master' into stable-4.3

* master:
  Add config parameter gc.prunePackExpire for packfile expiration
  In TestRepository, use a consistent clock

Change-Id: I7ac568e650fbd191e48a8f1a4068af72deb242e8
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
index e259156..1119e63 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
@@ -112,23 +112,18 @@
  *            type of Repository the test data is stored on.
  */
 public class TestRepository<R extends Repository> {
-	private static final PersonIdent defaultAuthor;
 
-	private static final PersonIdent defaultCommitter;
+	public static final String AUTHOR = "J. Author";
 
-	static {
-		final MockSystemReader m = new MockSystemReader();
-		final long now = m.getCurrentTime();
-		final int tz = m.getTimezone(now);
+	public static final String AUTHOR_EMAIL = "jauthor@example.com";
 
-		final String an = "J. Author";
-		final String ae = "jauthor@example.com";
-		defaultAuthor = new PersonIdent(an, ae, now, tz);
+	public static final String COMMITTER = "J. Committer";
 
-		final String cn = "J. Committer";
-		final String ce = "jcommitter@example.com";
-		defaultCommitter = new PersonIdent(cn, ce, now, tz);
-	}
+	public static final String COMMITTER_EMAIL = "jcommitter@example.com";
+
+	private final PersonIdent defaultAuthor;
+
+	private final PersonIdent defaultCommitter;
 
 	private final R db;
 
@@ -184,6 +179,10 @@ public TestRepository(R db, RevWalk rw, MockSystemReader reader)
 		this.pool = rw;
 		this.inserter = db.newObjectInserter();
 		this.mockSystemReader = reader;
+		long now = mockSystemReader.getCurrentTime();
+		int tz = mockSystemReader.getTimezone(now);
+		defaultAuthor = new PersonIdent(AUTHOR, AUTHOR_EMAIL, now, tz);
+		defaultCommitter = new PersonIdent(COMMITTER, COMMITTER_EMAIL, now, tz);
 	}
 
 	/** @return the repository this helper class operates against. */
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
index f549fb5..41a1a5d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
@@ -213,9 +213,20 @@ public void testDonePruneTooYoungPacks() throws Exception {
 		assertEquals(9, stats.numberOfPackedObjects);
 		assertEquals(2, stats.numberOfPackFiles);
 
+		// repack again but now without a grace period for loose objects. Since
+		// we don't have loose objects anymore this shouldn't change anything
+		gc.setExpireAgeMillis(0);
+		gc.gc();
+		stats = gc.getStatistics();
+		assertEquals(0, stats.numberOfLooseObjects);
+		// if objects exist in multiple packFiles then they are counted multiple
+		// times
+		assertEquals(9, stats.numberOfPackedObjects);
+		assertEquals(2, stats.numberOfPackFiles);
+
 		// repack again but now without a grace period for packfiles. We should
 		// end up with one packfile
-		gc.setExpireAgeMillis(0);
+		gc.setPackExpireAgeMillis(0);
 		gc.gc();
 		stats = gc.getStatistics();
 		assertEquals(0, stats.numberOfLooseObjects);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java
index 5fda070..49e1f6f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java
@@ -95,7 +95,8 @@ public void testBitmapSpansNoMerges() throws Exception {
 			}
 			currentCommits = nextCommitCount;
 
-			gc.setExpireAgeMillis(0); // immediately delete old packs
+			gc.setPackExpireAgeMillis(0); // immediately delete old packs
+			gc.setExpireAgeMillis(0);
 			gc.gc();
 			assertEquals(currentCommits * 3, // commit/tree/object
 					gc.getStatistics().numberOfPackedObjects);
@@ -158,7 +159,8 @@ public void testBitmapSpansWithMerges() throws Exception {
 			}
 			currentCommits = nextCommitCount;
 
-			gc.setExpireAgeMillis(0); // immediately delete old packs
+			gc.setPackExpireAgeMillis(0); // immediately delete old packs
+			gc.setExpireAgeMillis(0);
 			gc.gc();
 			assertEquals(currentCommits + " commits: ", expectedBitmapCount,
 					gc.getStatistics().numberOfBitmaps);
@@ -198,7 +200,8 @@ public void testBitmapsForExcessiveBranches() throws Exception {
 		final int commitsForShallowBranches = 100;
 
 		// Excessive branch history pruning, one old branch.
-		gc.setExpireAgeMillis(0); // immediately delete old packs
+		gc.setPackExpireAgeMillis(0); // immediately delete old packs
+		gc.setExpireAgeMillis(0);
 		gc.gc();
 		assertEquals(
 				commitsForSparseBranch + commitsForFullBranch
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
index 49f9335..9cdb753 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
@@ -111,6 +111,8 @@
 public class GC {
 	private static final String PRUNE_EXPIRE_DEFAULT = "2.weeks.ago"; //$NON-NLS-1$
 
+	private static final String PRUNE_PACK_EXPIRE_DEFAULT = "1.hour.ago"; //$NON-NLS-1$
+
 	private final FileRepository repo;
 
 	private ProgressMonitor pm;
@@ -119,6 +121,10 @@ public class GC {
 
 	private Date expire;
 
+	private long packExpireAgeMillis = -1;
+
+	private Date packExpire;
+
 	private PackConfig pconfig = null;
 
 	/**
@@ -186,7 +192,7 @@ public Collection<PackFile> gc() throws IOException, ParseException {
 	 */
 	private void deleteOldPacks(Collection<PackFile> oldPacks,
 			Collection<PackFile> newPacks) throws ParseException {
-		long expireDate = getExpireDate();
+		long packExpireDate = getPackExpireDate();
 		oldPackLoop: for (PackFile oldPack : oldPacks) {
 			String oldName = oldPack.getPackName();
 			// check whether an old pack file is also among the list of new
@@ -196,7 +202,7 @@ private void deleteOldPacks(Collection<PackFile> oldPacks,
 					continue oldPackLoop;
 
 			if (!oldPack.shouldBeKept()
-					&& oldPack.getPackFile().lastModified() < expireDate) {
+					&& oldPack.getPackFile().lastModified() < packExpireDate) {
 				oldPack.close();
 				prunePack(oldName);
 			}
@@ -449,6 +455,26 @@ private long getExpireDate() throws ParseException {
 		return expireDate;
 	}
 
+	private long getPackExpireDate() throws ParseException {
+		long packExpireDate = Long.MAX_VALUE;
+
+		if (packExpire == null && packExpireAgeMillis == -1) {
+			String prunePackExpireStr = repo.getConfig().getString(
+					ConfigConstants.CONFIG_GC_SECTION, null,
+					ConfigConstants.CONFIG_KEY_PRUNEPACKEXPIRE);
+			if (prunePackExpireStr == null)
+				prunePackExpireStr = PRUNE_PACK_EXPIRE_DEFAULT;
+			packExpire = GitDateParser.parse(prunePackExpireStr, null,
+					SystemReader.getInstance().getLocale());
+			packExpireAgeMillis = -1;
+		}
+		if (packExpire != null)
+			packExpireDate = packExpire.getTime();
+		if (packExpireAgeMillis != -1)
+			packExpireDate = System.currentTimeMillis() - packExpireAgeMillis;
+		return packExpireDate;
+	}
+
 	/**
 	 * Remove all entries from a map which key is the id of an object referenced
 	 * by the given ObjectWalk
@@ -983,6 +1009,20 @@ public void setExpireAgeMillis(long expireAgeMillis) {
 	}
 
 	/**
+	 * During gc() or prune() packfiles which are created or modified in the
+	 * last <code>packExpireAgeMillis</code> milliseconds will not be deleted.
+	 * Only older packfiles may be deleted. If set to 0 then every packfile is a
+	 * candidate for deletion.
+	 *
+	 * @param packExpireAgeMillis
+	 *            minimal age of packfiles to be deleted in milliseconds.
+	 */
+	public void setPackExpireAgeMillis(long packExpireAgeMillis) {
+		this.packExpireAgeMillis = packExpireAgeMillis;
+		expire = null;
+	}
+
+	/**
 	 * Set the PackConfig used when (re-)writing packfiles. This allows to
 	 * influence how packs are written and to implement something similar to
 	 * "git gc --aggressive"
@@ -1011,4 +1051,18 @@ public void setExpire(Date expire) {
 		this.expire = expire;
 		expireAgeMillis = -1;
 	}
+
+	/**
+	 * During gc() or prune() packfiles which are created or modified after or
+	 * at <code>packExpire</code> will not be deleted. Only older packfiles may
+	 * be deleted. If set to null then every packfile is a candidate for
+	 * deletion.
+	 *
+	 * @param packExpire
+	 *            instant in time which defines packfile expiration
+	 */
+	public void setPackExpire(Date packExpire) {
+		this.packExpire = packExpire;
+		packExpireAgeMillis = -1;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index abc8c26..9e3e0b7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -248,6 +248,12 @@ public class ConfigConstants {
 	public static final String CONFIG_KEY_PRUNEEXPIRE = "pruneexpire";
 
 	/**
+	 * The "prunepackexpire" key
+	 * @since 4.3
+	 */
+	public static final String CONFIG_KEY_PRUNEPACKEXPIRE = "prunepackexpire";
+
+	/**
 	 * The "aggressiveDepth" key
 	 * @since 3.6
 	 */