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