This commit is contained in:
Florian Scholdei
2019-02-07 17:30:54 +01:00
258 changed files with 2966 additions and 2468 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 object to the json response.
*
* @param rel name of relation
* @param embeddedItem embedded object
*/
void appendEmbedded(String rel, HalRepresentation embeddedItem);
/**
* Builder for link arrays.
@@ -31,7 +40,7 @@ public interface LinkAppender {
interface LinkArrayBuilder {
/**
* Append an link to the array.
* Append a link to the array.
*
* @param name name of link
* @param href link target

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

@@ -27,6 +27,7 @@
},
"repo-config": {
"link": "Konfiguration",
"title": "Git Einstellungen",
"default-branch": "Standard Branch",
"submit": "Speichern",
"error": {

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

@@ -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);
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);
if (SvnUtil.isTransactionEntryId(revisionOrTransactionId)) {
modifications = getModificationsFromTransaction(SvnUtil.getTransactionId(revisionOrTransactionId));
} else {
modifications = getModificationFromRevision(revisionOrTransactionId);
}
return modifications;
} catch (SVNException ex) {
throw new InternalRepositoryException(repository, "could not open repository", 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)) {
return SvnUtil.createModifications(entries.iterator().next(), revision);
}
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

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

@@ -2,31 +2,40 @@
import React from "react";
import moment from "moment";
import { translate } from "react-i18next";
import injectSheet from "react-jss";
const styles = {
date: {
borderBottom: "1px dotted rgba(219, 219, 219)",
cursor: "help"
}
};
type Props = {
date?: string,
// context props
classes: any,
i18n: any
};
class DateFromNow extends React.Component<Props> {
static format(locale: string, date?: string) {
let fromNow = "";
if (date) {
fromNow = moment(date)
.locale(locale)
.fromNow();
}
return fromNow;
}
render() {
const { i18n, date } = this.props;
const { i18n, date, classes } = this.props;
const fromNow = DateFromNow.format(i18n.language, date);
return <span>{fromNow}</span>;
if (date) {
const dateWithLocale = moment(date).locale(i18n.language);
return (
<time title={dateWithLocale.format()} className={classes.date}>
{dateWithLocale.fromNow()}
</time>
);
}
return null;
}
}
export default translate()(DateFromNow);
export default injectSheet(styles)(translate()(DateFromNow));

View File

@@ -4,21 +4,27 @@ import classNames from "classnames";
type Props = {
message: string,
className: string,
className?: string,
location: string,
children: React.Node
};
class Tooltip extends React.Component<Props> {
static defaultProps = {
location: "right"
};
render() {
const { className, message, children } = this.props;
const { className, message, location, children } = this.props;
const multiline = message.length > 60 ? "is-tooltip-multiline" : "";
return (
<div
className={classNames("tooltip", "is-tooltip-right", multiline, className)}
<span
className={classNames("tooltip", "is-tooltip-" + location, multiline, className)}
data-tooltip={message}
>
{children}
</div>
</span>
);
}
}

View File

@@ -1,16 +1,17 @@
//@flow
import React from "react";
import * as React from "react";
import classNames from "classnames";
import { withRouter } from "react-router-dom";
export type ButtonProps = {
label: string,
label?: string,
loading?: boolean,
disabled?: boolean,
action?: (event: Event) => void,
link?: string,
fullWidth?: boolean,
className?: string,
children?: React.Node,
classes: any
};
@@ -45,6 +46,7 @@ class Button extends React.Component<Props> {
type,
color,
fullWidth,
children,
className
} = this.props;
const loadingClass = loading ? "is-loading" : "";
@@ -62,7 +64,7 @@ class Button extends React.Component<Props> {
className
)}
>
{label}
{label} {children}
</button>
);
};

View File

@@ -1,41 +1,30 @@
// @flow
import React from "react";
import Button from "./Button";
import * as React from "react";
type Props = {
firstlabel: string,
secondlabel: string,
firstAction?: (event: Event) => void,
secondAction?: (event: Event) => void,
firstIsSelected: boolean
addons?: boolean,
className?: string,
children: React.Node
};
class ButtonGroup extends React.Component<Props> {
static defaultProps = {
addons: true
};
render() {
const { firstlabel, secondlabel, firstAction, secondAction, firstIsSelected } = this.props;
let showFirstColor = "";
let showSecondColor = "";
if (firstIsSelected) {
showFirstColor += "link is-selected";
} else {
showSecondColor += "link is-selected";
const { addons, className, children } = this.props;
let styleClasses = "buttons";
if (addons) {
styleClasses += " has-addons";
}
if (className) {
styleClasses += " " + className;
}
return (
<div className="buttons has-addons">
<Button
label={firstlabel}
color={showFirstColor}
action={firstAction}
/>
<Button
label={secondlabel}
color={showSecondColor}
action={secondAction}
/>
<div className={styleClasses}>
{ children }
</div>
);
}

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

@@ -72,6 +72,33 @@ class ConfigurationBinder {
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
binder.bind("repository.route", RepoRoute, repoPredicate);
}
}
export default new ConfigurationBinder();

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 || this.state.entryToAdd ===""}
disabled={disabled || this.state.entryToAdd ==="" || !this.isValid()}
/>
</div>
);

View File

@@ -1,102 +0,0 @@
/*modified from https://github.com/GA-MO/react-confirm-alert*/
.react-confirm-alert-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
background: rgba(255, 255, 255, 0.9);
display: -webkit-flex;
display: -moz-flex;
display: -ms-flex;
display: -o-flex;
display: flex;
justify-content: center;
-ms-align-items: center;
align-items: center;
opacity: 0;
-webkit-animation: react-confirm-alert-fadeIn 0.5s 0.2s forwards;
-moz-animation: react-confirm-alert-fadeIn 0.5s 0.2s forwards;
-o-animation: react-confirm-alert-fadeIn 0.5s 0.2s forwards;
animation: react-confirm-alert-fadeIn 0.5s 0.2s forwards;
}
.react-confirm-alert-body {
font-family: Arial, Helvetica, sans-serif;
width: 400px;
padding: 30px;
text-align: left;
background: #fff;
border-radius: 10px;
box-shadow: 0 20px 75px rgba(0, 0, 0, 0.13);
color: #666;
}
.react-confirm-alert-body > h1 {
margin-top: 0;
}
.react-confirm-alert-body > h3 {
margin: 0;
font-size: 16px;
}
.react-confirm-alert-button-group {
display: -webkit-flex;
display: -moz-flex;
display: -ms-flex;
display: -o-flex;
display: flex;
justify-content: flex-start;
margin-top: 20px;
}
.react-confirm-alert-button-group > button {
outline: none;
background: #333;
border: none;
display: inline-block;
padding: 6px 18px;
color: #eee;
margin-right: 10px;
border-radius: 5px;
font-size: 12px;
cursor: pointer;
}
@-webkit-keyframes react-confirm-alert-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-moz-keyframes react-confirm-alert-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-o-keyframes react-confirm-alert-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes react-confirm-alert-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@@ -1,9 +1,7 @@
// @flow
//modified from https://github.com/GA-MO/react-confirm-alert
import * as React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import "./ConfirmAlert.css";
import ReactDOM from "react-dom";
import Modal from "./Modal";
type Button = {
label: string,
@@ -25,59 +23,47 @@ class ConfirmAlert extends React.Component<Props> {
};
close = () => {
removeElementReconfirm();
ReactDOM.unmountComponentAtNode(document.getElementById("modalRoot"));
};
render() {
const { title, message, buttons } = this.props;
return (
<div className="react-confirm-alert-overlay">
<div className="react-confirm-alert">
{
<div className="react-confirm-alert-body">
{title && <h1>{title}</h1>}
{message}
<div className="react-confirm-alert-button-group">
{buttons.map((button, i) => (
<button
key={i}
onClick={() => this.handleClickButton(button)}
href="javascript:void(0);"
>
{button.label}
</button>
))}
</div>
</div>
}
</div>
const body = <>{message}</>;
const footer = (
<div className="field is-grouped">
{buttons.map((button, i) => (
<p className="control">
<a
className="button is-info"
key={i}
onClick={() => this.handleClickButton(button)}
>
{button.label}
</a>
</p>
))}
</div>
);
return (
<Modal
title={title}
closeFunction={() => this.close()}
body={body}
active={true}
footer={footer}
/>
);
}
}
function createElementReconfirm(properties: Props) {
const divTarget = document.createElement("div");
divTarget.id = "react-confirm-alert";
if (document.body) {
document.body.appendChild(divTarget);
render(<ConfirmAlert {...properties} />, divTarget);
}
}
function removeElementReconfirm() {
const target = document.getElementById("react-confirm-alert");
if (target) {
unmountComponentAtNode(target);
if (target.parentNode) {
target.parentNode.removeChild(target);
}
}
}
export function confirmAlert(properties: Props) {
createElementReconfirm(properties);
const root = document.getElementById("modalRoot");
if (root) {
ReactDOM.render(<ConfirmAlert {...properties} />, root);
}
}
export default ConfirmAlert;

View File

@@ -0,0 +1,54 @@
// @flow
import * as React from "react";
import classNames from "classnames";
import injectSheet from "react-jss";
type Props = {
title: string,
closeFunction: () => void,
body: any,
footer?: any,
active: boolean,
classes: any
};
const styles = {
resize: {
maxWidth: "100%",
width: "auto !important",
display: "inline-block"
}
};
class Modal extends React.Component<Props> {
render() {
const { title, closeFunction, body, footer, active, classes } = this.props;
const isActive = active ? "is-active" : null;
let showFooter = null;
if (footer) {
showFooter = <footer className="modal-card-foot">{footer}</footer>;
}
return (
<div className={classNames("modal", isActive)}>
<div className="modal-background" />
<div className={classNames("modal-card", classes.resize)}>
<header className="modal-card-head">
<p className="modal-card-title">{title}</p>
<button
className="delete"
aria-label="close"
onClick={closeFunction}
/>
</header>
<section className="modal-card-body">{body}</section>
{showFooter}
</div>
</div>
);
}
}
export default injectSheet(styles)(Modal);

View File

@@ -1,4 +1,5 @@
// @create-index
export { default as ConfirmAlert, confirmAlert } from "./ConfirmAlert.js";
export { default as Modal } from "./Modal.js";

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

@@ -1,9 +1,14 @@
//@flow
import React from "react";
import type {Changeset} from "@scm-manager/ui-types";
import type { Changeset } from "@scm-manager/ui-types";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import {translate} from "react-i18next";
type Props = {
changeset: Changeset
changeset: Changeset,
// context props
t: (string) => string
};
class ChangesetAuthor extends React.Component<Props> {
@@ -13,26 +18,35 @@ class ChangesetAuthor extends React.Component<Props> {
return null;
}
const { name } = changeset.author;
const { name, mail } = changeset.author;
if (mail) {
return this.withExtensionPoint(this.renderWithMail(name, mail));
}
return this.withExtensionPoint(<>{name}</>);
}
renderWithMail(name: string, mail: string) {
const { t } = this.props;
return (
<>
{name} {this.renderMail()}
</>
<a href={"mailto: " + mail} title={t("changeset.author.mailto") + " " + mail}>
{name}
</a>
);
}
renderMail() {
const { mail } = this.props.changeset.author;
if (mail) {
return (
<a className="is-hidden-mobile" href={"mailto:" + mail}>
&lt;
{mail}
&gt;
</a>
);
}
withExtensionPoint(child: any) {
const { t } = this.props;
return (
<>
{t("changeset.author.prefix")} {child}
<ExtensionPoint
name="changesets.author.suffix"
props={{ changeset: this.props.changeset }}
renderAll={true}
/>
</>
);
}
}
export default ChangesetAuthor;
export default translate("repos")(ChangesetAuthor);

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