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
44 package org.eclipse.jgit.treewalk;
45
46 import static org.junit.Assert.assertEquals;
47 import static org.junit.Assert.assertFalse;
48 import static org.junit.Assert.assertNotNull;
49 import static org.junit.Assert.assertTrue;
50
51 import java.io.File;
52 import java.io.IOException;
53 import java.security.MessageDigest;
54
55 import org.eclipse.jgit.api.Git;
56 import org.eclipse.jgit.dircache.DirCache;
57 import org.eclipse.jgit.dircache.DirCacheCheckout;
58 import org.eclipse.jgit.dircache.DirCacheEditor;
59 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
60 import org.eclipse.jgit.dircache.DirCacheEntry;
61 import org.eclipse.jgit.dircache.DirCacheIterator;
62 import org.eclipse.jgit.errors.CorruptObjectException;
63 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
64 import org.eclipse.jgit.errors.MissingObjectException;
65 import org.eclipse.jgit.junit.JGitTestUtil;
66 import org.eclipse.jgit.junit.RepositoryTestCase;
67 import org.eclipse.jgit.lib.ConfigConstants;
68 import org.eclipse.jgit.lib.Constants;
69 import org.eclipse.jgit.lib.FileMode;
70 import org.eclipse.jgit.lib.ObjectId;
71 import org.eclipse.jgit.lib.ObjectReader;
72 import org.eclipse.jgit.lib.Repository;
73 import org.eclipse.jgit.revwalk.RevCommit;
74 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
75 import org.eclipse.jgit.treewalk.WorkingTreeIterator.MetadataDiff;
76 import org.eclipse.jgit.treewalk.filter.PathFilter;
77 import org.eclipse.jgit.util.FS;
78 import org.eclipse.jgit.util.FileUtils;
79 import org.eclipse.jgit.util.RawParseUtils;
80 import org.junit.Before;
81 import org.junit.Test;
82
83 public class FileTreeIteratorTest extends RepositoryTestCase {
84 private final String[] paths = { "a,", "a,b", "a/b", "a0b" };
85
86 private long[] mtime;
87
88 @Before
89 public void setUp() throws Exception {
90 super.setUp();
91
92
93
94
95
96
97
98 mtime = new long[paths.length];
99 for (int i = paths.length - 1; i >= 0; i--) {
100 final String s = paths[i];
101 writeTrashFile(s, s);
102 mtime[i] = new File(trash, s).lastModified();
103 }
104 }
105
106 @Test
107 public void testGetEntryContentLength() throws Exception {
108 final FileTreeIterator fti = new FileTreeIterator(db);
109 fti.next(1);
110 assertEquals(3, fti.getEntryContentLength());
111 fti.back(1);
112 assertEquals(2, fti.getEntryContentLength());
113 fti.next(1);
114 assertEquals(3, fti.getEntryContentLength());
115 fti.reset();
116 assertEquals(2, fti.getEntryContentLength());
117 }
118
119 @Test
120 public void testEmptyIfRootIsFile() throws Exception {
121 final File r = new File(trash, paths[0]);
122 assertTrue(r.isFile());
123 final FileTreeIterator fti = new FileTreeIterator(r, db.getFS(),
124 db.getConfig().get(WorkingTreeOptions.KEY));
125 assertTrue(fti.first());
126 assertTrue(fti.eof());
127 }
128
129 @Test
130 public void testEmptyIfRootDoesNotExist() throws Exception {
131 final File r = new File(trash, "not-existing-file");
132 assertFalse(r.exists());
133 final FileTreeIterator fti = new FileTreeIterator(r, db.getFS(),
134 db.getConfig().get(WorkingTreeOptions.KEY));
135 assertTrue(fti.first());
136 assertTrue(fti.eof());
137 }
138
139 @Test
140 public void testEmptyIfRootIsEmpty() throws Exception {
141 final File r = new File(trash, "not-existing-file");
142 assertFalse(r.exists());
143 FileUtils.mkdir(r);
144
145 final FileTreeIterator fti = new FileTreeIterator(r, db.getFS(),
146 db.getConfig().get(WorkingTreeOptions.KEY));
147 assertTrue(fti.first());
148 assertTrue(fti.eof());
149 }
150
151 @Test
152 public void testEmptyIteratorOnEmptyDirectory() throws Exception {
153 String nonExistingFileName = "not-existing-file";
154 final File r = new File(trash, nonExistingFileName);
155 assertFalse(r.exists());
156 FileUtils.mkdir(r);
157
158 final FileTreeIterator parent = new FileTreeIterator(db);
159
160 while (!parent.getEntryPathString().equals(nonExistingFileName))
161 parent.next(1);
162
163 final FileTreeIterator childIter = new FileTreeIterator(parent, r,
164 db.getFS());
165 assertTrue(childIter.first());
166 assertTrue(childIter.eof());
167
168 String parentPath = parent.getEntryPathString();
169 assertEquals(nonExistingFileName, parentPath);
170
171
172
173 String childPath = childIter.getEntryPathString();
174
175
176 EmptyTreeIterator e = childIter.createEmptyTreeIterator();
177 assertNotNull(e);
178
179
180
181
182 assertEquals(parentPath, parent.getEntryPathString());
183 assertEquals(parentPath + "/", childPath);
184 assertEquals(parentPath + "/", childIter.getEntryPathString());
185 assertEquals(childPath + "/", e.getEntryPathString());
186 }
187
188 @Test
189 public void testSimpleIterate() throws Exception {
190 final FileTreeIterator top = new FileTreeIterator(trash, db.getFS(),
191 db.getConfig().get(WorkingTreeOptions.KEY));
192
193 assertTrue(top.first());
194 assertFalse(top.eof());
195 assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode);
196 assertEquals(paths[0], nameOf(top));
197 assertEquals(paths[0].length(), top.getEntryLength());
198 assertEquals(mtime[0], top.getEntryLastModified());
199
200 top.next(1);
201 assertFalse(top.first());
202 assertFalse(top.eof());
203 assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode);
204 assertEquals(paths[1], nameOf(top));
205 assertEquals(paths[1].length(), top.getEntryLength());
206 assertEquals(mtime[1], top.getEntryLastModified());
207
208 top.next(1);
209 assertFalse(top.first());
210 assertFalse(top.eof());
211 assertEquals(FileMode.TREE.getBits(), top.mode);
212
213 final ObjectReader reader = db.newObjectReader();
214 final AbstractTreeIterator sub = top.createSubtreeIterator(reader);
215 assertTrue(sub instanceof FileTreeIterator);
216 final FileTreeIterator subfti = (FileTreeIterator) sub;
217 assertTrue(sub.first());
218 assertFalse(sub.eof());
219 assertEquals(paths[2], nameOf(sub));
220 assertEquals(paths[2].length(), subfti.getEntryLength());
221 assertEquals(mtime[2], subfti.getEntryLastModified());
222
223 sub.next(1);
224 assertTrue(sub.eof());
225
226 top.next(1);
227 assertFalse(top.first());
228 assertFalse(top.eof());
229 assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode);
230 assertEquals(paths[3], nameOf(top));
231 assertEquals(paths[3].length(), top.getEntryLength());
232 assertEquals(mtime[3], top.getEntryLastModified());
233
234 top.next(1);
235 assertTrue(top.eof());
236 }
237
238 @Test
239 public void testComputeFileObjectId() throws Exception {
240 final FileTreeIterator top = new FileTreeIterator(trash, db.getFS(),
241 db.getConfig().get(WorkingTreeOptions.KEY));
242
243 final MessageDigest md = Constants.newMessageDigest();
244 md.update(Constants.encodeASCII(Constants.TYPE_BLOB));
245 md.update((byte) ' ');
246 md.update(Constants.encodeASCII(paths[0].length()));
247 md.update((byte) 0);
248 md.update(Constants.encode(paths[0]));
249 final ObjectId expect = ObjectId.fromRaw(md.digest());
250
251 assertEquals(expect, top.getEntryObjectId());
252
253
254
255 FileUtils.delete(new File(trash, paths[0]));
256 assertEquals(expect, top.getEntryObjectId());
257 }
258
259 @Test
260 public void testDirCacheMatchingId() throws Exception {
261 File f = writeTrashFile("file", "content");
262 try (Git git = new Git(db)) {
263 writeTrashFile("file", "content");
264 fsTick(f);
265 git.add().addFilepattern("file").call();
266 }
267 DirCacheEntry dce = db.readDirCache().getEntry("file");
268 TreeWalk tw = new TreeWalk(db);
269 FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(), db
270 .getConfig().get(WorkingTreeOptions.KEY));
271 tw.addTree(fti);
272 DirCacheIterator dci = new DirCacheIterator(db.readDirCache());
273 tw.addTree(dci);
274 fti.setDirCacheIterator(tw, 1);
275 while (tw.next() && !tw.getPathString().equals("file")) {
276
277 }
278 assertEquals(MetadataDiff.EQUAL, fti.compareMetadata(dce));
279 ObjectId fromRaw = ObjectId.fromRaw(fti.idBuffer(), fti.idOffset());
280 assertEquals("6b584e8ece562ebffc15d38808cd6b98fc3d97ea",
281 fromRaw.getName());
282 try (ObjectReader objectReader = db.newObjectReader()) {
283 assertFalse(fti.isModified(dce, false, objectReader));
284 }
285 }
286
287 @Test
288 public void testTreewalkEnterSubtree() throws Exception {
289 try (Git git = new Git(db)) {
290 writeTrashFile("b/c", "b/c");
291 writeTrashFile("z/.git", "gitdir: /tmp/somewhere");
292 git.add().addFilepattern(".").call();
293 git.rm().addFilepattern("a,").addFilepattern("a,b")
294 .addFilepattern("a0b").call();
295 assertEquals("[a/b, mode:100644][b/c, mode:100644][z, mode:160000]",
296 indexState(0));
297 FileUtils.delete(new File(db.getWorkTree(), "b"),
298 FileUtils.RECURSIVE);
299
300 TreeWalk tw = new TreeWalk(db);
301 tw.addTree(new DirCacheIterator(db.readDirCache()));
302 tw.addTree(new FileTreeIterator(db));
303 assertTrue(tw.next());
304 assertEquals("a", tw.getPathString());
305 tw.enterSubtree();
306 tw.next();
307 assertEquals("a/b", tw.getPathString());
308 tw.next();
309 assertEquals("b", tw.getPathString());
310 tw.enterSubtree();
311 tw.next();
312 assertEquals("b/c", tw.getPathString());
313 assertNotNull(tw.getTree(0, AbstractTreeIterator.class));
314 assertNotNull(tw.getTree(EmptyTreeIterator.class));
315 }
316 }
317
318 @Test
319 public void testIsModifiedSymlinkAsFile() throws Exception {
320 writeTrashFile("symlink", "content");
321 try (Git git = new Git(db)) {
322 db.getConfig().setString(ConfigConstants.CONFIG_CORE_SECTION, null,
323 ConfigConstants.CONFIG_KEY_SYMLINKS, "false");
324 git.add().addFilepattern("symlink").call();
325 git.commit().setMessage("commit").call();
326 }
327
328
329 DirCacheEntry dce = db.readDirCache().getEntry("symlink");
330 dce.setFileMode(FileMode.SYMLINK);
331 try (ObjectReader objectReader = db.newObjectReader()) {
332 DirCacheCheckout.checkoutEntry(db, dce, objectReader);
333
334 FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(),
335 db.getConfig().get(WorkingTreeOptions.KEY));
336 while (!fti.getEntryPathString().equals("symlink"))
337 fti.next(1);
338 assertFalse(fti.isModified(dce, false, objectReader));
339 }
340 }
341
342 @Test
343 public void testIsModifiedFileSmudged() throws Exception {
344 File f = writeTrashFile("file", "content");
345 try (Git git = new Git(db)) {
346
347
348 fsTick(f);
349 writeTrashFile("file", "content");
350 long lastModified = f.lastModified();
351 git.add().addFilepattern("file").call();
352 writeTrashFile("file", "conten2");
353 f.setLastModified(lastModified);
354
355
356
357
358 db.getIndexFile().setLastModified(lastModified);
359 }
360 DirCacheEntry dce = db.readDirCache().getEntry("file");
361 FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(), db
362 .getConfig().get(WorkingTreeOptions.KEY));
363 while (!fti.getEntryPathString().equals("file"))
364 fti.next(1);
365
366
367 assertEquals(MetadataDiff.SMUDGED, fti.compareMetadata(dce));
368 try (ObjectReader objectReader = db.newObjectReader()) {
369 assertTrue(fti.isModified(dce, false, objectReader));
370 }
371 }
372
373 @Test
374 public void submoduleHeadMatchesIndex() throws Exception {
375 try (Git git = new Git(db);
376 TreeWalk walk = new TreeWalk(db)) {
377 writeTrashFile("file.txt", "content");
378 git.add().addFilepattern("file.txt").call();
379 final RevCommit id = git.commit().setMessage("create file").call();
380 final String path = "sub";
381 DirCache cache = db.lockDirCache();
382 DirCacheEditor editor = cache.editor();
383 editor.add(new PathEdit(path) {
384
385 public void apply(DirCacheEntry ent) {
386 ent.setFileMode(FileMode.GITLINK);
387 ent.setObjectId(id);
388 }
389 });
390 editor.commit();
391
392 Git.cloneRepository().setURI(db.getDirectory().toURI().toString())
393 .setDirectory(new File(db.getWorkTree(), path)).call()
394 .getRepository().close();
395
396 DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
397 FileTreeIterator workTreeIter = new FileTreeIterator(db);
398 walk.addTree(indexIter);
399 walk.addTree(workTreeIter);
400 walk.setFilter(PathFilter.create(path));
401
402 assertTrue(walk.next());
403 assertTrue(indexIter.idEqual(workTreeIter));
404 }
405 }
406
407 @Test
408 public void submoduleWithNoGitDirectory() throws Exception {
409 try (Git git = new Git(db);
410 TreeWalk walk = new TreeWalk(db)) {
411 writeTrashFile("file.txt", "content");
412 git.add().addFilepattern("file.txt").call();
413 final RevCommit id = git.commit().setMessage("create file").call();
414 final String path = "sub";
415 DirCache cache = db.lockDirCache();
416 DirCacheEditor editor = cache.editor();
417 editor.add(new PathEdit(path) {
418
419 public void apply(DirCacheEntry ent) {
420 ent.setFileMode(FileMode.GITLINK);
421 ent.setObjectId(id);
422 }
423 });
424 editor.commit();
425
426 File submoduleRoot = new File(db.getWorkTree(), path);
427 assertTrue(submoduleRoot.mkdir());
428 assertTrue(new File(submoduleRoot, Constants.DOT_GIT).mkdir());
429
430 DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
431 FileTreeIterator workTreeIter = new FileTreeIterator(db);
432 walk.addTree(indexIter);
433 walk.addTree(workTreeIter);
434 walk.setFilter(PathFilter.create(path));
435
436 assertTrue(walk.next());
437 assertFalse(indexIter.idEqual(workTreeIter));
438 assertEquals(ObjectId.zeroId(), workTreeIter.getEntryObjectId());
439 }
440 }
441
442 @Test
443 public void submoduleWithNoHead() throws Exception {
444 try (Git git = new Git(db);
445 TreeWalk walk = new TreeWalk(db)) {
446 writeTrashFile("file.txt", "content");
447 git.add().addFilepattern("file.txt").call();
448 final RevCommit id = git.commit().setMessage("create file").call();
449 final String path = "sub";
450 DirCache cache = db.lockDirCache();
451 DirCacheEditor editor = cache.editor();
452 editor.add(new PathEdit(path) {
453
454 public void apply(DirCacheEntry ent) {
455 ent.setFileMode(FileMode.GITLINK);
456 ent.setObjectId(id);
457 }
458 });
459 editor.commit();
460
461 assertNotNull(Git.init().setDirectory(new File(db.getWorkTree(), path))
462 .call().getRepository());
463
464 DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
465 FileTreeIterator workTreeIter = new FileTreeIterator(db);
466 walk.addTree(indexIter);
467 walk.addTree(workTreeIter);
468 walk.setFilter(PathFilter.create(path));
469
470 assertTrue(walk.next());
471 assertFalse(indexIter.idEqual(workTreeIter));
472 assertEquals(ObjectId.zeroId(), workTreeIter.getEntryObjectId());
473 }
474 }
475
476 @Test
477 public void submoduleDirectoryIterator() throws Exception {
478 try (Git git = new Git(db);
479 TreeWalk walk = new TreeWalk(db)) {
480 writeTrashFile("file.txt", "content");
481 git.add().addFilepattern("file.txt").call();
482 final RevCommit id = git.commit().setMessage("create file").call();
483 final String path = "sub";
484 DirCache cache = db.lockDirCache();
485 DirCacheEditor editor = cache.editor();
486 editor.add(new PathEdit(path) {
487
488 public void apply(DirCacheEntry ent) {
489 ent.setFileMode(FileMode.GITLINK);
490 ent.setObjectId(id);
491 }
492 });
493 editor.commit();
494
495 Git.cloneRepository().setURI(db.getDirectory().toURI().toString())
496 .setDirectory(new File(db.getWorkTree(), path)).call()
497 .getRepository().close();
498
499 DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
500 FileTreeIterator workTreeIter = new FileTreeIterator(db.getWorkTree(),
501 db.getFS(), db.getConfig().get(WorkingTreeOptions.KEY));
502 walk.addTree(indexIter);
503 walk.addTree(workTreeIter);
504 walk.setFilter(PathFilter.create(path));
505
506 assertTrue(walk.next());
507 assertTrue(indexIter.idEqual(workTreeIter));
508 }
509 }
510
511 @Test
512 public void submoduleNestedWithHeadMatchingIndex() throws Exception {
513 try (Git git = new Git(db);
514 TreeWalk walk = new TreeWalk(db)) {
515 writeTrashFile("file.txt", "content");
516 git.add().addFilepattern("file.txt").call();
517 final RevCommit id = git.commit().setMessage("create file").call();
518 final String path = "sub/dir1/dir2";
519 DirCache cache = db.lockDirCache();
520 DirCacheEditor editor = cache.editor();
521 editor.add(new PathEdit(path) {
522
523 public void apply(DirCacheEntry ent) {
524 ent.setFileMode(FileMode.GITLINK);
525 ent.setObjectId(id);
526 }
527 });
528 editor.commit();
529
530 Git.cloneRepository().setURI(db.getDirectory().toURI().toString())
531 .setDirectory(new File(db.getWorkTree(), path)).call()
532 .getRepository().close();
533
534 DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
535 FileTreeIterator workTreeIter = new FileTreeIterator(db);
536 walk.addTree(indexIter);
537 walk.addTree(workTreeIter);
538 walk.setFilter(PathFilter.create(path));
539
540 assertTrue(walk.next());
541 assertTrue(indexIter.idEqual(workTreeIter));
542 }
543 }
544
545 @Test
546 public void idOffset() throws Exception {
547 try (Git git = new Git(db);
548 TreeWalk tw = new TreeWalk(db)) {
549 writeTrashFile("fileAinfsonly", "A");
550 File fileBinindex = writeTrashFile("fileBinindex", "B");
551 fsTick(fileBinindex);
552 git.add().addFilepattern("fileBinindex").call();
553 writeTrashFile("fileCinfsonly", "C");
554 DirCacheIterator indexIter = new DirCacheIterator(db.readDirCache());
555 FileTreeIterator workTreeIter = new FileTreeIterator(db);
556 tw.addTree(indexIter);
557 tw.addTree(workTreeIter);
558 workTreeIter.setDirCacheIterator(tw, 0);
559 assertEntry("d46c305e85b630558ee19cc47e73d2e5c8c64cdc", "a,", tw);
560 assertEntry("58ee403f98538ec02409538b3f80adf610accdec", "a,b", tw);
561 assertEntry("0000000000000000000000000000000000000000", "a", tw);
562 assertEntry("b8d30ff397626f0f1d3538d66067edf865e201d6", "a0b", tw);
563
564
565 assertEntry("8c7e5a667f1b771847fe88c01c3de34413a1b220",
566 "fileAinfsonly", tw);
567 assertEntry("7371f47a6f8bd23a8fa1a8b2a9479cdd76380e54", "fileBinindex",
568 tw);
569 assertEntry("96d80cd6c4e7158dbebd0849f4fb7ce513e5828c",
570 "fileCinfsonly", tw);
571 assertFalse(tw.next());
572 }
573 }
574
575 private final FileTreeIterator.FileModeStrategy NO_GITLINKS_STRATEGY =
576 new FileTreeIterator.FileModeStrategy() {
577 @Override
578 public FileMode getMode(File f, FS.Attributes attributes) {
579 if (attributes.isSymbolicLink()) {
580 return FileMode.SYMLINK;
581 } else if (attributes.isDirectory()) {
582
583
584
585
586
587 return FileMode.TREE;
588 } else if (attributes.isExecutable()) {
589 return FileMode.EXECUTABLE_FILE;
590 } else {
591 return FileMode.REGULAR_FILE;
592 }
593 }
594 };
595
596 private Repository createNestedRepo() throws IOException {
597 File gitdir = createUniqueTestGitDir(false);
598 FileRepositoryBuilder builder = new FileRepositoryBuilder();
599 builder.setGitDir(gitdir);
600 Repository nestedRepo = builder.build();
601 nestedRepo.create();
602
603 JGitTestUtil.writeTrashFile(nestedRepo, "sub", "a.txt", "content");
604
605 File nestedRepoPath = new File(nestedRepo.getWorkTree(), "sub/nested");
606 FileRepositoryBuilder nestedBuilder = new FileRepositoryBuilder();
607 nestedBuilder.setWorkTree(nestedRepoPath);
608 nestedBuilder.build().create();
609
610 JGitTestUtil.writeTrashFile(nestedRepo, "sub/nested", "b.txt",
611 "content b");
612
613 return nestedRepo;
614 }
615
616 @Test
617 public void testCustomFileModeStrategy() throws Exception {
618 Repository nestedRepo = createNestedRepo();
619
620 Git git = new Git(nestedRepo);
621
622 WorkingTreeIterator customIterator =
623 new FileTreeIterator(nestedRepo, NO_GITLINKS_STRATEGY);
624 git.add().setWorkingTreeIterator(customIterator)
625 .addFilepattern(".").call();
626 assertEquals(
627 "[sub/a.txt, mode:100644, content:content]" +
628 "[sub/nested/b.txt, mode:100644, content:content b]",
629 indexState(nestedRepo, CONTENT));
630
631 }
632
633 @Test
634 public void testCustomFileModeStrategyFromParentIterator() throws Exception {
635 Repository nestedRepo = createNestedRepo();
636
637 Git git = new Git(nestedRepo);
638
639 FileTreeIterator customIterator =
640 new FileTreeIterator(nestedRepo, NO_GITLINKS_STRATEGY);
641 File r = new File(nestedRepo.getWorkTree(), "sub");
642
643
644
645
646 FileTreeIterator childIterator =
647 new FileTreeIterator(customIterator, r, nestedRepo.getFS());
648 git.add().setWorkingTreeIterator(childIterator).addFilepattern(".").call();
649 assertEquals(
650 "[sub/a.txt, mode:100644, content:content]" +
651 "[sub/nested/b.txt, mode:100644, content:content b]",
652 indexState(nestedRepo, CONTENT));
653 }
654
655
656 private static void assertEntry(String sha1string, String path, TreeWalk tw)
657 throws MissingObjectException, IncorrectObjectTypeException,
658 CorruptObjectException, IOException {
659 assertTrue(tw.next());
660 assertEquals(path, tw.getPathString());
661 assertEquals(sha1string, tw.getObjectId(1).getName() );
662 }
663
664 private static String nameOf(final AbstractTreeIterator i) {
665 return RawParseUtils.decode(Constants.CHARSET, i.path, 0, i.pathLen);
666 }
667 }