View Javadoc
1   /*
2    * Copyright (C) 2009-2010, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.junit;
45  
46  import static org.eclipse.jgit.lib.Constants.CHARSET;
47  import static org.junit.Assert.assertEquals;
48  import static org.junit.Assert.fail;
49  
50  import java.io.BufferedOutputStream;
51  import java.io.File;
52  import java.io.FileOutputStream;
53  import java.io.IOException;
54  import java.io.OutputStream;
55  import java.security.MessageDigest;
56  import java.util.ArrayList;
57  import java.util.Arrays;
58  import java.util.Collections;
59  import java.util.Date;
60  import java.util.HashSet;
61  import java.util.List;
62  import java.util.Set;
63  import java.util.TimeZone;
64  
65  import org.eclipse.jgit.api.Git;
66  import org.eclipse.jgit.dircache.DirCache;
67  import org.eclipse.jgit.dircache.DirCacheBuilder;
68  import org.eclipse.jgit.dircache.DirCacheEditor;
69  import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
70  import org.eclipse.jgit.dircache.DirCacheEditor.DeleteTree;
71  import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
72  import org.eclipse.jgit.dircache.DirCacheEntry;
73  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
74  import org.eclipse.jgit.errors.MissingObjectException;
75  import org.eclipse.jgit.errors.ObjectWritingException;
76  import org.eclipse.jgit.internal.storage.file.FileRepository;
77  import org.eclipse.jgit.internal.storage.file.LockFile;
78  import org.eclipse.jgit.internal.storage.file.ObjectDirectory;
79  import org.eclipse.jgit.internal.storage.file.PackFile;
80  import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
81  import org.eclipse.jgit.internal.storage.pack.PackWriter;
82  import org.eclipse.jgit.lib.AnyObjectId;
83  import org.eclipse.jgit.lib.Constants;
84  import org.eclipse.jgit.lib.FileMode;
85  import org.eclipse.jgit.lib.NullProgressMonitor;
86  import org.eclipse.jgit.lib.ObjectChecker;
87  import org.eclipse.jgit.lib.ObjectId;
88  import org.eclipse.jgit.lib.ObjectInserter;
89  import org.eclipse.jgit.lib.PersonIdent;
90  import org.eclipse.jgit.lib.Ref;
91  import org.eclipse.jgit.lib.RefUpdate;
92  import org.eclipse.jgit.lib.RefWriter;
93  import org.eclipse.jgit.lib.Repository;
94  import org.eclipse.jgit.lib.TagBuilder;
95  import org.eclipse.jgit.merge.MergeStrategy;
96  import org.eclipse.jgit.merge.ThreeWayMerger;
97  import org.eclipse.jgit.revwalk.ObjectWalk;
98  import org.eclipse.jgit.revwalk.RevBlob;
99  import org.eclipse.jgit.revwalk.RevCommit;
100 import org.eclipse.jgit.revwalk.RevObject;
101 import org.eclipse.jgit.revwalk.RevTag;
102 import org.eclipse.jgit.revwalk.RevTree;
103 import org.eclipse.jgit.revwalk.RevWalk;
104 import org.eclipse.jgit.treewalk.TreeWalk;
105 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
106 import org.eclipse.jgit.util.ChangeIdUtil;
107 import org.eclipse.jgit.util.FileUtils;
108 
109 /**
110  * Wrapper to make creating test data easier.
111  *
112  * @param <R>
113  *            type of Repository the test data is stored on.
114  */
115 public class TestRepository<R extends Repository> {
116 
117 	/** Constant <code>AUTHOR="J. Author"</code> */
118 	public static final String AUTHOR = "J. Author";
119 
120 	/** Constant <code>AUTHOR_EMAIL="jauthor@example.com"</code> */
121 	public static final String AUTHOR_EMAIL = "jauthor@example.com";
122 
123 	/** Constant <code>COMMITTER="J. Committer"</code> */
124 	public static final String COMMITTER = "J. Committer";
125 
126 	/** Constant <code>COMMITTER_EMAIL="jcommitter@example.com"</code> */
127 	public static final String COMMITTER_EMAIL = "jcommitter@example.com";
128 
129 	private final PersonIdent defaultAuthor;
130 
131 	private final PersonIdent defaultCommitter;
132 
133 	private final R db;
134 
135 	private final Git git;
136 
137 	private final RevWalk pool;
138 
139 	private final ObjectInserter inserter;
140 
141 	private final MockSystemReader mockSystemReader;
142 
143 	/**
144 	 * Wrap a repository with test building tools.
145 	 *
146 	 * @param db
147 	 *            the test repository to write into.
148 	 * @throws IOException
149 	 */
150 	public TestRepository(R db) throws IOException {
151 		this(db, new RevWalk(db), new MockSystemReader());
152 	}
153 
154 	/**
155 	 * Wrap a repository with test building tools.
156 	 *
157 	 * @param db
158 	 *            the test repository to write into.
159 	 * @param rw
160 	 *            the RevObject pool to use for object lookup.
161 	 * @throws IOException
162 	 */
163 	public TestRepository(R db, RevWalk rw) throws IOException {
164 		this(db, rw, new MockSystemReader());
165 	}
166 
167 	/**
168 	 * Wrap a repository with test building tools.
169 	 *
170 	 * @param db
171 	 *            the test repository to write into.
172 	 * @param rw
173 	 *            the RevObject pool to use for object lookup.
174 	 * @param reader
175 	 *            the MockSystemReader to use for clock and other system
176 	 *            operations.
177 	 * @throws IOException
178 	 * @since 4.2
179 	 */
180 	public TestRepository(R db, RevWalk rw, MockSystemReader reader)
181 			throws IOException {
182 		this.db = db;
183 		this.git = Git.wrap(db);
184 		this.pool = rw;
185 		this.inserter = db.newObjectInserter();
186 		this.mockSystemReader = reader;
187 		long now = mockSystemReader.getCurrentTime();
188 		int tz = mockSystemReader.getTimezone(now);
189 		defaultAuthor = new PersonIdent(AUTHOR, AUTHOR_EMAIL, now, tz);
190 		defaultCommitter = new PersonIdent(COMMITTER, COMMITTER_EMAIL, now, tz);
191 	}
192 
193 	/**
194 	 * Get repository
195 	 *
196 	 * @return the repository this helper class operates against.
197 	 */
198 	public R getRepository() {
199 		return db;
200 	}
201 
202 	/**
203 	 * Get RevWalk
204 	 *
205 	 * @return get the RevWalk pool all objects are allocated through.
206 	 */
207 	public RevWalk getRevWalk() {
208 		return pool;
209 	}
210 
211 	/**
212 	 * Return Git API wrapper
213 	 *
214 	 * @return an API wrapper for the underlying repository. This wrapper does
215 	 *         not allocate any new resources and need not be closed (but
216 	 *         closing it is harmless).
217 	 */
218 	public Git git() {
219 		return git;
220 	}
221 
222 	/**
223 	 * Get date
224 	 *
225 	 * @return current date.
226 	 * @since 4.2
227 	 */
228 	public Date getDate() {
229 		return new Date(mockSystemReader.getCurrentTime());
230 	}
231 
232 	/**
233 	 * Get timezone
234 	 *
235 	 * @return timezone used for default identities.
236 	 */
237 	public TimeZone getTimeZone() {
238 		return mockSystemReader.getTimeZone();
239 	}
240 
241 	/**
242 	 * Adjust the current time that will used by the next commit.
243 	 *
244 	 * @param secDelta
245 	 *            number of seconds to add to the current time.
246 	 */
247 	public void tick(int secDelta) {
248 		mockSystemReader.tick(secDelta);
249 	}
250 
251 	/**
252 	 * Set the author and committer using {@link #getDate()}.
253 	 *
254 	 * @param c
255 	 *            the commit builder to store.
256 	 */
257 	public void setAuthorAndCommitter(org.eclipse.jgit.lib.CommitBuilder c) {
258 		c.setAuthor(new PersonIdent(defaultAuthor, getDate()));
259 		c.setCommitter(new PersonIdent(defaultCommitter, getDate()));
260 	}
261 
262 	/**
263 	 * Create a new blob object in the repository.
264 	 *
265 	 * @param content
266 	 *            file content, will be UTF-8 encoded.
267 	 * @return reference to the blob.
268 	 * @throws Exception
269 	 */
270 	public RevBlob blob(String content) throws Exception {
271 		return blob(content.getBytes(CHARSET));
272 	}
273 
274 	/**
275 	 * Create a new blob object in the repository.
276 	 *
277 	 * @param content
278 	 *            binary file content.
279 	 * @return reference to the blob.
280 	 * @throws Exception
281 	 */
282 	public RevBlob blob(byte[] content) throws Exception {
283 		ObjectId id;
284 		try (ObjectInserter ins = inserter) {
285 			id = ins.insert(Constants.OBJ_BLOB, content);
286 			ins.flush();
287 		}
288 		return pool.lookupBlob(id);
289 	}
290 
291 	/**
292 	 * Construct a regular file mode tree entry.
293 	 *
294 	 * @param path
295 	 *            path of the file.
296 	 * @param blob
297 	 *            a blob, previously constructed in the repository.
298 	 * @return the entry.
299 	 * @throws Exception
300 	 */
301 	public DirCacheEntry file(String path, RevBlob blob)
302 			throws Exception {
303 		final DirCacheEntry e = new DirCacheEntry(path);
304 		e.setFileMode(FileMode.REGULAR_FILE);
305 		e.setObjectId(blob);
306 		return e;
307 	}
308 
309 	/**
310 	 * Construct a tree from a specific listing of file entries.
311 	 *
312 	 * @param entries
313 	 *            the files to include in the tree. The collection does not need
314 	 *            to be sorted properly and may be empty.
315 	 * @return reference to the tree specified by the entry list.
316 	 * @throws Exception
317 	 */
318 	public RevTree tree(DirCacheEntry... entries) throws Exception {
319 		final DirCache dc = DirCache.newInCore();
320 		final DirCacheBuilder b = dc.builder();
321 		for (DirCacheEntry e : entries)
322 			b.add(e);
323 		b.finish();
324 		ObjectId root;
325 		try (ObjectInserter ins = inserter) {
326 			root = dc.writeTree(ins);
327 			ins.flush();
328 		}
329 		return pool.lookupTree(root);
330 	}
331 
332 	/**
333 	 * Lookup an entry stored in a tree, failing if not present.
334 	 *
335 	 * @param tree
336 	 *            the tree to search.
337 	 * @param path
338 	 *            the path to find the entry of.
339 	 * @return the parsed object entry at this path, never null.
340 	 * @throws Exception
341 	 */
342 	public RevObject get(RevTree tree, String path)
343 			throws Exception {
344 		try (TreeWalk tw = new TreeWalk(pool.getObjectReader())) {
345 			tw.setFilter(PathFilterGroup.createFromStrings(Collections
346 					.singleton(path)));
347 			tw.reset(tree);
348 			while (tw.next()) {
349 				if (tw.isSubtree() && !path.equals(tw.getPathString())) {
350 					tw.enterSubtree();
351 					continue;
352 				}
353 				final ObjectId entid = tw.getObjectId(0);
354 				final FileMode entmode = tw.getFileMode(0);
355 				return pool.lookupAny(entid, entmode.getObjectType());
356 			}
357 		}
358 		fail("Can't find " + path + " in tree " + tree.name());
359 		return null; // never reached.
360 	}
361 
362 	/**
363 	 * Create a new commit.
364 	 * <p>
365 	 * See {@link #commit(int, RevTree, RevCommit...)}. The tree is the empty
366 	 * tree (no files or subdirectories).
367 	 *
368 	 * @param parents
369 	 *            zero or more parents of the commit.
370 	 * @return the new commit.
371 	 * @throws Exception
372 	 */
373 	public RevCommit commit(RevCommit... parents) throws Exception {
374 		return commit(1, tree(), parents);
375 	}
376 
377 	/**
378 	 * Create a new commit.
379 	 * <p>
380 	 * See {@link #commit(int, RevTree, RevCommit...)}.
381 	 *
382 	 * @param tree
383 	 *            the root tree for the commit.
384 	 * @param parents
385 	 *            zero or more parents of the commit.
386 	 * @return the new commit.
387 	 * @throws Exception
388 	 */
389 	public RevCommit commit(RevTree tree, RevCommit... parents)
390 			throws Exception {
391 		return commit(1, tree, parents);
392 	}
393 
394 	/**
395 	 * Create a new commit.
396 	 * <p>
397 	 * See {@link #commit(int, RevTree, RevCommit...)}. The tree is the empty
398 	 * tree (no files or subdirectories).
399 	 *
400 	 * @param secDelta
401 	 *            number of seconds to advance {@link #tick(int)} by.
402 	 * @param parents
403 	 *            zero or more parents of the commit.
404 	 * @return the new commit.
405 	 * @throws Exception
406 	 */
407 	public RevCommit commit(int secDelta, RevCommit... parents)
408 			throws Exception {
409 		return commit(secDelta, tree(), parents);
410 	}
411 
412 	/**
413 	 * Create a new commit.
414 	 * <p>
415 	 * The author and committer identities are stored using the current
416 	 * timestamp, after being incremented by {@code secDelta}. The message body
417 	 * is empty.
418 	 *
419 	 * @param secDelta
420 	 *            number of seconds to advance {@link #tick(int)} by.
421 	 * @param tree
422 	 *            the root tree for the commit.
423 	 * @param parents
424 	 *            zero or more parents of the commit.
425 	 * @return the new commit.
426 	 * @throws Exception
427 	 */
428 	public RevCommit commit(final int secDelta, final RevTree tree,
429 			final RevCommit... parents) throws Exception {
430 		tick(secDelta);
431 
432 		final org.eclipse.jgit.lib.CommitBuilder c;
433 
434 		c = new org.eclipse.jgit.lib.CommitBuilder();
435 		c.setTreeId(tree);
436 		c.setParentIds(parents);
437 		c.setAuthor(new PersonIdent(defaultAuthor, getDate()));
438 		c.setCommitter(new PersonIdent(defaultCommitter, getDate()));
439 		c.setMessage("");
440 		ObjectId id;
441 		try (ObjectInserter ins = inserter) {
442 			id = ins.insert(c);
443 			ins.flush();
444 		}
445 		return pool.lookupCommit(id);
446 	}
447 
448 	/**
449 	 * Create commit builder
450 	 *
451 	 * @return a new commit builder.
452 	 */
453 	public CommitBuilder commit() {
454 		return new CommitBuilder();
455 	}
456 
457 	/**
458 	 * Construct an annotated tag object pointing at another object.
459 	 * <p>
460 	 * The tagger is the committer identity, at the current time as specified by
461 	 * {@link #tick(int)}. The time is not increased.
462 	 * <p>
463 	 * The tag message is empty.
464 	 *
465 	 * @param name
466 	 *            name of the tag. Traditionally a tag name should not start
467 	 *            with {@code refs/tags/}.
468 	 * @param dst
469 	 *            object the tag should be pointed at.
470 	 * @return the annotated tag object.
471 	 * @throws Exception
472 	 */
473 	public RevTag tag(String name, RevObject dst) throws Exception {
474 		final TagBuilder t = new TagBuilder();
475 		t.setObjectId(dst);
476 		t.setTag(name);
477 		t.setTagger(new PersonIdent(defaultCommitter, getDate()));
478 		t.setMessage("");
479 		ObjectId id;
480 		try (ObjectInserter ins = inserter) {
481 			id = ins.insert(t);
482 			ins.flush();
483 		}
484 		return (RevTag) pool.lookupAny(id, Constants.OBJ_TAG);
485 	}
486 
487 	/**
488 	 * Update a reference to point to an object.
489 	 *
490 	 * @param ref
491 	 *            the name of the reference to update to. If {@code ref} does
492 	 *            not start with {@code refs/} and is not the magic names
493 	 *            {@code HEAD} {@code FETCH_HEAD} or {@code MERGE_HEAD}, then
494 	 *            {@code refs/heads/} will be prefixed in front of the given
495 	 *            name, thereby assuming it is a branch.
496 	 * @param to
497 	 *            the target object.
498 	 * @return the target object.
499 	 * @throws Exception
500 	 */
501 	public RevCommit update(String ref, CommitBuilder to) throws Exception {
502 		return update(ref, to.create());
503 	}
504 
505 	/**
506 	 * Amend an existing ref.
507 	 *
508 	 * @param ref
509 	 *            the name of the reference to amend, which must already exist.
510 	 *            If {@code ref} does not start with {@code refs/} and is not the
511 	 *            magic names {@code HEAD} {@code FETCH_HEAD} or {@code
512 	 *            MERGE_HEAD}, then {@code refs/heads/} will be prefixed in front
513 	 *            of the given name, thereby assuming it is a branch.
514 	 * @return commit builder that amends the branch on commit.
515 	 * @throws Exception
516 	 */
517 	public CommitBuilder amendRef(String ref) throws Exception {
518 		String name = normalizeRef(ref);
519 		Ref r = db.exactRef(name);
520 		if (r == null)
521 			throw new IOException("Not a ref: " + ref);
522 		return amend(pool.parseCommit(r.getObjectId()), branch(name).commit());
523 	}
524 
525 	/**
526 	 * Amend an existing commit.
527 	 *
528 	 * @param id
529 	 *            the id of the commit to amend.
530 	 * @return commit builder.
531 	 * @throws Exception
532 	 */
533 	public CommitBuilder amend(AnyObjectId id) throws Exception {
534 		return amend(pool.parseCommit(id), commit());
535 	}
536 
537 	private CommitBuilder amend(RevCommit old, CommitBuilder b) throws Exception {
538 		pool.parseBody(old);
539 		b.author(old.getAuthorIdent());
540 		b.committer(old.getCommitterIdent());
541 		b.message(old.getFullMessage());
542 		// Use the committer name from the old commit, but update it after ticking
543 		// the clock in CommitBuilder#create().
544 		b.updateCommitterTime = true;
545 
546 		// Reset parents to original parents.
547 		b.noParents();
548 		for (int i = 0; i < old.getParentCount(); i++)
549 			b.parent(old.getParent(i));
550 
551 		// Reset tree to original tree; resetting parents reset tree contents to the
552 		// first parent.
553 		b.tree.clear();
554 		try (TreeWalk tw = new TreeWalk(db)) {
555 			tw.reset(old.getTree());
556 			tw.setRecursive(true);
557 			while (tw.next()) {
558 				b.edit(new PathEdit(tw.getPathString()) {
559 					@Override
560 					public void apply(DirCacheEntry ent) {
561 						ent.setFileMode(tw.getFileMode(0));
562 						ent.setObjectId(tw.getObjectId(0));
563 					}
564 				});
565 			}
566 		}
567 
568 		return b;
569 	}
570 
571 	/**
572 	 * Update a reference to point to an object.
573 	 *
574 	 * @param <T>
575 	 *            type of the target object.
576 	 * @param ref
577 	 *            the name of the reference to update to. If {@code ref} does
578 	 *            not start with {@code refs/} and is not the magic names
579 	 *            {@code HEAD} {@code FETCH_HEAD} or {@code MERGE_HEAD}, then
580 	 *            {@code refs/heads/} will be prefixed in front of the given
581 	 *            name, thereby assuming it is a branch.
582 	 * @param obj
583 	 *            the target object.
584 	 * @return the target object.
585 	 * @throws Exception
586 	 */
587 	public <T extends AnyObjectId> T update(String ref, T obj) throws Exception {
588 		ref = normalizeRef(ref);
589 		RefUpdate u = db.updateRef(ref);
590 		u.setNewObjectId(obj);
591 		switch (u.forceUpdate()) {
592 		case FAST_FORWARD:
593 		case FORCED:
594 		case NEW:
595 		case NO_CHANGE:
596 			updateServerInfo();
597 			return obj;
598 
599 		default:
600 			throw new IOException("Cannot write " + ref + " " + u.getResult());
601 		}
602 	}
603 
604 	/**
605 	 * Delete a reference.
606 	 *
607 	 * @param ref
608 	 *	      the name of the reference to delete. This is normalized
609 	 *	      in the same way as {@link #update(String, AnyObjectId)}.
610 	 * @throws Exception
611 	 * @since 4.4
612 	 */
613 	public void delete(String ref) throws Exception {
614 		ref = normalizeRef(ref);
615 		RefUpdate u = db.updateRef(ref);
616 		u.setForceUpdate(true);
617 		switch (u.delete()) {
618 		case FAST_FORWARD:
619 		case FORCED:
620 		case NEW:
621 		case NO_CHANGE:
622 			updateServerInfo();
623 			return;
624 
625 		default:
626 			throw new IOException("Cannot delete " + ref + " " + u.getResult());
627 		}
628 	}
629 
630 	private static String normalizeRef(String ref) {
631 		if (Constants.HEAD.equals(ref)) {
632 			// nothing
633 		} else if ("FETCH_HEAD".equals(ref)) {
634 			// nothing
635 		} else if ("MERGE_HEAD".equals(ref)) {
636 			// nothing
637 		} else if (ref.startsWith(Constants.R_REFS)) {
638 			// nothing
639 		} else
640 			ref = Constants.R_HEADS + ref;
641 		return ref;
642 	}
643 
644 	/**
645 	 * Soft-reset HEAD to a detached state.
646 	 *
647 	 * @param id
648 	 *            ID of detached head.
649 	 * @throws Exception
650 	 * @see #reset(String)
651 	 */
652 	public void reset(AnyObjectId id) throws Exception {
653 		RefUpdate ru = db.updateRef(Constants.HEAD, true);
654 		ru.setNewObjectId(id);
655 		RefUpdate.Result result = ru.forceUpdate();
656 		switch (result) {
657 			case FAST_FORWARD:
658 			case FORCED:
659 			case NEW:
660 			case NO_CHANGE:
661 				break;
662 			default:
663 				throw new IOException(String.format(
664 						"Checkout \"%s\" failed: %s", id.name(), result));
665 		}
666 	}
667 
668 	/**
669 	 * Soft-reset HEAD to a different commit.
670 	 * <p>
671 	 * This is equivalent to {@code git reset --soft} in that it modifies HEAD but
672 	 * not the index or the working tree of a non-bare repository.
673 	 *
674 	 * @param name
675 	 *            revision string; either an existing ref name, or something that
676 	 *            can be parsed to an object ID.
677 	 * @throws Exception
678 	 */
679 	public void reset(String name) throws Exception {
680 		RefUpdate.Result result;
681 		ObjectId id = db.resolve(name);
682 		if (id == null)
683 			throw new IOException("Not a revision: " + name);
684 		RefUpdate ru = db.updateRef(Constants.HEAD, false);
685 		ru.setNewObjectId(id);
686 		result = ru.forceUpdate();
687 		switch (result) {
688 			case FAST_FORWARD:
689 			case FORCED:
690 			case NEW:
691 			case NO_CHANGE:
692 				break;
693 			default:
694 				throw new IOException(String.format(
695 						"Checkout \"%s\" failed: %s", name, result));
696 		}
697 	}
698 
699 	/**
700 	 * Cherry-pick a commit onto HEAD.
701 	 * <p>
702 	 * This differs from {@code git cherry-pick} in that it works in a bare
703 	 * repository. As a result, any merge failure results in an exception, as
704 	 * there is no way to recover.
705 	 *
706 	 * @param id
707 	 *            commit-ish to cherry-pick.
708 	 * @return newly created commit, or null if no work was done due to the
709 	 *         resulting tree being identical.
710 	 * @throws Exception
711 	 */
712 	public RevCommit cherryPick(AnyObjectId id) throws Exception {
713 		RevCommit commit = pool.parseCommit(id);
714 		pool.parseBody(commit);
715 		if (commit.getParentCount() != 1)
716 			throw new IOException(String.format(
717 					"Expected 1 parent for %s, found: %s",
718 					id.name(), Arrays.asList(commit.getParents())));
719 		RevCommit parent = commit.getParent(0);
720 		pool.parseHeaders(parent);
721 
722 		Ref headRef = db.exactRef(Constants.HEAD);
723 		if (headRef == null)
724 			throw new IOException("Missing HEAD");
725 		RevCommit head = pool.parseCommit(headRef.getObjectId());
726 
727 		ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(db, true);
728 		merger.setBase(parent.getTree());
729 		if (merger.merge(head, commit)) {
730 			if (AnyObjectId.equals(head.getTree(), merger.getResultTreeId()))
731 				return null;
732 			tick(1);
733 			org.eclipse.jgit.lib.CommitBuilder b =
734 					new org.eclipse.jgit.lib.CommitBuilder();
735 			b.setParentId(head);
736 			b.setTreeId(merger.getResultTreeId());
737 			b.setAuthor(commit.getAuthorIdent());
738 			b.setCommitter(new PersonIdent(defaultCommitter, getDate()));
739 			b.setMessage(commit.getFullMessage());
740 			ObjectId result;
741 			try (ObjectInserter ins = inserter) {
742 				result = ins.insert(b);
743 				ins.flush();
744 			}
745 			update(Constants.HEAD, result);
746 			return pool.parseCommit(result);
747 		} else {
748 			throw new IOException("Merge conflict");
749 		}
750 	}
751 
752 	/**
753 	 * Update the dumb client server info files.
754 	 *
755 	 * @throws Exception
756 	 */
757 	public void updateServerInfo() throws Exception {
758 		if (db instanceof FileRepository) {
759 			final FileRepository fr = (FileRepository) db;
760 			RefWriter rw = new RefWriter(fr.getAllRefs().values()) {
761 				@Override
762 				protected void writeFile(String name, byte[] bin)
763 						throws IOException {
764 					File path = new File(fr.getDirectory(), name);
765 					TestRepository.this.writeFile(path, bin);
766 				}
767 			};
768 			rw.writePackedRefs();
769 			rw.writeInfoRefs();
770 
771 			final StringBuilder w = new StringBuilder();
772 			for (PackFile p : fr.getObjectDatabase().getPacks()) {
773 				w.append("P ");
774 				w.append(p.getPackFile().getName());
775 				w.append('\n');
776 			}
777 			writeFile(new File(new File(fr.getObjectDatabase().getDirectory(),
778 					"info"), "packs"), Constants.encodeASCII(w.toString()));
779 		}
780 	}
781 
782 	/**
783 	 * Ensure the body of the given object has been parsed.
784 	 *
785 	 * @param <T>
786 	 *            type of object, e.g. {@link org.eclipse.jgit.revwalk.RevTag}
787 	 *            or {@link org.eclipse.jgit.revwalk.RevCommit}.
788 	 * @param object
789 	 *            reference to the (possibly unparsed) object to force body
790 	 *            parsing of.
791 	 * @return {@code object}
792 	 * @throws Exception
793 	 */
794 	public <T extends RevObject> T parseBody(T object) throws Exception {
795 		pool.parseBody(object);
796 		return object;
797 	}
798 
799 	/**
800 	 * Create a new branch builder for this repository.
801 	 *
802 	 * @param ref
803 	 *            name of the branch to be constructed. If {@code ref} does not
804 	 *            start with {@code refs/} the prefix {@code refs/heads/} will
805 	 *            be added.
806 	 * @return builder for the named branch.
807 	 */
808 	public BranchBuilder branch(String ref) {
809 		if (Constants.HEAD.equals(ref)) {
810 			// nothing
811 		} else if (ref.startsWith(Constants.R_REFS)) {
812 			// nothing
813 		} else
814 			ref = Constants.R_HEADS + ref;
815 		return new BranchBuilder(ref);
816 	}
817 
818 	/**
819 	 * Tag an object using a lightweight tag.
820 	 *
821 	 * @param name
822 	 *            the tag name. The /refs/tags/ prefix will be added if the name
823 	 *            doesn't start with it
824 	 * @param obj
825 	 *            the object to tag
826 	 * @return the tagged object
827 	 * @throws Exception
828 	 */
829 	public ObjectId lightweightTag(String name, ObjectId obj) throws Exception {
830 		if (!name.startsWith(Constants.R_TAGS))
831 			name = Constants.R_TAGS + name;
832 		return update(name, obj);
833 	}
834 
835 	/**
836 	 * Run consistency checks against the object database.
837 	 * <p>
838 	 * This method completes silently if the checks pass. A temporary revision
839 	 * pool is constructed during the checking.
840 	 *
841 	 * @param tips
842 	 *            the tips to start checking from; if not supplied the refs of
843 	 *            the repository are used instead.
844 	 * @throws MissingObjectException
845 	 * @throws IncorrectObjectTypeException
846 	 * @throws IOException
847 	 */
848 	public void fsck(RevObject... tips) throws MissingObjectException,
849 			IncorrectObjectTypeException, IOException {
850 		try (ObjectWalk ow = new ObjectWalk(db)) {
851 			if (tips.length != 0) {
852 				for (RevObject o : tips)
853 					ow.markStart(ow.parseAny(o));
854 			} else {
855 				for (Ref r : db.getAllRefs().values())
856 					ow.markStart(ow.parseAny(r.getObjectId()));
857 			}
858 
859 			ObjectChecker oc = new ObjectChecker();
860 			for (;;) {
861 				final RevCommit o = ow.next();
862 				if (o == null)
863 					break;
864 
865 				final byte[] bin = db.open(o, o.getType()).getCachedBytes();
866 				oc.checkCommit(o, bin);
867 				assertHash(o, bin);
868 			}
869 
870 			for (;;) {
871 				final RevObject o = ow.nextObject();
872 				if (o == null)
873 					break;
874 
875 				final byte[] bin = db.open(o, o.getType()).getCachedBytes();
876 				oc.check(o, o.getType(), bin);
877 				assertHash(o, bin);
878 			}
879 		}
880 	}
881 
882 	private static void assertHash(RevObject id, byte[] bin) {
883 		MessageDigest md = Constants.newMessageDigest();
884 		md.update(Constants.encodedTypeString(id.getType()));
885 		md.update((byte) ' ');
886 		md.update(Constants.encodeASCII(bin.length));
887 		md.update((byte) 0);
888 		md.update(bin);
889 		assertEquals(id, ObjectId.fromRaw(md.digest()));
890 	}
891 
892 	/**
893 	 * Pack all reachable objects in the repository into a single pack file.
894 	 * <p>
895 	 * All loose objects are automatically pruned. Existing packs however are
896 	 * not removed.
897 	 *
898 	 * @throws Exception
899 	 */
900 	public void packAndPrune() throws Exception {
901 		if (db.getObjectDatabase() instanceof ObjectDirectory) {
902 			ObjectDirectory odb = (ObjectDirectory) db.getObjectDatabase();
903 			NullProgressMonitor m = NullProgressMonitor.INSTANCE;
904 
905 			final File pack, idx;
906 			try (PackWriter pw = new PackWriter(db)) {
907 				Set<ObjectId> all = new HashSet<>();
908 				for (Ref r : db.getAllRefs().values())
909 					all.add(r.getObjectId());
910 				pw.preparePack(m, all, PackWriter.NONE);
911 
912 				final ObjectId name = pw.computeName();
913 
914 				pack = nameFor(odb, name, ".pack");
915 				try (OutputStream out =
916 						new BufferedOutputStream(new FileOutputStream(pack))) {
917 					pw.writePack(m, m, out);
918 				}
919 				pack.setReadOnly();
920 
921 				idx = nameFor(odb, name, ".idx");
922 				try (OutputStream out =
923 						new BufferedOutputStream(new FileOutputStream(idx))) {
924 					pw.writeIndex(out);
925 				}
926 				idx.setReadOnly();
927 			}
928 
929 			odb.openPack(pack);
930 			updateServerInfo();
931 			prunePacked(odb);
932 		}
933 	}
934 
935 	private static void prunePacked(ObjectDirectory odb) throws IOException {
936 		for (PackFile p : odb.getPacks()) {
937 			for (MutableEntry e : p)
938 				FileUtils.delete(odb.fileFor(e.toObjectId()));
939 		}
940 	}
941 
942 	private static File nameFor(ObjectDirectory odb, ObjectId name, String t) {
943 		File packdir = odb.getPackDirectory();
944 		return new File(packdir, "pack-" + name.name() + t);
945 	}
946 
947 	private void writeFile(File p, byte[] bin) throws IOException,
948 			ObjectWritingException {
949 		final LockFile lck = new LockFile(p);
950 		if (!lck.lock())
951 			throw new ObjectWritingException("Can't write " + p);
952 		try {
953 			lck.write(bin);
954 		} catch (IOException ioe) {
955 			throw new ObjectWritingException("Can't write " + p);
956 		}
957 		if (!lck.commit())
958 			throw new ObjectWritingException("Can't write " + p);
959 	}
960 
961 	/** Helper to build a branch with one or more commits */
962 	public class BranchBuilder {
963 		private final String ref;
964 
965 		BranchBuilder(String ref) {
966 			this.ref = ref;
967 		}
968 
969 		/**
970 		 * @return construct a new commit builder that updates this branch. If
971 		 *         the branch already exists, the commit builder will have its
972 		 *         first parent as the current commit and its tree will be
973 		 *         initialized to the current files.
974 		 * @throws Exception
975 		 *             the commit builder can't read the current branch state
976 		 */
977 		public CommitBuilder commit() throws Exception {
978 			return new CommitBuilder(this);
979 		}
980 
981 		/**
982 		 * Forcefully update this branch to a particular commit.
983 		 *
984 		 * @param to
985 		 *            the commit to update to.
986 		 * @return {@code to}.
987 		 * @throws Exception
988 		 */
989 		public RevCommit update(CommitBuilder to) throws Exception {
990 			return update(to.create());
991 		}
992 
993 		/**
994 		 * Forcefully update this branch to a particular commit.
995 		 *
996 		 * @param to
997 		 *            the commit to update to.
998 		 * @return {@code to}.
999 		 * @throws Exception
1000 		 */
1001 		public RevCommit update(RevCommit to) throws Exception {
1002 			return TestRepository.this.update(ref, to);
1003 		}
1004 
1005 		/**
1006 		 * Delete this branch.
1007 		 * @throws Exception
1008 		 * @since 4.4
1009 		 */
1010 		public void delete() throws Exception {
1011 			TestRepository.this.delete(ref);
1012 		}
1013 	}
1014 
1015 	/** Helper to generate a commit. */
1016 	public class CommitBuilder {
1017 		private final BranchBuilder branch;
1018 
1019 		private final DirCache tree = DirCache.newInCore();
1020 
1021 		private ObjectId topLevelTree;
1022 
1023 		private final List<RevCommit> parents = new ArrayList<>(2);
1024 
1025 		private int tick = 1;
1026 
1027 		private String message = "";
1028 
1029 		private RevCommit self;
1030 
1031 		private PersonIdent author;
1032 		private PersonIdent committer;
1033 
1034 		private String changeId;
1035 
1036 		private boolean updateCommitterTime;
1037 
1038 		CommitBuilder() {
1039 			branch = null;
1040 		}
1041 
1042 		CommitBuilder(BranchBuilder b) throws Exception {
1043 			branch = b;
1044 
1045 			Ref ref = db.exactRef(branch.ref);
1046 			if (ref != null && ref.getObjectId() != null)
1047 				parent(pool.parseCommit(ref.getObjectId()));
1048 		}
1049 
1050 		CommitBuilder(CommitBuilder prior) throws Exception {
1051 			branch = prior.branch;
1052 
1053 			DirCacheBuilder b = tree.builder();
1054 			for (int i = 0; i < prior.tree.getEntryCount(); i++)
1055 				b.add(prior.tree.getEntry(i));
1056 			b.finish();
1057 
1058 			parents.add(prior.create());
1059 		}
1060 
1061 		public CommitBuilder parent(RevCommit p) throws Exception {
1062 			if (parents.isEmpty()) {
1063 				DirCacheBuilder b = tree.builder();
1064 				parseBody(p);
1065 				b.addTree(new byte[0], DirCacheEntry.STAGE_0, pool
1066 						.getObjectReader(), p.getTree());
1067 				b.finish();
1068 			}
1069 			parents.add(p);
1070 			return this;
1071 		}
1072 
1073 		public List<RevCommit> parents() {
1074 			return Collections.unmodifiableList(parents);
1075 		}
1076 
1077 		public CommitBuilder noParents() {
1078 			parents.clear();
1079 			return this;
1080 		}
1081 
1082 		public CommitBuilder noFiles() {
1083 			tree.clear();
1084 			return this;
1085 		}
1086 
1087 		public CommitBuilder setTopLevelTree(ObjectId treeId) {
1088 			topLevelTree = treeId;
1089 			return this;
1090 		}
1091 
1092 		public CommitBuilder add(String path, String content) throws Exception {
1093 			return add(path, blob(content));
1094 		}
1095 
1096 		public CommitBuilder add(String path, RevBlob id)
1097 				throws Exception {
1098 			return edit(new PathEdit(path) {
1099 				@Override
1100 				public void apply(DirCacheEntry ent) {
1101 					ent.setFileMode(FileMode.REGULAR_FILE);
1102 					ent.setObjectId(id);
1103 				}
1104 			});
1105 		}
1106 
1107 		public CommitBuilder edit(PathEdit edit) {
1108 			DirCacheEditor e = tree.editor();
1109 			e.add(edit);
1110 			e.finish();
1111 			return this;
1112 		}
1113 
1114 		public CommitBuilder rm(String path) {
1115 			DirCacheEditor e = tree.editor();
1116 			e.add(new DeletePath(path));
1117 			e.add(new DeleteTree(path));
1118 			e.finish();
1119 			return this;
1120 		}
1121 
1122 		public CommitBuilder message(String m) {
1123 			message = m;
1124 			return this;
1125 		}
1126 
1127 		public String message() {
1128 			return message;
1129 		}
1130 
1131 		public CommitBuilder tick(int secs) {
1132 			tick = secs;
1133 			return this;
1134 		}
1135 
1136 		public CommitBuilder ident(PersonIdent ident) {
1137 			author = ident;
1138 			committer = ident;
1139 			return this;
1140 		}
1141 
1142 		public CommitBuilder author(PersonIdent a) {
1143 			author = a;
1144 			return this;
1145 		}
1146 
1147 		public PersonIdent author() {
1148 			return author;
1149 		}
1150 
1151 		public CommitBuilder committer(PersonIdent c) {
1152 			committer = c;
1153 			return this;
1154 		}
1155 
1156 		public PersonIdent committer() {
1157 			return committer;
1158 		}
1159 
1160 		public CommitBuilder insertChangeId() {
1161 			changeId = "";
1162 			return this;
1163 		}
1164 
1165 		public CommitBuilder insertChangeId(String c) {
1166 			// Validate, but store as a string so we can use "" as a sentinel.
1167 			ObjectId.fromString(c);
1168 			changeId = c;
1169 			return this;
1170 		}
1171 
1172 		public RevCommit create() throws Exception {
1173 			if (self == null) {
1174 				TestRepository.this.tick(tick);
1175 
1176 				final org.eclipse.jgit.lib.CommitBuilder c;
1177 
1178 				c = new org.eclipse.jgit.lib.CommitBuilder();
1179 				c.setParentIds(parents);
1180 				setAuthorAndCommitter(c);
1181 				if (author != null)
1182 					c.setAuthor(author);
1183 				if (committer != null) {
1184 					if (updateCommitterTime)
1185 						committer = new PersonIdent(committer, getDate());
1186 					c.setCommitter(committer);
1187 				}
1188 
1189 				ObjectId commitId;
1190 				try (ObjectInserter ins = inserter) {
1191 					if (topLevelTree != null)
1192 						c.setTreeId(topLevelTree);
1193 					else
1194 						c.setTreeId(tree.writeTree(ins));
1195 					insertChangeId(c);
1196 					c.setMessage(message);
1197 					commitId = ins.insert(c);
1198 					ins.flush();
1199 				}
1200 				self = pool.lookupCommit(commitId);
1201 
1202 				if (branch != null)
1203 					branch.update(self);
1204 			}
1205 			return self;
1206 		}
1207 
1208 		private void insertChangeId(org.eclipse.jgit.lib.CommitBuilder c) {
1209 			if (changeId == null)
1210 				return;
1211 			int idx = ChangeIdUtil.indexOfChangeId(message, "\n");
1212 			if (idx >= 0)
1213 				return;
1214 
1215 			ObjectId firstParentId = null;
1216 			if (!parents.isEmpty())
1217 				firstParentId = parents.get(0);
1218 
1219 			ObjectId cid;
1220 			if (changeId.equals(""))
1221 				cid = ChangeIdUtil.computeChangeId(c.getTreeId(), firstParentId,
1222 						c.getAuthor(), c.getCommitter(), message);
1223 			else
1224 				cid = ObjectId.fromString(changeId);
1225 			message = ChangeIdUtil.insertId(message, cid);
1226 			if (cid != null)
1227 				message = message.replaceAll("\nChange-Id: I" //$NON-NLS-1$
1228 						+ ObjectId.zeroId().getName() + "\n", "\nChange-Id: I" //$NON-NLS-1$ //$NON-NLS-2$
1229 						+ cid.getName() + "\n"); //$NON-NLS-1$
1230 		}
1231 
1232 		public CommitBuilder child() throws Exception {
1233 			return new CommitBuilder(this);
1234 		}
1235 	}
1236 }