aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@sonarsource.com>2015-04-27 16:36:40 +0200
committerJulien Lancelot <julien.lancelot@sonarsource.com>2015-04-27 16:36:40 +0200
commit5a94ef9310cb7049170563d919b900a95c9059f4 (patch)
treefea99aebc0d6ad6f170a36cb81be58662d0764ea /server
parent93099f4d1f8e17ae0bf22b306104aebdb315371d (diff)
downloadsonarqube-5a94ef9310cb7049170563d919b900a95c9059f4.tar.gz
sonarqube-5a94ef9310cb7049170563d919b900a95c9059f4.zip
Add key in /api/sources/lines WS
Diffstat (limited to 'server')
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java52
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java204
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/source/ws/SourcesWsTest.java5
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source.json6
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source_by_file_key.json10
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source_with_params_from_and_to.json2
6 files changed, 183 insertions, 96 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java b/server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java
index c5fb6cf9a01..ff750c5c984 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java
@@ -28,7 +28,9 @@ import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.text.JsonWriter;
import org.sonar.api.web.UserRole;
import org.sonar.core.component.ComponentDto;
-import org.sonar.server.component.ComponentService;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.MyBatis;
+import org.sonar.server.db.DbClient;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.source.HtmlSourceDecorator;
import org.sonar.server.source.index.SourceLineDoc;
@@ -40,14 +42,17 @@ import java.util.List;
public class LinesAction implements SourcesAction {
+ private static final String PARAM_UUID = "uuid";
+ private static final String PARAM_KEY = "key";
+
private final SourceLineIndex sourceLineIndex;
private final HtmlSourceDecorator htmlSourceDecorator;
- private final ComponentService componentService;
+ private final DbClient dbClient;
- public LinesAction(SourceLineIndex sourceLineIndex, HtmlSourceDecorator htmlSourceDecorator, ComponentService componentService) {
+ public LinesAction(DbClient dbClient, SourceLineIndex sourceLineIndex, HtmlSourceDecorator htmlSourceDecorator) {
this.sourceLineIndex = sourceLineIndex;
this.htmlSourceDecorator = htmlSourceDecorator;
- this.componentService = componentService;
+ this.dbClient = dbClient;
}
@Override
@@ -74,12 +79,16 @@ public class LinesAction implements SourcesAction {
.setHandler(this);
action
- .createParam("uuid")
- .setRequired(true)
- .setDescription("File uuid")
+ .createParam(PARAM_UUID)
+ .setDescription("File uuid. Mandatory if param 'key' is not set")
.setExampleValue("f333aab4-7e3a-4d70-87e1-f4c491f05e5c");
action
+ .createParam(PARAM_KEY)
+ .setDescription("File key. Mandatory if param 'uuid' is not set. Available since 5.2")
+ .setExampleValue("org.sample:src/main/java/Foo.java");
+
+ action
.createParam("from")
.setDescription("First line to return. Starts at 1")
.setExampleValue("10")
@@ -93,16 +102,15 @@ public class LinesAction implements SourcesAction {
@Override
public void handle(Request request, Response response) {
- String fileUuid = request.mandatoryParam("uuid");
- ComponentDto component = componentService.getByUuid(fileUuid);
+ ComponentDto component = loadComponent(request);
UserSession.get().checkProjectUuidPermission(UserRole.CODEVIEWER, component.projectUuid());
int from = Math.max(request.mandatoryParamAsInt("from"), 1);
int to = (Integer) ObjectUtils.defaultIfNull(request.paramAsInt("to"), Integer.MAX_VALUE);
- List<SourceLineDoc> sourceLines = sourceLineIndex.getLines(fileUuid, from, to);
+ List<SourceLineDoc> sourceLines = sourceLineIndex.getLines(component.uuid(), from, to);
if (sourceLines.isEmpty()) {
- throw new NotFoundException("File '" + fileUuid + "' has no sources");
+ throw new NotFoundException("File '" + component.key() + "' has no sources");
}
JsonWriter json = response.newJsonWriter().beginObject();
@@ -113,7 +121,7 @@ public class LinesAction implements SourcesAction {
private void writeSource(List<SourceLineDoc> lines, JsonWriter json) {
json.name("sources").beginArray();
- for (SourceLineDoc line: lines) {
+ for (SourceLineDoc line : lines) {
json.beginObject()
.prop("line", line.line())
.prop("code", htmlSourceDecorator.getDecoratedSourceAsHtml(line.source(), line.highlighting(), line.symbols()))
@@ -127,11 +135,29 @@ public class LinesAction implements SourcesAction {
.prop("itLineHits", line.itLineHits())
.prop("itConditions", line.itConditions())
.prop("itCoveredConditions", line.itCoveredConditions());
- if (! line.duplications().isEmpty()) {
+ if (!line.duplications().isEmpty()) {
json.prop("duplicated", true);
}
json.endObject();
}
json.endArray();
}
+
+ private ComponentDto loadComponent(Request request) {
+ DbSession session = dbClient.openSession(false);
+ try {
+ String fileUuid = request.param(PARAM_UUID);
+ if (fileUuid != null) {
+ return dbClient.componentDao().getByUuid(session, fileUuid);
+ }
+ String fileKey = request.param(PARAM_KEY);
+ if (fileKey != null) {
+ return dbClient.componentDao().getByKey(session, fileKey);
+ }
+ throw new IllegalArgumentException(String.format("Param %s or param %s is missing", PARAM_UUID, PARAM_KEY));
+ } finally {
+ MyBatis.closeQuietly(session);
+ }
+ }
+
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java
index 7dbbbb30cfb..f8a3825b08e 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java
@@ -17,21 +17,23 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
package org.sonar.server.source.ws;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import org.apache.commons.lang.StringEscapeUtils;
+import org.junit.After;
import org.junit.Before;
+import org.junit.ClassRule;
import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.runners.MockitoJUnitRunner;
-import org.mockito.stubbing.Answer;
+import org.sonar.api.config.Settings;
import org.sonar.api.web.UserRole;
import org.sonar.core.component.ComponentDto;
-import org.sonar.server.component.ComponentService;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.DbTester;
+import org.sonar.server.component.ComponentTesting;
+import org.sonar.server.component.db.ComponentDao;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.source.HtmlSourceDecorator;
@@ -43,54 +45,67 @@ import org.sonar.server.ws.WsTester;
import java.util.Date;
-import static com.google.common.collect.Lists.newArrayList;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
import static org.junit.Assert.fail;
-import static org.mockito.Matchers.*;
-import static org.mockito.Mockito.when;
-@RunWith(MockitoJUnitRunner.class)
public class LinesActionTest {
- @Mock
+ private static final String PROJECT_UUID = "abcd";
+ private static final String FILE_UUID = "efgh";
+ private static final String FILE_KEY = "Foo.java";
+
+ @ClassRule
+ public static EsTester esTester = new EsTester().addDefinitions(new SourceLineIndexDefinition(new Settings()));
+
+ @ClassRule
+ public static DbTester dbTester = new DbTester();
+
SourceLineIndex sourceLineIndex;
- @Mock
HtmlSourceDecorator htmlSourceDecorator;
- @Mock
- ComponentService componentService;
+ ComponentDao componentDao;
+
+ DbSession session;
- WsTester tester;
+ WsTester wsTester;
@Before
public void setUp() throws Exception {
- tester = new WsTester(new SourcesWs(new LinesAction(sourceLineIndex, htmlSourceDecorator, componentService)));
- when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString())).thenAnswer(new Answer<String>() {
- @Override
- public String answer(InvocationOnMock invocation) throws Throwable {
- return "<span class=\"" + invocation.getArguments()[1] + " sym-" + invocation.getArguments()[2] + "\">" +
- StringEscapeUtils.escapeHtml((String) invocation.getArguments()[0]) +
- "</span>";
- }
- });
+ dbTester.truncateTables();
+ esTester.truncateIndices();
+
+ htmlSourceDecorator = new HtmlSourceDecorator();
+ sourceLineIndex = new SourceLineIndex(esTester.client());
+ componentDao = new ComponentDao();
+ DbClient dbClient = new DbClient(dbTester.database(), dbTester.myBatis(), componentDao);
+ session = dbClient.openSession(false);
+ wsTester = new WsTester(new SourcesWs(new LinesAction(dbClient, sourceLineIndex, htmlSourceDecorator)));
+
+ MockUserSession.set();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ session.close();
}
@Test
public void show_source() throws Exception {
- String projectUuid = "abcd";
- String componentUuid = "efgh";
+ newFile();
+
Date updatedAt = new Date();
String scmDate = "2014-01-01T12:34:56.789Z";
SourceLineDoc line1 = new SourceLineDoc()
- .setProjectUuid(projectUuid)
- .setFileUuid(componentUuid)
+ .setProjectUuid(PROJECT_UUID)
+ .setFileUuid(FILE_UUID)
.setLine(1)
.setScmRevision("cafebabe")
.setScmAuthor("polop")
- .setSource("class Polop {")
- .setHighlighting("h1")
- .setSymbols("palap")
+ .setSource("package org.polop;")
+ .setHighlighting("0,7,k")
+ .setSymbols("8,17,42")
.setUtLineHits(3)
.setUtConditions(2)
.setUtCoveredConditions(1)
@@ -102,33 +117,33 @@ public class LinesActionTest {
line1.setField(SourceLineIndexDefinition.FIELD_SCM_DATE, scmDate);
SourceLineDoc line2 = new SourceLineDoc()
- .setProjectUuid(projectUuid)
- .setFileUuid(componentUuid)
+ .setProjectUuid(PROJECT_UUID)
+ .setFileUuid(FILE_UUID)
.setLine(2)
.setScmRevision("cafebabe")
.setScmAuthor("polop")
- .setSource(" // Empty")
- .setHighlighting("h2")
- .setSymbols("pulup")
+ .setSource("abc")
+ .setHighlighting("0,5,c")
+ .setSymbols("")
.setUtLineHits(3)
.setUtConditions(2)
.setUtCoveredConditions(1)
.setItLineHits(null)
.setItConditions(null)
.setItCoveredConditions(null)
- .setDuplications(ImmutableList.<Integer>of(1))
+ .setDuplications(ImmutableList.of(1))
.setUpdateDate(updatedAt);
line2.setField(SourceLineIndexDefinition.FIELD_SCM_DATE, scmDate);
SourceLineDoc line3 = new SourceLineDoc()
- .setProjectUuid(projectUuid)
- .setFileUuid(componentUuid)
+ .setProjectUuid(PROJECT_UUID)
+ .setFileUuid(FILE_UUID)
.setLine(3)
.setScmRevision("cafebabe")
.setScmAuthor("polop")
.setSource("}")
- .setHighlighting("h3")
- .setSymbols("pylyp")
+ .setHighlighting(null)
+ .setSymbols(null)
.setUtLineHits(null)
.setUtConditions(null)
.setUtCoveredConditions(null)
@@ -139,33 +154,22 @@ public class LinesActionTest {
.setUpdateDate(updatedAt);
line3.setField(SourceLineIndexDefinition.FIELD_SCM_DATE, scmDate);
- when(sourceLineIndex.getLines(eq(componentUuid), anyInt(), anyInt())).thenReturn(newArrayList(
- line1,
- line2,
- line3
- ));
+ esTester.putDocuments(SourceLineIndexDefinition.INDEX, SourceLineIndexDefinition.TYPE, line1, line2, line3);
- String componentKey = "componentKey";
- when(componentService.getByUuid(componentUuid)).thenReturn(new ComponentDto().setKey(componentKey).setProjectUuid(projectUuid));
- MockUserSession.set().setLogin("login").addProjectUuidPermissions(UserRole.CODEVIEWER, projectUuid);
+ MockUserSession.set().setLogin("login").addProjectUuidPermissions(UserRole.CODEVIEWER, PROJECT_UUID);
- WsTester.TestRequest request = tester.newGetRequest("api/sources", "lines").setParam("uuid", componentUuid);
- // Using non-strict match b/c of dates
+ WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("uuid", FILE_UUID);
request.execute().assertJson(getClass(), "show_source.json");
}
@Test
public void fail_to_show_source_if_no_source_found() throws Exception {
- String componentUuid = "abcd";
- String projectUuid = "efgh";
- when(sourceLineIndex.getLines(anyString(), anyInt(), anyInt())).thenReturn(Lists.<SourceLineDoc>newArrayList());
+ newFile();
- String componentKey = "componentKey";
- when(componentService.getByUuid(componentUuid)).thenReturn(new ComponentDto().setKey(componentKey).setProjectUuid(projectUuid));
- MockUserSession.set().setLogin("login").addProjectUuidPermissions(UserRole.CODEVIEWER, projectUuid);
+ MockUserSession.set().setLogin("login").addProjectUuidPermissions(UserRole.CODEVIEWER, PROJECT_UUID);
try {
- WsTester.TestRequest request = tester.newGetRequest("api/sources", "lines").setParam("uuid", componentUuid);
+ WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("uuid", FILE_UUID);
request.execute();
fail();
} catch (Exception e) {
@@ -175,17 +179,14 @@ public class LinesActionTest {
@Test
public void show_source_with_from_and_to_params() throws Exception {
- String projectUuid = "abcd";
- String fileUuid = "efgh";
+ newFile();
- String componentKey = "componentKey";
- when(componentService.getByUuid(fileUuid)).thenReturn(new ComponentDto().setKey(componentKey).setProjectUuid(projectUuid));
- MockUserSession.set().setLogin("login").addProjectUuidPermissions(UserRole.CODEVIEWER, projectUuid);
+ MockUserSession.set().setLogin("login").addProjectUuidPermissions(UserRole.CODEVIEWER, PROJECT_UUID);
- when(sourceLineIndex.getLines(fileUuid, 3, 3)).thenReturn(newArrayList(
+ esTester.putDocuments(SourceLineIndexDefinition.INDEX, SourceLineIndexDefinition.TYPE,
new SourceLineDoc()
- .setProjectUuid(projectUuid)
- .setFileUuid(fileUuid)
+ .setProjectUuid(PROJECT_UUID)
+ .setFileUuid(FILE_UUID)
.setLine(3)
.setScmRevision("cafebabe")
.setScmDate(null)
@@ -201,25 +202,76 @@ public class LinesActionTest {
.setItCoveredConditions(null)
.setDuplications(null)
.setUpdateDate(new Date())
- ));
- WsTester.TestRequest request = tester
+ );
+
+ WsTester.TestRequest request = wsTester
.newGetRequest("api/sources", "lines")
- .setParam("uuid", fileUuid)
+ .setParam("uuid", FILE_UUID)
.setParam("from", "3")
.setParam("to", "3");
request.execute().assertJson(getClass(), "show_source_with_params_from_and_to.json");
}
+ @Test
+ public void show_source_by_file_key() throws Exception {
+ newFile();
+
+ esTester.putDocuments(SourceLineIndexDefinition.INDEX, SourceLineIndexDefinition.TYPE,
+ new SourceLineDoc()
+ .setProjectUuid(PROJECT_UUID)
+ .setFileUuid(FILE_UUID)
+ .setLine(3)
+ .setScmRevision("cafebabe")
+ .setScmDate(null)
+ .setScmAuthor("polop")
+ .setSource("}")
+ .setHighlighting("")
+ .setSymbols("")
+ .setUtLineHits(null)
+ .setUtConditions(null)
+ .setUtCoveredConditions(null)
+ .setItLineHits(null)
+ .setItConditions(null)
+ .setItCoveredConditions(null)
+ .setDuplications(null)
+ .setUpdateDate(new Date())
+ );
+
+ MockUserSession.set().setLogin("login").addProjectUuidPermissions(UserRole.CODEVIEWER, PROJECT_UUID);
+
+ WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("key", FILE_KEY);
+ request.execute().assertJson(getClass(), "show_source_by_file_key.json");
+ }
+
+ @Test
+ public void fail_when_no_uuid_or_key_param() throws Exception {
+ newFile();
+ MockUserSession.set().setLogin("login").addProjectUuidPermissions(UserRole.CODEVIEWER, PROJECT_UUID);
+ WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines");
+
+ try {
+ request.execute();
+ failBecauseExceptionWasNotThrown(IllegalArgumentException.class);
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessage("Param uuid or param key is missing");
+ }
+ }
+
@Test(expected = ForbiddenException.class)
public void should_check_permission() throws Exception {
- String fileUuid = "efgh";
+ newFile();
- String componentKey = "componentKey";
- when(componentService.getByUuid(fileUuid)).thenReturn(new ComponentDto().setKey(componentKey));
MockUserSession.set().setLogin("login");
- tester.newGetRequest("api/sources", "lines")
- .setParam("uuid", fileUuid)
+ wsTester.newGetRequest("api/sources", "lines")
+ .setParam("uuid", FILE_UUID)
.execute();
}
+
+ private void newFile(){
+ ComponentDto project = ComponentTesting.newProjectDto(PROJECT_UUID);
+ ComponentDto file = ComponentTesting.newFileDto(project, FILE_UUID).setKey(FILE_KEY);
+ componentDao.insert(session, project, file);
+ session.commit();
+ }
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/source/ws/SourcesWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/source/ws/SourcesWsTest.java
index 491d42912e6..32a7887d33c 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/source/ws/SourcesWsTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/source/ws/SourcesWsTest.java
@@ -22,7 +22,6 @@ package org.sonar.server.source.ws;
import org.junit.Test;
import org.sonar.api.server.ws.WebService;
-import org.sonar.server.component.ComponentService;
import org.sonar.server.db.DbClient;
import org.sonar.server.source.HtmlSourceDecorator;
import org.sonar.server.source.SourceService;
@@ -36,7 +35,7 @@ public class SourcesWsTest {
ShowAction showAction = new ShowAction(mock(SourceService.class), mock(DbClient.class));
RawAction rawAction = new RawAction(mock(DbClient.class), mock(SourceService.class));
- LinesAction linesAction = new LinesAction(mock(SourceLineIndex.class), mock(HtmlSourceDecorator.class), mock(ComponentService.class));
+ LinesAction linesAction = new LinesAction(mock(DbClient.class), mock(SourceLineIndex.class), mock(HtmlSourceDecorator.class));
HashAction hashAction = new HashAction(mock(DbClient.class));
IndexAction indexAction = new IndexAction(mock(DbClient.class), mock(SourceService.class));
WsTester tester = new WsTester(new SourcesWs(showAction, rawAction, linesAction, hashAction, indexAction));
@@ -71,7 +70,7 @@ public class SourcesWsTest {
assertThat(lines.since()).isEqualTo("5.0");
assertThat(lines.isInternal()).isTrue();
assertThat(lines.responseExampleAsString()).isNotEmpty();
- assertThat(lines.params()).hasSize(3);
+ assertThat(lines.params()).hasSize(4);
WebService.Action hash = controller.action("hash");
assertThat(hash).isNotNull();
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source.json b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source.json
index 7655e20c817..eebd43bc502 100644
--- a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source.json
+++ b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source.json
@@ -1,7 +1,7 @@
{
"sources": [
{
- "code": "<span class=\"h1 sym-palap\">class Polop {</span>",
+ "code": "<span class=\"k\">package</span> <span class=\"sym-42 sym\">org.polop</span>;",
"line": 1,
"scmAuthor": "polop",
"scmRevision": "cafebabe",
@@ -13,7 +13,7 @@
"itCoveredConditions": 1
},
{
- "code": "<span class=\"h2 sym-pulup\"> // Empty</span>",
+ "code": "<span class=\"c\">abc</span>",
"line": 2,
"scmAuthor": "polop",
"scmRevision": "cafebabe",
@@ -23,7 +23,7 @@
"duplicated": true
},
{
- "code": "<span class=\"h3 sym-pylyp\">}</span>",
+ "code": "}",
"line": 3,
"scmAuthor": "polop",
"scmRevision": "cafebabe",
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source_by_file_key.json b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source_by_file_key.json
new file mode 100644
index 00000000000..ba2a4c6e52b
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source_by_file_key.json
@@ -0,0 +1,10 @@
+{
+ "sources": [
+ {
+ "code": "}",
+ "line": 3,
+ "scmAuthor": "polop",
+ "scmRevision": "cafebabe"
+ }
+ ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source_with_params_from_and_to.json b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source_with_params_from_and_to.json
index 8845334e1d9..ba2a4c6e52b 100644
--- a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source_with_params_from_and_to.json
+++ b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_source_with_params_from_and_to.json
@@ -1,7 +1,7 @@
{
"sources": [
{
- "code": "<span class=\" sym-\">}</span>",
+ "code": "}",
"line": 3,
"scmAuthor": "polop",
"scmRevision": "cafebabe"