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