| /* |
| * 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 others |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Distribution License v. 1.0 which is available at |
| * https://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: BSD-3-Clause |
| */ |
| |
| package org.eclipse.jgit.junit; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| 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.nio.file.Path; |
| import java.time.Instant; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.TimeUnit; |
| |
| 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.AnyObjectId; |
| 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.Ref; |
| 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.After; |
| 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 { |
| /** |
| * Copy a file |
| * |
| * @param src |
| * @param dst |
| * @throws IOException |
| */ |
| protected static void copyFile(File src, File dst) |
| throws IOException { |
| try (FileInputStream fis = new FileInputStream(src); |
| FileOutputStream fos = new FileOutputStream(dst)) { |
| final byte[] buf = new byte[4096]; |
| int r; |
| while ((r = fis.read(buf)) > 0) { |
| fos.write(buf, 0, r); |
| } |
| } |
| } |
| |
| /** |
| * Write a trash file |
| * |
| * @param name |
| * @param data |
| * @return the trash file |
| * @throws IOException |
| */ |
| protected File writeTrashFile(String name, String data) |
| throws IOException { |
| return JGitTestUtil.writeTrashFile(db, name, data); |
| } |
| |
| /** |
| * Create a symbolic link |
| * |
| * @param link |
| * the path of the symbolic link to create |
| * @param target |
| * the target of the symbolic link |
| * @return the path to the symbolic link |
| * @throws Exception |
| * @since 4.2 |
| */ |
| protected Path writeLink(String link, String target) |
| throws Exception { |
| return JGitTestUtil.writeLink(db, link, target); |
| } |
| |
| /** |
| * Write a trash file |
| * |
| * @param subdir |
| * @param name |
| * @param data |
| * @return the trash file |
| * @throws IOException |
| */ |
| protected File writeTrashFile(final String subdir, final String name, |
| final String data) |
| throws IOException { |
| return JGitTestUtil.writeTrashFile(db, subdir, name, data); |
| } |
| |
| /** |
| * Read content of a file |
| * |
| * @param name |
| * @return the file's content |
| * @throws IOException |
| */ |
| protected String read(String name) throws IOException { |
| return JGitTestUtil.read(db, name); |
| } |
| |
| /** |
| * Check if file exists |
| * |
| * @param name |
| * file name |
| * @return if the file exists |
| */ |
| protected boolean check(String name) { |
| return JGitTestUtil.check(db, name); |
| } |
| |
| /** |
| * Delete a trash file |
| * |
| * @param name |
| * file name |
| * @throws IOException |
| */ |
| protected void deleteTrashFile(String name) throws IOException { |
| JGitTestUtil.deleteTrashFile(db, name); |
| } |
| |
| /** |
| * Check content of a file. |
| * |
| * @param f |
| * @param checkData |
| * expected content |
| * @throws IOException |
| */ |
| protected static void checkFile(File f, String checkData) |
| throws IOException { |
| try (Reader r = new InputStreamReader(new FileInputStream(f), |
| UTF_8)) { |
| if (checkData.length() > 0) { |
| char[] data = new char[checkData.length()]; |
| assertEquals(data.length, r.read(data)); |
| assertEquals(checkData, new String(data)); |
| } |
| assertEquals(-1, r.read()); |
| } |
| } |
| |
| /** Test repository, initialized for this test case. */ |
| protected FileRepository db; |
| |
| /** Working directory of {@link #db}. */ |
| protected File trash; |
| |
| /** {@inheritDoc} */ |
| @Override |
| @Before |
| public void setUp() throws Exception { |
| super.setUp(); |
| db = createWorkRepository(); |
| trash = db.getWorkTree(); |
| } |
| |
| @Override |
| @After |
| public void tearDown() throws Exception { |
| db.close(); |
| super.tearDown(); |
| } |
| |
| /** |
| * 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 org.eclipse.jgit.treewalk.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.getEntryLastModifiedInstant()); |
| dce.setLength((int) len); |
| try (FileInputStream in = new FileInputStream( |
| treeItr.getEntryFile())) { |
| dce.setObjectId( |
| inserter.insert(Constants.OBJ_BLOB, len, in)); |
| } |
| 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 lookupTable |
| * a table storing object-name mappings. |
| * @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 |
| * @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; |
| } |
| |
| /** |
| * Replaces '\' by '/' |
| * |
| * @param str |
| * the string in which backslashes should be replaced |
| * @return the resulting string with slashes |
| * @since 4.2 |
| */ |
| public static String slashify(String str) { |
| str = str.replace('\\', '/'); |
| return str; |
| } |
| |
| /** |
| * 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. The |
| * temporary file will be created as a sibling of lastFile. |
| * |
| * @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 Instant fsTick(File lastFile) |
| throws InterruptedException, |
| IOException { |
| File tmp; |
| FS fs = FS.DETECTED; |
| if (lastFile == null) { |
| lastFile = tmp = File |
| .createTempFile("fsTickTmpFile", null); |
| } else { |
| if (!fs.exists(lastFile)) { |
| throw new FileNotFoundException(lastFile.getPath()); |
| } |
| tmp = File.createTempFile("fsTickTmpFile", null, |
| lastFile.getParentFile()); |
| } |
| long res = FS.getFileStoreAttributes(tmp.toPath()) |
| .getFsTimestampResolution().toNanos(); |
| long sleepTime = res / 10; |
| try { |
| Instant startTime = fs.lastModifiedInstant(lastFile); |
| Instant actTime = fs.lastModifiedInstant(tmp); |
| while (actTime.compareTo(startTime) <= 0) { |
| TimeUnit.NANOSECONDS.sleep(sleepTime); |
| FileUtils.touch(tmp.toPath()); |
| actTime = fs.lastModifiedInstant(tmp); |
| } |
| return actTime; |
| } finally { |
| FileUtils.delete(tmp); |
| } |
| } |
| |
| /** |
| * Create a branch |
| * |
| * @param objectId |
| * @param branchName |
| * @throws IOException |
| */ |
| protected void createBranch(ObjectId objectId, String branchName) |
| throws IOException { |
| RefUpdate updateRef = db.updateRef(branchName); |
| updateRef.setNewObjectId(objectId); |
| updateRef.update(); |
| } |
| |
| /** |
| * Get all Refs |
| * |
| * @return list of refs |
| * @throws IOException |
| */ |
| public List<Ref> getRefs() throws IOException { |
| return db.getRefDatabase().getRefs(); |
| } |
| |
| /** |
| * Checkout a branch |
| * |
| * @param branchName |
| * @throws IllegalStateException |
| * @throws IOException |
| */ |
| 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.findRef(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 | GitAPIException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| /** |
| * Create <code>DirCacheEntry</code> |
| * |
| * @param path |
| * @param mode |
| * @return the DirCacheEntry |
| */ |
| protected DirCacheEntry createEntry(String path, FileMode mode) { |
| return createEntry(path, mode, DirCacheEntry.STAGE_0, path); |
| } |
| |
| /** |
| * Create <code>DirCacheEntry</code> |
| * |
| * @param path |
| * @param mode |
| * @param content |
| * @return the DirCacheEntry |
| */ |
| protected DirCacheEntry createEntry(final String path, final FileMode mode, |
| final String content) { |
| return createEntry(path, mode, DirCacheEntry.STAGE_0, content); |
| } |
| |
| /** |
| * Create <code>DirCacheEntry</code> |
| * |
| * @param path |
| * @param mode |
| * @param stage |
| * @param content |
| * @return the DirCacheEntry |
| */ |
| 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); |
| try (ObjectInserter.Formatter formatter = new ObjectInserter.Formatter()) { |
| entry.setObjectId(formatter.idFor( |
| Constants.OBJ_BLOB, Constants.encode(content))); |
| } |
| return entry; |
| } |
| |
| /** |
| * Create <code>DirCacheEntry</code> |
| * |
| * @param path |
| * @param objectId |
| * @return the DirCacheEntry |
| */ |
| protected DirCacheEntry createGitLink(String path, AnyObjectId objectId) { |
| final DirCacheEntry entry = new DirCacheEntry(path, |
| DirCacheEntry.STAGE_0); |
| entry.setFileMode(FileMode.GITLINK); |
| entry.setObjectId(objectId); |
| return entry; |
| } |
| |
| /** |
| * Assert files are equal |
| * |
| * @param expected |
| * @param actual |
| * @throws IOException |
| */ |
| public static void assertEqualsFile(File expected, File actual) |
| throws IOException { |
| assertEquals(expected.getCanonicalFile(), actual.getCanonicalFile()); |
| } |
| } |