This commit is contained in:
Florian Scholdei
2019-02-07 13:59:57 +01:00
247 changed files with 2885 additions and 2195 deletions

5
Jenkinsfile vendored
View File

@@ -50,6 +50,11 @@ node('docker') {
def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}"
if (isMainBranch()) {
stage('Lifecycle') {
nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build'
}
stage('Archive') {
archiveArtifacts 'scm-webapp/target/scm-webapp.war'
archiveArtifacts 'scm-server/target/scm-server-app.*'

21
pom.xml
View File

@@ -351,21 +351,6 @@
<scope>test</scope>
</dependency>
<!-- utils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<!-- http -->
<dependency>
@@ -825,11 +810,11 @@
<logback.version>1.2.3</logback.version>
<servlet.version>3.0.1</servlet.version>
<jaxrs.version>2.0.1</jaxrs.version>
<resteasy.version>3.1.3.Final</resteasy.version>
<jaxrs.version>2.1.1</jaxrs.version>
<resteasy.version>3.6.2.Final</resteasy.version>
<jersey-client.version>1.19.4</jersey-client.version>
<enunciate.version>2.11.1</enunciate.version>
<jackson.version>2.8.6</jackson.version>
<jackson.version>2.9.8</jackson.version>
<guice.version>4.0</guice.version>
<jaxb.version>2.3.0</jaxb.version>

View File

@@ -93,6 +93,7 @@
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
@@ -235,7 +236,6 @@
<links>
<link>http://download.oracle.com/javase/6/docs/api/</link>
<link>http://download.oracle.com/docs/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/</link>
<link>http://jersey.java.net/nonav/apidocs/${jersey.version}/jersey/</link>
<link>https://google.github.io/guice/api-docs/${guice.version}/javadoc</link>
<link>http://www.slf4j.org/api/</link>
<link>http://shiro.apache.org/static/${shiro.version}/apidocs/</link>

View File

@@ -36,7 +36,6 @@ package sonia.scm;
//~--- JDK imports ------------------------------------------------------------
import java.io.Closeable;
import java.io.IOException;
/**
* The base class of all handlers.

View File

@@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import org.mapstruct.Mapping;
public abstract class BaseMapper<T, D extends HalRepresentation> extends LinkAppenderMapper implements InstantAttributeMapper {
public abstract class BaseMapper<T, D extends HalRepresentation> extends HalAppenderMapper implements InstantAttributeMapper {
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract D map(T modelObject);

View File

@@ -1,5 +1,6 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
@@ -7,7 +8,6 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.Instant;
import java.util.List;
@Getter
@Setter
@@ -34,16 +34,7 @@ public class ChangesetDto extends HalRepresentation {
*/
private String description;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
public ChangesetDto(Links links, Embedded embedded) {
super(links, embedded);
}
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation withEmbedded(String rel, List<? extends HalRepresentation> halRepresentations) {
return super.withEmbedded(rel, halRepresentations);
}
}

View File

@@ -0,0 +1,14 @@
package sonia.scm.api.v2.resources;
import org.mapstruct.Context;
import org.mapstruct.Mapping;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Repository;
public interface ChangesetToChangesetDtoMapper {
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
ChangesetDto map(Changeset changeset, @Context Repository repository);
}

View File

@@ -1,12 +1,14 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
/**
* The {@link LinkAppender} can be used within an {@link LinkEnricher} to append hateoas links to a json response.
* The {@link HalAppender} can be used within an {@link HalEnricher} to append hateoas links to a json response.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
public interface LinkAppender {
public interface HalAppender {
/**
* Appends one link to the json response.
@@ -14,7 +16,7 @@ public interface LinkAppender {
* @param rel name of relation
* @param href link uri
*/
void appendOne(String rel, String href);
void appendLink(String rel, String href);
/**
* Returns a builder which is able to append an array of links to the resource.
@@ -22,8 +24,15 @@ public interface LinkAppender {
* @param rel name of link relation
* @return multi link builder
*/
LinkArrayBuilder arrayBuilder(String rel);
LinkArrayBuilder linkArrayBuilder(String rel);
/**
* Appends one embedded to the json response.
*
* @param rel name of relation
* @param embeddedItem embedded object
*/
void appendEmbedded(String rel, HalRepresentation embeddedItem);
/**
* Builder for link arrays.

View File

@@ -4,17 +4,17 @@ import com.google.common.annotations.VisibleForTesting;
import javax.inject.Inject;
public class LinkAppenderMapper {
public class HalAppenderMapper {
@Inject
private LinkEnricherRegistry registry;
private HalEnricherRegistry registry;
@VisibleForTesting
void setRegistry(LinkEnricherRegistry registry) {
void setRegistry(HalEnricherRegistry registry) {
this.registry = registry;
}
protected void appendLinks(LinkAppender appender, Object source, Object... contextEntries) {
protected void applyEnrichers(HalAppender appender, Object source, Object... contextEntries) {
// null check is only their to not break existing tests
if (registry != null) {
@@ -24,10 +24,10 @@ public class LinkAppenderMapper {
ctx[i + 1] = contextEntries[i];
}
LinkEnricherContext context = LinkEnricherContext.of(ctx);
HalEnricherContext context = HalEnricherContext.of(ctx);
Iterable<LinkEnricher> enrichers = registry.allByType(source.getClass());
for (LinkEnricher enricher : enrichers) {
Iterable<HalEnricher> enrichers = registry.allByType(source.getClass());
for (HalEnricher enricher : enrichers) {
enricher.enrich(context, appender);
}
}

View File

@@ -3,8 +3,8 @@ package sonia.scm.api.v2.resources;
import sonia.scm.plugin.ExtensionPoint;
/**
* A {@link LinkEnricher} can be used to append hateoas links to a specific json response.
* To register an enricher use the {@link Enrich} annotation or the {@link LinkEnricherRegistry} which is available
* A {@link HalEnricher} can be used to append hal specific attributes, such as links, to the json response.
* To register an enricher use the {@link Enrich} annotation or the {@link HalEnricherRegistry} which is available
* via injection.
*
* <b>Warning:</b> enrichers are always registered as singletons.
@@ -14,13 +14,13 @@ import sonia.scm.plugin.ExtensionPoint;
*/
@ExtensionPoint
@FunctionalInterface
public interface LinkEnricher {
public interface HalEnricher {
/**
* Enriches the response with hateoas links.
* Enriches the response with hal specific attributes.
*
* @param context contains the source for the json mapping and related objects
* @param appender can be used to append links to the json response
* @param appender can be used to append links or embedded objects to the json response
*/
void enrich(LinkEnricherContext context, LinkAppender appender);
void enrich(HalEnricherContext context, HalAppender appender);
}

View File

@@ -7,17 +7,17 @@ import java.util.NoSuchElementException;
import java.util.Optional;
/**
* Context object for the {@link LinkEnricher}. The context holds the source object for the json and all related
* objects, which can be useful for the link creation.
* Context object for the {@link HalEnricher}. The context holds the source object for the json and all related
* objects, which can be useful for the enrichment.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
public final class LinkEnricherContext {
public final class HalEnricherContext {
private final Map<Class, Object> instanceMap;
private LinkEnricherContext(Map<Class,Object> instanceMap) {
private HalEnricherContext(Map<Class,Object> instanceMap) {
this.instanceMap = instanceMap;
}
@@ -28,12 +28,12 @@ public final class LinkEnricherContext {
*
* @return context of given entries
*/
public static LinkEnricherContext of(Object... instances) {
public static HalEnricherContext of(Object... instances) {
ImmutableMap.Builder<Class, Object> builder = ImmutableMap.builder();
for (Object instance : instances) {
builder.put(instance.getClass(), instance);
}
return new LinkEnricherContext(builder.build());
return new HalEnricherContext(builder.build());
}
/**

View File

@@ -7,34 +7,34 @@ import sonia.scm.plugin.Extension;
import javax.inject.Singleton;
/**
* The {@link LinkEnricherRegistry} is responsible for binding {@link LinkEnricher} instances to their source types.
* The {@link HalEnricherRegistry} is responsible for binding {@link HalEnricher} instances to their source types.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
@Extension
@Singleton
public final class LinkEnricherRegistry {
public final class HalEnricherRegistry {
private final Multimap<Class, LinkEnricher> enrichers = HashMultimap.create();
private final Multimap<Class, HalEnricher> enrichers = HashMultimap.create();
/**
* Registers a new {@link LinkEnricher} for the given source type.
* Registers a new {@link HalEnricher} for the given source type.
*
* @param sourceType type of json mapping source
* @param enricher link enricher instance
*/
public void register(Class sourceType, LinkEnricher enricher) {
public void register(Class sourceType, HalEnricher enricher) {
enrichers.put(sourceType, enricher);
}
/**
* Returns all registered {@link LinkEnricher} for the given type.
* Returns all registered {@link HalEnricher} for the given type.
*
* @param sourceType type of json mapping source
* @return all registered enrichers
*/
public Iterable<LinkEnricher> allByType(Class sourceType) {
public Iterable<HalEnricher> allByType(Class sourceType) {
return enrichers.get(sourceType);
}
}

View File

@@ -1,7 +1,7 @@
package sonia.scm.api.v2.resources;
/**
* The {@link Index} object can be used to register a {@link LinkEnricher} for the index resource.
* The {@link Index} object can be used to register a {@link HalEnricher} for the index resource.
*
* @author Sebastian Sdorra
* @since 2.0.0

View File

@@ -1,7 +1,7 @@
package sonia.scm.api.v2.resources;
/**
* The {@link Me} object can be used to register a {@link LinkEnricher} for the me resource.
* The {@link Me} object can be used to register a {@link HalEnricher} for the me resource.
*
* @author Sebastian Sdorra
* @since 2.0.0

View File

@@ -1,24 +1,59 @@
package sonia.scm.filter;
import lombok.extern.slf4j.Slf4j;
import sonia.scm.util.WebUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Locale;
import java.util.zip.GZIPOutputStream;
@Provider
@Slf4j
public class GZipResponseFilter implements ContainerResponseFilter {
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
if (WebUtil.isGzipSupported(requestContext::getHeaderString)) {
log.trace("compress output with gzip");
GZIPOutputStream wrappedResponse = new GZIPOutputStream(responseContext.getEntityStream());
responseContext.getHeaders().add("Content-Encoding", "gzip");
responseContext.setEntityStream(wrappedResponse);
@javax.ws.rs.ext.Provider
public class GZipResponseFilter implements WriterInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(GZipResponseFilter.class);
private final Provider<HttpServletRequest> requestProvider;
@Inject
public GZipResponseFilter(Provider<HttpServletRequest> requestProvider) {
this.requestProvider = requestProvider;
}
@Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
if (isGZipSupported()) {
LOG.trace("compress output with gzip");
encodeWithGZip(context);
} else {
context.proceed();
}
}
private void encodeWithGZip(WriterInterceptorContext context) throws IOException {
context.getHeaders().remove(HttpHeaders.CONTENT_LENGTH);
context.getHeaders().add(HttpHeaders.CONTENT_ENCODING, "gzip");
OutputStream outputStream = context.getOutputStream();
GZIPOutputStream compressedOutputStream = new GZIPOutputStream(outputStream);
context.setOutputStream(compressedOutputStream);
try {
context.proceed();
} finally {
compressedOutputStream.finish();
context.setOutputStream(outputStream);
}
}
private boolean isGZipSupported() {
Object encoding = requestProvider.get().getHeader(HttpHeaders.ACCEPT_ENCODING);
return encoding != null && encoding.toString().toLowerCase(Locale.ENGLISH).contains("gzip");
}
}

View File

@@ -35,7 +35,6 @@ package sonia.scm.net.ahc;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;

View File

@@ -35,8 +35,6 @@ package sonia.scm.plugin;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Module;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collection;

View File

@@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.ConfigurationException;
import sonia.scm.io.CommandResult;
import sonia.scm.io.ExtendedCommand;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.store.ConfigurationStoreFactory;
import java.io.File;
@@ -67,11 +68,14 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
LoggerFactory.getLogger(AbstractSimpleRepositoryHandler.class);
private final RepositoryLocationResolver repositoryLocationResolver;
private final PluginLoader pluginLoader;
public AbstractSimpleRepositoryHandler(ConfigurationStoreFactory storeFactory,
RepositoryLocationResolver repositoryLocationResolver) {
RepositoryLocationResolver repositoryLocationResolver,
PluginLoader pluginLoader) {
super(storeFactory);
this.repositoryLocationResolver = repositoryLocationResolver;
this.pluginLoader = pluginLoader;
}
@Override
@@ -155,7 +159,7 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
String content = defaultContent;
try {
URL url = Resources.getResource(resource);
URL url = pluginLoader.getUberClassLoader().getResource(resource);
if (url != null) {
content = Resources.toString(url, Charsets.UTF_8);

View File

@@ -37,7 +37,6 @@ package sonia.scm.repository;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import org.apache.commons.collections.CollectionUtils;
import sonia.scm.security.PermissionObject;
import javax.xml.bind.annotation.XmlAccessType;
@@ -47,9 +46,10 @@ import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Collections.unmodifiableSet;
//~--- JDK imports ------------------------------------------------------------
@@ -68,7 +68,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
private boolean groupPermission = false;
private String name;
@XmlElement(name = "verb")
private Collection<String> verbs;
private Set<String> verbs;
/**
* Constructs a new {@link RepositoryPermission}.
@@ -79,7 +79,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
public RepositoryPermission(String name, Collection<String> verbs, boolean groupPermission)
{
this.name = name;
this.verbs = unmodifiableCollection(new LinkedHashSet<>(verbs));
this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs));
this.groupPermission = groupPermission;
}
@@ -109,7 +109,8 @@ public class RepositoryPermission implements PermissionObject, Serializable
final RepositoryPermission other = (RepositoryPermission) obj;
return Objects.equal(name, other.name)
&& CollectionUtils.isEqualCollection(verbs, other.verbs)
&& verbs.containsAll(other.verbs)
&& verbs.size() == other.verbs.size()
&& Objects.equal(groupPermission, other.groupPermission);
}
@@ -209,6 +210,6 @@ public class RepositoryPermission implements PermissionObject, Serializable
*/
public void setVerbs(Collection<String> verbs)
{
this.verbs = verbs;
this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs));
}
}

View File

@@ -5,7 +5,6 @@ import com.google.common.base.Objects;
import com.google.common.base.Strings;
import sonia.scm.Validateable;
import sonia.scm.repository.Person;
import sonia.scm.util.Util;
import java.io.Serializable;

View File

@@ -39,7 +39,6 @@ import com.google.common.base.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;

View File

@@ -34,8 +34,6 @@ package sonia.scm.security;
//~--- JDK imports ------------------------------------------------------------
import com.google.common.base.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;

View File

@@ -0,0 +1,88 @@
package sonia.scm.util;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Optional;
import static com.google.common.base.Preconditions.checkArgument;
public final class Comparables {
private static final CacheLoader<Class, BeanInfo> beanInfoCacheLoader = new CacheLoader<Class, BeanInfo>() {
@Override
public BeanInfo load(Class type) throws IntrospectionException {
return Introspector.getBeanInfo(type);
}
};
private static final LoadingCache<Class, BeanInfo> beanInfoCache = CacheBuilder.newBuilder()
.maximumSize(50) // limit the cache to avoid consuming to much memory on miss usage
.build(beanInfoCacheLoader);
private Comparables() {
}
public static <T> Comparator<T> comparator(Class<T> type, String sortBy) {
BeanInfo info = createBeanInfo(type);
PropertyDescriptor propertyDescriptor = findPropertyDescriptor(sortBy, info);
Method readMethod = propertyDescriptor.getReadMethod();
checkIfPropertyIsComparable(readMethod, sortBy);
return new MethodComparator<>(readMethod);
}
private static void checkIfPropertyIsComparable(Method readMethod, String sortBy) {
checkArgument(isReturnTypeComparable(readMethod), "property %s is not comparable", sortBy);
}
private static boolean isReturnTypeComparable(Method readMethod) {
return Comparable.class.isAssignableFrom(readMethod.getReturnType());
}
private static PropertyDescriptor findPropertyDescriptor(String sortBy, BeanInfo info) {
PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors();
Optional<PropertyDescriptor> sortByPropertyDescriptor = Arrays.stream(propertyDescriptors)
.filter(p -> p.getName().equals(sortBy))
.findFirst();
return sortByPropertyDescriptor.orElseThrow(() -> new IllegalArgumentException("could not find property " + sortBy));
}
private static <T> BeanInfo createBeanInfo(Class<T> type) {
return beanInfoCache.getUnchecked(type);
}
private static class MethodComparator<T> implements Comparator<T> {
private final Method readMethod;
private MethodComparator(Method readMethod) {
this.readMethod = readMethod;
}
@Override
@SuppressWarnings("unchecked")
public int compare(T left, T right) {
try {
Comparable leftResult = (Comparable) readMethod.invoke(left);
Comparable rightResult = (Comparable) readMethod.invoke(right);
return leftResult.compareTo(rightResult);
} catch (IllegalAccessException | InvocationTargetException ex) {
throw new IllegalArgumentException("failed to invoke read method", ex);
}
}
}
}

View File

@@ -11,51 +11,51 @@ import java.util.Optional;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class LinkAppenderMapperTest {
class HalAppenderMapperTest {
@Mock
private LinkAppender appender;
private HalAppender appender;
private LinkEnricherRegistry registry;
private LinkAppenderMapper mapper;
private HalEnricherRegistry registry;
private HalAppenderMapper mapper;
@BeforeEach
void beforeEach() {
registry = new LinkEnricherRegistry();
mapper = new LinkAppenderMapper();
registry = new HalEnricherRegistry();
mapper = new HalAppenderMapper();
mapper.setRegistry(registry);
}
@Test
void shouldAppendSimpleLink() {
registry.register(String.class, (ctx, appender) -> appender.appendOne("42", "https://hitchhiker.com"));
registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com"));
mapper.appendLinks(appender, "hello");
mapper.applyEnrichers(appender, "hello");
verify(appender).appendOne("42", "https://hitchhiker.com");
verify(appender).appendLink("42", "https://hitchhiker.com");
}
@Test
void shouldCallMultipleEnrichers() {
registry.register(String.class, (ctx, appender) -> appender.appendOne("42", "https://hitchhiker.com"));
registry.register(String.class, (ctx, appender) -> appender.appendOne("21", "https://scm.hitchhiker.com"));
registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com"));
registry.register(String.class, (ctx, appender) -> appender.appendLink("21", "https://scm.hitchhiker.com"));
mapper.appendLinks(appender, "hello");
mapper.applyEnrichers(appender, "hello");
verify(appender).appendOne("42", "https://hitchhiker.com");
verify(appender).appendOne("21", "https://scm.hitchhiker.com");
verify(appender).appendLink("42", "https://hitchhiker.com");
verify(appender).appendLink("21", "https://scm.hitchhiker.com");
}
@Test
void shouldAppendLinkByUsingSourceFromContext() {
registry.register(String.class, (ctx, appender) -> {
Optional<String> rel = ctx.oneByType(String.class);
appender.appendOne(rel.get(), "https://hitchhiker.com");
appender.appendLink(rel.get(), "https://hitchhiker.com");
});
mapper.appendLinks(appender, "42");
mapper.applyEnrichers(appender, "42");
verify(appender).appendOne("42", "https://hitchhiker.com");
verify(appender).appendLink("42", "https://hitchhiker.com");
}
@Test
@@ -63,12 +63,12 @@ class LinkAppenderMapperTest {
registry.register(Integer.class, (ctx, appender) -> {
Optional<Integer> rel = ctx.oneByType(Integer.class);
Optional<String> href = ctx.oneByType(String.class);
appender.appendOne(String.valueOf(rel.get()), href.get());
appender.appendLink(String.valueOf(rel.get()), href.get());
});
mapper.appendLinks(appender, Integer.valueOf(42), "https://hitchhiker.com");
mapper.applyEnrichers(appender, Integer.valueOf(42), "https://hitchhiker.com");
verify(appender).appendOne("42", "https://hitchhiker.com");
verify(appender).appendLink("42", "https://hitchhiker.com");
}
}

View File

@@ -7,17 +7,17 @@ import org.junit.jupiter.api.Test;
import java.util.NoSuchElementException;
class LinkEnricherContextTest {
class HalEnricherContextTest {
@Test
void shouldCreateContextFromSingleObject() {
LinkEnricherContext context = LinkEnricherContext.of("hello");
HalEnricherContext context = HalEnricherContext.of("hello");
assertThat(context.oneByType(String.class)).contains("hello");
}
@Test
void shouldCreateContextFromMultipleObjects() {
LinkEnricherContext context = LinkEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L));
HalEnricherContext context = HalEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L));
assertThat(context.oneByType(String.class)).contains("hello");
assertThat(context.oneByType(Integer.class)).contains(42);
assertThat(context.oneByType(Long.class)).contains(21L);
@@ -25,19 +25,19 @@ class LinkEnricherContextTest {
@Test
void shouldReturnEmptyOptionalForUnknownTypes() {
LinkEnricherContext context = LinkEnricherContext.of();
HalEnricherContext context = HalEnricherContext.of();
assertThat(context.oneByType(String.class)).isNotPresent();
}
@Test
void shouldReturnRequiredObject() {
LinkEnricherContext context = LinkEnricherContext.of("hello");
HalEnricherContext context = HalEnricherContext.of("hello");
assertThat(context.oneRequireByType(String.class)).isEqualTo("hello");
}
@Test
void shouldThrowAnNoSuchElementExceptionForUnknownTypes() {
LinkEnricherContext context = LinkEnricherContext.of();
HalEnricherContext context = HalEnricherContext.of();
assertThrows(NoSuchElementException.class, () -> context.oneRequireByType(String.class));
}

View File

@@ -5,54 +5,54 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class LinkEnricherRegistryTest {
class HalEnricherRegistryTest {
private LinkEnricherRegistry registry;
private HalEnricherRegistry registry;
@BeforeEach
void setUpObjectUnderTest() {
registry = new LinkEnricherRegistry();
registry = new HalEnricherRegistry();
}
@Test
void shouldRegisterTheEnricher() {
SampleLinkEnricher enricher = new SampleLinkEnricher();
SampleHalEnricher enricher = new SampleHalEnricher();
registry.register(String.class, enricher);
Iterable<LinkEnricher> enrichers = registry.allByType(String.class);
Iterable<HalEnricher> enrichers = registry.allByType(String.class);
assertThat(enrichers).containsOnly(enricher);
}
@Test
void shouldRegisterMultipleEnrichers() {
SampleLinkEnricher one = new SampleLinkEnricher();
SampleHalEnricher one = new SampleHalEnricher();
registry.register(String.class, one);
SampleLinkEnricher two = new SampleLinkEnricher();
SampleHalEnricher two = new SampleHalEnricher();
registry.register(String.class, two);
Iterable<LinkEnricher> enrichers = registry.allByType(String.class);
Iterable<HalEnricher> enrichers = registry.allByType(String.class);
assertThat(enrichers).containsOnly(one, two);
}
@Test
void shouldRegisterEnrichersForDifferentTypes() {
SampleLinkEnricher one = new SampleLinkEnricher();
SampleHalEnricher one = new SampleHalEnricher();
registry.register(String.class, one);
SampleLinkEnricher two = new SampleLinkEnricher();
SampleHalEnricher two = new SampleHalEnricher();
registry.register(Integer.class, two);
Iterable<LinkEnricher> enrichers = registry.allByType(String.class);
Iterable<HalEnricher> enrichers = registry.allByType(String.class);
assertThat(enrichers).containsOnly(one);
enrichers = registry.allByType(Integer.class);
assertThat(enrichers).containsOnly(two);
}
private static class SampleLinkEnricher implements LinkEnricher {
private static class SampleHalEnricher implements HalEnricher {
@Override
public void enrich(LinkEnricherContext context, LinkAppender appender) {
public void enrich(HalEnricherContext context, HalAppender appender) {
}
}

View File

@@ -0,0 +1,87 @@
package sonia.scm.filter;
import com.google.inject.util.Providers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.WriterInterceptorContext;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.GZIPOutputStream;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GZipResponseFilterTest {
@Mock
private HttpServletRequest request;
@Mock
private WriterInterceptorContext context;
@Mock
private MultivaluedMap<String,Object> headers;
private GZipResponseFilter filter;
@BeforeEach
void setupObjectUnderTest() {
filter = new GZipResponseFilter(Providers.of(request));
}
@Test
void shouldSkipGZipCompression() throws IOException {
when(request.getHeader(HttpHeaders.ACCEPT_ENCODING)).thenReturn("deflate, br");
filter.aroundWriteTo(context);
verifySkipped();
}
@Test
void shouldSkipGZipCompressionWithoutAcceptEncodingHeader() throws IOException {
filter.aroundWriteTo(context);
verifySkipped();
}
private void verifySkipped() throws IOException {
verify(context, never()).getOutputStream();
verify(context).proceed();
}
@Nested
class AcceptGZipEncoding {
@BeforeEach
void setUpContext() {
when(request.getHeader(HttpHeaders.ACCEPT_ENCODING)).thenReturn("gzip, deflate, br");
when(context.getHeaders()).thenReturn(headers);
when(context.getOutputStream()).thenReturn(new ByteArrayOutputStream());
}
@Test
void shouldEncode() throws IOException {
filter.aroundWriteTo(context);
verify(headers).remove(HttpHeaders.CONTENT_LENGTH);
verify(headers).add(HttpHeaders.CONTENT_ENCODING, "gzip");
verify(context).setOutputStream(any(GZIPOutputStream.class));
verify(context, times(2)).setOutputStream(any(OutputStream.class));
}
}
}

View File

@@ -36,7 +36,7 @@ import com.google.common.io.ByteSource;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

View File

@@ -46,4 +46,13 @@ class RepositoryPermissionTest {
assertThat(permission1).isNotEqualTo(permission2);
}
@Test
void shouldBeEqualWithRedundantVerbs() {
RepositoryPermission permission1 = new RepositoryPermission("name1", asList("one", "two"), false);
RepositoryPermission permission2 = new RepositoryPermission("name1", asList("one", "two"), false);
permission2.setVerbs(asList("one", "two", "two"));
assertThat(permission1).isEqualTo(permission2);
}
}

View File

@@ -1,7 +1,6 @@
package sonia.scm.security;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.UnknownAccountException;

View File

@@ -0,0 +1,57 @@
package sonia.scm.util;
import org.junit.jupiter.api.Test;
import java.util.Comparator;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class ComparablesTest {
@Test
void shouldCompare() {
One a = new One("a");
One b = new One("b");
Comparator<One> comparable = Comparables.comparator(One.class, "value");
assertThat(comparable.compare(a, b)).isEqualTo(-1);
}
@Test
void shouldThrowAnExceptionForNonExistingField() {
assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "awesome"));
}
@Test
void shouldThrowAnExceptionForNonComparableField() {
assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "nonComparable"));
}
@Test
void shouldThrowAnExceptionIfTheFieldHasNoGetter() {
assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "incredible"));
}
private static class One {
private String value;
private String incredible;
private NonComparable nonComparable;
One(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public NonComparable getNonComparable() {
return nonComparable;
}
}
private static class NonComparable {}
}

View File

@@ -37,9 +37,6 @@ package sonia.scm.store;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.security.KeyGenerator;

View File

@@ -38,7 +38,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.GenericDAO;
import sonia.scm.ModelObject;
import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.store.ConfigurationStore;
import java.util.Collection;

View File

@@ -1,6 +1,5 @@
package sonia.scm.xml;
import com.google.common.base.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -36,7 +36,6 @@ package sonia.scm.it;
import org.apache.http.HttpStatus;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

View File

@@ -5,10 +5,8 @@ import io.restassured.response.Response;
import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.user.User;
import sonia.scm.web.VndMediaType;
import java.net.ConnectException;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

View File

@@ -18,8 +18,6 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
/**
* RESTful Web Service Resource to manage the configuration of the git plugin.
*/

View File

@@ -44,6 +44,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.spi.GitRepositoryServiceProvider;
import sonia.scm.schedule.Scheduler;
import sonia.scm.schedule.Task;
@@ -103,9 +104,10 @@ public class GitRepositoryHandler
public GitRepositoryHandler(ConfigurationStoreFactory storeFactory,
Scheduler scheduler,
RepositoryLocationResolver repositoryLocationResolver,
GitWorkdirFactory workdirFactory)
GitWorkdirFactory workdirFactory,
PluginLoader pluginLoader)
{
super(storeFactory, repositoryLocationResolver);
super(storeFactory, repositoryLocationResolver, pluginLoader);
this.scheduler = scheduler;
this.workdirFactory = workdirFactory;
}

View File

@@ -35,7 +35,6 @@ package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.RepositoryNotFoundException;

View File

@@ -2,7 +2,7 @@
import React from "react";
import {apiClient, BranchSelector, ErrorPage, Loading, SubmitButton} from "@scm-manager/ui-components";
import {apiClient, BranchSelector, ErrorPage, Loading, Subtitle, SubmitButton} from "@scm-manager/ui-components";
import type {Branch, Repository} from "@scm-manager/ui-types";
import {translate} from "react-i18next";
@@ -113,6 +113,7 @@ class RepositoryConfig extends React.Component<Props, State> {
if (!(loadingBranches || loadingDefaultBranch)) {
return (
<>
<Subtitle subtitle={t("scm-git-plugin.repo-config.title")}/>
{this.renderBranchChangedNotification()}
<form onSubmit={this.submit}>
<BranchSelector
@@ -127,6 +128,7 @@ class RepositoryConfig extends React.Component<Props, State> {
disabled={!this.state.selectedBranchName}
/>
</form>
<hr />
</>
);
} else {

View File

@@ -27,14 +27,9 @@ binder.bind(
);
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);
cfgBinder.bindRepository(
"/configuration",
"scm-git-plugin.repo-config.link",
"configuration",
RepositoryConfig
);
// global config
binder.bind("repo-config.route", RepositoryConfig, gitPredicate);
// global config
cfgBinder.bindGlobal(
"/git",
"scm-git-plugin.config.link",

View File

@@ -1,9 +1,56 @@
{
"scm-git-plugin": {
"information": {
"clone" : "Repository Klonen",
"create" : "Neue Repository erstellen",
"replace" : "Eine existierende Repository aktualisieren"
"clone" : "Repository klonen",
"create" : "Neues Repository erstellen",
"replace" : "Ein bestehendes Repository aktualisieren",
"merge": {
"heading": "Merge des Source Branch in den Target Branch",
"checkout": "1. Sicherstellen, dass der Workspace aufgeräumt ist und der Target Branch ausgecheckt wurde.",
"update": "2. Update Workspace",
"merge": "3. Merge Source Branch",
"resolve": "4. Merge Konflikte auflösen und korrigierte Dateien dem Index hinzufügen.",
"commit": "5. Commit",
"push": "6. Push des Merge"
}
},
"config": {
"link": "Git",
"title": "Git Konfiguration",
"gcExpression": "GC Cron Ausdruck",
"gcExpressionHelpText": "Benutze Quartz Cron Ausdrücke (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK), um git GC regelmäßig auszuführen.",
"nonFastForwardDisallowed": "Deaktiviere \"Non Fast-Forward\"",
"nonFastForwardDisallowedHelpText": "Git Pushes ablehnen, die nicht \"fast-forward\" sind, wie \"--force\".",
"disabled": "Deaktiviert",
"disabledHelpText": "Aktiviere oder deaktiviere das Git Plugin",
"submit": "Speichern"
},
"repo-config": {
"link": "Konfiguration",
"title": "Git Einstellungen",
"default-branch": "Standard Branch",
"submit": "Speichern",
"error": {
"title": "Fehler",
"subtitle": "Ein Fehler ist aufgetreten."
},
"success": "Der standard Branch wurde geändert!"
}
},
"permissions" : {
"configuration": {
"read": {
"git": {
"displayName": "Git Konfiguration lesen",
"description": "Darf die git Konfiguration lesen."
}
},
"write": {
"git": {
"displayName": "Git Konfiguration schreiben",
"description": "Darf die git Konfiguration verändern."
}
}
}
}
}

View File

@@ -27,6 +27,7 @@
},
"repo-config": {
"link": "Configuration",
"title": "Git Settings",
"default-branch": "Default branch",
"submit": "Submit",
"error": {

View File

@@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.GitConfig;
import static org.junit.Assert.*;

View File

@@ -17,7 +17,7 @@ import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.GitRepositoryHandler;
@@ -29,6 +29,7 @@ import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.web.GitVndMediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -100,7 +101,7 @@ public class GitConfigResourceTest {
@Test
@SubjectAware(username = "readWrite")
public void shouldGetGitConfig() throws URISyntaxException {
public void shouldGetGitConfig() throws URISyntaxException, UnsupportedEncodingException {
MockHttpResponse response = get();
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
@@ -115,7 +116,7 @@ public class GitConfigResourceTest {
@Test
@SubjectAware(username = "readWrite")
public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException {
public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException, UnsupportedEncodingException {
when(repositoryHandler.getConfig()).thenReturn(null);
MockHttpResponse response = get();
@@ -126,7 +127,7 @@ public class GitConfigResourceTest {
@Test
@SubjectAware(username = "readOnly")
public void shouldGetGitConfigWithoutUpdateLink() throws URISyntaxException {
public void shouldGetGitConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException {
MockHttpResponse response = get();
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
@@ -159,7 +160,7 @@ public class GitConfigResourceTest {
@Test
@SubjectAware(username = "writeOnly")
public void shouldReadDefaultRepositoryConfig() throws URISyntaxException {
public void shouldReadDefaultRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException {
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X"));
MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X");
@@ -176,7 +177,7 @@ public class GitConfigResourceTest {
@Test
@SubjectAware(username = "readOnly")
public void shouldNotHaveUpdateLinkForReadOnlyUser() throws URISyntaxException {
public void shouldNotHaveUpdateLinkForReadOnlyUser() throws URISyntaxException, UnsupportedEncodingException {
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X"));
MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X");
@@ -193,7 +194,7 @@ public class GitConfigResourceTest {
@Test
@SubjectAware(username = "writeOnly")
public void shouldReadStoredRepositoryConfig() throws URISyntaxException {
public void shouldReadStoredRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException {
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X"));
GitRepositoryConfig gitRepositoryConfig = new GitRepositoryConfig();
gitRepositoryConfig.setDefaultBranch("test");

View File

@@ -11,10 +11,9 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.GitConfig;
import java.io.File;
import java.net.URI;
import static org.junit.Assert.assertEquals;

View File

@@ -14,9 +14,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.JsonEnricherContext;
import sonia.scm.web.VndMediaType;

View File

@@ -40,7 +40,7 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.File;
import java.io.IOException;

View File

@@ -94,7 +94,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
RepositoryLocationResolver locationResolver,
File directory) {
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
scheduler, locationResolver, gitWorkdirFactory);
scheduler, locationResolver, gitWorkdirFactory, null);
repositoryHandler.init(contextProvider);
GitConfig config = new GitConfig();
@@ -108,7 +108,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test
public void getDirectory() {
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
scheduler, locationResolver, gitWorkdirFactory);
scheduler, locationResolver, gitWorkdirFactory, null);
GitConfig config = new GitConfig();
config.setDisabled(false);
config.setGcExpression("gc exp");

View File

@@ -35,10 +35,8 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.After;
import org.junit.Before;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.store.InMemoryConfigurationStore;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
/**

View File

@@ -35,11 +35,9 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Test;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.BlameLine;
import sonia.scm.repository.BlameResult;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
import java.io.IOException;

View File

@@ -32,11 +32,9 @@
package sonia.scm.repository.spi;
import org.junit.Test;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
import java.io.IOException;
import java.util.Collection;

View File

@@ -36,10 +36,8 @@ package sonia.scm.repository.spi;
import com.google.common.io.Files;
import org.junit.Test;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.ClearRepositoryCacheEvent;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.Modifications;

View File

@@ -42,7 +42,7 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryHandler;

View File

@@ -35,14 +35,12 @@ package sonia.scm.installer;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.util.IOUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
import sonia.scm.net.ahc.AdvancedHttpClient;
/**

View File

@@ -39,7 +39,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.io.INIConfiguration;
import sonia.scm.io.INIConfigurationReader;
import sonia.scm.io.INIConfigurationWriter;
import sonia.scm.io.INISection;
import sonia.scm.util.ValidationUtil;

View File

@@ -51,6 +51,7 @@ import sonia.scm.io.INIConfigurationReader;
import sonia.scm.io.INIConfigurationWriter;
import sonia.scm.io.INISection;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.spi.HgRepositoryServiceProvider;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.util.IOUtil;
@@ -111,9 +112,10 @@ public class HgRepositoryHandler
@Inject
public HgRepositoryHandler(ConfigurationStoreFactory storeFactory,
Provider<HgContext> hgContextProvider,
RepositoryLocationResolver repositoryLocationResolver)
RepositoryLocationResolver repositoryLocationResolver,
PluginLoader pluginLoader)
{
super(storeFactory, repositoryLocationResolver);
super(storeFactory, repositoryLocationResolver, pluginLoader);
this.hgContextProvider = hgContextProvider;
try

View File

@@ -41,7 +41,6 @@ import com.aragost.javahg.internals.AbstractCommand;
import com.aragost.javahg.internals.HgInputStream;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.SubRepository;
@@ -52,7 +51,6 @@ import java.io.IOException;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
/**
* Mercurial command to list files of a repository.

View File

@@ -44,7 +44,6 @@ import sonia.scm.web.filter.PermissionFilter;
import sonia.scm.repository.HgRepositoryHandler;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.util.Set;

View File

@@ -1,9 +1,48 @@
{
"scm-hg-plugin": {
"information": {
"clone" : "Repository Klonen",
"create" : "Neue Repository erstellen",
"replace" : "Eine existierende Repository aktualisieren"
"clone" : "Repository klonen",
"create" : "Neues Repository erstellen",
"replace" : "Ein bestehendes Repository aktualisieren"
},
"config": {
"link": "Mercurial",
"title": "Mercurial Konfiguration",
"hgBinary": "HG Binary",
"hgBinaryHelpText": "Pfad des Mercurial Binary.",
"pythonBinary": "Python Binary",
"pythonBinaryHelpText": "Pfad des Python binary.",
"pythonPath": "Python Module Such Pfad",
"pythonPathHelpText": "Python Module Such Pfad (PYTHONPATH).",
"encoding": "Encoding",
"encodingHelpText": "Repository Encoding.",
"useOptimizedBytecode": "Optimized Bytecode (.pyo)",
"useOptimizedBytecodeHelpText": "Verwende den Python '-O' Switch.",
"showRevisionInId": "Revision anzeigen",
"showRevisionInIdHelpText": "Die Revision als Teil der Node ID anzeigen.",
"enableHttpPostArgs": "HttpPostArgs Protocol aktivieren",
"enableHttpPostArgsHelpText": "Aktiviert das experimentelle HttpPostArgs Protokoll von Mercurial. Das HttpPostArgs Protokoll verwendet den Post Request Body anstatt des HTTP Headers um Meta Informationen zu versenden. Dieses Vorgehen reduziert die Header Größe der Mercurial Requests. HttpPostArgs wird seit Mercurial 3.8 unterstützt.",
"disableHookSSLValidation": "SSL Validierung für Hooks deaktivieren",
"disableHookSSLValidationHelpText": "Deaktiviert die Validierung von SSL Zertifikaten für den Mercurial Hook, der die Repositoryänderungen wieder zurück an den SCM-Manager leitet. Diese Option sollte nur benutzt werden, wenn der SCM-Manager ein selbstsigniertes Zertifikat verwendet.",
"disabled": "Deaktiviert",
"disabledHelpText": "Aktiviert oder deaktiviert das Mercurial Plugin.",
"required": "Dieser Konfigurationswert wird benötigt"
}
},
"permissions" : {
"configuration": {
"read": {
"hg": {
"displayName": "Mercurial Konfiguration lesen",
"description": "Darf die Mercurial Konfiguration lesen"
}
},
"write": {
"hg": {
"displayName": "Mercurial Konfiguration schreiben",
"description": "Darf die Mercurial Konfiguration verändern"
}
}
}
}
}

View File

@@ -21,9 +21,9 @@
"showRevisionInId": "Show Revision",
"showRevisionInIdHelpText": "Show revision as part of the node id.",
"enableHttpPostArgs": "Enable HttpPostArgs Protocol",
"enableHttpPostArgsHelpText": "Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. This option should only be used, if SCM-Manager uses a self signed certificate.",
"enableHttpPostArgsHelpText": "Enables the experimental HttpPostArgs Protocol of mercurial. The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers. This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.",
"disableHookSSLValidation": "Disable SSL Validation on Hooks",
"disableHookSSLValidationHelpText": "Enables the experimental HttpPostArgs Protocol of mercurial. The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers. This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.",
"disableHookSSLValidationHelpText": "Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. This option should only be used, if SCM-Manager uses a self signed certificate.",
"disabled": "Disabled",
"disabledHelpText": "Enable or disable the Mercurial plugin.",
"required": "This configuration value is required"

View File

@@ -14,7 +14,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.web.HgVndMediaType;

View File

@@ -3,11 +3,9 @@ package sonia.scm.api.v2.resources;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.HgConfig;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

View File

@@ -14,7 +14,7 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import javax.inject.Provider;
import javax.servlet.http.HttpServletResponse;

View File

@@ -6,7 +6,7 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import java.net.URI;
import java.util.Arrays;

View File

@@ -17,7 +17,7 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.installer.HgPackage;
import sonia.scm.installer.HgPackageReader;
import sonia.scm.net.ahc.AdvancedHttpClient;

View File

@@ -6,7 +6,7 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.installer.HgPackage;
import sonia.scm.installer.HgPackages;

View File

@@ -16,15 +16,15 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.web.HgVndMediaType;
import javax.inject.Provider;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -99,7 +99,7 @@ public class HgConfigResourceTest {
@Test
@SubjectAware(username = "readWrite")
public void shouldGetHgConfigEvenWhenItsEmpty() throws URISyntaxException {
public void shouldGetHgConfigEvenWhenItsEmpty() throws URISyntaxException, UnsupportedEncodingException {
when(repositoryHandler.getConfig()).thenReturn(null);
MockHttpResponse response = get();
@@ -110,7 +110,7 @@ public class HgConfigResourceTest {
@Test
@SubjectAware(username = "readOnly")
public void shouldGetHgConfigWithoutUpdateLink() throws URISyntaxException {
public void shouldGetHgConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException {
MockHttpResponse response = get();
assertEquals(HttpServletResponse.SC_OK, response.getStatus());

View File

@@ -3,8 +3,6 @@ package sonia.scm.api.v2.resources;
import sonia.scm.installer.HgPackage;
import sonia.scm.repository.HgConfig;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

View File

@@ -11,7 +11,7 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.HgConfig;
import java.net.URI;

View File

@@ -77,7 +77,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) {
HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver);
HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null);
handler.init(contextProvider);
HgTestUtil.checkForSkip(handler);
@@ -87,7 +87,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test
public void getDirectory() {
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver);
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null);
HgConfig hgConfig = new HgConfig();
hgConfig.setHgBinary("hg");

View File

@@ -105,7 +105,7 @@ public final class HgTestUtil
RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(context, repoDao, new InitialRepositoryLocationResolver());
HgRepositoryHandler handler =
new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver);
new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null);
Path repoDir = directory.toPath();
when(repoDao.getPath(any())).thenReturn(repoDir);
handler.init(context);

View File

@@ -8,7 +8,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.mockito.Matchers.anyInt;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

View File

@@ -48,8 +48,6 @@ import javax.servlet.http.HttpServletRequest;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static sonia.scm.web.WireProtocolRequestMockFactory.CMDS_HEADS_KNOWN_NODES;
import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.BOOKMARKS;
import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.PHASES;

View File

@@ -37,7 +37,7 @@ import com.google.common.collect.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;

View File

@@ -53,9 +53,9 @@ import sonia.scm.io.INIConfigurationWriter;
import sonia.scm.io.INISection;
import sonia.scm.logging.SVNKitLogger;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.repository.spi.SvnRepositoryServiceProvider;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.util.Util;
@@ -97,9 +97,10 @@ public class SvnRepositoryHandler
@Inject
public SvnRepositoryHandler(ConfigurationStoreFactory storeFactory,
HookEventFacade eventFacade,
RepositoryLocationResolver repositoryLocationResolver)
RepositoryLocationResolver repositoryLocationResolver,
PluginLoader pluginLoader)
{
super(storeFactory, repositoryLocationResolver);
super(storeFactory, repositoryLocationResolver, pluginLoader);
// register logger
SVNDebugLog.setDefaultLog(new SVNKitLogger());

View File

@@ -36,7 +36,6 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tmatesoft.svn.core.SVNDirEntry;
@@ -53,7 +52,6 @@ import sonia.scm.repository.SvnUtil;
import sonia.scm.util.Util;
import java.util.Collection;
import java.util.List;
//~--- JDK imports ------------------------------------------------------------

View File

@@ -4,6 +4,8 @@ import lombok.extern.slf4j.Slf4j;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.admin.SVNLookClient;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Repository;
@@ -19,23 +21,45 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif
super(context, repository);
}
@Override
@SuppressWarnings("unchecked")
public Modifications getModifications(String revision) {
Modifications modifications = null;
log.debug("get modifications {}", revision);
public Modifications getModifications(String revisionOrTransactionId) {
Modifications modifications;
try {
long revisionNumber = SvnUtil.parseRevision(revision, repository);
if (SvnUtil.isTransactionEntryId(revisionOrTransactionId)) {
modifications = getModificationsFromTransaction(SvnUtil.getTransactionId(revisionOrTransactionId));
} else {
modifications = getModificationFromRevision(revisionOrTransactionId);
}
return modifications;
} catch (SVNException ex) {
throw new InternalRepositoryException(
repository,
"failed to get svn modifications for " + revisionOrTransactionId,
ex
);
}
}
@SuppressWarnings("unchecked")
private Modifications getModificationFromRevision(String revision) throws SVNException {
log.debug("get svn modifications from revision: {}", revision);
long revisionNumber = SvnUtil.getRevisionNumber(revision, repository);
SVNRepository repo = open();
Collection<SVNLogEntry> entries = repo.log(null, null, revisionNumber,
revisionNumber, true, true);
if (Util.isNotEmpty(entries)) {
modifications = SvnUtil.createModifications(entries.iterator().next(), revision);
return SvnUtil.createModifications(entries.iterator().next(), revision);
}
} catch (SVNException ex) {
throw new InternalRepositoryException(repository, "could not open repository", ex);
return null;
}
private Modifications getModificationsFromTransaction(String transaction) throws SVNException {
log.debug("get svn modifications from transaction: {}", transaction);
final Modifications modifications = new Modifications();
SVNLookClient client = SVNClientManager.newInstance().getLookClient();
client.doGetChanged(context.getDirectory(), transaction,
e -> SvnUtil.appendModification(modifications, e.getType(), e.getPath()), true);
return modifications;
}
@@ -44,5 +68,4 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif
return getModifications(request.getRevision());
}
}

View File

@@ -2,6 +2,41 @@
"scm-svn-plugin": {
"information": {
"checkout": "Repository auschecken"
},
"config": {
"link": "Subversion",
"title": "Subversion Konfiguration",
"compatibility": "Version Kompatibilität",
"compatibilityHelpText": "Gibt an, mit welcher Subversion Version die Repositories kompatibel sind.",
"compatibility-values": {
"none": "Keine Kompatibilität",
"pre14": "Vor 1.4 kompatibel",
"pre15": "Vor 1.5 kompatibel",
"pre16": "Vor 1.6 kompatibel",
"pre17": "Vor 1.7 kompatibel",
"with17": "Mit 1.7 kompatibel"
},
"enabledGZip": "GZip Compression aktivieren",
"enabledGZipHelpText": "Aktiviert GZip Kompression für SVN Responses",
"disabled": "Deaktiviert",
"disabledHelpText": "Aktiviert oder deaktiviert das SVN Plugin",
"required": "Dieser Konfigurationswert wird benötigt"
}
},
"permissions": {
"configuration": {
"read": {
"svn": {
"displayName": "Subversion Konfiguration lesen",
"description": "Darf die Subversion Konfiguration lesen"
}
},
"write": {
"svn": {
"displayName": "Subversion Konfiguration schreiben",
"description": "Darf die Subversion Konfiguration verändern"
}
}
}
}
}

View File

@@ -7,7 +7,7 @@
"link": "Subversion",
"title": "Subversion Configuration",
"compatibility": "Version Compatibility",
"compatibilityHelpText": "Specifies with which subversion version repositories are compatible.",
"compatibilityHelpText": "Specifies with which Subversion version repositories are compatible.",
"compatibility-values": {
"none": "No compatibility",
"pre14": "Pre 1.4 Compatible",
@@ -17,9 +17,9 @@
"with17": "With 1.7 Compatible"
},
"enabledGZip": "Enable GZip Compression",
"enabledGZipHelpText": "Enable GZip compression for svn responses.",
"enabledGZipHelpText": "Enable GZip compression for SVN responses.",
"disabled": "Disabled",
"disabledHelpText": "Enable or disable the Git plugin",
"disabledHelpText": "Enable or disable the SVN plugin",
"required": "This configuration value is required"
}
},

View File

@@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.Compatibility;
import sonia.scm.repository.SvnConfig;

View File

@@ -16,14 +16,14 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.SvnConfig;
import sonia.scm.repository.SvnRepositoryHandler;
import sonia.scm.web.SvnVndMediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -98,7 +98,7 @@ public class SvnConfigResourceTest {
@Test
@SubjectAware(username = "readOnly")
public void shouldGetSvnConfigWithoutUpdateLink() throws URISyntaxException {
public void shouldGetSvnConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException {
MockHttpResponse response = get();
assertEquals(HttpServletResponse.SC_OK, response.getStatus());

View File

@@ -11,11 +11,10 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.Compatibility;
import sonia.scm.repository.SvnConfig;
import java.io.File;
import java.net.URI;
import static org.junit.Assert.assertEquals;

View File

@@ -32,14 +32,10 @@
package sonia.scm.repository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import java.io.File;
@@ -47,7 +43,7 @@ import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@@ -93,7 +89,7 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory,
RepositoryLocationResolver locationResolver,
File directory) {
SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, null, locationResolver);
SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, null, locationResolver, null);
handler.init(contextProvider);
@@ -109,7 +105,7 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
public void getDirectory() {
when(factory.withType(any())).thenCallRealMethod();
SvnRepositoryHandler repositoryHandler = new SvnRepositoryHandler(factory,
facade, locationResolver);
facade, locationResolver, null);
SvnConfig svnConfig = new SvnConfig();
repositoryHandler.setConfig(svnConfig);

View File

@@ -39,8 +39,6 @@ import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.Modifications;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

View File

@@ -59,7 +59,7 @@ public class DummyRepositoryHandler
private final Set<String> existingRepoNames = new HashSet<>();
public DummyRepositoryHandler(ConfigurationStoreFactory storeFactory, RepositoryLocationResolver repositoryLocationResolver) {
super(storeFactory, repositoryLocationResolver);
super(storeFactory, repositoryLocationResolver, null);
}
@Override

View File

@@ -13,10 +13,11 @@ const styles = {
minWidthOfLabel: {
minWidth: "4.5rem"
},
wrapper: {
padding: "1rem 1.5rem 0.25rem 1.5rem",
border: "1px solid #eee",
borderRadius: "5px 5px 0 0"
labelSizing: {
fontSize: "1rem !important"
},
noBottomMargin: {
marginBottom: "0 !important"
}
};
@@ -52,9 +53,9 @@ class BranchSelector extends React.Component<Props, State> {
return (
<div
className={classNames(
"has-background-light field",
"field",
"is-horizontal",
classes.wrapper
classes.noBottomMargin
)}
>
<div
@@ -65,10 +66,14 @@ class BranchSelector extends React.Component<Props, State> {
classes.minWidthOfLabel
)}
>
<label className="label">{label}</label>
<label className={classNames("label", classes.labelSizing)}>
{label}
</label>
</div>
<div className="field-body">
<div className="field is-narrow">
<div
className={classNames("field is-narrow", classes.noBottomMargin)}
>
<div className="control">
<DropDown
className="is-fullwidth"

View File

@@ -3,14 +3,17 @@ import React from "react";
type Props = {
displayName: string,
url: string
url: string,
disabled: boolean,
onClick?: () => void
};
class DownloadButton extends React.Component<Props> {
render() {
const { displayName, url } = this.props;
const { displayName, url, disabled, onClick } = this.props;
const onClickOrDefault = !!onClick ? onClick : () => {};
return (
<a className="button is-large is-link" href={url}>
<a className="button is-large is-link" href={url} disabled={disabled} onClick={onClickOrDefault}>
<span className="icon is-medium">
<i className="fas fa-arrow-circle-down" />
</span>

View File

@@ -36,9 +36,9 @@ class ConfigurationBinder {
binder.bind("config.navigation", ConfigNavLink, configPredicate);
// route for global configuration, passes the link from the index resource to component
const ConfigRoute = ({ url, links }) => {
const ConfigRoute = ({ url, links, ...additionalProps }) => {
const link = links[linkName].href;
return this.route(url + to, <ConfigurationComponent link={link}/>);
return this.route(url + to, <ConfigurationComponent link={link} {...additionalProps} />);
};
// bind config route to extension point
@@ -63,9 +63,36 @@ class ConfigurationBinder {
// route for global configuration, passes the current repository to component
const RepoRoute = ({url, repository}) => {
const link = repository._links[linkName].href
return this.route(url + to, <RepositoryComponent repository={repository} link={link}/>);
const RepoRoute = ({url, repository, ...additionalProps}) => {
const link = repository._links[linkName].href;
return this.route(url + to, <RepositoryComponent repository={repository} link={link} {...additionalProps}/>);
};
// bind config route to extension point
binder.bind("repository.route", RepoRoute, repoPredicate);
}
bindRepositorySetting(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) {
// create predicate based on the link name of the current repository route
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const repoPredicate = (props: Object) => {
return props.repository && props.repository._links && props.repository._links[linkName];
};
// create NavigationLink with translated label
const RepoNavLink = translate(this.i18nNamespace)(({t, url}) => {
return this.navLink(url + "/settings" + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind("repository.subnavigation", RepoNavLink, repoPredicate);
// route for global configuration, passes the current repository to component
const RepoRoute = ({url, repository, ...additionalProps}) => {
const link = repository._links[linkName].href;
return this.route(url + "/settings" + to, <RepositoryComponent repository={repository} link={link} {...additionalProps}/>);
};
// bind config route to extension point

View File

@@ -10,7 +10,8 @@ type Props = {
buttonLabel: string,
fieldLabel: string,
errorMessage: string,
helpText?: string
helpText?: string,
validateEntry?: string => boolean
};
type State = {
@@ -25,6 +26,15 @@ class AddEntryToTableField extends React.Component<Props, State> {
};
}
isValid = () => {
const {validateEntry} = this.props;
if (!this.state.entryToAdd || this.state.entryToAdd === "" || !validateEntry) {
return true;
} else {
return validateEntry(this.state.entryToAdd);
}
};
render() {
const {
disabled,
@@ -39,7 +49,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
label={fieldLabel}
errorMessage={errorMessage}
onChange={this.handleAddEntryChange}
validationError={false}
validationError={!this.isValid()}
value={this.state.entryToAdd}
onReturnPressed={this.appendEntry}
disabled={disabled}
@@ -48,7 +58,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
<AddButton
label={buttonLabel}
action={this.addButtonClicked}
disabled={disabled}
disabled={disabled || this.state.entryToAdd ==="" || !this.isValid()}
/>
</div>
);

View File

@@ -28,7 +28,7 @@ class NavLink extends React.Component<Props> {
let showIcon = null;
if (icon) {
showIcon = (<><i className={icon}></i>{" "}</>);
showIcon = (<><i className={icon} />{" "}</>);
}
return (

View File

@@ -50,8 +50,19 @@ class PrimaryNavigation extends React.Component<Props> {
createNavigationItems = () => {
const navigationItems = [];
const { t, links } = this.props;
const props = {
links,
label: t("primary-navigation.first-menu")
};
const append = this.createNavigationAppender(navigationItems);
if (binder.hasExtension("primary-navigation.first-menu", props)) {
navigationItems.push(
<ExtensionPoint name="primary-navigation.first-menu" props={props} />
);
}
append("/repos", "/(repo|repos)", "primary-navigation.repositories", "repositories");
append("/users", "/(user|users)", "primary-navigation.users", "users");
append("/groups", "/(group|groups)", "primary-navigation.groups", "groups");

View File

@@ -0,0 +1,65 @@
//@flow
import * as React from "react";
import { Link, Route } from "react-router-dom";
type Props = {
to: string,
icon?: string,
label: string,
activeOnlyWhenExact?: boolean,
activeWhenMatch?: (route: any) => boolean,
children?: React.Node
};
class SubNavigation extends React.Component<Props> {
static defaultProps = {
activeOnlyWhenExact: false
};
isActive(route: any) {
const { activeWhenMatch } = this.props;
return route.match || (activeWhenMatch && activeWhenMatch(route));
}
renderLink = (route: any) => {
const { to, icon, label } = this.props;
let defaultIcon = "fas fa-cog";
if (icon) {
defaultIcon = icon;
}
let children = null;
if (this.isActive(route)) {
children = <ul className="sub-menu">{this.props.children}</ul>;
}
return (
<li>
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
<i className={defaultIcon} /> {label}
</Link>
{children}
</li>
);
};
render() {
const { to, activeOnlyWhenExact } = this.props;
// removes last part of url
let parents = to.split("/");
parents.splice(-1, 1);
let parent = parents.join("/");
return (
<Route
path={parent}
exact={activeOnlyWhenExact}
children={this.renderLink}
/>
);
}
}
export default SubNavigation;

View File

@@ -3,6 +3,7 @@
export { default as NavAction } from "./NavAction.js";
export { default as NavLink } from "./NavLink.js";
export { default as Navigation } from "./Navigation.js";
export { default as SubNavigation } from "./SubNavigation.js";
export { default as PrimaryNavigation } from "./PrimaryNavigation.js";
export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink.js";
export { default as Section } from "./Section.js";

View File

@@ -25,7 +25,7 @@ class ChangesetDiff extends React.Component<Props> {
render() {
const { changeset, t } = this.props;
if (!this.isDiffSupported(changeset)) {
return <Notification type="danger">{t("changesets.diff.not-supported")}</Notification>;
return <Notification type="danger">{t("changesets.changeset.diffNotSupported")}</Notification>;
} else {
const url = this.createUrl(changeset);
return <LoadingDiff url={url} />;

View File

@@ -21,7 +21,7 @@ class ChangesetList extends React.Component<Props> {
/>
);
});
return <div className="box">{content}</div>;
return <>{content}</>;
}
}

View File

@@ -1,7 +1,7 @@
{
"login": {
"title": "Anmeldung",
"subtitle": "Bitte anmelden um fortzufahren.",
"subtitle": "Bitte anmelden, um fortzufahren.",
"logo-alt": "SCM-Manager",
"username-placeholder": "Benutzername",
"password-placeholder": "Passwort",
@@ -10,13 +10,13 @@
"logout": {
"error": {
"title": "Abmeldung fehlgeschlagen",
"subtitle": "Während der Abmeldung ist ein Fehler aufgetreten"
"subtitle": "Während der Abmeldung ist ein Fehler aufgetreten."
}
},
"app": {
"error": {
"title": "Fehler",
"subtitle": "Ein unbekannter Fehler ist aufgetreten"
"subtitle": "Ein unbekannter Fehler ist aufgetreten."
}
},
"error-notification": {
@@ -43,8 +43,10 @@
"previous": "Zurück"
},
"profile": {
"navigation-label": "Navigation",
"actions-label": "Aktionen",
"navigationLabel": "Profil Navigation",
"informationNavLink": "Information",
"changePasswordNavLink": "Passwort ändern",
"settingsNavLink": "Einstellungen",
"username": "Benutzername",
"displayName": "Anzeigename",
"mail": "E-Mail",
@@ -52,21 +54,21 @@
"information": "Informationen",
"change-password": "Passwort ändern",
"error-title": "Fehler",
"error-subtitle": "Das Profil kann nicht angezeigt werden",
"error-subtitle": "Das Profil kann nicht angezeigt werden.",
"error": "Fehler",
"error-message": "'me' ist nicht definiert"
},
"password": {
"label": "Passwort",
"newPassword": "Neues Passwort",
"passwordHelpText": "Plaintext Passwort des Benutzers.",
"passwordHelpText": "Klartext Passwort des Benutzers.",
"passwordConfirmHelpText": "Passwort zur Bestätigen wiederholen.",
"currentPassword": "Aktuelles Passwort",
"currentPasswordHelpText": "Dieses Passwort wird momentan bereits verwendet.",
"confirmPassword": "Passwort wiederholen",
"passwordInvalid": "Das Passwort muss zwischen 6 und 32 Zeichen lang sein",
"passwordConfirmFailed": "Passwörter müssen identisch sein",
"passwordInvalid": "Das Passwort muss zwischen 6 und 32 Zeichen lang sein!",
"passwordConfirmFailed": "Passwörter müssen identisch sein!",
"submit": "Speichern",
"changedSuccessfully": "Passwort erfolgreich geändert"
"changedSuccessfully": "Passwort erfolgreich geändert!"
}
}

View File

@@ -1,12 +1,10 @@
{
"config": {
"navigation-title": "Navigation"
},
"global-config": {
"navigationLabel": "Einstellungs Navigation",
"globalConfigurationNavLink": "Globale Einstellungen",
"title": "Einstellungen",
"navigation-label": "Globale Einstellungen",
"error-title": "Fehler",
"error-subtitle": "Unbekannter Einstellungen Fehler"
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Einstellungen Fehler"
},
"config-form": {
"submit": "Speichern",
@@ -47,12 +45,12 @@
"login-attempt": {
"name": "Anmeldeversuche",
"login-attempt-limit": "Limit für Anmeldeversuche",
"login-attempt-limit-timeout": "Timeout bei fehlgeschlagenen Anmeldeversuche"
"login-attempt-limit-timeout": "Timeout bei fehlgeschlagenen Anmeldeversuchen"
},
"general-settings": {
"realm-description": "Realm Beschreibung",
"enable-repository-archive": "Repository Archiv aktivieren",
"disable-grouping-grid": "Gruppen deaktiviern",
"disable-grouping-grid": "Gruppen deaktivieren",
"date-format": "Datumsformat",
"anonymous-access-enabled": "Anonyme Zugriffe erlauben",
"skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen",
@@ -67,27 +65,27 @@
"plugin-url-invalid": "Dies ist keine gültige URL"
},
"help": {
"realmDescriptionHelpText": "Beschreibung des authentication realm",
"dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der momentjs Dokumentation beschrieben.",
"realmDescriptionHelpText": "Beschreibung des Authentication Realm.",
"dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der MomentJS Dokumentation beschrieben.",
"pluginRepositoryHelpText": "Die URL des Plugin Repositories. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur",
"enableForwardingHelpText": "mod_proxy Port Weiterleitung aktivieren.",
"enableRepositoryArchiveHelpText": "Repository Archive aktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.",
"disableGroupingGridHelpText": "Repository Gruppen deaktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.",
"allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf öffentliche Repositories.",
"skipFailedAuthenticatorsHelpText": "Die Kette der Authentifikatoren wird nicht beendet wenn ein Authentifikator einen Benutzer findet, ihn aber nicht erfolgreich authentifizieren kann.",
"skipFailedAuthenticatorsHelpText": "Die Kette der Authentifikatoren wird nicht beendet, wenn ein Authentifikator einen Benutzer findet, ihn aber nicht erfolgreich authentifizieren kann.",
"adminGroupsHelpText": "Namen von Gruppen mit Admin-Berechtigungen.",
"adminUsersHelpText": "Namen von Benutzern mit Admin-Berechtigungen.",
"forceBaseUrlHelpText": "Zugriffe, die von einer anderen URL kommen, werden auf die Base URL weiter geleitet.",
"baseUrlHelpText": "Die URL der Applikation mit Kontextpfad, z.B. http://localhost:8080/scm",
"loginAttemptLimitHelpText": "Maximale Anzahl von Anmeldeversuchen. Durch Verwendung von -1 wird die Begrenzung der Anmeldeversuche deaktiviert.",
"loginAttemptLimitTimeoutHelpText": "Timeout in Sekunden für Benutzer, die vorübergehend wegen zu vieler fehlgeschlagener Anmeldeversuche deaktiviert wurden.",
"enableProxyHelpText": "Proxy aktiviern",
"loginAttemptLimitTimeoutHelpText": "Timeout in Sekunden für Benutzer, die vorübergehend wegen zu vieler fehlgeschlagener Anmeldeversuche, deaktiviert wurden.",
"enableProxyHelpText": "Proxy aktivieren",
"proxyPortHelpText": "Der Proxy Port",
"proxyPasswordHelpText": "Das Passwort für die Proxy Server Anmeldung.",
"proxyServerHelpText": "Der Proxy Server",
"proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.",
"proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.",
"enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.",
"defaultNameSpaceStrategyHelpText": "Die Standardstrategie für Namespaces"
"defaultNameSpaceStrategyHelpText": "Die Standardstrategie für Namespaces."
}
}

View File

@@ -11,20 +11,23 @@
"title": "Gruppen",
"subtitle": "Verwaltung der Gruppen"
},
"single-group": {
"error-title": "Fehler",
"error-subtitle": "Unbekannter Gruppen Fehler",
"navigation-label": "Navigation",
"actions-label": "Aktionen",
"information-label": "Informationen",
"back-label": "Zurück"
"singleGroup": {
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Gruppen Fehler",
"menu": {
"navigationLabel": "Gruppen Navigation",
"informationNavLink": "Informationen",
"settingsNavLink": "Einstellungen",
"generalNavLink": "Generell",
"setPermissionsNavLink": "Berechtigungen"
}
},
"add-group": {
"title": "Gruppe erstellen",
"subtitle": "Erstllen einer neuen Gruppe"
"subtitle": "Erstellen einer neuen Gruppe"
},
"create-group-button": {
"label": "Erstellen"
"label": "Gruppe erstellen"
},
"edit-group-button": {
"label": "Bearbeiten"
@@ -41,30 +44,28 @@
},
"add-member-autocomplete": {
"placeholder": "Benutzername eingeben",
"loading": "suche...",
"loading": "Suche...",
"no-options": "Kein Vorschlag für Benutzername verfügbar"
},
"group-form": {
"groupForm": {
"subtitle": "Gruppe bearbeiten",
"submit": "Speichern",
"name-error": "Name ist ungültig",
"description-error": "Beschreibung ist ungültig",
"nameError": "Name ist ungültig",
"descriptionError": "Beschreibung ist ungültig",
"help": {
"nameHelpText": "Einzigartiger Name der Gruppe",
"nameHelpText": "Eindeutiger Name der Gruppe",
"descriptionHelpText": "Eine kurze Beschreibung der Gruppe",
"memberHelpText": "Benutzername des Mitglieds der Gruppe"
}
},
"delete-group-button": {
"label": "Löschen",
"confirm-alert": {
"deleteGroup": {
"subtitle": "Gruppe löschen",
"button": "Löschen",
"confirmAlert": {
"title": "Gruppe löschen",
"message": "Soll die Gruppe wirklich gelöscht werden?",
"submit": "Ja",
"cancel": "Nein"
}
},
"set-permissions-button": {
"label": "Berechtigungen ändern"
}
}

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