blob: 71446a0dceb10e4b6a6d385925532043ea7e35fc [file] [log] [blame]
/*
* Copyright (C) 2009-2010, Google Inc.
* 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 java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import junit.framework.Assert;
import junit.framework.AssertionFailedError;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
import org.eclipse.jgit.dircache.DirCacheEditor.DeleteTree;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.ObjectWritingException;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectChecker;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefWriter;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TagBuilder;
import org.eclipse.jgit.revwalk.ObjectWalk;
import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepository;
import org.eclipse.jgit.storage.file.LockFile;
import org.eclipse.jgit.storage.file.ObjectDirectory;
import org.eclipse.jgit.storage.file.PackFile;
import org.eclipse.jgit.storage.file.PackIndex.MutableEntry;
import org.eclipse.jgit.storage.pack.PackWriter;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.util.FileUtils;
/**
* Wrapper to make creating test data easier.
*
* @param <R>
* type of Repository the test data is stored on.
*/
public class TestRepository<R extends Repository> {
private static final PersonIdent author;
private static final PersonIdent committer;
static {
final MockSystemReader m = new MockSystemReader();
final long now = m.getCurrentTime();
final int tz = m.getTimezone(now);
final String an = "J. Author";
final String ae = "jauthor@example.com";
author = new PersonIdent(an, ae, now, tz);
final String cn = "J. Committer";
final String ce = "jcommitter@example.com";
committer = new PersonIdent(cn, ce, now, tz);
}
private final R db;
private final RevWalk pool;
private final ObjectInserter inserter;
private long now;
/**
* Wrap a repository with test building tools.
*
* @param db
* the test repository to write into.
* @throws IOException
*/
public TestRepository(R db) throws IOException {
this(db, new RevWalk(db));
}
/**
* Wrap a repository with test building tools.
*
* @param db
* the test repository to write into.
* @param rw
* the RevObject pool to use for object lookup.
* @throws IOException
*/
public TestRepository(R db, RevWalk rw) throws IOException {
this.db = db;
this.pool = rw;
this.inserter = db.newObjectInserter();
this.now = 1236977987000L;
}
/** @return the repository this helper class operates against. */
public R getRepository() {
return db;
}
/** @return get the RevWalk pool all objects are allocated through. */
public RevWalk getRevWalk() {
return pool;
}
/** @return current time adjusted by {@link #tick(int)}. */
public Date getClock() {
return new Date(now);
}
/**
* Adjust the current time that will used by the next commit.
*
* @param secDelta
* number of seconds to add to the current time.
*/
public void tick(final int secDelta) {
now += secDelta * 1000L;
}
/**
* Set the author and committer using {@link #getClock()}.
*
* @param c
* the commit builder to store.
*/
public void setAuthorAndCommitter(org.eclipse.jgit.lib.CommitBuilder c) {
c.setAuthor(new PersonIdent(author, new Date(now)));
c.setCommitter(new PersonIdent(committer, new Date(now)));
}
/**
* Create a new blob object in the repository.
*
* @param content
* file content, will be UTF-8 encoded.
* @return reference to the blob.
* @throws Exception
*/
public RevBlob blob(final String content) throws Exception {
return blob(content.getBytes("UTF-8"));
}
/**
* Create a new blob object in the repository.
*
* @param content
* binary file content.
* @return reference to the blob.
* @throws Exception
*/
public RevBlob blob(final byte[] content) throws Exception {
ObjectId id;
try {
id = inserter.insert(Constants.OBJ_BLOB, content);
inserter.flush();
} finally {
inserter.release();
}
return pool.lookupBlob(id);
}
/**
* Construct a regular file mode tree entry.
*
* @param path
* path of the file.
* @param blob
* a blob, previously constructed in the repository.
* @return the entry.
* @throws Exception
*/
public DirCacheEntry file(final String path, final RevBlob blob)
throws Exception {
final DirCacheEntry e = new DirCacheEntry(path);
e.setFileMode(FileMode.REGULAR_FILE);
e.setObjectId(blob);
return e;
}
/**
* Construct a tree from a specific listing of file entries.
*
* @param entries
* the files to include in the tree. The collection does not need
* to be sorted properly and may be empty.
* @return reference to the tree specified by the entry list.
* @throws Exception
*/
public RevTree tree(final DirCacheEntry... entries) throws Exception {
final DirCache dc = DirCache.newInCore();
final DirCacheBuilder b = dc.builder();
for (final DirCacheEntry e : entries)
b.add(e);
b.finish();
ObjectId root;
try {
root = dc.writeTree(inserter);
inserter.flush();
} finally {
inserter.release();
}
return pool.lookupTree(root);
}
/**
* Lookup an entry stored in a tree, failing if not present.
*
* @param tree
* the tree to search.
* @param path
* the path to find the entry of.
* @return the parsed object entry at this path, never null.
* @throws AssertionFailedError
* if the path does not exist in the given tree.
* @throws Exception
*/
public RevObject get(final RevTree tree, final String path)
throws AssertionFailedError, Exception {
final TreeWalk tw = new TreeWalk(pool.getObjectReader());
tw.setFilter(PathFilterGroup.createFromStrings(Collections
.singleton(path)));
tw.reset(tree);
while (tw.next()) {
if (tw.isSubtree() && !path.equals(tw.getPathString())) {
tw.enterSubtree();
continue;
}
final ObjectId entid = tw.getObjectId(0);
final FileMode entmode = tw.getFileMode(0);
return pool.lookupAny(entid, entmode.getObjectType());
}
Assert.fail("Can't find " + path + " in tree " + tree.name());
return null; // never reached.
}
/**
* Create a new commit.
* <p>
* See {@link #commit(int, RevTree, RevCommit...)}. The tree is the empty
* tree (no files or subdirectories).
*
* @param parents
* zero or more parents of the commit.
* @return the new commit.
* @throws Exception
*/
public RevCommit commit(final RevCommit... parents) throws Exception {
return commit(1, tree(), parents);
}
/**
* Create a new commit.
* <p>
* See {@link #commit(int, RevTree, RevCommit...)}.
*
* @param tree
* the root tree for the commit.
* @param parents
* zero or more parents of the commit.
* @return the new commit.
* @throws Exception
*/
public RevCommit commit(final RevTree tree, final RevCommit... parents)
throws Exception {
return commit(1, tree, parents);
}
/**
* Create a new commit.
* <p>
* See {@link #commit(int, RevTree, RevCommit...)}. The tree is the empty
* tree (no files or subdirectories).
*
* @param secDelta
* number of seconds to advance {@link #tick(int)} by.
* @param parents
* zero or more parents of the commit.
* @return the new commit.
* @throws Exception
*/
public RevCommit commit(final int secDelta, final RevCommit... parents)
throws Exception {
return commit(secDelta, tree(), parents);
}
/**
* Create a new commit.
* <p>
* The author and committer identities are stored using the current
* timestamp, after being incremented by {@code secDelta}. The message body
* is empty.
*
* @param secDelta
* number of seconds to advance {@link #tick(int)} by.
* @param tree
* the root tree for the commit.
* @param parents
* zero or more parents of the commit.
* @return the new commit.
* @throws Exception
*/
public RevCommit commit(final int secDelta, final RevTree tree,
final RevCommit... parents) throws Exception {
tick(secDelta);
final org.eclipse.jgit.lib.CommitBuilder c;
c = new org.eclipse.jgit.lib.CommitBuilder();
c.setTreeId(tree);
c.setParentIds(parents);
c.setAuthor(new PersonIdent(author, new Date(now)));
c.setCommitter(new PersonIdent(committer, new Date(now)));
c.setMessage("");
ObjectId id;
try {
id = inserter.insert(c);
inserter.flush();
} finally {
inserter.release();
}
return pool.lookupCommit(id);
}
/** @return a new commit builder. */
public CommitBuilder commit() {
return new CommitBuilder();
}
/**
* Construct an annotated tag object pointing at another object.
* <p>
* The tagger is the committer identity, at the current time as specified by
* {@link #tick(int)}. The time is not increased.
* <p>
* The tag message is empty.
*
* @param name
* name of the tag. Traditionally a tag name should not start
* with {@code refs/tags/}.
* @param dst
* object the tag should be pointed at.
* @return the annotated tag object.
* @throws Exception
*/
public RevTag tag(final String name, final RevObject dst) throws Exception {
final TagBuilder t = new TagBuilder();
t.setObjectId(dst);
t.setTag(name);
t.setTagger(new PersonIdent(committer, new Date(now)));
t.setMessage("");
ObjectId id;
try {
id = inserter.insert(t);
inserter.flush();
} finally {
inserter.release();
}
return (RevTag) pool.lookupAny(id, Constants.OBJ_TAG);
}
/**
* Update a reference to point to an object.
*
* @param ref
* the name of the reference to update to. If {@code ref} does
* not start with {@code refs/} and is not the magic names
* {@code HEAD} {@code FETCH_HEAD} or {@code MERGE_HEAD}, then
* {@code refs/heads/} will be prefixed in front of the given
* name, thereby assuming it is a branch.
* @param to
* the target object.
* @return the target object.
* @throws Exception
*/
public RevCommit update(String ref, CommitBuilder to) throws Exception {
return update(ref, to.create());
}
/**
* Update a reference to point to an object.
*
* @param <T>
* type of the target object.
* @param ref
* the name of the reference to update to. If {@code ref} does
* not start with {@code refs/} and is not the magic names
* {@code HEAD} {@code FETCH_HEAD} or {@code MERGE_HEAD}, then
* {@code refs/heads/} will be prefixed in front of the given
* name, thereby assuming it is a branch.
* @param obj
* the target object.
* @return the target object.
* @throws Exception
*/
public <T extends AnyObjectId> T update(String ref, T obj) throws Exception {
if (Constants.HEAD.equals(ref)) {
} else if ("FETCH_HEAD".equals(ref)) {
} else if ("MERGE_HEAD".equals(ref)) {
} else if (ref.startsWith(Constants.R_REFS)) {
} else
ref = Constants.R_HEADS + ref;
RefUpdate u = db.updateRef(ref);
u.setNewObjectId(obj);
switch (u.forceUpdate()) {
case FAST_FORWARD:
case FORCED:
case NEW:
case NO_CHANGE:
updateServerInfo();
return obj;
default:
throw new IOException("Cannot write " + ref + " " + u.getResult());
}
}
/**
* Update the dumb client server info files.
*
* @throws Exception
*/
public void updateServerInfo() throws Exception {
if (db instanceof FileRepository) {
final FileRepository fr = (FileRepository) db;
RefWriter rw = new RefWriter(fr.getAllRefs().values()) {
@Override
protected void writeFile(final String name, final byte[] bin)
throws IOException {
File path = new File(fr.getDirectory(), name);
TestRepository.this.writeFile(path, bin);
}
};
rw.writePackedRefs();
rw.writeInfoRefs();
final StringBuilder w = new StringBuilder();
for (PackFile p : fr.getObjectDatabase().getPacks()) {
w.append("P ");
w.append(p.getPackFile().getName());
w.append('\n');
}
writeFile(new File(new File(fr.getObjectDatabase().getDirectory(),
"info"), "packs"), Constants.encodeASCII(w.toString()));
}
}
/**
* Ensure the body of the given object has been parsed.
*
* @param <T>
* type of object, e.g. {@link RevTag} or {@link RevCommit}.
* @param object
* reference to the (possibly unparsed) object to force body
* parsing of.
* @return {@code object}
* @throws Exception
*/
public <T extends RevObject> T parseBody(final T object) throws Exception {
pool.parseBody(object);
return object;
}
/**
* Create a new branch builder for this repository.
*
* @param ref
* name of the branch to be constructed. If {@code ref} does not
* start with {@code refs/} the prefix {@code refs/heads/} will
* be added.
* @return builder for the named branch.
*/
public BranchBuilder branch(String ref) {
if (Constants.HEAD.equals(ref)) {
} else if (ref.startsWith(Constants.R_REFS)) {
} else
ref = Constants.R_HEADS + ref;
return new BranchBuilder(ref);
}
/**
* Run consistency checks against the object database.
* <p>
* This method completes silently if the checks pass. A temporary revision
* pool is constructed during the checking.
*
* @param tips
* the tips to start checking from; if not supplied the refs of
* the repository are used instead.
* @throws MissingObjectException
* @throws IncorrectObjectTypeException
* @throws IOException
*/
public void fsck(RevObject... tips) throws MissingObjectException,
IncorrectObjectTypeException, IOException {
ObjectWalk ow = new ObjectWalk(db);
if (tips.length != 0) {
for (RevObject o : tips)
ow.markStart(ow.parseAny(o));
} else {
for (Ref r : db.getAllRefs().values())
ow.markStart(ow.parseAny(r.getObjectId()));
}
ObjectChecker oc = new ObjectChecker();
for (;;) {
final RevCommit o = ow.next();
if (o == null)
break;
final byte[] bin = db.open(o, o.getType()).getCachedBytes();
oc.checkCommit(bin);
assertHash(o, bin);
}
for (;;) {
final RevObject o = ow.nextObject();
if (o == null)
break;
final byte[] bin = db.open(o, o.getType()).getCachedBytes();
oc.check(o.getType(), bin);
assertHash(o, bin);
}
}
private static void assertHash(RevObject id, byte[] bin) {
MessageDigest md = Constants.newMessageDigest();
md.update(Constants.encodedTypeString(id.getType()));
md.update((byte) ' ');
md.update(Constants.encodeASCII(bin.length));
md.update((byte) 0);
md.update(bin);
Assert.assertEquals(id, ObjectId.fromRaw(md.digest()));
}
/**
* Pack all reachable objects in the repository into a single pack file.
* <p>
* All loose objects are automatically pruned. Existing packs however are
* not removed.
*
* @throws Exception
*/
public void packAndPrune() throws Exception {
if (db.getObjectDatabase() instanceof ObjectDirectory) {
ObjectDirectory odb = (ObjectDirectory) db.getObjectDatabase();
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
final File pack, idx;
PackWriter pw = new PackWriter(db);
try {
Set<ObjectId> all = new HashSet<ObjectId>();
for (Ref r : db.getAllRefs().values())
all.add(r.getObjectId());
pw.preparePack(m, all, Collections.<ObjectId> emptySet());
final ObjectId name = pw.computeName();
OutputStream out;
pack = nameFor(odb, name, ".pack");
out = new BufferedOutputStream(new FileOutputStream(pack));
try {
pw.writePack(m, m, out);
} finally {
out.close();
}
pack.setReadOnly();
idx = nameFor(odb, name, ".idx");
out = new BufferedOutputStream(new FileOutputStream(idx));
try {
pw.writeIndex(out);
} finally {
out.close();
}
idx.setReadOnly();
} finally {
pw.release();
}
odb.openPack(pack, idx);
updateServerInfo();
prunePacked(odb);
}
}
private void prunePacked(ObjectDirectory odb) throws IOException {
for (PackFile p : odb.getPacks()) {
for (MutableEntry e : p)
FileUtils.delete(odb.fileFor(e.toObjectId()));
}
}
private static File nameFor(ObjectDirectory odb, ObjectId name, String t) {
File packdir = new File(odb.getDirectory(), "pack");
return new File(packdir, "pack-" + name.name() + t);
}
private void writeFile(final File p, final byte[] bin) throws IOException,
ObjectWritingException {
final LockFile lck = new LockFile(p, db.getFS());
if (!lck.lock())
throw new ObjectWritingException("Can't write " + p);
try {
lck.write(bin);
} catch (IOException ioe) {
throw new ObjectWritingException("Can't write " + p);
}
if (!lck.commit())
throw new ObjectWritingException("Can't write " + p);
}
/** Helper to build a branch with one or more commits */
public class BranchBuilder {
private final String ref;
BranchBuilder(final String ref) {
this.ref = ref;
}
/**
* @return construct a new commit builder that updates this branch. If
* the branch already exists, the commit builder will have its
* first parent as the current commit and its tree will be
* initialized to the current files.
* @throws Exception
* the commit builder can't read the current branch state
*/
public CommitBuilder commit() throws Exception {
return new CommitBuilder(this);
}
/**
* Forcefully update this branch to a particular commit.
*
* @param to
* the commit to update to.
* @return {@code to}.
* @throws Exception
*/
public RevCommit update(CommitBuilder to) throws Exception {
return update(to.create());
}
/**
* Forcefully update this branch to a particular commit.
*
* @param to
* the commit to update to.
* @return {@code to}.
* @throws Exception
*/
public RevCommit update(RevCommit to) throws Exception {
return TestRepository.this.update(ref, to);
}
}
/** Helper to generate a commit. */
public class CommitBuilder {
private final BranchBuilder branch;
private final DirCache tree = DirCache.newInCore();
private final List<RevCommit> parents = new ArrayList<RevCommit>(2);
private int tick = 1;
private String message = "";
private RevCommit self;
CommitBuilder() {
branch = null;
}
CommitBuilder(BranchBuilder b) throws Exception {
branch = b;
Ref ref = db.getRef(branch.ref);
if (ref != null) {
parent(pool.parseCommit(ref.getObjectId()));
}
}
CommitBuilder(CommitBuilder prior) throws Exception {
branch = prior.branch;
DirCacheBuilder b = tree.builder();
for (int i = 0; i < prior.tree.getEntryCount(); i++)
b.add(prior.tree.getEntry(i));
b.finish();
parents.add(prior.create());
}
public CommitBuilder parent(RevCommit p) throws Exception {
if (parents.isEmpty()) {
DirCacheBuilder b = tree.builder();
parseBody(p);
b.addTree(new byte[0], DirCacheEntry.STAGE_0, pool
.getObjectReader(), p.getTree());
b.finish();
}
parents.add(p);
return this;
}
public CommitBuilder noParents() {
parents.clear();
return this;
}
public CommitBuilder noFiles() {
tree.clear();
return this;
}
public CommitBuilder add(String path, String content) throws Exception {
return add(path, blob(content));
}
public CommitBuilder add(String path, final RevBlob id)
throws Exception {
DirCacheEditor e = tree.editor();
e.add(new PathEdit(path) {
@Override
public void apply(DirCacheEntry ent) {
ent.setFileMode(FileMode.REGULAR_FILE);
ent.setObjectId(id);
}
});
e.finish();
return this;
}
public CommitBuilder rm(String path) {
DirCacheEditor e = tree.editor();
e.add(new DeletePath(path));
e.add(new DeleteTree(path));
e.finish();
return this;
}
public CommitBuilder message(String m) {
message = m;
return this;
}
public CommitBuilder tick(int secs) {
tick = secs;
return this;
}
public RevCommit create() throws Exception {
if (self == null) {
TestRepository.this.tick(tick);
final org.eclipse.jgit.lib.CommitBuilder c;
c = new org.eclipse.jgit.lib.CommitBuilder();
c.setParentIds(parents);
setAuthorAndCommitter(c);
c.setMessage(message);
ObjectId commitId;
try {
c.setTreeId(tree.writeTree(inserter));
commitId = inserter.insert(c);
inserter.flush();
} finally {
inserter.release();
}
self = pool.lookupCommit(commitId);
if (branch != null)
branch.update(self);
}
return self;
}
public CommitBuilder child() throws Exception {
return new CommitBuilder(this);
}
}
}