| /* | |
| * 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.time.Instant; | |
| import java.time.ZoneId; | |
| 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.annotations.Nullable; | |
| 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.PackFile; | |
| import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry; | |
| import org.eclipse.jgit.internal.storage.pack.PackExt; | |
| 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.storage.pack.PackConfig; | |
| 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> implements AutoCloseable { | |
| /** Constant <code>AUTHOR="J. Author"</code> */ | |
| public static final String AUTHOR = "J. Author"; | |
| /** Constant <code>AUTHOR_EMAIL="jauthor@example.com"</code> */ | |
| public static final String AUTHOR_EMAIL = "jauthor@example.com"; | |
| /** Constant <code>COMMITTER="J. Committer"</code> */ | |
| public static final String COMMITTER = "J. Committer"; | |
| /** Constant <code>COMMITTER_EMAIL="jcommitter@example.com"</code> */ | |
| 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 | |
| * if an IO error occurred | |
| */ | |
| 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 | |
| * if an IO error occurred | |
| */ | |
| 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 | |
| * if an IO error occurred | |
| * @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; | |
| Instant now = mockSystemReader.now(); | |
| ZoneId tz = mockSystemReader.getTimeZoneAt(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 | |
| * @deprecated Use {@link #getInstant()} instead. | |
| */ | |
| @Deprecated(since = "7.2") | |
| public Date getDate() { | |
| return new Date(mockSystemReader.getCurrentTime()); | |
| } | |
| /** | |
| * Get instant | |
| * | |
| * @return current instant. | |
| * @since 6.8 | |
| */ | |
| public Instant getInstant() { | |
| return mockSystemReader.now(); | |
| } | |
| /** | |
| * Get timezone | |
| * | |
| * @return timezone used for default identities. | |
| * @deprecated Use {@link #getTimeZoneId()} instead. | |
| */ | |
| @Deprecated(since = "7.2") | |
| public TimeZone getTimeZone() { | |
| return mockSystemReader.getTimeZone(); | |
| } | |
| /** | |
| * Get timezone | |
| * | |
| * @return timezone used for default identities. | |
| * @since 7.2 | |
| */ | |
| public ZoneId getTimeZoneId() { | |
| return mockSystemReader.getTimeZoneId(); | |
| } | |
| /** | |
| * 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 #getInstant()}. | |
| * | |
| * @param c | |
| * the commit builder to store. | |
| */ | |
| public void setAuthorAndCommitter(org.eclipse.jgit.lib.CommitBuilder c) { | |
| c.setAuthor(new PersonIdent(defaultAuthor, getInstant())); | |
| c.setCommitter(new PersonIdent(defaultCommitter, getInstant())); | |
| } | |
| /** | |
| * Create a new blob object in the repository. | |
| * | |
| * @param content | |
| * file content, will be UTF-8 encoded. | |
| * @return reference to the blob. | |
| * @throws Exception | |
| * if an error occurred | |
| */ | |
| 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 | |
| * if an error occurred | |
| */ | |
| 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 | |
| * if an error occurred | |
| */ | |
| 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 symlink mode tree entry. | |
| * | |
| * @param path | |
| * path of the symlink. | |
| * @param blob | |
| * a blob, previously constructed in the repository. | |
| * @return the entry. | |
| * @throws Exception | |
| * if an error occurred | |
| * @since 6.3 | |
| */ | |
| public DirCacheEntry link(String path, RevBlob blob) throws Exception { | |
| DirCacheEntry e = new DirCacheEntry(path); | |
| e.setFileMode(FileMode.SYMLINK); | |
| 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 | |
| * if an error occurred | |
| */ | |
| 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 | |
| * if an error occurred | |
| */ | |
| 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. | |
| * <p> | |
| * 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 | |
| * if an error occurred | |
| * @since 5.5 | |
| */ | |
| public ObjectId unparsedCommit(ObjectId... parents) throws Exception { | |
| return unparsedCommit(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 parents | |
| * zero or more parents of the commit. | |
| * @return the new commit. | |
| * @throws Exception | |
| * if an error occurred | |
| */ | |
| public RevCommit commit(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 | |
| * if an error occurred | |
| */ | |
| public RevCommit commit(RevTree tree, 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 | |
| * if an error occurred | |
| */ | |
| public RevCommit commit(int secDelta, 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, fully parsed commit. | |
| * @throws Exception | |
| * if an error occurred | |
| */ | |
| 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. | |
| * <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 IDs of the commit's parents. | |
| * @return the ID of the new commit. | |
| * @throws Exception | |
| * if an error occurred | |
| * @since 5.5 | |
| */ | |
| 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, getInstant())); | |
| c.setCommitter(new PersonIdent(defaultCommitter, getInstant())); | |
| 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. | |
| * <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 new, fully parsed annotated tag object. | |
| * @throws Exception | |
| * if an error occurred | |
| */ | |
| 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, getInstant())); | |
| 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 | |
| * if an error occurred | |
| */ | |
| 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 | |
| * if an error occurred | |
| */ | |
| 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 | |
| * if an error occurred | |
| */ | |
| 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 | |
| * if an error occurred | |
| */ | |
| 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 | |
| * if an error occurred | |
| * @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 | |
| * if an error occurred | |
| * @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 | |
| * if an error occurred | |
| */ | |
| 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 the new, fully parsed commit, or null if no work was done due to | |
| * the resulting tree being identical. | |
| * @throws Exception | |
| * if an error occurred | |
| */ | |
| 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, getInstant())); | |
| 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 | |
| * if an error occurred | |
| */ | |
| 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 <T> | |
| * 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 | |
| * if an error occurred | |
| */ | |
| public <T extends RevObject> 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 | |
| * if an error occurred | |
| */ | |
| 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 | |
| * if object is missing | |
| * @throws IncorrectObjectTypeException | |
| * if object has unexpected type | |
| * @throws IOException | |
| * if an IO error occurred | |
| */ | |
| 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. | |
| * <p> | |
| * All loose objects are automatically pruned. Existing packs however are | |
| * not removed. | |
| * | |
| * @throws Exception | |
| * if an error occurred | |
| */ | |
| public void packAndPrune() throws Exception { | |
| if (db.getObjectDatabase() instanceof ObjectDirectory) { | |
| ObjectDirectory odb = (ObjectDirectory) db.getObjectDatabase(); | |
| NullProgressMonitor m = NullProgressMonitor.INSTANCE; | |
| PackFile pack; | |
| try (PackWriter pw = new PackWriter(db)) { | |
| Set<ObjectId> all = new HashSet<>(); | |
| for (Ref r : db.getRefDatabase().getRefs()) | |
| all.add(r.getObjectId()); | |
| pw.preparePack(m, all, PackWriter.NONE); | |
| pack = new PackFile(odb.getPackDirectory(), pw.computeName(), | |
| PackExt.PACK); | |
| try (OutputStream out = | |
| new BufferedOutputStream(new FileOutputStream(pack))) { | |
| pw.writePack(m, m, out); | |
| } | |
| pack.setReadOnly(); | |
| PackFile idx = pack.create(PackExt.INDEX); | |
| try (OutputStream out = | |
| new BufferedOutputStream(new FileOutputStream(idx))) { | |
| pw.writeIndex(out); | |
| } | |
| idx.setReadOnly(); | |
| PackConfig pc = new PackConfig(db); | |
| if (pc.getMinBytesForObjSizeIndex() >= 0) { | |
| PackFile oidx = pack.create(PackExt.OBJECT_SIZE_INDEX); | |
| try (OutputStream out = new BufferedOutputStream( | |
| new FileOutputStream(oidx))) { | |
| pw.writeObjectSizeIndex(out); | |
| } | |
| oidx.setReadOnly(); | |
| } | |
| } | |
| odb.openPack(pack); | |
| updateServerInfo(); | |
| prunePacked(odb); | |
| } | |
| } | |
| /** | |
| * Closes the underlying {@link Repository} object and any other internal | |
| * resources. | |
| * <p> | |
| * {@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()), | |
| FileUtils.SKIP_MISSING); | |
| } | |
| } | |
| 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; | |
| } | |
| /** | |
| * Create commit builder | |
| * | |
| * @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 | |
| * if an error occurred | |
| */ | |
| 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 | |
| * if an error occurred | |
| */ | |
| public RevCommit update(RevCommit to) throws Exception { | |
| return TestRepository.this.update(ref, to); | |
| } | |
| /** | |
| * Delete this branch. | |
| * | |
| * @throws Exception | |
| * if an error occurred | |
| * @since 4.4 | |
| */ | |
| public void delete() throws Exception { | |
| TestRepository.this.delete(ref); | |
| } | |
| } | |
| /** Helper to generate a commit. */ | |
| public class CommitBuilder { | |
| private final BranchBuilder branch; | |
| private final DirCache tree = DirCache.newInCore(); | |
| private ObjectId topLevelTree; | |
| private final List<RevCommit> parents = new ArrayList<>(2); | |
| private int tick = 1; | |
| private String message = ""; | |
| private RevCommit self; | |
| private PersonIdent author; | |
| private PersonIdent committer; | |
| private String changeId; | |
| private boolean updateCommitterTime; | |
| CommitBuilder() { | |
| branch = null; | |
| } | |
| CommitBuilder(BranchBuilder b) throws Exception { | |
| branch = b; | |
| Ref ref = db.exactRef(branch.ref); | |
| if (ref != null && ref.getObjectId() != null) | |
| parent(pool.parseCommit(ref.getObjectId())); | |
| } | |
| CommitBuilder(CommitBuilder prior) throws Exception { | |
| branch = prior.branch; | |
| DirCacheBuilder b = tree.builder(); | |
| for (int i = 0; i < prior.tree.getEntryCount(); i++) | |
| b.add(prior.tree.getEntry(i)); | |
| b.finish(); | |
| parents.add(prior.create()); | |
| } | |
| /** | |
| * Set parent commit | |
| * | |
| * @param p | |
| * parent commit, can be {@code null} | |
| * @return this commit builder | |
| * @throws Exception | |
| * if an error occurred | |
| */ | |
| public CommitBuilder parent(@Nullable RevCommit p) throws Exception { | |
| if (p == null) { | |
| return this; | |
| } | |
| 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<RevCommit> 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 | |
| * if an error occurred | |
| */ | |
| 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 | |
| * if an error occurred | |
| */ | |
| 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, getInstant()); | |
| 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 | |
| * if an error occurred | |
| */ | |
| public CommitBuilder child() throws Exception { | |
| return new CommitBuilder(this); | |
| } | |
| } | |
| } |