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