diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index 815e830c8a..99c0623d97 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -35,23 +35,16 @@ package sonia.scm.repository.spi.javahg; //~--- non-JDK imports -------------------------------------------------------- -import com.aragost.javahg.DateTime; import com.aragost.javahg.Repository; import com.aragost.javahg.internals.AbstractCommand; import com.aragost.javahg.internals.HgInputStream; -import com.google.common.base.Strings; - import sonia.scm.repository.FileObject; -import sonia.scm.repository.SubRepository; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; -import java.util.Deque; -import java.util.LinkedList; - /** * Mercurial command to list files of a repository. * @@ -60,7 +53,6 @@ import java.util.LinkedList; public class HgFileviewCommand extends AbstractCommand { - public static final char TRUNCATED_MARK = 't'; private boolean disableLastCommit = false; private HgFileviewCommand(Repository repository) @@ -182,139 +174,9 @@ public class HgFileviewCommand extends AbstractCommand { cmdAppend("-t"); - Deque stack = new LinkedList<>(); - HgInputStream stream = launchStream(); - FileObject last = null; - while (stream.peek() != -1 && stream.peek() != TRUNCATED_MARK) { - FileObject file = read(stream); - - while (!stack.isEmpty()) { - FileObject current = stack.peek(); - if (isParent(current, file)) { - current.addChild(file); - break; - } else { - stack.pop(); - } - } - - if (file.isDirectory()) { - stack.push(file); - } - last = file; - } - - if (stack.isEmpty()) { - // if the stack is empty, the requested path is probably a file - return last; - } else { - // if the stack is not empty, the requested path is a directory - if (stream.read() == TRUNCATED_MARK) { - stack.getLast().setTruncated(true); - } - return stack.getLast(); - } - } - - private FileObject read(HgInputStream stream) throws IOException { - char type = (char) stream.read(); - - FileObject file; - switch (type) { - case 'd': - file = readDirectory(stream); - break; - case 'f': - file = readFile(stream); - break; - case 's': - file = readSubRepository(stream); - break; - default: - throw new IOException("unknown file object type: " + type); - } - return file; - } - - private boolean isParent(FileObject parent, FileObject child) { - String parentPath = parent.getPath(); - if (parentPath.equals("")) { - return true; - } - return child.getParentPath().equals(parentPath); - } - - private FileObject readDirectory(HgInputStream stream) throws IOException { - FileObject directory = new FileObject(); - String path = removeTrailingSlash(stream.textUpTo('\0')); - - directory.setName(getNameFromPath(path)); - directory.setDirectory(true); - directory.setPath(path); - - return directory; - } - - private FileObject readFile(HgInputStream stream) throws IOException { - FileObject file = new FileObject(); - String path = removeTrailingSlash(stream.textUpTo('\n')); - - file.setName(getNameFromPath(path)); - file.setPath(path); - file.setDirectory(false); - file.setLength((long) stream.decimalIntUpTo(' ')); - - DateTime timestamp = stream.dateTimeUpTo(' '); - String description = stream.textUpTo('\0'); - - if (!disableLastCommit) { - file.setCommitDate(timestamp.getDate().getTime()); - file.setDescription(description); - } - - return file; - } - - private FileObject readSubRepository(HgInputStream stream) throws IOException { - FileObject directory = new FileObject(); - String path = removeTrailingSlash(stream.textUpTo('\n')); - - directory.setName(getNameFromPath(path)); - directory.setDirectory(true); - directory.setPath(path); - - String revision = stream.textUpTo(' '); - String url = stream.textUpTo('\0'); - - SubRepository subRepository = new SubRepository(url); - - if (!Strings.isNullOrEmpty(revision)) { - subRepository.setRevision(revision); - } - - directory.setSubRepository(subRepository); - - return directory; - } - - private String removeTrailingSlash(String path) { - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } - - return path; - } - - private String getNameFromPath(String path) { - int index = path.lastIndexOf('/'); - - if (index > 0) { - path = path.substring(index + 1); - } - - return path; + return new HgFileviewCommandResultReader(stream, disableLastCommit).parseResult(); } /** diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java new file mode 100644 index 0000000000..c3c2efe4b5 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java @@ -0,0 +1,190 @@ +package sonia.scm.repository.spi.javahg; + +import com.aragost.javahg.DateTime; +import com.aragost.javahg.internals.HgInputStream; +import com.google.common.base.Strings; +import sonia.scm.repository.FileObject; +import sonia.scm.repository.SubRepository; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.LinkedList; + +class HgFileviewCommandResultReader { + + private static final char TRUNCATED_MARK = 't'; + + private final HgInputStream stream; + private final boolean disableLastCommit; + + HgFileviewCommandResultReader(HgInputStream stream, boolean disableLastCommit) { + this.stream = stream; + this.disableLastCommit = disableLastCommit; + } + + FileObject parseResult() throws IOException { + Deque stack = new LinkedList<>(); + + FileObject last = null; + while (stream.peek() != -1 && stream.peek() != TRUNCATED_MARK) { + FileObject file = read(stream); + + while (!stack.isEmpty()) { + FileObject current = stack.peek(); + if (isParent(current, file)) { + current.addChild(file); + break; + } else if (isAncestor(current, file)) { + Collection missingParents = createMissingParents(current, file); + for (FileObject subDir : missingParents) { + current.addChild(subDir); + stack.push(subDir); + current = stack.peek(); + } + current.addChild(file); + break; + } else { + stack.pop(); + } + } + + if (file.isDirectory()) { + stack.push(file); + } + last = file; + } + + if (stack.isEmpty()) { + // if the stack is empty, the requested path is probably a file + return last; + } else { + // if the stack is not empty, the requested path is a directory + if (stream.read() == TRUNCATED_MARK) { + stack.getLast().setTruncated(true); + } + return stack.getLast(); + } + } + + private FileObject read(HgInputStream stream) throws IOException { + char type = (char) stream.read(); + + FileObject file; + switch (type) { + case 'd': + file = readDirectory(stream); + break; + case 'f': + file = readFile(stream); + break; + case 's': + file = readSubRepository(stream); + break; + default: + throw new IOException("unknown file object type: " + type); + } + return file; + } + + private boolean isParent(FileObject parent, FileObject child) { + String parentPath = parent.getPath(); + return child.getParentPath().equals(parentPath); + } + + private boolean isAncestor(FileObject ancestor, FileObject child) { + String ancestorPath = ancestor.getPath(); + return ancestorPath.equals("") || child.getParentPath().startsWith(ancestorPath + '/'); + } + + private Collection createMissingParents(FileObject current, FileObject file) { + String missingPath = file.getPath().substring(current.getPath().length(), file.getPath().lastIndexOf('/')); + + FileObject directory = new FileObject(); + directory.setName(getNameFromPath(missingPath)); + directory.setDirectory(true); + directory.setPath(missingPath); + + Collection parents = new ArrayList<>(); + + if (!isParent(current, directory)) { + parents.addAll(createMissingParents(current, directory)); + } + + parents.add(directory); + + return parents; + } + + private FileObject readDirectory(HgInputStream stream) throws IOException { + FileObject directory = new FileObject(); + String path = removeTrailingSlash(stream.textUpTo('\0')); + + directory.setName(getNameFromPath(path)); + directory.setDirectory(true); + directory.setPath(path); + + return directory; + } + + private FileObject readFile(HgInputStream stream) throws IOException { + FileObject file = new FileObject(); + String path = removeTrailingSlash(stream.textUpTo('\n')); + + file.setName(getNameFromPath(path)); + file.setPath(path); + file.setDirectory(false); + file.setLength((long) stream.decimalIntUpTo(' ')); + + DateTime timestamp = stream.dateTimeUpTo(' '); + String description = stream.textUpTo('\0'); + + if (!disableLastCommit) { + file.setCommitDate(timestamp.getDate().getTime()); + file.setDescription(description); + } + + return file; + } + + private FileObject readSubRepository(HgInputStream stream) throws IOException { + FileObject directory = new FileObject(); + String path = removeTrailingSlash(stream.textUpTo('\n')); + + directory.setName(getNameFromPath(path)); + directory.setDirectory(true); + directory.setPath(path); + + String revision = stream.textUpTo(' '); + String url = stream.textUpTo('\0'); + + SubRepository subRepository = new SubRepository(url); + + if (!Strings.isNullOrEmpty(revision)) { + subRepository.setRevision(revision); + } + + directory.setSubRepository(subRepository); + + return directory; + } + + private String removeTrailingSlash(String path) { + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + return path; + } + + private String getNameFromPath(String path) { + int index = path.lastIndexOf('/'); + + if (index > 0) { + path = path.substring(index + 1); + } + + return path; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 7c43ae65ae..329587fc8e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -217,18 +217,21 @@ class File_Printer: self.revCtx = revCtx self.disableLastCommit = disableLastCommit self.transport = transport - self.result_count = -1 + self.result_count = 0 + self.initial_path_printed = False self.limit = limit self.offset = offset def print_directory(self, path): - if self.shouldPrintResult(): + if not self.initial_path_printed or self.offset == 0 or self.shouldPrintResult(): + self.initial_path_printed = True format = '%s/\n' if self.transport: format = 'd%s/\0' self.writer.write( format % path) def print_file(self, path): + self.result_count += 1 if self.shouldPrintResult(): file = self.revCtx[path] date = '0 0' @@ -258,10 +261,7 @@ class File_Printer: self.print_file(file.path) def shouldPrintResult(self): - # The first result is the selected path (or root if not specified). This - # always has to be printed. Therefore we start counting with -1. - self.result_count += 1 - return self.result_count == 0 or self.offset < self.result_count <= self.limit + self.offset + return self.offset < self.result_count <= self.limit + self.offset def isTruncated(self): return self.result_count > self.limit + self.offset diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py index 13be49c5a9..bce3fa0ec0 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py @@ -26,7 +26,7 @@ class File_Object_Collector(): def __init__(self): self.stack = [] - + def __getitem__(self, key): if len(self.stack) == 0 and key == 0: return self.last @@ -43,7 +43,7 @@ class File_Object_Collector(): if file.directory: self.stack.append(file) self.last = file - + class CollectingWriter: def __init__(self): self.stack = [] @@ -85,14 +85,13 @@ class Test_File_Viewer(unittest.TestCase): def test_printer_with_limit(self): paths = ["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"] - writer = self.view_with_limit_and_offset(paths, 3, 0) + writer = self.view_with_limit_and_offset(paths, 1, 0) self.assertPaths(writer, ["/", "c/", "c/g/", "c/g/h.txt"]) - # TODO fix - def x_test_printer_with_offset(self): - paths = ["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"] - writer = self.view_with_limit_and_offset(paths, 100, 3) - self.assertPaths(writer, ["/", "c", "c/d.txt", "c/e.txt", "a", "b", "f.txt"]) + def test_printer_with_offset(self): + paths = ["c/g/h.txt", "c/g/i.txt", "c/d.txt", "c/e.txt", "a", "b", "f.txt"] + writer = self.view_with_limit_and_offset(paths, 100, 1) + self.assertPaths(writer, ["/", "c/g/i.txt", "c/d.txt", "c/e.txt", "a", "b", "f.txt"]) def view_with_limit_and_offset(self, paths, limit, offset): revCtx = DummyRevContext(paths) @@ -113,9 +112,9 @@ class Test_File_Viewer(unittest.TestCase): self.assertTrue(nextChar == " " or nextChar == "\n", expected + " does not match " + actual) def assertPaths(self, actual, expected): + self.assertEqual(len(actual), len(expected)) for idx,item in enumerate(actual): self.assertPath(item, expected[idx]) - self.assertEqual(len(actual), len(expected)) def test_recursive_with_path(self): root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "c", True) @@ -178,7 +177,7 @@ class Test_File_Viewer(unittest.TestCase): self.assertEqual(len(parent), len(expectedPaths)) for idx,item in enumerate(parent.children): self.assertEqual(item.path, expectedPaths[idx]) - + def assertFile(self, file, expectedPath): self.assertEquals(file.path, expectedPath) self.assertFalse(file.directory) diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index c017d0632c..52bc1d0535 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -183,14 +183,14 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { @Test public void testLimit() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(2); + request.setLimit(1); BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); FileObject root = result.getFile(); Collection foList = root.getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "a.txt"); + assertThat(foList).extracting("name").containsExactly("c", "a.txt"); assertThat(root.isTruncated()).isTrue(); } @@ -198,14 +198,14 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { public void testOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setLimit(2); - request.setOffset(2); + request.setOffset(1); BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); FileObject root = result.getFile(); Collection foList = root.getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("b.txt", "f.txt"); + assertThat(foList).extracting("name").containsExactly("b.txt", "f.txt"); assertThat(root.isTruncated()).isFalse(); } @@ -214,7 +214,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { public void testRecursiveLimit() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(4); + request.setLimit(3); request.setRecursive(true); FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); @@ -237,7 +237,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { public void testRecursiveLimitInSubDir() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(2); + request.setLimit(1); request.setRecursive(true); FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); @@ -260,7 +260,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { public void testRecursiveOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setOffset(2); + request.setOffset(1); request.setRecursive(true); FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); @@ -308,8 +308,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); assertNotNull(foList); - assertFalse(foList.isEmpty()); - assertEquals(4, foList.size()); + assertThat(foList).extracting("name").containsExactly("c", "a.txt", "b.txt", "f.txt"); return foList; } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java new file mode 100644 index 0000000000..806896b1f8 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java @@ -0,0 +1,285 @@ +package sonia.scm.repository.spi.javahg; + +import com.aragost.javahg.internals.HgInputStream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import sonia.scm.repository.FileObject; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Iterator; +import java.util.OptionalLong; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.assertj.core.api.Assertions.assertThat; + +class HgFileviewCommandResultReaderTest { + + @Test + void shouldParseSimpleAttributes() throws IOException { + Instant time1 = Instant.now().truncatedTo(ChronoUnit.SECONDS); + Instant time2 = time1.minus(1, ChronoUnit.DAYS); + + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .dir("dir") + .file("a.txt", 10, time1.toEpochMilli(), "file a") + .file("b.txt", 100, time2.toEpochMilli(), "file b\nwith some\nmore text") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.isDirectory()).isTrue(); + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("dir", "a.txt", "b.txt"); + assertThat(fileObject.getChildren()) + .extracting("directory") + .containsExactly(true, false, false); + assertThat(fileObject.getChildren()) + .extracting("length") + .containsExactly(OptionalLong.empty(), OptionalLong.of(10L), OptionalLong.of(100L)); + assertThat(fileObject.getChildren()) + .extracting("description") + .containsExactly(empty(), of("file a"), of("file b\nwith some\nmore text")); + assertThat(fileObject.getChildren()) + .extracting("commitDate") + .containsExactly(OptionalLong.empty(), OptionalLong.of(time1.toEpochMilli()), OptionalLong.of(time2.toEpochMilli())); + assertThat(fileObject.isTruncated()).isFalse(); + } + + @Test + void shouldParseTruncatedFlag() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .dir("dir") + .file("a.txt") + .truncated(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.isTruncated()).isTrue(); + } + + @Test + void shouldParseSubDirectory() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("dir") + .file("dir/a.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.isDirectory()).isTrue(); + assertThat(fileObject.getName()).isEqualTo("dir"); + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("a.txt"); + } + + @Test + void shouldParseRecursiveResult() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .dir("dir") + .dir("dir/more") + .file("dir/more/c.txt") + .file("dir/a.txt") + .file("dir/b.txt") + .file("d.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("dir", "d.txt"); + assertThat(fileObject.getChildren()) + .extracting("directory") + .containsExactly(true, false); + + FileObject subDir = fileObject.getChildren().iterator().next(); + assertThat(subDir.getChildren()) + .extracting("name") + .containsExactly("more", "a.txt", "b.txt"); + assertThat(subDir.getChildren()) + .extracting("directory") + .containsExactly(true, false, false); + + FileObject subSubDir = subDir.getChildren().iterator().next(); + assertThat(subSubDir.getChildren()) + .extracting("name") + .containsExactly("c.txt"); + assertThat(subSubDir.getChildren()) + .extracting("directory") + .containsExactly(false); + } + + @Test + void shouldCreateDirectoriesImplicitly() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .file("dir/a.txt") + .file("dir/b.txt") + .file("d.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("dir", "d.txt"); + assertThat(fileObject.getChildren()) + .extracting("directory") + .containsExactly(true, false); + + FileObject subDir = fileObject.getChildren().iterator().next(); + assertThat(subDir.getChildren()) + .extracting("name") + .containsExactly("a.txt", "b.txt"); + assertThat(subDir.getChildren()) + .extracting("directory") + .containsExactly(false, false); + } + + @Test + void shouldCreateSubSubDirectoriesImplicitly() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .file("dir/more/a.txt") + .file("dir/b.txt") + .file("d.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("dir", "d.txt"); + assertThat(fileObject.getChildren()) + .extracting("directory") + .containsExactly(true, false); + + FileObject subDir = fileObject.getChildren().iterator().next(); + assertThat(subDir.getChildren()) + .extracting("name") + .containsExactly("more", "b.txt"); + assertThat(subDir.getChildren()) + .extracting("directory") + .containsExactly(true, false); + + FileObject subSubDir = subDir.getChildren().iterator().next(); + assertThat(subSubDir.getChildren()) + .extracting("name") + .containsExactly("a.txt"); + assertThat(subSubDir.getChildren()) + .extracting("directory") + .containsExactly(false); + } + + @Test + void shouldCreateSimilarSubDirectoriesCorrectly() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .file("dir/a.txt") + .file("directory/b.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("dir", "directory"); + assertThat(fileObject.getChildren()) + .extracting("directory") + .containsExactly(true, true); + + Iterator fileIterator = fileObject.getChildren().iterator(); + FileObject firstSubDir = fileIterator.next(); + assertThat(firstSubDir.getChildren()) + .extracting("name") + .containsExactly("a.txt"); + assertThat(firstSubDir.getChildren()) + .extracting("directory") + .containsExactly(false); + + FileObject secondSubDir = fileIterator.next(); + assertThat(secondSubDir.getChildren()) + .extracting("name") + .containsExactly("b.txt"); + assertThat(secondSubDir.getChildren()) + .extracting("directory") + .containsExactly(false); + } + + @Test + void shouldIgnoreTimeAndCommentWhenDisabled() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .dir("c") + .file("a.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.getChildren()) + .extracting("description") + .containsOnly(empty()); + assertThat(fileObject.getChildren()) + .extracting("commitDate") + .containsOnly(OptionalLong.empty()); + } + + private HgInputStream createInputStream(String input) { + return new HgInputStream(new ByteArrayInputStream(input.getBytes(UTF_8)), UTF_8.newDecoder()); + } + + private class MockInput { + private final StringBuilder stringBuilder = new StringBuilder(); + private boolean disableLastCommit = false; + + MockInput dir(String name) { + stringBuilder + .append('d') + .append(name) + .append('/') + .append('\0'); + return this; + } + + MockInput file(String name) { + disableLastCommit = true; + return file(name, 1024, 0, "n/a"); + } + + MockInput file(String name, int length, long time, String comment) { + stringBuilder + .append('f') + .append(name) + .append('\n') + .append(length) + .append(' ') + .append(time/1000) + .append(' ') + .append(0) + .append(' ') + .append(comment) + .append('\0'); + return this; + } + + HgFileviewCommandResultReader truncated() { + stringBuilder.append("t"); + return build(); + } + + HgFileviewCommandResultReader build() { + HgInputStream inputStream = createInputStream(stringBuilder.toString()); + return new HgFileviewCommandResultReader(inputStream, disableLastCommit); + } + } +}