/* * Copyright (C) 2009-2010, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.junit; import static java.nio.charset.StandardCharsets.UTF_8; 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.Pack; 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 * type of Repository the test data is stored on. */ public class TestRepository implements AutoCloseable { /** Constant AUTHOR="J. Author" */ public static final String AUTHOR = "J. Author"; /** Constant AUTHOR_EMAIL="jauthor@example.com" */ public static final String AUTHOR_EMAIL = "jauthor@example.com"; /** Constant COMMITTER="J. Committer" */ public static final String COMMITTER = "J. Committer"; /** Constant COMMITTER_EMAIL="jcommitter@example.com" */ 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); } /** * Get repository * * @return the repository this helper class operates against. */ public R getRepository() { return db; } /** * Get RevWalk * * @return get the RevWalk pool all objects are allocated through. */ public RevWalk getRevWalk() { return pool; } /** * Return Git API wrapper * * @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; } /** * Get date * * @return current date. * @since 4.2 */ public Date getDate() { return new Date(mockSystemReader.getCurrentTime()); } /** * Get timezone * * @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(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(String content) throws Exception { return blob(content.getBytes(UTF_8)); } /** * Create a new blob object in the repository. * * @param content * binary file content. * @return the new, fully parsed blob. * @throws Exception */ public RevBlob blob(byte[] content) throws Exception { ObjectId id; try (ObjectInserter ins = inserter) { id = ins.insert(Constants.OBJ_BLOB, content); ins.flush(); } return (RevBlob) pool.parseAny(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(String path, 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 the new, fully parsed tree specified by the entry list. * @throws Exception */ public RevTree tree(DirCacheEntry... entries) throws Exception { final DirCache dc = DirCache.newInCore(); final DirCacheBuilder b = dc.builder(); for (DirCacheEntry e : entries) { b.add(e); } b.finish(); ObjectId root; try (ObjectInserter ins = inserter) { root = dc.writeTree(ins); ins.flush(); } return pool.parseTree(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(RevTree tree, 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, unparsed commit. *

* See {@link #unparsedCommit(int, RevTree, ObjectId...)}. The tree is the * empty tree (no files or subdirectories). * * @param parents * zero or more IDs of the commit's parents. * @return the ID of the new commit. * @throws Exception */ public ObjectId unparsedCommit(ObjectId... parents) throws Exception { return unparsedCommit(1, tree(), parents); } /** * Create a new commit. *

* 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(RevCommit... parents) throws Exception { return commit(1, tree(), parents); } /** * Create a new commit. *

* 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(RevTree tree, RevCommit... parents) throws Exception { return commit(1, tree, parents); } /** * Create a new commit. *

* 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(int secDelta, RevCommit... parents) throws Exception { return commit(secDelta, tree(), parents); } /** * Create a new commit. *

* 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, fully parsed commit. * @throws Exception */ public RevCommit commit(final int secDelta, final RevTree tree, final RevCommit... parents) throws Exception { ObjectId id = unparsedCommit(secDelta, tree, parents); return pool.parseCommit(id); } /** * Create a new, unparsed commit. *

* 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 IDs of the commit's parents. * @return the ID of the new commit. * @throws Exception */ public ObjectId unparsedCommit(final int secDelta, final RevTree tree, final ObjectId... 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 id; } /** * Create commit builder * * @return a new commit builder. */ public CommitBuilder commit() { return new CommitBuilder(); } /** * Construct an annotated tag object pointing at another object. *

* The tagger is the committer identity, at the current time as specified by * {@link #tick(int)}. The time is not increased. *

* 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 new, fully parsed annotated tag object. * @throws Exception */ public RevTag tag(String name, 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 pool.parseTag(id); } /** * 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 * 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 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); u.setForceUpdate(true); 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. * * @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. *

* 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. *

* 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 the new, fully parsed 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.isEqual(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); } 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.getRefDatabase().getRefs()) { @Override protected void writeFile(String name, 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 (Pack 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 * type of object, e.g. {@link org.eclipse.jgit.revwalk.RevTag} * or {@link org.eclipse.jgit.revwalk.RevCommit}. * @param object * reference to the (possibly unparsed) object to force body * parsing of. * @return {@code object} * @throws Exception */ public T parseBody(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. *

* 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.getRefDatabase().getRefs()) 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. *

* 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 all = new HashSet<>(); for (Ref r : db.getRefDatabase().getRefs()) 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); } } /** * Closes the underlying {@link Repository} object and any other internal * resources. *

* {@link AutoCloseable} resources that may escape this object, such as * those returned by the {@link #git} and {@link #getRevWalk()} methods are * not closed. */ @Override public void close() { try { inserter.close(); } finally { db.close(); } } private static void prunePacked(ObjectDirectory odb) throws IOException { for (Pack 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 = odb.getPackDirectory(); return new File(packdir, "pack-" + name.name() + t); } private void writeFile(File p, 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, ioe); } 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(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 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()); } /** * set parent commit * * @param p * parent commit * @return this commit builder * @throws Exception */ 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; } /** * Get parent commits * * @return parent commits */ public List parents() { return Collections.unmodifiableList(parents); } /** * Remove parent commits * * @return this commit builder */ public CommitBuilder noParents() { parents.clear(); return this; } /** * Remove files * * @return this commit builder */ public CommitBuilder noFiles() { tree.clear(); return this; } /** * Set top level tree * * @param treeId * the top level tree * @return this commit builder */ public CommitBuilder setTopLevelTree(ObjectId treeId) { topLevelTree = treeId; return this; } /** * Add file with given content * * @param path * path of the file * @param content * the file content * @return this commit builder * @throws Exception */ public CommitBuilder add(String path, String content) throws Exception { return add(path, blob(content)); } /** * Add file with given path and blob * * @param path * path of the file * @param id * blob for this file * @return this commit builder * @throws Exception */ public CommitBuilder add(String path, RevBlob id) throws Exception { return edit(new PathEdit(path) { @Override public void apply(DirCacheEntry ent) { ent.setFileMode(FileMode.REGULAR_FILE); ent.setObjectId(id); } }); } /** * Edit the index * * @param edit * the index record update * @return this commit builder */ public CommitBuilder edit(PathEdit edit) { DirCacheEditor e = tree.editor(); e.add(edit); e.finish(); return this; } /** * Remove a file * * @param path * path of the file * @return this commit builder */ public CommitBuilder rm(String path) { DirCacheEditor e = tree.editor(); e.add(new DeletePath(path)); e.add(new DeleteTree(path)); e.finish(); return this; } /** * Set commit message * * @param m * the message * @return this commit builder */ public CommitBuilder message(String m) { message = m; return this; } /** * Get the commit message * * @return the commit message */ public String message() { return message; } /** * Tick the clock * * @param secs * number of seconds * @return this commit builder */ public CommitBuilder tick(int secs) { tick = secs; return this; } /** * Set author and committer identity * * @param ident * identity to set * @return this commit builder */ public CommitBuilder ident(PersonIdent ident) { author = ident; committer = ident; return this; } /** * Set the author identity * * @param a * the author's identity * @return this commit builder */ public CommitBuilder author(PersonIdent a) { author = a; return this; } /** * Get the author identity * * @return the author identity */ public PersonIdent author() { return author; } /** * Set the committer identity * * @param c * the committer identity * @return this commit builder */ public CommitBuilder committer(PersonIdent c) { committer = c; return this; } /** * Get the committer identity * * @return the committer identity */ public PersonIdent committer() { return committer; } /** * Insert changeId * * @return this commit builder */ public CommitBuilder insertChangeId() { changeId = ""; return this; } /** * Insert given changeId * * @param c * changeId * @return this commit builder */ 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; } /** * Create the commit * * @return the new commit * @throws Exception * if creation failed */ 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.parseCommit(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.isEmpty()) 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$ } /** * Create child commit builder * * @return child commit builder * @throws Exception */ public CommitBuilder child() throws Exception { return new CommitBuilder(this); } } }