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..b93b9b2fb0 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java @@ -0,0 +1,158 @@ +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.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 { + 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; + } +} 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..33ed321867 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java @@ -0,0 +1,171 @@ +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.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 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 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); + } + } +}