Additional unit tests for the GC

Change-Id: Id5b578f7040c6c896ab9386a6b5ed62b0f495ed5
Signed-off-by: Sasa Zivkov <sasa.zivkov@sap.com>
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 b36b5c7..cc4852a 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
@@ -550,6 +550,23 @@
 	}
 
 	/**
+	 * Tag an object using a lightweight tag.
+	 *
+	 * @param name
+	 *            the tag name. The /refs/tags/ prefix will be added if the name
+	 *            doesn't start with it
+	 * @param obj
+	 *            the object to tag
+	 * @return the tagged object
+	 * @throws Exception
+	 */
+	public ObjectId lightweightTag(String name, ObjectId obj) throws Exception {
+		if (!name.startsWith(Constants.R_TAGS))
+			name = Constants.R_TAGS + name;
+		return update(name, obj);
+	}
+
+	/**
 	 * Run consistency checks against the object database.
 	 * <p>
 	 * This method completes silently if the checks pass. A temporary revision
@@ -733,6 +750,8 @@
 
 		private final DirCache tree = DirCache.newInCore();
 
+		private ObjectId topLevelTree;
+
 		private final List<RevCommit> parents = new ArrayList<RevCommit>(2);
 
 		private int tick = 1;
@@ -787,6 +806,11 @@
 			return this;
 		}
 
+		public CommitBuilder setTopLevelTree(ObjectId treeId) {
+			topLevelTree = treeId;
+			return this;
+		}
+
 		public CommitBuilder add(String path, String content) throws Exception {
 			return add(path, blob(content));
 		}
@@ -840,7 +864,10 @@
 
 				ObjectId commitId;
 				try {
-					c.setTreeId(tree.writeTree(inserter));
+					if (topLevelTree != null)
+						c.setTreeId(topLevelTree);
+					else
+						c.setTreeId(tree.writeTree(inserter));
 					commitId = inserter.insert(c);
 					inserter.flush();
 				} finally {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/GCTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/GCTest.java
index b2a7927..17c918b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/GCTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/GCTest.java
@@ -44,19 +44,40 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.util.Collection;
+import java.io.IOException;
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.junit.TestRepository.CommitBuilder;
+import org.eclipse.jgit.lib.EmptyProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RepositoryTestCase;
+import org.eclipse.jgit.lib.Ref.Storage;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.Merger;
+import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.storage.file.GC.RepoStatistics;
 import org.eclipse.jgit.storage.file.PackIndex.MutableEntry;
 import org.eclipse.jgit.util.FileUtils;
@@ -86,6 +107,310 @@
 		super.tearDown();
 	}
 
+	// GC.packRefs tests
+
+	@Test
+	public void packRefs_looseRefPacked() throws Exception {
+		RevBlob a = tr.blob("a");
+		tr.lightweightTag("t", a);
+
+		gc.packRefs();
+		assertSame(repo.getRef("t").getStorage(), Storage.PACKED);
+	}
+
+	@Test
+	public void concurrentPackRefs_onlyOneWritesPackedRefs() throws Exception {
+		RevBlob a = tr.blob("a");
+		tr.lightweightTag("t", a);
+
+		final CyclicBarrier syncPoint = new CyclicBarrier(2);
+
+		Callable<Integer> packRefs = new Callable<Integer>() {
+
+			/** @return 0 for success, 1 in case of error when writing pack */
+			public Integer call() throws Exception {
+				syncPoint.await();
+				try {
+					gc.packRefs();
+					return 0;
+				} catch (IOException e) {
+					return 1;
+				}
+			}
+		};
+		ExecutorService pool = Executors.newFixedThreadPool(2);
+		try {
+			Future<Integer> p1 = pool.submit(packRefs);
+			Future<Integer> p2 = pool.submit(packRefs);
+			assertTrue(p1.get() + p2.get() == 1);
+		} finally {
+			pool.shutdown();
+			pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+		}
+	}
+
+	@Test
+	public void packRefsWhileRefLocked_refNotPackedNoError()
+			throws Exception {
+		RevBlob a = tr.blob("a");
+		tr.lightweightTag("t1", a);
+		tr.lightweightTag("t2", a);
+		LockFile refLock = new LockFile(new File(repo.getDirectory(),
+				"refs/tags/t1"), repo.getFS());
+		try {
+			refLock.lock();
+			gc.packRefs();
+		} finally {
+			refLock.unlock();
+		}
+
+		assertSame(repo.getRef("refs/tags/t1").getStorage(), Storage.LOOSE);
+		assertSame(repo.getRef("refs/tags/t2").getStorage(), Storage.PACKED);
+	}
+
+	@Test
+	public void packRefsWhileRefUpdated_refUpdateSucceeds()
+			throws Exception {
+		RevBlob a = tr.blob("a");
+		tr.lightweightTag("t", a);
+		final RevBlob b = tr.blob("b");
+
+		final CyclicBarrier refUpdateLockedRef = new CyclicBarrier(2);
+		final CyclicBarrier packRefsDone = new CyclicBarrier(2);
+		ExecutorService pool = Executors.newFixedThreadPool(2);
+		try {
+			Future<Result> result = pool.submit(new Callable<Result>() {
+
+				public Result call() throws Exception {
+					RefUpdate update = new RefDirectoryUpdate(
+							(RefDirectory) repo.getRefDatabase(),
+							repo.getRef("refs/tags/t")) {
+						@Override
+						public boolean isForceUpdate() {
+							try {
+								refUpdateLockedRef.await();
+								packRefsDone.await();
+							} catch (InterruptedException e) {
+								Thread.currentThread().interrupt();
+							} catch (BrokenBarrierException e) {
+								Thread.currentThread().interrupt();
+							}
+							return super.isForceUpdate();
+						}
+					};
+					update.setForceUpdate(true);
+					update.setNewObjectId(b);
+					return update.update();
+				}
+			});
+
+			pool.submit(new Callable<Void>() {
+				public Void call() throws Exception {
+					refUpdateLockedRef.await();
+					gc.packRefs();
+					packRefsDone.await();
+					return null;
+				}
+			});
+
+			assertSame(result.get(), Result.FORCED);
+
+		} finally {
+			pool.shutdownNow();
+			pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+		}
+
+		assertEquals(repo.getRef("refs/tags/t").getObjectId(), b);
+	}
+
+	// GC.repack tests
+
+	@Test
+	public void repackEmptyRepo_noPackCreated() throws IOException {
+		gc.repack();
+		assertEquals(0, repo.getObjectDatabase().getPacks().size());
+	}
+
+	@Test
+	public void concurrentRepack() throws Exception {
+		final CyclicBarrier syncPoint = new CyclicBarrier(2);
+
+		class DoRepack extends EmptyProgressMonitor implements
+				Callable<Integer> {
+
+			public void beginTask(String title, int totalWork) {
+				if (title.equals(JGitText.get().writingObjects)) {
+					try {
+						syncPoint.await();
+					} catch (InterruptedException e) {
+						Thread.currentThread().interrupt();
+					} catch (BrokenBarrierException ignored) {
+						//
+					}
+				}
+			}
+
+			/** @return 0 for success, 1 in case of error when writing pack */
+			public Integer call() throws Exception {
+				try {
+					gc.setProgressMonitor(this);
+					gc.repack();
+					return 0;
+				} catch (IOException e) {
+					// leave the syncPoint in broken state so any awaiting
+					// threads and any threads that call await in the future get
+					// the BrokenBarrierException
+					Thread.currentThread().interrupt();
+					try {
+						syncPoint.await();
+					} catch (InterruptedException ignored) {
+						//
+					}
+					return 1;
+				}
+			}
+		}
+
+		RevBlob a = tr.blob("a");
+		tr.lightweightTag("t", a);
+
+		ExecutorService pool = Executors.newFixedThreadPool(2);
+		try {
+			DoRepack repack1 = new DoRepack();
+			DoRepack repack2 = new DoRepack();
+			Future<Integer> result1 = pool.submit(repack1);
+			Future<Integer> result2 = pool.submit(repack2);
+			assertTrue(result1.get() + result2.get() == 0);
+		} finally {
+			pool.shutdown();
+			pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+		}
+	}
+
+	// GC.prune tests
+
+	@Test
+	public void nonReferencedNonExpiredObject_notPruned() throws Exception {
+		long start = now();
+
+		fsTick();
+		RevBlob a = tr.blob("a");
+		long delta = now() - start;
+		gc.setExpireAgeMillis(delta);
+		gc.prune(Collections.<ObjectId> emptySet());
+		assertTrue(repo.hasObject(a));
+	}
+
+	@Test
+	public void nonReferencedExpiredObject_pruned() throws Exception {
+		RevBlob a = tr.blob("a");
+		gc.setExpireAgeMillis(0);
+		gc.prune(Collections.<ObjectId> emptySet());
+		assertFalse(repo.hasObject(a));
+	}
+
+	@Test
+	public void nonReferencedExpiredObjectTree_pruned() throws Exception {
+		RevBlob a = tr.blob("a");
+		RevTree t = tr.tree(tr.file("a", a));
+		gc.setExpireAgeMillis(0);
+		gc.prune(Collections.<ObjectId> emptySet());
+		assertFalse(repo.hasObject(t));
+		assertFalse(repo.hasObject(a));
+	}
+
+	@Test
+	public void nonReferencedObjects_onlyExpiredPruned() throws Exception {
+		RevBlob a = tr.blob("a");
+
+		fsTick();
+		long start = now();
+
+		fsTick();
+		RevBlob b = tr.blob("b");
+		gc.setExpireAgeMillis(now() - start);
+		gc.prune(Collections.<ObjectId> emptySet());
+		assertFalse(repo.hasObject(a));
+		assertTrue(repo.hasObject(b));
+	}
+
+	@Test
+	public void lightweightTag_objectNotPruned() throws Exception {
+		RevBlob a = tr.blob("a");
+		tr.lightweightTag("t", a);
+		gc.setExpireAgeMillis(0);
+		gc.prune(Collections.<ObjectId> emptySet());
+		assertTrue(repo.hasObject(a));
+	}
+
+	@Test
+	public void annotatedTag_objectNotPruned() throws Exception {
+		RevBlob a = tr.blob("a");
+		RevTag t = tr.tag("t", a); // this doesn't create the refs/tags/t ref
+		tr.lightweightTag("t", t);
+
+		gc.setExpireAgeMillis(0);
+		gc.prune(Collections.<ObjectId> emptySet());
+		assertTrue(repo.hasObject(t));
+		assertTrue(repo.hasObject(a));
+	}
+
+	@Test
+	public void branch_historyNotPruned() throws Exception {
+		RevCommit tip = commitChain(10);
+		tr.branch("b").update(tip);
+		gc.setExpireAgeMillis(0);
+		gc.prune(Collections.<ObjectId> emptySet());
+		do {
+			assertTrue(repo.hasObject(tip));
+			tr.parseBody(tip);
+			RevTree t = tip.getTree();
+			assertTrue(repo.hasObject(t));
+			assertTrue(repo.hasObject(tr.get(t, "a")));
+			tip = tip.getParentCount() > 0 ? tip.getParent(0) : null;
+		} while (tip != null);
+	}
+
+	@Test
+	public void deleteBranch_historyPruned() throws Exception {
+		RevCommit tip = commitChain(10);
+		tr.branch("b").update(tip);
+		RefUpdate update = repo.updateRef("refs/heads/b");
+		update.setForceUpdate(true);
+		update.delete();
+		gc.setExpireAgeMillis(0);
+		gc.prune(Collections.<ObjectId> emptySet());
+		assertTrue(gc.getStatistics().numberOfLooseObjects == 0);
+	}
+
+	@Test
+	public void deleteMergedBranch_historyNotPruned() throws Exception {
+		RevCommit parent = tr.commit().create();
+		RevCommit b1Tip = tr.branch("b1").commit().parent(parent).add("x", "x")
+				.create();
+		RevCommit b2Tip = tr.branch("b2").commit().parent(parent).add("y", "y")
+				.create();
+
+		// merge b1Tip and b2Tip and update refs/heads/b1 to the merge commit
+		Merger merger = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
+		merger.merge(b1Tip, b2Tip);
+		CommitBuilder cb = tr.commit();
+		cb.parent(b1Tip).parent(b2Tip);
+		cb.setTopLevelTree(merger.getResultTreeId());
+		RevCommit mergeCommit = cb.create();
+		RefUpdate u = repo.updateRef("refs/heads/b1");
+		u.setNewObjectId(mergeCommit);
+		u.update();
+
+		RefUpdate update = repo.updateRef("refs/heads/b2");
+		update.setForceUpdate(true);
+		update.delete();
+
+		gc.setExpireAgeMillis(0);
+		gc.prune(Collections.<ObjectId> emptySet());
+		assertTrue(repo.hasObject(b2Tip));
+	}
+
 	@Test
 	public void testPackAllObjectsInOnePack() throws Exception {
 		tr.branch("refs/heads/master").commit().add("A", "A").add("B", "B")
@@ -345,4 +670,41 @@
 		stats = gc.getStatistics();
 		assertEquals(8, stats.numberOfLooseObjects);
 	}
+
+	/**
+	 * Create a chain of commits of given depth.
+	 * <p>
+	 * Each commit contains one file named "a" containing the index of the
+	 * commit in the chain as its content. The created commit chain is
+	 * referenced from any ref.
+	 * <p>
+	 * A chain of depth = N will create 3*N objects in Gits object database. For
+	 * each depth level three objects are created: the commit object, the
+	 * top-level tree object and a blob for the content of the file "a".
+	 *
+	 * @param depth
+	 *            the depth of the commit chain.
+	 * @return the commit that is the tip of the commit chain
+	 * @throws Exception
+	 */
+	private RevCommit commitChain(int depth) throws Exception {
+		if (depth <= 0)
+			throw new IllegalArgumentException("Chain depth must be > 0");
+		CommitBuilder cb = tr.commit();
+		RevCommit tip;
+		do {
+			--depth;
+			tip = cb.add("a", "" + depth).message("" + depth).create();
+			cb = cb.child();
+		} while (depth > 0);
+		return tip;
+	}
+
+	private static long now() {
+		return System.currentTimeMillis();
+	}
+
+	private static void fsTick() throws InterruptedException, IOException {
+		RepositoryTestCase.fsTick(null);
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/EmptyProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/EmptyProgressMonitor.java
new file mode 100644
index 0000000..eabcbbf
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/EmptyProgressMonitor.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2012, Sasa Zivkov <sasa.zivkov@sap.com>
+ * 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;
+
+/**
+ * A convenient base class which provides empty method bodies for all
+ * ProgressMonitor methods.
+ * <p>
+ * Could be used in scenarios when only some of the progress notifications are
+ * important and others can be ignored.
+ */
+public abstract class EmptyProgressMonitor implements ProgressMonitor {
+
+	public void start(int totalTasks) {
+		// empty
+	}
+
+	public void beginTask(String title, int totalWork) {
+		// empty
+	}
+
+	public void update(int completed) {
+		// empty
+	}
+
+	public void endTask() {
+		// empty
+	}
+
+	public boolean isCancelled() {
+		return false;
+	}
+
+}