Merge with default

This commit is contained in:
Rene Pfeuffer
2019-12-13 08:57:16 +01:00
209 changed files with 2725 additions and 1077 deletions

4
Jenkinsfile vendored
View File

@@ -7,7 +7,7 @@ import com.cloudogu.ces.cesbuildlib.*
node('docker') { node('docker') {
// Change this as when we go back to default - necessary for proper SonarQube analysis // Change this as when we go back to default - necessary for proper SonarQube analysis
mainBranch = '2.0.0-m3' mainBranch = 'default'
properties([ properties([
// Keep only the last 10 build to preserve space // Keep only the last 10 build to preserve space
@@ -37,7 +37,7 @@ node('docker') {
} }
stage('Integration Test') { stage('Integration Test') {
mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true' mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true -DClassLoaderLeakPreventor.threadWaitMs=10'
} }
stage('SonarQube') { stage('SonarQube') {

View File

@@ -11,7 +11,8 @@
"test": "lerna run --scope '@scm-manager/ui-*' test", "test": "lerna run --scope '@scm-manager/ui-*' test",
"typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck", "typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck",
"serve": "webpack-dev-server --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js", "serve": "webpack-dev-server --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js",
"deploy": "ui-scripts publish" "deploy": "ui-scripts publish",
"set-version": "ui-scripts version"
}, },
"devDependencies": { "devDependencies": {
"babel-plugin-reflow": "^0.2.7", "babel-plugin-reflow": "^0.2.7",

14
pom.xml
View File

@@ -220,7 +220,13 @@
<dependency> <dependency>
<groupId>org.jboss.resteasy</groupId> <groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId> <artifactId>resteasy-core</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-core-spi</artifactId>
<version>${resteasy.version}</version> <version>${resteasy.version}</version>
</dependency> </dependency>
@@ -444,7 +450,7 @@
<plugin> <plugin>
<groupId>sonia.scm.maven</groupId> <groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId> <artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-8</version> <version>1.0.0-rc1</version>
</plugin> </plugin>
<plugin> <plugin>
@@ -831,7 +837,7 @@
<servlet.version>3.0.1</servlet.version> <servlet.version>3.0.1</servlet.version>
<jaxrs.version>2.1.1</jaxrs.version> <jaxrs.version>2.1.1</jaxrs.version>
<resteasy.version>3.6.2.Final</resteasy.version> <resteasy.version>4.4.1.Final</resteasy.version>
<jersey-client.version>1.19.4</jersey-client.version> <jersey-client.version>1.19.4</jersey-client.version>
<enunciate.version>2.11.1</enunciate.version> <enunciate.version>2.11.1</enunciate.version>
<jackson.version>2.10.0</jackson.version> <jackson.version>2.10.0</jackson.version>
@@ -839,7 +845,7 @@
<jaxb.version>2.3.0</jaxb.version> <jaxb.version>2.3.0</jaxb.version>
<!-- event bus --> <!-- event bus -->
<legman.version>1.5.1</legman.version> <legman.version>1.6.1</legman.version>
<!-- webserver --> <!-- webserver -->
<jetty.version>9.4.22.v20191022</jetty.version> <jetty.version>9.4.22.v20191022</jetty.version>

View File

@@ -99,8 +99,9 @@ import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource; import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamResult;
import static javax.lang.model.util.ElementFilter.methodsIn;
/** /**
*
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@SupportedAnnotationTypes("*") @SupportedAnnotationTypes("*")
@@ -372,6 +373,18 @@ public final class ScmAnnotationProcessor extends AbstractProcessor {
attributes.put(entry.getKey().getSimpleName().toString(), attributes.put(entry.getKey().getSimpleName().toString(),
getValue(entry.getValue())); getValue(entry.getValue()));
} }
// add default values
for (ExecutableElement meth : methodsIn(annotationMirror.getAnnotationType().asElement().getEnclosedElements())) {
String attribute = meth.getSimpleName().toString();
AnnotationValue defaultValue = meth.getDefaultValue();
if (defaultValue != null && !attributes.containsKey(attribute)) {
String value = getValue(defaultValue);
if (value != null && !value.isEmpty()) {
attributes.put(attribute, value);
}
}
}
} }
} }

View File

@@ -8,8 +8,7 @@
<artifactId>scm</artifactId> <artifactId>scm</artifactId>
<version>2.0.0-SNAPSHOT</version> <version>2.0.0-SNAPSHOT</version>
</parent> </parent>
<groupId>sonia.scm</groupId>
<artifactId>scm-annotations</artifactId> <artifactId>scm-annotations</artifactId>
<version>2.0.0-SNAPSHOT</version> <version>2.0.0-SNAPSHOT</version>
<name>scm-annotations</name> <name>scm-annotations</name>

View File

@@ -98,7 +98,7 @@
<dependency> <dependency>
<groupId>org.jboss.resteasy</groupId> <groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId> <artifactId>resteasy-core</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>

View File

@@ -59,7 +59,7 @@ public final class SCMContext
*/ */
public static final User ANONYMOUS = new User(USER_ANONYMOUS, public static final User ANONYMOUS = new User(USER_ANONYMOUS,
"SCM Anonymous", "SCM Anonymous",
"scm-anonymous@scm-manager.com"); "scm-anonymous@scm-manager.org");
/** Singleton instance of {@link SCMContextProvider} */ /** Singleton instance of {@link SCMContextProvider} */
private static volatile SCMContextProvider provider; private static volatile SCMContextProvider provider;

View File

@@ -134,7 +134,7 @@ public final class IterableQueue<T> implements Iterable<T>
else else
{ {
logger.trace("create queue iterator"); logger.trace("create queue iterator");
iterator = new QueueIterator<T>(this); iterator = new QueueIterator<>(this);
} }
return iterator; return iterator;

View File

@@ -63,7 +63,7 @@ public class LimitedSortedSet<E> extends ForwardingSortedSet<E>
*/ */
public LimitedSortedSet(int maxSize) public LimitedSortedSet(int maxSize)
{ {
this.sortedSet = new TreeSet<E>(); this.sortedSet = new TreeSet<>();
this.maxSize = maxSize; this.maxSize = maxSize;
} }

View File

@@ -183,5 +183,5 @@ public abstract class AbstractResourceProcessor implements ResourceProcessor
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */
private Map<String, String> variableMap = new HashMap<String, String>(); private Map<String, String> variableMap = new HashMap<>();
} }

View File

@@ -52,7 +52,7 @@ public class INIConfiguration
*/ */
public INIConfiguration() public INIConfiguration()
{ {
this.sectionMap = new LinkedHashMap<String, INISection>(); this.sectionMap = new LinkedHashMap<>();
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------

View File

@@ -55,7 +55,7 @@ public class INISection
public INISection(String name) public INISection(String name)
{ {
this.name = name; this.name = name;
this.parameters = new LinkedHashMap<String, String>(); this.parameters = new LinkedHashMap<>();
} }
/** /**

View File

@@ -25,7 +25,7 @@ public class AvailablePlugin implements Plugin {
return pending; return pending;
} }
public AvailablePlugin install() { AvailablePlugin install() {
Preconditions.checkState(!pending, "installation is already pending"); Preconditions.checkState(!pending, "installation is already pending");
return new AvailablePlugin(pluginDescriptor, true); return new AvailablePlugin(pluginDescriptor, true);
} }

View File

@@ -113,7 +113,6 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler
Repository repository = new Repository(); Repository repository = new Repository();
repository.setName(repositoryName); repository.setName(repositoryName);
repository.setPublicReadable(false);
repository.setType(getTypeName()); repository.setType(getTypeName());
return repository; return repository;

View File

@@ -83,8 +83,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private String name; private String name;
@XmlElement(name = "permission") @XmlElement(name = "permission")
private Set<RepositoryPermission> permissions = new HashSet<>(); private Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private String type; private String type;
@@ -225,15 +223,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return Util.isEmpty(healthCheckFailures); return Util.isEmpty(healthCheckFailures);
} }
/**
* Returns true if the {@link Repository} is public readable.
*
* @return true if the {@link Repository} is public readable
*/
public boolean isPublicReadable() {
return publicReadable;
}
/** /**
* Returns true if the {@link Repository} is valid. * Returns true if the {@link Repository} is valid.
* <ul> * <ul>
@@ -292,10 +281,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return this.permissions.remove(permission); return this.permissions.remove(permission);
} }
public void setPublicReadable(boolean publicReadable) {
this.publicReadable = publicReadable;
}
public void setType(String type) { public void setType(String type) {
this.type = type; this.type = type;
} }
@@ -332,7 +317,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
repository.setLastModified(lastModified); repository.setLastModified(lastModified);
repository.setDescription(description); repository.setDescription(description);
repository.setPermissions(permissions); repository.setPermissions(permissions);
repository.setPublicReadable(publicReadable);
// do not copy health check results // do not copy health check results
} }
@@ -360,7 +344,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
&& Objects.equal(name, other.name) && Objects.equal(name, other.name)
&& Objects.equal(contact, other.contact) && Objects.equal(contact, other.contact)
&& Objects.equal(description, other.description) && Objects.equal(description, other.description)
&& Objects.equal(publicReadable, other.publicReadable)
&& Objects.equal(permissions, other.permissions) && Objects.equal(permissions, other.permissions)
&& Objects.equal(type, other.type) && Objects.equal(type, other.type)
&& Objects.equal(creationDate, other.creationDate) && Objects.equal(creationDate, other.creationDate)
@@ -371,7 +354,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode(id, namespace, name, contact, description, publicReadable, return Objects.hashCode(id, namespace, name, contact, description,
permissions, type, creationDate, lastModified, properties, permissions, type, creationDate, lastModified, properties,
healthCheckFailures); healthCheckFailures);
} }
@@ -384,7 +367,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
.add("name", name) .add("name", name)
.add("contact", contact) .add("contact", contact)
.add("description", description) .add("description", description)
.add("publicReadable", publicReadable)
.add("permissions", permissions) .add("permissions", permissions)
.add("type", type) .add("type", type)
.add("lastModified", lastModified) .add("lastModified", lastModified)

View File

@@ -46,7 +46,7 @@ public final class Base32 extends AbstractBase
{ {
/** base value */ /** base value */
private static final BigInteger BASE = BigInteger.valueOf(32l); private static final BigInteger BASE = BigInteger.valueOf(32L);
/** char table */ /** char table */
private static final String CHARS = "0123456789bcdefghjkmnpqrstuvwxyz"; private static final String CHARS = "0123456789bcdefghjkmnpqrstuvwxyz";

View File

@@ -46,7 +46,7 @@ public final class Base62 extends AbstractBase
{ {
/** base value */ /** base value */
private static final BigInteger BASE = BigInteger.valueOf(62l); private static final BigInteger BASE = BigInteger.valueOf(62L);
/** char table */ /** char table */
private static final String CHARS = private static final String CHARS =

View File

@@ -79,7 +79,7 @@ public final class LinkTextParser
public static String parseText(String content) public static String parseText(String content)
{ {
Matcher m = REGEX_URL.matcher(content); Matcher m = REGEX_URL.matcher(content);
List<Token> tokens = new ArrayList<Token>(); List<Token> tokens = new ArrayList<>();
int position = 0; int position = 0;
String tokenContent = null; String tokenContent = null;

View File

@@ -119,7 +119,7 @@ public final class ServiceUtil
*/ */
public static <T> List<T> getServices(Class<T> type) public static <T> List<T> getServices(Class<T> type)
{ {
List<T> result = new ArrayList<T>(); List<T> result = new ArrayList<>();
try try
{ {

View File

@@ -38,23 +38,12 @@ package sonia.scm.util;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
//~--- JDK imports ------------------------------------------------------------
import java.math.BigInteger; import java.math.BigInteger;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*;
import java.util.ArrayList; //~--- JDK imports ------------------------------------------------------------
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
/** /**
* *
@@ -273,12 +262,12 @@ public final class Util
Comparator<T> comparator, CollectionAppender<T> appender, int start, Comparator<T> comparator, CollectionAppender<T> appender, int start,
int limit) int limit)
{ {
List<T> result = new ArrayList<T>(); List<T> result = new ArrayList<>();
List<T> valueList = new ArrayList<T>(values); List<T> valueList = new ArrayList<>(values);
if (comparator != null) if (comparator != null)
{ {
Collections.sort(valueList, comparator); valueList.sort(comparator);
} }
int length = valueList.size(); int length = valueList.size();
@@ -506,12 +495,10 @@ public final class Util
{ {
StringBuilder buffer = new StringBuilder(); StringBuilder buffer = new StringBuilder();
for (int i = 0; i < byteValue.length; i++) for (final byte aByteValue : byteValue) {
{ int x = aByteValue & 0xff;
int x = byteValue[i] & 0xff;
if (x < 16) if (x < 16) {
{
buffer.append('0'); buffer.append('0');
} }

View File

@@ -66,7 +66,7 @@ public class EnvList
*/ */
public EnvList() public EnvList()
{ {
envMap = new HashMap<String, String>(); envMap = new HashMap<>();
} }
/** /**
@@ -77,7 +77,7 @@ public class EnvList
*/ */
public EnvList(EnvList l) public EnvList(EnvList l)
{ {
envMap = new HashMap<String, String>(l.envMap); envMap = new HashMap<>(l.envMap);
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------

View File

@@ -470,13 +470,13 @@ public class BufferedHttpServletResponse extends HttpServletResponseWrapper
private ByteArrayPrintWriter pw = null; private ByteArrayPrintWriter pw = null;
/** Field description */ /** Field description */
private Set<Cookie> cookies = new HashSet<Cookie>(); private Set<Cookie> cookies = new HashSet<>();
/** Field description */ /** Field description */
private int statusCode = HttpServletResponse.SC_OK; private int statusCode = HttpServletResponse.SC_OK;
/** Field description */ /** Field description */
private Map<String, String> headers = new LinkedHashMap<String, String>(); private Map<String, String> headers = new LinkedHashMap<>();
/** Field description */ /** Field description */
private String statusMessage; private String statusMessage;

View File

@@ -40,28 +40,20 @@ import com.google.common.io.ByteStreams;
import com.google.common.io.Closer; import com.google.common.io.Closer;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
//~--- JDK imports ------------------------------------------------------------ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedInputStream; import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream; import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import javax.servlet.http.HttpServlet; //~--- JDK imports ------------------------------------------------------------
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** /**
* *
@@ -175,10 +167,8 @@ public class ProxyServlet extends HttpServlet
private void copyContent(HttpURLConnection con, HttpServletResponse response) private void copyContent(HttpURLConnection con, HttpServletResponse response)
throws IOException throws IOException
{ {
Closer closer = Closer.create();
try try (Closer closer = Closer.create()) {
{
InputStream webToProxyBuf = InputStream webToProxyBuf =
closer.register(new BufferedInputStream(con.getInputStream())); closer.register(new BufferedInputStream(con.getInputStream()));
OutputStream proxyToClientBuf = OutputStream proxyToClientBuf =
@@ -188,10 +178,6 @@ public class ProxyServlet extends HttpServlet
logger.trace("copied {} bytes for proxy", bytes); logger.trace("copied {} bytes for proxy", bytes);
} }
finally
{
closer.close();
}
} }
/** /**

View File

@@ -102,7 +102,7 @@ public class XmlMapStringAdapter
public Map<String, String> unmarshal(XmlMapStringElement[] elements) public Map<String, String> unmarshal(XmlMapStringElement[] elements)
throws Exception throws Exception
{ {
Map<String, String> map = new HashMap<String, String>(); Map<String, String> map = new HashMap<>();
if (elements != null) if (elements != null)
{ {

View File

@@ -90,7 +90,7 @@ public class XmlSetStringAdapter extends XmlAdapter<String, Set<String>>
@Override @Override
public Set<String> unmarshal(String rawString) throws Exception public Set<String> unmarshal(String rawString) throws Exception
{ {
Set<String> tokens = new HashSet<String>(); Set<String> tokens = new HashSet<>();
for (String token : rawString.split(",")) for (String token : rawString.split(","))
{ {

View File

@@ -63,7 +63,7 @@ public class IterableQueueTest
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void testDuplicatedEndReached() public void testDuplicatedEndReached()
{ {
IterableQueue<String> queue = new IterableQueue<String>(); IterableQueue<String> queue = new IterableQueue<>();
queue.endReached(); queue.endReached();
queue.endReached(); queue.endReached();
@@ -76,7 +76,7 @@ public class IterableQueueTest
@Test @Test
public void testIterator() public void testIterator()
{ {
IterableQueue<String> queue = new IterableQueue<String>(); IterableQueue<String> queue = new IterableQueue<>();
assertEquals(QueueIterator.class, queue.iterator().getClass()); assertEquals(QueueIterator.class, queue.iterator().getClass());
queue.endReached(); queue.endReached();
@@ -120,7 +120,7 @@ public class IterableQueueTest
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void testPushEndReached() public void testPushEndReached()
{ {
IterableQueue<String> queue = new IterableQueue<String>(); IterableQueue<String> queue = new IterableQueue<>();
queue.push("a"); queue.push("a");
queue.endReached(); queue.endReached();
@@ -134,7 +134,7 @@ public class IterableQueueTest
@Test @Test
public void testSingleConsumer() public void testSingleConsumer()
{ {
final IterableQueue<Integer> queue = new IterableQueue<Integer>(); final IterableQueue<Integer> queue = new IterableQueue<>();
new Thread(new IntegerProducer(queue, false, 100)).start(); new Thread(new IntegerProducer(queue, false, 100)).start();
assertResult(Lists.newArrayList(queue), 100); assertResult(Lists.newArrayList(queue), 100);
@@ -176,12 +176,12 @@ public class IterableQueueTest
ExecutorService executor = Executors.newFixedThreadPool(threads); ExecutorService executor = Executors.newFixedThreadPool(threads);
List<Future<List<Integer>>> futures = Lists.newArrayList(); List<Future<List<Integer>>> futures = Lists.newArrayList();
final IterableQueue<Integer> queue = new IterableQueue<Integer>(); final IterableQueue<Integer> queue = new IterableQueue<>();
for (int i = 0; i < consumer; i++) for (int i = 0; i < consumer; i++)
{ {
Future<List<Integer>> future = Future<List<Integer>> future =
executor.submit(new CallableQueueCollector<Integer>(queue)); executor.submit(new CallableQueueCollector<>(queue));
futures.add(future); futures.add(future);
} }

View File

@@ -71,8 +71,8 @@ public class ScmModuleTest
assertThat( assertThat(
module.getExtensions(), module.getExtensions(),
containsInAnyOrder( containsInAnyOrder(
(Class<?>) String.class, String.class,
(Class<?>) Integer.class Integer.class
) )
); );
assertThat( assertThat(
@@ -86,8 +86,8 @@ public class ScmModuleTest
assertThat( assertThat(
module.getEvents(), module.getEvents(),
containsInAnyOrder( containsInAnyOrder(
(Class<?>) String.class, String.class,
(Class<?>) Boolean.class Boolean.class
) )
); );
assertThat( assertThat(
@@ -100,15 +100,15 @@ public class ScmModuleTest
assertThat( assertThat(
module.getRestProviders(), module.getRestProviders(),
containsInAnyOrder( containsInAnyOrder(
(Class<?>) Integer.class, Integer.class,
(Class<?>) Long.class Long.class
) )
); );
assertThat( assertThat(
module.getRestResources(), module.getRestResources(),
containsInAnyOrder( containsInAnyOrder(
(Class<?>) Float.class, Float.class,
(Class<?>) Double.class Double.class
) )
); );
//J+ //J+

View File

@@ -128,7 +128,7 @@ public class TemplateEngineFactoryTest
assertTrue(engines.contains(engine1)); assertTrue(engines.contains(engine1));
assertTrue(engines.contains(engine2)); assertTrue(engines.contains(engine2));
Set<TemplateEngine> ce = new HashSet<TemplateEngine>(); Set<TemplateEngine> ce = new HashSet<>();
ce.add(engine1); ce.add(engine1);
factory = new TemplateEngineFactory(ce, engine2); factory = new TemplateEngineFactory(ce, engine2);

View File

@@ -56,7 +56,7 @@ public class UrlBuilderTest
UrlBuilder builder = new UrlBuilder("http://www.short.de"); UrlBuilder builder = new UrlBuilder("http://www.short.de");
builder.appendParameter("i", 123).appendParameter("s", "abc"); builder.appendParameter("i", 123).appendParameter("s", "abc");
builder.appendParameter("b", true).appendParameter("l", 321l); builder.appendParameter("b", true).appendParameter("l", 321L);
assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321", builder.toString()); assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321", builder.toString());
builder.appendParameter("c", "a b"); builder.appendParameter("c", "a b");
assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321&c=a%20b", builder.toString()); assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321&c=a%20b", builder.toString());

View File

@@ -199,7 +199,7 @@ public class XmlGroupDatabase implements XmlDatabase<Group>
/** Field description */ /** Field description */
@XmlJavaTypeAdapter(XmlGroupMapAdapter.class) @XmlJavaTypeAdapter(XmlGroupMapAdapter.class)
@XmlElement(name = "groups") @XmlElement(name = "groups")
private Map<String, Group> groupMap = new LinkedHashMap<String, Group>(); private Map<String, Group> groupMap = new LinkedHashMap<>();
/** Field description */ /** Field description */
private Long lastModified; private Long lastModified;

View File

@@ -72,7 +72,7 @@ public class XmlGroupList implements Iterable<Group>
*/ */
public XmlGroupList(Map<String, Group> groupMap) public XmlGroupList(Map<String, Group> groupMap)
{ {
this.groups = new LinkedList<Group>(groupMap.values()); this.groups = new LinkedList<>(groupMap.values());
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------

View File

@@ -81,7 +81,7 @@ public class XmlGroupMapAdapter
@Override @Override
public Map<String, Group> unmarshal(XmlGroupList groups) throws Exception public Map<String, Group> unmarshal(XmlGroupList groups) throws Exception
{ {
Map<String, Group> groupMap = new LinkedHashMap<String, Group>(); Map<String, Group> groupMap = new LinkedHashMap<>();
for (Group group : groups) for (Group group : groups)
{ {

View File

@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry; import sonia.scm.ContextEntry;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.store.CopyOnWrite;
import sonia.scm.store.StoreConstants; import sonia.scm.store.StoreConstants;
import sonia.scm.update.UpdateStepRepositoryMetadataAccess; import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
@@ -43,7 +44,10 @@ public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
try { try {
Marshaller marshaller = jaxbContext.createMarshaller(); Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(repository, resolveDataPath(path).toFile()); CopyOnWrite.withTemporaryFile(
temp -> marshaller.marshal(repository, temp.toFile()),
resolveDataPath(path)
);
} catch (JAXBException ex) { } catch (JAXBException ex) {
throw new InternalRepositoryException(repository, "failed write repository metadata", ex); throw new InternalRepositoryException(repository, "failed write repository metadata", ex);
} }

View File

@@ -4,6 +4,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry; import sonia.scm.ContextEntry;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.store.CopyOnWrite;
import sonia.scm.xml.IndentXMLStreamWriter; import sonia.scm.xml.IndentXMLStreamWriter;
import sonia.scm.xml.XmlStreams; import sonia.scm.xml.XmlStreams;
@@ -40,23 +41,28 @@ class PathDatabase {
ensureParentDirectoryExists(); ensureParentDirectoryExists();
LOG.trace("write repository path database to {}", storePath); LOG.trace("write repository path database to {}", storePath);
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(storePath)) { CopyOnWrite.withTemporaryFile(
writer.writeStartDocument(ENCODING, VERSION); temp -> {
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(temp)) {
writer.writeStartDocument(ENCODING, VERSION);
writeRepositoriesStart(writer, creationTime, lastModified); writeRepositoriesStart(writer, creationTime, lastModified);
for (Map.Entry<String, Path> e : pathDatabase.entrySet()) { for (Map.Entry<String, Path> e : pathDatabase.entrySet()) {
writeRepository(writer, e.getKey(), e.getValue()); writeRepository(writer, e.getKey(), e.getValue());
} }
writer.writeEndElement(); writer.writeEndElement();
writer.writeEndDocument(); writer.writeEndDocument();
} catch (XMLStreamException | IOException ex) { } catch (XMLStreamException | IOException ex) {
throw new InternalRepositoryException( throw new InternalRepositoryException(
ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(), ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(),
"failed to write repository path database", "failed to write repository path database",
ex ex
); );
} }
},
storePath
);
} }
private void ensureParentDirectoryExists() { private void ensureParentDirectoryExists() {

View File

@@ -0,0 +1,112 @@
package sonia.scm.store;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
public final class CopyOnWrite {
private static final Logger LOG = LoggerFactory.getLogger(CopyOnWrite.class);
private CopyOnWrite() {}
public static void withTemporaryFile(FileWriter writer, Path targetFile) {
validateInput(targetFile);
Path temporaryFile = createTemporaryFile(targetFile);
executeCallback(writer, targetFile, temporaryFile);
replaceOriginalFile(targetFile, temporaryFile);
}
@SuppressWarnings("squid:S3725") // performance of Files#isDirectory
private static void validateInput(Path targetFile) {
if (Files.isDirectory(targetFile)) {
throw new IllegalArgumentException("target file has to be a regular file, not a directory");
}
if (targetFile.getParent() == null) {
throw new IllegalArgumentException("target file has to be specified with a parent directory");
}
}
private static Path createTemporaryFile(Path targetFile) {
Path temporaryFile = targetFile.getParent().resolve(UUID.randomUUID().toString());
try {
Files.createFile(temporaryFile);
} catch (IOException ex) {
LOG.error("Error creating temporary file {} to replace file {}", temporaryFile, targetFile);
throw new StoreException("could not create temporary file", ex);
}
return temporaryFile;
}
private static void executeCallback(FileWriter writer, Path targetFile, Path temporaryFile) {
try {
writer.write(temporaryFile);
} catch (RuntimeException e) {
throw e;
} catch (Exception ex) {
LOG.error("Error writing to temporary file {}. Target file {} has not been modified", temporaryFile, targetFile);
throw new StoreException("could not write temporary file", ex);
}
}
private static void replaceOriginalFile(Path targetFile, Path temporaryFile) {
Path backupFile = backupOriginalFile(targetFile);
try {
Files.move(temporaryFile, targetFile);
} catch (IOException e) {
LOG.error("Error renaming temporary file {} to target file {}", temporaryFile, targetFile);
restoreBackup(targetFile, backupFile);
throw new StoreException("could rename temporary file to target file", e);
}
deleteBackupFile(backupFile);
}
@SuppressWarnings("squid:S3725") // performance of Files#exists
private static Path backupOriginalFile(Path targetFile) {
Path directory = targetFile.getParent();
if (Files.exists(targetFile)) {
Path backupFile = directory.resolve(UUID.randomUUID().toString());
try {
Files.move(targetFile, backupFile);
} catch (IOException e) {
LOG.error("Could not backup original file {}. Aborting here so that original file will not be overwritten.", targetFile);
throw new StoreException("could not create backup of file", e);
}
return backupFile;
} else {
return null;
}
}
private static void deleteBackupFile(Path backupFile) {
if (backupFile != null) {
try {
Files.delete(backupFile);
} catch (IOException e) {
LOG.warn("Could not delete backup file {}", backupFile);
throw new StoreException("could not delete backup file", e);
}
}
}
private static void restoreBackup(Path targetFile, Path backupFile) {
if (backupFile != null) {
try {
Files.move(backupFile, targetFile);
LOG.info("Recovered original file {} from backup", targetFile);
} catch (IOException e) {
LOG.error("Could not replace original file {} with backup file {} after failure", targetFile, backupFile);
}
}
}
@FunctionalInterface
public interface FileWriter {
@SuppressWarnings("squid:S00112") // We do not want to limit exceptions here
void write(Path t) throws Exception;
}
}

View File

@@ -317,48 +317,47 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
{ {
logger.debug("store configuration to {}", file); logger.debug("store configuration to {}", file);
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(file)) CopyOnWrite.withTemporaryFile(
{ temp -> {
writer.writeStartDocument(); try (IndentXMLStreamWriter writer = XmlStreams.createWriter(temp)) {
writer.writeStartDocument();
// configuration start // configuration start
writer.writeStartElement(TAG_CONFIGURATION); writer.writeStartElement(TAG_CONFIGURATION);
Marshaller m = context.createMarshaller(); Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
for (Entry<String, V> e : entries.entrySet()) for (Entry<String, V> e : entries.entrySet()) {
{
// entry start // entry start
writer.writeStartElement(TAG_ENTRY); writer.writeStartElement(TAG_ENTRY);
// key start // key start
writer.writeStartElement(TAG_KEY); writer.writeStartElement(TAG_KEY);
writer.writeCharacters(e.getKey()); writer.writeCharacters(e.getKey());
// key end // key end
writer.writeEndElement(); writer.writeEndElement();
// value // value
JAXBElement<V> je = new JAXBElement<V>(QName.valueOf(TAG_VALUE), type, JAXBElement<V> je = new JAXBElement<>(QName.valueOf(TAG_VALUE), type,
e.getValue()); e.getValue());
m.marshal(je, writer); m.marshal(je, writer);
// entry end // entry end
writer.writeEndElement(); writer.writeEndElement();
} }
// configuration end // configuration end
writer.writeEndElement(); writer.writeEndElement();
writer.writeEndDocument(); writer.writeEndDocument();
} }
catch (Exception ex) },
{ file.toPath()
throw new StoreException("could not store configuration", ex); );
}
} }
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------

View File

@@ -113,7 +113,10 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
Marshaller marshaller = context.createMarshaller(); Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(object, configFile); CopyOnWrite.withTemporaryFile(
temp -> marshaller.marshal(object, temp.toFile()),
configFile.toPath()
);
} }
catch (JAXBException ex) { catch (JAXBException ex) {
throw new StoreException("failed to marshall object", ex); throw new StoreException("failed to marshall object", ex);

View File

@@ -202,5 +202,5 @@ public class XmlUserDatabase implements XmlDatabase<User>
/** Field description */ /** Field description */
@XmlJavaTypeAdapter(XmlUserMapAdapter.class) @XmlJavaTypeAdapter(XmlUserMapAdapter.class)
@XmlElement(name = "users") @XmlElement(name = "users")
private Map<String, User> userMap = new LinkedHashMap<String, User>(); private Map<String, User> userMap = new LinkedHashMap<>();
} }

View File

@@ -72,7 +72,7 @@ public class XmlUserList implements Iterable<User>
*/ */
public XmlUserList(Map<String, User> userMap) public XmlUserList(Map<String, User> userMap)
{ {
this.users = new LinkedList<User>(userMap.values()); this.users = new LinkedList<>(userMap.values());
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------

View File

@@ -81,7 +81,7 @@ public class XmlUserMapAdapter
@Override @Override
public Map<String, User> unmarshal(XmlUserList users) throws Exception public Map<String, User> unmarshal(XmlUserList users) throws Exception
{ {
Map<String, User> userMap = new LinkedHashMap<String, User>(); Map<String, User> userMap = new LinkedHashMap<>();
for (User user : users) for (User user : users)
{ {

View File

@@ -52,7 +52,9 @@ public final class XmlStreams {
} }
private static XMLStreamReader createReader(Reader reader) throws XMLStreamException { private static XMLStreamReader createReader(Reader reader) throws XMLStreamException {
return XMLInputFactory.newInstance().createXMLStreamReader(reader); XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
return factory.createXMLStreamReader(reader);
} }

View File

@@ -0,0 +1,114 @@
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 java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
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) {
Path expectedFile = tempDir.resolve("toBeCreated.txt");
withTemporaryFile(
file -> new FileOutputStream(file.toFile()).write("great success".getBytes()),
expectedFile);
Assertions.assertThat(expectedFile).hasContent("great success");
}
@Test
void shouldOverwriteExistingFile(@TempDirectory.TempDir Path tempDir) throws IOException {
Path expectedFile = tempDir.resolve("toBeOverwritten.txt");
Files.createFile(expectedFile);
withTemporaryFile(
file -> new FileOutputStream(file.toFile()).write("great success".getBytes()),
expectedFile);
Assertions.assertThat(expectedFile).hasContent("great success");
}
@Test
void shouldFailForDirectory(@TempDirectory.TempDir Path tempDir) {
assertThrows(IllegalArgumentException.class,
() -> withTemporaryFile(
file -> new FileOutputStream(file.toFile()).write("should not be written".getBytes()),
tempDir));
}
@Test
void shouldFailForMissingDirectory() {
assertThrows(
IllegalArgumentException.class,
() -> withTemporaryFile(
file -> new FileOutputStream(file.toFile()).write("should not be written".getBytes()),
Paths.get("someFile")));
}
@Test
void shouldKeepBackupIfTemporaryFileCouldNotBeWritten(@TempDirectory.TempDir Path tempDir) throws IOException {
Path unchangedOriginalFile = tempDir.resolve("notToBeDeleted.txt");
new FileOutputStream(unchangedOriginalFile.toFile()).write("this should be kept".getBytes());
assertThrows(
StoreException.class,
() -> withTemporaryFile(
file -> {
throw new IOException("test");
},
unchangedOriginalFile));
Assertions.assertThat(unchangedOriginalFile).hasContent("this should be kept");
}
@Test
void shouldNotWrapRuntimeExceptions(@TempDirectory.TempDir Path tempDir) throws IOException {
Path someFile = tempDir.resolve("something.txt");
assertThrows(
NullPointerException.class,
() -> withTemporaryFile(
file -> {
throw new NullPointerException("test");
},
someFile));
}
@Test
void shouldKeepBackupIfTemporaryFileIsMissing(@TempDirectory.TempDir Path tempDir) throws IOException {
Path backedUpFile = tempDir.resolve("notToBeDeleted.txt");
new FileOutputStream(backedUpFile.toFile()).write("this should be kept".getBytes());
assertThrows(
StoreException.class,
() -> withTemporaryFile(
Files::delete,
backedUpFile));
Assertions.assertThat(backedUpFile).hasContent("this should be kept");
}
@Test
void shouldDeleteExistingFile(@TempDirectory.TempDir Path tempDir) throws IOException {
Path expectedFile = tempDir.resolve("toBeReplaced.txt");
new FileOutputStream(expectedFile.toFile()).write("this should be removed".getBytes());
withTemporaryFile(
file -> new FileOutputStream(file.toFile()).write("overwritten".getBytes()),
expectedFile);
Assertions.assertThat(Files.list(tempDir)).hasSize(1);
}
}

View File

@@ -72,7 +72,13 @@
<dependency> <dependency>
<groupId>org.jboss.resteasy</groupId> <groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId> <artifactId>resteasy-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-core-spi</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>

View File

@@ -70,6 +70,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import static java.util.Optional.empty;
import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound; import static sonia.scm.NotFoundException.notFound;
@@ -209,7 +210,7 @@ public class GitBrowseCommand extends AbstractGitCommand
logger.trace("fetch last commit for {} at {}", path, revId.getName()); logger.trace("fetch last commit for {} at {}", path, revId.getName());
RevCommit commit = getLatestCommit(repo, revId, path); RevCommit commit = getLatestCommit(repo, revId, path);
Optional<LfsPointer> lfsPointer = GitUtil.getLfsPointer(repo, path, commit, treeWalk); Optional<LfsPointer> lfsPointer = commit == null? empty(): GitUtil.getLfsPointer(repo, path, commit, treeWalk);
if (lfsPointer.isPresent()) { if (lfsPointer.isPresent()) {
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);

View File

@@ -14,6 +14,12 @@ const Switcher = styled(ButtonAddons)`
right: 0; right: 0;
`; `;
const SmallButton = styled(Button).attrs(props => ({
className: "is-small"
}))`
height: inherit;
`;
type Props = { type Props = {
repository: Repository; repository: Repository;
}; };
@@ -62,9 +68,9 @@ export default class ProtocolInformation extends React.Component<Props, State> {
} }
return ( return (
<Button color={color} action={() => this.selectProtocol(protocol)}> <SmallButton color={color} action={() => this.selectProtocol(protocol)}>
{name.toUpperCase()} {name.toUpperCase()}
</Button> </SmallButton>
); );
}; };

View File

@@ -1,7 +1,7 @@
import React, { FormEvent } from "react"; import React, { FormEvent } from "react";
import { WithTranslation, withTranslation } from "react-i18next"; import { WithTranslation, withTranslation } from "react-i18next";
import { Branch, Repository, Link } from "@scm-manager/ui-types"; import { Branch, Repository, Link } from "@scm-manager/ui-types";
import { apiClient, BranchSelector, ErrorPage, Loading, Subtitle, SubmitButton } from "@scm-manager/ui-components"; import { apiClient, BranchSelector, ErrorPage, Loading, Subtitle, Level, SubmitButton } from "@scm-manager/ui-components";
type Props = WithTranslation & { type Props = WithTranslation & {
repository: Repository; repository: Repository;
@@ -143,10 +143,14 @@ class RepositoryConfig extends React.Component<Props, State> {
} }
const submitButton = disabled ? null : ( const submitButton = disabled ? null : (
<SubmitButton <Level
label={t("scm-git-plugin.repo-config.submit")} right={
loading={submitPending} <SubmitButton
disabled={!this.state.selectedBranchName} label={t("scm-git-plugin.repo-config.submit")}
loading={submitPending}
disabled={!this.state.selectedBranchName}
/>
}
/> />
); );

View File

@@ -2,14 +2,11 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Answers; import org.mockito.Answers;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@@ -27,6 +24,7 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.web.GitVndMediaType; import sonia.scm.web.GitVndMediaType;
import sonia.scm.web.RestDispatcher;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@@ -52,10 +50,7 @@ public class GitConfigResourceTest {
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
@Rule private RestDispatcher dispatcher = new RestDispatcher();
public ExpectedException thrown = ExpectedException.none();
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
private final URI baseUri = URI.create("/"); private final URI baseUri = URI.create("/");
@@ -89,7 +84,7 @@ public class GitConfigResourceTest {
when(repositoryHandler.getConfig()).thenReturn(gitConfig); when(repositoryHandler.getConfig()).thenReturn(gitConfig);
GitRepositoryConfigResource gitRepositoryConfigResource = new GitRepositoryConfigResource(repositoryConfigMapper, repositoryManager, new GitRepositoryConfigStoreProvider(configurationStoreFactory)); GitRepositoryConfigResource gitRepositoryConfigResource = new GitRepositoryConfigResource(repositoryConfigMapper, repositoryManager, new GitRepositoryConfigStoreProvider(configurationStoreFactory));
GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, of(gitRepositoryConfigResource)); GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, of(gitRepositoryConfigResource));
dispatcher.getRegistry().addSingletonResource(gitConfigResource); dispatcher.addSingletonResource(gitConfigResource);
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
} }
@@ -137,10 +132,11 @@ public class GitConfigResourceTest {
@Test @Test
@SubjectAware(username = "writeOnly") @SubjectAware(username = "writeOnly")
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException { public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
thrown.expectMessage("Subject does not have permission [configuration:read:git]"); MockHttpResponse response = get();
get(); assertEquals("Subject does not have permission [configuration:read:git]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
@Test @Test
@@ -152,10 +148,11 @@ public class GitConfigResourceTest {
@Test @Test
@SubjectAware(username = "readOnly") @SubjectAware(username = "readOnly")
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException { public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
thrown.expectMessage("Subject does not have permission [configuration:write:git]"); MockHttpResponse response = put();
put(); assertEquals("Subject does not have permission [configuration:write:git]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
@Test @Test

View File

@@ -145,7 +145,7 @@ public class HgPackageReader
*/ */
private void filterPackage(HgPackages packages) private void filterPackage(HgPackages packages)
{ {
List<HgPackage> pkgList = new ArrayList<HgPackage>(); List<HgPackage> pkgList = new ArrayList<>();
for (HgPackage pkg : packages) for (HgPackage pkg : packages)
{ {
@@ -228,7 +228,7 @@ public class HgPackageReader
if (packages == null) if (packages == null)
{ {
packages = new HgPackages(); packages = new HgPackages();
packages.setPackages(new ArrayList<HgPackage>()); packages.setPackages(new ArrayList<>());
} }
return packages; return packages;

View File

@@ -2,14 +2,11 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
@@ -18,12 +15,15 @@ import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.web.HgVndMediaType; import sonia.scm.web.HgVndMediaType;
import sonia.scm.web.RestDispatcher;
import javax.inject.Provider; import javax.inject.Provider;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -37,10 +37,7 @@ public class HgConfigAutoConfigurationResourceTest {
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
@Rule private RestDispatcher dispatcher = new RestDispatcher();
public ExpectedException thrown = ExpectedException.none();
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
@InjectMocks @InjectMocks
private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper; private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper;
@@ -57,7 +54,7 @@ public class HgConfigAutoConfigurationResourceTest {
new HgConfigAutoConfigurationResource(dtoToConfigMapper, repositoryHandler); new HgConfigAutoConfigurationResource(dtoToConfigMapper, repositoryHandler);
when(resourceProvider.get()).thenReturn(resource); when(resourceProvider.get()).thenReturn(resource);
dispatcher.getRegistry().addSingletonResource( dispatcher.addSingletonResource(
new HgConfigResource(null, null, null, null, new HgConfigResource(null, null, null, null,
resourceProvider, null)); resourceProvider, null));
} }
@@ -76,9 +73,10 @@ public class HgConfigAutoConfigurationResourceTest {
@Test @Test
@SubjectAware(username = "readOnly") @SubjectAware(username = "readOnly")
public void shouldNotSetDefaultConfigAndInstallHgWhenNotAuthorized() throws Exception { public void shouldNotSetDefaultConfigAndInstallHgWhenNotAuthorized() throws Exception {
thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); MockHttpResponse response = put(null);
put(null); assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
@Test @Test
@@ -95,9 +93,10 @@ public class HgConfigAutoConfigurationResourceTest {
@Test @Test
@SubjectAware(username = "readOnly") @SubjectAware(username = "readOnly")
public void shouldNotUpdateConfigAndInstallHgWhenNotAuthorized() throws Exception { public void shouldNotUpdateConfigAndInstallHgWhenNotAuthorized() throws Exception {
thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); MockHttpResponse response = put("{\"disabled\":true}");
put("{\"disabled\":true}"); assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
private MockHttpResponse put(String content) throws URISyntaxException { private MockHttpResponse put(String content) throws URISyntaxException {

View File

@@ -2,19 +2,17 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Answers; import org.mockito.Answers;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.web.RestDispatcher;
import javax.inject.Provider; import javax.inject.Provider;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -35,10 +33,7 @@ public class HgConfigInstallationsResourceTest {
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
@Rule private RestDispatcher dispatcher = new RestDispatcher();
public ExpectedException thrown = ExpectedException.none();
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
private final URI baseUri = URI.create("/"); private final URI baseUri = URI.create("/");
@@ -57,7 +52,7 @@ public class HgConfigInstallationsResourceTest {
HgConfigInstallationsResource resource = new HgConfigInstallationsResource(mapper); HgConfigInstallationsResource resource = new HgConfigInstallationsResource(mapper);
when(resourceProvider.get()).thenReturn(resource); when(resourceProvider.get()).thenReturn(resource);
dispatcher.getRegistry().addSingletonResource( dispatcher.addSingletonResource(
new HgConfigResource(null, null, null, null, new HgConfigResource(null, null, null, null,
null, resourceProvider)); null, resourceProvider));
@@ -82,9 +77,10 @@ public class HgConfigInstallationsResourceTest {
@Test @Test
@SubjectAware(username = "writeOnly") @SubjectAware(username = "writeOnly")
public void shouldNotGetHgInstallationsWhenNotAuthorized() throws Exception { public void shouldNotGetHgInstallationsWhenNotAuthorized() throws Exception {
thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); MockHttpResponse response = get("hg");
get("hg"); assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
@Test @Test
@@ -104,9 +100,10 @@ public class HgConfigInstallationsResourceTest {
@Test @Test
@SubjectAware(username = "writeOnly") @SubjectAware(username = "writeOnly")
public void shouldNotGetPythonInstallationsWhenNotAuthorized() throws Exception { public void shouldNotGetPythonInstallationsWhenNotAuthorized() throws Exception {
thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); MockHttpResponse response = get("python");
get("python"); assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
private MockHttpResponse get(String path) throws URISyntaxException { private MockHttpResponse get(String path) throws URISyntaxException {

View File

@@ -5,14 +5,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Answers; import org.mockito.Answers;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
@@ -23,6 +20,7 @@ import sonia.scm.installer.HgPackageReader;
import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.web.RestDispatcher;
import javax.inject.Provider; import javax.inject.Provider;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -49,10 +47,7 @@ public class HgConfigPackageResourceTest {
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
@Rule private RestDispatcher dispatcher = new RestDispatcher();
public ExpectedException thrown = ExpectedException.none();
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
private final URI baseUri = java.net.URI.create("/"); private final URI baseUri = java.net.URI.create("/");
@@ -113,9 +108,10 @@ public class HgConfigPackageResourceTest {
@Test @Test
@SubjectAware(username = "writeOnly") @SubjectAware(username = "writeOnly")
public void shouldNotGetPackagesWhenNotAuthorized() throws Exception { public void shouldNotGetPackagesWhenNotAuthorized() throws Exception {
thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); MockHttpResponse response = get();
get(); assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
@Test @Test
@@ -158,9 +154,10 @@ public class HgConfigPackageResourceTest {
@Test @Test
@SubjectAware(username = "readOnly") @SubjectAware(username = "readOnly")
public void shouldNotInstallPackageWhenNotAuthorized() throws Exception { public void shouldNotInstallPackageWhenNotAuthorized() throws Exception {
thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); MockHttpResponse response = put("don-t-care");
put("don-t-care"); assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
private List<HgPackage> createPackages() { private List<HgPackage> createPackages() {
@@ -191,7 +188,7 @@ public class HgConfigPackageResourceTest {
new HgConfigPackageResource(hgPackageReader, advancedHttpClient, repositoryHandler, mapper); new HgConfigPackageResource(hgPackageReader, advancedHttpClient, repositoryHandler, mapper);
when(hgConfigPackageResourceProvider.get()).thenReturn(hgConfigPackageResource); when(hgConfigPackageResourceProvider.get()).thenReturn(hgConfigPackageResource);
dispatcher.getRegistry().addSingletonResource( dispatcher.addSingletonResource(
new HgConfigResource(null, null, null, new HgConfigResource(null, null, null,
hgConfigPackageResourceProvider, null, null)); hgConfigPackageResourceProvider, null, null));
} }

View File

@@ -4,14 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Answers; import org.mockito.Answers;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
@@ -20,6 +17,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.web.HgVndMediaType; import sonia.scm.web.HgVndMediaType;
import sonia.scm.web.RestDispatcher;
import javax.inject.Provider; import javax.inject.Provider;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -43,10 +41,7 @@ public class HgConfigResourceTest {
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
@Rule private RestDispatcher dispatcher = new RestDispatcher();
public ExpectedException thrown = ExpectedException.none();
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
private final URI baseUri = URI.create("/"); private final URI baseUri = URI.create("/");
@@ -78,7 +73,7 @@ public class HgConfigResourceTest {
HgConfigResource gitConfigResource = HgConfigResource gitConfigResource =
new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, packagesResource, new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, packagesResource,
autoconfigResource, installationsResource); autoconfigResource, installationsResource);
dispatcher.getRegistry().addSingletonResource(gitConfigResource); dispatcher.addSingletonResource(gitConfigResource);
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
} }
@@ -120,10 +115,11 @@ public class HgConfigResourceTest {
@Test @Test
@SubjectAware(username = "writeOnly") @SubjectAware(username = "writeOnly")
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException { public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); MockHttpResponse response = get();
get(); assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
@Test @Test
@@ -135,10 +131,11 @@ public class HgConfigResourceTest {
@Test @Test
@SubjectAware(username = "readOnly") @SubjectAware(username = "readOnly")
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException { public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); MockHttpResponse response = put();
put(); assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
private MockHttpResponse get() throws URISyntaxException { private MockHttpResponse get() throws URISyntaxException {

View File

@@ -211,15 +211,7 @@ public class SVNKitLogger extends SVNDebugLogAdapter
*/ */
private Logger getLogger(SVNLogType type) private Logger getLogger(SVNLogType type)
{ {
Logger logger = loggerMap.get(type); return loggerMap.computeIfAbsent(type, t -> LoggerFactory.getLogger(parseName(t.getName())));
if (logger == null)
{
logger = LoggerFactory.getLogger(parseName(type.getName()));
loggerMap.put(type, logger);
}
return logger;
} }
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------

View File

@@ -4,14 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Answers; import org.mockito.Answers;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
@@ -19,6 +16,7 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.SvnConfig; import sonia.scm.repository.SvnConfig;
import sonia.scm.repository.SvnRepositoryHandler; import sonia.scm.repository.SvnRepositoryHandler;
import sonia.scm.web.RestDispatcher;
import sonia.scm.web.SvnVndMediaType; import sonia.scm.web.SvnVndMediaType;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -42,10 +40,7 @@ public class SvnConfigResourceTest {
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
@Rule private RestDispatcher dispatcher = new RestDispatcher();
public ExpectedException thrown = ExpectedException.none();
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
private final URI baseUri = URI.create("/"); private final URI baseUri = URI.create("/");
@@ -66,7 +61,7 @@ public class SvnConfigResourceTest {
SvnConfig gitConfig = createConfiguration(); SvnConfig gitConfig = createConfiguration();
when(repositoryHandler.getConfig()).thenReturn(gitConfig); when(repositoryHandler.getConfig()).thenReturn(gitConfig);
SvnConfigResource gitConfigResource = new SvnConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler); SvnConfigResource gitConfigResource = new SvnConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler);
dispatcher.getRegistry().addSingletonResource(gitConfigResource); dispatcher.addSingletonResource(gitConfigResource);
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
} }
@@ -108,10 +103,11 @@ public class SvnConfigResourceTest {
@Test @Test
@SubjectAware(username = "writeOnly") @SubjectAware(username = "writeOnly")
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException { public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
thrown.expectMessage("Subject does not have permission [configuration:read:svn]"); MockHttpResponse response = get();
get(); assertEquals("Subject does not have permission [configuration:read:svn]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
@Test @Test
@@ -123,10 +119,11 @@ public class SvnConfigResourceTest {
@Test @Test
@SubjectAware(username = "readOnly") @SubjectAware(username = "readOnly")
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException { public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
thrown.expectMessage("Subject does not have permission [configuration:write:svn]"); MockHttpResponse response = put();
put(); assertEquals("Subject does not have permission [configuration:write:svn]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
} }
private MockHttpResponse get() throws URISyntaxException { private MockHttpResponse get() throws URISyntaxException {

View File

@@ -78,6 +78,7 @@
<systemProperties> <systemProperties>
<arg>java.awt.headless=true</arg> <arg>java.awt.headless=true</arg>
<arg>logback.configurationFile=logging.xml</arg> <arg>logback.configurationFile=logging.xml</arg>
<arg>ClassLoaderLeakPreventor.threadWaitMs=100</arg>
</systemProperties> </systemProperties>
</jvmSettings> </jvmSettings>

View File

@@ -42,6 +42,20 @@
<version>${mockito.version}</version> <version>${mockito.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-core-spi</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-core</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId> <artifactId>slf4j-simple</artifactId>

View File

@@ -81,15 +81,7 @@ public class MapCacheManager
@Override @Override
public synchronized <K, V> MapCache<K, V> getCache(String name) public synchronized <K, V> MapCache<K, V> getCache(String name)
{ {
MapCache<K, V> cache = cacheMap.get(name); return (MapCache<K, V>) cacheMap.computeIfAbsent(name, k -> new MapCache<K, V>());
if (cache == null)
{
cache = new MapCache<K, V>();
cacheMap.put(name, cache);
}
return cache;
} }
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------

View File

@@ -0,0 +1,111 @@
package sonia.scm.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.spi.Dispatcher;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
import sonia.scm.BadRequestException;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.ScmConstraintViolationException;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import java.util.HashMap;
import java.util.Map;
public class RestDispatcher {
private static final Logger LOG = LoggerFactory.getLogger(RestDispatcher.class);
private final Dispatcher dispatcher;
private final EnhanceableExceptionMapper exceptionMapper;
public RestDispatcher() {
dispatcher = MockDispatcherFactory.createDispatcher();
exceptionMapper = new EnhanceableExceptionMapper();
dispatcher.getProviderFactory().register(exceptionMapper);
dispatcher.getProviderFactory().registerProviderInstance(new JacksonProducer());
}
public void addSingletonResource(Object resource) {
dispatcher.getRegistry().addSingletonResource(resource);
}
public void invoke(HttpRequest in, HttpResponse response) {
dispatcher.invoke(in, response);
}
public void registerException(Class<? extends RuntimeException> exceptionClass, Status status) {
exceptionMapper.registerException(exceptionClass, status);
}
public <T> void putDefaultContextObject(Class<T> clazz, T object) {
dispatcher.getDefaultContextObjects().put(clazz, object);
}
private static class EnhanceableExceptionMapper implements ExceptionMapper<Exception> {
private final Map<Class<? extends RuntimeException>, Integer> statusCodes = new HashMap<>();
public EnhanceableExceptionMapper() {
registerException(NotFoundException.class, Status.NOT_FOUND);
registerException(AlreadyExistsException.class, Status.CONFLICT);
registerException(ConcurrentModificationException.class, Status.CONFLICT);
registerException(UnauthorizedException.class, Status.FORBIDDEN);
registerException(AuthorizationException.class, Status.FORBIDDEN);
registerException(BadRequestException.class, Status.BAD_REQUEST);
registerException(ScmConstraintViolationException.class, Status.BAD_REQUEST);
}
private void registerException(Class<? extends RuntimeException> exceptionClass, Status status) {
statusCodes.put(exceptionClass, status.getStatusCode());
}
@Override
public Response toResponse(Exception e) {
return Response.status(getStatus(e)).entity(e.getMessage()).build();
}
private Integer getStatus(Exception ex) {
return statusCodes
.entrySet()
.stream()
.filter(e -> e.getKey().isAssignableFrom(ex.getClass()))
.map(Map.Entry::getValue)
.findAny()
.orElse(handleUnknownException(ex));
}
private Integer handleUnknownException(Exception ex) {
LOG.info("got unknown exception in rest api test", ex);
return 500;
}
}
@Provider
@Produces("application/*+json")
public static class JacksonProducer implements ContextResolver<ObjectMapper> {
public JacksonProducer() {
this.json
= new ObjectMapper().findAndRegisterModules();
}
@Override
public ObjectMapper getContext(Class<?> objectType) {
return json;
}
private final ObjectMapper json;
}
}

View File

@@ -17,6 +17,8 @@
<name>scm-ui</name> <name>scm-ui</name>
<properties> <properties>
<build.script>build</build.script>
<skipTests>false</skipTests>
<sonar.language>typescript</sonar.language> <sonar.language>typescript</sonar.language>
<sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources> <sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources>
<sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions> <sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions>
@@ -68,7 +70,7 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<script>build</script> <script>${build.script}</script>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@@ -118,4 +120,21 @@
</plugins> </plugins>
</build> </build>
<profiles>
<profile>
<id>dev</id>
<activation>
<property>
<name>development</name>
</property>
</activation>
<properties>
<build.script>build:dev</build.script>
</properties>
</profile>
</profiles>
</project> </project>

View File

@@ -7,7 +7,7 @@
"private": false, "private": false,
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"prettier": "^1.18.2" "prettier": "^1.19.1"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -7,7 +7,7 @@
"private": false, "private": false,
"main": "tsconfig.json", "main": "tsconfig.json",
"dependencies": { "dependencies": {
"typescript": "^3.6.4" "typescript": "^3.7.2"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -42,7 +42,7 @@
"raf": "^3.4.0", "raf": "^3.4.0",
"react-test-renderer": "^16.10.2", "react-test-renderer": "^16.10.2",
"storybook-addon-i18next": "^1.2.1", "storybook-addon-i18next": "^1.2.1",
"typescript": "^3.6.4" "typescript": "^3.7.2"
}, },
"dependencies": { "dependencies": {
"@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT", "@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT",

View File

@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import classNames from "classnames";
import { Async, AsyncCreatable } from "react-select"; import { Async, AsyncCreatable } from "react-select";
import { SelectValue } from "@scm-manager/ui-types"; import { SelectValue } from "@scm-manager/ui-types";
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon"; import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
@@ -7,13 +8,14 @@ import { ActionMeta, ValueType } from "react-select/lib/types";
type Props = { type Props = {
loadSuggestions: (p: string) => Promise<SelectValue[]>; loadSuggestions: (p: string) => Promise<SelectValue[]>;
valueSelected: (p: SelectValue) => void; valueSelected: (p: SelectValue) => void;
label: string; label?: string;
helpText?: string; helpText?: string;
value?: SelectValue; value?: SelectValue;
placeholder: string; placeholder: string;
loadingMessage: string; loadingMessage: string;
noOptionsMessage: string; noOptionsMessage: string;
creatable?: boolean; creatable?: boolean;
className?: string;
}; };
type State = {}; type State = {};
@@ -53,10 +55,11 @@ class Autocomplete extends React.Component<Props, State> {
loadingMessage, loadingMessage,
noOptionsMessage, noOptionsMessage,
loadSuggestions, loadSuggestions,
creatable creatable,
className
} = this.props; } = this.props;
return ( return (
<div className="field"> <div className={classNames("field", className)}>
<LabelWithHelpIcon label={label} helpText={helpText} /> <LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control"> <div className="control">
{creatable ? ( {creatable ? (

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { SelectValue, AutocompleteObject } from "@scm-manager/ui-types"; import { SelectValue, AutocompleteObject } from "@scm-manager/ui-types";
import Autocomplete from "./Autocomplete"; import Autocomplete from "./Autocomplete";
import { apiClient } from "./apiclient";
export type AutocompleteProps = { export type AutocompleteProps = {
autocompleteLink?: string; autocompleteLink?: string;
@@ -19,7 +20,8 @@ export default class UserGroupAutocomplete extends React.Component<Props> {
loadSuggestions = (inputValue: string): Promise<SelectValue[]> => { loadSuggestions = (inputValue: string): Promise<SelectValue[]> => {
const url = this.props.autocompleteLink; const url = this.props.autocompleteLink;
const link = url + "?q="; const link = url + "?q=";
return fetch(link + inputValue) return apiClient
.get(link + inputValue)
.then(response => response.json()) .then(response => response.json())
.then((json: AutocompleteObject[]) => { .then((json: AutocompleteObject[]) => {
return json.map(element => { return json.map(element => {

View File

@@ -336,7 +336,7 @@ exports[`Storyshots DateFromNow Default 1`] = `
exports[`Storyshots Forms|Checkbox Default 1`] = ` exports[`Storyshots Forms|Checkbox Default 1`] = `
<div <div
className="sc-gipzik xsalO" className="sc-fBuWsC ldmpJA"
> >
<div <div
className="field" className="field"
@@ -381,7 +381,7 @@ exports[`Storyshots Forms|Checkbox Default 1`] = `
exports[`Storyshots Forms|Checkbox Disabled 1`] = ` exports[`Storyshots Forms|Checkbox Disabled 1`] = `
<div <div
className="sc-gipzik xsalO" className="sc-fBuWsC ldmpJA"
> >
<div <div
className="field" className="field"
@@ -409,7 +409,7 @@ exports[`Storyshots Forms|Checkbox Disabled 1`] = `
exports[`Storyshots Forms|Radio Default 1`] = ` exports[`Storyshots Forms|Radio Default 1`] = `
<div <div
className="sc-csuQGl fFFkRK" className="sc-fMiknA keSQNk"
> >
<label <label
className="radio" className="radio"
@@ -438,7 +438,7 @@ exports[`Storyshots Forms|Radio Default 1`] = `
exports[`Storyshots Forms|Radio Disabled 1`] = ` exports[`Storyshots Forms|Radio Disabled 1`] = `
<div <div
className="sc-csuQGl fFFkRK" className="sc-fMiknA keSQNk"
> >
<label <label
className="radio" className="radio"
@@ -456,6 +456,83 @@ exports[`Storyshots Forms|Radio Disabled 1`] = `
</div> </div>
`; `;
exports[`Storyshots Forms|Textarea OnCancel 1`] = `
<div
className="sc-dVhcbM ituOZx"
>
<div
className="field"
>
<div
className="control"
>
<textarea
className="textarea"
disabled={false}
onChange={[Function]}
onKeyDown={[Function]}
value="Use the escape key to clear the textarea"
/>
</div>
</div>
</div>
`;
exports[`Storyshots Forms|Textarea OnChange 1`] = `
<div
className="sc-dVhcbM ituOZx"
>
<div
className="field"
>
<div
className="control"
>
<textarea
className="textarea"
disabled={false}
onChange={[Function]}
onKeyDown={[Function]}
value="Start typing"
/>
</div>
</div>
<hr />
<p>
Start typing
</p>
</div>
`;
exports[`Storyshots Forms|Textarea OnSubmit 1`] = `
<div
className="sc-dVhcbM ituOZx"
>
<div
className="field"
>
<div
className="control"
>
<textarea
className="textarea"
disabled={false}
onChange={[Function]}
onKeyDown={[Function]}
value="Use the ctrl/command + Enter to submit the textarea"
/>
</div>
</div>
<hr />
<p>
</p>
</div>
`;
exports[`Storyshots Loading Default 1`] = ` exports[`Storyshots Loading Default 1`] = `
<div> <div>
<div <div
@@ -2311,3 +2388,173 @@ PORT_NUMBER =
</pre> </pre>
</div> </div>
`; `;
exports[`Storyshots Table|Table Default 1`] = `
<table
className="sc-jhAzac hmXDXQ table content is-hoverable"
>
<thead>
<tr>
<th
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
First Name
</th>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Last Name
<i
className="fas fa-sort-amount-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
<th
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
E-Mail
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<h4>
Tricia
</h4>
</td>
<td>
<b
style={
Object {
"color": "red",
}
}
>
McMillan
</b>
</td>
<td>
<a>
tricia@hitchhiker.com
</a>
</td>
</tr>
<tr>
<td>
<h4>
Arthur
</h4>
</td>
<td>
<b
style={
Object {
"color": "red",
}
}
>
Dent
</b>
</td>
<td>
<a>
arthur@hitchhiker.com
</a>
</td>
</tr>
</tbody>
</table>
`;
exports[`Storyshots Table|Table Empty 1`] = `
<div
className="notification is-info"
>
No data found.
</div>
`;
exports[`Storyshots Table|Table TextColumn 1`] = `
<table
className="sc-jhAzac hmXDXQ table content is-hoverable"
>
<thead>
<tr>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Id
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Name
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Description
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
21
</td>
<td>
Pommes
</td>
<td>
Fried potato sticks
</td>
</tr>
<tr>
<td>
42
</td>
<td>
Quarter-Pounder
</td>
<td>
Big burger
</td>
</tr>
<tr>
<td>
-84
</td>
<td>
Icecream
</td>
<td>
Cold dessert
</td>
</tr>
</tbody>
</table>
`;

View File

@@ -27,13 +27,17 @@ const extractXsrfToken = () => {
}; };
const applyFetchOptions: (p: RequestInit) => RequestInit = o => { const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
const headers: { [key: string]: string } = { if (!o.headers) {
Cache: "no-cache", o.headers = {};
// identify the request as ajax request }
"X-Requested-With": "XMLHttpRequest",
// identify the web interface // @ts-ignore We are sure that here we only get headers of type Record<string, string>
"X-SCM-Client": "WUI" const headers: Record<string, string> = o.headers;
}; headers["Cache"] = "no-cache";
// identify the request as ajax request
headers["X-Requested-With"] = "XMLHttpRequest";
// identify the web interface
headers["X-SCM-Client"] = "WUI";
const xsrf = extractXsrfToken(); const xsrf = extractXsrfToken();
if (xsrf) { if (xsrf) {
@@ -80,11 +84,19 @@ class ApiClient {
return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure); return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
} }
post(url: string, payload?: any, contentType = "application/json", additionalHeaders = new Headers()) { post(url: string, payload?: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithJSONBody("POST", url, contentType, additionalHeaders, payload); return this.httpRequestWithJSONBody("POST", url, contentType, additionalHeaders, payload);
} }
postBinary(url: string, fileAppender: (p: FormData) => void, additionalHeaders = new Headers()) { postText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithTextBody("POST", url, additionalHeaders, payload);
}
putText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithTextBody("PUT", url, additionalHeaders, payload);
}
postBinary(url: string, fileAppender: (p: FormData) => void, additionalHeaders: Record<string, string> = {}) {
const formData = new FormData(); const formData = new FormData();
fileAppender(formData); fileAppender(formData);
@@ -96,7 +108,7 @@ class ApiClient {
return this.httpRequestWithBinaryBody(options, url); return this.httpRequestWithBinaryBody(options, url);
} }
put(url: string, payload: any, contentType = "application/json", additionalHeaders = new Headers()) { put(url: string, payload: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithJSONBody("PUT", url, contentType, additionalHeaders, payload); return this.httpRequestWithJSONBody("PUT", url, contentType, additionalHeaders, payload);
} }
@@ -120,7 +132,7 @@ class ApiClient {
method: string, method: string,
url: string, url: string,
contentType: string, contentType: string,
additionalHeaders: Headers, additionalHeaders: Record<string, string>,
payload?: any payload?: any
): Promise<Response> { ): Promise<Response> {
const options: RequestInit = { const options: RequestInit = {
@@ -133,13 +145,27 @@ class ApiClient {
return this.httpRequestWithBinaryBody(options, url, contentType); return this.httpRequestWithBinaryBody(options, url, contentType);
} }
httpRequestWithTextBody(
method: string,
url: string,
additionalHeaders: Record<string, string> = {},
payload: string
) {
const options: RequestInit = {
method: method,
headers: additionalHeaders
};
options.body = payload;
return this.httpRequestWithBinaryBody(options, url, "text/plain");
}
httpRequestWithBinaryBody(options: RequestInit, url: string, contentType?: string) { httpRequestWithBinaryBody(options: RequestInit, url: string, contentType?: string) {
options = applyFetchOptions(options); options = applyFetchOptions(options);
if (contentType) { if (contentType) {
if (!options.headers) { if (!options.headers) {
options.headers = new Headers(); options.headers = {};
} }
// @ts-ignore // @ts-ignore We are sure that here we only get headers of type Record<string, string>
options.headers["Content-Type"] = contentType; options.headers["Content-Type"] = contentType;
} }

View File

@@ -2,8 +2,8 @@ import React from "react";
type Props = { type Props = {
displayName: string; displayName: string;
url: string; url?: string;
disabled: boolean; disabled?: boolean;
onClick?: () => void; onClick?: () => void;
}; };

View File

@@ -0,0 +1,126 @@
import { byKey, byValueLength, byNestedKeys } from "./comparators";
const createObject = (key: string, value?: string) => {
return {
[key]: value
};
};
const createObjects = (key: string, values: Array<string | undefined>) => {
return values.map(v => createObject(key, v));
};
describe("key comparator tests", () => {
it("should sort array", () => {
const array = createObjects("key", ["z", "a", "y", "b"]);
array.sort(byKey("key"));
expect(array).toEqual(createObjects("key", ["a", "b", "y", "z"]));
});
it("should not fail if value is undefined", () => {
const array = createObjects("key", ["z", undefined, "a"]);
array.sort(byKey("key"));
expect(array).toEqual(createObjects("key", ["a", "z", undefined]));
});
it("should not fail if key is undefined", () => {
const array = createObjects("key", ["a"]);
array.push({});
array.push(createObject("key", "z"));
array.sort(byKey("key"));
expect(array).toEqual([createObject("key", "a"), createObject("key", "z"), {}]);
});
it("should not fail if item is undefined", () => {
const array: any[] = createObjects("key", ["a"]);
array.push(undefined);
array.push(createObject("key", "z"));
array.sort(byKey("key"));
expect(array).toEqual([createObject("key", "a"), createObject("key", "z"), undefined]);
});
});
describe("length comparator tests", () => {
it("should sort array", () => {
const array = createObjects("key", ["....", ".", "...", ".."]);
array.sort(byValueLength("key"));
expect(array).toEqual(createObjects("key", [".", "..", "...", "...."]));
});
it("should not fail if value is undefined", () => {
const array = createObjects("key", ["..", undefined, "."]);
array.sort(byValueLength("key"));
expect(array).toEqual(createObjects("key", [".", "..", undefined]));
});
it("should not fail if key is undefined", () => {
const array = createObjects("key", ["."]);
array.push({});
array.push(createObject("key", ".."));
array.sort(byValueLength("key"));
expect(array).toEqual([createObject("key", "."), createObject("key", ".."), {}]);
});
it("should not fail if item is undefined", () => {
const array: any[] = createObjects("key", ["."]);
array.push(undefined);
array.push(createObject("key", ".."));
array.sort(byValueLength("key"));
expect(array).toEqual([createObject("key", "."), createObject("key", ".."), undefined]);
});
});
describe("nested key comparator tests", () => {
const createObject = (key: string, nested?: string, value?: string) => {
if (!nested) {
return {
[key]: undefined
};
}
return {
[key]: {
[nested]: value
}
};
};
const createObjects = (key: string, nested: string, values: Array<string | undefined>) => {
return values.map(v => createObject(key, nested, v));
};
it("should sort array", () => {
const array = createObjects("key", "nested", ["z", "a", "y", "b"]);
array.sort(byNestedKeys("key", "nested"));
expect(array).toEqual(createObjects("key", "nested", ["a", "b", "y", "z"]));
});
it("should not fail if value is undefined", () => {
const array = createObjects("key", "nested", ["z", undefined, "a"]);
array.sort(byNestedKeys("key", "nested"));
expect(array).toEqual(createObjects("key", "nested", ["a", "z", undefined]));
});
it("should not fail if key is undefined", () => {
const array = createObjects("key", "nested", ["a"]);
array.push({});
array.push(createObject("key", "nested", "z"));
array.sort(byNestedKeys("key", "nested"));
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), {}]);
});
it("should not fail if nested key is undefined", () => {
const array = createObjects("key", "nested", ["a"]);
array.push(createObject("key", undefined, "y"));
array.push(createObject("key", "nested", "z"));
array.sort(byNestedKeys("key", "nested"));
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), { key: undefined }]);
});
it("should not fail if item is undefined", () => {
const array: any[] = createObjects("key", "nested", ["a"]);
array.push(undefined);
array.push(createObject("key", "nested", "z"));
array.sort(byNestedKeys("key", "nested"));
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), undefined]);
});
});

View File

@@ -0,0 +1,75 @@
const isUndefined = (o: any, key: string, nested?: string) => {
if (typeof o === "undefined" || typeof o[key] === "undefined") {
return true;
}
if (nested) {
return typeof o[key][nested] === "undefined";
}
return false;
};
export const byKey = (key: string) => {
return (a: any, b: any) => {
if (isUndefined(a, key)) {
return 1;
}
if (isUndefined(b, key)) {
return 0;
}
if (a[key] < b[key]) {
return -1;
} else if (a[key] > b[key]) {
return 1;
} else {
return 0;
}
};
};
export const byValueLength = (key: string) => {
return (a: any, b: any) => {
if (isUndefined(a, key)) {
return 1;
}
if (isUndefined(b, key)) {
return 0;
}
if (a[key].length < b[key].length) {
return -1;
} else if (a[key].length > b[key].length) {
return 1;
} else {
return 0;
}
};
};
export const byNestedKeys = (key: string, nestedKey: string) => {
return (a: any, b: any) => {
if (isUndefined(a, key, nestedKey)) {
return 1;
}
if (isUndefined(b, key, nestedKey)) {
return 0;
}
if (a[key][nestedKey] < b[key][nestedKey]) {
return -1;
} else if (a[key][nestedKey] > b[key][nestedKey]) {
return 1;
} else {
return 0;
}
};
};
export default {
byKey,
byValueLength,
byNestedKeys
};

View File

@@ -1,8 +1,7 @@
import React from "react"; import React, { FormEvent } from "react";
import { WithTranslation, withTranslation } from "react-i18next"; import { WithTranslation, withTranslation } from "react-i18next";
import { Links, Link } from "@scm-manager/ui-types"; import { Links, Link } from "@scm-manager/ui-types";
import { apiClient, SubmitButton, Loading, ErrorNotification } from "../"; import { apiClient, Level, SubmitButton, Loading, ErrorNotification } from "../";
import { FormEvent } from "react";
type RenderProps = { type RenderProps = {
readOnly: boolean; readOnly: boolean;
@@ -179,7 +178,9 @@ class Configuration extends React.Component<Props, State> {
<form onSubmit={this.modifyConfiguration}> <form onSubmit={this.modifyConfiguration}>
{this.props.render(renderProps)} {this.props.render(renderProps)}
<hr /> <hr />
<SubmitButton label={t("config.form.submit")} disabled={!valid || readOnly} loading={modifying} /> <Level
right={<SubmitButton label={t("config.form.submit")} disabled={!valid || readOnly} loading={modifying} />}
/>
</form> </form>
</> </>
); );

View File

@@ -1,7 +1,8 @@
import React, { MouseEvent } from "react"; import React, { MouseEvent } from "react";
import styled from "styled-components";
import { AddButton } from "../buttons"; import Level from "../layout/Level";
import InputField from "./InputField"; import InputField from "./InputField";
import AddButton from "../buttons/AddButton";
type Props = { type Props = {
addEntry: (p: string) => void; addEntry: (p: string) => void;
@@ -17,6 +18,22 @@ type State = {
entryToAdd: string; entryToAdd: string;
}; };
const StyledLevel = styled(Level)`
align-items: stretch;
margin-bottom: 1rem !important; // same margin as field
`;
const StyledInputField = styled(InputField)`
width: 100%;
margin-right: 1.5rem;
`;
const StyledField = styled.div.attrs(props => ({
className: "field"
}))`
align-self: flex-end;
`;
class AddEntryToTableField extends React.Component<Props, State> { class AddEntryToTableField extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@@ -37,23 +54,29 @@ class AddEntryToTableField extends React.Component<Props, State> {
render() { render() {
const { disabled, buttonLabel, fieldLabel, errorMessage, helpText } = this.props; const { disabled, buttonLabel, fieldLabel, errorMessage, helpText } = this.props;
return ( return (
<> <StyledLevel
<InputField children={
label={fieldLabel} <StyledInputField
errorMessage={errorMessage} label={fieldLabel}
onChange={this.handleAddEntryChange} errorMessage={errorMessage}
validationError={!this.isValid()} onChange={this.handleAddEntryChange}
value={this.state.entryToAdd} validationError={!this.isValid()}
onReturnPressed={this.appendEntry} value={this.state.entryToAdd}
disabled={disabled} onReturnPressed={this.appendEntry}
helpText={helpText} disabled={disabled}
/> helpText={helpText}
<AddButton />
label={buttonLabel} }
action={this.addButtonClicked} right={
disabled={disabled || this.state.entryToAdd === "" || !this.isValid()} <StyledField>
/> <AddButton
</> label={buttonLabel}
action={this.addButtonClicked}
disabled={disabled || this.state.entryToAdd === "" || !this.isValid()}
/>
</StyledField>
}
/>
); );
} }

View File

@@ -1,6 +1,7 @@
import React, { MouseEvent } from "react"; import React, { MouseEvent } from "react";
import styled from "styled-components";
import { AutocompleteObject, SelectValue } from "@scm-manager/ui-types"; import { SelectValue } from "@scm-manager/ui-types";
import Level from "../layout/Level";
import Autocomplete from "../Autocomplete"; import Autocomplete from "../Autocomplete";
import AddButton from "../buttons/AddButton"; import AddButton from "../buttons/AddButton";
@@ -20,6 +21,11 @@ type State = {
selectedValue?: SelectValue; selectedValue?: SelectValue;
}; };
const StyledAutocomplete = styled(Autocomplete)`
width: 100%;
margin-right: 1.5rem;
`;
class AutocompleteAddEntryToTableField extends React.Component<Props, State> { class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@@ -41,21 +47,26 @@ class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
const { selectedValue } = this.state; const { selectedValue } = this.state;
return ( return (
<div className="field"> <Level
<Autocomplete children={
label={fieldLabel} <StyledAutocomplete
loadSuggestions={loadSuggestions} label={fieldLabel}
valueSelected={this.handleAddEntryChange} loadSuggestions={loadSuggestions}
helpText={helpText} valueSelected={this.handleAddEntryChange}
value={selectedValue} helpText={helpText}
placeholder={placeholder} value={selectedValue}
loadingMessage={loadingMessage} placeholder={placeholder}
noOptionsMessage={noOptionsMessage} loadingMessage={loadingMessage}
creatable={true} noOptionsMessage={noOptionsMessage}
/> creatable={true}
/>
<AddButton label={buttonLabel} action={this.addButtonClicked} disabled={disabled} /> }
</div> right={
<div className="field">
<AddButton label={buttonLabel} action={this.addButtonClicked} disabled={disabled} />
</div>
}
/>
); );
} }

View File

@@ -15,6 +15,7 @@ type Props = {
errorMessage?: string; errorMessage?: string;
disabled?: boolean; disabled?: boolean;
helpText?: string; helpText?: string;
className?: string;
}; };
class InputField extends React.Component<Props> { class InputField extends React.Component<Props> {
@@ -47,11 +48,21 @@ class InputField extends React.Component<Props> {
}; };
render() { render() {
const { type, placeholder, value, validationError, errorMessage, disabled, label, helpText } = this.props; const {
type,
placeholder,
value,
validationError,
errorMessage,
disabled,
label,
helpText,
className
} = this.props;
const errorView = validationError ? "is-danger" : ""; const errorView = validationError ? "is-danger" : "";
const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : ""; const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : "";
return ( return (
<div className="field"> <div className={classNames("field", className)}>
<LabelWithHelpIcon label={label} helpText={helpText} /> <LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control"> <div className="control">
<input <input

View File

@@ -0,0 +1,56 @@
import React, {useState} from "react";
import { storiesOf } from "@storybook/react";
import styled from "styled-components";
import Textarea from "./Textarea";
const Spacing = styled.div`
padding: 2em;
`;
const OnChangeTextarea = () => {
const [value, setValue] = useState("Start typing");
return (
<Spacing>
<Textarea value={value} onChange={v => setValue(v)} />
<hr />
<p>{value}</p>
</Spacing>
);
};
const OnSubmitTextare = () => {
const [value, setValue] = useState("Use the ctrl/command + Enter to submit the textarea");
const [submitted, setSubmitted] = useState("");
const submit = () => {
setSubmitted(value);
setValue("");
};
return (
<Spacing>
<Textarea value={value} onChange={v => setValue(v)} onSubmit={submit} />
<hr />
<p>{submitted}</p>
</Spacing>
);
};
const OnCancelTextare = () => {
const [value, setValue] = useState("Use the escape key to clear the textarea");
const cancel = () => {
setValue("");
};
return (
<Spacing>
<Textarea value={value} onChange={v => setValue(v)} onCancel={cancel} />
</Spacing>
);
};
storiesOf("Forms|Textarea", module)
.add("OnChange", () => <OnChangeTextarea />)
.add("OnSubmit", () => <OnSubmitTextare />)
.add("OnCancel", () => <OnCancelTextare />);

View File

@@ -1,4 +1,4 @@
import React, { ChangeEvent } from "react"; import React, { ChangeEvent, KeyboardEvent } from "react";
import LabelWithHelpIcon from "./LabelWithHelpIcon"; import LabelWithHelpIcon from "./LabelWithHelpIcon";
type Props = { type Props = {
@@ -10,6 +10,8 @@ type Props = {
onChange: (value: string, name?: string) => void; onChange: (value: string, name?: string) => void;
helpText?: string; helpText?: string;
disabled?: boolean; disabled?: boolean;
onSubmit?: () => void;
onCancel?: () => void;
}; };
class Textarea extends React.Component<Props> { class Textarea extends React.Component<Props> {
@@ -25,6 +27,19 @@ class Textarea extends React.Component<Props> {
this.props.onChange(event.target.value, this.props.name); this.props.onChange(event.target.value, this.props.name);
}; };
onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
const { onCancel } = this.props;
if (onCancel && event.key === "Escape") {
onCancel();
return;
}
const { onSubmit } = this.props;
if (onSubmit && event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
onSubmit();
}
};
render() { render() {
const { placeholder, value, label, helpText, disabled } = this.props; const { placeholder, value, label, helpText, disabled } = this.props;
@@ -41,6 +56,7 @@ class Textarea extends React.Component<Props> {
onChange={this.handleInput} onChange={this.handleInput}
value={value} value={value}
disabled={!!disabled} disabled={!!disabled}
onKeyDown={this.onKeyDown}
/> />
</div> </div>
</div> </div>

View File

@@ -52,6 +52,8 @@ export { default as OverviewPageActions } from "./OverviewPageActions";
export { default as CardColumnGroup } from "./CardColumnGroup"; export { default as CardColumnGroup } from "./CardColumnGroup";
export { default as CardColumn } from "./CardColumn"; export { default as CardColumn } from "./CardColumn";
export { default as comparators } from "./comparators";
export { apiClient } from "./apiclient"; export { apiClient } from "./apiclient";
export * from "./errors"; export * from "./errors";
@@ -63,6 +65,7 @@ export * from "./layout";
export * from "./modals"; export * from "./modals";
export * from "./navigation"; export * from "./navigation";
export * from "./repos"; export * from "./repos";
export * from "./table";
export { export {
File, File,

View File

@@ -4,15 +4,22 @@ import classNames from "classnames";
type Props = { type Props = {
className?: string; className?: string;
left?: ReactNode; left?: ReactNode;
children?: ReactNode;
right?: ReactNode; right?: ReactNode;
}; };
export default class Level extends React.Component<Props> { export default class Level extends React.Component<Props> {
render() { render() {
const { className, left, right } = this.props; const { className, left, children, right } = this.props;
let child = null;
if (children) {
child = <div className="level-item">{children}</div>;
}
return ( return (
<div className={classNames("level", className)}> <div className={classNames("level", className)}>
<div className="level-left">{left}</div> <div className="level-left">{left}</div>
{child}
<div className="level-right">{right}</div> <div className="level-right">{right}</div>
</div> </div>
); );

View File

@@ -1,11 +1,14 @@
import React from "react"; import React from "react";
import DiffFile from "./DiffFile"; import DiffFile from "./DiffFile";
import { DiffObjectProps, File } from "./DiffTypes"; import { DiffObjectProps, File } from "./DiffTypes";
import Notification from "../Notification";
import { WithTranslation, withTranslation } from "react-i18next";
type Props = DiffObjectProps & { type Props = WithTranslation &
diff: File[]; DiffObjectProps & {
defaultCollapse?: boolean; diff: File[];
}; defaultCollapse?: boolean;
};
class Diff extends React.Component<Props> { class Diff extends React.Component<Props> {
static defaultProps: Partial<Props> = { static defaultProps: Partial<Props> = {
@@ -13,15 +16,17 @@ class Diff extends React.Component<Props> {
}; };
render() { render() {
const { diff, ...fileProps } = this.props; const { diff, t, ...fileProps } = this.props;
return ( return (
<> <>
{diff.map((file, index) => ( {diff.length === 0 ? (
<DiffFile key={index} file={file} {...fileProps} {...this.props} /> <Notification type="info">{t("diff.noDiffFound")}</Notification>
))} ) : (
diff.map((file, index) => <DiffFile key={index} file={file} {...fileProps} {...this.props} />)
)}
</> </>
); );
} }
} }
export default Diff; export default withTranslation("repos")(Diff);

View File

@@ -43,6 +43,7 @@ class LoadingDiff extends React.Component<Props, State> {
fetchDiff = () => { fetchDiff = () => {
const { url } = this.props; const { url } = this.props;
this.setState({loading: true});
apiClient apiClient
.get(url) .get(url)
.then(response => response.text()) .then(response => response.text())

View File

@@ -0,0 +1,18 @@
import React, { FC, ReactNode } from "react";
import { ColumnProps } from "./types";
type Props = ColumnProps & {
children: (row: any, columnIndex: number) => ReactNode;
};
const Column: FC<Props> = ({ row, columnIndex, children }) => {
if (row === undefined) {
throw new Error("missing row, use column only as child of Table");
}
if (columnIndex === undefined) {
throw new Error("missing row, use column only as child of Table");
}
return <>{children(row, columnIndex)}</>;
};
export default Column;

View File

@@ -0,0 +1,19 @@
import React, { FC } from "react";
import styled from "styled-components";
import Icon from "../Icon";
type Props = {
name: string;
isVisible: boolean;
};
const IconWithMarginLeft = styled(Icon)`
visibility: ${(props: Props) => (props.isVisible ? "visible" : "hidden")};
margin-left: 0.25em;
`;
const SortIcon: FC<Props> = (props: Props) => {
return <IconWithMarginLeft name={props.name} isVisible={props.isVisible} />;
};
export default SortIcon;

View File

@@ -0,0 +1,53 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import Table from "./Table";
import Column from "./Column";
import TextColumn from "./TextColumn";
storiesOf("Table|Table", module)
.add("Default", () => (
<Table
data={[
{ firstname: "Tricia", lastname: "McMillan", email: "tricia@hitchhiker.com" },
{ firstname: "Arthur", lastname: "Dent", email: "arthur@hitchhiker.com" }
]}
>
<Column header={"First Name"}>{(row: any) => <h4>{row.firstname}</h4>}</Column>
<Column
header={"Last Name"}
createComparator={() => {
return (a: any, b: any) => {
if (a.lastname > b.lastname) {
return -1;
} else if (a.lastname < b.lastname) {
return 1;
} else {
return 0;
}
};
}}
>
{(row: any) => <b style={{ color: "red" }}>{row.lastname}</b>}
</Column>
<Column header={"E-Mail"}>{(row: any) => <a>{row.email}</a>}</Column>
</Table>
))
.add("TextColumn", () => (
<Table
data={[
{ id: "21", title: "Pommes", desc: "Fried potato sticks" },
{ id: "42", title: "Quarter-Pounder", desc: "Big burger" },
{ id: "-84", title: "Icecream", desc: "Cold dessert" }
]}
>
<TextColumn header="Id" dataKey="id" />
<TextColumn header="Name" dataKey="title" />
<TextColumn header="Description" dataKey="desc" />
</Table>
))
.add("Empty", () => (
<Table data={[]} emptyMessage="No data found.">
<TextColumn header="Id" dataKey="id" />
<TextColumn header="Name" dataKey="name" />
</Table>
));

View File

@@ -0,0 +1,120 @@
import React, { FC, ReactElement, useEffect, useState } from "react";
import styled from "styled-components";
import { Comparator } from "./types";
import SortIcon from "./SortIcon";
import Notification from "../Notification";
const StyledTable = styled.table.attrs(() => ({
className: "table content is-hoverable"
}))``;
type Props = {
data: any[];
sortable?: boolean;
emptyMessage?: string;
children: Array<ReactElement>;
};
const Table: FC<Props> = ({ data, sortable, children, emptyMessage }) => {
const [tableData, setTableData] = useState(data);
useEffect(() => {
setTableData(data);
}, [data]);
const [ascending, setAscending] = useState(false);
const [lastSortBy, setlastSortBy] = useState<number | undefined>();
const [hoveredColumnIndex, setHoveredColumnIndex] = useState<number | undefined>();
const isSortable = (child: ReactElement) => {
return sortable && child.props.createComparator;
};
const sortFunctions: Comparator | undefined[] = [];
React.Children.forEach(children, (child, index) => {
if (child && isSortable(child)) {
sortFunctions.push(child.props.createComparator(child.props, index));
} else {
sortFunctions.push(undefined);
}
});
const mapDataToColumns = (row: any) => {
return (
<tr>
{React.Children.map(children, (child, columnIndex) => {
return <td>{React.cloneElement(child, { ...child.props, columnIndex, row })}</td>;
})}
</tr>
);
};
const sortDescending = (sortAscending: (a: any, b: any) => number) => {
return (a: any, b: any) => {
return sortAscending(a, b) * -1;
};
};
const tableSort = (index: number) => {
const sortFn = sortFunctions[index];
if (!sortFn) {
throw new Error(`column with index ${index} is not sortable`);
}
const sortableData = [...tableData];
let sortOrder = ascending;
if (lastSortBy !== index) {
setAscending(true);
sortOrder = true;
}
const sortFunction = sortOrder ? sortFn : sortDescending(sortFn);
sortableData.sort(sortFunction);
setTableData(sortableData);
setAscending(!sortOrder);
setlastSortBy(index);
};
const shouldShowIcon = (index: number) => {
return index === lastSortBy || index === hoveredColumnIndex;
};
if (!tableData || tableData.length <= 0) {
if (emptyMessage) {
return <Notification type="info">{emptyMessage}</Notification>;
} else {
return null;
}
}
return (
<StyledTable>
<thead>
<tr>
{React.Children.map(children, (child, index) => (
<th
className={isSortable(child) && "has-cursor-pointer"}
onClick={isSortable(child) ? () => tableSort(index) : undefined}
onMouseEnter={() => setHoveredColumnIndex(index)}
onMouseLeave={() => setHoveredColumnIndex(undefined)}
>
{child.props.header}
{isSortable(child) && renderSortIcon(child, ascending, shouldShowIcon(index))}
</th>
))}
</tr>
</thead>
<tbody>{tableData.map(mapDataToColumns)}</tbody>
</StyledTable>
);
};
Table.defaultProps = {
sortable: true
};
const renderSortIcon = (child: ReactElement, ascending: boolean, showIcon: boolean) => {
if (child.props.ascendingIcon && child.props.descendingIcon) {
return <SortIcon name={ascending ? child.props.ascendingIcon : child.props.descendingIcon} isVisible={showIcon} />;
} else {
return <SortIcon name={ascending ? "sort-amount-down-alt" : "sort-amount-down"} isVisible={showIcon} />;
}
};
export default Table;

View File

@@ -0,0 +1,21 @@
import React, { FC } from "react";
import { ColumnProps } from "./types";
import comparators from "../comparators";
type Props = ColumnProps & {
dataKey: string;
};
const TextColumn: FC<Props> = ({ row, dataKey }) => {
return row[dataKey];
};
TextColumn.defaultProps = {
createComparator: (props: Props) => {
return comparators.byKey(props.dataKey);
},
ascendingIcon: "sort-alpha-down-alt",
descendingIcon: "sort-alpha-down"
};
export default TextColumn;

View File

@@ -0,0 +1,4 @@
export { default as Table } from "./Table";
export { default as Column } from "./Column";
export { default as TextColumn } from "./TextColumn";
export { default as SortIcon } from "./SortIcon";

View File

@@ -0,0 +1,12 @@
import { ReactNode } from "react";
export type Comparator = (a: any, b: any) => number;
export type ColumnProps = {
header: ReactNode;
row?: any;
columnIndex?: number;
createComparator?: (props: any, columnIndex: number) => Comparator;
ascendingIcon?: string;
descendingIcon?: string;
};

View File

@@ -16,7 +16,7 @@
"@types/enzyme": "^3.10.3", "@types/enzyme": "^3.10.3",
"@types/jest": "^24.0.19", "@types/jest": "^24.0.19",
"@types/react": "^16.9.9", "@types/react": "^16.9.9",
"typescript": "^3.6.4" "typescript": "^3.7.2"
}, },
"babel": { "babel": {
"presets": [ "presets": [

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const { spawnSync } = require("child_process"); const { spawnSync } = require("child_process");
const commands = ["plugin", "plugin-watch", "publish"]; const commands = ["plugin", "plugin-watch", "publish", "version"];
const args = process.argv.slice(2); const args = process.argv.slice(2);

View File

@@ -4,7 +4,7 @@ const versions = require("../versions");
const args = process.argv.slice(2); const args = process.argv.slice(2);
if (args.length < 1) { if (args.length < 1) {
console.log("usage ui-scripts publish version"); console.log("usage ui-scripts publish <version>");
process.exit(1); process.exit(1);
} }

View File

@@ -0,0 +1,12 @@
const lerna = require("../lerna");
const args = process.argv.slice(2);
if (args.length < 1) {
console.log("usage ui-scripts version <new-version>");
process.exit(1);
}
const version = args[0];
lerna.version(version);

View File

@@ -7,41 +7,6 @@ const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const root = path.resolve(process.cwd(), "scm-ui"); const root = path.resolve(process.cwd(), "scm-ui");
module.exports = [ module.exports = [
{
context: root,
entry: "./ui-styles/src/scm.scss",
module: {
rules: [
{
test: /\.(css|scss|sass)$/i,
use: [
{
loader: MiniCssExtractPlugin.loader
},
"css-loader",
"sass-loader"
]
},
{
test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/,
use: ["file-loader"]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "ui-styles.css",
ignoreOrder: false
})
],
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})]
},
output: {
path: path.join(root, "target", "assets"),
filename: "ui-styles.bundle.js"
}
},
{ {
context: root, context: root,
entry: { entry: {
@@ -142,6 +107,41 @@ module.exports = [
} }
} }
}, },
{
context: root,
entry: "./ui-styles/src/scm.scss",
module: {
rules: [
{
test: /\.(css|scss|sass)$/i,
use: [
{
loader: MiniCssExtractPlugin.loader
},
"css-loader",
"sass-loader"
]
},
{
test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/,
use: ["file-loader"]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "ui-styles.css",
ignoreOrder: false
})
],
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})]
},
output: {
path: path.join(root, "target", "assets"),
filename: "ui-styles.bundle.js"
}
},
{ {
context: path.resolve(root), context: path.resolve(root),
entry: { entry: {

View File

@@ -622,6 +622,10 @@ form .field:not(.is-grouped) {
} }
} }
.help {
position: absolute;
}
// label with help-icon compensation // label with help-icon compensation
.label-icon-spacing { .label-icon-spacing {
margin-top: 30px; margin-top: 30px;
@@ -809,10 +813,6 @@ form .field:not(.is-grouped) {
} }
} }
.modal-card-body div div:last-child {
border-bottom: none;
}
// cursor // cursor
.has-cursor-pointer { .has-cursor-pointer {
cursor: pointer; cursor: pointer;

View File

@@ -26,6 +26,6 @@
"@types/enzyme": "^3.10.3", "@types/enzyme": "^3.10.3",
"@types/enzyme-adapter-react-16": "^1.0.5", "@types/enzyme-adapter-react-16": "^1.0.5",
"@types/jest": "^24.0.19", "@types/jest": "^24.0.19",
"typescript": "^3.6.4" "typescript": "^3.7.2"
} }
} }

View File

@@ -14,7 +14,7 @@
"typecheck": "tsc" "typecheck": "tsc"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^3.6.4" "typescript": "^3.7.2"
}, },
"babel": { "babel": {
"presets": [ "presets": [

View File

@@ -26,8 +26,7 @@
"systemjs": "0.21.6" "systemjs": "0.21.6"
}, },
"scripts": { "scripts": {
"test": "jest", "test": "jest"
"flow": "flow"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-tests": "^2.0.0-SNAPSHOT", "@scm-manager/ui-tests": "^2.0.0-SNAPSHOT",

View File

@@ -6,7 +6,7 @@
"settingsNavLink": "Einstellungen", "settingsNavLink": "Einstellungen",
"generalNavLink": "Generell" "generalNavLink": "Generell"
}, },
"info": { "info": {
"currentAppVersion": "Aktuelle Software-Versionsnummer", "currentAppVersion": "Aktuelle Software-Versionsnummer",
"communityTitle": "Community Support", "communityTitle": "Community Support",
"communityIconAlt": "Community Support Icon", "communityIconAlt": "Community Support Icon",
@@ -91,8 +91,7 @@
"submit": "Speichern" "submit": "Speichern"
}, },
"delete": { "delete": {
"button": "Löschen", "button": "Berechtigungsrolle löschen",
"subtitle": "Berechtigungsrolle löschen",
"confirmAlert": { "confirmAlert": {
"title": "Berechtigungsrolle löschen?", "title": "Berechtigungsrolle löschen?",
"message": "Wollen Sie diese Rolle wirklich löschen? Alle Benutzer mit dieser Rolle verlieren die entsprechenden Berechtigungen.", "message": "Wollen Sie diese Rolle wirklich löschen? Alle Benutzer mit dieser Rolle verlieren die entsprechenden Berechtigungen.",

View File

@@ -60,8 +60,7 @@
} }
}, },
"deleteGroup": { "deleteGroup": {
"subtitle": "Gruppe löschen", "button": "Gruppe löschen",
"button": "Löschen",
"confirmAlert": { "confirmAlert": {
"title": "Gruppe löschen", "title": "Gruppe löschen",
"message": "Soll die Gruppe wirklich gelöscht werden?", "message": "Soll die Gruppe wirklich gelöscht werden?",

View File

@@ -114,7 +114,7 @@
"size": "Größe" "size": "Größe"
}, },
"noSources": "Keine Sources in diesem Branch gefunden.", "noSources": "Keine Sources in diesem Branch gefunden.",
"extension" : { "extension": {
"notBound": "Keine Erweiterung angebunden." "notBound": "Keine Erweiterung angebunden."
} }
}, },
@@ -166,8 +166,7 @@
} }
}, },
"deleteRepo": { "deleteRepo": {
"subtitle": "Repository löschen", "button": "Repository löschen",
"button": "Löschen",
"confirmAlert": { "confirmAlert": {
"title": "Repository löschen", "title": "Repository löschen",
"message": "Soll das Repository wirklich gelöscht werden?", "message": "Soll das Repository wirklich gelöscht werden?",
@@ -177,7 +176,8 @@
}, },
"diff": { "diff": {
"sideBySide": "Zweispaltig", "sideBySide": "Zweispaltig",
"combined": "Kombiniert" "combined": "Kombiniert",
"noDiffFound": "Kein Diff zwischen den ausgewählten Branches gefunden."
}, },
"fileUpload": { "fileUpload": {
"clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.", "clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.",

View File

@@ -45,8 +45,7 @@
"subtitle": "Erstellen eines neuen Benutzers" "subtitle": "Erstellen eines neuen Benutzers"
}, },
"deleteUser": { "deleteUser": {
"subtitle": "Benutzer löschen", "button": "Benutzer löschen",
"button": "Löschen",
"confirmAlert": { "confirmAlert": {
"title": "Benutzer löschen", "title": "Benutzer löschen",
"message": "Soll der Benutzer wirklich gelöscht werden?", "message": "Soll der Benutzer wirklich gelöscht werden?",

View File

@@ -11,7 +11,7 @@
"communityTitle": "Community Support", "communityTitle": "Community Support",
"communityIconAlt": "Community Support Icon", "communityIconAlt": "Community Support Icon",
"communityInfo": "Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels.", "communityInfo": "Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels.",
"communityButton": "Contact our team", "communityButton": "Contact our Team",
"enterpriseTitle": "Enterprise Support", "enterpriseTitle": "Enterprise Support",
"enterpriseIconAlt": "Enterprise Support Icon", "enterpriseIconAlt": "Enterprise Support Icon",
"enterpriseInfo": "You require support with the integration of SCM-Manager into your processes, with the customization of the tool or simply a service level agreement (SLA)?", "enterpriseInfo": "You require support with the integration of SCM-Manager into your processes, with the customization of the tool or simply a service level agreement (SLA)?",
@@ -76,8 +76,8 @@
"createSubtitle": "Create Permission Role", "createSubtitle": "Create Permission Role",
"editSubtitle": "Edit Permission Role", "editSubtitle": "Edit Permission Role",
"overview": { "overview": {
"title": "Overview of all permission roles", "title": "Overview of all Permission Roles",
"noPermissionRoles": "No permission roles found.", "noPermissionRoles": "No Permission Roles found.",
"createButton": "Create Permission Role" "createButton": "Create Permission Role"
}, },
"editButton": "Edit", "editButton": "Edit",
@@ -91,10 +91,9 @@
"submit": "Save" "submit": "Save"
}, },
"delete": { "delete": {
"button": "Delete", "button": "Delete Permission Role",
"subtitle": "Delete permission role",
"confirmAlert": { "confirmAlert": {
"title": "Delete permission role", "title": "Delete Permission Role",
"message": "Do you really want to delete this permission role? All users will lose their corresponding permissions.", "message": "Do you really want to delete this permission role? All users will lose their corresponding permissions.",
"submit": "Yes", "submit": "Yes",
"cancel": "No" "cancel": "No"

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