blob: 59504aa7802069c1c629c1ee15d05e8575819a3b [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.Commit;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.LockFile;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectChecker;
import org.eclipse.jgit.lib.ObjectDatabase;
import org.eclipse.jgit.lib.ObjectDirectory;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectWriter;
import org.eclipse.jgit.lib.PackFile;
import org.eclipse.jgit.lib.PackWriter;
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.Tag;
import org.eclipse.jgit.lib.PackIndex.MutableEntry;
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.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
/** Wrapper to make creating test data easier. */
public class TestRepository {
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 Repository db;
private final RevWalk pool;
private final ObjectWriter writer;
private long now;
/**
* Wrap a repository with test building tools.
*
* @param db
* the test repository to write into.
* @throws Exception
*/
public TestRepository(Repository db) throws Exception {
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 Exception
*/
public TestRepository(Repository db, RevWalk rw) throws Exception {
this.db = db;
this.pool = rw;
this.writer = new ObjectWriter(db);
this.now = 1236977987000L;
}
/** @return the repository this helper class operates against. */
public Repository 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;
}
/**
* 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 {
return pool.lookupBlob(writer.writeBlob(content));
}
/**
* 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();
return pool.lookupTree(dc.writeTree(writer));
}
/**
* 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(db);
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 Commit c = new Commit(db);
c.setTreeId(tree);
c.setParentIds(parents);
c.setAuthor(new PersonIdent(author, new Date(now)));
c.setCommitter(new PersonIdent(committer, new Date(now)));
c.setMessage("");
return pool.lookupCommit(writer.writeCommit(c));
}
/** @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 Tag t = new Tag(db);
t.setType(Constants.typeString(dst.getType()));
t.setObjId(dst.toObjectId());
t.setTag(name);
t.setTagger(new PersonIdent(committer, new Date(now)));
t.setMessage("");
return (RevTag) pool.lookupAny(writer.writeTag(t), 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 {
final ObjectDatabase odb = db.getObjectDatabase();
if (odb instanceof ObjectDirectory) {
RefWriter rw = new RefWriter(db.getAllRefs().values()) {
@Override
protected void writeFile(final String name, final byte[] bin)
throws IOException {
TestRepository.this.writeFile(name, bin);
}
};
rw.writePackedRefs();
rw.writeInfoRefs();
final StringBuilder w = new StringBuilder();
for (PackFile p : ((ObjectDirectory) odb).getPacks()) {
w.append("P ");
w.append(p.getPackFile().getName());
w.append('\n');
}
writeFile("objects/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.openObject(o).getCachedBytes();
oc.checkCommit(bin);
assertHash(o, bin);
}
for (;;) {
final RevObject o = ow.nextObject();
if (o == null)
break;
final byte[] bin = db.openObject(o).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.copy(), 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 {
final ObjectDirectory odb = (ObjectDirectory) db.getObjectDatabase();
final PackWriter pw = new PackWriter(db, NullProgressMonitor.INSTANCE);
Set<ObjectId> all = new HashSet<ObjectId>();
for (Ref r : db.getAllRefs().values())
all.add(r.getObjectId());
pw.preparePack(all, Collections.<ObjectId> emptySet());
final ObjectId name = pw.computeName();
OutputStream out;
final File pack = nameFor(odb, name, ".pack");
out = new BufferedOutputStream(new FileOutputStream(pack));
try {
pw.writePack(out);
} finally {
out.close();
}
pack.setReadOnly();
final File idx = nameFor(odb, name, ".idx");
out = new BufferedOutputStream(new FileOutputStream(idx));
try {
pw.writeIndex(out);
} finally {
out.close();
}
idx.setReadOnly();
odb.openPack(pack, idx);
updateServerInfo();
prunePacked(odb);
}
private void prunePacked(ObjectDirectory odb) {
for (PackFile p : odb.getPacks()) {
for (MutableEntry e : p)
odb.fileFor(e.toObjectId()).delete();
}
}
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 String name, final byte[] bin)
throws IOException, ObjectWritingException {
final File p = new File(db.getDirectory(), name);
final LockFile lck = new LockFile(p);
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, db, 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 Commit c = new Commit(db);
c.setTreeId(pool.lookupTree(tree.writeTree(writer)));
c.setParentIds(parents.toArray(new RevCommit[parents.size()]));
c.setAuthor(new PersonIdent(author, new Date(now)));
c.setCommitter(new PersonIdent(committer, new Date(now)));
c.setMessage(message);
self = pool.lookupCommit(writer.writeCommit(c));
if (branch != null)
branch.update(self);
}
return self;
}
public CommitBuilder child() throws Exception {
return new CommitBuilder(this);
}
}
}