2 * Copyright (C) 2009-2010, Google Inc.
3 * and other copyright owners as documented in the project's IP log.
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
10 * All rights reserved.
12 * Redistribution and use in source and binary forms, with or
13 * without modification, are permitted provided that the following
16 * - Redistributions of source code must retain the above copyright
17 * notice, this list of conditions and the following disclaimer.
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.
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
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.
44 package org
.eclipse
.jgit
.junit
;
46 import java
.io
.BufferedOutputStream
;
48 import java
.io
.FileOutputStream
;
49 import java
.io
.IOException
;
50 import java
.io
.OutputStream
;
51 import java
.security
.MessageDigest
;
52 import java
.util
.ArrayList
;
53 import java
.util
.Collections
;
54 import java
.util
.Date
;
55 import java
.util
.HashSet
;
56 import java
.util
.List
;
59 import junit
.framework
.Assert
;
60 import junit
.framework
.AssertionFailedError
;
62 import org
.eclipse
.jgit
.dircache
.DirCache
;
63 import org
.eclipse
.jgit
.dircache
.DirCacheBuilder
;
64 import org
.eclipse
.jgit
.dircache
.DirCacheEditor
;
65 import org
.eclipse
.jgit
.dircache
.DirCacheEntry
;
66 import org
.eclipse
.jgit
.dircache
.DirCacheEditor
.DeletePath
;
67 import org
.eclipse
.jgit
.dircache
.DirCacheEditor
.DeleteTree
;
68 import org
.eclipse
.jgit
.dircache
.DirCacheEditor
.PathEdit
;
69 import org
.eclipse
.jgit
.errors
.IncorrectObjectTypeException
;
70 import org
.eclipse
.jgit
.errors
.MissingObjectException
;
71 import org
.eclipse
.jgit
.errors
.ObjectWritingException
;
72 import org
.eclipse
.jgit
.lib
.AnyObjectId
;
73 import org
.eclipse
.jgit
.lib
.Commit
;
74 import org
.eclipse
.jgit
.lib
.Constants
;
75 import org
.eclipse
.jgit
.lib
.FileMode
;
76 import org
.eclipse
.jgit
.lib
.LockFile
;
77 import org
.eclipse
.jgit
.lib
.NullProgressMonitor
;
78 import org
.eclipse
.jgit
.lib
.ObjectChecker
;
79 import org
.eclipse
.jgit
.lib
.ObjectDatabase
;
80 import org
.eclipse
.jgit
.lib
.ObjectDirectory
;
81 import org
.eclipse
.jgit
.lib
.ObjectId
;
82 import org
.eclipse
.jgit
.lib
.ObjectWriter
;
83 import org
.eclipse
.jgit
.lib
.PackFile
;
84 import org
.eclipse
.jgit
.lib
.PackWriter
;
85 import org
.eclipse
.jgit
.lib
.PersonIdent
;
86 import org
.eclipse
.jgit
.lib
.Ref
;
87 import org
.eclipse
.jgit
.lib
.RefUpdate
;
88 import org
.eclipse
.jgit
.lib
.RefWriter
;
89 import org
.eclipse
.jgit
.lib
.Repository
;
90 import org
.eclipse
.jgit
.lib
.Tag
;
91 import org
.eclipse
.jgit
.lib
.PackIndex
.MutableEntry
;
92 import org
.eclipse
.jgit
.revwalk
.ObjectWalk
;
93 import org
.eclipse
.jgit
.revwalk
.RevBlob
;
94 import org
.eclipse
.jgit
.revwalk
.RevCommit
;
95 import org
.eclipse
.jgit
.revwalk
.RevObject
;
96 import org
.eclipse
.jgit
.revwalk
.RevTag
;
97 import org
.eclipse
.jgit
.revwalk
.RevTree
;
98 import org
.eclipse
.jgit
.revwalk
.RevWalk
;
99 import org
.eclipse
.jgit
.treewalk
.TreeWalk
;
100 import org
.eclipse
.jgit
.treewalk
.filter
.PathFilterGroup
;
102 /** Wrapper to make creating test data easier. */
103 public class TestRepository
{
104 private static final PersonIdent author
;
106 private static final PersonIdent committer
;
109 final MockSystemReader m
= new MockSystemReader();
110 final long now
= m
.getCurrentTime();
111 final int tz
= m
.getTimezone(now
);
113 final String an
= "J. Author";
114 final String ae
= "jauthor@example.com";
115 author
= new PersonIdent(an
, ae
, now
, tz
);
117 final String cn
= "J. Committer";
118 final String ce
= "jcommitter@example.com";
119 committer
= new PersonIdent(cn
, ce
, now
, tz
);
122 private final Repository db
;
124 private final RevWalk pool
;
126 private final ObjectWriter writer
;
131 * Wrap a repository with test building tools.
134 * the test repository to write into.
137 public TestRepository(Repository db
) throws Exception
{
138 this(db
, new RevWalk(db
));
142 * Wrap a repository with test building tools.
145 * the test repository to write into.
147 * the RevObject pool to use for object lookup.
150 public TestRepository(Repository db
, RevWalk rw
) throws Exception
{
153 this.writer
= new ObjectWriter(db
);
154 this.now
= 1236977987000L;
157 /** @return the repository this helper class operates against. */
158 public Repository
getRepository() {
162 /** @return get the RevWalk pool all objects are allocated through. */
163 public RevWalk
getRevWalk() {
167 /** @return current time adjusted by {@link #tick(int)}. */
168 public Date
getClock() {
169 return new Date(now
);
173 * Adjust the current time that will used by the next commit.
176 * number of seconds to add to the current time.
178 public void tick(final int secDelta
) {
179 now
+= secDelta
* 1000L;
183 * Create a new blob object in the repository.
186 * file content, will be UTF-8 encoded.
187 * @return reference to the blob.
190 public RevBlob
blob(final String content
) throws Exception
{
191 return blob(content
.getBytes("UTF-8"));
195 * Create a new blob object in the repository.
198 * binary file content.
199 * @return reference to the blob.
202 public RevBlob
blob(final byte[] content
) throws Exception
{
203 return pool
.lookupBlob(writer
.writeBlob(content
));
207 * Construct a regular file mode tree entry.
212 * a blob, previously constructed in the repository.
216 public DirCacheEntry
file(final String path
, final RevBlob blob
)
218 final DirCacheEntry e
= new DirCacheEntry(path
);
219 e
.setFileMode(FileMode
.REGULAR_FILE
);
225 * Construct a tree from a specific listing of file entries.
228 * the files to include in the tree. The collection does not need
229 * to be sorted properly and may be empty.
230 * @return reference to the tree specified by the entry list.
233 public RevTree
tree(final DirCacheEntry
... entries
) throws Exception
{
234 final DirCache dc
= DirCache
.newInCore();
235 final DirCacheBuilder b
= dc
.builder();
236 for (final DirCacheEntry e
: entries
)
239 return pool
.lookupTree(dc
.writeTree(writer
));
243 * Lookup an entry stored in a tree, failing if not present.
246 * the tree to search.
248 * the path to find the entry of.
249 * @return the parsed object entry at this path, never null.
250 * @throws AssertionFailedError
251 * if the path does not exist in the given tree.
254 public RevObject
get(final RevTree tree
, final String path
)
255 throws AssertionFailedError
, Exception
{
256 final TreeWalk tw
= new TreeWalk(db
);
257 tw
.setFilter(PathFilterGroup
.createFromStrings(Collections
261 if (tw
.isSubtree() && !path
.equals(tw
.getPathString())) {
265 final ObjectId entid
= tw
.getObjectId(0);
266 final FileMode entmode
= tw
.getFileMode(0);
267 return pool
.lookupAny(entid
, entmode
.getObjectType());
269 Assert
.fail("Can't find " + path
+ " in tree " + tree
.name());
270 return null; // never reached.
274 * Create a new commit.
276 * See {@link #commit(int, RevTree, RevCommit...)}. The tree is the empty
277 * tree (no files or subdirectories).
280 * zero or more parents of the commit.
281 * @return the new commit.
284 public RevCommit
commit(final RevCommit
... parents
) throws Exception
{
285 return commit(1, tree(), parents
);
289 * Create a new commit.
291 * See {@link #commit(int, RevTree, RevCommit...)}.
294 * the root tree for the commit.
296 * zero or more parents of the commit.
297 * @return the new commit.
300 public RevCommit
commit(final RevTree tree
, final RevCommit
... parents
)
302 return commit(1, tree
, parents
);
306 * Create a new commit.
308 * See {@link #commit(int, RevTree, RevCommit...)}. The tree is the empty
309 * tree (no files or subdirectories).
312 * number of seconds to advance {@link #tick(int)} by.
314 * zero or more parents of the commit.
315 * @return the new commit.
318 public RevCommit
commit(final int secDelta
, final RevCommit
... parents
)
320 return commit(secDelta
, tree(), parents
);
324 * Create a new commit.
326 * The author and committer identities are stored using the current
327 * timestamp, after being incremented by {@code secDelta}. The message body
331 * number of seconds to advance {@link #tick(int)} by.
333 * the root tree for the commit.
335 * zero or more parents of the commit.
336 * @return the new commit.
339 public RevCommit
commit(final int secDelta
, final RevTree tree
,
340 final RevCommit
... parents
) throws Exception
{
343 final Commit c
= new Commit(db
);
345 c
.setParentIds(parents
);
346 c
.setAuthor(new PersonIdent(author
, new Date(now
)));
347 c
.setCommitter(new PersonIdent(committer
, new Date(now
)));
349 return pool
.lookupCommit(writer
.writeCommit(c
));
352 /** @return a new commit builder. */
353 public CommitBuilder
commit() {
354 return new CommitBuilder();
358 * Construct an annotated tag object pointing at another object.
360 * The tagger is the committer identity, at the current time as specified by
361 * {@link #tick(int)}. The time is not increased.
363 * The tag message is empty.
366 * name of the tag. Traditionally a tag name should not start
367 * with {@code refs/tags/}.
369 * object the tag should be pointed at.
370 * @return the annotated tag object.
373 public RevTag
tag(final String name
, final RevObject dst
) throws Exception
{
374 final Tag t
= new Tag(db
);
375 t
.setType(Constants
.typeString(dst
.getType()));
376 t
.setObjId(dst
.toObjectId());
378 t
.setTagger(new PersonIdent(committer
, new Date(now
)));
380 return (RevTag
) pool
.lookupAny(writer
.writeTag(t
), Constants
.OBJ_TAG
);
384 * Update a reference to point to an object.
387 * the name of the reference to update to. If {@code ref} does
388 * not start with {@code refs/} and is not the magic names
389 * {@code HEAD} {@code FETCH_HEAD} or {@code MERGE_HEAD}, then
390 * {@code refs/heads/} will be prefixed in front of the given
391 * name, thereby assuming it is a branch.
394 * @return the target object.
397 public RevCommit
update(String ref
, CommitBuilder to
) throws Exception
{
398 return update(ref
, to
.create());
402 * Update a reference to point to an object.
405 * type of the target object.
407 * the name of the reference to update to. If {@code ref} does
408 * not start with {@code refs/} and is not the magic names
409 * {@code HEAD} {@code FETCH_HEAD} or {@code MERGE_HEAD}, then
410 * {@code refs/heads/} will be prefixed in front of the given
411 * name, thereby assuming it is a branch.
414 * @return the target object.
417 public <T
extends AnyObjectId
> T
update(String ref
, T obj
) throws Exception
{
418 if (Constants
.HEAD
.equals(ref
)) {
419 } else if ("FETCH_HEAD".equals(ref
)) {
420 } else if ("MERGE_HEAD".equals(ref
)) {
421 } else if (ref
.startsWith(Constants
.R_REFS
)) {
423 ref
= Constants
.R_HEADS
+ ref
;
425 RefUpdate u
= db
.updateRef(ref
);
426 u
.setNewObjectId(obj
);
427 switch (u
.forceUpdate()) {
436 throw new IOException("Cannot write " + ref
+ " " + u
.getResult());
441 * Update the dumb client server info files.
445 public void updateServerInfo() throws Exception
{
446 final ObjectDatabase odb
= db
.getObjectDatabase();
447 if (odb
instanceof ObjectDirectory
) {
448 RefWriter rw
= new RefWriter(db
.getAllRefs().values()) {
450 protected void writeFile(final String name
, final byte[] bin
)
452 TestRepository
.this.writeFile(name
, bin
);
455 rw
.writePackedRefs();
458 final StringBuilder w
= new StringBuilder();
459 for (PackFile p
: ((ObjectDirectory
) odb
).getPacks()) {
461 w
.append(p
.getPackFile().getName());
464 writeFile("objects/info/packs", Constants
.encodeASCII(w
.toString()));
469 * Ensure the body of the given object has been parsed.
472 * type of object, e.g. {@link RevTag} or {@link RevCommit}.
474 * reference to the (possibly unparsed) object to force body
476 * @return {@code object}
479 public <T
extends RevObject
> T
parseBody(final T object
) throws Exception
{
480 pool
.parseBody(object
);
485 * Create a new branch builder for this repository.
488 * name of the branch to be constructed. If {@code ref} does not
489 * start with {@code refs/} the prefix {@code refs/heads/} will
491 * @return builder for the named branch.
493 public BranchBuilder
branch(String ref
) {
494 if (Constants
.HEAD
.equals(ref
)) {
495 } else if (ref
.startsWith(Constants
.R_REFS
)) {
497 ref
= Constants
.R_HEADS
+ ref
;
498 return new BranchBuilder(ref
);
502 * Run consistency checks against the object database.
504 * This method completes silently if the checks pass. A temporary revision
505 * pool is constructed during the checking.
508 * the tips to start checking from; if not supplied the refs of
509 * the repository are used instead.
510 * @throws MissingObjectException
511 * @throws IncorrectObjectTypeException
512 * @throws IOException
514 public void fsck(RevObject
... tips
) throws MissingObjectException
,
515 IncorrectObjectTypeException
, IOException
{
516 ObjectWalk ow
= new ObjectWalk(db
);
517 if (tips
.length
!= 0) {
518 for (RevObject o
: tips
)
519 ow
.markStart(ow
.parseAny(o
));
521 for (Ref r
: db
.getAllRefs().values())
522 ow
.markStart(ow
.parseAny(r
.getObjectId()));
525 ObjectChecker oc
= new ObjectChecker();
527 final RevCommit o
= ow
.next();
531 final byte[] bin
= db
.openObject(o
).getCachedBytes();
537 final RevObject o
= ow
.nextObject();
541 final byte[] bin
= db
.openObject(o
).getCachedBytes();
542 oc
.check(o
.getType(), bin
);
547 private static void assertHash(RevObject id
, byte[] bin
) {
548 MessageDigest md
= Constants
.newMessageDigest();
549 md
.update(Constants
.encodedTypeString(id
.getType()));
550 md
.update((byte) ' ');
551 md
.update(Constants
.encodeASCII(bin
.length
));
554 Assert
.assertEquals(id
.copy(), ObjectId
.fromRaw(md
.digest()));
558 * Pack all reachable objects in the repository into a single pack file.
560 * All loose objects are automatically pruned. Existing packs however are
565 public void packAndPrune() throws Exception
{
566 final ObjectDirectory odb
= (ObjectDirectory
) db
.getObjectDatabase();
567 final PackWriter pw
= new PackWriter(db
, NullProgressMonitor
.INSTANCE
);
569 Set
<ObjectId
> all
= new HashSet
<ObjectId
>();
570 for (Ref r
: db
.getAllRefs().values())
571 all
.add(r
.getObjectId());
572 pw
.preparePack(all
, Collections
.<ObjectId
> emptySet());
574 final ObjectId name
= pw
.computeName();
577 final File pack
= nameFor(odb
, name
, ".pack");
578 out
= new BufferedOutputStream(new FileOutputStream(pack
));
586 final File idx
= nameFor(odb
, name
, ".idx");
587 out
= new BufferedOutputStream(new FileOutputStream(idx
));
595 odb
.openPack(pack
, idx
);
600 private void prunePacked(ObjectDirectory odb
) {
601 for (PackFile p
: odb
.getPacks()) {
602 for (MutableEntry e
: p
)
603 odb
.fileFor(e
.toObjectId()).delete();
607 private static File
nameFor(ObjectDirectory odb
, ObjectId name
, String t
) {
608 File packdir
= new File(odb
.getDirectory(), "pack");
609 return new File(packdir
, "pack-" + name
.name() + t
);
612 private void writeFile(final String name
, final byte[] bin
)
613 throws IOException
, ObjectWritingException
{
614 final File p
= new File(db
.getDirectory(), name
);
615 final LockFile lck
= new LockFile(p
);
617 throw new ObjectWritingException("Can't write " + p
);
620 } catch (IOException ioe
) {
621 throw new ObjectWritingException("Can't write " + p
);
624 throw new ObjectWritingException("Can't write " + p
);
627 /** Helper to build a branch with one or more commits */
628 public class BranchBuilder
{
629 private final String ref
;
631 BranchBuilder(final String ref
) {
636 * @return construct a new commit builder that updates this branch. If
637 * the branch already exists, the commit builder will have its
638 * first parent as the current commit and its tree will be
639 * initialized to the current files.
641 * the commit builder can't read the current branch state
643 public CommitBuilder
commit() throws Exception
{
644 return new CommitBuilder(this);
648 * Forcefully update this branch to a particular commit.
651 * the commit to update to.
652 * @return {@code to}.
655 public RevCommit
update(CommitBuilder to
) throws Exception
{
656 return update(to
.create());
660 * Forcefully update this branch to a particular commit.
663 * the commit to update to.
664 * @return {@code to}.
667 public RevCommit
update(RevCommit to
) throws Exception
{
668 return TestRepository
.this.update(ref
, to
);
672 /** Helper to generate a commit. */
673 public class CommitBuilder
{
674 private final BranchBuilder branch
;
676 private final DirCache tree
= DirCache
.newInCore();
678 private final List
<RevCommit
> parents
= new ArrayList
<RevCommit
>(2);
680 private int tick
= 1;
682 private String message
= "";
684 private RevCommit self
;
690 CommitBuilder(BranchBuilder b
) throws Exception
{
693 Ref ref
= db
.getRef(branch
.ref
);
695 parent(pool
.parseCommit(ref
.getObjectId()));
699 CommitBuilder(CommitBuilder prior
) throws Exception
{
700 branch
= prior
.branch
;
702 DirCacheBuilder b
= tree
.builder();
703 for (int i
= 0; i
< prior
.tree
.getEntryCount(); i
++)
704 b
.add(prior
.tree
.getEntry(i
));
707 parents
.add(prior
.create());
710 public CommitBuilder
parent(RevCommit p
) throws Exception
{
711 if (parents
.isEmpty()) {
712 DirCacheBuilder b
= tree
.builder();
714 b
.addTree(new byte[0], DirCacheEntry
.STAGE_0
, db
, p
.getTree());
721 public CommitBuilder
noParents() {
726 public CommitBuilder
noFiles() {
731 public CommitBuilder
add(String path
, String content
) throws Exception
{
732 return add(path
, blob(content
));
735 public CommitBuilder
add(String path
, final RevBlob id
)
737 DirCacheEditor e
= tree
.editor();
738 e
.add(new PathEdit(path
) {
740 public void apply(DirCacheEntry ent
) {
741 ent
.setFileMode(FileMode
.REGULAR_FILE
);
749 public CommitBuilder
rm(String path
) {
750 DirCacheEditor e
= tree
.editor();
751 e
.add(new DeletePath(path
));
752 e
.add(new DeleteTree(path
));
757 public CommitBuilder
message(String m
) {
762 public CommitBuilder
tick(int secs
) {
767 public RevCommit
create() throws Exception
{
769 TestRepository
.this.tick(tick
);
771 final Commit c
= new Commit(db
);
772 c
.setTreeId(pool
.lookupTree(tree
.writeTree(writer
)));
773 c
.setParentIds(parents
.toArray(new RevCommit
[parents
.size()]));
774 c
.setAuthor(new PersonIdent(author
, new Date(now
)));
775 c
.setCommitter(new PersonIdent(committer
, new Date(now
)));
776 c
.setMessage(message
);
778 self
= pool
.lookupCommit(writer
.writeCommit(c
));
786 public CommitBuilder
child() throws Exception
{
787 return new CommitBuilder(this);