blob: d1358ee2a5e3d275913abcd7ae2bb443a97ff58a [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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
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.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
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.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.ObjectWritingException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.internal.storage.file.LockFile;
import org.eclipse.jgit.internal.storage.file.ObjectDirectory;
import org.eclipse.jgit.internal.storage.file.PackFile;
import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
import org.eclipse.jgit.internal.storage.pack.PackWriter;
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.merge.MergeStrategy;
import org.eclipse.jgit.merge.ThreeWayMerger;
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;
import org.eclipse.jgit.util.ChangeIdUtil;
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> {
public static final String AUTHOR = "J. Author";
public static final String AUTHOR_EMAIL = "jauthor@example.com";
public static final String COMMITTER = "J. Committer";
public static final String COMMITTER_EMAIL = "jcommitter@example.com";
private final PersonIdent defaultAuthor;
private final PersonIdent defaultCommitter;
private final R db;
private final Git git;
private final RevWalk pool;
private final ObjectInserter inserter;
private final MockSystemReader mockSystemReader;
/**
* 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), new MockSystemReader());
}
/**
* 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, rw, new MockSystemReader());
}
/**
* 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.
* @param reader
* the MockSystemReader to use for clock and other system
* operations.
* @throws IOException
* @since 4.2
*/
public TestRepository(R db, RevWalk rw, MockSystemReader reader)
throws IOException {
this.db = db;
this.git = Git.wrap(db);
this.pool = rw;
this.inserter = db.newObjectInserter();
this.mockSystemReader = reader;
long now = mockSystemReader.getCurrentTime();
int tz = mockSystemReader.getTimezone(now);
defaultAuthor = new PersonIdent(AUTHOR, AUTHOR_EMAIL, now, tz);
defaultCommitter = new PersonIdent(COMMITTER, COMMITTER_EMAIL, now, tz);
}
/** @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 an API wrapper for the underlying repository. This wrapper does
* not allocate any new resources and need not be closed (but closing
* it is harmless). */
public Git git() {
return git;
}
/**
* @return current date.
* @since 4.2
*/
public Date getDate() {
return new Date(mockSystemReader.getCurrentTime());
}
/** @return timezone used for default identities. */
public TimeZone getTimeZone() {
return mockSystemReader.getTimeZone();
}
/**
* 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) {
mockSystemReader.tick(secDelta);
}
/**
* Set the author and committer using {@link #getDate()}.
*
* @param c
* the commit builder to store.
*/
public void setAuthorAndCommitter(org.eclipse.jgit.lib.CommitBuilder c) {
c.setAuthor(new PersonIdent(defaultAuthor, getDate()));
c.setCommitter(new PersonIdent(defaultCommitter, getDate()));
}
/**
* 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 (ObjectInserter ins = inserter) {
id = ins.insert(Constants.OBJ_BLOB, content);
ins.flush();
}
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 (ObjectInserter ins = inserter) {
root = dc.writeTree(ins);
ins.flush();
}
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 Exception
*/
public RevObject get(final RevTree tree, final String path)
throws Exception {
try (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());
}
}
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(defaultAuthor, getDate()));
c.setCommitter(new PersonIdent(defaultCommitter, getDate()));
c.setMessage("");
ObjectId id;
try (ObjectInserter ins = inserter) {
id = ins.insert(c);
ins.flush();
}
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(defaultCommitter, getDate()));
t.setMessage("");
ObjectId id;
try (ObjectInserter ins = inserter) {
id = ins.insert(t);
ins.flush();
}
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());
}
/**
* Amend an existing ref.
*
* @param ref
* the name of the reference to amend, which must already exist.
* 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.
* @return commit builder that amends the branch on commit.
* @throws Exception
*/
public CommitBuilder amendRef(String ref) throws Exception {
String name = normalizeRef(ref);
Ref r = db.exactRef(name);
if (r == null)
throw new IOException("Not a ref: " + ref);
return amend(pool.parseCommit(r.getObjectId()), branch(name).commit());
}
/**
* Amend an existing commit.
*
* @param id
* the id of the commit to amend.
* @return commit builder.
* @throws Exception
*/
public CommitBuilder amend(AnyObjectId id) throws Exception {
return amend(pool.parseCommit(id), commit());
}
private CommitBuilder amend(RevCommit old, CommitBuilder b) throws Exception {
pool.parseBody(old);
b.author(old.getAuthorIdent());
b.committer(old.getCommitterIdent());
b.message(old.getFullMessage());
// Use the committer name from the old commit, but update it after ticking
// the clock in CommitBuilder#create().
b.updateCommitterTime = true;
// Reset parents to original parents.
b.noParents();
for (int i = 0; i < old.getParentCount(); i++)
b.parent(old.getParent(i));
// Reset tree to original tree; resetting parents reset tree contents to the
// first parent.
b.tree.clear();
try (TreeWalk tw = new TreeWalk(db)) {
tw.reset(old.getTree());
tw.setRecursive(true);
while (tw.next()) {
b.edit(new PathEdit(tw.getPathString()) {
@Override
public void apply(DirCacheEntry ent) {
ent.setFileMode(tw.getFileMode(0));
ent.setObjectId(tw.getObjectId(0));
}
});
}
}
return b;
}
/**
* 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 {
ref = normalizeRef(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());
}
}
/**
* Delete a reference.
*
* @param ref
* the name of the reference to delete. This is normalized
* in the same way as {@link #update(String, AnyObjectId)}.
* @throws Exception
* @since 4.4
*/
public void delete(String ref) throws Exception {
ref = normalizeRef(ref);
RefUpdate u = db.updateRef(ref);
switch (u.delete()) {
case FAST_FORWARD:
case FORCED:
case NEW:
case NO_CHANGE:
updateServerInfo();
return;
default:
throw new IOException("Cannot delete " + ref + " " + u.getResult());
}
}
private static String normalizeRef(String ref) {
if (Constants.HEAD.equals(ref)) {
// nothing
} else if ("FETCH_HEAD".equals(ref)) {
// nothing
} else if ("MERGE_HEAD".equals(ref)) {
// nothing
} else if (ref.startsWith(Constants.R_REFS)) {
// nothing
} else
ref = Constants.R_HEADS + ref;
return ref;
}
/**
* Soft-reset HEAD to a detached state.
* <p>
* @param id
* ID of detached head.
* @throws Exception
* @see #reset(String)
*/
public void reset(AnyObjectId id) throws Exception {
RefUpdate ru = db.updateRef(Constants.HEAD, true);
ru.setNewObjectId(id);
RefUpdate.Result result = ru.forceUpdate();
switch (result) {
case FAST_FORWARD:
case FORCED:
case NEW:
case NO_CHANGE:
break;
default:
throw new IOException(String.format(
"Checkout \"%s\" failed: %s", id.name(), result));
}
}
/**
* Soft-reset HEAD to a different commit.
* <p>
* This is equivalent to {@code git reset --soft} in that it modifies HEAD but
* not the index or the working tree of a non-bare repository.
*
* @param name
* revision string; either an existing ref name, or something that
* can be parsed to an object ID.
* @throws Exception
*/
public void reset(String name) throws Exception {
RefUpdate.Result result;
ObjectId id = db.resolve(name);
if (id == null)
throw new IOException("Not a revision: " + name);
RefUpdate ru = db.updateRef(Constants.HEAD, false);
ru.setNewObjectId(id);
result = ru.forceUpdate();
switch (result) {
case FAST_FORWARD:
case FORCED:
case NEW:
case NO_CHANGE:
break;
default:
throw new IOException(String.format(
"Checkout \"%s\" failed: %s", name, result));
}
}
/**
* Cherry-pick a commit onto HEAD.
* <p>
* This differs from {@code git cherry-pick} in that it works in a bare
* repository. As a result, any merge failure results in an exception, as
* there is no way to recover.
*
* @param id
* commit-ish to cherry-pick.
* @return newly created commit, or null if no work was done due to the
* resulting tree being identical.
* @throws Exception
*/
public RevCommit cherryPick(AnyObjectId id) throws Exception {
RevCommit commit = pool.parseCommit(id);
pool.parseBody(commit);
if (commit.getParentCount() != 1)
throw new IOException(String.format(
"Expected 1 parent for %s, found: %s",
id.name(), Arrays.asList(commit.getParents())));
RevCommit parent = commit.getParent(0);
pool.parseHeaders(parent);
Ref headRef = db.exactRef(Constants.HEAD);
if (headRef == null)
throw new IOException("Missing HEAD");
RevCommit head = pool.parseCommit(headRef.getObjectId());
ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(db, true);
merger.setBase(parent.getTree());
if (merger.merge(head, commit)) {
if (AnyObjectId.equals(head.getTree(), merger.getResultTreeId()))
return null;
tick(1);
org.eclipse.jgit.lib.CommitBuilder b =
new org.eclipse.jgit.lib.CommitBuilder();
b.setParentId(head);
b.setTreeId(merger.getResultTreeId());
b.setAuthor(commit.getAuthorIdent());
b.setCommitter(new PersonIdent(defaultCommitter, getDate()));
b.setMessage(commit.getFullMessage());
ObjectId result;
try (ObjectInserter ins = inserter) {
result = ins.insert(b);
ins.flush();
}
update(Constants.HEAD, result);
return pool.parseCommit(result);
} else {
throw new IOException("Merge conflict");
}
}
/**
* 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)) {
// nothing
} else if (ref.startsWith(Constants.R_REFS)) {
// nothing
} else
ref = Constants.R_HEADS + ref;
return new BranchBuilder(ref);
}
/**
* Tag an object using a lightweight tag.
*
* @param name
* the tag name. The /refs/tags/ prefix will be added if the name
* doesn't start with it
* @param obj
* the object to tag
* @return the tagged object
* @throws Exception
*/
public ObjectId lightweightTag(String name, ObjectId obj) throws Exception {
if (!name.startsWith(Constants.R_TAGS))
name = Constants.R_TAGS + name;
return update(name, obj);
}
/**
* 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 {
try (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(o, 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, 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);
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;
try (PackWriter pw = new PackWriter(db)) {
Set<ObjectId> all = new HashSet<>();
for (Ref r : db.getAllRefs().values())
all.add(r.getObjectId());
pw.preparePack(m, all, PackWriter.NONE);
final ObjectId name = pw.computeName();
pack = nameFor(odb, name, ".pack");
try (OutputStream out =
new BufferedOutputStream(new FileOutputStream(pack))) {
pw.writePack(m, m, out);
}
pack.setReadOnly();
idx = nameFor(odb, name, ".idx");
try (OutputStream out =
new BufferedOutputStream(new FileOutputStream(idx))) {
pw.writeIndex(out);
}
idx.setReadOnly();
}
odb.openPack(pack);
updateServerInfo();
prunePacked(odb);
}
}
private static 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);
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);
}
/**
* Delete this branch.
* @throws Exception
* @since 4.4
*/
public void delete() throws Exception {
TestRepository.this.delete(ref);
}
}
/** Helper to generate a commit. */
public class CommitBuilder {
private final BranchBuilder branch;
private final DirCache tree = DirCache.newInCore();
private ObjectId topLevelTree;
private final List<RevCommit> parents = new ArrayList<>(2);
private int tick = 1;
private String message = "";
private RevCommit self;
private PersonIdent author;
private PersonIdent committer;
private String changeId;
private boolean updateCommitterTime;
CommitBuilder() {
branch = null;
}
CommitBuilder(BranchBuilder b) throws Exception {
branch = b;
Ref ref = db.exactRef(branch.ref);
if (ref != null && ref.getObjectId() != 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 List<RevCommit> parents() {
return Collections.unmodifiableList(parents);
}
public CommitBuilder noParents() {
parents.clear();
return this;
}
public CommitBuilder noFiles() {
tree.clear();
return this;
}
public CommitBuilder setTopLevelTree(ObjectId treeId) {
topLevelTree = treeId;
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 {
return edit(new PathEdit(path) {
@Override
public void apply(DirCacheEntry ent) {
ent.setFileMode(FileMode.REGULAR_FILE);
ent.setObjectId(id);
}
});
}
public CommitBuilder edit(PathEdit edit) {
DirCacheEditor e = tree.editor();
e.add(edit);
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 String message() {
return message;
}
public CommitBuilder tick(int secs) {
tick = secs;
return this;
}
public CommitBuilder ident(PersonIdent ident) {
author = ident;
committer = ident;
return this;
}
public CommitBuilder author(PersonIdent a) {
author = a;
return this;
}
public PersonIdent author() {
return author;
}
public CommitBuilder committer(PersonIdent c) {
committer = c;
return this;
}
public PersonIdent committer() {
return committer;
}
public CommitBuilder insertChangeId() {
changeId = "";
return this;
}
public CommitBuilder insertChangeId(String c) {
// Validate, but store as a string so we can use "" as a sentinel.
ObjectId.fromString(c);
changeId = c;
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);
if (author != null)
c.setAuthor(author);
if (committer != null) {
if (updateCommitterTime)
committer = new PersonIdent(committer, getDate());
c.setCommitter(committer);
}
ObjectId commitId;
try (ObjectInserter ins = inserter) {
if (topLevelTree != null)
c.setTreeId(topLevelTree);
else
c.setTreeId(tree.writeTree(ins));
insertChangeId(c);
c.setMessage(message);
commitId = ins.insert(c);
ins.flush();
}
self = pool.lookupCommit(commitId);
if (branch != null)
branch.update(self);
}
return self;
}
private void insertChangeId(org.eclipse.jgit.lib.CommitBuilder c) {
if (changeId == null)
return;
int idx = ChangeIdUtil.indexOfChangeId(message, "\n");
if (idx >= 0)
return;
ObjectId firstParentId = null;
if (!parents.isEmpty())
firstParentId = parents.get(0);
ObjectId cid;
if (changeId.equals(""))
cid = ChangeIdUtil.computeChangeId(c.getTreeId(), firstParentId,
c.getAuthor(), c.getCommitter(), message);
else
cid = ObjectId.fromString(changeId);
message = ChangeIdUtil.insertId(message, cid);
if (cid != null)
message = message.replaceAll("\nChange-Id: I" //$NON-NLS-1$
+ ObjectId.zeroId().getName() + "\n", "\nChange-Id: I" //$NON-NLS-1$ //$NON-NLS-2$
+ cid.getName() + "\n"); //$NON-NLS-1$
}
public CommitBuilder child() throws Exception {
return new CommitBuilder(this);
}
}
}