Extract parsing of fileview stream and bootstrap tests

This commit is contained in:
René Pfeuffer
2020-03-04 09:30:25 +01:00
parent 5fb338fbd2
commit f2ce14294d
3 changed files with 330 additions and 139 deletions

View File

@@ -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<FileObject> 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();
}
/**

View File

@@ -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<FileObject> 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;
}
}

View File

@@ -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);
}
}
}