xref: /JGit/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java (revision 5c5f7c6b146b24f2bd4afae1902df85ad6e57ea3)
149aac325SShawn O. Pearce /*
2f5eb0d93SShawn O. Pearce  * Copyright (C) 2009-2010, Google Inc.
349aac325SShawn O. Pearce  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
4*5c5f7c6bSMatthias Sohn  * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org> and others
549aac325SShawn O. Pearce  *
6*5c5f7c6bSMatthias Sohn  * This program and the accompanying materials are made available under the
7*5c5f7c6bSMatthias Sohn  * terms of the Eclipse Distribution License v. 1.0 which is available at
8*5c5f7c6bSMatthias Sohn  * https://www.eclipse.org/org/documents/edl-v10.php.
949aac325SShawn O. Pearce  *
10*5c5f7c6bSMatthias Sohn  * SPDX-License-Identifier: BSD-3-Clause
1149aac325SShawn O. Pearce  */
1249aac325SShawn O. Pearce 
1349aac325SShawn O. Pearce package org.eclipse.jgit.junit;
1449aac325SShawn O. Pearce 
1530c6c754SDavid Pursehouse import static java.nio.charset.StandardCharsets.UTF_8;
16d9e07a57SRobin Rosenberg import static org.junit.Assert.assertFalse;
17d9e07a57SRobin Rosenberg import static org.junit.Assert.fail;
18d9e07a57SRobin Rosenberg 
1949aac325SShawn O. Pearce import java.io.File;
2049aac325SShawn O. Pearce import java.io.IOException;
21902935c3SMatthias Sohn import java.io.PrintStream;
2295e8264cSMatthias Sohn import java.time.Instant;
23a45cfee7SDavid Pursehouse import java.util.ArrayList;
24a45cfee7SDavid Pursehouse import java.util.Collections;
25a45cfee7SDavid Pursehouse import java.util.HashMap;
26d69a829bSDavid Pursehouse import java.util.HashSet;
27a45cfee7SDavid Pursehouse import java.util.List;
28a45cfee7SDavid Pursehouse import java.util.Map;
29d69a829bSDavid Pursehouse import java.util.Set;
30a45cfee7SDavid Pursehouse import java.util.TreeSet;
3149aac325SShawn O. Pearce 
3237a1e4beSChris Price import org.eclipse.jgit.dircache.DirCache;
3337a1e4beSChris Price import org.eclipse.jgit.dircache.DirCacheEntry;
34f32b8612SShawn Pearce import org.eclipse.jgit.internal.storage.file.FileRepository;
356b1e3c58SDavid Turner import org.eclipse.jgit.lib.ConfigConstants;
36a45cfee7SDavid Pursehouse import org.eclipse.jgit.lib.Constants;
37a45cfee7SDavid Pursehouse import org.eclipse.jgit.lib.ObjectId;
38a45cfee7SDavid Pursehouse import org.eclipse.jgit.lib.PersonIdent;
39a45cfee7SDavid Pursehouse import org.eclipse.jgit.lib.Repository;
40a45cfee7SDavid Pursehouse import org.eclipse.jgit.lib.RepositoryCache;
41ad5238dcSShawn O. Pearce import org.eclipse.jgit.storage.file.FileBasedConfig;
42ad5238dcSShawn O. Pearce import org.eclipse.jgit.storage.file.WindowCacheConfig;
43db82b8d7SJens Baumgart import org.eclipse.jgit.util.FS;
4438eec8f4SMatthias Sohn import org.eclipse.jgit.util.FileUtils;
4549aac325SShawn O. Pearce import org.eclipse.jgit.util.SystemReader;
46d9e07a57SRobin Rosenberg import org.junit.After;
47d9e07a57SRobin Rosenberg import org.junit.Before;
4849aac325SShawn O. Pearce 
4949aac325SShawn O. Pearce /**
5049aac325SShawn O. Pearce  * JUnit TestCase with specialized support for temporary local repository.
5149aac325SShawn O. Pearce  * <p>
5249aac325SShawn O. Pearce  * A temporary directory is created for each test, allowing each test to use a
5349aac325SShawn O. Pearce  * fresh environment. The temporary directory is cleaned up after the test ends.
5449aac325SShawn O. Pearce  * <p>
55a90b75b4SMatthias Sohn  * Callers should not use {@link org.eclipse.jgit.lib.RepositoryCache} from
56a90b75b4SMatthias Sohn  * within these tests as it may wedge file descriptors open past the end of the
57a90b75b4SMatthias Sohn  * test.
5849aac325SShawn O. Pearce  * <p>
5949aac325SShawn O. Pearce  * A system property {@code jgit.junit.usemmap} defines whether memory mapping
6049aac325SShawn O. Pearce  * is used. Memory mapping has an effect on the file system, in that memory
6149aac325SShawn O. Pearce  * mapped files in Java cannot be deleted as long as the mapped arrays have not
6249aac325SShawn O. Pearce  * been reclaimed by the garbage collector. The programmer cannot control this
6349aac325SShawn O. Pearce  * with precision, so temporary files may hang around longer than desired during
6449aac325SShawn O. Pearce  * a test, or tests may fail altogether if there is insufficient file
6549aac325SShawn O. Pearce  * descriptors or address space for the test process.
6649aac325SShawn O. Pearce  */
67d9e07a57SRobin Rosenberg public abstract class LocalDiskRepositoryTestCase {
6849aac325SShawn O. Pearce 	private static final boolean useMMAP = "true".equals(System
6949aac325SShawn O. Pearce 			.getProperty("jgit.junit.usemmap"));
7049aac325SShawn O. Pearce 
7149aac325SShawn O. Pearce 	/** A fake (but stable) identity for author fields in the test. */
7249aac325SShawn O. Pearce 	protected PersonIdent author;
7349aac325SShawn O. Pearce 
7449aac325SShawn O. Pearce 	/** A fake (but stable) identity for committer fields in the test. */
7549aac325SShawn O. Pearce 	protected PersonIdent committer;
7649aac325SShawn O. Pearce 
772cdc130dSMatthias Sohn 	/**
782cdc130dSMatthias Sohn 	 * A {@link SystemReader} used to coordinate time, envars, etc.
792cdc130dSMatthias Sohn 	 * @since 4.2
802cdc130dSMatthias Sohn 	 */
81069040ddSTerry Parker 	protected MockSystemReader mockSystemReader;
82069040ddSTerry Parker 
83d69a829bSDavid Pursehouse 	private final Set<Repository> toClose = new HashSet<>();
8436144e12SShawn Pearce 	private File tmp;
8549aac325SShawn O. Pearce 
86a90b75b4SMatthias Sohn 	/**
87a90b75b4SMatthias Sohn 	 * Setup test
88a90b75b4SMatthias Sohn 	 *
89a90b75b4SMatthias Sohn 	 * @throws Exception
90a90b75b4SMatthias Sohn 	 */
91d9e07a57SRobin Rosenberg 	@Before
setUp()92d9e07a57SRobin Rosenberg 	public void setUp() throws Exception {
9336144e12SShawn Pearce 		tmp = File.createTempFile("jgit_test_", "_tmp");
9436144e12SShawn Pearce 		CleanupThread.deleteOnShutdown(tmp);
95838b5a84SMatthias Sohn 		if (!tmp.delete() || !tmp.mkdir()) {
9636144e12SShawn Pearce 			throw new IOException("Cannot create " + tmp);
97838b5a84SMatthias Sohn 		}
9849aac325SShawn O. Pearce 
9949aac325SShawn O. Pearce 		mockSystemReader = new MockSystemReader();
100c28167e1SMatthias Sohn 		SystemReader.setInstance(mockSystemReader);
1017f92a70fSMatthias Sohn 
1027f92a70fSMatthias Sohn 		// Measure timer resolution before the test to avoid time critical tests
1037f92a70fSMatthias Sohn 		// are affected by time needed for measurement.
1047f92a70fSMatthias Sohn 		// The MockSystemReader must be configured first since we need to use
1057f92a70fSMatthias Sohn 		// the same one here
106db82b8d7SJens Baumgart 		FS.getFileStoreAttributes(tmp.toPath().getParent());
1076b1e3c58SDavid Turner 
108838b5a84SMatthias Sohn 		FileBasedConfig jgitConfig = new FileBasedConfig(
109838b5a84SMatthias Sohn 				new File(tmp, "jgitconfig"), FS.DETECTED);
110838b5a84SMatthias Sohn 		FileBasedConfig systemConfig = new FileBasedConfig(jgitConfig,
111838b5a84SMatthias Sohn 				new File(tmp, "systemgitconfig"), FS.DETECTED);
112838b5a84SMatthias Sohn 		FileBasedConfig userConfig = new FileBasedConfig(systemConfig,
113f383206aSMatthias Sohn 				new File(tmp, "usergitconfig"), FS.DETECTED);
1146b1e3c58SDavid Turner 		// We have to set autoDetach to false for tests, because tests expect to be able
1156b1e3c58SDavid Turner 		// to clean up by recursively removing the repository, and background GC might be
1166b1e3c58SDavid Turner 		// in the middle of writing or deleting files, which would disrupt this.
117f383206aSMatthias Sohn 		userConfig.setBoolean(ConfigConstants.CONFIG_GC_SECTION,
1186b1e3c58SDavid Turner 				null, ConfigConstants.CONFIG_KEY_AUTODETACH, false);
119f383206aSMatthias Sohn 		userConfig.save();
120838b5a84SMatthias Sohn 		mockSystemReader.setJGitConfig(jgitConfig);
121838b5a84SMatthias Sohn 		mockSystemReader.setSystemGitConfig(systemConfig);
122f383206aSMatthias Sohn 		mockSystemReader.setUserGitConfig(userConfig);
123838b5a84SMatthias Sohn 
124eb63bfc1SRobin Rosenberg 		ceilTestDirectories(getCeilings());
12549aac325SShawn O. Pearce 
12649aac325SShawn O. Pearce 		author = new PersonIdent("J. Author", "jauthor@example.com");
12749aac325SShawn O. Pearce 		committer = new PersonIdent("J. Committer", "jcommitter@example.com");
12849aac325SShawn O. Pearce 
12949aac325SShawn O. Pearce 		final WindowCacheConfig c = new WindowCacheConfig();
13049aac325SShawn O. Pearce 		c.setPackedGitLimit(128 * WindowCacheConfig.KB);
13149aac325SShawn O. Pearce 		c.setPackedGitWindowSize(8 * WindowCacheConfig.KB);
13249aac325SShawn O. Pearce 		c.setPackedGitMMAP(useMMAP);
13349aac325SShawn O. Pearce 		c.setDeltaBaseCacheLimit(8 * WindowCacheConfig.KB);
134f32b8612SShawn Pearce 		c.install();
13549aac325SShawn O. Pearce 	}
13649aac325SShawn O. Pearce 
137a90b75b4SMatthias Sohn 	/**
138a90b75b4SMatthias Sohn 	 * Get temporary directory.
139a90b75b4SMatthias Sohn 	 *
140a90b75b4SMatthias Sohn 	 * @return the temporary directory
141a90b75b4SMatthias Sohn 	 */
getTemporaryDirectory()14236144e12SShawn Pearce 	protected File getTemporaryDirectory() {
14336144e12SShawn Pearce 		return tmp.getAbsoluteFile();
14436144e12SShawn Pearce 	}
145eb63bfc1SRobin Rosenberg 
146a90b75b4SMatthias Sohn 	/**
147a90b75b4SMatthias Sohn 	 * Get list of ceiling directories
148a90b75b4SMatthias Sohn 	 *
149a90b75b4SMatthias Sohn 	 * @return list of ceiling directories
150a90b75b4SMatthias Sohn 	 */
getCeilings()151eb63bfc1SRobin Rosenberg 	protected List<File> getCeilings() {
15236144e12SShawn Pearce 		return Collections.singletonList(getTemporaryDirectory());
153eb63bfc1SRobin Rosenberg 	}
154eb63bfc1SRobin Rosenberg 
ceilTestDirectories(List<File> ceilings)155eb63bfc1SRobin Rosenberg 	private void ceilTestDirectories(List<File> ceilings) {
156eb63bfc1SRobin Rosenberg 		mockSystemReader.setProperty(Constants.GIT_CEILING_DIRECTORIES_KEY, makePath(ceilings));
157eb63bfc1SRobin Rosenberg 	}
158eb63bfc1SRobin Rosenberg 
makePath(List<?> objects)159a57dd1c1SRobin Rosenberg 	private static String makePath(List<?> objects) {
160eb63bfc1SRobin Rosenberg 		final StringBuilder stringBuilder = new StringBuilder();
161eb63bfc1SRobin Rosenberg 		for (Object object : objects) {
162eb63bfc1SRobin Rosenberg 			if (stringBuilder.length() > 0)
163eb63bfc1SRobin Rosenberg 				stringBuilder.append(File.pathSeparatorChar);
164eb63bfc1SRobin Rosenberg 			stringBuilder.append(object.toString());
165eb63bfc1SRobin Rosenberg 		}
166eb63bfc1SRobin Rosenberg 		return stringBuilder.toString();
167eb63bfc1SRobin Rosenberg 	}
168eb63bfc1SRobin Rosenberg 
169a90b75b4SMatthias Sohn 	/**
170a90b75b4SMatthias Sohn 	 * Tear down the test
171a90b75b4SMatthias Sohn 	 *
172a90b75b4SMatthias Sohn 	 * @throws Exception
173a90b75b4SMatthias Sohn 	 */
174d9e07a57SRobin Rosenberg 	@After
tearDown()175d9e07a57SRobin Rosenberg 	public void tearDown() throws Exception {
17649aac325SShawn O. Pearce 		RepositoryCache.clear();
17749aac325SShawn O. Pearce 		for (Repository r : toClose)
17849aac325SShawn O. Pearce 			r.close();
17949aac325SShawn O. Pearce 		toClose.clear();
18049aac325SShawn O. Pearce 
18149aac325SShawn O. Pearce 		// Since memory mapping is controlled by the GC we need to
18249aac325SShawn O. Pearce 		// tell it this is a good time to clean up and unlock
18349aac325SShawn O. Pearce 		// memory mapped files.
18449aac325SShawn O. Pearce 		//
18549aac325SShawn O. Pearce 		if (useMMAP)
18649aac325SShawn O. Pearce 			System.gc();
18736144e12SShawn Pearce 		if (tmp != null)
18836144e12SShawn Pearce 			recursiveDelete(tmp, false, true);
18936144e12SShawn Pearce 		if (tmp != null && !tmp.exists())
19036144e12SShawn Pearce 			CleanupThread.removed(tmp);
19109711a4bSRobin Stocker 
19209711a4bSRobin Stocker 		SystemReader.setInstance(null);
19349aac325SShawn O. Pearce 	}
19449aac325SShawn O. Pearce 
195a90b75b4SMatthias Sohn 	/**
196a90b75b4SMatthias Sohn 	 * Increment the {@link #author} and {@link #committer} times.
197a90b75b4SMatthias Sohn 	 */
tick()19849aac325SShawn O. Pearce 	protected void tick() {
199069040ddSTerry Parker 		mockSystemReader.tick(5 * 60);
200069040ddSTerry Parker 		final long now = mockSystemReader.getCurrentTime();
20149aac325SShawn O. Pearce 		final int tz = mockSystemReader.getTimezone(now);
20249aac325SShawn O. Pearce 
20349aac325SShawn O. Pearce 		author = new PersonIdent(author, now, tz);
20449aac325SShawn O. Pearce 		committer = new PersonIdent(committer, now, tz);
20549aac325SShawn O. Pearce 	}
20649aac325SShawn O. Pearce 
20749aac325SShawn O. Pearce 	/**
20849aac325SShawn O. Pearce 	 * Recursively delete a directory, failing the test if the delete fails.
20949aac325SShawn O. Pearce 	 *
21049aac325SShawn O. Pearce 	 * @param dir
21149aac325SShawn O. Pearce 	 *            the recursively directory to delete, if present.
21249aac325SShawn O. Pearce 	 */
recursiveDelete(File dir)2136d370d83SHan-Wen Nienhuys 	protected void recursiveDelete(File dir) {
21436144e12SShawn Pearce 		recursiveDelete(dir, false, true);
21549aac325SShawn O. Pearce 	}
21649aac325SShawn O. Pearce 
recursiveDelete(final File dir, boolean silent, boolean failOnError)21736144e12SShawn Pearce 	private static boolean recursiveDelete(final File dir,
21836144e12SShawn Pearce 			boolean silent, boolean failOnError) {
21949aac325SShawn O. Pearce 		assert !(silent && failOnError);
220902935c3SMatthias Sohn 		int options = FileUtils.RECURSIVE | FileUtils.RETRY
221902935c3SMatthias Sohn 				| FileUtils.SKIP_MISSING;
222902935c3SMatthias Sohn 		if (silent) {
223902935c3SMatthias Sohn 			options |= FileUtils.IGNORE_ERRORS;
2246e03645aSCarsten Hammer 		}
225902935c3SMatthias Sohn 		try {
226902935c3SMatthias Sohn 			FileUtils.delete(dir, options);
227902935c3SMatthias Sohn 		} catch (IOException e) {
228902935c3SMatthias Sohn 			reportDeleteFailure(failOnError, dir, e);
229902935c3SMatthias Sohn 			return !failOnError;
23049aac325SShawn O. Pearce 		}
231902935c3SMatthias Sohn 		return true;
23249aac325SShawn O. Pearce 	}
23349aac325SShawn O. Pearce 
reportDeleteFailure(boolean failOnError, File f, Exception cause)234902935c3SMatthias Sohn 	private static void reportDeleteFailure(boolean failOnError, File f,
235902935c3SMatthias Sohn 			Exception cause) {
23636144e12SShawn Pearce 		String severity = failOnError ? "ERROR" : "WARNING";
237902935c3SMatthias Sohn 		String msg = severity + ": Failed to delete " + f;
238902935c3SMatthias Sohn 		if (failOnError) {
23949aac325SShawn O. Pearce 			fail(msg);
240902935c3SMatthias Sohn 		} else {
24149aac325SShawn O. Pearce 			System.err.println(msg);
24249aac325SShawn O. Pearce 		}
243902935c3SMatthias Sohn 		cause.printStackTrace(new PrintStream(System.err));
244902935c3SMatthias Sohn 	}
24549aac325SShawn O. Pearce 
246a90b75b4SMatthias Sohn 	/** Constant <code>MOD_TIME=1</code> */
24737a1e4beSChris Price 	public static final int MOD_TIME = 1;
24837a1e4beSChris Price 
249a90b75b4SMatthias Sohn 	/** Constant <code>SMUDGE=2</code> */
25037a1e4beSChris Price 	public static final int SMUDGE = 2;
25137a1e4beSChris Price 
252a90b75b4SMatthias Sohn 	/** Constant <code>LENGTH=4</code> */
25337a1e4beSChris Price 	public static final int LENGTH = 4;
25437a1e4beSChris Price 
255a90b75b4SMatthias Sohn 	/** Constant <code>CONTENT_ID=8</code> */
25637a1e4beSChris Price 	public static final int CONTENT_ID = 8;
25737a1e4beSChris Price 
258a90b75b4SMatthias Sohn 	/** Constant <code>CONTENT=16</code> */
25937a1e4beSChris Price 	public static final int CONTENT = 16;
26037a1e4beSChris Price 
261a90b75b4SMatthias Sohn 	/** Constant <code>ASSUME_UNCHANGED=32</code> */
26237a1e4beSChris Price 	public static final int ASSUME_UNCHANGED = 32;
26337a1e4beSChris Price 
26437a1e4beSChris Price 	/**
26537a1e4beSChris Price 	 * Represent the state of the index in one String. This representation is
26637a1e4beSChris Price 	 * useful when writing tests which do assertions on the state of the index.
26737a1e4beSChris Price 	 * By default information about path, mode, stage (if different from 0) is
26837a1e4beSChris Price 	 * included. A bitmask controls which additional info about
26937a1e4beSChris Price 	 * modificationTimes, smudge state and length is included.
27037a1e4beSChris Price 	 * <p>
27137a1e4beSChris Price 	 * The format of the returned string is described with this BNF:
27237a1e4beSChris Price 	 *
27337a1e4beSChris Price 	 * <pre>
27437a1e4beSChris Price 	 * result = ( "[" path mode stage? time? smudge? length? sha1? content? "]" )* .
27537a1e4beSChris Price 	 * mode = ", mode:" number .
27637a1e4beSChris Price 	 * stage = ", stage:" number .
27737a1e4beSChris Price 	 * time = ", time:t" timestamp-index .
27837a1e4beSChris Price 	 * smudge = "" | ", smudged" .
27937a1e4beSChris Price 	 * length = ", length:" number .
28037a1e4beSChris Price 	 * sha1 = ", sha1:" hex-sha1 .
28137a1e4beSChris Price 	 * content = ", content:" blob-data .
28237a1e4beSChris Price 	 * </pre>
28337a1e4beSChris Price 	 *
28437a1e4beSChris Price 	 * 'stage' is only presented when the stage is different from 0. All
28537a1e4beSChris Price 	 * reported time stamps are mapped to strings like "t0", "t1", ... "tn". The
28637a1e4beSChris Price 	 * smallest reported time-stamp will be called "t0". This allows to write
28737a1e4beSChris Price 	 * assertions against the string although the concrete value of the time
28837a1e4beSChris Price 	 * stamps is unknown.
28937a1e4beSChris Price 	 *
29037a1e4beSChris Price 	 * @param repo
29137a1e4beSChris Price 	 *            the repository the index state should be determined for
29237a1e4beSChris Price 	 * @param includedOptions
29337a1e4beSChris Price 	 *            a bitmask constructed out of the constants {@link #MOD_TIME},
29437a1e4beSChris Price 	 *            {@link #SMUDGE}, {@link #LENGTH}, {@link #CONTENT_ID} and
29537a1e4beSChris Price 	 *            {@link #CONTENT} controlling which info is present in the
29637a1e4beSChris Price 	 *            resulting string.
29737a1e4beSChris Price 	 * @return a string encoding the index state
29837a1e4beSChris Price 	 * @throws IllegalStateException
29937a1e4beSChris Price 	 * @throws IOException
30037a1e4beSChris Price 	 */
indexState(Repository repo, int includedOptions)30137a1e4beSChris Price 	public static String indexState(Repository repo, int includedOptions)
30237a1e4beSChris Price 			throws IllegalStateException, IOException {
30337a1e4beSChris Price 		DirCache dc = repo.readDirCache();
30437a1e4beSChris Price 		StringBuilder sb = new StringBuilder();
30595e8264cSMatthias Sohn 		TreeSet<Instant> timeStamps = new TreeSet<>();
30637a1e4beSChris Price 
30737a1e4beSChris Price 		// iterate once over the dircache just to collect all time stamps
30837a1e4beSChris Price 		if (0 != (includedOptions & MOD_TIME)) {
30995e8264cSMatthias Sohn 			for (int i = 0; i < dc.getEntryCount(); ++i) {
31095e8264cSMatthias Sohn 				timeStamps.add(dc.getEntry(i).getLastModifiedInstant());
31195e8264cSMatthias Sohn 			}
31237a1e4beSChris Price 		}
31337a1e4beSChris Price 
31437a1e4beSChris Price 		// iterate again, now produce the result string
31537a1e4beSChris Price 		for (int i=0; i<dc.getEntryCount(); ++i) {
31637a1e4beSChris Price 			DirCacheEntry entry = dc.getEntry(i);
31737a1e4beSChris Price 			sb.append("["+entry.getPathString()+", mode:" + entry.getFileMode());
31837a1e4beSChris Price 			int stage = entry.getStage();
31937a1e4beSChris Price 			if (stage != 0)
32037a1e4beSChris Price 				sb.append(", stage:" + stage);
32137a1e4beSChris Price 			if (0 != (includedOptions & MOD_TIME)) {
32237a1e4beSChris Price 				sb.append(", time:t"+
32395e8264cSMatthias Sohn 						timeStamps.headSet(entry.getLastModifiedInstant())
32495e8264cSMatthias Sohn 								.size());
32537a1e4beSChris Price 			}
32637a1e4beSChris Price 			if (0 != (includedOptions & SMUDGE))
32737a1e4beSChris Price 				if (entry.isSmudged())
32837a1e4beSChris Price 					sb.append(", smudged");
32937a1e4beSChris Price 			if (0 != (includedOptions & LENGTH))
33037a1e4beSChris Price 				sb.append(", length:"
33137a1e4beSChris Price 						+ Integer.toString(entry.getLength()));
33237a1e4beSChris Price 			if (0 != (includedOptions & CONTENT_ID))
33337a1e4beSChris Price 				sb.append(", sha1:" + ObjectId.toString(entry.getObjectId()));
33437a1e4beSChris Price 			if (0 != (includedOptions & CONTENT)) {
33537a1e4beSChris Price 				sb.append(", content:"
33637a1e4beSChris Price 						+ new String(repo.open(entry.getObjectId(),
33730c6c754SDavid Pursehouse 								Constants.OBJ_BLOB).getCachedBytes(), UTF_8));
33837a1e4beSChris Price 			}
33937a1e4beSChris Price 			if (0 != (includedOptions & ASSUME_UNCHANGED))
34037a1e4beSChris Price 				sb.append(", assume-unchanged:"
34137a1e4beSChris Price 						+ Boolean.toString(entry.isAssumeValid()));
34237a1e4beSChris Price 			sb.append("]");
34337a1e4beSChris Price 		}
34437a1e4beSChris Price 		return sb.toString();
34537a1e4beSChris Price 	}
34637a1e4beSChris Price 
34737a1e4beSChris Price 
34849aac325SShawn O. Pearce 	/**
34949aac325SShawn O. Pearce 	 * Creates a new empty bare repository.
35049aac325SShawn O. Pearce 	 *
351ee32ca22SDavid Pursehouse 	 * @return the newly created bare repository, opened for access. The
352ee32ca22SDavid Pursehouse 	 *         repository will not be closed in {@link #tearDown()}; the caller
353ee32ca22SDavid Pursehouse 	 *         is responsible for closing it.
35449aac325SShawn O. Pearce 	 * @throws IOException
35549aac325SShawn O. Pearce 	 *             the repository could not be created in the temporary area
35649aac325SShawn O. Pearce 	 */
createBareRepository()35789d4a737SShawn O. Pearce 	protected FileRepository createBareRepository() throws IOException {
35849aac325SShawn O. Pearce 		return createRepository(true /* bare */);
35949aac325SShawn O. Pearce 	}
36049aac325SShawn O. Pearce 
36149aac325SShawn O. Pearce 	/**
36249aac325SShawn O. Pearce 	 * Creates a new empty repository within a new empty working directory.
36349aac325SShawn O. Pearce 	 *
364ee32ca22SDavid Pursehouse 	 * @return the newly created repository, opened for access. The repository
365ee32ca22SDavid Pursehouse 	 *         will not be closed in {@link #tearDown()}; the caller is
366ee32ca22SDavid Pursehouse 	 *         responsible for closing it.
36749aac325SShawn O. Pearce 	 * @throws IOException
36849aac325SShawn O. Pearce 	 *             the repository could not be created in the temporary area
36949aac325SShawn O. Pearce 	 */
createWorkRepository()37089d4a737SShawn O. Pearce 	protected FileRepository createWorkRepository() throws IOException {
37149aac325SShawn O. Pearce 		return createRepository(false /* not bare */);
37249aac325SShawn O. Pearce 	}
37349aac325SShawn O. Pearce 
37449aac325SShawn O. Pearce 	/**
37549aac325SShawn O. Pearce 	 * Creates a new empty repository.
37649aac325SShawn O. Pearce 	 *
37749aac325SShawn O. Pearce 	 * @param bare
37849aac325SShawn O. Pearce 	 *            true to create a bare repository; false to make a repository
37949aac325SShawn O. Pearce 	 *            within its working directory
380ee32ca22SDavid Pursehouse 	 * @return the newly created repository, opened for access. The repository
381ee32ca22SDavid Pursehouse 	 *         will not be closed in {@link #tearDown()}; the caller is
382ee32ca22SDavid Pursehouse 	 *         responsible for closing it.
38349aac325SShawn O. Pearce 	 * @throws IOException
38449aac325SShawn O. Pearce 	 *             the repository could not be created in the temporary area
38552923e9bSDavid Pursehouse 	 * @since 5.3
38649aac325SShawn O. Pearce 	 */
createRepository(boolean bare)38752923e9bSDavid Pursehouse 	protected FileRepository createRepository(boolean bare)
38853ad4373SMatthias Sohn 			throws IOException {
38952923e9bSDavid Pursehouse 		return createRepository(bare, false /* auto close */);
39053ad4373SMatthias Sohn 	}
39153ad4373SMatthias Sohn 
39253ad4373SMatthias Sohn 	/**
39353ad4373SMatthias Sohn 	 * Creates a new empty repository.
39453ad4373SMatthias Sohn 	 *
39553ad4373SMatthias Sohn 	 * @param bare
39653ad4373SMatthias Sohn 	 *            true to create a bare repository; false to make a repository
39753ad4373SMatthias Sohn 	 *            within its working directory
39853ad4373SMatthias Sohn 	 * @param autoClose
39952923e9bSDavid Pursehouse 	 *            auto close the repository in {@link #tearDown()}
40053ad4373SMatthias Sohn 	 * @return the newly created repository, opened for access
40153ad4373SMatthias Sohn 	 * @throws IOException
40253ad4373SMatthias Sohn 	 *             the repository could not be created in the temporary area
40352923e9bSDavid Pursehouse 	 * @deprecated use {@link #createRepository(boolean)} instead
40453ad4373SMatthias Sohn 	 */
40552923e9bSDavid Pursehouse 	@Deprecated
createRepository(boolean bare, boolean autoClose)40653ad4373SMatthias Sohn 	public FileRepository createRepository(boolean bare, boolean autoClose)
40753ad4373SMatthias Sohn 			throws IOException {
408ea0f2f9eSMatthias Sohn 		File gitdir = createUniqueTestGitDir(bare);
40989d4a737SShawn O. Pearce 		FileRepository db = new FileRepository(gitdir);
41049aac325SShawn O. Pearce 		assertFalse(gitdir.exists());
41135c00a7aSChristian Halstrick 		db.create(bare);
41253ad4373SMatthias Sohn 		if (autoClose) {
4135d0ca36dSDavid Pursehouse 			addRepoToClose(db);
41453ad4373SMatthias Sohn 		}
41549aac325SShawn O. Pearce 		return db;
41649aac325SShawn O. Pearce 	}
41749aac325SShawn O. Pearce 
41849aac325SShawn O. Pearce 	/**
419c1525e2aSChristian Halstrick 	 * Adds a repository to the list of repositories which is closed at the end
420c1525e2aSChristian Halstrick 	 * of the tests
421c1525e2aSChristian Halstrick 	 *
422c1525e2aSChristian Halstrick 	 * @param r
423c1525e2aSChristian Halstrick 	 *            the repository to be closed
424c1525e2aSChristian Halstrick 	 */
addRepoToClose(Repository r)425c1525e2aSChristian Halstrick 	public void addRepoToClose(Repository r) {
426c1525e2aSChristian Halstrick 		toClose.add(r);
427c1525e2aSChristian Halstrick 	}
428c1525e2aSChristian Halstrick 
429ad74bbf9SAdrian Goerler 	/**
430ad74bbf9SAdrian Goerler 	 * Creates a unique directory for a test
431ad74bbf9SAdrian Goerler 	 *
432ad74bbf9SAdrian Goerler 	 * @param name
433ad74bbf9SAdrian Goerler 	 *            a subdirectory
434ad74bbf9SAdrian Goerler 	 * @return a unique directory for a test
435ad74bbf9SAdrian Goerler 	 * @throws IOException
436ad74bbf9SAdrian Goerler 	 */
createTempDirectory(String name)437ad74bbf9SAdrian Goerler 	protected File createTempDirectory(String name) throws IOException {
43836144e12SShawn Pearce 		File directory = new File(createTempFile(), name);
439ad74bbf9SAdrian Goerler 		FileUtils.mkdirs(directory);
440ad74bbf9SAdrian Goerler 		return directory.getCanonicalFile();
441ad74bbf9SAdrian Goerler 	}
442ad74bbf9SAdrian Goerler 
443c1525e2aSChristian Halstrick 	/**
444ea0f2f9eSMatthias Sohn 	 * Creates a new unique directory for a test repository
445ea0f2f9eSMatthias Sohn 	 *
446ea0f2f9eSMatthias Sohn 	 * @param bare
447ea0f2f9eSMatthias Sohn 	 *            true for a bare repository; false for a repository with a
448ea0f2f9eSMatthias Sohn 	 *            working directory
449ea0f2f9eSMatthias Sohn 	 * @return a unique directory for a test repository
450ea0f2f9eSMatthias Sohn 	 * @throws IOException
451ea0f2f9eSMatthias Sohn 	 */
createUniqueTestGitDir(boolean bare)452ea0f2f9eSMatthias Sohn 	protected File createUniqueTestGitDir(boolean bare) throws IOException {
45336144e12SShawn Pearce 		String gitdirName = createTempFile().getPath();
454ad74bbf9SAdrian Goerler 		if (!bare)
455ad74bbf9SAdrian Goerler 			gitdirName += "/";
45636144e12SShawn Pearce 		return new File(gitdirName + Constants.DOT_GIT);
457ea0f2f9eSMatthias Sohn 	}
458ea0f2f9eSMatthias Sohn 
45936144e12SShawn Pearce 	/**
46036144e12SShawn Pearce 	 * Allocates a new unique file path that does not exist.
46136144e12SShawn Pearce 	 * <p>
46236144e12SShawn Pearce 	 * Unlike the standard {@code File.createTempFile} the returned path does
46336144e12SShawn Pearce 	 * not exist, but may be created by another thread in a race with the
46436144e12SShawn Pearce 	 * caller. Good luck.
46536144e12SShawn Pearce 	 * <p>
46636144e12SShawn Pearce 	 * This method is inherently unsafe due to a race condition between creating
46736144e12SShawn Pearce 	 * the name and the first use that reserves it.
46836144e12SShawn Pearce 	 *
46936144e12SShawn Pearce 	 * @return a unique path that does not exist.
47036144e12SShawn Pearce 	 * @throws IOException
47136144e12SShawn Pearce 	 */
createTempFile()472d60001c8SKetan Padegaonkar 	protected File createTempFile() throws IOException {
47336144e12SShawn Pearce 		File p = File.createTempFile("tmp_", "", tmp);
47436144e12SShawn Pearce 		if (!p.delete()) {
47536144e12SShawn Pearce 			throw new IOException("Cannot obtain unique path " + tmp);
47636144e12SShawn Pearce 		}
47736144e12SShawn Pearce 		return p;
478d60001c8SKetan Padegaonkar 	}
479d60001c8SKetan Padegaonkar 
480ea0f2f9eSMatthias Sohn 	/**
48149aac325SShawn O. Pearce 	 * Run a hook script in the repository, returning the exit status.
48249aac325SShawn O. Pearce 	 *
48349aac325SShawn O. Pearce 	 * @param db
48449aac325SShawn O. Pearce 	 *            repository the script should see in GIT_DIR environment
48549aac325SShawn O. Pearce 	 * @param hook
48649aac325SShawn O. Pearce 	 *            path of the hook script to execute, must be executable file
48749aac325SShawn O. Pearce 	 *            type on this platform
48849aac325SShawn O. Pearce 	 * @param args
48949aac325SShawn O. Pearce 	 *            arguments to pass to the hook script
49049aac325SShawn O. Pearce 	 * @return exit status code of the invoked hook
49149aac325SShawn O. Pearce 	 * @throws IOException
49249aac325SShawn O. Pearce 	 *             the hook could not be executed
49349aac325SShawn O. Pearce 	 * @throws InterruptedException
49449aac325SShawn O. Pearce 	 *             the caller was interrupted before the hook completed
49549aac325SShawn O. Pearce 	 */
runHook(final Repository db, final File hook, final String... args)49649aac325SShawn O. Pearce 	protected int runHook(final Repository db, final File hook,
49749aac325SShawn O. Pearce 			final String... args) throws IOException, InterruptedException {
49849aac325SShawn O. Pearce 		final String[] argv = new String[1 + args.length];
49949aac325SShawn O. Pearce 		argv[0] = hook.getAbsolutePath();
50049aac325SShawn O. Pearce 		System.arraycopy(args, 0, argv, 1, args.length);
50149aac325SShawn O. Pearce 
50249aac325SShawn O. Pearce 		final Map<String, String> env = cloneEnv();
50349aac325SShawn O. Pearce 		env.put("GIT_DIR", db.getDirectory().getAbsolutePath());
50449aac325SShawn O. Pearce 		putPersonIdent(env, "AUTHOR", author);
50549aac325SShawn O. Pearce 		putPersonIdent(env, "COMMITTER", committer);
50649aac325SShawn O. Pearce 
507203bd662SShawn O. Pearce 		final File cwd = db.getWorkTree();
50849aac325SShawn O. Pearce 		final Process p = Runtime.getRuntime().exec(argv, toEnvArray(env), cwd);
50949aac325SShawn O. Pearce 		p.getOutputStream().close();
51049aac325SShawn O. Pearce 		p.getErrorStream().close();
51149aac325SShawn O. Pearce 		p.getInputStream().close();
51249aac325SShawn O. Pearce 		return p.waitFor();
51349aac325SShawn O. Pearce 	}
51449aac325SShawn O. Pearce 
putPersonIdent(final Map<String, String> env, final String type, final PersonIdent who)51549aac325SShawn O. Pearce 	private static void putPersonIdent(final Map<String, String> env,
51649aac325SShawn O. Pearce 			final String type, final PersonIdent who) {
51749aac325SShawn O. Pearce 		final String ident = who.toExternalString();
51849aac325SShawn O. Pearce 		final String date = ident.substring(ident.indexOf("> ") + 2);
51949aac325SShawn O. Pearce 		env.put("GIT_" + type + "_NAME", who.getName());
52049aac325SShawn O. Pearce 		env.put("GIT_" + type + "_EMAIL", who.getEmailAddress());
52149aac325SShawn O. Pearce 		env.put("GIT_" + type + "_DATE", date);
52249aac325SShawn O. Pearce 	}
52349aac325SShawn O. Pearce 
52449aac325SShawn O. Pearce 	/**
52549aac325SShawn O. Pearce 	 * Create a string to a UTF-8 temporary file and return the path.
52649aac325SShawn O. Pearce 	 *
52749aac325SShawn O. Pearce 	 * @param body
52849aac325SShawn O. Pearce 	 *            complete content to write to the file. If the file should end
52949aac325SShawn O. Pearce 	 *            with a trailing LF, the string should end with an LF.
53049aac325SShawn O. Pearce 	 * @return path of the temporary file created within the trash area.
53149aac325SShawn O. Pearce 	 * @throws IOException
53249aac325SShawn O. Pearce 	 *             the file could not be written.
53349aac325SShawn O. Pearce 	 */
write(String body)5346d370d83SHan-Wen Nienhuys 	protected File write(String body) throws IOException {
53536144e12SShawn Pearce 		final File f = File.createTempFile("temp", "txt", tmp);
53649aac325SShawn O. Pearce 		try {
53749aac325SShawn O. Pearce 			write(f, body);
53849aac325SShawn O. Pearce 			return f;
539c0268f89SCarsten Hammer 		} catch (Error | RuntimeException | IOException e) {
54049aac325SShawn O. Pearce 			f.delete();
54149aac325SShawn O. Pearce 			throw e;
54249aac325SShawn O. Pearce 		}
54349aac325SShawn O. Pearce 	}
54449aac325SShawn O. Pearce 
54549aac325SShawn O. Pearce 	/**
54649aac325SShawn O. Pearce 	 * Write a string as a UTF-8 file.
54749aac325SShawn O. Pearce 	 *
54849aac325SShawn O. Pearce 	 * @param f
54949aac325SShawn O. Pearce 	 *            file to write the string to. Caller is responsible for making
55049aac325SShawn O. Pearce 	 *            sure it is in the trash directory or will otherwise be cleaned
55149aac325SShawn O. Pearce 	 *            up at the end of the test. If the parent directory does not
55249aac325SShawn O. Pearce 	 *            exist, the missing parent directories are automatically
55349aac325SShawn O. Pearce 	 *            created.
55449aac325SShawn O. Pearce 	 * @param body
55549aac325SShawn O. Pearce 	 *            content to write to the file.
55649aac325SShawn O. Pearce 	 * @throws IOException
55749aac325SShawn O. Pearce 	 *             the file could not be written.
55849aac325SShawn O. Pearce 	 */
write(File f, String body)5596d370d83SHan-Wen Nienhuys 	protected void write(File f, String body) throws IOException {
560b649eaa9SDariusz Luksza 		JGitTestUtil.write(f, body);
56149aac325SShawn O. Pearce 	}
56249aac325SShawn O. Pearce 
563a90b75b4SMatthias Sohn 	/**
564a90b75b4SMatthias Sohn 	 * Read a file's content
565a90b75b4SMatthias Sohn 	 *
566a90b75b4SMatthias Sohn 	 * @param f
567a90b75b4SMatthias Sohn 	 *            the file
568a90b75b4SMatthias Sohn 	 * @return the content of the file
569a90b75b4SMatthias Sohn 	 * @throws IOException
570a90b75b4SMatthias Sohn 	 */
read(File f)5716d370d83SHan-Wen Nienhuys 	protected String read(File f) throws IOException {
5729ea38173SRobin Stocker 		return JGitTestUtil.read(f);
57349aac325SShawn O. Pearce 	}
57449aac325SShawn O. Pearce 
toEnvArray(Map<String, String> env)5756d370d83SHan-Wen Nienhuys 	private static String[] toEnvArray(Map<String, String> env) {
57649aac325SShawn O. Pearce 		final String[] envp = new String[env.size()];
57749aac325SShawn O. Pearce 		int i = 0;
5782062d623SAdrian Goerler 		for (Map.Entry<String, String> e : env.entrySet())
57949aac325SShawn O. Pearce 			envp[i++] = e.getKey() + "=" + e.getValue();
58049aac325SShawn O. Pearce 		return envp;
58149aac325SShawn O. Pearce 	}
58249aac325SShawn O. Pearce 
cloneEnv()58349aac325SShawn O. Pearce 	private static HashMap<String, String> cloneEnv() {
5843b444863SDavid Pursehouse 		return new HashMap<>(System.getenv());
58549aac325SShawn O. Pearce 	}
58649aac325SShawn O. Pearce 
58736144e12SShawn Pearce 	private static final class CleanupThread extends Thread {
58836144e12SShawn Pearce 		private static final CleanupThread me;
58936144e12SShawn Pearce 		static {
59036144e12SShawn Pearce 			me = new CleanupThread();
59136144e12SShawn Pearce 			Runtime.getRuntime().addShutdownHook(me);
59249aac325SShawn O. Pearce 		}
593ad74bbf9SAdrian Goerler 
deleteOnShutdown(File tmp)59436144e12SShawn Pearce 		static void deleteOnShutdown(File tmp) {
59536144e12SShawn Pearce 			synchronized (me) {
59636144e12SShawn Pearce 				me.toDelete.add(tmp);
59736144e12SShawn Pearce 			}
59836144e12SShawn Pearce 		}
59936144e12SShawn Pearce 
removed(File tmp)60036144e12SShawn Pearce 		static void removed(File tmp) {
60136144e12SShawn Pearce 			synchronized (me) {
60236144e12SShawn Pearce 				me.toDelete.remove(tmp);
60336144e12SShawn Pearce 			}
60436144e12SShawn Pearce 		}
60536144e12SShawn Pearce 
6063b444863SDavid Pursehouse 		private final List<File> toDelete = new ArrayList<>();
60736144e12SShawn Pearce 
60836144e12SShawn Pearce 		@Override
run()60936144e12SShawn Pearce 		public void run() {
61036144e12SShawn Pearce 			// On windows accidentally open files or memory
61136144e12SShawn Pearce 			// mapped regions may prevent files from being deleted.
61236144e12SShawn Pearce 			// Suggesting a GC increases the likelihood that our
61336144e12SShawn Pearce 			// test repositories actually get removed after the
61436144e12SShawn Pearce 			// tests, even in the case of failure.
61536144e12SShawn Pearce 			System.gc();
61636144e12SShawn Pearce 			synchronized (this) {
61736144e12SShawn Pearce 				boolean silent = false;
61836144e12SShawn Pearce 				boolean failOnError = false;
61936144e12SShawn Pearce 				for (File tmp : toDelete)
62036144e12SShawn Pearce 					recursiveDelete(tmp, silent, failOnError);
62136144e12SShawn Pearce 			}
62236144e12SShawn Pearce 		}
62336144e12SShawn Pearce 	}
62449aac325SShawn O. Pearce }
625