1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 package org.eclipse.jgit.merge;
44
45 import static java.nio.charset.StandardCharsets.UTF_8;
46 import static java.time.Instant.EPOCH;
47 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
48 import static org.junit.Assert.assertEquals;
49 import static org.junit.Assert.assertFalse;
50 import static org.junit.Assert.assertNotNull;
51 import static org.junit.Assert.assertNull;
52 import static org.junit.Assert.assertTrue;
53
54 import java.io.ByteArrayOutputStream;
55 import java.io.File;
56 import java.io.FileInputStream;
57 import java.io.IOException;
58 import java.time.Instant;
59 import java.util.Arrays;
60 import java.util.Map;
61
62 import org.eclipse.jgit.api.Git;
63 import org.eclipse.jgit.api.MergeResult;
64 import org.eclipse.jgit.api.MergeResult.MergeStatus;
65 import org.eclipse.jgit.api.RebaseResult;
66 import org.eclipse.jgit.api.errors.CheckoutConflictException;
67 import org.eclipse.jgit.api.errors.GitAPIException;
68 import org.eclipse.jgit.api.errors.JGitInternalException;
69 import org.eclipse.jgit.dircache.DirCache;
70 import org.eclipse.jgit.dircache.DirCacheEditor;
71 import org.eclipse.jgit.dircache.DirCacheEntry;
72 import org.eclipse.jgit.errors.ConfigInvalidException;
73 import org.eclipse.jgit.errors.NoMergeBaseException;
74 import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
75 import org.eclipse.jgit.junit.RepositoryTestCase;
76 import org.eclipse.jgit.junit.TestRepository;
77 import org.eclipse.jgit.lib.AnyObjectId;
78 import org.eclipse.jgit.lib.ConfigConstants;
79 import org.eclipse.jgit.lib.Constants;
80 import org.eclipse.jgit.lib.FileMode;
81 import org.eclipse.jgit.lib.ObjectId;
82 import org.eclipse.jgit.lib.ObjectInserter;
83 import org.eclipse.jgit.lib.ObjectLoader;
84 import org.eclipse.jgit.lib.ObjectReader;
85 import org.eclipse.jgit.lib.ObjectStream;
86 import org.eclipse.jgit.lib.StoredConfig;
87 import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
88 import org.eclipse.jgit.revwalk.RevCommit;
89 import org.eclipse.jgit.revwalk.RevObject;
90 import org.eclipse.jgit.revwalk.RevTree;
91 import org.eclipse.jgit.revwalk.RevWalk;
92 import org.eclipse.jgit.storage.file.FileBasedConfig;
93 import org.eclipse.jgit.treewalk.FileTreeIterator;
94 import org.eclipse.jgit.util.FS;
95 import org.eclipse.jgit.util.FileUtils;
96 import org.junit.Assert;
97 import org.junit.experimental.theories.DataPoints;
98 import org.junit.experimental.theories.Theories;
99 import org.junit.experimental.theories.Theory;
100 import org.junit.runner.RunWith;
101
102 @RunWith(Theories.class)
103 public class MergerTest extends RepositoryTestCase {
104
105 @DataPoints
106 public static MergeStrategy[] strategiesUnderTest = new MergeStrategy[] {
107 MergeStrategy.RECURSIVE, MergeStrategy.RESOLVE };
108
109 @Theory
110 public void failingDeleteOfDirectoryWithUntrackedContent(
111 MergeStrategy strategy) throws Exception {
112 File folder1 = new File(db.getWorkTree(), "folder1");
113 FileUtils.mkdir(folder1);
114 File file = new File(folder1, "file1.txt");
115 write(file, "folder1--file1.txt");
116 file = new File(folder1, "file2.txt");
117 write(file, "folder1--file2.txt");
118
119 try (Git git = new Git(db)) {
120 git.add().addFilepattern(folder1.getName()).call();
121 RevCommit base = git.commit().setMessage("adding folder").call();
122
123 recursiveDelete(folder1);
124 git.rm().addFilepattern("folder1/file1.txt")
125 .addFilepattern("folder1/file2.txt").call();
126 RevCommit other = git.commit()
127 .setMessage("removing folders on 'other'").call();
128
129 git.checkout().setName(base.name()).call();
130
131 file = new File(db.getWorkTree(), "unrelated.txt");
132 write(file, "unrelated");
133
134 git.add().addFilepattern("unrelated.txt").call();
135 RevCommit head = git.commit().setMessage("Adding another file").call();
136
137
138
139 file = new File(folder1, "file3.txt");
140 write(file, "folder1--file3.txt");
141
142 ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, false);
143 merger.setCommitNames(new String[] { "BASE", "HEAD", "other" });
144 merger.setWorkingTreeIterator(new FileTreeIterator(db));
145 boolean ok = merger.merge(head.getId(), other.getId());
146 assertTrue(ok);
147 assertTrue(file.exists());
148 }
149 }
150
151
152
153
154
155
156
157
158 @Theory
159 public void checkMergeConflictingTreesWithoutIndex(MergeStrategy strategy)
160 throws Exception {
161 Git git = Git.wrap(db);
162
163 writeTrashFile("d/1", "orig");
164 git.add().addFilepattern("d/1").call();
165 RevCommit first = git.commit().setMessage("added d/1").call();
166
167 writeTrashFile("d/1", "master");
168 RevCommit masterCommit = git.commit().setAll(true)
169 .setMessage("modified d/1 on master").call();
170
171 git.checkout().setCreateBranch(true).setStartPoint(first)
172 .setName("side").call();
173 writeTrashFile("d/1", "side");
174 git.commit().setAll(true).setMessage("modified d/1 on side").call();
175
176 git.rm().addFilepattern("d/1").call();
177 git.rm().addFilepattern("d").call();
178 MergeResult mergeRes = git.merge().setStrategy(strategy)
179 .include(masterCommit).call();
180 assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
181 assertEquals(
182 "[d/1, mode:100644, stage:1, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
183 indexState(CONTENT));
184 }
185
186
187
188
189
190
191
192
193 @Theory
194 public void checkMergeMergeableTreesWithoutIndex(MergeStrategy strategy)
195 throws Exception {
196 Git git = Git.wrap(db);
197
198 writeTrashFile("d/1", "1\n2\n3");
199 git.add().addFilepattern("d/1").call();
200 RevCommit first = git.commit().setMessage("added d/1").call();
201
202 writeTrashFile("d/1", "1master\n2\n3");
203 RevCommit masterCommit = git.commit().setAll(true)
204 .setMessage("modified d/1 on master").call();
205
206 git.checkout().setCreateBranch(true).setStartPoint(first)
207 .setName("side").call();
208 writeTrashFile("d/1", "1\n2\n3side");
209 git.commit().setAll(true).setMessage("modified d/1 on side").call();
210
211 git.rm().addFilepattern("d/1").call();
212 git.rm().addFilepattern("d").call();
213 MergeResult mergeRes = git.merge().setStrategy(strategy)
214 .include(masterCommit).call();
215 assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
216 assertEquals("[d/1, mode:100644, content:1master\n2\n3side]",
217 indexState(CONTENT));
218 }
219
220
221
222
223
224
225
226
227 @Theory
228 public void checkUntrackedFolderIsNotAConflict(
229 MergeStrategy strategy) throws Exception {
230 Git git = Git.wrap(db);
231
232 writeTrashFile("d/1", "1");
233 git.add().addFilepattern("d/1").call();
234 RevCommit first = git.commit().setMessage("added d/1").call();
235
236 writeTrashFile("e/1", "4");
237 git.add().addFilepattern("e/1").call();
238 RevCommit masterCommit = git.commit().setMessage("added e/1").call();
239
240 git.checkout().setCreateBranch(true).setStartPoint(first)
241 .setName("side").call();
242 writeTrashFile("f/1", "5");
243 git.add().addFilepattern("f/1").call();
244 git.commit().setAll(true).setMessage("added f/1")
245 .call();
246
247
248 writeTrashFile("e/2", "d two");
249
250 MergeResult mergeRes = git.merge().setStrategy(strategy)
251 .include(masterCommit).call();
252 assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
253 assertEquals(
254 "[d/1, mode:100644, content:1][e/1, mode:100644, content:4][f/1, mode:100644, content:5]",
255 indexState(CONTENT));
256 }
257
258
259
260
261
262
263
264 @Theory
265 public void checkFileReplacedByFolderInTheirs(MergeStrategy strategy)
266 throws Exception {
267 Git git = Git.wrap(db);
268
269 writeTrashFile("sub", "file");
270 git.add().addFilepattern("sub").call();
271 RevCommit first = git.commit().setMessage("initial").call();
272
273 git.checkout().setCreateBranch(true).setStartPoint(first)
274 .setName("side").call();
275
276 git.rm().addFilepattern("sub").call();
277 writeTrashFile("sub/file", "subfile");
278 git.add().addFilepattern("sub/file").call();
279 RevCommit masterCommit = git.commit().setMessage("file -> folder")
280 .call();
281
282 git.checkout().setName("master").call();
283 writeTrashFile("noop", "other");
284 git.add().addFilepattern("noop").call();
285 git.commit().setAll(true).setMessage("noop").call();
286
287 MergeResult mergeRes = git.merge().setStrategy(strategy)
288 .include(masterCommit).call();
289 assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
290 assertEquals(
291 "[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
292 indexState(CONTENT));
293 }
294
295
296
297
298
299
300
301 @Theory
302 public void checkFileReplacedByFolderInOurs(MergeStrategy strategy)
303 throws Exception {
304 Git git = Git.wrap(db);
305
306 writeTrashFile("sub", "file");
307 git.add().addFilepattern("sub").call();
308 RevCommit first = git.commit().setMessage("initial").call();
309
310 git.checkout().setCreateBranch(true).setStartPoint(first)
311 .setName("side").call();
312 writeTrashFile("noop", "other");
313 git.add().addFilepattern("noop").call();
314 RevCommit sideCommit = git.commit().setAll(true).setMessage("noop")
315 .call();
316
317 git.checkout().setName("master").call();
318 git.rm().addFilepattern("sub").call();
319 writeTrashFile("sub/file", "subfile");
320 git.add().addFilepattern("sub/file").call();
321 git.commit().setMessage("file -> folder")
322 .call();
323
324 MergeResult mergeRes = git.merge().setStrategy(strategy)
325 .include(sideCommit).call();
326 assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
327 assertEquals(
328 "[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
329 indexState(CONTENT));
330 }
331
332
333
334
335
336
337
338
339 @Theory
340 public void checkUntrackedEmpytFolderIsNotAConflictWithFile(
341 MergeStrategy strategy)
342 throws Exception {
343 Git git = Git.wrap(db);
344
345 writeTrashFile("d/1", "1");
346 git.add().addFilepattern("d/1").call();
347 RevCommit first = git.commit().setMessage("added d/1").call();
348
349 writeTrashFile("e", "4");
350 git.add().addFilepattern("e").call();
351 RevCommit masterCommit = git.commit().setMessage("added e").call();
352
353 git.checkout().setCreateBranch(true).setStartPoint(first)
354 .setName("side").call();
355 writeTrashFile("f/1", "5");
356 git.add().addFilepattern("f/1").call();
357 git.commit().setAll(true).setMessage("added f/1").call();
358
359
360
361 FileUtils.mkdirs(new File(trash, "e/1"), true);
362
363 MergeResult mergeRes = git.merge().setStrategy(strategy)
364 .include(masterCommit).call();
365 assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
366 assertEquals(
367 "[d/1, mode:100644, content:1][e, mode:100644, content:4][f/1, mode:100644, content:5]",
368 indexState(CONTENT));
369 }
370
371 @Theory
372 public void mergeWithCrlfInWT(MergeStrategy strategy) throws IOException,
373 GitAPIException {
374 Git git = Git.wrap(db);
375 db.getConfig().setString("core", null, "autocrlf", "false");
376 db.getConfig().save();
377 writeTrashFile("crlf.txt", "some\r\ndata\r\n");
378 git.add().addFilepattern("crlf.txt").call();
379 git.commit().setMessage("base").call();
380
381 git.branchCreate().setName("brancha").call();
382
383 writeTrashFile("crlf.txt", "some\r\nmore\r\ndata\r\n");
384 git.add().addFilepattern("crlf.txt").call();
385 git.commit().setMessage("on master").call();
386
387 git.checkout().setName("brancha").call();
388 writeTrashFile("crlf.txt", "some\r\ndata\r\ntoo\r\n");
389 git.add().addFilepattern("crlf.txt").call();
390 git.commit().setMessage("on brancha").call();
391
392 db.getConfig().setString("core", null, "autocrlf", "input");
393 db.getConfig().save();
394
395 MergeResult mergeResult = git.merge().setStrategy(strategy)
396 .include(db.resolve("master"))
397 .call();
398 assertEquals(MergeResult.MergeStatus.MERGED,
399 mergeResult.getMergeStatus());
400 }
401
402 @Theory
403 public void mergeWithCrlfAutoCrlfTrue(MergeStrategy strategy)
404 throws IOException, GitAPIException {
405 Git git = Git.wrap(db);
406 db.getConfig().setString("core", null, "autocrlf", "true");
407 db.getConfig().save();
408 writeTrashFile("crlf.txt", "a crlf file\r\n");
409 git.add().addFilepattern("crlf.txt").call();
410 git.commit().setMessage("base").call();
411
412 git.branchCreate().setName("brancha").call();
413
414 writeTrashFile("crlf.txt", "a crlf file\r\na second line\r\n");
415 git.add().addFilepattern("crlf.txt").call();
416 git.commit().setMessage("on master").call();
417
418 git.checkout().setName("brancha").call();
419 File testFile = writeTrashFile("crlf.txt",
420 "a first line\r\na crlf file\r\n");
421 git.add().addFilepattern("crlf.txt").call();
422 git.commit().setMessage("on brancha").call();
423
424 MergeResult mergeResult = git.merge().setStrategy(strategy)
425 .include(db.resolve("master")).call();
426 assertEquals(MergeResult.MergeStatus.MERGED,
427 mergeResult.getMergeStatus());
428 checkFile(testFile, "a first line\r\na crlf file\r\na second line\r\n");
429 assertEquals(
430 "[crlf.txt, mode:100644, content:a first line\na crlf file\na second line\n]",
431 indexState(CONTENT));
432 }
433
434 @Theory
435 public void rebaseWithCrlfAutoCrlfTrue(MergeStrategy strategy)
436 throws IOException, GitAPIException {
437 Git git = Git.wrap(db);
438 db.getConfig().setString("core", null, "autocrlf", "true");
439 db.getConfig().save();
440 writeTrashFile("crlf.txt", "line 1\r\nline 2\r\nline 3\r\n");
441 git.add().addFilepattern("crlf.txt").call();
442 RevCommit first = git.commit().setMessage("base").call();
443
444 git.checkout().setCreateBranch(true).setStartPoint(first)
445 .setName("brancha").call();
446
447 File testFile = writeTrashFile("crlf.txt",
448 "line 1\r\nmodified line\r\nline 3\r\n");
449 git.add().addFilepattern("crlf.txt").call();
450 git.commit().setMessage("on brancha").call();
451
452 git.checkout().setName("master").call();
453 File otherFile = writeTrashFile("otherfile.txt", "a line\r\n");
454 git.add().addFilepattern("otherfile.txt").call();
455 git.commit().setMessage("on master").call();
456
457 git.checkout().setName("brancha").call();
458 checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n");
459 assertFalse(otherFile.exists());
460
461 RebaseResult rebaseResult = git.rebase().setStrategy(strategy)
462 .setUpstream(db.resolve("master")).call();
463 assertEquals(RebaseResult.Status.OK, rebaseResult.getStatus());
464 checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n");
465 checkFile(otherFile, "a line\r\n");
466 assertEquals(
467 "[crlf.txt, mode:100644, content:line 1\nmodified line\nline 3\n]"
468 + "[otherfile.txt, mode:100644, content:a line\n]",
469 indexState(CONTENT));
470 }
471
472
473
474
475
476
477
478
479 @Theory
480 public void checkMergeEqualTreesWithoutIndex(MergeStrategy strategy)
481 throws Exception {
482 Git git = Git.wrap(db);
483
484 writeTrashFile("d/1", "orig");
485 git.add().addFilepattern("d/1").call();
486 RevCommit first = git.commit().setMessage("added d/1").call();
487
488 writeTrashFile("d/1", "modified");
489 RevCommit masterCommit = git.commit().setAll(true)
490 .setMessage("modified d/1 on master").call();
491
492 git.checkout().setCreateBranch(true).setStartPoint(first)
493 .setName("side").call();
494 writeTrashFile("d/1", "modified");
495 git.commit().setAll(true).setMessage("modified d/1 on side").call();
496
497 git.rm().addFilepattern("d/1").call();
498 git.rm().addFilepattern("d").call();
499 MergeResult mergeRes = git.merge().setStrategy(strategy)
500 .include(masterCommit).call();
501 assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
502 assertEquals("[d/1, mode:100644, content:modified]",
503 indexState(CONTENT));
504 }
505
506
507
508
509
510
511
512
513 @Theory
514 public void checkMergeEqualTreesInCore(MergeStrategy strategy)
515 throws Exception {
516 Git git = Git.wrap(db);
517
518 writeTrashFile("d/1", "orig");
519 git.add().addFilepattern("d/1").call();
520 RevCommit first = git.commit().setMessage("added d/1").call();
521
522 writeTrashFile("d/1", "modified");
523 RevCommit masterCommit = git.commit().setAll(true)
524 .setMessage("modified d/1 on master").call();
525
526 git.checkout().setCreateBranch(true).setStartPoint(first)
527 .setName("side").call();
528 writeTrashFile("d/1", "modified");
529 RevCommit sideCommit = git.commit().setAll(true)
530 .setMessage("modified d/1 on side").call();
531
532 git.rm().addFilepattern("d/1").call();
533 git.rm().addFilepattern("d").call();
534
535 ThreeWayMerger resolveMerger = (ThreeWayMerger) strategy.newMerger(db,
536 true);
537 boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
538 assertTrue(noProblems);
539 }
540
541
542
543
544
545
546
547
548 @Theory
549 public void checkMergeEqualTreesInCore_noRepo(MergeStrategy strategy)
550 throws Exception {
551 Git git = Git.wrap(db);
552
553 writeTrashFile("d/1", "orig");
554 git.add().addFilepattern("d/1").call();
555 RevCommit first = git.commit().setMessage("added d/1").call();
556
557 writeTrashFile("d/1", "modified");
558 RevCommit masterCommit = git.commit().setAll(true)
559 .setMessage("modified d/1 on master").call();
560
561 git.checkout().setCreateBranch(true).setStartPoint(first)
562 .setName("side").call();
563 writeTrashFile("d/1", "modified");
564 RevCommit sideCommit = git.commit().setAll(true)
565 .setMessage("modified d/1 on side").call();
566
567 git.rm().addFilepattern("d/1").call();
568 git.rm().addFilepattern("d").call();
569
570 try (ObjectInserter ins = db.newObjectInserter()) {
571 ThreeWayMerger resolveMerger =
572 (ThreeWayMerger) strategy.newMerger(ins, db.getConfig());
573 boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
574 assertTrue(noProblems);
575 }
576 }
577
578
579
580
581
582
583
584
585 @Theory
586 public void checkMergeEqualNewTrees(MergeStrategy strategy)
587 throws Exception {
588 Git git = Git.wrap(db);
589
590 writeTrashFile("2", "orig");
591 git.add().addFilepattern("2").call();
592 RevCommit first = git.commit().setMessage("added 2").call();
593
594 writeTrashFile("d/1", "orig");
595 git.add().addFilepattern("d/1").call();
596 RevCommit masterCommit = git.commit().setAll(true)
597 .setMessage("added d/1 on master").call();
598
599 git.checkout().setCreateBranch(true).setStartPoint(first)
600 .setName("side").call();
601 writeTrashFile("d/1", "orig");
602 git.add().addFilepattern("d/1").call();
603 git.commit().setAll(true).setMessage("added d/1 on side").call();
604
605 git.rm().addFilepattern("d/1").call();
606 git.rm().addFilepattern("d").call();
607 MergeResult mergeRes = git.merge().setStrategy(strategy)
608 .include(masterCommit).call();
609 assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
610 assertEquals(
611 "[2, mode:100644, content:orig][d/1, mode:100644, content:orig]",
612 indexState(CONTENT));
613 }
614
615
616
617
618
619
620
621
622 @Theory
623 public void checkMergeConflictingNewTrees(MergeStrategy strategy)
624 throws Exception {
625 Git git = Git.wrap(db);
626
627 writeTrashFile("2", "orig");
628 git.add().addFilepattern("2").call();
629 RevCommit first = git.commit().setMessage("added 2").call();
630
631 writeTrashFile("d/1", "master");
632 git.add().addFilepattern("d/1").call();
633 RevCommit masterCommit = git.commit().setAll(true)
634 .setMessage("added d/1 on master").call();
635
636 git.checkout().setCreateBranch(true).setStartPoint(first)
637 .setName("side").call();
638 writeTrashFile("d/1", "side");
639 git.add().addFilepattern("d/1").call();
640 git.commit().setAll(true).setMessage("added d/1 on side").call();
641
642 git.rm().addFilepattern("d/1").call();
643 git.rm().addFilepattern("d").call();
644 MergeResult mergeRes = git.merge().setStrategy(strategy)
645 .include(masterCommit).call();
646 assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
647 assertEquals(
648 "[2, mode:100644, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
649 indexState(CONTENT));
650 }
651
652
653
654
655
656
657
658
659 @Theory
660 public void checkMergeConflictingFilesWithTreeInIndex(MergeStrategy strategy)
661 throws Exception {
662 Git git = Git.wrap(db);
663
664 writeTrashFile("0", "orig");
665 git.add().addFilepattern("0").call();
666 RevCommit first = git.commit().setMessage("added 0").call();
667
668 writeTrashFile("0", "master");
669 RevCommit masterCommit = git.commit().setAll(true)
670 .setMessage("modified 0 on master").call();
671
672 git.checkout().setCreateBranch(true).setStartPoint(first)
673 .setName("side").call();
674 writeTrashFile("0", "side");
675 git.commit().setAll(true).setMessage("modified 0 on side").call();
676
677 git.rm().addFilepattern("0").call();
678 writeTrashFile("0/0", "side");
679 git.add().addFilepattern("0/0").call();
680 MergeResult mergeRes = git.merge().setStrategy(strategy)
681 .include(masterCommit).call();
682 assertEquals(MergeStatus.FAILED, mergeRes.getMergeStatus());
683 }
684
685
686
687
688
689
690
691
692 @Theory
693 public void checkMergeMergeableFilesWithTreeInIndex(MergeStrategy strategy)
694 throws Exception {
695 Git git = Git.wrap(db);
696
697 writeTrashFile("0", "orig");
698 writeTrashFile("1", "1\n2\n3");
699 git.add().addFilepattern("0").addFilepattern("1").call();
700 RevCommit first = git.commit().setMessage("added 0, 1").call();
701
702 writeTrashFile("1", "1master\n2\n3");
703 RevCommit masterCommit = git.commit().setAll(true)
704 .setMessage("modified 1 on master").call();
705
706 git.checkout().setCreateBranch(true).setStartPoint(first)
707 .setName("side").call();
708 writeTrashFile("1", "1\n2\n3side");
709 git.commit().setAll(true).setMessage("modified 1 on side").call();
710
711 git.rm().addFilepattern("0").call();
712 writeTrashFile("0/0", "modified");
713 git.add().addFilepattern("0/0").call();
714 try {
715 git.merge().setStrategy(strategy).include(masterCommit).call();
716 Assert.fail("Didn't get the expected exception");
717 } catch (CheckoutConflictException e) {
718 assertEquals(1, e.getConflictingPaths().size());
719 assertEquals("0/0", e.getConflictingPaths().get(0));
720 }
721 }
722
723 @Theory
724 public void checkContentMergeNoConflict(MergeStrategy strategy)
725 throws Exception {
726 Git git = Git.wrap(db);
727
728 writeTrashFile("file", "1\n2\n3");
729 git.add().addFilepattern("file").call();
730 RevCommit first = git.commit().setMessage("added file").call();
731
732 writeTrashFile("file", "1master\n2\n3");
733 git.commit().setAll(true).setMessage("modified file on master").call();
734
735 git.checkout().setCreateBranch(true).setStartPoint(first)
736 .setName("side").call();
737 writeTrashFile("file", "1\n2\n3side");
738 RevCommit sideCommit = git.commit().setAll(true)
739 .setMessage("modified file on side").call();
740
741 git.checkout().setName("master").call();
742 MergeResult result =
743 git.merge().setStrategy(strategy).include(sideCommit).call();
744 assertEquals(MergeStatus.MERGED, result.getMergeStatus());
745 String expected = "1master\n2\n3side";
746 assertEquals(expected, read("file"));
747 }
748
749 @Theory
750 public void checkContentMergeNoConflict_noRepo(MergeStrategy strategy)
751 throws Exception {
752 Git git = Git.wrap(db);
753
754 writeTrashFile("file", "1\n2\n3");
755 git.add().addFilepattern("file").call();
756 RevCommit first = git.commit().setMessage("added file").call();
757
758 writeTrashFile("file", "1master\n2\n3");
759 RevCommit masterCommit = git.commit().setAll(true)
760 .setMessage("modified file on master").call();
761
762 git.checkout().setCreateBranch(true).setStartPoint(first)
763 .setName("side").call();
764 writeTrashFile("file", "1\n2\n3side");
765 RevCommit sideCommit = git.commit().setAll(true)
766 .setMessage("modified file on side").call();
767
768 try (ObjectInserter ins = db.newObjectInserter()) {
769 ResolveMerger merger =
770 (ResolveMerger) strategy.newMerger(ins, db.getConfig());
771 boolean noProblems = merger.merge(masterCommit, sideCommit);
772 assertTrue(noProblems);
773 assertEquals("1master\n2\n3side",
774 readBlob(merger.getResultTreeId(), "file"));
775 }
776 }
777
778
779
780
781
782
783
784
785 @Theory
786 public void checkContentMergeLargeBinaries(MergeStrategy strategy) throws Exception {
787 Git git = Git.wrap(db);
788 final int LINELEN = 72;
789
790
791
792 byte[] binary = new byte[LINELEN * 2000];
793 for (int i = 0; i < binary.length; i++) {
794 binary[i] = (byte)((i % LINELEN) == 0 ? '\n' : 'x');
795 }
796 binary[50] = '\0';
797
798 writeTrashFile("file", new String(binary, UTF_8));
799 git.add().addFilepattern("file").call();
800 RevCommit first = git.commit().setMessage("added file").call();
801
802
803 int idx = LINELEN * 1200 + 1;
804 byte save = binary[idx];
805 binary[idx] = '@';
806 writeTrashFile("file", new String(binary, UTF_8));
807
808 binary[idx] = save;
809 git.add().addFilepattern("file").call();
810 RevCommit masterCommit = git.commit().setAll(true)
811 .setMessage("modified file l 1200").call();
812
813 git.checkout().setCreateBranch(true).setStartPoint(first).setName("side").call();
814 binary[LINELEN * 1500 + 1] = '!';
815 writeTrashFile("file", new String(binary, UTF_8));
816 git.add().addFilepattern("file").call();
817 RevCommit sideCommit = git.commit().setAll(true)
818 .setMessage("modified file l 1500").call();
819
820 try (ObjectInserter ins = db.newObjectInserter()) {
821
822 ObjectInserter forbidInserter = new ObjectInserter.Filter() {
823 @Override
824 protected ObjectInserter delegate() {
825 return ins;
826 }
827
828 @Override
829 public ObjectReader newReader() {
830 return new BigReadForbiddenReader(super.newReader(), 8000);
831 }
832 };
833
834 ResolveMerger merger =
835 (ResolveMerger) strategy.newMerger(forbidInserter, db.getConfig());
836 boolean noProblems = merger.merge(masterCommit, sideCommit);
837 assertFalse(noProblems);
838 }
839 }
840
841
842
843
844 static class BigReadForbiddenStream extends ObjectStream.Filter {
845 int limit;
846
847 BigReadForbiddenStream(ObjectStream orig, int limit) {
848 super(orig.getType(), orig.getSize(), orig);
849 this.limit = limit;
850 }
851
852 @Override
853 public long skip(long n) throws IOException {
854 limit -= n;
855 if (limit < 0) {
856 throw new IllegalStateException();
857 }
858
859 return super.skip(n);
860 }
861
862 @Override
863 public int read() throws IOException {
864 int r = super.read();
865 limit--;
866 if (limit < 0) {
867 throw new IllegalStateException();
868 }
869 return r;
870 }
871
872 @Override
873 public int read(byte[] b, int off, int len) throws IOException {
874 int n = super.read(b, off, len);
875 limit -= n;
876 if (limit < 0) {
877 throw new IllegalStateException();
878 }
879 return n;
880 }
881 }
882
883 static class BigReadForbiddenReader extends ObjectReader.Filter {
884 ObjectReader delegate;
885 int limit;
886
887 @Override
888 protected ObjectReader delegate() {
889 return delegate;
890 }
891
892 BigReadForbiddenReader(ObjectReader delegate, int limit) {
893 this.delegate = delegate;
894 this.limit = limit;
895 }
896
897 @Override
898 public ObjectLoader open(AnyObjectId objectId, int typeHint) throws IOException {
899 ObjectLoader orig = super.open(objectId, typeHint);
900 return new ObjectLoader.Filter() {
901 @Override
902 protected ObjectLoader delegate() {
903 return orig;
904 }
905
906 @Override
907 public ObjectStream openStream() throws IOException {
908 ObjectStream os = orig.openStream();
909 return new BigReadForbiddenStream(os, limit);
910 }
911 };
912 }
913 }
914
915 @Theory
916 public void checkContentMergeConflict(MergeStrategy strategy)
917 throws Exception {
918 Git git = Git.wrap(db);
919
920 writeTrashFile("file", "1\n2\n3");
921 git.add().addFilepattern("file").call();
922 RevCommit first = git.commit().setMessage("added file").call();
923
924 writeTrashFile("file", "1master\n2\n3");
925 git.commit().setAll(true).setMessage("modified file on master").call();
926
927 git.checkout().setCreateBranch(true).setStartPoint(first)
928 .setName("side").call();
929 writeTrashFile("file", "1side\n2\n3");
930 RevCommit sideCommit = git.commit().setAll(true)
931 .setMessage("modified file on side").call();
932
933 git.checkout().setName("master").call();
934 MergeResult result =
935 git.merge().setStrategy(strategy).include(sideCommit).call();
936 assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
937 String expected = "<<<<<<< HEAD\n"
938 + "1master\n"
939 + "=======\n"
940 + "1side\n"
941 + ">>>>>>> " + sideCommit.name() + "\n"
942 + "2\n"
943 + "3";
944 assertEquals(expected, read("file"));
945 }
946
947 @Theory
948 public void checkContentMergeConflict_noTree(MergeStrategy strategy)
949 throws Exception {
950 Git git = Git.wrap(db);
951
952 writeTrashFile("file", "1\n2\n3");
953 git.add().addFilepattern("file").call();
954 RevCommit first = git.commit().setMessage("added file").call();
955
956 writeTrashFile("file", "1master\n2\n3");
957 RevCommit masterCommit = git.commit().setAll(true)
958 .setMessage("modified file on master").call();
959
960 git.checkout().setCreateBranch(true).setStartPoint(first)
961 .setName("side").call();
962 writeTrashFile("file", "1side\n2\n3");
963 RevCommit sideCommit = git.commit().setAll(true)
964 .setMessage("modified file on side").call();
965
966 try (ObjectInserter ins = db.newObjectInserter()) {
967 ResolveMerger merger =
968 (ResolveMerger) strategy.newMerger(ins, db.getConfig());
969 boolean noProblems = merger.merge(masterCommit, sideCommit);
970 assertFalse(noProblems);
971 assertEquals(Arrays.asList("file"), merger.getUnmergedPaths());
972
973 MergeFormatter fmt = new MergeFormatter();
974 merger.getMergeResults().get("file");
975 try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
976 fmt.formatMerge(out, merger.getMergeResults().get("file"),
977 "BASE", "OURS", "THEIRS", UTF_8.name());
978 String expected = "<<<<<<< OURS\n"
979 + "1master\n"
980 + "=======\n"
981 + "1side\n"
982 + ">>>>>>> THEIRS\n"
983 + "2\n"
984 + "3";
985 assertEquals(expected, new String(out.toByteArray(), UTF_8));
986 }
987 }
988 }
989
990
991
992
993
994
995
996
997 @Theory
998 public void checkMergeCrissCross(MergeStrategy strategy) throws Exception {
999 Git git = Git.wrap(db);
1000
1001 writeTrashFile("1", "1\n2\n3");
1002 git.add().addFilepattern("1").call();
1003 RevCommit first = git.commit().setMessage("added 1").call();
1004
1005 writeTrashFile("1", "1master\n2\n3");
1006 RevCommit masterCommit = git.commit().setAll(true)
1007 .setMessage("modified 1 on master").call();
1008
1009 writeTrashFile("1", "1master2\n2\n3");
1010 git.commit().setAll(true)
1011 .setMessage("modified 1 on master again").call();
1012
1013 git.checkout().setCreateBranch(true).setStartPoint(first)
1014 .setName("side").call();
1015 writeTrashFile("1", "1\n2\na\nb\nc\n3side");
1016 RevCommit sideCommit = git.commit().setAll(true)
1017 .setMessage("modified 1 on side").call();
1018
1019 writeTrashFile("1", "1\n2\n3side2");
1020 git.commit().setAll(true)
1021 .setMessage("modified 1 on side again").call();
1022
1023 MergeResult result = git.merge().setStrategy(strategy)
1024 .include(masterCommit).call();
1025 assertEquals(MergeStatus.MERGED, result.getMergeStatus());
1026 result.getNewHead();
1027 git.checkout().setName("master").call();
1028 result = git.merge().setStrategy(strategy).include(sideCommit).call();
1029 assertEquals(MergeStatus.MERGED, result.getMergeStatus());
1030
1031
1032
1033
1034 try {
1035 MergeResult mergeResult = git.merge().setStrategy(strategy)
1036 .include(git.getRepository().exactRef("refs/heads/side"))
1037 .call();
1038 assertEquals(MergeStrategy.RECURSIVE, strategy);
1039 assertEquals(MergeResult.MergeStatus.MERGED,
1040 mergeResult.getMergeStatus());
1041 assertEquals("1master2\n2\n3side2", read("1"));
1042 } catch (JGitInternalException e) {
1043 assertEquals(MergeStrategy.RESOLVE, strategy);
1044 assertTrue(e.getCause() instanceof NoMergeBaseException);
1045 assertEquals(((NoMergeBaseException) e.getCause()).getReason(),
1046 MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
1047 }
1048 }
1049
1050 @Theory
1051 public void checkLockedFilesToBeDeleted(MergeStrategy strategy)
1052 throws Exception {
1053 Git git = Git.wrap(db);
1054
1055 writeTrashFile("a.txt", "orig");
1056 writeTrashFile("b.txt", "orig");
1057 git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
1058 RevCommit first = git.commit().setMessage("added a.txt, b.txt").call();
1059
1060
1061 writeTrashFile("a.txt", "master");
1062 git.rm().addFilepattern("b.txt").call();
1063 RevCommit masterCommit = git.commit()
1064 .setMessage("modified a.txt, deleted b.txt").setAll(true)
1065 .call();
1066
1067
1068 git.checkout().setCreateBranch(true).setStartPoint(first)
1069 .setName("side").call();
1070 writeTrashFile("c.txt", "side");
1071 git.add().addFilepattern("c.txt").call();
1072 git.commit().setMessage("added c.txt").call();
1073
1074
1075 try (FileInputStream fis = new FileInputStream(
1076 new File(db.getWorkTree(), "b.txt"))) {
1077 MergeResult mergeRes = git.merge().setStrategy(strategy)
1078 .include(masterCommit).call();
1079 if (mergeRes.getMergeStatus().equals(MergeStatus.FAILED)) {
1080
1081 assertEquals(1, mergeRes.getFailingPaths().size());
1082 assertEquals(MergeFailureReason.COULD_NOT_DELETE,
1083 mergeRes.getFailingPaths().get("b.txt"));
1084 }
1085 assertEquals(
1086 "[a.txt, mode:100644, content:master]"
1087 + "[c.txt, mode:100644, content:side]",
1088 indexState(CONTENT));
1089 }
1090 }
1091
1092 @Theory
1093 public void checkForCorrectIndex(MergeStrategy strategy) throws Exception {
1094 File f;
1095 Instant lastTs4, lastTsIndex;
1096 Git git = Git.wrap(db);
1097 File indexFile = db.getIndexFile();
1098
1099
1100 f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
1101 lastTs4 = FS.DETECTED.lastModifiedInstant(f);
1102
1103
1104
1105
1106
1107 fsTick(f);
1108 git.add().addFilepattern(".").call();
1109 RevCommit firstCommit = git.commit().setMessage("initial commit")
1110 .call();
1111 checkConsistentLastModified("0", "1", "2", "3", "4");
1112 checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index");
1113 assertEquals("Commit should not touch working tree file 4", lastTs4,
1114 FS.DETECTED
1115 .lastModifiedInstant(new File(db.getWorkTree(), "4")));
1116 lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1117
1118
1119
1120 fsTick(indexFile);
1121 f = writeTrashFiles(false, "master", null, "1master\n2\n3", "master",
1122 null);
1123 fsTick(f);
1124 git.add().addFilepattern(".").call();
1125 RevCommit masterCommit = git.commit().setMessage("master commit")
1126 .call();
1127 checkConsistentLastModified("0", "1", "2", "3", "4");
1128 checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
1129 + lastTsIndex, "<0", "2", "3", "<.git/index");
1130 lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1131
1132
1133 fsTick(indexFile);
1134 git.checkout().setCreateBranch(true).setStartPoint(firstCommit)
1135 .setName("side").call();
1136 checkConsistentLastModified("0", "1", "2", "3", "4");
1137 checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
1138 + lastTsIndex, "<0", "2", "3", ".git/index");
1139 lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1140
1141
1142
1143
1144 assertEquals("[0, mode:100644, content:orig]"
1145 + "[1, mode:100644, content:orig]"
1146 + "[2, mode:100644, content:1\n2\n3]"
1147 + "[3, mode:100644, content:orig]"
1148 + "[4, mode:100644, content:orig]",
1149 indexState(CONTENT));
1150 fsTick(indexFile);
1151 f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
1152 lastTs4 = FS.DETECTED.lastModifiedInstant(f);
1153 fsTick(f);
1154 git.add().addFilepattern(".").call();
1155 checkConsistentLastModified("0", "1", "2", "3", "4");
1156 checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3",
1157 "4", "<.git/index");
1158 lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1159
1160
1161 fsTick(indexFile);
1162 f = writeTrashFiles(false, null, "side", "1\n2\n3side", "side", null);
1163 fsTick(f);
1164 git.add().addFilepattern(".").call();
1165 git.commit().setMessage("side commit").call();
1166 checkConsistentLastModified("0", "1", "2", "3", "4");
1167 checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*"
1168 + lastTsIndex, "<1", "2", "3", "<.git/index");
1169 lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1170
1171
1172 fsTick(indexFile);
1173 git.merge().setStrategy(strategy).include(masterCommit).call();
1174 checkConsistentLastModified("0", "1", "2", "4");
1175 checkModificationTimeStampOrder("4", "*" + lastTs4, "<1", "<*"
1176 + lastTsIndex, "<0", "2", "3", ".git/index");
1177 assertEquals(
1178 "[0, mode:100644, content:master]"
1179 + "[1, mode:100644, content:side]"
1180 + "[2, mode:100644, content:1master\n2\n3side]"
1181 + "[3, mode:100644, stage:1, content:orig][3, mode:100644, stage:2, content:side][3, mode:100644, stage:3, content:master]"
1182 + "[4, mode:100644, content:orig]",
1183 indexState(CONTENT));
1184 }
1185
1186
1187
1188
1189
1190
1191
1192
1193 @Theory
1194 public void checkMergeConflictingSubmodulesWithoutIndex(
1195 MergeStrategy strategy) throws Exception {
1196 Git git = Git.wrap(db);
1197 writeTrashFile("initial", "initial");
1198 git.add().addFilepattern("initial").call();
1199 RevCommit initial = git.commit().setMessage("initial").call();
1200
1201 writeSubmodule("one", ObjectId
1202 .fromString("1000000000000000000000000000000000000000"));
1203 git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1204 RevCommit right = git.commit().setMessage("added one").call();
1205
1206
1207
1208 git.checkout().setStartPoint(initial).setName("left")
1209 .setCreateBranch(true).call();
1210 writeSubmodule("one", ObjectId
1211 .fromString("2000000000000000000000000000000000000000"));
1212
1213 git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1214 git.commit().setMessage("a different one").call();
1215
1216 MergeResult result = git.merge().setStrategy(strategy).include(right)
1217 .call();
1218
1219 assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
1220 Map<String, int[][]> conflicts = result.getConflicts();
1221 assertEquals(1, conflicts.size());
1222 assertNotNull(conflicts.get("one"));
1223 }
1224
1225
1226
1227
1228
1229
1230
1231
1232 @Theory
1233 public void checkMergeNonConflictingSubmodulesWithoutIndex(
1234 MergeStrategy strategy) throws Exception {
1235 Git git = Git.wrap(db);
1236 writeTrashFile("initial", "initial");
1237 git.add().addFilepattern("initial").call();
1238
1239 writeSubmodule("one", ObjectId
1240 .fromString("1000000000000000000000000000000000000000"));
1241
1242
1243
1244
1245
1246
1247
1248 String existing = read(Constants.DOT_GIT_MODULES);
1249 String context = "\n# context\n# more context\n# yet more context\n";
1250 write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1251 existing + context + context + context);
1252
1253 git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1254 RevCommit initial = git.commit().setMessage("initial").call();
1255
1256 writeSubmodule("two", ObjectId
1257 .fromString("1000000000000000000000000000000000000000"));
1258 git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1259
1260 RevCommit right = git.commit().setMessage("added two").call();
1261
1262 git.checkout().setStartPoint(initial).setName("left")
1263 .setCreateBranch(true).call();
1264
1265
1266
1267 addSubmoduleToIndex("three", ObjectId
1268 .fromString("1000000000000000000000000000000000000000"));
1269 new File(db.getWorkTree(), "three").mkdir();
1270
1271 existing = read(Constants.DOT_GIT_MODULES);
1272 String three = "[submodule \"three\"]\n\tpath = three\n\turl = "
1273 + db.getDirectory().toURI() + "\n";
1274 write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1275 three + existing);
1276
1277 git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1278 git.commit().setMessage("a different one").call();
1279
1280 MergeResult result = git.merge().setStrategy(strategy).include(right)
1281 .call();
1282
1283 assertNull(result.getCheckoutConflicts());
1284 assertNull(result.getFailingPaths());
1285 for (String dir : Arrays.asList("one", "two", "three")) {
1286 assertTrue(new File(db.getWorkTree(), dir).isDirectory());
1287 }
1288 }
1289
1290 private void writeSubmodule(String path, ObjectId commit)
1291 throws IOException, ConfigInvalidException {
1292 addSubmoduleToIndex(path, commit);
1293 new File(db.getWorkTree(), path).mkdir();
1294
1295 StoredConfig config = db.getConfig();
1296 config.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
1297 ConfigConstants.CONFIG_KEY_URL,
1298 db.getDirectory().toURI().toString());
1299 config.save();
1300
1301 FileBasedConfig modulesConfig = new FileBasedConfig(
1302 new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1303 db.getFS());
1304 modulesConfig.load();
1305 modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
1306 ConfigConstants.CONFIG_KEY_PATH, path);
1307 modulesConfig.save();
1308
1309 }
1310
1311 private void addSubmoduleToIndex(String path, ObjectId commit)
1312 throws IOException {
1313 DirCache cache = db.lockDirCache();
1314 DirCacheEditor editor = cache.editor();
1315 editor.add(new DirCacheEditor.PathEdit(path) {
1316
1317 @Override
1318 public void apply(DirCacheEntry ent) {
1319 ent.setFileMode(FileMode.GITLINK);
1320 ent.setObjectId(commit);
1321 }
1322 });
1323 editor.commit();
1324 }
1325
1326
1327
1328 private void checkConsistentLastModified(String... pathes)
1329 throws IOException {
1330 DirCache dc = db.readDirCache();
1331 File workTree = db.getWorkTree();
1332 for (String path : pathes)
1333 assertEquals(
1334 "IndexEntry with path "
1335 + path
1336 + " has lastmodified which is different from the worktree file",
1337 FS.DETECTED.lastModifiedInstant(new File(workTree, path)),
1338 dc.getEntry(path)
1339 .getLastModifiedInstant());
1340 }
1341
1342
1343
1344
1345
1346
1347
1348 private void checkModificationTimeStampOrder(String... pathes) {
1349 Instant lastMod = EPOCH;
1350 for (String p : pathes) {
1351 boolean strong = p.startsWith("<");
1352 boolean fixed = p.charAt(strong ? 1 : 0) == '*';
1353 p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0));
1354 Instant curMod = fixed ? Instant.parse(p)
1355 : FS.DETECTED
1356 .lastModifiedInstant(new File(db.getWorkTree(), p));
1357 if (strong) {
1358 assertTrue("path " + p + " is not younger than predecesssor",
1359 curMod.compareTo(lastMod) > 0);
1360 } else {
1361 assertTrue("path " + p + " is older than predecesssor",
1362 curMod.compareTo(lastMod) >= 0);
1363 }
1364 }
1365 }
1366
1367 private String readBlob(ObjectId treeish, String path) throws Exception {
1368 TestRepository<?> tr = new TestRepository<>(db);
1369 RevWalk rw = tr.getRevWalk();
1370 RevTree tree = rw.parseTree(treeish);
1371 RevObject obj = tr.get(tree, path);
1372 if (obj == null) {
1373 return null;
1374 }
1375 return new String(rw.getObjectReader().open(obj, OBJ_BLOB).getBytes(),
1376 UTF_8);
1377 }
1378 }