Merge with upstream

This commit is contained in:
Rene Pfeuffer
2020-03-06 16:19:35 +01:00
6 changed files with 499 additions and 164 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,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<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 if (isAncestor(current, file)) {
Collection<FileObject> 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<FileObject> 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<FileObject> 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;
}
}

View File

@@ -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

View File

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

View File

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

View File

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