/* * 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<ObjectId>(); 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<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.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); } } }