| /* |
| * Copyright (C) 2009, Google Inc. |
| * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> |
| * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> |
| * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.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.junit; |
| |
| import static org.junit.Assert.assertEquals; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.util.Map; |
| |
| import org.eclipse.jgit.api.Git; |
| import org.eclipse.jgit.api.errors.GitAPIException; |
| import org.eclipse.jgit.dircache.DirCacheBuilder; |
| import org.eclipse.jgit.dircache.DirCacheCheckout; |
| import org.eclipse.jgit.dircache.DirCacheEntry; |
| import org.eclipse.jgit.internal.storage.file.FileRepository; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.FileMode; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.RefUpdate; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.treewalk.FileTreeIterator; |
| import org.eclipse.jgit.util.FS; |
| import org.eclipse.jgit.util.FileUtils; |
| import org.junit.Before; |
| |
| /** |
| * Base class for most JGit unit tests. |
| * |
| * Sets up a predefined test repository and has support for creating additional |
| * repositories and destroying them when the tests are finished. |
| */ |
| public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { |
| protected static void copyFile(final File src, final File dst) |
| throws IOException { |
| final FileInputStream fis = new FileInputStream(src); |
| try { |
| final FileOutputStream fos = new FileOutputStream(dst); |
| try { |
| final byte[] buf = new byte[4096]; |
| int r; |
| while ((r = fis.read(buf)) > 0) { |
| fos.write(buf, 0, r); |
| } |
| } finally { |
| fos.close(); |
| } |
| } finally { |
| fis.close(); |
| } |
| } |
| |
| protected File writeTrashFile(final String name, final String data) |
| throws IOException { |
| return JGitTestUtil.writeTrashFile(db, name, data); |
| } |
| |
| protected File writeTrashFile(final String subdir, final String name, |
| final String data) |
| throws IOException { |
| return JGitTestUtil.writeTrashFile(db, subdir, name, data); |
| } |
| |
| protected String read(final String name) throws IOException { |
| return JGitTestUtil.read(db, name); |
| } |
| |
| protected boolean check(final String name) { |
| return JGitTestUtil.check(db, name); |
| } |
| |
| protected void deleteTrashFile(final String name) throws IOException { |
| JGitTestUtil.deleteTrashFile(db, name); |
| } |
| |
| protected static void checkFile(File f, final String checkData) |
| throws IOException { |
| Reader r = new InputStreamReader(new FileInputStream(f), "ISO-8859-1"); |
| try { |
| char[] data = new char[(int) f.length()]; |
| if (f.length() != r.read(data)) |
| throw new IOException("Internal error reading file data from "+f); |
| assertEquals(checkData, new String(data)); |
| } finally { |
| r.close(); |
| } |
| } |
| |
| /** Test repository, initialized for this test case. */ |
| protected FileRepository db; |
| |
| /** Working directory of {@link #db}. */ |
| protected File trash; |
| |
| @Override |
| @Before |
| public void setUp() throws Exception { |
| super.setUp(); |
| db = createWorkRepository(); |
| trash = db.getWorkTree(); |
| } |
| |
| /** |
| * Represent the state of the index in one String. This representation is |
| * useful when writing tests which do assertions on the state of the index. |
| * By default information about path, mode, stage (if different from 0) is |
| * included. A bitmask controls which additional info about |
| * modificationTimes, smudge state and length is included. |
| * <p> |
| * The format of the returned string is described with this BNF: |
| * |
| * <pre> |
| * result = ( "[" path mode stage? time? smudge? length? sha1? content? "]" )* . |
| * mode = ", mode:" number . |
| * stage = ", stage:" number . |
| * time = ", time:t" timestamp-index . |
| * smudge = "" | ", smudged" . |
| * length = ", length:" number . |
| * sha1 = ", sha1:" hex-sha1 . |
| * content = ", content:" blob-data . |
| * </pre> |
| * |
| * 'stage' is only presented when the stage is different from 0. All |
| * reported time stamps are mapped to strings like "t0", "t1", ... "tn". The |
| * smallest reported time-stamp will be called "t0". This allows to write |
| * assertions against the string although the concrete value of the time |
| * stamps is unknown. |
| * |
| * @param includedOptions |
| * a bitmask constructed out of the constants {@link #MOD_TIME}, |
| * {@link #SMUDGE}, {@link #LENGTH}, {@link #CONTENT_ID} and |
| * {@link #CONTENT} controlling which info is present in the |
| * resulting string. |
| * @return a string encoding the index state |
| * @throws IllegalStateException |
| * @throws IOException |
| */ |
| public String indexState(int includedOptions) |
| throws IllegalStateException, IOException { |
| return indexState(db, includedOptions); |
| } |
| |
| /** |
| * Resets the index to represent exactly some filesystem content. E.g. the |
| * following call will replace the index with the working tree content: |
| * <p> |
| * <code>resetIndex(new FileSystemIterator(db))</code> |
| * <p> |
| * This method can be used by testcases which first prepare a new commit |
| * somewhere in the filesystem (e.g. in the working-tree) and then want to |
| * have an index which matches their prepared content. |
| * |
| * @param treeItr |
| * a {@link FileTreeIterator} which determines which files should |
| * go into the new index |
| * @throws FileNotFoundException |
| * @throws IOException |
| */ |
| protected void resetIndex(FileTreeIterator treeItr) |
| throws FileNotFoundException, IOException { |
| try (ObjectInserter inserter = db.newObjectInserter()) { |
| DirCacheBuilder builder = db.lockDirCache().builder(); |
| DirCacheEntry dce; |
| |
| while (!treeItr.eof()) { |
| long len = treeItr.getEntryLength(); |
| |
| dce = new DirCacheEntry(treeItr.getEntryPathString()); |
| dce.setFileMode(treeItr.getEntryFileMode()); |
| dce.setLastModified(treeItr.getEntryLastModified()); |
| dce.setLength((int) len); |
| FileInputStream in = new FileInputStream( |
| treeItr.getEntryFile()); |
| dce.setObjectId(inserter.insert(Constants.OBJ_BLOB, len, in)); |
| in.close(); |
| builder.add(dce); |
| treeItr.next(1); |
| } |
| builder.commit(); |
| inserter.flush(); |
| } |
| } |
| |
| /** |
| * Helper method to map arbitrary objects to user-defined names. This can be |
| * used create short names for objects to produce small and stable debug |
| * output. It is guaranteed that when you lookup the same object multiple |
| * times even with different nameTemplates this method will always return |
| * the same name which was derived from the first nameTemplate. |
| * nameTemplates can contain "%n" which will be replaced by a running number |
| * before used as a name. |
| * |
| * @param l |
| * the object to lookup |
| * @param nameTemplate |
| * the name for that object. Can contain "%n" which will be |
| * replaced by a running number before used as a name. If the |
| * lookup table already contains the object this parameter will |
| * be ignored |
| * @param lookupTable |
| * a table storing object-name mappings. |
| * @return a name of that object. Is not guaranteed to be unique. Use |
| * nameTemplates containing "%n" to always have unique names |
| */ |
| public static String lookup(Object l, String nameTemplate, |
| Map<Object, String> lookupTable) { |
| String name = lookupTable.get(l); |
| if (name == null) { |
| name = nameTemplate.replaceAll("%n", |
| Integer.toString(lookupTable.size())); |
| lookupTable.put(l, name); |
| } |
| return name; |
| } |
| |
| /** |
| * Waits until it is guaranteed that a subsequent file modification has a |
| * younger modification timestamp than the modification timestamp of the |
| * given file. This is done by touching a temporary file, reading the |
| * lastmodified attribute and, if needed, sleeping. After sleeping this loop |
| * starts again until the filesystem timer has advanced enough. |
| * |
| * @param lastFile |
| * the file on which we want to wait until the filesystem timer |
| * has advanced more than the lastmodification timestamp of this |
| * file |
| * @return return the last measured value of the filesystem timer which is |
| * greater than then the lastmodification time of lastfile. |
| * @throws InterruptedException |
| * @throws IOException |
| */ |
| public static long fsTick(File lastFile) throws InterruptedException, |
| IOException { |
| long sleepTime = 64; |
| FS fs = FS.DETECTED; |
| if (lastFile != null && !fs.exists(lastFile)) |
| throw new FileNotFoundException(lastFile.getPath()); |
| File tmp = File.createTempFile("FileTreeIteratorWithTimeControl", null); |
| try { |
| long startTime = (lastFile == null) ? fs.lastModified(tmp) : fs |
| .lastModified(lastFile); |
| long actTime = fs.lastModified(tmp); |
| while (actTime <= startTime) { |
| Thread.sleep(sleepTime); |
| sleepTime *= 2; |
| FileOutputStream fos = new FileOutputStream(tmp); |
| fos.close(); |
| actTime = fs.lastModified(tmp); |
| } |
| return actTime; |
| } finally { |
| FileUtils.delete(tmp); |
| } |
| } |
| |
| protected void createBranch(ObjectId objectId, String branchName) |
| throws IOException { |
| RefUpdate updateRef = db.updateRef(branchName); |
| updateRef.setNewObjectId(objectId); |
| updateRef.update(); |
| } |
| |
| protected void checkoutBranch(String branchName) |
| throws IllegalStateException, IOException { |
| try (RevWalk walk = new RevWalk(db)) { |
| RevCommit head = walk.parseCommit(db.resolve(Constants.HEAD)); |
| RevCommit branch = walk.parseCommit(db.resolve(branchName)); |
| DirCacheCheckout dco = new DirCacheCheckout(db, |
| head.getTree().getId(), db.lockDirCache(), |
| branch.getTree().getId()); |
| dco.setFailOnConflict(true); |
| dco.checkout(); |
| } |
| // update the HEAD |
| RefUpdate refUpdate = db.updateRef(Constants.HEAD); |
| refUpdate.setRefLogMessage("checkout: moving to " + branchName, false); |
| refUpdate.link(branchName); |
| } |
| |
| /** |
| * Writes a number of files in the working tree. The first content specified |
| * will be written into a file named '0', the second into a file named "1" |
| * and so on. If <code>null</code> is specified as content then this file is |
| * skipped. |
| * |
| * @param ensureDistinctTimestamps |
| * if set to <code>true</code> then between two write operations |
| * this method will wait to ensure that the second file will get |
| * a different lastmodification timestamp than the first file. |
| * @param contents |
| * the contents which should be written into the files |
| * @return the File object associated to the last written file. |
| * @throws IOException |
| * @throws InterruptedException |
| */ |
| protected File writeTrashFiles(boolean ensureDistinctTimestamps, |
| String... contents) |
| throws IOException, InterruptedException { |
| File f = null; |
| for (int i = 0; i < contents.length; i++) |
| if (contents[i] != null) { |
| if (ensureDistinctTimestamps && (f != null)) |
| fsTick(f); |
| f = writeTrashFile(Integer.toString(i), contents[i]); |
| } |
| return f; |
| } |
| |
| /** |
| * Commit a file with the specified contents on the specified branch, |
| * creating the branch if it didn't exist before. |
| * <p> |
| * It switches back to the original branch after the commit if there was |
| * one. |
| * |
| * @param filename |
| * @param contents |
| * @param branch |
| * @return the created commit |
| */ |
| protected RevCommit commitFile(String filename, String contents, String branch) { |
| try { |
| Git git = new Git(db); |
| Repository repo = git.getRepository(); |
| String originalBranch = repo.getFullBranch(); |
| boolean empty = repo.resolve(Constants.HEAD) == null; |
| if (!empty) { |
| if (repo.getRef(branch) == null) |
| git.branchCreate().setName(branch).call(); |
| git.checkout().setName(branch).call(); |
| } |
| |
| writeTrashFile(filename, contents); |
| git.add().addFilepattern(filename).call(); |
| RevCommit commit = git.commit() |
| .setMessage(branch + ": " + filename).call(); |
| |
| if (originalBranch != null) |
| git.checkout().setName(originalBranch).call(); |
| else if (empty) |
| git.branchCreate().setName(branch).setStartPoint(commit).call(); |
| |
| return commit; |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } catch (GitAPIException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| protected DirCacheEntry createEntry(final String path, final FileMode mode) { |
| return createEntry(path, mode, DirCacheEntry.STAGE_0, path); |
| } |
| |
| protected DirCacheEntry createEntry(final String path, final FileMode mode, |
| final String content) { |
| return createEntry(path, mode, DirCacheEntry.STAGE_0, content); |
| } |
| |
| protected DirCacheEntry createEntry(final String path, final FileMode mode, |
| final int stage, final String content) { |
| final DirCacheEntry entry = new DirCacheEntry(path, stage); |
| entry.setFileMode(mode); |
| entry.setObjectId(new ObjectInserter.Formatter().idFor( |
| Constants.OBJ_BLOB, Constants.encode(content))); |
| return entry; |
| } |
| |
| public static void assertEqualsFile(File expected, File actual) |
| throws IOException { |
| assertEquals(expected.getCanonicalFile(), actual.getCanonicalFile()); |
| } |
| } |