|
|
|
@ -44,19 +44,40 @@ package org.eclipse.jgit.storage.file;
|
|
|
|
|
|
|
|
|
|
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 @@ public class GCTest extends LocalDiskRepositoryTestCase {
|
|
|
|
|
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 @@ public class GCTest extends LocalDiskRepositoryTestCase {
|
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|