| /* |
| * 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.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; |
| import org.eclipse.jgit.util.io.SafeBufferedOutputStream; |
| |
| /** |
| * 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 defaultAuthor; |
| |
| private static final PersonIdent defaultCommitter; |
| |
| 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"; |
| defaultAuthor = new PersonIdent(an, ae, now, tz); |
| |
| final String cn = "J. Committer"; |
| final String ce = "jcommitter@example.com"; |
| defaultCommitter = new PersonIdent(cn, ce, now, tz); |
| } |
| |
| private final R db; |
| |
| private final Git git; |
| |
| 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.git = Git.wrap(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 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 time adjusted by {@link #tick(int)}. */ |
| public Date getClock() { |
| return new Date(now); |
| } |
| |
| /** @return timezone used for default identities. */ |
| public TimeZone getTimeZone() { |
| return defaultCommitter.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) { |
| 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(defaultAuthor, new Date(now))); |
| c.setCommitter(new PersonIdent(defaultCommitter, 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 (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, new Date(now))); |
| c.setCommitter(new PersonIdent(defaultCommitter, new Date(now))); |
| 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, new Date(now))); |
| 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.getRef(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()); |
| } |
| } |
| |
| 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.getRef(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, new Date(now))); |
| 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(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); |
| 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<ObjectId>(); |
| for (Ref r : db.getAllRefs().values()) |
| all.add(r.getObjectId()); |
| pw.preparePack(m, all, Collections.<ObjectId> emptySet()); |
| |
| final ObjectId name = pw.computeName(); |
| |
| pack = nameFor(odb, name, ".pack"); |
| try (OutputStream out = |
| new SafeBufferedOutputStream(new FileOutputStream(pack))) { |
| pw.writePack(m, m, out); |
| } |
| pack.setReadOnly(); |
| |
| idx = nameFor(odb, name, ".idx"); |
| try (OutputStream out = |
| new SafeBufferedOutputStream(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, 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 ObjectId topLevelTree; |
| |
| private final List<RevCommit> parents = new ArrayList<RevCommit>(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.getRef(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, new Date(now)); |
| 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) |
| throws IOException { |
| 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); |
| } |
| } |
| } |