Merge branch 'develop' into feature/packaging

This commit is contained in:
Sebastian Sdorra
2020-05-25 06:57:22 +02:00
103 changed files with 2957 additions and 1347 deletions

View File

@@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Detect renamed files in git and hg diffs ([#1157](https://github.com/scm-manager/scm-manager/pull/1157))
### Fixed
- Correctly resolve Links in markdown files ([#1152](https://github.com/scm-manager/scm-manager/pull/1152))
- Missing copy on write in the data store ([#1155](https://github.com/scm-manager/scm-manager/pull/1155))
- Resolved conflicting dependencies for scm-webapp ([#1159](https://github.com/scm-manager/scm-manager/pull/1159))
## [2.0.0-rc8] - 2020-05-08
### Added
@@ -26,7 +33,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Handle obscure line breaks in diff viewer ([#1129](https://github.com/scm-manager/scm-manager/pull/1129))
- Validate subversion client checksum ([#1113](https://github.com/scm-manager/scm-manager/issues/1113))
- Fix plugin manage permission ([#1135](https://github.com/scm-manager/scm-manager/pull/1135))
- Missing copy on write in the data store ([#1155](https://github.com/scm-manager/scm-manager/pull/1155))
## [2.0.0-rc7] - 2020-04-09
### Added

3
Jenkinsfile vendored
View File

@@ -194,6 +194,7 @@ Maven setupMavenBuild() {
def logConf = "scm-webapp/src/main/resources/logback.ci.xml"
mvn.additionalArgs += " -Dlogback.configurationFile=${logConf}"
mvn.additionalArgs += " -Dscm-it.logbackConfiguration=${logConf}"
mvn.additionalArgs += " -Dsonar.coverage.exclusions=**/*.test.ts,**/*.test.tsx,**/*.stories.tsx"
if (isMainBranch() || isReleaseBranch()) {
// Release starts javadoc, which takes very long, so do only for certain branches
@@ -218,7 +219,7 @@ boolean isMainBranch() {
boolean waitForQualityGateWebhookToBeCalled() {
boolean isQualityGateSucceeded = true
timeout(time: 5, unit: 'MINUTES') { // Needed when there is no webhook for example
timeout(time: 10, unit: 'MINUTES') { // Needed when there is no webhook for example
def qGate = waitForQualityGate()
echo "SonarQube Quality Gate status: ${qGate.status}"
if (qGate.status != 'OK') {

View File

@@ -20,7 +20,7 @@
},
"resolutions": {
"babel-core": "7.0.0-bridge.0",
"gitdiff-parser": "https://github.com/scm-manager/gitdiff-parser#ed3fe7de73dbb0a06c3e6adbbdf22dbae6e66351",
"gitdiff-parser": "https://github.com/scm-manager/gitdiff-parser#617747460280bf4522bb84d217a9064ac8eb6d3d",
"lowlight": "1.13.1"
},
"babel": {

83
pom.xml
View File

@@ -41,7 +41,6 @@
<url>https://github.com/scm-manager/scm-manager</url>
<licenses>
<license>
<name>MIT License</name>
@@ -82,7 +81,7 @@
<ciManagement>
<system>Jenkins</system>
<url>https://scm-manager.ci.cloudbees.com/</url>
<url>https://oss.cloudogu.com/jenkins/</url>
</ciManagement>
<prerequisites>
@@ -153,11 +152,6 @@
<artifactId>junit-vintage-engine</artifactId>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
@@ -242,6 +236,16 @@
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-core</artifactId>
<version>${resteasy.version}</version>
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.ws.rs</groupId>
<artifactId>jboss-jaxrs-api_2.1_spec</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.spec.javax.xml.bind</groupId>
<artifactId>jboss-jaxb-api_2.3_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
@@ -254,6 +258,12 @@
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
<version>${resteasy.version}</version>
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.xml.bind</groupId>
<artifactId>jboss-jaxb-api_2.3_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
@@ -268,6 +278,30 @@
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client-api</artifactId>
<version>${resteasy.version}</version>
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.ws.rs</groupId>
<artifactId>jboss-jaxrs-api_2.1_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<version>${resteasy.version}</version>
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.ws.rs</groupId>
<artifactId>jboss-jaxrs-api_2.1_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-guice</artifactId>
@@ -278,6 +312,12 @@
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-servlet-initializer</artifactId>
<version>${resteasy.version}</version>
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.ws.rs</groupId>
<artifactId>jboss-jaxrs-api_2.1_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--
@@ -357,13 +397,6 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>0.5.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@@ -419,6 +452,12 @@
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- logging -->
@@ -438,8 +477,8 @@
<!-- xml -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>${jaxb.version}</version>
</dependency>
@@ -449,12 +488,6 @@
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- utils -->
<dependency>
@@ -821,20 +854,20 @@
<properties>
<!-- test libraries -->
<mockito.version>2.28.2</mockito.version>
<hamcrest.version>1.3</hamcrest.version>
<hamcrest.version>2.1</hamcrest.version>
<junit.version>5.6.2</junit.version>
<!-- logging libraries -->
<slf4j.version>1.7.30</slf4j.version>
<logback.version>1.2.3</logback.version>
<servlet.version>3.0.1</servlet.version>
<servlet.version>3.1.0</servlet.version>
<jaxrs.version>2.1.1</jaxrs.version>
<resteasy.version>4.5.3.Final</resteasy.version>
<jersey-client.version>1.19.4</jersey-client.version>
<jackson.version>2.11.0</jackson.version>
<guice.version>4.2.3</guice.version>
<jaxb.version>2.3.1</jaxb.version>
<jaxb.version>2.3.3</jaxb.version>
<hibernate-validator.version>6.1.4.Final</hibernate-validator.version>
<!-- event bus -->

View File

@@ -173,8 +173,8 @@
<!-- xml -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
@@ -182,11 +182,6 @@
<artifactId>jaxb-runtime</artifactId>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</dependency>
<!-- validation -->
<dependency>

View File

@@ -39,6 +39,7 @@ import java.io.IOException;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
/**
@@ -245,6 +246,20 @@ public class GZipResponseStream extends ServletOutputStream
return closed;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
try {
writeListener.onWritePossible();
} catch (IOException e) {
logger.debug("could not call writeListener.onWritePossible()", e);
}
}
//~--- fields ---------------------------------------------------------------
/** Field description */

View File

@@ -0,0 +1,41 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import lombok.Value;
import java.util.stream.Stream;
import static java.util.stream.Stream.of;
@Value
public class Added extends Modification {
private final String path;
@Override
Stream<String> getEffectedPaths() {
return of(path);
}
}

View File

@@ -0,0 +1,42 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import lombok.Value;
import java.util.stream.Stream;
import static java.util.stream.Stream.of;
@Value
public class Copied extends Modification {
private final String sourcePath;
private final String targetPath;
@Override
Stream<String> getEffectedPaths() {
return of(targetPath);
}
}

View File

@@ -0,0 +1,31 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import java.util.stream.Stream;
abstract class EffectedPath {
abstract Stream<String> getEffectedPaths();
}

View File

@@ -0,0 +1,30 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import java.io.Serializable;
public abstract class Modification extends EffectedPath implements Serializable {
}

View File

@@ -24,71 +24,78 @@
package sonia.scm.repository;
import com.google.common.collect.Lists;
import com.google.common.collect.ImmutableList;
import lombok.EqualsAndHashCode;
import lombok.Setter;
import lombok.Getter;
import lombok.ToString;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
@EqualsAndHashCode
@ToString
@Setter
@Getter
public class Modifications implements Serializable {
private static final long serialVersionUID = -8902033326668658140L;
private String revision;
/**
* lists of changed files
*/
private List<String> added;
private List<String> modified;
private List<String> removed;
private final String revision;
private final Collection<Modification> modifications;
public Modifications() {
public Modifications(String revision, Modification... modifications) {
this(revision, asList(modifications));
}
public Modifications(List<String> added) {
this(added, null, null);
public Modifications(String revision, Collection<Modification> modifications) {
this.revision = revision;
this.modifications = ImmutableList.copyOf(modifications);
}
public Modifications(List<String> added, List<String> modified) {
this(added, modified, null);
public List<String> getEffectedPaths() {
return effectedPathsStream().collect(toList());
}
public Modifications(List<String> added, List<String> modified, List<String> removed) {
this.added = added;
this.modified = modified;
this.removed = removed;
public Stream<String> effectedPathsStream() {
return modifications.stream().flatMap(Modification::getEffectedPaths);
}
public List<String> getAdded() {
if (added == null) {
added = Lists.newArrayList();
public List<Added> getAdded() {
return modifications.stream()
.filter(m -> m instanceof Added)
.map(m -> (Added) m)
.collect(toList());
}
return added;
public List<Removed> getRemoved() {
return modifications.stream()
.filter(m -> m instanceof Removed)
.map(m -> (Removed) m)
.collect(toList());
}
public List<String> getModified() {
if (modified == null) {
modified = Lists.newArrayList();
public List<Modified> getModified() {
return modifications.stream()
.filter(m -> m instanceof Modified)
.map(m -> (Modified) m)
.collect(toList());
}
return modified;
public List<Renamed> getRenamed() {
return modifications.stream()
.filter(m -> m instanceof Renamed)
.map(m -> (Renamed) m)
.collect(toList());
}
public List<String> getRemoved() {
if (removed == null) {
removed = Lists.newArrayList();
}
return removed;
}
public String getRevision() {
return revision;
public List<Copied> getCopied() {
return modifications.stream()
.filter(m -> m instanceof Copied)
.map(m -> (Copied) m)
.collect(toList());
}
}

View File

@@ -0,0 +1,41 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import lombok.Value;
import java.util.stream.Stream;
import static java.util.stream.Stream.of;
@Value
public class Modified extends Modification {
private final String path;
@Override
Stream<String> getEffectedPaths() {
return of(path);
}
}

View File

@@ -0,0 +1,41 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import lombok.Value;
import java.util.stream.Stream;
import static java.util.stream.Stream.of;
@Value
public class Removed extends Modification {
private final String path;
@Override
Stream<String> getEffectedPaths() {
return of(path);
}
}

View File

@@ -0,0 +1,42 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import lombok.Value;
import java.util.stream.Stream;
import static java.util.stream.Stream.of;
@Value
public class Renamed extends Modification {
private final String oldPath;
private final String newPath;
@Override
Stream<String> getEffectedPaths() {
return of(oldPath, newPath);
}
}

View File

@@ -33,4 +33,10 @@ public interface DiffFile extends Iterable<Hunk> {
String getOldPath();
String getNewPath();
ChangeType getChangeType();
enum ChangeType {
ADD, MODIFY, DELETE, RENAME, COPY
}
}

View File

@@ -36,6 +36,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
@@ -197,6 +198,25 @@ public class BufferedHttpServletRequest extends HttpServletRequestWrapper
return bais.read(buf, off, len);
}
@Override
public boolean isFinished() {
return bais.available() == 0;
}
@Override
public boolean isReady() {
return bais.available() > 0;
}
@Override
public void setReadListener(ReadListener readListener) {
try {
readListener.onDataAvailable();
} catch (IOException e) {
logger.debug("could not call readListener.onDataAvailable()", e);
}
}
//~--- fields -------------------------------------------------------------
/** Field description */

View File

@@ -26,6 +26,9 @@ package sonia.scm.web.filter;
//~--- JDK imports ------------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
@@ -37,6 +40,7 @@ import java.util.Map;
import java.util.Set;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
@@ -48,6 +52,8 @@ import javax.servlet.http.HttpServletResponseWrapper;
public class BufferedHttpServletResponse extends HttpServletResponseWrapper
{
private static final Logger LOG = LoggerFactory.getLogger(BufferedHttpServletResponse.class);
/**
* Constructs ...
*
@@ -445,6 +451,20 @@ public class BufferedHttpServletResponse extends HttpServletResponseWrapper
baos.write(param);
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
try {
writeListener.onWritePossible();
} catch (IOException e) {
LOG.debug("could not call writeListener.onWritePossible()", e);
}
}
//~--- fields -------------------------------------------------------------
/** Field description */

View File

@@ -27,15 +27,13 @@ package sonia.scm;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(TempDirectory.class)
class BasicContextProviderTest {
@Nested
@@ -68,13 +66,13 @@ class BasicContextProviderTest {
private BasicContextProvider context;
@BeforeEach
void setUpContext(@TempDirectory.TempDir Path baseDirectory) {
void setUpContext(@TempDir Path baseDirectory) {
this.baseDirectory = baseDirectory;
context = new BasicContextProvider(baseDirectory.toFile(), "x.y.z", Stage.PRODUCTION);
}
@Test
void shouldReturnAbsolutePathAsIs(@TempDirectory.TempDir Path path) {
void shouldReturnAbsolutePathAsIs(@TempDir Path path) {
Path absolutePath = path.toAbsolutePath();
Path resolved = context.resolve(absolutePath);

View File

@@ -0,0 +1,71 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class ModificationsTest {
public static final Modifications MODIFICATIONS = new Modifications("123",
new Added("added"),
new Removed("removed"),
new Modified("modified"),
new Renamed("rename from", "rename to"),
new Copied("copy from", "copy to")
);
@Test
void shouldFindAddedFilesAsEffected() {
assertThat(MODIFICATIONS.getEffectedPaths())
.contains("added");
}
@Test
void shouldFindRemovedFilesAsEffected() {
assertThat(MODIFICATIONS.getEffectedPaths())
.contains("removed");
}
@Test
void shouldFindModifiedFilesAsEffected() {
assertThat(MODIFICATIONS.getEffectedPaths())
.contains("modified");
}
@Test
void shouldFindRenamedFilesAsEffected() {
assertThat(MODIFICATIONS.getEffectedPaths())
.contains("rename from", "rename to");
}
@Test
void shouldFindTargetOfCopiedFilesAsEffected() {
assertThat(MODIFICATIONS.getEffectedPaths())
.contains("copy to")
.doesNotContain("copy from");
}
}

View File

@@ -28,7 +28,7 @@ import com.google.common.io.ByteSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
@@ -57,7 +57,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class ModifyCommandBuilderTest {
@Mock
@@ -71,7 +70,7 @@ class ModifyCommandBuilderTest {
Path workdir;
@BeforeEach
void initWorkdir(@TempDirectory.TempDir Path temp) throws IOException {
void initWorkdir(@TempDir Path temp) throws IOException {
workdir = Files.createDirectory(temp.resolve("workdir"));
lenient().when(workdirProvider.createNewWorkdir()).thenReturn(workdir.toFile());
commandBuilder = new ModifyCommandBuilder(command, workdirProvider);
@@ -207,7 +206,7 @@ class ModifyCommandBuilderTest {
}
@Test
void shouldDeleteTemporaryFiles(@TempDirectory.TempDir Path temp) throws IOException {
void shouldDeleteTemporaryFiles(@TempDir Path temp) throws IOException {
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<File> fileCaptor = ArgumentCaptor.forClass(File.class);
doNothing().when(worker).modify(nameCaptor.capture(), fileCaptor.capture());

View File

@@ -25,8 +25,7 @@
package sonia.scm.repository.spi;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import sonia.scm.repository.Repository;
import java.io.File;
@@ -36,11 +35,10 @@ import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(TempDirectory.class)
class ModifyWorkerHelperTest {
@Test
void shouldKeepExecutableFlag(@TempDirectory.TempDir Path temp) throws IOException {
void shouldKeepExecutableFlag(@TempDir Path temp) throws IOException {
File target = createFile(temp, "executable.sh");
File newFile = createFile(temp, "other");

View File

@@ -27,7 +27,7 @@ package sonia.scm.security;
import com.google.common.io.Files;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
@@ -45,7 +45,7 @@ import static org.mockito.Mockito.when;
*
* @author Sebastian Sdorra
*/
@ExtendWith({MockitoExtension.class, TempDirectory.class})
@ExtendWith({MockitoExtension.class})
public class DefaultCipherHandlerTest {
@Mock
@@ -58,7 +58,7 @@ public class DefaultCipherHandlerTest {
* Tests loading and storing default key.
*/
@Test
void shouldLoadAndStoreDefaultKey(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldLoadAndStoreDefaultKey(@TempDir Path tempDir) throws IOException {
File baseDirectory = tempDir.toFile();
when(context.getBaseDirectory()).thenReturn(baseDirectory);
@@ -84,7 +84,7 @@ public class DefaultCipherHandlerTest {
@Test
@SuppressWarnings("UnstableApiUsage") // is ok for unit test
void shouldReEncodeOldFormattedDefaultKey(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldReEncodeOldFormattedDefaultKey(@TempDir Path tempDir) throws IOException {
String oldKey = "17eXopruTtX3S4dJ9KTEmbZ-vfZztw==";
String encryptedValue = "A11kQF7wytpWCkjPflxJB-zUWJ1CVKU3qhwhRFq4Pvl6XqiS9V2w-gqNktqMX6YNDw==";
String plainValue = "Marvin The Paranoid Android - RAM";

View File

@@ -25,8 +25,7 @@
package sonia.scm.xml;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import javax.xml.bind.JAXB;
import javax.xml.bind.annotation.XmlAccessType;
@@ -36,13 +35,12 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.nio.file.Path;
import java.time.Instant;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(TempDirectory.class)
class XmlInstantAdapterTest {
@Test
void shouldMarshalAndUnmarshalInstant(@TempDirectory.TempDir Path tempDirectory) {
void shouldMarshalAndUnmarshalInstant(@TempDir Path tempDirectory) {
Path path = tempDirectory.resolve("instant.xml");
Instant instant = Instant.now();

View File

@@ -29,7 +29,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
@@ -52,7 +52,6 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class PathBasedRepositoryLocationResolverTest {
@@ -74,7 +73,7 @@ class PathBasedRepositoryLocationResolverTest {
private PathBasedRepositoryLocationResolver resolver;
@BeforeEach
void beforeEach(@TempDirectory.TempDir Path temp) {
void beforeEach(@TempDir Path temp) {
this.basePath = temp;
when(contextProvider.getBaseDirectory()).thenReturn(temp.toFile());
when(contextProvider.resolve(any(Path.class))).thenAnswer(invocation -> invocation.getArgument(0));

View File

@@ -31,7 +31,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -61,7 +61,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith({MockitoExtension.class, TempDirectory.class})
@ExtendWith({MockitoExtension.class})
@MockitoSettings(strictness = Strictness.LENIENT)
class XmlRepositoryDAOTest {
@@ -76,7 +76,7 @@ class XmlRepositoryDAOTest {
private XmlRepositoryDAO dao;
@BeforeEach
void createDAO(@TempDirectory.TempDir Path basePath) {
void createDAO(@TempDir Path basePath) {
when(locationResolver.create(Path.class)).thenReturn(
new RepositoryLocationResolver.RepositoryLocationResolverInstance<Path>() {
@Override
@@ -103,7 +103,7 @@ class XmlRepositoryDAOTest {
when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString()));
}
private Path createMockedRepoPath(@TempDirectory.TempDir Path basePath, InvocationOnMock invocation) {
private Path createMockedRepoPath(@TempDir Path basePath, InvocationOnMock invocation) {
Path resolvedPath = basePath.resolve(invocation.getArgument(0).toString());
try {
Files.createDirectories(resolvedPath);
@@ -337,7 +337,7 @@ class XmlRepositoryDAOTest {
private Path repositoryPath;
@BeforeEach
void createMetadataFileForRepository(@TempDirectory.TempDir Path basePath) throws IOException {
void createMetadataFileForRepository(@TempDir Path basePath) throws IOException {
repositoryPath = basePath.resolve("existing");
Files.createDirectories(repositoryPath);

View File

@@ -26,8 +26,7 @@ package sonia.scm.store;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -38,11 +37,10 @@ import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static sonia.scm.store.CopyOnWrite.withTemporaryFile;
@ExtendWith(TempDirectory.class)
class CopyOnWriteTest {
@Test
void shouldCreateNewFile(@TempDirectory.TempDir Path tempDir) {
void shouldCreateNewFile(@TempDir Path tempDir) {
Path expectedFile = tempDir.resolve("toBeCreated.txt");
withTemporaryFile(
@@ -53,7 +51,7 @@ class CopyOnWriteTest {
}
@Test
void shouldOverwriteExistingFile(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldOverwriteExistingFile(@TempDir Path tempDir) throws IOException {
Path expectedFile = tempDir.resolve("toBeOverwritten.txt");
Files.createFile(expectedFile);
@@ -65,7 +63,7 @@ class CopyOnWriteTest {
}
@Test
void shouldFailForDirectory(@TempDirectory.TempDir Path tempDir) {
void shouldFailForDirectory(@TempDir Path tempDir) {
assertThrows(IllegalArgumentException.class,
() -> withTemporaryFile(
file -> new FileOutputStream(file.toFile()).write("should not be written".getBytes()),
@@ -82,7 +80,7 @@ class CopyOnWriteTest {
}
@Test
void shouldKeepBackupIfTemporaryFileCouldNotBeWritten(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldKeepBackupIfTemporaryFileCouldNotBeWritten(@TempDir Path tempDir) throws IOException {
Path unchangedOriginalFile = tempDir.resolve("notToBeDeleted.txt");
new FileOutputStream(unchangedOriginalFile.toFile()).write("this should be kept".getBytes());
@@ -98,7 +96,7 @@ class CopyOnWriteTest {
}
@Test
void shouldNotWrapRuntimeExceptions(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldNotWrapRuntimeExceptions(@TempDir Path tempDir) throws IOException {
Path someFile = tempDir.resolve("something.txt");
assertThrows(
@@ -111,7 +109,7 @@ class CopyOnWriteTest {
}
@Test
void shouldKeepBackupIfTemporaryFileIsMissing(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldKeepBackupIfTemporaryFileIsMissing(@TempDir Path tempDir) throws IOException {
Path backedUpFile = tempDir.resolve("notToBeDeleted.txt");
new FileOutputStream(backedUpFile.toFile()).write("this should be kept".getBytes());
@@ -125,7 +123,7 @@ class CopyOnWriteTest {
}
@Test
void shouldDeleteExistingFile(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldDeleteExistingFile(@TempDir Path tempDir) throws IOException {
Path expectedFile = tempDir.resolve("toBeReplaced.txt");
new FileOutputStream(expectedFile.toFile()).write("this should be removed".getBytes());

View File

@@ -28,7 +28,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
@@ -48,7 +48,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
@ExtendWith(TempDirectory.class)
@ExtendWith(MockitoExtension.class)
class JAXBPropertyFileAccessTest {
@@ -63,7 +62,7 @@ class JAXBPropertyFileAccessTest {
JAXBPropertyFileAccess fileAccess;
@BeforeEach
void initTempDir(@TempDirectory.TempDir Path tempDir) {
void initTempDir(@TempDir Path tempDir) {
lenient().when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
lenient().when(contextProvider.resolve(any())).thenAnswer(invocation -> tempDir.resolve(invocation.getArgument(0).toString()));
@@ -99,7 +98,7 @@ class JAXBPropertyFileAccessTest {
}
@Test
void shouldMoveStoreFileToRepositoryBasedLocation(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldMoveStoreFileToRepositoryBasedLocation(@TempDir Path tempDir) throws IOException {
createV1StoreFile(tempDir, "myStore.xml");
fileAccess.forStoreName(STORE_NAME).moveAsRepositoryStore(Paths.get("myStore.xml"), REPOSITORY_ID);
@@ -108,7 +107,7 @@ class JAXBPropertyFileAccessTest {
}
@Test
void shouldMoveAllStoreFilesToRepositoryBasedLocations(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldMoveAllStoreFilesToRepositoryBasedLocations(@TempDir Path tempDir) throws IOException {
locationResolver.forClass(Path.class).createLocation("repoId2");
createV1StoreFile(tempDir, REPOSITORY_ID + ".xml");
@@ -122,7 +121,7 @@ class JAXBPropertyFileAccessTest {
}
}
private void createV1StoreFile(@TempDirectory.TempDir Path tempDir, String name) throws IOException {
private void createV1StoreFile(@TempDir Path tempDir, String name) throws IOException {
Path v1Dir = tempDir.resolve("var").resolve("data").resolve(STORE_NAME);
IOUtil.mkdirs(v1Dir.toFile());
Files.createFile(v1Dir.resolve(name));
@@ -132,7 +131,7 @@ class JAXBPropertyFileAccessTest {
class ForMissingRepository {
@Test
void shouldIgnoreStoreFile(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldIgnoreStoreFile(@TempDir Path tempDir) throws IOException {
createV1StoreFile(tempDir, "myStore.xml");
fileAccess.forStoreName(STORE_NAME).moveAsRepositoryStore(Paths.get("myStore.xml"), REPOSITORY_ID);

View File

@@ -102,7 +102,7 @@
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>3.1.0</version>
<version>4.3.0</version>
<scope>test</scope>
</dependency>

View File

@@ -31,14 +31,9 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junitpioneer.jupiter.TempDirectory;
import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.RestUtil;
import sonia.scm.it.utils.ScmRequests;
@@ -50,12 +45,10 @@ import sonia.scm.repository.client.api.RepositoryClientException;
import javax.json.Json;
import javax.json.JsonArray;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.stream.Stream;
import static java.util.Collections.emptyMap;
import static org.junit.Assert.assertEquals;
@@ -65,7 +58,6 @@ import static sonia.scm.it.utils.TestData.USER_ANONYMOUS;
import static sonia.scm.it.utils.TestData.WRITE;
import static sonia.scm.it.utils.TestData.getDefaultRepositoryUrl;
@ExtendWith(TempDirectory.class)
class AnonymousAccessITCase {
@Test
@@ -118,7 +110,7 @@ class AnonymousAccessITCase {
@ParameterizedTest
@ArgumentsSource(ScmTypes.class)
void shouldNotCloneRepository(String type, @TempDirectory.TempDir Path temporaryFolder) {
void shouldNotCloneRepository(String type, @TempDir Path temporaryFolder) {
assertThrows(RepositoryClientException.class, () -> RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile()));
}
}
@@ -142,7 +134,7 @@ class AnonymousAccessITCase {
@ParameterizedTest
@ArgumentsSource(ScmTypes.class)
void shouldCloneRepository(String type, @TempDirectory.TempDir Path temporaryFolder) throws IOException {
void shouldCloneRepository(String type, @TempDir Path temporaryFolder) throws IOException {
RepositoryClient client = RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile());
assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length);
}

View File

@@ -66,7 +66,7 @@ public class RoleITCase {
given(VndMediaType.REPOSITORY_ROLE)
.when()
.content("{" +
.body("{" +
"\"name\": \"" + ROLE_NAME + "\"," +
"\"verbs\": [\"read\",\"permissionRead\"]" +
"}")
@@ -84,7 +84,7 @@ public class RoleITCase {
given(VndMediaType.REPOSITORY_PERMISSION)
.when()
.content("{\n" +
.body("{\n" +
"\t\"role\": \"" + ROLE_NAME + "\",\n" +
"\t\"name\": \"" + USER + "\",\n" +
"\t\"groupPermission\": false\n" +

View File

@@ -86,7 +86,7 @@ public class TestData {
String admin = isAdmin ? "true" : "false";
given(VndMediaType.USER)
.when()
.content(new StringBuilder()
.body(new StringBuilder()
.append(" {\n")
.append(" \"active\": true,\n")
.append(" \"admin\": ").append(admin).append(",\n")
@@ -124,7 +124,7 @@ public class TestData {
LOG.info("create group with group name: {} and description {}", groupName, desc);
given(VndMediaType.GROUP)
.when()
.content(getGroupJson(groupName,desc))
.body(getGroupJson(groupName,desc))
.post(getGroupsUrl())
.then()
.statusCode(HttpStatus.SC_CREATED)
@@ -136,7 +136,7 @@ public class TestData {
LOG.info("create permission with name {} and verbs {} using the endpoint: {}", username, verbs, defaultPermissionUrl);
given(VndMediaType.REPOSITORY_PERMISSION)
.when()
.content("{\n" +
.body("{\n" +
"\t\"verbs\": " + verbs.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" +
"\t\"name\": \"" + username + "\",\n" +
"\t\"groupPermission\": false\n" +

View File

@@ -26,12 +26,14 @@ package sonia.scm.repository.spi;
import com.google.common.base.Strings;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
@@ -48,16 +50,18 @@ final class Differ implements AutoCloseable {
private final RevWalk walk;
private final TreeWalk treeWalk;
private final RevCommit commit;
private final PathFilter pathFilter;
private Differ(RevCommit commit, RevWalk walk, TreeWalk treeWalk) {
private Differ(RevCommit commit, RevWalk walk, TreeWalk treeWalk, PathFilter pathFilter) {
this.commit = commit;
this.walk = walk;
this.treeWalk = treeWalk;
this.pathFilter = pathFilter;
}
static Diff diff(Repository repository, DiffCommandRequest request) throws IOException {
try (Differ differ = create(repository, request)) {
return differ.diff();
return differ.diff(repository);
}
}
@@ -81,11 +85,11 @@ final class Differ implements AutoCloseable {
treeWalk.reset();
treeWalk.setRecursive(true);
PathFilter pathFilter = null;
if (Util.isNotEmpty(request.getPath())) {
treeWalk.setFilter(PathFilter.create(request.getPath()));
pathFilter = PathFilter.create(request.getPath());
}
if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) {
ObjectId otherRevision = repository.resolve(request.getAncestorChangeset());
ObjectId ancestorId = GitUtil.computeCommonAncestor(repository, revision, otherRevision);
@@ -105,14 +109,29 @@ final class Differ implements AutoCloseable {
treeWalk.addTree(commit.getTree());
return new Differ(commit, walk, treeWalk);
return new Differ(commit, walk, treeWalk, pathFilter);
}
private Diff diff() throws IOException {
List<DiffEntry> entries = DiffEntry.scan(treeWalk);
private Diff diff(Repository repository) throws IOException {
List<DiffEntry> entries = scanWithRename(repository, pathFilter, treeWalk);
return new Diff(commit, entries);
}
static List<DiffEntry> scanWithRename(Repository repository, PathFilter pathFilter, TreeWalk treeWalk) throws IOException {
List<DiffEntry> entries;
try (DiffFormatter diffFormatter = new DiffFormatter(null)) {
diffFormatter.setRepository(repository);
diffFormatter.setDetectRenames(true);
if (pathFilter != null) {
diffFormatter.setPathFilter(pathFilter);
}
entries = diffFormatter.scan(
treeWalk.getTree(0, AbstractTreeIterator.class),
treeWalk.getTree(1, AbstractTreeIterator.class));
}
return entries;
}
@Override
public void close() {
GitUtil.release(walk);

View File

@@ -57,7 +57,7 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
formatter.setRepository(repository);
for (DiffEntry e : diff.getEntries()) {
if (!e.getOldId().equals(e.getNewId())) {
if (idOrPathChanged(e)) {
formatter.format(e);
}
}
@@ -67,6 +67,10 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
};
}
private boolean idOrPathChanged(DiffEntry e) {
return !e.getOldId().equals(e.getNewId()) || !e.getNewPath().equals(e.getOldPath());
}
static class DequoteOutputStream extends OutputStream {
private static final String[] DEQUOTE_STARTS = {

View File

@@ -108,6 +108,24 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu
return diffEntry.getNewPath();
}
@Override
public ChangeType getChangeType() {
switch (diffEntry.getChangeType()) {
case ADD:
return ChangeType.ADD;
case MODIFY:
return ChangeType.MODIFY;
case RENAME:
return ChangeType.RENAME;
case DELETE:
return ChangeType.DELETE;
case COPY:
return ChangeType.COPY;
default:
throw new IllegalArgumentException("Unknown change type: " + diffEntry.getChangeType());
}
}
@Override
public Iterator<Hunk> iterator() {
String content = format(repository, diffEntry);

View File

@@ -32,12 +32,19 @@ import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import sonia.scm.repository.Added;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modification;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Modified;
import sonia.scm.repository.Removed;
import sonia.scm.repository.Renamed;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@@ -72,15 +79,14 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif
treeWalk.addTree(new EmptyTreeIterator());
}
treeWalk.addTree(commit.getTree());
List<DiffEntry> entries = DiffEntry.scan(treeWalk);
Modifications modifications = new Modifications();
List<DiffEntry> entries = Differ.scanWithRename(context.open(), null, treeWalk);
Collection<Modification> modifications = new ArrayList<>();
for (DiffEntry e : entries) {
if (!e.getOldId().equals(e.getNewId())) {
appendModification(modifications, e);
if (!e.getOldId().equals(e.getNewId()) || !e.getOldPath().equals(e.getNewPath())) {
modifications.add(asModification(e));
}
}
modifications.setRevision(revision);
return modifications;
return new Modifications(revision, modifications);
}
@Override
@@ -111,15 +117,18 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif
return getModifications(request.getRevision());
}
private void appendModification(Modifications modifications, DiffEntry entry) throws UnsupportedModificationTypeException {
private Modification asModification(DiffEntry entry) throws UnsupportedModificationTypeException {
DiffEntry.ChangeType type = entry.getChangeType();
if (type == DiffEntry.ChangeType.ADD) {
modifications.getAdded().add(entry.getNewPath());
} else if (type == DiffEntry.ChangeType.MODIFY) {
modifications.getModified().add(entry.getNewPath());
} else if (type == DiffEntry.ChangeType.DELETE) {
modifications.getRemoved().add(entry.getOldPath());
} else {
switch (type) {
case ADD:
return new Added(entry.getNewPath());
case MODIFY:
return new Modified(entry.getNewPath());
case DELETE:
return new Removed(entry.getOldPath());
case RENAME:
return new Renamed(entry.getOldPath(), entry.getNewPath());
default:
throw new UnsupportedModificationTypeException(entity(repository), MessageFormat.format("The modification type: {0} is not supported.", type));
}
}

View File

@@ -24,11 +24,13 @@
package sonia.scm.repository.spi;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
public class GitDiffCommandTest extends AbstractGitCommandTestBase {
@@ -140,4 +142,19 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
assertEquals(DIFF_FILE_PARTIAL_MERGE, output.toString());
}
@Test
public void diffBetweenTwoBranchesWithMovedFiles() throws IOException {
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext());
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
diffCommandRequest.setRevision("rename");
ByteArrayOutputStream output = new ByteArrayOutputStream();
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
assertThat(output.toString())
.contains("similarity index 100%")
.contains("rename from a.txt")
.contains("rename to a-copy.txt")
.contains("rename from b.txt")
.contains("rename to b-copy.txt");
}
}

View File

@@ -103,6 +103,22 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
assertThat(hunk.getNewLineCount()).isEqualTo(2);
}
@Test
public void shouldReturnRenames() throws IOException {
DiffResult diffResult = createDiffResult("rename");
Iterator<DiffFile> fileIterator = diffResult.iterator();
DiffFile renameA = fileIterator.next();
assertThat(renameA.getOldPath()).isEqualTo("a.txt");
assertThat(renameA.getNewPath()).isEqualTo("a-copy.txt");
assertThat(renameA.iterator().hasNext()).isFalse();
DiffFile renameB = fileIterator.next();
assertThat(renameB.getOldPath()).isEqualTo("b.txt");
assertThat(renameB.getNewPath()).isEqualTo("b-copy.txt");
assertThat(renameB.iterator().hasNext()).isFalse();
}
private DiffResult createDiffResult(String s) throws IOException {
GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext());
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();

View File

@@ -39,11 +39,11 @@ import java.io.File;
import java.io.IOException;
import static java.nio.charset.Charset.defaultCharset;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
@@ -188,7 +188,9 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertTrue("removed list should be empty", modifications.getRemoved().isEmpty());
assertFalse("added list should not be empty", modifications.getAdded().isEmpty());
assertEquals(2, modifications.getAdded().size());
assertThat(modifications.getAdded(), contains("a.txt", "b.txt"));
assertThat(modifications.getAdded())
.extracting("path")
.containsExactly("a.txt", "b.txt");
}
@Test
@@ -198,14 +200,14 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
GitLogCommand command = createCommand();
Changeset c = command.getChangeset("435df2f061add3589cb3", request);
Assertions.assertThat(c.getBranches()).containsOnly("master");
assertThat(c.getBranches()).containsOnly("master");
}
@Test
public void shouldNotReturnCommitFromDifferentBranch() {
when(request.getBranch()).thenReturn("master");
Changeset changeset = createCommand().getChangeset("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", request);
Assertions.assertThat(changeset).isNull();
assertThat(changeset).isNull();
}
@Test

View File

@@ -39,6 +39,7 @@ import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.Added;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult;
@@ -318,7 +319,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
assertThat(message).isEqualTo("squash three commits");
GitModificationsCommand modificationsCommand = new GitModificationsCommand(createContext());
List<String> changes = modificationsCommand.getModifications("master").getAdded();
List<Added> changes = modificationsCommand.getModifications("master").getAdded();
assertThat(changes.size()).isEqualTo(3);
}

View File

@@ -86,6 +86,25 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
}
@Test
public void shouldReadRenamedFiles() throws Exception {
String originalFile = "a.txt";
write(outgoing, outgoingDirectory, originalFile, "bal bla");
commit(outgoing, "add file");
write(outgoing, outgoingDirectory, "b.txt", "bal bla");
File file = new File(outgoingDirectory, originalFile);
file.delete();
outgoing.rm().addFilepattern(originalFile).call();
RevCommit modifiedFileCommit = commit(outgoing, "rename file");
String revision = modifiedFileCommit.getName();
Consumer<Modifications> assertModifications = assertRenamedFiles("b.txt");
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
pushOutgoingAndPullIncoming();
assertModifications.accept(incomingModificationsCommand.getModifications(revision));
}
void pushOutgoingAndPullIncoming() throws IOException {
GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory, outgoingRepository, null));
PushCommandRequest request = new PushCommandRequest();
@@ -102,31 +121,62 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
.asList()
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
.asList()
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.asList()
.hasSize(1)
.extracting("path")
.containsOnly(fileName);
};
}
Consumer<Modifications> assertRenamedFiles(String fileName) {
return (modifications) -> {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.asList()
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.asList()
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.asList()
.isEmpty();
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.asList()
.hasSize(1)
.extracting("newPath")
.containsOnly(fileName);
};
}
Consumer<Modifications> assertModifiedFiles(String file) {
return (modifications) -> {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
.asList()
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.asList()
.extracting("path")
.hasSize(1)
.containsOnly(file);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
.asList()
.isEmpty();
};
}
@@ -135,14 +185,18 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.asList()
.hasSize(1)
.extracting("path")
.containsOnly(file);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
.asList()
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
.asList()
.isEmpty();
};
}
}

View File

@@ -33,6 +33,7 @@ import sonia.scm.repository.spi.ScmProviderHttpServlet;
import sonia.scm.util.HttpUtil;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
@@ -150,6 +151,16 @@ public class GitPermissionFilterTest {
public String toString() {
return baos.toString();
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
}

View File

@@ -24,9 +24,12 @@
package sonia.scm.repository.spi;
import sonia.scm.repository.Modification;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.spi.javahg.HgLogChangesetCommand;
import java.util.Collection;
public class HgModificationsCommand extends AbstractCommand implements ModificationsCommand {
HgModificationsCommand(HgCommandContext context) {
@@ -38,9 +41,8 @@ public class HgModificationsCommand extends AbstractCommand implements Modificat
public Modifications getModifications(String revision) {
com.aragost.javahg.Repository repository = open();
HgLogChangesetCommand hgLogChangesetCommand = HgLogChangesetCommand.on(repository, getContext().getConfig());
Modifications modifications = hgLogChangesetCommand.rev(revision).extractModifications();
modifications.setRevision(revision);
return modifications;
Collection<Modification> modifications = hgLogChangesetCommand.rev(revision).extractModifications();
return new Modifications(revision, modifications);
}
@Override

View File

@@ -36,10 +36,11 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Modification;
import sonia.scm.repository.Person;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
//~--- JDK imports ------------------------------------------------------------
@@ -251,7 +252,7 @@ public abstract class AbstractChangesetCommand extends AbstractCommand
return changeset;
}
protected Modifications readModificationsFromStream(HgInputStream in) {
protected Collection<Modification> readModificationsFromStream(HgInputStream in) {
try {
boolean found = in.find(CHANGESET_PATTERN);
if (found) {
@@ -265,20 +266,14 @@ public abstract class AbstractChangesetCommand extends AbstractCommand
return null;
}
private Modifications extractModifications(HgInputStream in) throws IOException {
Modifications modifications = new Modifications();
private Collection<Modification> extractModifications(HgInputStream in) throws IOException {
HgModificationParser hgModificationParser = new HgModificationParser();
String line = in.textUpTo('\n');
while (line.length() > 0) {
if (line.startsWith("a ")) {
modifications.getAdded().add(line.substring(2));
} else if (line.startsWith("m ")) {
modifications.getModified().add(line.substring(2));
} else if (line.startsWith("d ")) {
modifications.getRemoved().add(line.substring(2));
}
hgModificationParser.addLine(line);
line = in.textUpTo('\n');
}
return modifications;
return hgModificationParser.getModifications();
}
/**

View File

@@ -31,9 +31,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Modification;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
/**
@@ -64,7 +65,7 @@ public class HgLogChangesetCommand extends AbstractChangesetCommand {
return readListFromStream(getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH));
}
public Modifications extractModifications(String... files) {
public Collection<Modification> extractModifications(String... files) {
HgInputStream hgInputStream = getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH);
try {
return readModificationsFromStream(hgInputStream);

View File

@@ -0,0 +1,64 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi.javahg;
import sonia.scm.repository.Added;
import sonia.scm.repository.Copied;
import sonia.scm.repository.Modification;
import sonia.scm.repository.Modified;
import sonia.scm.repository.Removed;
import sonia.scm.repository.Renamed;
import java.util.Collection;
import java.util.LinkedHashSet;
class HgModificationParser {
private final Collection<Modification> modifications = new LinkedHashSet<>();
void addLine(String line) {
if (line.startsWith("a ")) {
modifications.add(new Added(line.substring(2)));
} else if (line.startsWith("m ")) {
modifications.add(new Modified(line.substring(2)));
} else if (line.startsWith("d ")) {
modifications.add(new Removed(line.substring(2)));
} else if (line.startsWith("c ")) {
String sourceTarget = line.substring(2);
int divider = sourceTarget.indexOf('\0');
String source = sourceTarget.substring(0, divider);
String target = sourceTarget.substring(divider + 1);
modifications.remove(new Added(target));
if (modifications.remove(new Removed(source))) {
modifications.add(new Renamed(source, target));
} else {
modifications.add(new Copied(source, target));
}
}
}
Collection<Modification> getModifications() {
return modifications;
}
}

View File

@@ -26,6 +26,7 @@ package sonia.scm.web;
import com.google.common.base.Preconditions;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -76,4 +77,19 @@ public class HgServletInputStream extends ServletInputStream {
public void close() throws IOException {
original.close();
}
@Override
public boolean isFinished() {
return original.isFinished();
}
@Override
public boolean isReady() {
return original.isReady();
}
@Override
public void setReadListener(ReadListener readListener) {
original.setReadListener(readListener);
}
}

View File

@@ -1,8 +1,9 @@
header = "%{pattern}"
changeset = "{rev}:{node}{author}\n{date|hgdate}\n{branch}\n{parents}{extras}\n{tags}{file_adds}{file_mods}{file_dels}\n{desc}\0"
changeset = "{rev}:{node}{author}\n{date|hgdate}\n{branch}\n{parents}{extras}\n{tags}{file_adds}{file_mods}{file_dels}{file_copies}\n{desc}\0"
tag = "t {tag}\n"
file_add = "a {file_add}\n"
file_mod = "m {file_mod}\n"
file_del = "d {file_del}\n"
file_copy = "c {source}\0{name}\n"
extra = "{key}={value|stringescape},"
footer = "%{pattern}"

View File

@@ -33,11 +33,10 @@ import sonia.scm.repository.Modifications;
import java.io.IOException;
import static org.hamcrest.Matchers.contains;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
//~--- JDK imports ------------------------------------------------------------
@@ -162,7 +161,9 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
assertTrue("removed list should be empty", modifications.getRemoved().isEmpty());
assertFalse("added list should not be empty", modifications.getAdded().isEmpty());
assertEquals(2, modifications.getAdded().size());
assertThat(modifications.getAdded(), contains("a.txt", "b.txt"));
assertThat(modifications.getAdded())
.extracting("path")
.containsExactly("a.txt", "b.txt");
}
@Test

View File

@@ -25,7 +25,9 @@
package sonia.scm.repository.spi;
import com.aragost.javahg.Changeset;
import com.aragost.javahg.commands.CopyCommand;
import com.aragost.javahg.commands.RemoveCommand;
import com.aragost.javahg.commands.RenameCommand;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.repository.HgTestUtil;
@@ -34,7 +36,7 @@ import sonia.scm.repository.Modifications;
import java.io.File;
import java.util.function.Consumer;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThat;
public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
@@ -83,6 +85,31 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
}
@Test
public void shouldReadRenamedFiles() throws Exception {
String oldFileName = "a.txt";
String newFileName = "b.txt";
writeNewFile(outgoing, outgoingDirectory, oldFileName, "bal bla");
commit(outgoing, "added a.txt");
RenameCommand.on(outgoing).execute(oldFileName, newFileName);
Changeset changeset = commit(outgoing, "rename a.txt to b.txt");
String revision = String.valueOf(changeset.getRevision());
Consumer<Modifications> assertModifications = assertRenamedFiles(oldFileName, newFileName);
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
}
@Test
public void shouldReadCopiedFiles() throws Exception {
String srcFileName = "a.txt";
String newFileName = "b.txt";
writeNewFile(outgoing, outgoingDirectory, srcFileName, "bal bla");
commit(outgoing, "added a.txt");
CopyCommand.on(outgoing).execute(srcFileName, newFileName);
Changeset changeset = commit(outgoing, "copy a.txt to b.txt");
String revision = String.valueOf(changeset.getRevision());
Consumer<Modifications> assertModifications = assertCopiedFiles(srcFileName, newFileName);
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
}
Consumer<Modifications> assertRemovedFiles(String fileName) {
return (modifications) -> {
@@ -96,10 +123,66 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(1)
.extracting("path")
.containsOnly(fileName);
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.isEmpty();
};
}
Consumer<Modifications> assertRenamedFiles(String oldFileName, String newFileName) {
return (modifications) -> {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.isEmpty();
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.hasSize(1)
.extracting("oldPath")
.containsOnly(oldFileName);
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.hasSize(1)
.extracting("newPath")
.containsOnly(newFileName);
};
}
Consumer<Modifications> assertCopiedFiles(String srcFileName, String newFileName) {
return (modifications) -> {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.isEmpty();
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.isEmpty();
assertThat(modifications.getCopied())
.as("copied files modifications")
.hasSize(1)
.extracting("sourcePath")
.containsOnly(srcFileName);
assertThat(modifications.getCopied())
.as("copied files modifications")
.hasSize(1)
.extracting("targetPath")
.containsOnly(newFileName);
};
}
Consumer<Modifications> assertModifiedFiles(String file) {
return (modifications) -> {
@@ -110,10 +193,14 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(1)
.extracting("path")
.containsOnly(file);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.hasSize(0);
};
}
@@ -123,6 +210,7 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(1)
.extracting("path")
.containsOnly(addedFile);
assertThat(modifications.getModified())
.as("modified files modifications")
@@ -130,6 +218,9 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.hasSize(0);
};
}
}

View File

@@ -0,0 +1,82 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi.javahg;
import org.junit.jupiter.api.Test;
import sonia.scm.repository.Added;
import sonia.scm.repository.Copied;
import sonia.scm.repository.Modified;
import sonia.scm.repository.Removed;
import sonia.scm.repository.Renamed;
import static org.assertj.core.api.Assertions.assertThat;
class HgModificationParserTest {
HgModificationParser parser = new HgModificationParser();
@Test
void shouldDetectAddedPath() {
parser.addLine("a added/file");
assertThat(parser.getModifications())
.containsExactly(new Added("added/file"));
}
@Test
void shouldDetectModifiedPath() {
parser.addLine("m modified/file");
assertThat(parser.getModifications())
.containsExactly(new Modified("modified/file"));
}
@Test
void shouldDetectRemovedPath() {
parser.addLine("d removed/file");
assertThat(parser.getModifications())
.containsExactly(new Removed("removed/file"));
}
@Test
void shouldDetectRenamedPath() {
parser.addLine("a new/path");
parser.addLine("d old/path");
parser.addLine("c old/path\0new/path");
assertThat(parser.getModifications())
.containsExactly(new Renamed("old/path", "new/path"));
}
@Test
void shouldCopiedRenamedPath() {
parser.addLine("a new/path");
parser.addLine("c old/path\0new/path");
assertThat(parser.getModifications())
.containsExactly(new Copied("old/path", "new/path"));
}
}

View File

@@ -28,6 +28,7 @@ import com.google.common.base.Charsets;
import com.google.common.io.ByteStreams;
import org.junit.Test;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -69,6 +70,20 @@ public class HgServletInputStreamTest {
public int read() {
return input.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
}
}

View File

@@ -31,6 +31,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
@@ -181,6 +182,19 @@ public class WireProtocolTest {
return input.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
}
}

View File

@@ -48,13 +48,6 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<version>1.1.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>

View File

@@ -49,7 +49,11 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.Optional.empty;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
@@ -116,30 +120,23 @@ public final class SvnUtil
public static Modifications createModifications(SVNLogEntry entry, String revision) {
Modifications modifications = new Modifications();
modifications.setRevision(revision);
Map<String, SVNLogEntryPath> changeMap = entry.getChangedPaths();
List<Modification> modificationList;
if (Util.isNotEmpty(changeMap)) {
for (SVNLogEntryPath e : changeMap.values()) {
appendModification(modifications, e.getType(), e.getPath());
}
}
return modifications;
modificationList = changeMap.values().stream()
.map(e -> asModification(e.getType(), e.getPath()))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
} else {
modificationList = emptyList();
}
/**
* Method description
*
*
* @param modifications
* @param type
* @param path
*/
public static void appendModification(Modifications modifications, char type,
String path)
{
return new Modifications(revision, modificationList);
}
public static Optional<Modification> asModification(char type, String path) {
if (path.startsWith("/"))
{
path = path.substring(1);
@@ -148,23 +145,18 @@ public final class SvnUtil
switch (type)
{
case SVNLogEntryPath.TYPE_ADDED :
modifications.getAdded().add(path);
break;
return Optional.of(new Added(path));
case SVNLogEntryPath.TYPE_DELETED :
modifications.getRemoved().add(path);
break;
return Optional.of(new Removed(path));
case TYPE_UPDATED :
case SVNLogEntryPath.TYPE_MODIFIED :
modifications.getModified().add(path);
break;
return Optional.of(new Modified(path));
default :
logger.debug("unknown modification type {}", type);
return empty();
}
}

View File

@@ -31,10 +31,12 @@ import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.admin.SVNLookClient;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modification;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.SvnUtil;
import sonia.scm.util.Util;
import java.util.ArrayList;
import java.util.Collection;
@Slf4j
@@ -78,12 +80,12 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif
private Modifications getModificationsFromTransaction(String transaction) throws SVNException {
log.debug("get svn modifications from transaction: {}", transaction);
final Modifications modifications = new Modifications();
SVNLookClient client = SVNClientManager.newInstance().getLookClient();
Collection<Modification> modificationList = new ArrayList<>();
client.doGetChanged(context.getDirectory(), transaction,
e -> SvnUtil.appendModification(modifications, e.getType(), e.getPath()), true);
e -> SvnUtil.asModification(e.getType(), e.getPath()).ifPresent(modificationList::add), true);
return modifications;
return new Modifications(null, modificationList);
}
@Override

View File

@@ -143,8 +143,8 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase
assertEquals(1, modifications.getModified().size());
assertEquals(1, modifications.getRemoved().size());
assertTrue("added list should be empty", modifications.getAdded().isEmpty());
assertEquals("a.txt", modifications.getModified().get(0));
assertEquals("b.txt", modifications.getRemoved().get(0));
assertEquals("a.txt", modifications.getModified().get(0).getPath());
assertEquals("b.txt", modifications.getRemoved().get(0).getPath());
}
@Test

View File

@@ -0,0 +1,123 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { isAnchorLink, isExternalLink, isLinkWithProtocol, createLocalLink } from "./MarkdownLinkRenderer";
describe("test isAnchorLink", () => {
it("should return true", () => {
expect(isAnchorLink("#some-thing")).toBe(true);
expect(isAnchorLink("#/some/more/complicated-link")).toBe(true);
});
it("should return false", () => {
expect(isAnchorLink("https://cloudogu.com")).toBe(false);
expect(isAnchorLink("/some/path/link")).toBe(false);
});
});
describe("test isExternalLink", () => {
it("should return true", () => {
expect(isExternalLink("https://cloudogu.com")).toBe(true);
expect(isExternalLink("http://cloudogu.com")).toBe(true);
});
it("should return false", () => {
expect(isExternalLink("some/path/link")).toBe(false);
expect(isExternalLink("/some/path/link")).toBe(false);
expect(isExternalLink("#some-anchor")).toBe(false);
expect(isExternalLink("mailto:trillian@hitchhiker.com")).toBe(false);
});
});
describe("test isLinkWithProtocol", () => {
it("should return true", () => {
expect(isLinkWithProtocol("ldap://[2001:db8::7]/c=GB?objectClass?one")).toBe(true);
expect(isLinkWithProtocol("mailto:trillian@hitchhiker.com")).toBe(true);
expect(isLinkWithProtocol("tel:+1-816-555-1212")).toBe(true);
expect(isLinkWithProtocol("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")).toBe(true);
expect(isLinkWithProtocol("about:config")).toBe(true);
expect(isLinkWithProtocol("http://cloudogu.com")).toBe(true);
expect(isLinkWithProtocol("file:///srv/git/project.git")).toBe(true);
expect(isLinkWithProtocol("ssh://trillian@server/project.git")).toBe(true);
});
it("should return false", () => {
expect(isLinkWithProtocol("some/path/link")).toBe(false);
expect(isLinkWithProtocol("/some/path/link")).toBe(false);
expect(isLinkWithProtocol("#some-anchor")).toBe(false);
});
});
describe("test createLocalLink", () => {
it("should handle relative links", () => {
expectLocalLink("/src", "/src/README.md", "docs/Home.md", "/src/docs/Home.md");
});
it("should handle absolute links", () => {
expectLocalLink("/src", "/src/README.md", "/docs/CHANGELOG.md", "/src/docs/CHANGELOG.md");
});
it("should handle relative links from locations with trailing slash", () => {
expectLocalLink("/src", "/src/README.md/", "/docs/LICENSE.md", "/src/docs/LICENSE.md");
});
it("should handle relative links from location outside of base", () => {
expectLocalLink("/src", "/info/readme", "docs/index.md", "/src/docs/index.md");
});
it("should handle absolute links from location outside of base", () => {
expectLocalLink("/src", "/info/readme", "/info/index.md", "/src/info/index.md");
});
it("should handle relative links from sub directories", () => {
expectLocalLink("/src", "/src/docs/index.md", "installation/linux.md", "/src/docs/installation/linux.md");
});
it("should handle absolute links from sub directories", () => {
expectLocalLink("/src", "/src/docs/index.md", "/docs/CONTRIBUTIONS.md", "/src/docs/CONTRIBUTIONS.md");
});
it("should resolve .. with in path", () => {
expectLocalLink("/src", "/src/docs/installation/index.md", "../../README.md", "/src/README.md");
});
it("should resolve .. to / if we reached the end", () => {
expectLocalLink("/", "/index.md", "../../README.md", "/README.md");
});
it("should resolve . with in path", () => {
expectLocalLink("/src", "/src/README.md", "./SHAPESHIPS.md", "/src/SHAPESHIPS.md");
});
it("should resolve . with the current directory", () => {
expectLocalLink("/", "/README.md", "././HITCHHIKER.md", "/HITCHHIKER.md");
});
it("should handle complex path", () => {
expectLocalLink("/src", "/src/docs/installation/index.md", "./.././../docs/index.md", "/src/docs/index.md");
});
const expectLocalLink = (basePath: string, currentPath: string, link: string, expected: string) => {
const localLink = createLocalLink(basePath, currentPath, link);
expect(localLink).toBe(expected);
};
});

View File

@@ -0,0 +1,125 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, {FC} from "react";
import {Link, useLocation} from "react-router-dom";
import ExternalLink from "./navigation/ExternalLink";
import {withContextPath} from "./urls";
const externalLinkRegex = new RegExp("^http(s)?://");
export const isExternalLink = (link: string) => {
return externalLinkRegex.test(link);
};
export const isAnchorLink = (link: string) => {
return link.startsWith("#");
};
const linkWithProtcolRegex = new RegExp("^[a-z]+:");
export const isLinkWithProtocol = (link: string) => {
return linkWithProtcolRegex.test(link);
};
const join = (left: string, right: string) => {
if (left.endsWith("/") && right.startsWith("/")) {
return left + right.substring(1);
} else if (!left.endsWith("/") && !right.startsWith("/")) {
return left + "/" + right;
}
return left + right;
};
const normalizePath = (path: string) => {
const stack = [];
const parts = path.split("/");
for (const part of parts) {
if (part === "..") {
stack.pop();
} else if (part !== ".") {
stack.push(part)
}
}
const normalizedPath = stack.join("/")
if (normalizedPath.startsWith("/")) {
return normalizedPath;
}
return "/" + normalizedPath;
};
const isAbsolute = (link: string) => {
return link.startsWith("/");
};
const isSubDirectoryOf = (basePath: string, currentPath: string) => {
return currentPath.startsWith(basePath);
};
export const createLocalLink = (basePath: string, currentPath: string, link: string) => {
if (isAbsolute(link)) {
return join(basePath, link);
}
if (!isSubDirectoryOf(basePath, currentPath)) {
return join(basePath, link);
}
let path = currentPath;
if (currentPath.endsWith("/")) {
path = currentPath.substring(0, currentPath.length - 2);
}
const lastSlash = path.lastIndexOf("/");
if (lastSlash < 0) {
path = "";
} else {
path = path.substring(0, lastSlash);
}
return normalizePath(join(path, link));
};
type LinkProps = {
href: string;
};
type Props = LinkProps & {
base: string;
};
const MarkdownLinkRenderer: FC<Props> = ({href, base, children}) => {
const location = useLocation();
if (isExternalLink(href)) {
return <ExternalLink to={href}>{children}</ExternalLink>;
} else if (isLinkWithProtocol(href)) {
return <a href={href}>{children}</a>;
} else if (isAnchorLink(href)) {
return <a href={withContextPath(location.pathname) + href}>{children}</a>;
} else {
const localLink = createLocalLink(base, location.pathname, href);
return <Link to={localLink}>{children}</Link>;
}
};
// we use a factory method, because react-markdown does not pass
// base as prop down to our link component.
export const create = (base: string): FC<LinkProps> => {
return props => <MarkdownLinkRenderer base={base} {...props} />;
};
export default MarkdownLinkRenderer;

View File

@@ -30,6 +30,7 @@ import TestPage from "./__resources__/test-page.md";
import MarkdownWithoutLang from "./__resources__/markdown-without-lang.md";
import MarkdownXmlCodeBlock from "./__resources__/markdown-xml-codeblock.md";
import MarkdownInlineXml from "./__resources__/markdown-inline-xml.md";
import MarkdownLinks from "./__resources__/markdown-links.md";
import Title from "./layout/Title";
import { Subtitle } from "./layout";
import { MemoryRouter } from "react-router-dom";
@@ -50,4 +51,5 @@ storiesOf("MarkdownView", module)
<Subtitle subtitle="Inline xml outside of a code block is not supported" />
<MarkdownView content={MarkdownInlineXml} />
</>
));
))
.add("Links", () => <MarkdownView content={MarkdownLinks} basePath="/" />);

View File

@@ -29,6 +29,7 @@ import { binder } from "@scm-manager/ui-extensions";
import ErrorBoundary from "./ErrorBoundary";
import SyntaxHighlighter from "./SyntaxHighlighter";
import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer";
import { create } from "./MarkdownLinkRenderer";
import { useTranslation } from "react-i18next";
import Notification from "./Notification";
@@ -38,6 +39,8 @@ type Props = RouteComponentProps & {
renderers?: any;
skipHtml?: boolean;
enableAnchorHeadings?: boolean;
// basePath for markdown links
basePath?: string;
};
const xmlMarkupSample = `\`\`\`xml
@@ -97,7 +100,7 @@ class MarkdownView extends React.Component<Props> {
}
render() {
const { content, renderers, renderContext, enableAnchorHeadings, skipHtml } = this.props;
const { content, renderers, renderContext, enableAnchorHeadings, skipHtml, basePath } = this.props;
const rendererFactory = binder.getExtension("markdown-renderer-factory");
let rendererList = renderers;
@@ -114,6 +117,10 @@ class MarkdownView extends React.Component<Props> {
rendererList.heading = MarkdownHeadingRenderer;
}
if (basePath && !rendererList.link) {
rendererList.link = create(basePath);
}
if (!rendererList.code) {
rendererList.code = SyntaxHighlighter;
}

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
export default `# Links
Show case for different style of markdown links.
Please note that some of the links may not work in storybook,
the story is mostly for checking if the links are rendered correct.
## External
External Links should be opened in a new tab: [external link](https://scm-manager.org)
## Anchor
Anchor Links should be rendered a simple a tag with an href: [anchor link](#sample)
## Protocol
Links with a protocol other than http should be rendered a simple a tag with an href e.g.: [mail link](mailto:marvin@hitchhiker.com)
## Internal
Internal links should be rendered by react-router: [internal link](/buttons)
`;

View File

@@ -33060,6 +33060,7 @@ exports[`Storyshots Layout|Footer Default 1`] = `
<li>
<a
href="https://www.scm-manager.org/"
rel="noopener noreferrer"
target="_blank"
>
SCM-Manager 2.0.0
@@ -33085,6 +33086,7 @@ exports[`Storyshots Layout|Footer Default 1`] = `
<li>
<a
href="https://www.scm-manager.org/support/"
rel="noopener noreferrer"
target="_blank"
>
footer.support.community
@@ -33093,6 +33095,7 @@ exports[`Storyshots Layout|Footer Default 1`] = `
<li>
<a
href="https://cloudogu.com/en/scm-manager-enterprise/"
rel="noopener noreferrer"
target="_blank"
>
footer.support.enterprise
@@ -33182,6 +33185,7 @@ exports[`Storyshots Layout|Footer Full 1`] = `
<li>
<a
href="https://www.scm-manager.org/"
rel="noopener noreferrer"
target="_blank"
>
SCM-Manager 2.0.0
@@ -33190,6 +33194,7 @@ exports[`Storyshots Layout|Footer Full 1`] = `
<li>
<a
href="#"
rel="noopener noreferrer"
target="_blank"
>
REST API
@@ -33198,6 +33203,7 @@ exports[`Storyshots Layout|Footer Full 1`] = `
<li>
<a
href="#"
rel="noopener noreferrer"
target="_blank"
>
CLI
@@ -33223,6 +33229,7 @@ exports[`Storyshots Layout|Footer Full 1`] = `
<li>
<a
href="https://www.scm-manager.org/support/"
rel="noopener noreferrer"
target="_blank"
>
footer.support.community
@@ -33231,6 +33238,7 @@ exports[`Storyshots Layout|Footer Full 1`] = `
<li>
<a
href="https://cloudogu.com/en/scm-manager-enterprise/"
rel="noopener noreferrer"
target="_blank"
>
footer.support.enterprise
@@ -33239,6 +33247,7 @@ exports[`Storyshots Layout|Footer Full 1`] = `
<li>
<a
href="#"
rel="noopener noreferrer"
target="_blank"
>
FAQ
@@ -33319,6 +33328,7 @@ exports[`Storyshots Layout|Footer With Avatar 1`] = `
<li>
<a
href="https://www.scm-manager.org/"
rel="noopener noreferrer"
target="_blank"
>
SCM-Manager 2.0.0
@@ -33344,6 +33354,7 @@ exports[`Storyshots Layout|Footer With Avatar 1`] = `
<li>
<a
href="https://www.scm-manager.org/support/"
rel="noopener noreferrer"
target="_blank"
>
footer.support.community
@@ -33352,6 +33363,7 @@ exports[`Storyshots Layout|Footer With Avatar 1`] = `
<li>
<a
href="https://cloudogu.com/en/scm-manager-enterprise/"
rel="noopener noreferrer"
target="_blank"
>
footer.support.enterprise
@@ -33436,6 +33448,7 @@ exports[`Storyshots Layout|Footer With Plugin Links 1`] = `
<li>
<a
href="https://www.scm-manager.org/"
rel="noopener noreferrer"
target="_blank"
>
SCM-Manager 2.0.0
@@ -33444,6 +33457,7 @@ exports[`Storyshots Layout|Footer With Plugin Links 1`] = `
<li>
<a
href="#"
rel="noopener noreferrer"
target="_blank"
>
REST API
@@ -33452,6 +33466,7 @@ exports[`Storyshots Layout|Footer With Plugin Links 1`] = `
<li>
<a
href="#"
rel="noopener noreferrer"
target="_blank"
>
CLI
@@ -33477,6 +33492,7 @@ exports[`Storyshots Layout|Footer With Plugin Links 1`] = `
<li>
<a
href="https://www.scm-manager.org/support/"
rel="noopener noreferrer"
target="_blank"
>
footer.support.community
@@ -33485,6 +33501,7 @@ exports[`Storyshots Layout|Footer With Plugin Links 1`] = `
<li>
<a
href="https://cloudogu.com/en/scm-manager-enterprise/"
rel="noopener noreferrer"
target="_blank"
>
footer.support.enterprise
@@ -33493,6 +33510,7 @@ exports[`Storyshots Layout|Footer With Plugin Links 1`] = `
<li>
<a
href="#"
rel="noopener noreferrer"
target="_blank"
>
FAQ
@@ -34372,6 +34390,74 @@ exports[`Storyshots MarkdownView Inline Xml 1`] = `
</div>
`;
exports[`Storyshots MarkdownView Links 1`] = `
<div
className="MarkdownViewstories__Spacing-sc-1hk90gd-0 eqTNCI"
>
<div>
<div
className="content is-word-break"
>
<h1>
Links
</h1>
<p>
Show case for different style of markdown links.
Please note that some of the links may not work in storybook,
the story is mostly for checking if the links are rendered correct.
</p>
<h2>
External
</h2>
<p>
External Links should be opened in a new tab:
<a
href="https://scm-manager.org"
rel="noopener noreferrer"
target="_blank"
>
external link
</a>
</p>
<h2>
Anchor
</h2>
<p>
Anchor Links should be rendered a simple a tag with an href:
<a
href="/#sample"
>
anchor link
</a>
</p>
<h2>
Protocol
</h2>
<p>
Links with a protocol other than http should be rendered a simple a tag with an href e.g.:
<a
href="mailto:marvin@hitchhiker.com"
>
mail link
</a>
</p>
<h2>
Internal
</h2>
<p>
Internal links should be rendered by react-router:
<a
href="/buttons"
onClick={[Function]}
>
internal link
</a>
</p>
</div>
</div>
</div>
`;
exports[`Storyshots MarkdownView Xml Code Block 1`] = `
<div
className="MarkdownViewstories__Spacing-sc-1hk90gd-0 eqTNCI"

View File

@@ -32,7 +32,7 @@ import hitchhiker from "../__resources__/hitchhiker.png";
// @ts-ignore ignore unknown jpg
import marvin from "../__resources__/marvin.jpg";
import NavLink from "../navigation/NavLink";
import ExternalLink from "../navigation/ExternalLink";
import ExternalNavLink from "../navigation/ExternalNavLink";
import { MemoryRouter } from "react-router-dom";
const trillian: Me = {
@@ -50,9 +50,9 @@ const bindAvatar = (binder: Binder, avatar: string) => {
};
const bindLinks = (binder: Binder) => {
binder.bind("footer.information", () => <ExternalLink to="#" label="REST API" />);
binder.bind("footer.information", () => <ExternalLink to="#" label="CLI" />);
binder.bind("footer.support", () => <ExternalLink to="#" label="FAQ" />);
binder.bind("footer.information", () => <ExternalNavLink to="#" label="REST API" />);
binder.bind("footer.information", () => <ExternalNavLink to="#" label="CLI" />);
binder.bind("footer.support", () => <ExternalNavLink to="#" label="FAQ" />);
binder.bind("profile.setting", () => <NavLink label="Authorized Keys" to="#" />);
};

View File

@@ -29,7 +29,7 @@ import NavLink from "../navigation/NavLink";
import FooterSection from "./FooterSection";
import styled from "styled-components";
import { EXTENSION_POINT } from "../avatar/Avatar";
import ExternalLink from "../navigation/ExternalLink";
import ExternalNavLink from "../navigation/ExternalNavLink";
import { useTranslation } from "react-i18next";
type Props = {
@@ -99,12 +99,15 @@ const Footer: FC<Props> = ({ me, version, links }) => {
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</FooterSection>
<FooterSection title={<TitleWithIcon title={t("footer.information.title")} icon="info-circle" />}>
<ExternalLink to="https://www.scm-manager.org/" label={`SCM-Manager ${version}`} />
<ExternalNavLink to="https://www.scm-manager.org/" label={`SCM-Manager ${version}`} />
<ExtensionPoint name="footer.information" props={extensionProps} renderAll={true} />
</FooterSection>
<FooterSection title={<TitleWithIcon title={t("footer.support.title")} icon="life-ring" />}>
<ExternalLink to="https://www.scm-manager.org/support/" label={t("footer.support.community")} />
<ExternalLink to="https://cloudogu.com/en/scm-manager-enterprise/" label={t("footer.support.enterprise")} />
<ExternalNavLink to="https://www.scm-manager.org/support/" label={t("footer.support.community")} />
<ExternalNavLink
to="https://cloudogu.com/en/scm-manager-enterprise/"
label={t("footer.support.enterprise")}
/>
<ExtensionPoint name="footer.support" props={extensionProps} renderAll={true} />
</FooterSection>
</div>

View File

@@ -22,34 +22,15 @@
* SOFTWARE.
*/
import React, { FC } from "react";
import classNames from "classnames";
type Props = {
to: string;
icon?: string;
label: string;
};
// TODO is it used in the menu? should it use MenuContext for collapse state?
const ExternalLink: FC<Props> = ({ to, icon, label }) => {
let showIcon;
if (icon) {
showIcon = (
<>
<i className={classNames(icon, "fa-fw")} />{" "}
</>
);
}
return (
<li>
<a target="_blank" href={to}>
{showIcon}
{label}
const ExternalLink: FC<Props> = ({ to, children }) => (
<a href={to} target="_blank" rel="noopener noreferrer">
{children}
</a>
</li>
);
};
export default ExternalLink;

View File

@@ -0,0 +1,56 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import classNames from "classnames";
import ExternalLink from "./ExternalLink";
type Props = {
to: string;
icon?: string;
label: string;
};
// TODO is it used in the menu? should it use MenuContext for collapse state?
const ExternalNavLink: FC<Props> = ({ to, icon, label }) => {
let showIcon;
if (icon) {
showIcon = (
<>
<i className={classNames(icon, "fa-fw")} />{" "}
</>
);
}
return (
<li>
<ExternalLink to={to}>
{showIcon}
{label}
</ExternalLink>
</li>
);
};
export default ExternalNavLink;

View File

@@ -33,3 +33,5 @@ export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink";
export { default as SecondaryNavigation } from "./SecondaryNavigation";
export { MenuContext, StateMenuContextProvider } from "./MenuContext";
export { default as SecondaryNavigationItem } from "./SecondaryNavigationItem";
export { default as ExternalLink } from "./ExternalLink";
export { default as ExternalNavLink } from "./ExternalNavLink";

View File

@@ -29,13 +29,14 @@ import styled from "styled-components";
type Props = {
file: File;
basePath: string;
};
const MarkdownContent = styled.div`
padding: 0.5rem;
`;
const MarkdownViewer: FC<Props> = ({ file }) => {
const MarkdownViewer: FC<Props> = ({ file, basePath }) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | undefined>(undefined);
const [content, setContent] = useState("");
@@ -62,7 +63,7 @@ const MarkdownViewer: FC<Props> = ({ file }) => {
return (
<MarkdownContent>
<MarkdownView content={content} />
<MarkdownView content={content} basePath={basePath} />
</MarkdownContent>
);
};

View File

@@ -43,9 +43,10 @@ const Container = styled.div`
type Props = {
file: File;
basePath: string;
};
const SwitchableMarkdownViewer: FC<Props> = ({ file }) => {
const SwitchableMarkdownViewer: FC<Props> = ({file, basePath}) => {
const {t} = useTranslation("repos");
const [renderMarkdown, setRenderMarkdown] = useState(true);
@@ -66,7 +67,8 @@ const SwitchableMarkdownViewer: FC<Props> = ({ file }) => {
>
<i className="fab fa-markdown"/>
</ToggleButton>
{renderMarkdown ? <MarkdownViewer file={file} /> : <SourcecodeViewer file={file} language={"MARKDOWN"} />}
{renderMarkdown ? <MarkdownViewer file={file} basePath={basePath}/> :
<SourcecodeViewer file={file} language={"MARKDOWN"}/>}
</Container>
);
};

View File

@@ -76,13 +76,19 @@ class SourcesView extends React.Component<Props, State> {
});
}
createBasePath() {
const { repository, revision } = this.props;
return `/repo/${repository.namespace}/${repository.name}/code/sources/${revision}/`;
}
showSources() {
const { file, revision } = this.props;
const { contentType, language } = this.state;
const basePath = this.createBasePath();
if (contentType.startsWith("image/")) {
return <ImageViewer file={file} />;
} else if (contentType.includes("markdown")) {
return <SwitchableMarkdownViewer file={file} />;
return <SwitchableMarkdownViewer file={file} basePath={basePath} />;
} else if (language) {
return <SourcecodeViewer file={file} language={language} />;
} else if (contentType.startsWith("text/")) {
@@ -94,7 +100,8 @@ class SourcesView extends React.Component<Props, State> {
props={{
file,
contentType,
revision
revision,
basePath
}}
>
<DownloadViewer file={file} />

View File

@@ -146,6 +146,12 @@
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
</dependency>
<!-- rest api -->
<dependency>
@@ -191,9 +197,9 @@
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
@@ -202,16 +208,10 @@
<version>3.0.1-b11</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.0</version>
<version>${jaxb.version}</version>
</dependency>
<!-- injection -->
@@ -438,13 +438,6 @@
<!-- global excludes -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@@ -654,6 +647,20 @@
</configuration>
</plugin>
<plugin>
<groupId>org.basepom.maven</groupId>
<artifactId>duplicate-finder-maven-plugin</artifactId>
<version>1.3.0</version>
<configuration>
<printEqualFiles>false</printEqualFiles>
<failBuildInCaseOfDifferentContentConflict>false</failBuildInCaseOfDifferentContentConflict>
<failBuildInCaseOfEqualContentConflict>false</failBuildInCaseOfEqualContentConflict>
<failBuildInCaseOfConflict>false</failBuildInCaseOfConflict>
<checkCompileClasspath>true</checkCompileClasspath>
<checkRuntimeClasspath>true</checkRuntimeClasspath>
<checkTestClasspath>false</checkTestClasspath>
</configuration>
</plugin>
</plugins>
<finalName>scm-webapp</finalName>

View File

@@ -0,0 +1,59 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import sonia.scm.api.v2.resources.ErrorDto;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import java.util.Collections;
@Provider
public class InvalidFormatExceptionMapper implements ExceptionMapper<InvalidFormatException> {
private static final Logger LOG = LoggerFactory.getLogger(InvalidFormatExceptionMapper.class);
private static final String ERROR_CODE = "2qRyyaVcJ1";
@Override
public Response toResponse(InvalidFormatException exception) {
LOG.trace("got invalid format in json: {}", exception.getMessage());
ErrorDto errorDto = new ErrorDto();
errorDto.setMessage("invalid format in json content: " + exception.getMessage());
errorDto.setContext(Collections.emptyList());
errorDto.setErrorCode(ERROR_CODE);
errorDto.setTransactionId(MDC.get("transaction_id"));
return Response.status(Response.Status.BAD_REQUEST)
.entity(errorDto)
.type(VndMediaType.ERROR_TYPE)
.build();
}
}

View File

@@ -26,7 +26,6 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.spotter.ContentTypes;
import com.github.sdorra.spotter.Language;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.DiffFile;
@@ -42,7 +41,7 @@ import java.util.OptionalInt;
import static de.otto.edison.hal.Links.linkingTo;
/**
* TODO conflicts, copy and rename
* TODO conflicts
*/
class DiffResultToDiffResultDtoMapper {
@@ -83,18 +82,29 @@ class DiffResultToDiffResultDtoMapper {
String oldPath = file.getOldPath();
String path;
if (isFilePath(newPath) && isFileNull(oldPath)) {
switch (file.getChangeType()) {
case ADD:
path = newPath;
dto.setType("add");
} else if (isFileNull(newPath) && isFilePath(oldPath)) {
break;
case DELETE:
path = oldPath;
dto.setType("delete");
} else if (isFilePath(newPath) && isFilePath(oldPath)) {
break;
case RENAME:
path = newPath;
dto.setType("rename");
break;
case MODIFY:
path = newPath;
dto.setType("modify");
} else {
// TODO copy and rename?
throw new IllegalStateException("no file without path");
break;
case COPY:
path = newPath;
dto.setType("copy");
break;
default:
throw new IllegalArgumentException("unknown change type: " + file.getChangeType());
}
dto.setNewPath(newPath);
@@ -116,14 +126,6 @@ class DiffResultToDiffResultDtoMapper {
return dto;
}
private boolean isFilePath(String path) {
return !isFileNull(path);
}
private boolean isFileNull(String path) {
return Strings.isNullOrEmpty(path) || "/dev/null".equals(path);
}
private DiffResultDto.HunkDto mapHunk(Hunk hunk) {
DiffResultDto.HunkDto dto = new DiffResultDto.HunkDto();
dto.setContent(hunk.getRawHeader());

View File

@@ -54,10 +54,21 @@ public class ModificationsDto extends HalRepresentation {
*/
private List<String> removed;
/**
* Mapping of renamed files
*/
private List<RenamedDto> renamed;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
}
@Getter
@Setter
public static class RenamedDto {
private String oldPath;
private String newPath;
}
}

View File

@@ -30,7 +30,11 @@ import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import sonia.scm.repository.Added;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Modified;
import sonia.scm.repository.Removed;
import sonia.scm.repository.Renamed;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
@@ -52,4 +56,18 @@ public abstract class ModificationsToDtoMapper {
.self(resourceLinks.modifications().self(repository.getNamespace(), repository.getName(), target.getRevision()));
target.add(linksBuilder.build());
}
String map(Added added) {
return added.getPath();
}
String map(Removed removed) {
return removed.getPath();
}
String map(Modified modified) {
return modified.getPath();
}
abstract ModificationsDto.RenamedDto map(Renamed renamed);
}

View File

@@ -215,6 +215,10 @@
"8nRuFzjss1": {
"displayName": "Fehler beim Löschen falscher Downloads",
"description": "Ein fehlerhaft heruntergeladenes Plugin konnte nicht gelöscht werden. Bitte prüfen Sie die Server Logs und löschen die Datei manuell."
},
"2qRyyaVcJ1": {
"displayName": "Ungültig formatiertes Element",
"description": "Die Eingabe beinhaltete unfültige Formate. Bitte prüfen Sie die Server Logs für genauere Informationen."
}
},
"namespaceStrategies": {

View File

@@ -215,6 +215,10 @@
"8nRuFzjss1": {
"displayName": "Error while cleaning up failed plugin",
"description": "A failed plugin download could not be removed correctly. Please check the server log and remove the plugin manually."
},
"2qRyyaVcJ1": {
"displayName": "Invalid format in element",
"description": "The input had some invalid formats. Please check the server log for further information."
}
},
"namespaceStrategies": {

View File

@@ -33,16 +33,23 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.*;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.verify;
@@ -173,7 +180,7 @@ public class ProxyPushStateDispatcherTest {
private class DevServletInputStream extends ServletInputStream {
private InputStream inputStream;
private ByteArrayInputStream inputStream;
private DevServletInputStream(String content) {
inputStream = new ByteArrayInputStream(content.getBytes(Charsets.UTF_8));
@@ -183,6 +190,20 @@ public class ProxyPushStateDispatcherTest {
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isReady() {
return inputStream.available() > 0;
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public void setReadListener(ReadListener readListener) {
}
}
private class DevServletOutputStream extends ServletOutputStream {
@@ -193,6 +214,15 @@ public class ProxyPushStateDispatcherTest {
public void write(int b) {
stream.write(b);
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
}

View File

@@ -26,7 +26,7 @@ package sonia.scm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -36,14 +36,14 @@ import java.util.Properties;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith({MockitoExtension.class, TempDirectory.class})
@ExtendWith({MockitoExtension.class})
class ScmLogFilePropertyDefinerTest {
@Mock
private SCMContextProvider context;
@Test
void shouldReturnPath(@TempDirectory.TempDir Path tempDir) {
void shouldReturnPath(@TempDir Path tempDir) {
when(context.getBaseDirectory()).thenReturn(tempDir.toFile());
ScmLogFilePropertyDefiner definer = builder().create();
@@ -52,7 +52,7 @@ class ScmLogFilePropertyDefinerTest {
}
@Test
void shouldReturnOsxPath(@TempDirectory.TempDir Path tempDir) {
void shouldReturnOsxPath(@TempDir Path tempDir) {
ScmLogFilePropertyDefiner definer = builder()
.withOs("Mac OS X")
.withUserHome(tempDir.toAbsolutePath().toString())

View File

@@ -37,6 +37,7 @@ import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.UberWebResourceLoader;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
@@ -152,6 +153,15 @@ public class WebResourceServletTest {
public void write(int b) {
buffer.write(b);
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.jboss.resteasy.spi.Dispatcher;
import org.junit.jupiter.api.Test;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import static org.assertj.core.api.Assertions.assertThat;
class InvalidFormatExceptionMapperTest {
@Test
void shouldMapInvalidFormatExceptionDueToInvalidEnum() throws URISyntaxException, UnsupportedEncodingException {
Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
dispatcher.getRegistry().addSingletonResource(new SimpleResource());
dispatcher.getProviderFactory().registerProvider(InvalidFormatExceptionMapper.class);
MockHttpRequest request = MockHttpRequest
.post("/")
.contentType("application/json")
.content("{\"e\": \"NONE\"}".getBytes());
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getContentAsString()).contains("2qRyyaVcJ1");
}
@Path("/")
static class SimpleResource {
@POST
public void post(ObjectWithEnum o) {
}
}
static class ObjectWithEnum {
public Enum e;
}
enum Enum {
ONE, TWO
}
}

View File

@@ -35,11 +35,13 @@ import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.Hunk;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import static java.net.URI.create;
import static java.util.Collections.emptyIterator;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -60,6 +62,8 @@ class DiffResultToDiffResultDtoMapperTest {
assertAddedFile(files.get(0), "A.java", "abc", "java");
assertModifiedFile(files.get(1), "B.ts", "abc", "def", "typescript");
assertDeletedFile(files.get(2), "C.go", "ghi", "golang");
assertRenamedFile(files.get(3), "typo.ts", "okay.ts", "def", "fixed", "typescript");
assertCopiedFile(files.get(4), "good.ts", "better.ts", "def", "fixed", "typescript");
DiffResultDto.HunkDto hunk = files.get(1).getHunks().get(0);
assertHunk(hunk, "@@ -3,4 1,2 @@", 1, 2, 3, 4);
@@ -104,7 +108,9 @@ class DiffResultToDiffResultDtoMapperTest {
deletedLine("c", 3)
)
),
deletedFile("C.go", "ghi")
deletedFile("C.go", "ghi"),
renamedFile("okay.ts", "typo.ts", "fixed", "def"),
copiedFile("better.ts", "good.ts", "fixed", "def")
);
}
@@ -161,6 +167,24 @@ class DiffResultToDiffResultDtoMapperTest {
assertThat(file.getLanguage()).isEqualTo(language);
}
private void assertRenamedFile(DiffResultDto.FileDto file, String oldPath, String newPath, String oldRevision, String newRevision, String language) {
assertThat(file.getOldPath()).isEqualTo(oldPath);
assertThat(file.getNewPath()).isEqualTo(newPath);
assertThat(file.getOldRevision()).isEqualTo(oldRevision);
assertThat(file.getNewRevision()).isEqualTo(newRevision);
assertThat(file.getType()).isEqualTo("rename");
assertThat(file.getLanguage()).isEqualTo(language);
}
private void assertCopiedFile(DiffResultDto.FileDto file, String oldPath, String newPath, String oldRevision, String newRevision, String language) {
assertThat(file.getOldPath()).isEqualTo(oldPath);
assertThat(file.getNewPath()).isEqualTo(newPath);
assertThat(file.getOldRevision()).isEqualTo(oldRevision);
assertThat(file.getNewRevision()).isEqualTo(newRevision);
assertThat(file.getType()).isEqualTo("copy");
assertThat(file.getLanguage()).isEqualTo(language);
}
private DiffResult result(DiffFile... files) {
DiffResult result = mock(DiffResult.class);
when(result.iterator()).thenReturn(Arrays.asList(files).iterator());
@@ -171,6 +195,7 @@ class DiffResultToDiffResultDtoMapperTest {
DiffFile file = mock(DiffFile.class);
when(file.getNewPath()).thenReturn(path);
when(file.getNewRevision()).thenReturn(revision);
when(file.getChangeType()).thenReturn(DiffFile.ChangeType.ADD);
when(file.iterator()).thenReturn(Arrays.asList(hunks).iterator());
return file;
}
@@ -179,6 +204,7 @@ class DiffResultToDiffResultDtoMapperTest {
DiffFile file = mock(DiffFile.class);
when(file.getOldPath()).thenReturn(path);
when(file.getOldRevision()).thenReturn(revision);
when(file.getChangeType()).thenReturn(DiffFile.ChangeType.DELETE);
when(file.iterator()).thenReturn(Arrays.asList(hunks).iterator());
return file;
}
@@ -189,10 +215,33 @@ class DiffResultToDiffResultDtoMapperTest {
when(file.getNewRevision()).thenReturn(newRevision);
when(file.getOldPath()).thenReturn(path);
when(file.getOldRevision()).thenReturn(oldRevision);
when(file.getChangeType()).thenReturn(DiffFile.ChangeType.MODIFY);
when(file.iterator()).thenReturn(Arrays.asList(hunks).iterator());
return file;
}
private DiffFile renamedFile(String newPath, String oldPath, String newRevision, String oldRevision) {
DiffFile file = mock(DiffFile.class);
when(file.getNewPath()).thenReturn(newPath);
when(file.getNewRevision()).thenReturn(newRevision);
when(file.getOldPath()).thenReturn(oldPath);
when(file.getOldRevision()).thenReturn(oldRevision);
when(file.getChangeType()).thenReturn(DiffFile.ChangeType.RENAME);
when(file.iterator()).thenReturn(emptyIterator());
return file;
}
private DiffFile copiedFile(String newPath, String oldPath, String newRevision, String oldRevision) {
DiffFile file = mock(DiffFile.class);
when(file.getNewPath()).thenReturn(newPath);
when(file.getNewRevision()).thenReturn(newRevision);
when(file.getOldPath()).thenReturn(oldPath);
when(file.getOldRevision()).thenReturn(oldRevision);
when(file.getChangeType()).thenReturn(DiffFile.ChangeType.COPY);
when(file.iterator()).thenReturn(emptyIterator());
return file;
}
private Hunk hunk(String rawHeader, int newStart, int newLineCount, int oldStart, int oldLineCount, DiffLine... lines) {
Hunk hunk = mock(Hunk.class);
when(hunk.getRawHeader()).thenReturn(rawHeader);

View File

@@ -30,7 +30,6 @@ import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState;
import org.assertj.core.util.Lists;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.After;
@@ -40,9 +39,12 @@ import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.Added;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Removed;
import sonia.scm.repository.Modified;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.ModificationsCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
@@ -141,7 +143,6 @@ public class ModificationsResourceTest extends RepositoryTestBase {
@Test
public void shouldGetModifications() throws Exception {
Modifications modifications = new Modifications();
String revision = "revision";
String addedFile_1 = "a.txt";
String addedFile_2 = "b.txt";
@@ -149,10 +150,13 @@ public class ModificationsResourceTest extends RepositoryTestBase {
String modifiedFile_2 = "c.txt";
String removedFile_1 = "e.txt";
String removedFile_2 = "f.txt";
modifications.setRevision(revision);
modifications.setAdded(Lists.newArrayList(addedFile_1, addedFile_2));
modifications.setModified(Lists.newArrayList(modifiedFile_1, modifiedFile_2));
modifications.setRemoved(Lists.newArrayList(removedFile_1, removedFile_2));
Modifications modifications = new Modifications(revision,
new Added(addedFile_1),
new Added(addedFile_2),
new Modified(modifiedFile_1),
new Modified(modifiedFile_2),
new Removed(removedFile_1),
new Removed(removedFile_2));
when(modificationsCommandBuilder.getModifications()).thenReturn(modifications);
when(modificationsCommandBuilder.revision(eq(revision))).thenReturn(modificationsCommandBuilder);

View File

@@ -34,6 +34,7 @@ import sonia.scm.Stage;
import sonia.scm.event.ScmEventBus;
import sonia.scm.event.ScmTestEventBus;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -128,6 +129,21 @@ public class RestartServletTest {
private ServletInputStream createServletInputStream(final InputStream inputStream) {
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return inputStream.read();

View File

@@ -26,7 +26,7 @@ package sonia.scm.lifecycle;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -41,7 +41,7 @@ import java.nio.file.Paths;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
@ExtendWith({MockitoExtension.class, TempDirectory.class})
@ExtendWith({MockitoExtension.class})
class VersionsTest {
@Mock
@@ -51,7 +51,7 @@ class VersionsTest {
private Versions versions;
@Test
void shouldReturnTrueForVersionsPreviousTo160(@TempDirectory.TempDir Path directory) throws IOException {
void shouldReturnTrueForVersionsPreviousTo160(@TempDir Path directory) throws IOException {
setVersion(directory, "1.59");
assertThat(versions.isPreviousVersionTooOld()).isTrue();
@@ -60,19 +60,19 @@ class VersionsTest {
}
@Test
void shouldReturnFalseForVersion160(@TempDirectory.TempDir Path directory) throws IOException {
void shouldReturnFalseForVersion160(@TempDir Path directory) throws IOException {
setVersion(directory, "1.60");
assertThat(versions.isPreviousVersionTooOld()).isFalse();
}
@Test
void shouldNotFailIfVersionContainsLineBreak(@TempDirectory.TempDir Path directory) throws IOException {
void shouldNotFailIfVersionContainsLineBreak(@TempDir Path directory) throws IOException {
setVersion(directory, "1.59\n");
assertThat(versions.isPreviousVersionTooOld()).isTrue();
}
@Test
void shouldReturnFalseForVersionsNewerAs160(@TempDirectory.TempDir Path directory) throws IOException {
void shouldReturnFalseForVersionsNewerAs160(@TempDir Path directory) throws IOException {
setVersion(directory, "1.61");
assertThat(versions.isPreviousVersionTooOld()).isFalse();
@@ -81,13 +81,13 @@ class VersionsTest {
}
@Test
void shouldReturnFalseForNonExistingVersionFile(@TempDirectory.TempDir Path directory) {
void shouldReturnFalseForNonExistingVersionFile(@TempDir Path directory) {
setVersionFile(directory.resolve("version.txt"));
assertThat(versions.isPreviousVersionTooOld()).isFalse();
}
@Test
void shouldWriteNewVersion(@TempDirectory.TempDir Path directory) {
void shouldWriteNewVersion(@TempDir Path directory) {
Path config = directory.resolve("config");
doReturn(config).when(contextProvider).resolve(Paths.get("config"));
doReturn("2.0.0").when(contextProvider).getVersion();

View File

@@ -34,7 +34,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
@@ -48,7 +48,6 @@ import sonia.scm.lifecycle.Restarter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -56,7 +55,6 @@ import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.in;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
@@ -72,7 +70,6 @@ import static sonia.scm.plugin.PluginTestHelper.createAvailable;
import static sonia.scm.plugin.PluginTestHelper.createInstalled;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class DefaultPluginManagerTest {
@Mock
@@ -372,7 +369,7 @@ class DefaultPluginManagerTest {
}
@Test
void shouldCreateUninstallFile(@TempDirectory.TempDir Path temp) {
void shouldCreateUninstallFile(@TempDir Path temp) {
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
when(mailPlugin.getDirectory()).thenReturn(temp);
@@ -384,7 +381,7 @@ class DefaultPluginManagerTest {
}
@Test
void shouldMarkPluginForUninstall(@TempDirectory.TempDir Path temp) {
void shouldMarkPluginForUninstall(@TempDir Path temp) {
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
when(mailPlugin.getDirectory()).thenReturn(temp);
@@ -414,7 +411,7 @@ class DefaultPluginManagerTest {
}
@Test
void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDirectory.TempDir Path temp) {
void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDir Path temp) {
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
when(mailPlugin.getDirectory()).thenReturn(temp);
when(mailPlugin.isCore()).thenReturn(true);
@@ -484,7 +481,7 @@ class DefaultPluginManagerTest {
}
@Test
void shouldUndoPendingInstallations(@TempDirectory.TempDir Path temp) throws IOException {
void shouldUndoPendingInstallations(@TempDir Path temp) throws IOException {
InstalledPlugin mailPlugin = createInstalled("scm-ssh-plugin");
Path mailPluginPath = temp.resolve("scm-mail-plugin");
Files.createDirectories(mailPluginPath);

View File

@@ -26,7 +26,7 @@ package sonia.scm.plugin;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -39,14 +39,14 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
@ExtendWith({MockitoExtension.class, TempDirectory.class})
@ExtendWith({MockitoExtension.class})
class PendingPluginInstallationTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private AvailablePlugin plugin;
@Test
void shouldDeleteFileOnCancel(@TempDirectory.TempDir Path directory) throws IOException {
void shouldDeleteFileOnCancel(@TempDir Path directory) throws IOException {
Path file = directory.resolve("file");
Files.write(file, "42".getBytes());
@@ -59,7 +59,7 @@ class PendingPluginInstallationTest {
}
@Test
void shouldThrowExceptionIfCancelFailed(@TempDirectory.TempDir Path directory) {
void shouldThrowExceptionIfCancelFailed(@TempDir Path directory) {
Path file = directory.resolve("file");
when(plugin.getDescriptor().getInformation().getName()).thenReturn("scm-awesome-plugin");

View File

@@ -27,7 +27,7 @@ package sonia.scm.plugin;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
@@ -45,9 +45,13 @@ import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith({MockitoExtension.class, TempDirectory.class})
@ExtendWith({MockitoExtension.class})
class PluginInstallerTest {
@Mock
@@ -65,7 +69,7 @@ class PluginInstallerTest {
private Path directory;
@BeforeEach
void setUpContext(@TempDirectory.TempDir Path directory) throws IOException {
void setUpContext(@TempDir Path directory) throws IOException {
this.directory = directory;
lenient().when(context.resolve(any())).then(ic -> {
Path arg = ic.getArgument(0);

View File

@@ -26,8 +26,7 @@ package sonia.scm.plugin;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
@@ -38,7 +37,6 @@ import java.util.zip.ZipOutputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(TempDirectory.class)
class SmpDescriptorExtractorTest {
private static final String PLUGIN_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
@@ -66,7 +64,7 @@ class SmpDescriptorExtractorTest {
"</plugin>\n";
@Test
void shouldExtractPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldExtractPluginXml(@TempDir Path tempDir) throws IOException {
Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", PLUGIN_XML);
InstalledPluginDescriptor installedPluginDescriptor = new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile);
@@ -75,14 +73,14 @@ class SmpDescriptorExtractorTest {
}
@Test
void shouldFailWithoutPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldFailWithoutPluginXml(@TempDir Path tempDir) throws IOException {
Path pluginFile = createZipFile(tempDir, "META-INF/wrong/plugin.xml", PLUGIN_XML);
assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile));
}
@Test
void shouldFailWithIllegalPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldFailWithIllegalPluginXml(@TempDir Path tempDir) throws IOException {
Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", "<not><parsable>content</parsable></not>");
assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile));

View File

@@ -27,8 +27,7 @@ package sonia.scm.plugin;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Resources;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.net.URL;
@@ -43,13 +42,12 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(TempDirectory.class)
class UberClassLoaderTest {
private final URLClassLoader parentClassLoader = new URLClassLoader(new URL[0]);
@Test
void shouldOnlyUseClassloaderOnce(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldOnlyUseClassloaderOnce(@TempDir Path tempDir) throws IOException {
ClassLoader mailClassLoader = createClassLoader(tempDir, "plugin.txt", "mail");
ClassLoader reviewClassLoader = createClassLoader(mailClassLoader, tempDir, "plugin.txt", "review");
@@ -61,7 +59,7 @@ class UberClassLoaderTest {
}
@Test
void shouldReturnResourceFromEachPluginClassLoader(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldReturnResourceFromEachPluginClassLoader(@TempDir Path tempDir) throws IOException {
ClassLoader mailClassLoader = createClassLoader(tempDir, "scm.txt", "mail");
ClassLoader reviewClassLoader = createClassLoader(tempDir, "scm.txt", "review");

View File

@@ -28,7 +28,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@@ -39,7 +39,6 @@ import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
import sonia.scm.update.UpdateStepTestUtil;
import sonia.scm.update.V1Properties;
import sonia.scm.update.V1Property;
import javax.xml.bind.JAXBException;
import java.io.IOException;
@@ -55,7 +54,6 @@ import static org.mockito.Mockito.verify;
import static sonia.scm.store.InMemoryConfigurationEntryStoreFactory.create;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class XmlGroupV1UpdateStepTest {
@Mock
@@ -72,7 +70,7 @@ class XmlGroupV1UpdateStepTest {
@BeforeEach
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
void mockScmHome(@TempDir Path tempDir) {
testUtil = new UpdateStepTestUtil(tempDir);
updateStep = new XmlGroupV1UpdateStep(testUtil.getContextProvider(), groupDAO, storeFactory);
}

View File

@@ -27,7 +27,7 @@ package sonia.scm.update.repository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
@@ -44,7 +44,6 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
@ExtendWith(MockitoExtension.class)
class CopyMigrationStrategyTest {
@@ -54,30 +53,30 @@ class CopyMigrationStrategyTest {
RepositoryLocationResolver locationResolver;
@BeforeEach
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
void mockContextProvider(@TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
}
@BeforeEach
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
void createV1Home(@TempDir Path tempDir) throws IOException {
V1RepositoryFileSystem.createV1Home(tempDir);
}
@BeforeEach
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
void mockLocationResolver(@TempDir Path tempDir) {
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
}
@Test
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
void shouldUseStandardDirectory(@TempDir Path tempDir) {
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
}
@Test
void shouldCopyDataDirectory(@TempDirectory.TempDir Path tempDir) {
void shouldCopyDataDirectory(@TempDir Path tempDir) {
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target.resolve("data")).exists();
Path originalDataDir = tempDir

View File

@@ -29,7 +29,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
@@ -43,7 +43,6 @@ import static org.mockito.Mockito.when;
import static sonia.scm.update.repository.MigrationStrategy.INLINE;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class DefaultMigrationStrategyDAOTest {
@Mock
@@ -52,7 +51,7 @@ class DefaultMigrationStrategyDAOTest {
private ConfigurationStoreFactory storeFactory;
@BeforeEach
void initStore(@TempDirectory.TempDir Path tempDir) {
void initStore(@TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null);
}

View File

@@ -27,7 +27,7 @@ package sonia.scm.update.repository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
@@ -41,7 +41,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
@ExtendWith(MockitoExtension.class)
class InlineMigrationStrategyTest {
@@ -53,25 +52,25 @@ class InlineMigrationStrategyTest {
RepositoryLocationResolver.RepositoryLocationResolverInstance locationResolverInstance;
@BeforeEach
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
void mockContextProvider(@TempDir Path tempDir) {
when(locationResolver.forClass(Path.class)).thenReturn(locationResolverInstance);
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
}
@BeforeEach
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
void createV1Home(@TempDir Path tempDir) throws IOException {
V1RepositoryFileSystem.createV1Home(tempDir);
}
@Test
void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) {
void shouldUseExistingDirectory(@TempDir Path tempDir) {
Path target = new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target).isEqualTo(resolveOldDirectory(tempDir));
verify(locationResolverInstance).setLocation("b4f-a9f0-49f7-ad1f-37d3aae1c55f", target);
}
@Test
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
void shouldMoveDataDirectory(@TempDir Path tempDir) {
new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
assertThat(resolveOldDirectory(tempDir).resolve("data")).exists();
}

View File

@@ -29,7 +29,7 @@ import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -51,7 +51,6 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class MigrateVerbsToPermissionRolesTest {
private static final String EXISTING_REPOSITORY_ID = "id";
@@ -65,7 +64,7 @@ class MigrateVerbsToPermissionRolesTest {
private MigrateVerbsToPermissionRoles migration;
@BeforeEach
void init(@TempDirectory.TempDir Path tempDir) throws IOException {
void init(@TempDir Path tempDir) throws IOException {
URL metadataUrl = Resources.getResource("sonia/scm/update/repository/metadataWithoutRoles.xml");
Files.copy(metadataUrl.openStream(), tempDir.resolve("metadata.xml"));
doAnswer(invocation -> {
@@ -76,7 +75,7 @@ class MigrateVerbsToPermissionRolesTest {
}
@Test
void shouldUpdateToRolesIfPossible(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldUpdateToRolesIfPossible(@TempDir Path tempDir) throws IOException {
migration.doUpdate();
List<String> newMetadata = Files.readAllLines(tempDir.resolve("metadata.xml"));

View File

@@ -27,7 +27,7 @@ package sonia.scm.update.repository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
@@ -41,7 +41,6 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
@ExtendWith(MockitoExtension.class)
class MoveMigrationStrategyTest {
@@ -51,30 +50,30 @@ class MoveMigrationStrategyTest {
RepositoryLocationResolver locationResolver;
@BeforeEach
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
void mockContextProvider(@TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
}
@BeforeEach
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
void createV1Home(@TempDir Path tempDir) throws IOException {
V1RepositoryFileSystem.createV1Home(tempDir);
}
@BeforeEach
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
void mockLocationResolver(@TempDir Path tempDir) {
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
}
@Test
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
void shouldUseStandardDirectory(@TempDir Path tempDir) {
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
}
@Test
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
void shouldMoveDataDirectory(@TempDir Path tempDir) {
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target.resolve("data")).exists();
Path originalDataDir = tempDir

View File

@@ -27,7 +27,7 @@ package sonia.scm.update.repository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@@ -35,7 +35,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContext;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryRolePermissions;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.update.UpdateStepTestUtil;
@@ -49,7 +48,6 @@ import java.nio.file.Path;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junitpioneer.jupiter.TempDirectory.TempDir;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
@@ -58,7 +56,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class PublicFlagUpdateStepTest {
@Mock

View File

@@ -27,8 +27,7 @@ package sonia.scm.update.repository;
import com.google.common.io.Resources;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.repository.xml.XmlRepositoryDAO;
@@ -43,19 +42,18 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
class XmlRepositoryFileNameUpdateStepTest {
SCMContextProvider contextProvider = mock(SCMContextProvider.class);
XmlRepositoryDAO repositoryDAO = mock(XmlRepositoryDAO.class);
@BeforeEach
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
void mockScmHome(@TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
}
@Test
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDir Path tempDir) throws IOException {
XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider, repositoryDAO);
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
Path configDir = tempDir.resolve("config");

View File

@@ -30,7 +30,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@@ -63,7 +63,6 @@ import static org.mockito.Mockito.when;
import static sonia.scm.update.repository.MigrationStrategy.MOVE;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class XmlRepositoryV1UpdateStepTest {
Injector injectorMock = MigrationStrategyMock.init();
@@ -85,7 +84,7 @@ class XmlRepositoryV1UpdateStepTest {
XmlRepositoryV1UpdateStep updateStep;
@BeforeEach
void createUpdateStepFromMocks(@TempDirectory.TempDir Path tempDir) {
void createUpdateStepFromMocks(@TempDir Path tempDir) {
testUtil = new UpdateStepTestUtil(tempDir);
updateStep = new XmlRepositoryV1UpdateStep(
testUtil.getContextProvider(),
@@ -100,7 +99,7 @@ class XmlRepositoryV1UpdateStepTest {
class WithExistingDatabase {
@BeforeEach
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
void createV1Home(@TempDir Path tempDir) throws IOException {
V1RepositoryFileSystem.createV1Home(tempDir);
}
@@ -165,7 +164,7 @@ class XmlRepositoryV1UpdateStepTest {
}
@Test
void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException {
void shouldUseDirectoryFromStrategy(@TempDir Path tempDir) throws JAXBException {
Path targetDir = tempDir.resolve("someDir");
MigrationStrategy.Instance strategyMock = injectorMock.getInstance(MoveMigrationStrategy.class);
when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(of(targetDir));
@@ -195,7 +194,7 @@ class XmlRepositoryV1UpdateStepTest {
}
@Test
void shouldBackupOldRepositoryDatabaseFile(@TempDirectory.TempDir Path tempDir) throws JAXBException {
void shouldBackupOldRepositoryDatabaseFile(@TempDir Path tempDir) throws JAXBException {
updateStep.doUpdate();
assertThat(tempDir.resolve("config").resolve("repositories.xml")).doesNotExist();
@@ -209,14 +208,14 @@ class XmlRepositoryV1UpdateStepTest {
}
@Test
void shouldNotFailIfFormerV2DatabaseExists(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
void shouldNotFailIfFormerV2DatabaseExists(@TempDir Path tempDir) throws JAXBException, IOException {
createFormerV2RepositoriesFile(tempDir);
updateStep.doUpdate();
}
@Test
void shouldNotBackupFormerV2DatabaseFile(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
void shouldNotBackupFormerV2DatabaseFile(@TempDir Path tempDir) throws JAXBException, IOException {
createFormerV2RepositoriesFile(tempDir);
updateStep.doUpdate();
@@ -226,14 +225,14 @@ class XmlRepositoryV1UpdateStepTest {
}
@Test
void shouldGetNoMissingStrategiesWithFormerV2DatabaseFile(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldGetNoMissingStrategiesWithFormerV2DatabaseFile(@TempDir Path tempDir) throws IOException {
createFormerV2RepositoriesFile(tempDir);
assertThat(updateStep.getRepositoriesWithoutMigrationStrategies()).isEmpty();
}
@Test
void shouldFindMissingStrategies(@TempDirectory.TempDir Path tempDir) throws IOException {
void shouldFindMissingStrategies(@TempDir Path tempDir) throws IOException {
V1RepositoryFileSystem.createV1Home(tempDir);
assertThat(updateStep.getRepositoriesWithoutMigrationStrategies())
@@ -244,7 +243,7 @@ class XmlRepositoryV1UpdateStepTest {
"454972da-faf9-4437-b682-dc4a4e0aa8eb");
}
private void createFormerV2RepositoriesFile(@TempDirectory.TempDir Path tempDir) throws IOException {
private void createFormerV2RepositoriesFile(@TempDir Path tempDir) throws IOException {
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
Path configDir = tempDir.resolve("config");
Files.createDirectories(configDir);

Some files were not shown because too many files have changed in this diff Show More