@ -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 ) ;
}
}