Reduce multi-level buffered streams in transport code
[jgit/MarioXXX.git] / org.eclipse.jgit.junit / src / org / eclipse / jgit / junit / TestRepository.java
blob59504aa7802069c1c629c1ee15d05e8575819a3b
1 /*
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
14 * conditions are met:
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
27 * written permission.
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;
47 import java.io.File;
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;
57 import java.util.Set;
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;
108 static {
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;
128 private long now;
131 * Wrap a repository with test building tools.
133 * @param db
134 * the test repository to write into.
135 * @throws Exception
137 public TestRepository(Repository db) throws Exception {
138 this(db, new RevWalk(db));
142 * Wrap a repository with test building tools.
144 * @param db
145 * the test repository to write into.
146 * @param rw
147 * the RevObject pool to use for object lookup.
148 * @throws Exception
150 public TestRepository(Repository db, RevWalk rw) throws Exception {
151 this.db = db;
152 this.pool = rw;
153 this.writer = new ObjectWriter(db);
154 this.now = 1236977987000L;
157 /** @return the repository this helper class operates against. */
158 public Repository getRepository() {
159 return db;
162 /** @return get the RevWalk pool all objects are allocated through. */
163 public RevWalk getRevWalk() {
164 return pool;
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.
175 * @param secDelta
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.
185 * @param content
186 * file content, will be UTF-8 encoded.
187 * @return reference to the blob.
188 * @throws Exception
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.
197 * @param content
198 * binary file content.
199 * @return reference to the blob.
200 * @throws Exception
202 public RevBlob blob(final byte[] content) throws Exception {
203 return pool.lookupBlob(writer.writeBlob(content));
207 * Construct a regular file mode tree entry.
209 * @param path
210 * path of the file.
211 * @param blob
212 * a blob, previously constructed in the repository.
213 * @return the entry.
214 * @throws Exception
216 public DirCacheEntry file(final String path, final RevBlob blob)
217 throws Exception {
218 final DirCacheEntry e = new DirCacheEntry(path);
219 e.setFileMode(FileMode.REGULAR_FILE);
220 e.setObjectId(blob);
221 return e;
225 * Construct a tree from a specific listing of file entries.
227 * @param 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.
231 * @throws Exception
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)
237 b.add(e);
238 b.finish();
239 return pool.lookupTree(dc.writeTree(writer));
243 * Lookup an entry stored in a tree, failing if not present.
245 * @param tree
246 * the tree to search.
247 * @param path
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.
252 * @throws Exception
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
258 .singleton(path)));
259 tw.reset(tree);
260 while (tw.next()) {
261 if (tw.isSubtree() && !path.equals(tw.getPathString())) {
262 tw.enterSubtree();
263 continue;
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.
275 * <p>
276 * See {@link #commit(int, RevTree, RevCommit...)}. The tree is the empty
277 * tree (no files or subdirectories).
279 * @param parents
280 * zero or more parents of the commit.
281 * @return the new commit.
282 * @throws Exception
284 public RevCommit commit(final RevCommit... parents) throws Exception {
285 return commit(1, tree(), parents);
289 * Create a new commit.
290 * <p>
291 * See {@link #commit(int, RevTree, RevCommit...)}.
293 * @param tree
294 * the root tree for the commit.
295 * @param parents
296 * zero or more parents of the commit.
297 * @return the new commit.
298 * @throws Exception
300 public RevCommit commit(final RevTree tree, final RevCommit... parents)
301 throws Exception {
302 return commit(1, tree, parents);
306 * Create a new commit.
307 * <p>
308 * See {@link #commit(int, RevTree, RevCommit...)}. The tree is the empty
309 * tree (no files or subdirectories).
311 * @param secDelta
312 * number of seconds to advance {@link #tick(int)} by.
313 * @param parents
314 * zero or more parents of the commit.
315 * @return the new commit.
316 * @throws Exception
318 public RevCommit commit(final int secDelta, final RevCommit... parents)
319 throws Exception {
320 return commit(secDelta, tree(), parents);
324 * Create a new commit.
325 * <p>
326 * The author and committer identities are stored using the current
327 * timestamp, after being incremented by {@code secDelta}. The message body
328 * is empty.
330 * @param secDelta
331 * number of seconds to advance {@link #tick(int)} by.
332 * @param tree
333 * the root tree for the commit.
334 * @param parents
335 * zero or more parents of the commit.
336 * @return the new commit.
337 * @throws Exception
339 public RevCommit commit(final int secDelta, final RevTree tree,
340 final RevCommit... parents) throws Exception {
341 tick(secDelta);
343 final Commit c = new Commit(db);
344 c.setTreeId(tree);
345 c.setParentIds(parents);
346 c.setAuthor(new PersonIdent(author, new Date(now)));
347 c.setCommitter(new PersonIdent(committer, new Date(now)));
348 c.setMessage("");
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.
359 * <p>
360 * The tagger is the committer identity, at the current time as specified by
361 * {@link #tick(int)}. The time is not increased.
362 * <p>
363 * The tag message is empty.
365 * @param name
366 * name of the tag. Traditionally a tag name should not start
367 * with {@code refs/tags/}.
368 * @param dst
369 * object the tag should be pointed at.
370 * @return the annotated tag object.
371 * @throws Exception
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());
377 t.setTag(name);
378 t.setTagger(new PersonIdent(committer, new Date(now)));
379 t.setMessage("");
380 return (RevTag) pool.lookupAny(writer.writeTag(t), Constants.OBJ_TAG);
384 * Update a reference to point to an object.
386 * @param ref
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.
392 * @param to
393 * the target object.
394 * @return the target object.
395 * @throws Exception
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.
404 * @param <T>
405 * type of the target object.
406 * @param ref
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.
412 * @param obj
413 * the target object.
414 * @return the target object.
415 * @throws Exception
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)) {
422 } else
423 ref = Constants.R_HEADS + ref;
425 RefUpdate u = db.updateRef(ref);
426 u.setNewObjectId(obj);
427 switch (u.forceUpdate()) {
428 case FAST_FORWARD:
429 case FORCED:
430 case NEW:
431 case NO_CHANGE:
432 updateServerInfo();
433 return obj;
435 default:
436 throw new IOException("Cannot write " + ref + " " + u.getResult());
441 * Update the dumb client server info files.
443 * @throws Exception
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()) {
449 @Override
450 protected void writeFile(final String name, final byte[] bin)
451 throws IOException {
452 TestRepository.this.writeFile(name, bin);
455 rw.writePackedRefs();
456 rw.writeInfoRefs();
458 final StringBuilder w = new StringBuilder();
459 for (PackFile p : ((ObjectDirectory) odb).getPacks()) {
460 w.append("P ");
461 w.append(p.getPackFile().getName());
462 w.append('\n');
464 writeFile("objects/info/packs", Constants.encodeASCII(w.toString()));
469 * Ensure the body of the given object has been parsed.
471 * @param <T>
472 * type of object, e.g. {@link RevTag} or {@link RevCommit}.
473 * @param object
474 * reference to the (possibly unparsed) object to force body
475 * parsing of.
476 * @return {@code object}
477 * @throws Exception
479 public <T extends RevObject> T parseBody(final T object) throws Exception {
480 pool.parseBody(object);
481 return object;
485 * Create a new branch builder for this repository.
487 * @param ref
488 * name of the branch to be constructed. If {@code ref} does not
489 * start with {@code refs/} the prefix {@code refs/heads/} will
490 * be added.
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)) {
496 } else
497 ref = Constants.R_HEADS + ref;
498 return new BranchBuilder(ref);
502 * Run consistency checks against the object database.
503 * <p>
504 * This method completes silently if the checks pass. A temporary revision
505 * pool is constructed during the checking.
507 * @param tips
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));
520 } else {
521 for (Ref r : db.getAllRefs().values())
522 ow.markStart(ow.parseAny(r.getObjectId()));
525 ObjectChecker oc = new ObjectChecker();
526 for (;;) {
527 final RevCommit o = ow.next();
528 if (o == null)
529 break;
531 final byte[] bin = db.openObject(o).getCachedBytes();
532 oc.checkCommit(bin);
533 assertHash(o, bin);
536 for (;;) {
537 final RevObject o = ow.nextObject();
538 if (o == null)
539 break;
541 final byte[] bin = db.openObject(o).getCachedBytes();
542 oc.check(o.getType(), bin);
543 assertHash(o, 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));
552 md.update((byte) 0);
553 md.update(bin);
554 Assert.assertEquals(id.copy(), ObjectId.fromRaw(md.digest()));
558 * Pack all reachable objects in the repository into a single pack file.
559 * <p>
560 * All loose objects are automatically pruned. Existing packs however are
561 * not removed.
563 * @throws Exception
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();
575 OutputStream out;
577 final File pack = nameFor(odb, name, ".pack");
578 out = new BufferedOutputStream(new FileOutputStream(pack));
579 try {
580 pw.writePack(out);
581 } finally {
582 out.close();
584 pack.setReadOnly();
586 final File idx = nameFor(odb, name, ".idx");
587 out = new BufferedOutputStream(new FileOutputStream(idx));
588 try {
589 pw.writeIndex(out);
590 } finally {
591 out.close();
593 idx.setReadOnly();
595 odb.openPack(pack, idx);
596 updateServerInfo();
597 prunePacked(odb);
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);
616 if (!lck.lock())
617 throw new ObjectWritingException("Can't write " + p);
618 try {
619 lck.write(bin);
620 } catch (IOException ioe) {
621 throw new ObjectWritingException("Can't write " + p);
623 if (!lck.commit())
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) {
632 this.ref = 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.
640 * @throws Exception
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.
650 * @param to
651 * the commit to update to.
652 * @return {@code to}.
653 * @throws Exception
655 public RevCommit update(CommitBuilder to) throws Exception {
656 return update(to.create());
660 * Forcefully update this branch to a particular commit.
662 * @param to
663 * the commit to update to.
664 * @return {@code to}.
665 * @throws Exception
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;
686 CommitBuilder() {
687 branch = null;
690 CommitBuilder(BranchBuilder b) throws Exception {
691 branch = b;
693 Ref ref = db.getRef(branch.ref);
694 if (ref != null) {
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));
705 b.finish();
707 parents.add(prior.create());
710 public CommitBuilder parent(RevCommit p) throws Exception {
711 if (parents.isEmpty()) {
712 DirCacheBuilder b = tree.builder();
713 parseBody(p);
714 b.addTree(new byte[0], DirCacheEntry.STAGE_0, db, p.getTree());
715 b.finish();
717 parents.add(p);
718 return this;
721 public CommitBuilder noParents() {
722 parents.clear();
723 return this;
726 public CommitBuilder noFiles() {
727 tree.clear();
728 return this;
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)
736 throws Exception {
737 DirCacheEditor e = tree.editor();
738 e.add(new PathEdit(path) {
739 @Override
740 public void apply(DirCacheEntry ent) {
741 ent.setFileMode(FileMode.REGULAR_FILE);
742 ent.setObjectId(id);
745 e.finish();
746 return this;
749 public CommitBuilder rm(String path) {
750 DirCacheEditor e = tree.editor();
751 e.add(new DeletePath(path));
752 e.add(new DeleteTree(path));
753 e.finish();
754 return this;
757 public CommitBuilder message(String m) {
758 message = m;
759 return this;
762 public CommitBuilder tick(int secs) {
763 tick = secs;
764 return this;
767 public RevCommit create() throws Exception {
768 if (self == null) {
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));
780 if (branch != null)
781 branch.update(self);
783 return self;
786 public CommitBuilder child() throws Exception {
787 return new CommitBuilder(this);