mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 17:05:43 +01:00
Merge with 2.0.0-m3
This commit is contained in:
@@ -80,8 +80,20 @@ public interface AccessToken {
|
|||||||
*/
|
*/
|
||||||
Date getExpiration();
|
Date getExpiration();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns refresh expiration of token.
|
||||||
|
*
|
||||||
|
* @return refresh expiration
|
||||||
|
*/
|
||||||
Optional<Date> getRefreshExpiration();
|
Optional<Date> getRefreshExpiration();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns id of the parent key.
|
||||||
|
*
|
||||||
|
* @return parent key id
|
||||||
|
*/
|
||||||
|
Optional<String> getParentKey();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this
|
* Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this
|
||||||
* token. For example we could issue a token which can only be used to read a single repository. for more informations
|
* token. For example we could issue a token which can only be used to read a single repository. for more informations
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates cookies and invalidates access token cookies.
|
||||||
|
*
|
||||||
|
* @author Sebastian Sdorra
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public interface AccessTokenCookieIssuer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a cookie for token authentication and attaches it to the response.
|
||||||
|
*
|
||||||
|
* @param request http servlet request
|
||||||
|
* @param response http servlet response
|
||||||
|
* @param accessToken access token
|
||||||
|
*/
|
||||||
|
void authenticate(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken);
|
||||||
|
/**
|
||||||
|
* Invalidates the authentication cookie.
|
||||||
|
*
|
||||||
|
* @param request http servlet request
|
||||||
|
* @param response http servlet response
|
||||||
|
*/
|
||||||
|
void invalidate(HttpServletRequest request, HttpServletResponse response);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -164,7 +164,7 @@ public class DefaultCipherHandler implements CipherHandler {
|
|||||||
String result = null;
|
String result = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] encodedInput = Base64.getDecoder().decode(value);
|
byte[] encodedInput = Base64.getUrlDecoder().decode(value);
|
||||||
byte[] salt = new byte[SALT_LENGTH];
|
byte[] salt = new byte[SALT_LENGTH];
|
||||||
byte[] encoded = new byte[encodedInput.length - SALT_LENGTH];
|
byte[] encoded = new byte[encodedInput.length - SALT_LENGTH];
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ public class DefaultCipherHandler implements CipherHandler {
|
|||||||
System.arraycopy(salt, 0, result, 0, SALT_LENGTH);
|
System.arraycopy(salt, 0, result, 0, SALT_LENGTH);
|
||||||
System.arraycopy(encodedInput, 0, result, SALT_LENGTH,
|
System.arraycopy(encodedInput, 0, result, SALT_LENGTH,
|
||||||
result.length - SALT_LENGTH);
|
result.length - SALT_LENGTH);
|
||||||
res = new String(Base64.getEncoder().encode(result), ENCODING);
|
res = new String(Base64.getUrlEncoder().encode(result), ENCODING);
|
||||||
} catch (IOException | GeneralSecurityException ex) {
|
} catch (IOException | GeneralSecurityException ex) {
|
||||||
throw new CipherException("could not encode string", ex);
|
throw new CipherException("could not encode string", ex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,10 @@
|
|||||||
|
|
||||||
package sonia.scm.store;
|
package sonia.scm.store;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConfigurationStore for configuration objects. <strong>Note:</strong> the default
|
* ConfigurationStore for configuration objects. <strong>Note:</strong> the default
|
||||||
* implementation use JAXB to marshall the configuration objects.
|
* implementation use JAXB to marshall the configuration objects.
|
||||||
@@ -50,7 +54,17 @@ public interface ConfigurationStore<T>
|
|||||||
*
|
*
|
||||||
* @return configuration object from store
|
* @return configuration object from store
|
||||||
*/
|
*/
|
||||||
public T get();
|
T get();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the configuration object from store.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return configuration object from store
|
||||||
|
*/
|
||||||
|
default Optional<T> getOptional() {
|
||||||
|
return ofNullable(get());
|
||||||
|
}
|
||||||
|
|
||||||
//~--- set methods ----------------------------------------------------------
|
//~--- set methods ----------------------------------------------------------
|
||||||
|
|
||||||
@@ -58,7 +72,7 @@ public interface ConfigurationStore<T>
|
|||||||
* Stores the given configuration object to the store.
|
* Stores the given configuration object to the store.
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @param obejct configuration object to store
|
* @param object configuration object to store
|
||||||
*/
|
*/
|
||||||
public void set(T obejct);
|
void set(T object);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,10 @@
|
|||||||
|
|
||||||
package sonia.scm.store;
|
package sonia.scm.store;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for {@link BlobStore} and {@link DataStore}.
|
* Base class for {@link BlobStore} and {@link DataStore}.
|
||||||
*
|
*
|
||||||
@@ -67,4 +71,16 @@ public interface MultiEntryStore<T> {
|
|||||||
* @return item with the given id
|
* @return item with the given id
|
||||||
*/
|
*/
|
||||||
public T get(String id);
|
public T get(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the item with the given id from the store.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param id id of the item to return
|
||||||
|
*
|
||||||
|
* @return item with the given id
|
||||||
|
*/
|
||||||
|
default Optional<T> getOptional(String id) {
|
||||||
|
return ofNullable(get(id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package sonia.scm.web;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import static java.util.Collections.singletonMap;
|
||||||
|
import static sonia.scm.web.VndMediaType.REPOSITORY;
|
||||||
|
import static sonia.scm.web.VndMediaType.REPOSITORY_COLLECTION;
|
||||||
|
|
||||||
|
public abstract class AbstractRepositoryJsonEnricher extends JsonEnricherBase {
|
||||||
|
|
||||||
|
public AbstractRepositoryJsonEnricher(ObjectMapper objectMapper) {
|
||||||
|
super(objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enrich(JsonEnricherContext context) {
|
||||||
|
if (resultHasMediaType(REPOSITORY, context)) {
|
||||||
|
JsonNode repositoryNode = context.getResponseEntity();
|
||||||
|
enrichRepositoryNode(repositoryNode);
|
||||||
|
} else if (resultHasMediaType(REPOSITORY_COLLECTION, context)) {
|
||||||
|
JsonNode repositoryCollectionNode = context.getResponseEntity().get("_embedded").withArray("repositories");
|
||||||
|
repositoryCollectionNode.elements().forEachRemaining(this::enrichRepositoryNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enrichRepositoryNode(JsonNode repositoryNode) {
|
||||||
|
String namespace = repositoryNode.get("namespace").asText();
|
||||||
|
String name = repositoryNode.get("name").asText();
|
||||||
|
|
||||||
|
enrichRepositoryNode(repositoryNode, namespace, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void enrichRepositoryNode(JsonNode repositoryNode, String namespace, String name);
|
||||||
|
|
||||||
|
protected void addLink(JsonNode repositoryNode, String linkName, String link) {
|
||||||
|
JsonNode hrefNode = createObject(singletonMap("href", value(link)));
|
||||||
|
addPropertyNode(repositoryNode.get("_links"), linkName, hrefNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,7 +128,7 @@ public class AuthenticationFilter extends HttpFilter
|
|||||||
}
|
}
|
||||||
else if (subject.isAuthenticated())
|
else if (subject.isAuthenticated())
|
||||||
{
|
{
|
||||||
logger.trace("user is allready authenticated");
|
logger.trace("user is already authenticated");
|
||||||
processChain(request, response, chain, subject);
|
processChain(request, response, chain, subject);
|
||||||
}
|
}
|
||||||
else if (isAnonymousAccessEnabled())
|
else if (isAnonymousAccessEnabled())
|
||||||
|
|||||||
25
scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java
Normal file
25
scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package sonia.scm.xml;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.TemporalAccessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAXB adapter for {@link Instant} objects.
|
||||||
|
*
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class XmlInstantAdapter extends XmlAdapter<String, Instant> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String marshal(Instant instant) {
|
||||||
|
return DateTimeFormatter.ISO_INSTANT.format(instant);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant unmarshal(String text) {
|
||||||
|
TemporalAccessor parsed = DateTimeFormatter.ISO_INSTANT.parse(text);
|
||||||
|
return Instant.from(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package sonia.scm.web;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.google.common.io.Resources;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.api.v2.resources.ScmPathInfoStore;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class AbstractRepositoryJsonEnricherTest {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private AbstractRepositoryJsonEnricher linkEnricher;
|
||||||
|
private JsonNode rootNode;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void globalSetUp() {
|
||||||
|
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||||
|
pathInfoStore.set(() -> URI.create("/"));
|
||||||
|
|
||||||
|
linkEnricher = new AbstractRepositoryJsonEnricher(objectMapper) {
|
||||||
|
@Override
|
||||||
|
protected void enrichRepositoryNode(JsonNode repositoryNode, String namespace, String name) {
|
||||||
|
addLink(repositoryNode, "new-link", "/somewhere");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldEnrichRepositories() throws IOException {
|
||||||
|
URL resource = Resources.getResource("sonia/scm/repository/repository-001.json");
|
||||||
|
rootNode = objectMapper.readTree(resource);
|
||||||
|
|
||||||
|
JsonEnricherContext context = new JsonEnricherContext(
|
||||||
|
URI.create("/"),
|
||||||
|
MediaType.valueOf(VndMediaType.REPOSITORY),
|
||||||
|
rootNode
|
||||||
|
);
|
||||||
|
|
||||||
|
linkEnricher.enrich(context);
|
||||||
|
|
||||||
|
String configLink = context.getResponseEntity()
|
||||||
|
.get("_links")
|
||||||
|
.get("new-link")
|
||||||
|
.get("href")
|
||||||
|
.asText();
|
||||||
|
|
||||||
|
assertThat(configLink).isEqualTo("/somewhere");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldEnrichAllRepositories() throws IOException {
|
||||||
|
URL resource = Resources.getResource("sonia/scm/repository/repository-collection-001.json");
|
||||||
|
rootNode = objectMapper.readTree(resource);
|
||||||
|
|
||||||
|
JsonEnricherContext context = new JsonEnricherContext(
|
||||||
|
URI.create("/"),
|
||||||
|
MediaType.valueOf(VndMediaType.REPOSITORY_COLLECTION),
|
||||||
|
rootNode
|
||||||
|
);
|
||||||
|
|
||||||
|
linkEnricher.enrich(context);
|
||||||
|
|
||||||
|
context.getResponseEntity()
|
||||||
|
.get("_embedded")
|
||||||
|
.withArray("repositories")
|
||||||
|
.elements()
|
||||||
|
.forEachRemaining(node -> {
|
||||||
|
String configLink = node
|
||||||
|
.get("_links")
|
||||||
|
.get("new-link")
|
||||||
|
.get("href")
|
||||||
|
.asText();
|
||||||
|
|
||||||
|
assertThat(configLink).isEqualTo("/somewhere");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotModifyObjectsWithUnsupportedMediaType() throws IOException {
|
||||||
|
URL resource = Resources.getResource("sonia/scm/repository/repository-001.json");
|
||||||
|
rootNode = objectMapper.readTree(resource);
|
||||||
|
JsonEnricherContext context = new JsonEnricherContext(
|
||||||
|
URI.create("/"),
|
||||||
|
MediaType.valueOf(VndMediaType.USER),
|
||||||
|
rootNode
|
||||||
|
);
|
||||||
|
|
||||||
|
linkEnricher.enrich(context);
|
||||||
|
|
||||||
|
boolean hasNewPullRequestLink = context.getResponseEntity()
|
||||||
|
.get("_links")
|
||||||
|
.has("new-link");
|
||||||
|
|
||||||
|
assertThat(hasNewPullRequestLink).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package sonia.scm.xml;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
|
|
||||||
|
import javax.xml.bind.JAXB;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
class XmlInstantAdapterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMarshalAndUnmarshalInstant(@TempDirectory.TempDir Path tempDirectory) {
|
||||||
|
Path path = tempDirectory.resolve("instant.xml");
|
||||||
|
|
||||||
|
Instant instant = Instant.now();
|
||||||
|
InstantObject object = new InstantObject(instant);
|
||||||
|
JAXB.marshal(object, path.toFile());
|
||||||
|
|
||||||
|
InstantObject unmarshaled = JAXB.unmarshal(path.toFile(), InstantObject.class);
|
||||||
|
assertEquals(instant, unmarshaled.instant);
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlRootElement(name = "instant-object")
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public static class InstantObject {
|
||||||
|
|
||||||
|
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
|
||||||
|
private Instant instant;
|
||||||
|
|
||||||
|
public InstantObject() {
|
||||||
|
}
|
||||||
|
|
||||||
|
InstantObject(Instant instant) {
|
||||||
|
this.instant = instant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"creationDate": "2018-11-09T09:48:32.732Z",
|
||||||
|
"description": "Handling static webresources made easy",
|
||||||
|
"healthCheckFailures": [],
|
||||||
|
"lastModified": "2018-11-09T09:49:20.973Z",
|
||||||
|
"namespace": "scmadmin",
|
||||||
|
"name": "web-resources",
|
||||||
|
"archived": false,
|
||||||
|
"type": "git",
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
|
||||||
|
},
|
||||||
|
"protocol": [
|
||||||
|
{
|
||||||
|
"href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
|
||||||
|
"name": "http"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
|
||||||
|
},
|
||||||
|
"changesets": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"page": 0,
|
||||||
|
"pageTotal": 1,
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
|
||||||
|
},
|
||||||
|
"first": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
|
||||||
|
},
|
||||||
|
"last": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_embedded": {
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"creationDate": "2018-11-09T09:48:32.732Z",
|
||||||
|
"description": "Handling static webresources made easy",
|
||||||
|
"healthCheckFailures": [],
|
||||||
|
"lastModified": "2018-11-09T09:49:20.973Z",
|
||||||
|
"namespace": "scmadmin",
|
||||||
|
"name": "web-resources",
|
||||||
|
"archived": false,
|
||||||
|
"type": "git",
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
|
||||||
|
},
|
||||||
|
"protocol": [
|
||||||
|
{
|
||||||
|
"href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
|
||||||
|
"name": "http"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
|
||||||
|
},
|
||||||
|
"changesets": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"creationDate": "2018-11-09T09:48:32.732Z",
|
||||||
|
"description": "Handling static webresources made easy",
|
||||||
|
"healthCheckFailures": [],
|
||||||
|
"lastModified": "2018-11-09T09:49:20.973Z",
|
||||||
|
"namespace": "scmadmin",
|
||||||
|
"name": "web-resources",
|
||||||
|
"archived": false,
|
||||||
|
"type": "git",
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
|
||||||
|
},
|
||||||
|
"protocol": [
|
||||||
|
{
|
||||||
|
"href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
|
||||||
|
"name": "http"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
|
||||||
|
},
|
||||||
|
"changesets": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,8 +58,6 @@ public abstract class FileBasedStoreFactory {
|
|||||||
private RepositoryLocationResolver repositoryLocationResolver;
|
private RepositoryLocationResolver repositoryLocationResolver;
|
||||||
private Store store;
|
private Store store;
|
||||||
|
|
||||||
private File storeDirectory;
|
|
||||||
|
|
||||||
protected FileBasedStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, Store store) {
|
protected FileBasedStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, Store store) {
|
||||||
this.contextProvider = contextProvider;
|
this.contextProvider = contextProvider;
|
||||||
this.repositoryLocationResolver = repositoryLocationResolver;
|
this.repositoryLocationResolver = repositoryLocationResolver;
|
||||||
@@ -75,7 +73,7 @@ public abstract class FileBasedStoreFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected File getStoreLocation(String name, Class type, Repository repository) {
|
protected File getStoreLocation(String name, Class type, Repository repository) {
|
||||||
if (storeDirectory == null) {
|
File storeDirectory;
|
||||||
if (repository != null) {
|
if (repository != null) {
|
||||||
LOG.debug("create store with type: {}, name: {} and repository: {}", type, name, repository.getNamespaceAndName());
|
LOG.debug("create store with type: {}, name: {} and repository: {}", type, name, repository.getNamespaceAndName());
|
||||||
storeDirectory = this.getStoreDirectory(store, repository);
|
storeDirectory = this.getStoreDirectory(store, repository);
|
||||||
@@ -84,8 +82,7 @@ public abstract class FileBasedStoreFactory {
|
|||||||
storeDirectory = this.getStoreDirectory(store);
|
storeDirectory = this.getStoreDirectory(store);
|
||||||
}
|
}
|
||||||
IOUtil.mkdirs(storeDirectory);
|
IOUtil.mkdirs(storeDirectory);
|
||||||
}
|
return new File(storeDirectory, name);
|
||||||
return new File(this.storeDirectory, name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,13 +9,17 @@ import sonia.scm.repository.GitRepositoryHandler;
|
|||||||
import sonia.scm.web.GitVndMediaType;
|
import sonia.scm.web.GitVndMediaType;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Provider;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.core.Response;
|
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.
|
* RESTful Web Service Resource to manage the configuration of the git plugin.
|
||||||
*/
|
*/
|
||||||
@@ -26,13 +30,15 @@ public class GitConfigResource {
|
|||||||
private final GitConfigDtoToGitConfigMapper dtoToConfigMapper;
|
private final GitConfigDtoToGitConfigMapper dtoToConfigMapper;
|
||||||
private final GitConfigToGitConfigDtoMapper configToDtoMapper;
|
private final GitConfigToGitConfigDtoMapper configToDtoMapper;
|
||||||
private final GitRepositoryHandler repositoryHandler;
|
private final GitRepositoryHandler repositoryHandler;
|
||||||
|
private final Provider<GitRepositoryConfigResource> gitRepositoryConfigResource;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GitConfigResource(GitConfigDtoToGitConfigMapper dtoToConfigMapper, GitConfigToGitConfigDtoMapper configToDtoMapper,
|
public GitConfigResource(GitConfigDtoToGitConfigMapper dtoToConfigMapper, GitConfigToGitConfigDtoMapper configToDtoMapper,
|
||||||
GitRepositoryHandler repositoryHandler) {
|
GitRepositoryHandler repositoryHandler, Provider<GitRepositoryConfigResource> gitRepositoryConfigResource) {
|
||||||
this.dtoToConfigMapper = dtoToConfigMapper;
|
this.dtoToConfigMapper = dtoToConfigMapper;
|
||||||
this.configToDtoMapper = configToDtoMapper;
|
this.configToDtoMapper = configToDtoMapper;
|
||||||
this.repositoryHandler = repositoryHandler;
|
this.repositoryHandler = repositoryHandler;
|
||||||
|
this.gitRepositoryConfigResource = gitRepositoryConfigResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,4 +94,9 @@ public class GitConfigResource {
|
|||||||
|
|
||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("{namespace}/{name}")
|
||||||
|
public GitRepositoryConfigResource getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) {
|
||||||
|
return gitRepositoryConfigResource.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.github.legman.Subscribe;
|
||||||
|
import sonia.scm.EagerSingleton;
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
import sonia.scm.plugin.Extension;
|
||||||
|
import sonia.scm.repository.ClearRepositoryCacheEvent;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@EagerSingleton @Extension
|
||||||
|
public class GitRepositoryConfigChangeClearRepositoryCacheListener {
|
||||||
|
@Subscribe
|
||||||
|
public void sendClearRepositoryCacheEvent(GitRepositoryConfigChangedEvent event) {
|
||||||
|
if (!Objects.equals(event.getOldConfig().getDefaultBranch(), event.getNewConfig().getDefaultBranch())) {
|
||||||
|
ScmEventBus.getInstance().post(new ClearRepositoryCacheEvent(event.getRepository()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import sonia.scm.event.Event;
|
||||||
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
|
||||||
|
@Event
|
||||||
|
public class GitRepositoryConfigChangedEvent {
|
||||||
|
private final Repository repository;
|
||||||
|
private final GitRepositoryConfig oldConfig;
|
||||||
|
private final GitRepositoryConfig newConfig;
|
||||||
|
|
||||||
|
public GitRepositoryConfigChangedEvent(Repository repository, GitRepositoryConfig oldConfig, GitRepositoryConfig newConfig) {
|
||||||
|
this.repository = repository;
|
||||||
|
this.oldConfig = oldConfig;
|
||||||
|
this.newConfig = newConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Repository getRepository() {
|
||||||
|
return repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GitRepositoryConfig getOldConfig() {
|
||||||
|
return oldConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GitRepositoryConfig getNewConfig() {
|
||||||
|
return newConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
|
import de.otto.edison.hal.Links;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@SuppressWarnings("squid:S2160") // there is no proper semantic for equals on this dto
|
||||||
|
public class GitRepositoryConfigDto extends HalRepresentation {
|
||||||
|
|
||||||
|
private String defaultBranch;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||||
|
protected HalRepresentation add(Links links) {
|
||||||
|
return super.add(links);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import sonia.scm.plugin.Extension;
|
||||||
|
import sonia.scm.repository.GitRepositoryHandler;
|
||||||
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
|
import sonia.scm.repository.RepositoryManager;
|
||||||
|
import sonia.scm.web.AbstractRepositoryJsonEnricher;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
|
@Extension
|
||||||
|
public class GitRepositoryConfigEnricher extends AbstractRepositoryJsonEnricher {
|
||||||
|
|
||||||
|
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||||
|
private final RepositoryManager manager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GitRepositoryConfigEnricher(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper, RepositoryManager manager) {
|
||||||
|
super(objectMapper);
|
||||||
|
this.scmPathInfoStore = scmPathInfoStore;
|
||||||
|
this.manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void enrichRepositoryNode(JsonNode repositoryNode, String namespace, String name) {
|
||||||
|
if (GitRepositoryHandler.TYPE_NAME.equals(manager.get(new NamespaceAndName(namespace, name)).getType())) {
|
||||||
|
String repositoryConfigLink = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class)
|
||||||
|
.method("getRepositoryConfig")
|
||||||
|
.parameters(namespace, name)
|
||||||
|
.href();
|
||||||
|
addLink(repositoryNode, "configuration", repositoryConfigLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import de.otto.edison.hal.Links;
|
||||||
|
import org.mapstruct.AfterMapping;
|
||||||
|
import org.mapstruct.Context;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.MappingTarget;
|
||||||
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryPermissions;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static de.otto.edison.hal.Link.link;
|
||||||
|
import static de.otto.edison.hal.Links.linkingTo;
|
||||||
|
|
||||||
|
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
|
||||||
|
@SuppressWarnings("squid:S3306")
|
||||||
|
@Mapper
|
||||||
|
public abstract class GitRepositoryConfigMapper {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ScmPathInfoStore scmPathInfoStore;
|
||||||
|
|
||||||
|
public abstract GitRepositoryConfigDto map(GitRepositoryConfig config, @Context Repository repository);
|
||||||
|
public abstract GitRepositoryConfig map(GitRepositoryConfigDto dto);
|
||||||
|
|
||||||
|
@AfterMapping
|
||||||
|
void appendLinks(@MappingTarget GitRepositoryConfigDto target, @Context Repository repository) {
|
||||||
|
Links.Builder linksBuilder = linkingTo().self(self());
|
||||||
|
if (RepositoryPermissions.modify(repository).isPermitted()) {
|
||||||
|
linksBuilder.single(link("update", update()));
|
||||||
|
}
|
||||||
|
target.add(linksBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String self() {
|
||||||
|
LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), GitConfigResource.class);
|
||||||
|
return linkBuilder.method("get").parameters().href();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String update() {
|
||||||
|
LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), GitConfigResource.class);
|
||||||
|
return linkBuilder.method("update").parameters().href();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||||
|
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryManager;
|
||||||
|
import sonia.scm.store.ConfigurationStore;
|
||||||
|
import sonia.scm.web.GitVndMediaType;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
import static sonia.scm.NotFoundException.notFound;
|
||||||
|
|
||||||
|
public class GitRepositoryConfigResource {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(GitRepositoryConfigResource.class);
|
||||||
|
|
||||||
|
private final GitRepositoryConfigMapper repositoryConfigMapper;
|
||||||
|
private final RepositoryManager repositoryManager;
|
||||||
|
private final GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GitRepositoryConfigResource(GitRepositoryConfigMapper repositoryConfigMapper, RepositoryManager repositoryManager, GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider) {
|
||||||
|
this.repositoryConfigMapper = repositoryConfigMapper;
|
||||||
|
this.repositoryManager = repositoryManager;
|
||||||
|
this.gitRepositoryConfigStoreProvider = gitRepositoryConfigStoreProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/")
|
||||||
|
@Produces(GitVndMediaType.GIT_REPOSITORY_CONFIG)
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 200, condition = "success"),
|
||||||
|
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||||
|
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository config"),
|
||||||
|
@ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
public Response getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) {
|
||||||
|
Repository repository = getRepository(namespace, name);
|
||||||
|
ConfigurationStore<GitRepositoryConfig> repositoryConfigStore = getStore(repository);
|
||||||
|
GitRepositoryConfig config = repositoryConfigStore.get();
|
||||||
|
GitRepositoryConfigDto dto = repositoryConfigMapper.map(config, repository);
|
||||||
|
return Response.ok(dto).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("/")
|
||||||
|
@Consumes(GitVndMediaType.GIT_REPOSITORY_CONFIG)
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 204, condition = "update success"),
|
||||||
|
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||||
|
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the privilege to change this repositories config"),
|
||||||
|
@ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available/name available"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
public Response setRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name, GitRepositoryConfigDto dto) {
|
||||||
|
Repository repository = getRepository(namespace, name);
|
||||||
|
ConfigurationStore<GitRepositoryConfig> repositoryConfigStore = getStore(repository);
|
||||||
|
GitRepositoryConfig config = repositoryConfigMapper.map(dto);
|
||||||
|
repositoryConfigStore.set(config);
|
||||||
|
LOG.info("git default branch of repository {} has changed, sending clear cache event", repository.getNamespaceAndName());
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Repository getRepository(@PathParam("namespace") String namespace, @PathParam("name") String name) {
|
||||||
|
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||||
|
Repository repository = repositoryManager.get(namespaceAndName);
|
||||||
|
if (repository == null) {
|
||||||
|
throw notFound(entity(namespaceAndName));
|
||||||
|
}
|
||||||
|
return repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigurationStore<GitRepositoryConfig> getStore(Repository repository) {
|
||||||
|
return gitRepositoryConfigStoreProvider.get(repository);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.store.ConfigurationStore;
|
||||||
|
import sonia.scm.store.ConfigurationStoreFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
public class GitRepositoryConfigStoreProvider {
|
||||||
|
|
||||||
|
private final ConfigurationStoreFactory configurationStoreFactory;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GitRepositoryConfigStoreProvider(ConfigurationStoreFactory configurationStoreFactory) {
|
||||||
|
this.configurationStoreFactory = configurationStoreFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigurationStore<GitRepositoryConfig> get(Repository repository) {
|
||||||
|
return new StoreWrapper(configurationStoreFactory.withType(GitRepositoryConfig.class).withName("gitConfig").forRepository(repository).build(), repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class StoreWrapper implements ConfigurationStore<GitRepositoryConfig> {
|
||||||
|
|
||||||
|
private final ConfigurationStore<GitRepositoryConfig> delegate;
|
||||||
|
private final Repository repository;
|
||||||
|
|
||||||
|
private StoreWrapper(ConfigurationStore<GitRepositoryConfig> delegate, Repository repository) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GitRepositoryConfig get() {
|
||||||
|
GitRepositoryConfig config = delegate.get();
|
||||||
|
if (config == null) {
|
||||||
|
return new GitRepositoryConfig();
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void set(GitRepositoryConfig newConfig) {
|
||||||
|
GitRepositoryConfig oldConfig = get();
|
||||||
|
delegate.set(newConfig);
|
||||||
|
ScmEventBus.getInstance().post(new GitRepositoryConfigChangedEvent(repository, oldConfig, newConfig));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2014, Sebastian Sdorra
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer.
|
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from this
|
|
||||||
* software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package sonia.scm.repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constants for Git.
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
* @since 1.50
|
|
||||||
*/
|
|
||||||
public final class GitConstants {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default branch repository property.
|
|
||||||
*/
|
|
||||||
public static final String PROPERTY_DEFAULT_BRANCH = "git.default-branch";
|
|
||||||
|
|
||||||
private GitConstants() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2014, Sebastian Sdorra
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from this
|
||||||
|
* software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||||
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*
|
||||||
|
* http://bitbucket.org/sdorra/scm-manager
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Constants;
|
||||||
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.eclipse.jgit.lib.RefUpdate;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GitHeadModifier is able to modify the head of a git repository.
|
||||||
|
*
|
||||||
|
* @author Sebastian Sdorra
|
||||||
|
* @since 1.61
|
||||||
|
*/
|
||||||
|
public class GitHeadModifier {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(GitHeadModifier.class);
|
||||||
|
|
||||||
|
private final GitRepositoryHandler repositoryHandler;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GitHeadModifier(GitRepositoryHandler repositoryHandler) {
|
||||||
|
this.repositoryHandler = repositoryHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the repositories head points to the given branch. The method will return {@code false} if the
|
||||||
|
* repositories head points already to the given branch.
|
||||||
|
*
|
||||||
|
* @param repository repository to modify
|
||||||
|
* @param newHead branch which should be the new head of the repository
|
||||||
|
*
|
||||||
|
* @return {@code true} if the head has changed
|
||||||
|
*/
|
||||||
|
public boolean ensure(Repository repository, String newHead) {
|
||||||
|
try (org.eclipse.jgit.lib.Repository gitRepository = open(repository)) {
|
||||||
|
String currentHead = resolve(gitRepository);
|
||||||
|
if (!Objects.equals(currentHead, newHead)) {
|
||||||
|
return modify(gitRepository, newHead);
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.warn("failed to change head of repository", ex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolve(org.eclipse.jgit.lib.Repository gitRepository) throws IOException {
|
||||||
|
Ref ref = gitRepository.getRefDatabase().getRef(Constants.HEAD);
|
||||||
|
if ( ref.isSymbolic() ) {
|
||||||
|
ref = ref.getTarget();
|
||||||
|
}
|
||||||
|
return GitUtil.getBranch(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean modify(org.eclipse.jgit.lib.Repository gitRepository, String newHead) throws IOException {
|
||||||
|
RefUpdate refUpdate = gitRepository.getRefDatabase().newUpdate(Constants.HEAD, true);
|
||||||
|
refUpdate.setForceUpdate(true);
|
||||||
|
RefUpdate.Result result = refUpdate.link(Constants.R_HEADS + newHead);
|
||||||
|
return result == RefUpdate.Result.FORCED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private org.eclipse.jgit.lib.Repository open(Repository repository) throws IOException {
|
||||||
|
File directory = repositoryHandler.getDirectory(repository.getId());
|
||||||
|
return GitUtil.open(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
|
||||||
|
@XmlRootElement(name = "config")
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class GitRepositoryConfig {
|
||||||
|
|
||||||
|
public GitRepositoryConfig() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public GitRepositoryConfig(String defaultBranch) {
|
||||||
|
this.defaultBranch = defaultBranch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultBranch;
|
||||||
|
|
||||||
|
public String getDefaultBranch() {
|
||||||
|
return defaultBranch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultBranch(String defaultBranch) {
|
||||||
|
this.defaultBranch = defaultBranch;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,15 +31,13 @@
|
|||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
import com.github.legman.Subscribe;
|
import com.github.legman.Subscribe;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import com.google.common.base.Objects;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import sonia.scm.EagerSingleton;
|
import sonia.scm.EagerSingleton;
|
||||||
import sonia.scm.HandlerEventType;
|
import sonia.scm.api.v2.resources.GitRepositoryConfigChangedEvent;
|
||||||
import sonia.scm.event.ScmEventBus;
|
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||||
import sonia.scm.plugin.Extension;
|
import sonia.scm.plugin.Extension;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository listener which handles git related repository events.
|
* Repository listener which handles git related repository events.
|
||||||
*
|
*
|
||||||
@@ -50,10 +48,14 @@ import sonia.scm.plugin.Extension;
|
|||||||
@EagerSingleton
|
@EagerSingleton
|
||||||
public class GitRepositoryModifyListener {
|
public class GitRepositoryModifyListener {
|
||||||
|
|
||||||
/**
|
private final GitHeadModifier headModifier;
|
||||||
* the logger for GitRepositoryModifyListener
|
private final GitRepositoryConfigStoreProvider storeProvider;
|
||||||
*/
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GitRepositoryModifyListener.class);
|
@Inject
|
||||||
|
public GitRepositoryModifyListener(GitHeadModifier headModifier, GitRepositoryConfigStoreProvider storeProvider) {
|
||||||
|
this.headModifier = headModifier;
|
||||||
|
this.storeProvider = storeProvider;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives {@link RepositoryModificationEvent} and fires a {@link ClearRepositoryCacheEvent} if
|
* Receives {@link RepositoryModificationEvent} and fires a {@link ClearRepositoryCacheEvent} if
|
||||||
@@ -62,36 +64,12 @@ public class GitRepositoryModifyListener {
|
|||||||
* @param event repository modification event
|
* @param event repository modification event
|
||||||
*/
|
*/
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void handleEvent(RepositoryModificationEvent event){
|
public void handleEvent(GitRepositoryConfigChangedEvent event){
|
||||||
Repository repository = event.getItem();
|
Repository repository = event.getRepository();
|
||||||
|
|
||||||
if ( isModifyEvent(event) &&
|
String defaultBranch = storeProvider.get(repository).get().getDefaultBranch();
|
||||||
isGitRepository(event.getItem()) &&
|
if (defaultBranch != null) {
|
||||||
hasDefaultBranchChanged(event.getItemBeforeModification(), repository))
|
headModifier.ensure(repository, defaultBranch);
|
||||||
{
|
|
||||||
logger.info("git default branch of repository {} has changed, sending clear cache event", repository.getId());
|
|
||||||
sendClearRepositoryCacheEvent(repository);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
protected void sendClearRepositoryCacheEvent(Repository repository) {
|
|
||||||
ScmEventBus.getInstance().post(new ClearRepositoryCacheEvent(repository));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isModifyEvent(RepositoryEvent event) {
|
|
||||||
return event.getEventType() == HandlerEventType.MODIFY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isGitRepository(Repository repository) {
|
|
||||||
return GitRepositoryHandler.TYPE_NAME.equals(repository.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasDefaultBranchChanged(Repository old, Repository current) {
|
|
||||||
return !Objects.equal(
|
|
||||||
old.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH),
|
|
||||||
current.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import org.eclipse.jgit.lib.Ref;
|
|||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.repository.GitConstants;
|
|
||||||
import sonia.scm.repository.GitUtil;
|
import sonia.scm.repository.GitUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -110,7 +109,7 @@ public class AbstractGitCommand
|
|||||||
|
|
||||||
protected Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
|
protected Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
|
||||||
if ( Strings.isNullOrEmpty(requestedBranch) ) {
|
if ( Strings.isNullOrEmpty(requestedBranch) ) {
|
||||||
String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH);
|
String defaultBranchName = context.getConfig().getDefaultBranch();
|
||||||
if (!Strings.isNullOrEmpty(defaultBranchName)) {
|
if (!Strings.isNullOrEmpty(defaultBranchName)) {
|
||||||
return GitUtil.getBranchId(gitRepository, defaultBranchName);
|
return GitUtil.getBranchId(gitRepository, defaultBranchName);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ package sonia.scm.repository.spi;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||||
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
import sonia.scm.repository.GitUtil;
|
import sonia.scm.repository.GitUtil;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
|
||||||
@@ -68,10 +70,11 @@ public class GitContext implements Closeable
|
|||||||
* @param directory
|
* @param directory
|
||||||
* @param repository
|
* @param repository
|
||||||
*/
|
*/
|
||||||
public GitContext(File directory, Repository repository)
|
public GitContext(File directory, Repository repository, GitRepositoryConfigStoreProvider storeProvider)
|
||||||
{
|
{
|
||||||
this.directory = directory;
|
this.directory = directory;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
|
this.storeProvider = storeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
@@ -117,11 +120,25 @@ public class GitContext implements Closeable
|
|||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GitRepositoryConfig getConfig() {
|
||||||
|
GitRepositoryConfig config = storeProvider.get(repository).get();
|
||||||
|
if (config == null) {
|
||||||
|
return new GitRepositoryConfig();
|
||||||
|
} else {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setConfig(GitRepositoryConfig newConfig) {
|
||||||
|
storeProvider.get(repository).set(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private final File directory;
|
private final File directory;
|
||||||
private final Repository repository;
|
private final Repository repository;
|
||||||
|
private final GitRepositoryConfigStoreProvider storeProvider;
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private org.eclipse.jgit.lib.Repository gitRepository;
|
private org.eclipse.jgit.lib.Repository gitRepository;
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
|||||||
ObjectId ancestorId = null;
|
ObjectId ancestorId = null;
|
||||||
|
|
||||||
if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) {
|
if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) {
|
||||||
ancestorId = computeCommonAncestor(request, repository, startId, branch);
|
ancestorId = repository.resolve(request.getAncestorChangeset());
|
||||||
}
|
}
|
||||||
|
|
||||||
revWalk = new RevWalk(repository);
|
revWalk = new RevWalk(repository);
|
||||||
@@ -225,16 +225,15 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
|||||||
revWalk.markStart(revWalk.lookupCommit(branch.getObjectId()));
|
revWalk.markStart(revWalk.lookupCommit(branch.getObjectId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ancestorId != null) {
|
||||||
|
revWalk.markUninteresting(revWalk.lookupCommit(ancestorId));
|
||||||
|
}
|
||||||
|
|
||||||
Iterator<RevCommit> iterator = revWalk.iterator();
|
Iterator<RevCommit> iterator = revWalk.iterator();
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
RevCommit commit = iterator.next();
|
RevCommit commit = iterator.next();
|
||||||
|
|
||||||
if (commit.getId().equals(ancestorId)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((counter >= start)
|
if ((counter >= start)
|
||||||
&& ((limit < 0) || (counter < start + limit))) {
|
&& ((limit < 0) || (counter < start + limit))) {
|
||||||
changesetList.add(converter.createChangeset(commit));
|
changesetList.add(converter.createChangeset(commit));
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||||
import sonia.scm.repository.Feature;
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.GitRepositoryHandler;
|
import sonia.scm.repository.GitRepositoryHandler;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
@@ -73,10 +74,10 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
|||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
//~--- constructors ---------------------------------------------------------
|
||||||
|
|
||||||
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository) {
|
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider) {
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.context = new GitContext(handler.getDirectory(repository.getId()), repository);
|
this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ package sonia.scm.repository.spi;
|
|||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||||
import sonia.scm.plugin.Extension;
|
import sonia.scm.plugin.Extension;
|
||||||
import sonia.scm.repository.GitRepositoryHandler;
|
import sonia.scm.repository.GitRepositoryHandler;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
@@ -47,10 +48,12 @@ import sonia.scm.repository.Repository;
|
|||||||
public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
|
public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
|
||||||
|
|
||||||
private final GitRepositoryHandler handler;
|
private final GitRepositoryHandler handler;
|
||||||
|
private final GitRepositoryConfigStoreProvider storeProvider;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GitRepositoryServiceResolver(GitRepositoryHandler handler) {
|
public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider) {
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
|
this.storeProvider = storeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -58,7 +61,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
|
|||||||
GitRepositoryServiceProvider provider = null;
|
GitRepositoryServiceProvider provider = null;
|
||||||
|
|
||||||
if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
|
if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
|
||||||
provider = new GitRepositoryServiceProvider(handler, repository);
|
provider = new GitRepositoryServiceProvider(handler, repository, storeProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider;
|
return provider;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import org.eclipse.jgit.transport.ScmTransportProtocol;
|
|||||||
import org.mapstruct.factory.Mappers;
|
import org.mapstruct.factory.Mappers;
|
||||||
import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper;
|
import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper;
|
||||||
import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper;
|
import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper;
|
||||||
|
import sonia.scm.api.v2.resources.GitRepositoryConfigMapper;
|
||||||
import sonia.scm.plugin.Extension;
|
import sonia.scm.plugin.Extension;
|
||||||
import sonia.scm.repository.GitWorkdirFactory;
|
import sonia.scm.repository.GitWorkdirFactory;
|
||||||
import sonia.scm.repository.spi.SimpleGitWorkdirFactory;
|
import sonia.scm.repository.spi.SimpleGitWorkdirFactory;
|
||||||
@@ -65,6 +66,7 @@ public class GitServletModule extends ServletModule
|
|||||||
|
|
||||||
bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass());
|
bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass());
|
||||||
bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass());
|
bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass());
|
||||||
|
bind(GitRepositoryConfigMapper.class).to(Mappers.getMapper(GitRepositoryConfigMapper.class).getClass());
|
||||||
|
|
||||||
bind(GitWorkdirFactory.class).to(SimpleGitWorkdirFactory.class);
|
bind(GitWorkdirFactory.class).to(SimpleGitWorkdirFactory.class);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package sonia.scm.web;
|
|||||||
|
|
||||||
public class GitVndMediaType {
|
public class GitVndMediaType {
|
||||||
public static final String GIT_CONFIG = VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX;
|
public static final String GIT_CONFIG = VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX;
|
||||||
|
public static final String GIT_REPOSITORY_CONFIG = VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX;
|
||||||
|
|
||||||
private GitVndMediaType() {
|
private GitVndMediaType() {
|
||||||
}
|
}
|
||||||
|
|||||||
155
scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.js
Normal file
155
scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.js
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {apiClient, BranchSelector, ErrorPage, Loading, SubmitButton} from "@scm-manager/ui-components";
|
||||||
|
import type {Branch, Repository} from "@scm-manager/ui-types";
|
||||||
|
import {translate} from "react-i18next";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
repository: Repository,
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
loadingBranches: boolean,
|
||||||
|
loadingDefaultBranch: boolean,
|
||||||
|
submitPending: boolean,
|
||||||
|
error?: Error,
|
||||||
|
branches: Branch[],
|
||||||
|
selectedBranchName?: string,
|
||||||
|
defaultBranchChanged: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
const GIT_CONFIG_CONTENT_TYPE = "application/vnd.scmm-gitConfig+json";
|
||||||
|
|
||||||
|
class RepositoryConfig extends React.Component<Props, State> {
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
loadingBranches: true,
|
||||||
|
loadingDefaultBranch: true,
|
||||||
|
submitPending: false,
|
||||||
|
branches: [],
|
||||||
|
defaultBranchChanged: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { repository } = this.props;
|
||||||
|
this.setState({ ...this.state, loadingBranches: true });
|
||||||
|
apiClient
|
||||||
|
.get(repository._links.branches.href)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(payload => payload._embedded.branches)
|
||||||
|
.then(branches =>
|
||||||
|
this.setState({ ...this.state, branches, loadingBranches: false })
|
||||||
|
)
|
||||||
|
.catch(error => this.setState({ ...this.state, error }));
|
||||||
|
|
||||||
|
this.setState({ ...this.state, loadingDefaultBranch: true });
|
||||||
|
apiClient
|
||||||
|
.get(repository._links.configuration.href)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(payload => payload.defaultBranch)
|
||||||
|
.then(selectedBranchName =>
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
selectedBranchName,
|
||||||
|
loadingDefaultBranch: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => this.setState({ ...this.state, error }));
|
||||||
|
}
|
||||||
|
|
||||||
|
branchSelected = (branch: Branch) => {
|
||||||
|
if (!branch) {
|
||||||
|
this.setState({ ...this.state, selectedBranchName: undefined, defaultBranchChanged: false});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ ...this.state, selectedBranchName: branch.name, defaultBranchChanged: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
submit = (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { repository } = this.props;
|
||||||
|
const newConfig = {
|
||||||
|
defaultBranch: this.state.selectedBranchName
|
||||||
|
};
|
||||||
|
this.setState({ ...this.state, submitPending: true });
|
||||||
|
apiClient
|
||||||
|
.put(
|
||||||
|
repository._links.configuration.href,
|
||||||
|
newConfig,
|
||||||
|
GIT_CONFIG_CONTENT_TYPE
|
||||||
|
)
|
||||||
|
.then(() =>
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
submitPending: false,
|
||||||
|
defaultBranchChanged: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => this.setState({ ...this.state, error }));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
const { loadingBranches, loadingDefaultBranch, submitPending, error } = this.state;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<ErrorPage
|
||||||
|
title={t("scm-git-plugin.repo-config.error.title")}
|
||||||
|
subtitle={t("scm-git-plugin.repo-config.error.subtitle")}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(loadingBranches || loadingDefaultBranch)) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderBranchChangedNotification()}
|
||||||
|
<form onSubmit={this.submit}>
|
||||||
|
<BranchSelector
|
||||||
|
label={t("scm-git-plugin.repo-config.default-branch")}
|
||||||
|
branches={this.state.branches}
|
||||||
|
selected={this.branchSelected}
|
||||||
|
selectedBranch={this.state.selectedBranchName}
|
||||||
|
/>
|
||||||
|
<SubmitButton
|
||||||
|
label={t("scm-git-plugin.repo-config.submit")}
|
||||||
|
loading={submitPending}
|
||||||
|
disabled={!this.state.selectedBranchName}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBranchChangedNotification = () => {
|
||||||
|
if (this.state.defaultBranchChanged) {
|
||||||
|
return (
|
||||||
|
<div className="notification is-primary">
|
||||||
|
<button
|
||||||
|
className="delete"
|
||||||
|
onClick={() =>
|
||||||
|
this.setState({ ...this.state, defaultBranchChanged: false })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{this.props.t("scm-git-plugin.repo-config.success")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("plugins")(RepositoryConfig);
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import { binder } from "@scm-manager/ui-extensions";
|
import React from "react";
|
||||||
|
import {binder} from "@scm-manager/ui-extensions";
|
||||||
import ProtocolInformation from "./ProtocolInformation";
|
import ProtocolInformation from "./ProtocolInformation";
|
||||||
import GitAvatar from "./GitAvatar";
|
import GitAvatar from "./GitAvatar";
|
||||||
|
|
||||||
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
|
import {ConfigurationBinder as cfgBinder} from "@scm-manager/ui-components";
|
||||||
import GitGlobalConfiguration from "./GitGlobalConfiguration";
|
import GitGlobalConfiguration from "./GitGlobalConfiguration";
|
||||||
import GitMergeInformation from "./GitMergeInformation";
|
import GitMergeInformation from "./GitMergeInformation";
|
||||||
|
import RepositoryConfig from "./RepositoryConfig";
|
||||||
|
|
||||||
// repository
|
// repository
|
||||||
|
|
||||||
@@ -13,10 +15,29 @@ const gitPredicate = (props: Object) => {
|
|||||||
return props.repository && props.repository.type === "git";
|
return props.repository && props.repository.type === "git";
|
||||||
};
|
};
|
||||||
|
|
||||||
binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate);
|
binder.bind(
|
||||||
binder.bind("repos.repository-merge.information", GitMergeInformation, gitPredicate);
|
"repos.repository-details.information",
|
||||||
|
ProtocolInformation,
|
||||||
|
gitPredicate
|
||||||
|
);
|
||||||
|
binder.bind(
|
||||||
|
"repos.repository-merge.information",
|
||||||
|
GitMergeInformation,
|
||||||
|
gitPredicate
|
||||||
|
);
|
||||||
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);
|
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);
|
||||||
|
|
||||||
|
cfgBinder.bindRepository(
|
||||||
|
"/configuration",
|
||||||
|
"scm-git-plugin.repo-config.link",
|
||||||
|
"configuration",
|
||||||
|
RepositoryConfig
|
||||||
|
);
|
||||||
// global config
|
// global config
|
||||||
|
|
||||||
cfgBinder.bindGlobal("/git", "scm-git-plugin.config.link", "gitConfig", GitGlobalConfiguration);
|
cfgBinder.bindGlobal(
|
||||||
|
"/git",
|
||||||
|
"scm-git-plugin.config.link",
|
||||||
|
"gitConfig",
|
||||||
|
GitGlobalConfiguration
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"scm-git-plugin": {
|
"scm-git-plugin": {
|
||||||
"information": {
|
"information": {
|
||||||
"clone" : "Clone the repository",
|
"clone": "Clone the repository",
|
||||||
"create" : "Create a new repository",
|
"create": "Create a new repository",
|
||||||
"replace" : "Push an existing repository",
|
"replace": "Push an existing repository",
|
||||||
"merge": {
|
"merge": {
|
||||||
"heading": "How to merge source branch into target branch",
|
"heading": "How to merge source branch into target branch",
|
||||||
"checkout": "1. Make sure your workspace is clean and checkout target branch",
|
"checkout": "1. Make sure your workspace is clean and checkout target branch",
|
||||||
@@ -22,6 +22,16 @@
|
|||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"disabledHelpText": "Enable or disable the Git plugin",
|
"disabledHelpText": "Enable or disable the Git plugin",
|
||||||
"submit": "Submit"
|
"submit": "Submit"
|
||||||
|
},
|
||||||
|
"repo-config": {
|
||||||
|
"link": "Configuration",
|
||||||
|
"default-branch": "Default branch",
|
||||||
|
"submit": "Submit",
|
||||||
|
"error": {
|
||||||
|
"title": "Error",
|
||||||
|
"subtitle": "Something went wrong"
|
||||||
|
},
|
||||||
|
"success": "Default branch changed!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import com.github.sdorra.shiro.ShiroRule;
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
import com.github.sdorra.shiro.SubjectAware;
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
import org.jboss.resteasy.core.Dispatcher;
|
import org.jboss.resteasy.core.Dispatcher;
|
||||||
@@ -14,22 +12,33 @@ import org.junit.Test;
|
|||||||
import org.junit.rules.ExpectedException;
|
import org.junit.rules.ExpectedException;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Spy;
|
||||||
import org.mockito.runners.MockitoJUnitRunner;
|
import org.mockito.runners.MockitoJUnitRunner;
|
||||||
import sonia.scm.repository.GitConfig;
|
import sonia.scm.repository.GitConfig;
|
||||||
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
import sonia.scm.repository.GitRepositoryHandler;
|
import sonia.scm.repository.GitRepositoryHandler;
|
||||||
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryManager;
|
||||||
|
import sonia.scm.store.ConfigurationStore;
|
||||||
|
import sonia.scm.store.ConfigurationStoreFactory;
|
||||||
import sonia.scm.web.GitVndMediaType;
|
import sonia.scm.web.GitVndMediaType;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import static com.google.inject.util.Providers.of;
|
||||||
import static junit.framework.TestCase.assertTrue;
|
import static junit.framework.TestCase.assertTrue;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@SubjectAware(
|
@SubjectAware(
|
||||||
@@ -55,30 +64,48 @@ public class GitConfigResourceTest {
|
|||||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
private ScmPathInfoStore scmPathInfoStore;
|
private ScmPathInfoStore scmPathInfoStore;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RepositoryManager repositoryManager;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private GitConfigToGitConfigDtoMapperImpl configToDtoMapper;
|
private GitConfigToGitConfigDtoMapperImpl configToDtoMapper;
|
||||||
|
@InjectMocks
|
||||||
|
private GitRepositoryConfigMapperImpl repositoryConfigMapper;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private GitRepositoryHandler repositoryHandler;
|
private GitRepositoryHandler repositoryHandler;
|
||||||
|
|
||||||
|
@Mock(answer = Answers.CALLS_REAL_METHODS)
|
||||||
|
private ConfigurationStoreFactory configurationStoreFactory;
|
||||||
|
@Spy
|
||||||
|
private ConfigurationStore<Object> configurationStore;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Object> configurationStoreCaptor;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void prepareEnvironment() {
|
public void prepareEnvironment() {
|
||||||
GitConfig gitConfig = createConfiguration();
|
GitConfig gitConfig = createConfiguration();
|
||||||
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
|
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
|
||||||
GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler);
|
GitRepositoryConfigResource gitRepositoryConfigResource = new GitRepositoryConfigResource(repositoryConfigMapper, repositoryManager, new GitRepositoryConfigStoreProvider(configurationStoreFactory));
|
||||||
|
GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, of(gitRepositoryConfigResource));
|
||||||
dispatcher.getRegistry().addSingletonResource(gitConfigResource);
|
dispatcher.getRegistry().addSingletonResource(gitConfigResource);
|
||||||
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void initConfigStore() {
|
||||||
|
when(configurationStoreFactory.getStore(any())).thenReturn(configurationStore);
|
||||||
|
doNothing().when(configurationStore).set(configurationStoreCaptor.capture());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readWrite")
|
@SubjectAware(username = "readWrite")
|
||||||
public void shouldGetGitConfig() throws URISyntaxException, IOException {
|
public void shouldGetGitConfig() throws URISyntaxException {
|
||||||
MockHttpResponse response = get();
|
MockHttpResponse response = get();
|
||||||
|
|
||||||
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
|
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
|
||||||
|
|
||||||
String responseString = response.getContentAsString();
|
String responseString = response.getContentAsString();
|
||||||
ObjectNode responseJson = new ObjectMapper().readValue(responseString, ObjectNode.class);
|
|
||||||
|
|
||||||
assertTrue(responseString.contains("\"disabled\":false"));
|
assertTrue(responseString.contains("\"disabled\":false"));
|
||||||
assertTrue(responseString.contains("\"gcExpression\":\"valid Git GC Cron Expression\""));
|
assertTrue(responseString.contains("\"gcExpression\":\"valid Git GC Cron Expression\""));
|
||||||
@@ -88,7 +115,7 @@ public class GitConfigResourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readWrite")
|
@SubjectAware(username = "readWrite")
|
||||||
public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException, IOException {
|
public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException {
|
||||||
when(repositoryHandler.getConfig()).thenReturn(null);
|
when(repositoryHandler.getConfig()).thenReturn(null);
|
||||||
|
|
||||||
MockHttpResponse response = get();
|
MockHttpResponse response = get();
|
||||||
@@ -124,12 +151,84 @@ public class GitConfigResourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readOnly")
|
@SubjectAware(username = "readOnly")
|
||||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, IOException {
|
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:write:git]");
|
thrown.expectMessage("Subject does not have permission [configuration:write:git]");
|
||||||
|
|
||||||
put();
|
put();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SubjectAware(username = "writeOnly")
|
||||||
|
public void shouldReadDefaultRepositoryConfig() throws URISyntaxException {
|
||||||
|
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");
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
|
||||||
|
assertThat(response.getContentAsString())
|
||||||
|
.contains("\"defaultBranch\":null")
|
||||||
|
.contains("self")
|
||||||
|
.contains("update");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SubjectAware(username = "readOnly")
|
||||||
|
public void shouldNotHaveUpdateLinkForReadOnlyUser() throws URISyntaxException {
|
||||||
|
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");
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
|
||||||
|
assertThat(response.getContentAsString())
|
||||||
|
.contains("\"defaultBranch\":null")
|
||||||
|
.contains("self")
|
||||||
|
.doesNotContain("update");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SubjectAware(username = "writeOnly")
|
||||||
|
public void shouldReadStoredRepositoryConfig() throws URISyntaxException {
|
||||||
|
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X"));
|
||||||
|
GitRepositoryConfig gitRepositoryConfig = new GitRepositoryConfig();
|
||||||
|
gitRepositoryConfig.setDefaultBranch("test");
|
||||||
|
when(configurationStore.get()).thenReturn(gitRepositoryConfig);
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X");
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
|
||||||
|
assertThat(response.getContentAsString())
|
||||||
|
.contains("\"defaultBranch\":\"test\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SubjectAware(username = "writeOnly")
|
||||||
|
public void shouldStoreChangedRepositoryConfig() throws URISyntaxException {
|
||||||
|
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X"));
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.put("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X")
|
||||||
|
.contentType(GitVndMediaType.GIT_REPOSITORY_CONFIG)
|
||||||
|
.content("{\"defaultBranch\": \"new\"}".getBytes());
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||||
|
assertThat(configurationStoreCaptor.getValue())
|
||||||
|
.isInstanceOfSatisfying(GitRepositoryConfig.class, x -> { })
|
||||||
|
.extracting("defaultBranch")
|
||||||
|
.containsExactly("new");
|
||||||
|
}
|
||||||
|
|
||||||
private MockHttpResponse get() throws URISyntaxException {
|
private MockHttpResponse get() throws URISyntaxException {
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2);
|
MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
@@ -153,6 +252,4 @@ public class GitConfigResourceTest {
|
|||||||
config.setDisabled(false);
|
config.setDisabled(false);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.google.common.io.Resources;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class GitRepositoryConfigEnricherTest {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private GitRepositoryConfigEnricher linkEnricher;
|
||||||
|
private JsonNode rootNode;
|
||||||
|
@Mock
|
||||||
|
private RepositoryManager manager;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void globalSetUp() {
|
||||||
|
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||||
|
pathInfoStore.set(() -> URI.create("/"));
|
||||||
|
Provider<ScmPathInfoStore> pathInfoStoreProvider = Providers.of(pathInfoStore);
|
||||||
|
|
||||||
|
linkEnricher = new GitRepositoryConfigEnricher(pathInfoStoreProvider, objectMapper, manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class ForSingleRepository {
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws IOException {
|
||||||
|
URL resource = Resources.getResource("sonia/scm/repository/repository-001.json");
|
||||||
|
rootNode = objectMapper.readTree(resource);
|
||||||
|
|
||||||
|
when(manager.get(new NamespaceAndName("scmadmin", "web-resources"))).thenReturn(new Repository("id", "git", "scmadmin", "web-resources"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldEnrichGitRepositories() {
|
||||||
|
JsonEnricherContext context = new JsonEnricherContext(
|
||||||
|
URI.create("/"),
|
||||||
|
MediaType.valueOf(VndMediaType.REPOSITORY),
|
||||||
|
rootNode
|
||||||
|
);
|
||||||
|
|
||||||
|
linkEnricher.enrich(context);
|
||||||
|
|
||||||
|
String configLink = context.getResponseEntity()
|
||||||
|
.get("_links")
|
||||||
|
.get("configuration")
|
||||||
|
.get("href")
|
||||||
|
.asText();
|
||||||
|
|
||||||
|
assertThat(configLink).isEqualTo("/v2/config/git/scmadmin/web-resources");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotEnrichOtherRepositories() {
|
||||||
|
when(manager.get(new NamespaceAndName("scmadmin", "web-resources"))).thenReturn(new Repository("id", "hg", "scmadmin", "web-resources"));
|
||||||
|
|
||||||
|
JsonEnricherContext context = new JsonEnricherContext(
|
||||||
|
URI.create("/"),
|
||||||
|
MediaType.valueOf(VndMediaType.REPOSITORY),
|
||||||
|
rootNode
|
||||||
|
);
|
||||||
|
|
||||||
|
linkEnricher.enrich(context);
|
||||||
|
|
||||||
|
JsonNode configLink = context.getResponseEntity()
|
||||||
|
.get("_links")
|
||||||
|
.get("configuration");
|
||||||
|
|
||||||
|
assertThat(configLink).isNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class ForRepositoryCollection {
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws IOException {
|
||||||
|
URL resource = Resources.getResource("sonia/scm/repository/repository-collection-001.json");
|
||||||
|
rootNode = objectMapper.readTree(resource);
|
||||||
|
|
||||||
|
when(manager.get(new NamespaceAndName("scmadmin", "web-resources"))).thenReturn(new Repository("id", "git", "scmadmin", "web-resources"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldEnrichAllRepositories() {
|
||||||
|
JsonEnricherContext context = new JsonEnricherContext(
|
||||||
|
URI.create("/"),
|
||||||
|
MediaType.valueOf(VndMediaType.REPOSITORY_COLLECTION),
|
||||||
|
rootNode
|
||||||
|
);
|
||||||
|
|
||||||
|
linkEnricher.enrich(context);
|
||||||
|
|
||||||
|
context.getResponseEntity()
|
||||||
|
.get("_embedded")
|
||||||
|
.withArray("repositories")
|
||||||
|
.elements()
|
||||||
|
.forEachRemaining(node -> {
|
||||||
|
String configLink = node
|
||||||
|
.get("_links")
|
||||||
|
.get("configuration")
|
||||||
|
.get("href")
|
||||||
|
.asText();
|
||||||
|
|
||||||
|
assertThat(configLink).isEqualTo("/v2/config/git/scmadmin/web-resources");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotModifyObjectsWithUnsupportedMediaType() throws IOException {
|
||||||
|
URL resource = Resources.getResource("sonia/scm/repository/repository-001.json");
|
||||||
|
rootNode = objectMapper.readTree(resource);
|
||||||
|
JsonEnricherContext context = new JsonEnricherContext(
|
||||||
|
URI.create("/"),
|
||||||
|
MediaType.valueOf(VndMediaType.USER),
|
||||||
|
rootNode
|
||||||
|
);
|
||||||
|
|
||||||
|
linkEnricher.enrich(context);
|
||||||
|
|
||||||
|
boolean hasNewPullRequestLink = context.getResponseEntity()
|
||||||
|
.get("_links")
|
||||||
|
.has("configuration");
|
||||||
|
|
||||||
|
assertThat(hasNewPullRequestLink).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2014, Sebastian Sdorra
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from this
|
||||||
|
* software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||||
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*
|
||||||
|
* http://bitbucket.org/sdorra/scm-manager
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.io.Files;
|
||||||
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.runners.MockitoJUnitRunner;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class GitHeadModifierTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private GitRepositoryHandler repositoryHandler;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private GitHeadModifier modifier;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEnsure() throws IOException, GitAPIException {
|
||||||
|
Repository repository = RepositoryTestData.createHeartOfGold("git");
|
||||||
|
File headFile = create(repository, "master");
|
||||||
|
|
||||||
|
boolean result = modifier.ensure(repository, "develop");
|
||||||
|
|
||||||
|
assertEquals("ref: refs/heads/develop", Files.readFirstLine(headFile, Charsets.UTF_8));
|
||||||
|
assertTrue(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEnsureWithSameBranch() throws IOException, GitAPIException {
|
||||||
|
Repository repository = RepositoryTestData.createHeartOfGold("git");
|
||||||
|
create(repository, "develop");
|
||||||
|
|
||||||
|
boolean result = modifier.ensure(repository, "develop");
|
||||||
|
|
||||||
|
assertFalse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File create(Repository repository, String head) throws IOException, GitAPIException {
|
||||||
|
File directory = temporaryFolder.newFolder();
|
||||||
|
|
||||||
|
Git.init()
|
||||||
|
.setBare(true)
|
||||||
|
.setDirectory(directory)
|
||||||
|
.call();
|
||||||
|
|
||||||
|
File headFile = new File(directory, "HEAD");
|
||||||
|
Files.write(String.format("ref: refs/heads/%s\n", head), headFile, Charsets.UTF_8);
|
||||||
|
|
||||||
|
when(repositoryHandler.getDirectory(repository.getId())).thenReturn(directory);
|
||||||
|
|
||||||
|
return headFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@ import static org.mockito.Mockito.when;
|
|||||||
/**
|
/**
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||||
public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2014, Sebastian Sdorra
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer.
|
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from this
|
|
||||||
* software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sonia.scm.repository;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
import org.junit.Before;
|
|
||||||
import sonia.scm.HandlerEventType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for {@link GitRepositoryModifyListener}.
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
public class GitRepositoryModifyListenerTest {
|
|
||||||
|
|
||||||
private GitRepositoryModifyTestListener repositoryModifyListener;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up test object.
|
|
||||||
*/
|
|
||||||
@Before
|
|
||||||
public void setUpObjectUnderTest(){
|
|
||||||
repositoryModifyListener = new GitRepositoryModifyTestListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests happy path.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testHandleEvent() {
|
|
||||||
Repository old = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master");
|
|
||||||
Repository current = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop");
|
|
||||||
|
|
||||||
RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old);
|
|
||||||
repositoryModifyListener.handleEvent(event);
|
|
||||||
|
|
||||||
assertNotNull(repositoryModifyListener.repository);
|
|
||||||
assertSame(current, repositoryModifyListener.repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests with new default branch.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testWithNewDefaultBranch() {
|
|
||||||
Repository old = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
Repository current = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop");
|
|
||||||
|
|
||||||
RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old);
|
|
||||||
repositoryModifyListener.handleEvent(event);
|
|
||||||
|
|
||||||
assertNotNull(repositoryModifyListener.repository);
|
|
||||||
assertSame(current, repositoryModifyListener.repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests with non git repositories.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testNonGitRepository(){
|
|
||||||
Repository old = RepositoryTestData.createHeartOfGold("hg");
|
|
||||||
old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master");
|
|
||||||
Repository current = RepositoryTestData.createHeartOfGold("hg");
|
|
||||||
current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop");
|
|
||||||
|
|
||||||
RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old);
|
|
||||||
repositoryModifyListener.handleEvent(event);
|
|
||||||
|
|
||||||
assertNull(repositoryModifyListener.repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests without default branch.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testWithoutDefaultBranch(){
|
|
||||||
Repository old = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
Repository current = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
|
|
||||||
RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old);
|
|
||||||
repositoryModifyListener.handleEvent(event);
|
|
||||||
|
|
||||||
assertNull(repositoryModifyListener.repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests with non modify event.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testNonModifyEvent(){
|
|
||||||
Repository old = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master");
|
|
||||||
Repository current = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop");
|
|
||||||
|
|
||||||
RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.CREATE, current, old);
|
|
||||||
repositoryModifyListener.handleEvent(event);
|
|
||||||
|
|
||||||
assertNull(repositoryModifyListener.repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests with non git repositories.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testNoModification(){
|
|
||||||
Repository old = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master");
|
|
||||||
Repository current = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master");
|
|
||||||
|
|
||||||
RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old);
|
|
||||||
repositoryModifyListener.handleEvent(event);
|
|
||||||
|
|
||||||
assertNull(repositoryModifyListener.repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class GitRepositoryModifyTestListener extends GitRepositoryModifyListener {
|
|
||||||
|
|
||||||
private Repository repository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void sendClearRepositoryCacheEvent(Repository repository) {
|
|
||||||
this.repository = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -35,6 +35,11 @@ package sonia.scm.repository.spi;
|
|||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import org.junit.After;
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -51,6 +56,7 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
|
|||||||
public void close()
|
public void close()
|
||||||
{
|
{
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
|
context.setConfig(new GitRepositoryConfig());
|
||||||
context.close();
|
context.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +71,7 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
|
|||||||
{
|
{
|
||||||
if (context == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
context = new GitContext(repositoryDirectory, repository);
|
context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(InMemoryConfigurationStoreFactory.create()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ package sonia.scm.repository.spi;
|
|||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||||
import sonia.scm.repository.BlameLine;
|
import sonia.scm.repository.BlameLine;
|
||||||
import sonia.scm.repository.BlameResult;
|
import sonia.scm.repository.BlameResult;
|
||||||
import sonia.scm.repository.GitConstants;
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
|
import sonia.scm.store.InMemoryConfigurationStoreFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@@ -73,7 +75,7 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase
|
|||||||
assertEquals("fcd0ef1831e4002ac43ea539f4094334c79ea9ec", result.getLine(1).getRevision());
|
assertEquals("fcd0ef1831e4002ac43ea539f4094334c79ea9ec", result.getLine(1).getRevision());
|
||||||
|
|
||||||
// set default branch and test again
|
// set default branch and test again
|
||||||
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
|
createContext().setConfig(new GitRepositoryConfig("test-branch"));
|
||||||
result = createCommand().getBlameResult(request);
|
result = createCommand().getBlameResult(request);
|
||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
assertEquals(1, result.getTotal());
|
assertEquals(1, result.getTotal());
|
||||||
|
|||||||
@@ -32,9 +32,11 @@
|
|||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
import sonia.scm.repository.FileObject;
|
import sonia.scm.repository.FileObject;
|
||||||
import sonia.scm.repository.GitConstants;
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
|
import sonia.scm.store.InMemoryConfigurationStoreFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -78,7 +80,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExplicitDefaultBranch() throws IOException {
|
public void testExplicitDefaultBranch() throws IOException {
|
||||||
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
|
createContext().setConfig(new GitRepositoryConfig("test-branch"));
|
||||||
|
|
||||||
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
|
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
|
||||||
assertNotNull(root);
|
assertNotNull(root);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import org.junit.Rule;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
import org.junit.rules.ExpectedException;
|
||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.repository.GitConstants;
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -67,7 +67,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
|
|||||||
assertEquals("a\nline for blame", execute(request));
|
assertEquals("a\nline for blame", execute(request));
|
||||||
|
|
||||||
// set default branch for repository and check again
|
// set default branch for repository and check again
|
||||||
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
|
createContext().setConfig(new GitRepositoryConfig("test-branch"));
|
||||||
assertEquals("a and b", execute(request));
|
assertEquals("a and b", execute(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ import org.eclipse.jgit.api.errors.GitAPIException;
|
|||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||||
import sonia.scm.repository.ChangesetPagingResult;
|
import sonia.scm.repository.ChangesetPagingResult;
|
||||||
|
import sonia.scm.store.InMemoryConfigurationStoreFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ public class GitIncomingCommandTest
|
|||||||
|
|
||||||
commit(outgoing, "added a");
|
commit(outgoing, "added a");
|
||||||
|
|
||||||
GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory, null), incomingRepository);
|
GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory, null, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())), incomingRepository);
|
||||||
PullCommandRequest req = new PullCommandRequest();
|
PullCommandRequest req = new PullCommandRequest();
|
||||||
req.setRemoteRepository(outgoingRepository);
|
req.setRemoteRepository(outgoingRepository);
|
||||||
pull.pull(req);
|
pull.pull(req);
|
||||||
@@ -187,7 +189,7 @@ public class GitIncomingCommandTest
|
|||||||
*/
|
*/
|
||||||
private GitIncomingCommand createCommand()
|
private GitIncomingCommand createCommand()
|
||||||
{
|
{
|
||||||
return new GitIncomingCommand(handler, new GitContext(incomingDirectory, null),
|
return new GitIncomingCommand(handler, new GitContext(incomingDirectory, null, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())),
|
||||||
incomingRepository);
|
incomingRepository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010, Sebastian Sdorra
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from this
|
||||||
|
* software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||||
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*
|
||||||
|
* http://bitbucket.org/sdorra/scm-manager
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import sonia.scm.repository.ChangesetPagingResult;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link GitLogCommand} with an ancestor commit. This test uses the following git repository:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* * 86e9ca0 (HEAD -> b) b5
|
||||||
|
* * d69edb3 Merge branch 'master' into b
|
||||||
|
* |\
|
||||||
|
* | * 946a8db (master) f
|
||||||
|
* | * b19b9cc e
|
||||||
|
* * | 3d6109c b4
|
||||||
|
* * | 6330653 b3
|
||||||
|
* * | a49a28e Merge branch 'master' into b
|
||||||
|
* |\ \
|
||||||
|
* | |/
|
||||||
|
* | * 0235584 d
|
||||||
|
* | * 20251c5 c
|
||||||
|
* * | 5023b85 b2
|
||||||
|
* * | 201ecc1 b1
|
||||||
|
* |/
|
||||||
|
* * 36b19e4 b
|
||||||
|
* * c2190a9 a
|
||||||
|
* </pre>
|
||||||
|
* @author Sebastian Sdorra
|
||||||
|
*/
|
||||||
|
public class GitLogCommandAncestorTest extends AbstractGitCommandTestBase
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected String getZippedRepositoryResource()
|
||||||
|
{
|
||||||
|
return "sonia/scm/repository/spi/scm-git-ancestor-test.zip";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetAncestor()
|
||||||
|
{
|
||||||
|
LogCommandRequest request = new LogCommandRequest();
|
||||||
|
|
||||||
|
request.setBranch("b");
|
||||||
|
request.setAncestorChangeset("master");
|
||||||
|
|
||||||
|
ChangesetPagingResult result = createCommand().getChangesets(request);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(7, result.getTotal());
|
||||||
|
assertEquals(7, result.getChangesets().size());
|
||||||
|
|
||||||
|
assertEquals("86e9ca012202b36865373a63c12ef4f4353506cd", result.getChangesets().get(0).getId());
|
||||||
|
assertEquals("d69edb314d07ab20ad626e3101597702d3510b5d", result.getChangesets().get(1).getId());
|
||||||
|
assertEquals("3d6109c4c830e91eaf12ac6a331a5fccd670fe3c", result.getChangesets().get(2).getId());
|
||||||
|
assertEquals("63306538d06924d6b254f86541c638021c001141", result.getChangesets().get(3).getId());
|
||||||
|
assertEquals("a49a28e0beb0ab55f985598d05b8628c2231c9b6", result.getChangesets().get(4).getId());
|
||||||
|
assertEquals("5023b850c2077db857593a3c0269329c254a370d", result.getChangesets().get(5).getId());
|
||||||
|
assertEquals("201ecc1131e6b99fb0a0fe9dcbc8c044383e1a07", result.getChangesets().get(6).getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private GitLogCommand createCommand()
|
||||||
|
{
|
||||||
|
return new GitLogCommand(createContext(), repository);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,9 +36,11 @@ package sonia.scm.repository.spi;
|
|||||||
|
|
||||||
import com.google.common.io.Files;
|
import com.google.common.io.Files;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
import sonia.scm.repository.Changeset;
|
import sonia.scm.repository.Changeset;
|
||||||
import sonia.scm.repository.ChangesetPagingResult;
|
import sonia.scm.repository.ChangesetPagingResult;
|
||||||
import sonia.scm.repository.GitConstants;
|
import sonia.scm.repository.ClearRepositoryCacheEvent;
|
||||||
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
import sonia.scm.repository.Modifications;
|
import sonia.scm.repository.Modifications;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -78,7 +80,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
|||||||
assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty()));
|
assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty()));
|
||||||
|
|
||||||
// set default branch and fetch again
|
// set default branch and fetch again
|
||||||
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
|
createContext().setConfig(new GitRepositoryConfig("test-branch"));
|
||||||
|
|
||||||
result = createCommand().getChangesets(new LogCommandRequest());
|
result = createCommand().getChangesets(new LogCommandRequest());
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void init() {
|
public void init() {
|
||||||
incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory, null), incomingRepository);
|
incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory, null, null), incomingRepository);
|
||||||
outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory, null), outgoingRepository);
|
outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory, null, null), outgoingRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -63,12 +63,12 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void pushOutgoingAndPullIncoming() throws IOException {
|
void pushOutgoingAndPullIncoming() throws IOException {
|
||||||
GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory, null),
|
GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory, null, null),
|
||||||
outgoingRepository);
|
outgoingRepository);
|
||||||
PushCommandRequest request = new PushCommandRequest();
|
PushCommandRequest request = new PushCommandRequest();
|
||||||
request.setRemoteRepository(incomingRepository);
|
request.setRemoteRepository(incomingRepository);
|
||||||
cmd.push(request);
|
cmd.push(request);
|
||||||
GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory, null),
|
GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory, null, null),
|
||||||
incomingRepository);
|
incomingRepository);
|
||||||
PullCommandRequest pullRequest = new PullCommandRequest();
|
PullCommandRequest pullRequest = new PullCommandRequest();
|
||||||
pullRequest.setRemoteRepository(incomingRepository);
|
pullRequest.setRemoteRepository(incomingRepository);
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ package sonia.scm.repository.spi;
|
|||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||||
import sonia.scm.repository.ChangesetPagingResult;
|
import sonia.scm.repository.ChangesetPagingResult;
|
||||||
|
import sonia.scm.store.InMemoryConfigurationStoreFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
|
|||||||
commit(outgoing, "added a");
|
commit(outgoing, "added a");
|
||||||
|
|
||||||
GitPushCommand push = new GitPushCommand(handler,
|
GitPushCommand push = new GitPushCommand(handler,
|
||||||
new GitContext(outgoingDirectory, null),
|
new GitContext(outgoingDirectory, null, null),
|
||||||
outgoingRepository);
|
outgoingRepository);
|
||||||
PushCommandRequest req = new PushCommandRequest();
|
PushCommandRequest req = new PushCommandRequest();
|
||||||
|
|
||||||
@@ -158,7 +160,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
|
|||||||
*/
|
*/
|
||||||
private GitOutgoingCommand createCommand()
|
private GitOutgoingCommand createCommand()
|
||||||
{
|
{
|
||||||
return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, null),
|
return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, null, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())),
|
||||||
outgoingRepository);
|
outgoingRepository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase
|
|||||||
*/
|
*/
|
||||||
private GitPushCommand createCommand()
|
private GitPushCommand createCommand()
|
||||||
{
|
{
|
||||||
return new GitPushCommand(handler, new GitContext(outgoingDirectory, null),
|
return new GitPushCommand(handler, new GitContext(outgoingDirectory, null, null),
|
||||||
outgoingRepository);
|
outgoingRepository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[users]
|
[users]
|
||||||
readOnly = secret, reader
|
readOnly = secret, reader, repoRead
|
||||||
writeOnly = secret, writer
|
writeOnly = secret, writer, repoWrite
|
||||||
readWrite = secret, readerWriter
|
readWrite = secret, readerWriter
|
||||||
admin = secret, admin
|
admin = secret, admin
|
||||||
|
|
||||||
@@ -9,3 +9,5 @@ reader = configuration:read:git
|
|||||||
writer = configuration:write:git
|
writer = configuration:write:git
|
||||||
readerWriter = configuration:*:git
|
readerWriter = configuration:*:git
|
||||||
admin = *
|
admin = *
|
||||||
|
repoRead = repository:read:*
|
||||||
|
repoWrite = repository:modify:*
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"creationDate": "2018-11-09T09:48:32.732Z",
|
||||||
|
"description": "Handling static webresources made easy",
|
||||||
|
"healthCheckFailures": [],
|
||||||
|
"lastModified": "2018-11-09T09:49:20.973Z",
|
||||||
|
"namespace": "scmadmin",
|
||||||
|
"name": "web-resources",
|
||||||
|
"archived": false,
|
||||||
|
"type": "git",
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
|
||||||
|
},
|
||||||
|
"protocol": [
|
||||||
|
{
|
||||||
|
"href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
|
||||||
|
"name": "http"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
|
||||||
|
},
|
||||||
|
"changesets": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"page": 0,
|
||||||
|
"pageTotal": 1,
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
|
||||||
|
},
|
||||||
|
"first": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
|
||||||
|
},
|
||||||
|
"last": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_embedded": {
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"creationDate": "2018-11-09T09:48:32.732Z",
|
||||||
|
"description": "Handling static webresources made easy",
|
||||||
|
"healthCheckFailures": [],
|
||||||
|
"lastModified": "2018-11-09T09:49:20.973Z",
|
||||||
|
"namespace": "scmadmin",
|
||||||
|
"name": "web-resources",
|
||||||
|
"archived": false,
|
||||||
|
"type": "git",
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
|
||||||
|
},
|
||||||
|
"protocol": [
|
||||||
|
{
|
||||||
|
"href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
|
||||||
|
"name": "http"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
|
||||||
|
},
|
||||||
|
"changesets": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"creationDate": "2018-11-09T09:48:32.732Z",
|
||||||
|
"description": "Handling static webresources made easy",
|
||||||
|
"healthCheckFailures": [],
|
||||||
|
"lastModified": "2018-11-09T09:49:20.973Z",
|
||||||
|
"namespace": "scmadmin",
|
||||||
|
"name": "web-resources",
|
||||||
|
"archived": false,
|
||||||
|
"type": "git",
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
|
||||||
|
},
|
||||||
|
"protocol": [
|
||||||
|
{
|
||||||
|
"href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
|
||||||
|
"name": "http"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
|
||||||
|
},
|
||||||
|
"changesets": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -38,12 +38,30 @@ package sonia.scm.store;
|
|||||||
/**
|
/**
|
||||||
* In memory configuration store factory for testing purposes.
|
* In memory configuration store factory for testing purposes.
|
||||||
*
|
*
|
||||||
|
* Use {@link #create()} to get a store that creates the same store on each request.
|
||||||
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory {
|
public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory {
|
||||||
|
|
||||||
|
private ConfigurationStore store;
|
||||||
|
|
||||||
|
public static ConfigurationStoreFactory create() {
|
||||||
|
return new InMemoryConfigurationStoreFactory(new InMemoryConfigurationStore());
|
||||||
|
}
|
||||||
|
|
||||||
|
public InMemoryConfigurationStoreFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public InMemoryConfigurationStoreFactory(ConfigurationStore store) {
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ConfigurationStore getStore(TypedStoreParameters storeParameters) {
|
public ConfigurationStore getStore(TypedStoreParameters storeParameters) {
|
||||||
|
if (store != null) {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
return new InMemoryConfigurationStore<>();
|
return new InMemoryConfigurationStore<>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package sonia.scm.store;
|
||||||
|
|
||||||
|
import sonia.scm.security.KeyGenerator;
|
||||||
|
import sonia.scm.security.UUIDKeyGenerator;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In memory store implementation of {@link DataStore}.
|
||||||
|
*
|
||||||
|
* @author Sebastian Sdorra
|
||||||
|
*
|
||||||
|
* @param <T> type of stored object
|
||||||
|
*/
|
||||||
|
public class InMemoryDataStore<T> implements DataStore<T> {
|
||||||
|
|
||||||
|
private final Map<String, T> store = new HashMap<>();
|
||||||
|
private KeyGenerator generator = new UUIDKeyGenerator();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String put(T item) {
|
||||||
|
String key = generator.createKey();
|
||||||
|
store.put(key, item);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void put(String id, T item) {
|
||||||
|
store.put(id, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, T> getAll() {
|
||||||
|
return Collections.unmodifiableMap(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
store.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(String id) {
|
||||||
|
store.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T get(String id) {
|
||||||
|
return store.get(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package sonia.scm.store;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In memory configuration store factory for testing purposes.
|
||||||
|
*
|
||||||
|
* @author Sebastian Sdorra
|
||||||
|
*/
|
||||||
|
public class InMemoryDataStoreFactory implements DataStoreFactory {
|
||||||
|
|
||||||
|
private InMemoryDataStore store;
|
||||||
|
|
||||||
|
public InMemoryDataStoreFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public InMemoryDataStoreFactory(InMemoryDataStore store) {
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> DataStore<T> getStore(TypedStoreParameters<T> storeParameters) {
|
||||||
|
if (store != null) {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
return new InMemoryDataStore<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Branch } from "@scm-manager/ui-types";
|
import type {Branch} from "@scm-manager/ui-types";
|
||||||
import DropDown from "../components/DropDown";
|
|
||||||
import { translate } from "react-i18next";
|
|
||||||
import injectSheet from "react-jss";
|
import injectSheet from "react-jss";
|
||||||
import { compose } from "redux";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import DropDown from "./forms/DropDown";
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
zeroflex: {
|
zeroflex: {
|
||||||
@@ -14,17 +12,22 @@ const styles = {
|
|||||||
},
|
},
|
||||||
minWidthOfLabel: {
|
minWidthOfLabel: {
|
||||||
minWidth: "4.5rem"
|
minWidth: "4.5rem"
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
padding: "1rem 1.5rem 0.25rem 1.5rem",
|
||||||
|
border: "1px solid #eee",
|
||||||
|
borderRadius: "5px 5px 0 0"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
branches: Branch[], // TODO: Use generics?
|
branches: Branch[], // TODO: Use generics?
|
||||||
selected: (branch?: Branch) => void,
|
selected: (branch?: Branch) => void,
|
||||||
selectedBranch: string,
|
selectedBranch?: string,
|
||||||
|
label: string,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
classes: Object,
|
classes: Object
|
||||||
t: string => string
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = { selectedBranch?: Branch };
|
type State = { selectedBranch?: Branch };
|
||||||
@@ -36,17 +39,22 @@ class BranchSelector extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.branches
|
const selectedBranch = this.props.branches.find(branch => branch.name === this.props.selectedBranch);
|
||||||
.filter(branch => branch.name === this.props.selectedBranch)
|
this.setState({ selectedBranch });
|
||||||
.forEach(branch => this.setState({ selectedBranch: branch }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { branches, classes, t } = this.props;
|
const { branches, classes, label } = this.props;
|
||||||
|
|
||||||
if (branches) {
|
if (branches) {
|
||||||
return (
|
return (
|
||||||
<div className="box field is-horizontal">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"has-background-light field",
|
||||||
|
"is-horizontal",
|
||||||
|
classes.wrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"field-label",
|
"field-label",
|
||||||
@@ -55,7 +63,7 @@ class BranchSelector extends React.Component<Props, State> {
|
|||||||
classes.minWidthOfLabel
|
classes.minWidthOfLabel
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<label className="label">{t("branch-selector.label")}</label>
|
<label className="label">{label}</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="field-body">
|
<div className="field-body">
|
||||||
<div className="field is-narrow">
|
<div className="field is-narrow">
|
||||||
@@ -82,6 +90,12 @@ class BranchSelector extends React.Component<Props, State> {
|
|||||||
|
|
||||||
branchSelected = (branchName: string) => {
|
branchSelected = (branchName: string) => {
|
||||||
const { branches, selected } = this.props;
|
const { branches, selected } = this.props;
|
||||||
|
|
||||||
|
if (!branchName) {
|
||||||
|
this.setState({ selectedBranch: undefined });
|
||||||
|
selected(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const branch = branches.find(b => b.name === branchName);
|
const branch = branches.find(b => b.name === branchName);
|
||||||
|
|
||||||
selected(branch);
|
selected(branch);
|
||||||
@@ -89,7 +103,4 @@ class BranchSelector extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(
|
export default injectSheet(styles)(BranchSelector);
|
||||||
injectSheet(styles),
|
|
||||||
translate("repos")
|
|
||||||
)(BranchSelector);
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import Notification from "./Notification";
|
import Notification from "./Notification";
|
||||||
|
import {UNAUTHORIZED_ERROR} from "./apiclient";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
t: string => string,
|
t: string => string,
|
||||||
@@ -9,15 +10,26 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class ErrorNotification extends React.Component<Props> {
|
class ErrorNotification extends React.Component<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t, error } = this.props;
|
const { t, error } = this.props;
|
||||||
if (error) {
|
if (error) {
|
||||||
|
if (error === UNAUTHORIZED_ERROR) {
|
||||||
|
return (
|
||||||
|
<Notification type="danger">
|
||||||
|
<strong>{t("error-notification.prefix")}:</strong> {t("error-notification.timeout")}
|
||||||
|
{" "}
|
||||||
|
<a href="javascript:window.location.reload(true)">{t("error-notification.loginLink")}</a>
|
||||||
|
</Notification>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
return (
|
return (
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
<strong>{t("error-notification.prefix")}:</strong> {error.message}
|
<strong>{t("error-notification.prefix")}:</strong> {error.message}
|
||||||
</Notification>
|
</Notification>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import injectSheet from "react-jss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
classes: any
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
textinfo: {
|
||||||
|
color: "#98d8f3 !important"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class HelpIcon extends React.Component<Props> {
|
class HelpIcon extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
return <i className={classNames("fa fa-question has-text-info")} />
|
const { classes } = this.props;
|
||||||
|
return <i className={classNames("fa fa-question-circle has-text-info", classes.textinfo)}></i>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HelpIcon;
|
export default injectSheet(styles)(HelpIcon);
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import injectSheet from "react-jss";
|
import injectSheet from "react-jss";
|
||||||
import AddButton, { type ButtonProps } from "./Button";
|
import SubmitButton, { type ButtonProps } from "./SubmitButton";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
spacing: {
|
spacing: {
|
||||||
margin: "1em 0 0 1em"
|
marginTop: "2em",
|
||||||
|
border: "2px solid #e9f7fd",
|
||||||
|
padding: "1em 1em"
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class CreateButton extends React.Component<ButtonProps> {
|
class CreateButton extends React.Component<ButtonProps> {
|
||||||
render() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={classNames("is-pulled-right", classes.spacing)}>
|
<div className={classNames("has-text-centered", classes.spacing)}>
|
||||||
<AddButton {...this.props} />
|
<SubmitButton {...this.props} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,9 @@ class ConfigurationBinder {
|
|||||||
|
|
||||||
|
|
||||||
// route for global configuration, passes the current repository to component
|
// route for global configuration, passes the current repository to component
|
||||||
const RepoRoute = ({ url, repository }) => {
|
const RepoRoute = ({url, repository}) => {
|
||||||
return this.route(url + to, <RepositoryComponent repository={repository}/>);
|
const link = repository._links[linkName].href
|
||||||
|
return this.route(url + to, <RepositoryComponent repository={repository} link={link}/>);
|
||||||
};
|
};
|
||||||
|
|
||||||
// bind config route to extension point
|
// bind config route to extension point
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ export { default as InputField } from "./InputField.js";
|
|||||||
export { default as Select } from "./Select.js";
|
export { default as Select } from "./Select.js";
|
||||||
export { default as Textarea } from "./Textarea.js";
|
export { default as Textarea } from "./Textarea.js";
|
||||||
export { default as PasswordConfirmation } from "./PasswordConfirmation.js";
|
export { default as PasswordConfirmation } from "./PasswordConfirmation.js";
|
||||||
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon";
|
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
|
||||||
|
export { default as DropDown } from "./DropDown.js";
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export { default as HelpIcon } from "./HelpIcon";
|
|||||||
export { default as Tooltip } from "./Tooltip";
|
export { default as Tooltip } from "./Tooltip";
|
||||||
export { getPageFromMatch } from "./urls";
|
export { getPageFromMatch } from "./urls";
|
||||||
export { default as Autocomplete} from "./Autocomplete";
|
export { default as Autocomplete} from "./Autocomplete";
|
||||||
|
export { default as BranchSelector } from "./BranchSelector";
|
||||||
|
|
||||||
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR, CONFLICT_ERROR } from "./apiclient.js";
|
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR, CONFLICT_ERROR } from "./apiclient.js";
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ class LoadingDiff extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.fetchDiff();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if(prevProps.url !== this.props.url){
|
||||||
|
this.fetchDiff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDiff = () => {
|
||||||
const { url } = this.props;
|
const { url } = this.props;
|
||||||
apiClient
|
apiClient
|
||||||
.get(url)
|
.get(url)
|
||||||
@@ -46,15 +56,18 @@ class LoadingDiff extends React.Component<Props, State> {
|
|||||||
error
|
error
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { diff, loading, error } = this.state;
|
const { diff, loading, error } = this.state;
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorNotification error={error} />;
|
return <ErrorNotification error={error} />;
|
||||||
} else if (loading || !diff) {
|
} else if (loading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
} else {
|
} else if(!diff){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
return <Diff diff={diff} />;
|
return <Diff diff={diff} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 48 KiB |
BIN
scm-ui/public/images/scmManagerHero.jpg
Normal file
BIN
scm-ui/public/images/scmManagerHero.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -20,7 +20,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error-notification": {
|
"error-notification": {
|
||||||
"prefix": "Error"
|
"prefix": "Error",
|
||||||
|
"loginLink": "You can login here again.",
|
||||||
|
"timeout": "The session has expired."
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"alt": "Loading ..."
|
"alt": "Loading ..."
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class AdminSettings extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Subtitle subtitle={t("admin-settings.name")} />
|
<Subtitle subtitle={t("admin-settings.name")} />
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
<AdminGroupTable
|
<AdminGroupTable
|
||||||
adminGroups={adminGroups}
|
adminGroups={adminGroups}
|
||||||
onChange={(isValid, changedValue, name) =>
|
onChange={(isValid, changedValue, name) =>
|
||||||
@@ -27,6 +29,7 @@ class AdminSettings extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddEntryToTableField
|
<AddEntryToTableField
|
||||||
addEntry={this.addGroup}
|
addEntry={this.addGroup}
|
||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
@@ -34,6 +37,8 @@ class AdminSettings extends React.Component<Props> {
|
|||||||
fieldLabel={t("admin-settings.add-group-textfield")}
|
fieldLabel={t("admin-settings.add-group-textfield")}
|
||||||
errorMessage={t("admin-settings.add-group-error")}
|
errorMessage={t("admin-settings.add-group-error")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
<AdminUserTable
|
<AdminUserTable
|
||||||
adminUsers={adminUsers}
|
adminUsers={adminUsers}
|
||||||
onChange={(isValid, changedValue, name) =>
|
onChange={(isValid, changedValue, name) =>
|
||||||
@@ -49,6 +54,8 @@ class AdminSettings extends React.Component<Props> {
|
|||||||
errorMessage={t("admin-settings.add-user-error")}
|
errorMessage={t("admin-settings.add-user-error")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ class BaseUrlSettings extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Subtitle subtitle={t("base-url-settings.name")} />
|
<Subtitle subtitle={t("base-url-settings.name")} />
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={forceBaseUrl}
|
checked={forceBaseUrl}
|
||||||
label={t("base-url-settings.force-base-url")}
|
label={t("base-url-settings.force-base-url")}
|
||||||
@@ -25,6 +27,8 @@ class BaseUrlSettings extends React.Component<Props> {
|
|||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
helpText={t("help.forceBaseUrlHelpText")}
|
helpText={t("help.forceBaseUrlHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("base-url-settings.base-url")}
|
label={t("base-url-settings.base-url")}
|
||||||
onChange={this.handleBaseUrlChange}
|
onChange={this.handleBaseUrlChange}
|
||||||
@@ -33,6 +37,8 @@ class BaseUrlSettings extends React.Component<Props> {
|
|||||||
helpText={t("help.baseUrlHelpText")}
|
helpText={t("help.baseUrlHelpText")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class GeneralSettings extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("general-settings.realm-description")}
|
label={t("general-settings.realm-description")}
|
||||||
onChange={this.handleRealmDescriptionChange}
|
onChange={this.handleRealmDescriptionChange}
|
||||||
@@ -43,6 +45,8 @@ class GeneralSettings extends React.Component<Props> {
|
|||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
helpText={t("help.realmDescriptionHelpText")}
|
helpText={t("help.realmDescriptionHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("general-settings.date-format")}
|
label={t("general-settings.date-format")}
|
||||||
onChange={this.handleDateFormatChange}
|
onChange={this.handleDateFormatChange}
|
||||||
@@ -50,6 +54,10 @@ class GeneralSettings extends React.Component<Props> {
|
|||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
helpText={t("help.dateFormatHelpText")}
|
helpText={t("help.dateFormatHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("general-settings.plugin-url")}
|
label={t("general-settings.plugin-url")}
|
||||||
onChange={this.handlePluginUrlChange}
|
onChange={this.handlePluginUrlChange}
|
||||||
@@ -57,6 +65,8 @@ class GeneralSettings extends React.Component<Props> {
|
|||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
helpText={t("help.pluginRepositoryHelpText")}
|
helpText={t("help.pluginRepositoryHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("general-settings.default-namespace-strategy")}
|
label={t("general-settings.default-namespace-strategy")}
|
||||||
onChange={this.handleDefaultNamespaceStrategyChange}
|
onChange={this.handleDefaultNamespaceStrategyChange}
|
||||||
@@ -64,6 +74,10 @@ class GeneralSettings extends React.Component<Props> {
|
|||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
helpText={t("help.defaultNameSpaceStrategyHelpText")}
|
helpText={t("help.defaultNameSpaceStrategyHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={enabledXsrfProtection}
|
checked={enabledXsrfProtection}
|
||||||
label={t("general-settings.enabled-xsrf-protection")}
|
label={t("general-settings.enabled-xsrf-protection")}
|
||||||
@@ -71,6 +85,8 @@ class GeneralSettings extends React.Component<Props> {
|
|||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
helpText={t("help.enableXsrfProtectionHelpText")}
|
helpText={t("help.enableXsrfProtectionHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={enableRepositoryArchive}
|
checked={enableRepositoryArchive}
|
||||||
label={t("general-settings.enable-repository-archive")}
|
label={t("general-settings.enable-repository-archive")}
|
||||||
@@ -78,6 +94,10 @@ class GeneralSettings extends React.Component<Props> {
|
|||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
helpText={t("help.enableRepositoryArchiveHelpText")}
|
helpText={t("help.enableRepositoryArchiveHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={disableGroupingGrid}
|
checked={disableGroupingGrid}
|
||||||
label={t("general-settings.disable-grouping-grid")}
|
label={t("general-settings.disable-grouping-grid")}
|
||||||
@@ -85,6 +105,8 @@ class GeneralSettings extends React.Component<Props> {
|
|||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
helpText={t("help.disableGroupingGridHelpText")}
|
helpText={t("help.disableGroupingGridHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={anonymousAccessEnabled}
|
checked={anonymousAccessEnabled}
|
||||||
label={t("general-settings.anonymous-access-enabled")}
|
label={t("general-settings.anonymous-access-enabled")}
|
||||||
@@ -92,6 +114,10 @@ class GeneralSettings extends React.Component<Props> {
|
|||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
helpText={t("help.allowAnonymousAccessHelpText")}
|
helpText={t("help.allowAnonymousAccessHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={skipFailedAuthenticators}
|
checked={skipFailedAuthenticators}
|
||||||
label={t("general-settings.skip-failed-authenticators")}
|
label={t("general-settings.skip-failed-authenticators")}
|
||||||
@@ -100,6 +126,8 @@ class GeneralSettings extends React.Component<Props> {
|
|||||||
helpText={t("help.skipFailedAuthenticatorsHelpText")}
|
helpText={t("help.skipFailedAuthenticatorsHelpText")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ class LoginAttempt extends React.Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Subtitle subtitle={t("login-attempt.name")} />
|
<Subtitle subtitle={t("login-attempt.name")} />
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("login-attempt.login-attempt-limit")}
|
label={t("login-attempt.login-attempt-limit")}
|
||||||
onChange={this.handleLoginAttemptLimitChange}
|
onChange={this.handleLoginAttemptLimitChange}
|
||||||
@@ -49,6 +51,8 @@ class LoginAttempt extends React.Component<Props, State> {
|
|||||||
errorMessage={t("validation.login-attempt-limit-invalid")}
|
errorMessage={t("validation.login-attempt-limit-invalid")}
|
||||||
helpText={t("help.loginAttemptLimitHelpText")}
|
helpText={t("help.loginAttemptLimitHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("login-attempt.login-attempt-limit-timeout")}
|
label={t("login-attempt.login-attempt-limit-timeout")}
|
||||||
onChange={this.handleLoginAttemptLimitTimeoutChange}
|
onChange={this.handleLoginAttemptLimitTimeoutChange}
|
||||||
@@ -59,6 +63,8 @@ class LoginAttempt extends React.Component<Props, State> {
|
|||||||
helpText={t("help.loginAttemptLimitTimeoutHelpText")}
|
helpText={t("help.loginAttemptLimitTimeoutHelpText")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class ProxySettings extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Subtitle subtitle={t("proxy-settings.name")} />
|
<Subtitle subtitle={t("proxy-settings.name")} />
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-full">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={enableProxy}
|
checked={enableProxy}
|
||||||
label={t("proxy-settings.enable-proxy")}
|
label={t("proxy-settings.enable-proxy")}
|
||||||
@@ -44,6 +46,10 @@ class ProxySettings extends React.Component<Props> {
|
|||||||
disabled={!hasUpdatePermission}
|
disabled={!hasUpdatePermission}
|
||||||
helpText={t("help.enableProxyHelpText")}
|
helpText={t("help.enableProxyHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("proxy-settings.proxy-password")}
|
label={t("proxy-settings.proxy-password")}
|
||||||
onChange={this.handleProxyPasswordChange}
|
onChange={this.handleProxyPasswordChange}
|
||||||
@@ -52,6 +58,8 @@ class ProxySettings extends React.Component<Props> {
|
|||||||
disabled={!enableProxy || !hasUpdatePermission}
|
disabled={!enableProxy || !hasUpdatePermission}
|
||||||
helpText={t("help.proxyPasswordHelpText")}
|
helpText={t("help.proxyPasswordHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("proxy-settings.proxy-port")}
|
label={t("proxy-settings.proxy-port")}
|
||||||
value={proxyPort}
|
value={proxyPort}
|
||||||
@@ -59,6 +67,10 @@ class ProxySettings extends React.Component<Props> {
|
|||||||
disabled={!enableProxy || !hasUpdatePermission}
|
disabled={!enableProxy || !hasUpdatePermission}
|
||||||
helpText={t("help.proxyPortHelpText")}
|
helpText={t("help.proxyPortHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("proxy-settings.proxy-server")}
|
label={t("proxy-settings.proxy-server")}
|
||||||
value={proxyServer}
|
value={proxyServer}
|
||||||
@@ -66,6 +78,8 @@ class ProxySettings extends React.Component<Props> {
|
|||||||
disabled={!enableProxy || !hasUpdatePermission}
|
disabled={!enableProxy || !hasUpdatePermission}
|
||||||
helpText={t("help.proxyServerHelpText")}
|
helpText={t("help.proxyServerHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("proxy-settings.proxy-user")}
|
label={t("proxy-settings.proxy-user")}
|
||||||
value={proxyUser}
|
value={proxyUser}
|
||||||
@@ -73,6 +87,10 @@ class ProxySettings extends React.Component<Props> {
|
|||||||
disabled={!enableProxy || !hasUpdatePermission}
|
disabled={!enableProxy || !hasUpdatePermission}
|
||||||
helpText={t("help.proxyUserHelpText")}
|
helpText={t("help.proxyUserHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-full">
|
||||||
<ProxyExcludesTable
|
<ProxyExcludesTable
|
||||||
proxyExcludes={proxyExcludes}
|
proxyExcludes={proxyExcludes}
|
||||||
onChange={(isValid, changedValue, name) =>
|
onChange={(isValid, changedValue, name) =>
|
||||||
@@ -88,6 +106,8 @@ class ProxySettings extends React.Component<Props> {
|
|||||||
errorMessage={t("proxy-settings.add-proxy-exclude-error")}
|
errorMessage={t("proxy-settings.add-proxy-exclude-error")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class Config extends React.Component<Props> {
|
|||||||
renderAll={true}
|
renderAll={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="column">
|
<div className="column is-one-quarter">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<Section label={t("config.navigation-title")}>
|
<Section label={t("config.navigation-title")}>
|
||||||
<NavLink
|
<NavLink
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ export function fetchConfig(link: string) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(fetchConfigSuccess(data));
|
dispatch(fetchConfigSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`could not fetch config: ${cause.message}`);
|
dispatch(fetchConfigFailure(err));
|
||||||
dispatch(fetchConfigFailure(error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -73,13 +72,8 @@ export function modifyConfig(config: Config, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
dispatch(
|
dispatch(modifyConfigFailure(config, err));
|
||||||
modifyConfigFailure(
|
|
||||||
config,
|
|
||||||
new Error(`could not modify config: ${cause.message}`)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ class ProfileInfo extends React.Component<Props, State> {
|
|||||||
<table className="table">
|
<table className="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("profile.username")}</td>
|
<td className="has-text-weight-semibold">{t("profile.username")}</td>
|
||||||
<td>{me.name}</td>
|
<td>{me.name}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("profile.displayName")}</td>
|
<td className="has-text-weight-semibold">{t("profile.displayName")}</td>
|
||||||
<td>{me.displayName}</td>
|
<td>{me.displayName}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("profile.mail")}</td>
|
<td className="has-text-weight-semibold">{t("profile.mail")}</td>
|
||||||
<td>
|
<td>
|
||||||
<MailLink address={me.mail} />
|
<MailLink address={me.mail} />
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -17,25 +17,25 @@ class Details extends React.Component<Props> {
|
|||||||
<table className="table content">
|
<table className="table content">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("group.name")}</td>
|
<td className="has-text-weight-semibold">{t("group.name")}</td>
|
||||||
<td>{group.name}</td>
|
<td>{group.name}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("group.description")}</td>
|
<td className="has-text-weight-semibold">{t("group.description")}</td>
|
||||||
<td>{group.description}</td>
|
<td>{group.description}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("group.type")}</td>
|
<td className="has-text-weight-semibold">{t("group.type")}</td>
|
||||||
<td>{group.type}</td>
|
<td>{group.type}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("group.creationDate")}</td>
|
<td className="has-text-weight-semibold">{t("group.creationDate")}</td>
|
||||||
<td>
|
<td>
|
||||||
<DateFromNow date={group.creationDate} />
|
<DateFromNow date={group.creationDate} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("group.lastModified")}</td>
|
<td className="has-text-weight-semibold">{t("group.lastModified")}</td>
|
||||||
<td>
|
<td>
|
||||||
<DateFromNow date={group.lastModified} />
|
<DateFromNow date={group.lastModified} />
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class GroupTable extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
const { groups, t } = this.props;
|
const { groups, t } = this.props;
|
||||||
return (
|
return (
|
||||||
<table className="table is-hoverable is-fullwidth">
|
<table className="card-table table is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("group.name")}</th>
|
<th>{t("group.name")}</th>
|
||||||
|
|||||||
@@ -54,9 +54,8 @@ export function fetchGroupsByLink(link: string) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(fetchGroupsSuccess(data));
|
dispatch(fetchGroupsSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`could not fetch groups: ${cause.message}`);
|
dispatch(fetchGroupsFailure(link, err));
|
||||||
dispatch(fetchGroupsFailure(link, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -105,9 +104,8 @@ function fetchGroup(link: string, name: string) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(fetchGroupSuccess(data));
|
dispatch(fetchGroupSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`could not fetch group: ${cause.message}`);
|
dispatch(fetchGroupFailure(name, err));
|
||||||
dispatch(fetchGroupFailure(name, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -151,10 +149,10 @@ export function createGroup(link: string, group: Group, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(err => {
|
||||||
dispatch(
|
dispatch(
|
||||||
createGroupFailure(
|
createGroupFailure(
|
||||||
new Error(`Failed to create group ${group.name}: ${error.message}`)
|
err
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -201,11 +199,11 @@ export function modifyGroup(group: Group, callback?: () => void) {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(fetchGroupByLink(group));
|
dispatch(fetchGroupByLink(group));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
dispatch(
|
dispatch(
|
||||||
modifyGroupFailure(
|
modifyGroupFailure(
|
||||||
group,
|
group,
|
||||||
new Error(`could not modify group ${group.name}: ${cause.message}`)
|
err
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -259,11 +257,8 @@ export function deleteGroup(group: Group, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(
|
dispatch(deleteGroupFailure(group, err));
|
||||||
`could not delete group ${group.name}: ${cause.message}`
|
|
||||||
);
|
|
||||||
dispatch(deleteGroupFailure(group, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,31 +17,31 @@ class RepositoryDetailTable extends React.Component<Props> {
|
|||||||
<table className="table">
|
<table className="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("repository.name")}</td>
|
<td className="has-text-weight-semibold">{t("repository.name")}</td>
|
||||||
<td>{repository.name}</td>
|
<td>{repository.name}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("repository.type")}</td>
|
<td className="has-text-weight-semibold">{t("repository.type")}</td>
|
||||||
<td>{repository.type}</td>
|
<td>{repository.type}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("repository.contact")}</td>
|
<td className="has-text-weight-semibold">{t("repository.contact")}</td>
|
||||||
<td>
|
<td>
|
||||||
<MailLink address={repository.contact} />
|
<MailLink address={repository.contact} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("repository.description")}</td>
|
<td className="has-text-weight-semibold">{t("repository.description")}</td>
|
||||||
<td>{repository.description}</td>
|
<td>{repository.description}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("repository.creationDate")}</td>
|
<td className="has-text-weight-semibold">{t("repository.creationDate")}</td>
|
||||||
<td>
|
<td>
|
||||||
<DateFromNow date={repository.creationDate} />
|
<DateFromNow date={repository.creationDate} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("repository.lastModified")}</td>
|
<td className="has-text-weight-semibold">{t("repository.lastModified")}</td>
|
||||||
<td>
|
<td>
|
||||||
<DateFromNow date={repository.lastModified} />
|
<DateFromNow date={repository.lastModified} />
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class RepositoryDetails extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<RepositoryDetailTable repository={repository} />
|
<RepositoryDetailTable repository={repository} />
|
||||||
|
<hr />
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<ExtensionPoint
|
<ExtensionPoint
|
||||||
name="repos.repository-details.information"
|
name="repos.repository-details.information"
|
||||||
|
|||||||
@@ -9,15 +9,10 @@ import classNames from "classnames";
|
|||||||
import RepositoryAvatar from "./RepositoryAvatar";
|
import RepositoryAvatar from "./RepositoryAvatar";
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
outer: {
|
|
||||||
position: "relative"
|
|
||||||
},
|
|
||||||
overlay: {
|
overlay: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 0,
|
height: "calc(120px - 1.5rem)",
|
||||||
top: 0,
|
width: "calc(50% - 3rem)"
|
||||||
bottom: 0,
|
|
||||||
right: 0
|
|
||||||
},
|
},
|
||||||
inner: {
|
inner: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
@@ -26,11 +21,16 @@ const styles = {
|
|||||||
},
|
},
|
||||||
innerLink: {
|
innerLink: {
|
||||||
pointerEvents: "all"
|
pointerEvents: "all"
|
||||||
|
},
|
||||||
|
centerImage: {
|
||||||
|
marginTop: "0.8em",
|
||||||
|
marginLeft: "1em !important"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
|
fullColumnWidth?: boolean,
|
||||||
// context props
|
// context props
|
||||||
classes: any
|
classes: any
|
||||||
};
|
};
|
||||||
@@ -44,7 +44,7 @@ class RepositoryEntry extends React.Component<Props> {
|
|||||||
if (repository._links["changesets"]) {
|
if (repository._links["changesets"]) {
|
||||||
return (
|
return (
|
||||||
<RepositoryEntryLink
|
<RepositoryEntryLink
|
||||||
iconClass="fa-code-branch"
|
iconClass="fa-code-branch fa-lg"
|
||||||
to={repositoryLink + "/changesets"}
|
to={repositoryLink + "/changesets"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -56,7 +56,7 @@ class RepositoryEntry extends React.Component<Props> {
|
|||||||
if (repository._links["sources"]) {
|
if (repository._links["sources"]) {
|
||||||
return (
|
return (
|
||||||
<RepositoryEntryLink
|
<RepositoryEntryLink
|
||||||
iconClass="fa-code"
|
iconClass="fa-code fa-lg"
|
||||||
to={repositoryLink + "/sources"}
|
to={repositoryLink + "/sources"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -67,29 +67,40 @@ class RepositoryEntry extends React.Component<Props> {
|
|||||||
renderModifyLink = (repository: Repository, repositoryLink: string) => {
|
renderModifyLink = (repository: Repository, repositoryLink: string) => {
|
||||||
if (repository._links["update"]) {
|
if (repository._links["update"]) {
|
||||||
return (
|
return (
|
||||||
<RepositoryEntryLink iconClass="fa-cog" to={repositoryLink + "/edit"} />
|
<RepositoryEntryLink
|
||||||
|
iconClass="fa-cog fa-lg"
|
||||||
|
to={repositoryLink + "/edit"}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { repository, classes } = this.props;
|
const { repository, classes, fullColumnWidth } = this.props;
|
||||||
const repositoryLink = this.createLink(repository);
|
const repositoryLink = this.createLink(repository);
|
||||||
|
const halfColumn = fullColumnWidth ? "is-full" : "is-half";
|
||||||
return (
|
return (
|
||||||
<div className={classNames("box", "box-link-shadow", classes.outer)}>
|
<div
|
||||||
<Link className={classes.overlay} to={repositoryLink} />
|
className={classNames(
|
||||||
|
"box",
|
||||||
|
"box-link-shadow",
|
||||||
|
"column",
|
||||||
|
"is-clipped",
|
||||||
|
halfColumn
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link className={classNames(classes.overlay)} to={repositoryLink} />
|
||||||
<article className={classNames("media", classes.inner)}>
|
<article className={classNames("media", classes.inner)}>
|
||||||
<figure className="media-left">
|
<figure className={classNames(classes.centerImage, "media-left")}>
|
||||||
<RepositoryAvatar repository={repository} />
|
<RepositoryAvatar repository={repository} />
|
||||||
</figure>
|
</figure>
|
||||||
<div className="media-content">
|
<div className="media-content">
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<p>
|
<p className="is-marginless">
|
||||||
<strong>{repository.name}</strong>
|
<strong>{repository.name}</strong>
|
||||||
<br />
|
|
||||||
{repository.description}
|
|
||||||
</p>
|
</p>
|
||||||
|
<p className={"shorten-text"}>{repository.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<nav className="level is-mobile">
|
<nav className="level is-mobile">
|
||||||
<div className="level-left">
|
<div className="level-left">
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import classNames from "classnames";
|
|||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
link: {
|
link: {
|
||||||
pointerEvents: "all"
|
pointerEvents: "all",
|
||||||
|
marginRight: "1.25rem !important"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { RepositoryGroup } from "@scm-manager/ui-types";
|
import type { RepositoryGroup, Repository } from "@scm-manager/ui-types";
|
||||||
import injectSheet from "react-jss";
|
import injectSheet from "react-jss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import RepositoryEntry from "./RepositoryEntry";
|
import RepositoryEntry from "./RepositoryEntry";
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
pointer: {
|
pointer: {
|
||||||
cursor: "pointer"
|
cursor: "pointer",
|
||||||
|
fontSize: "1.5rem"
|
||||||
},
|
},
|
||||||
repoGroup: {
|
repoGroup: {
|
||||||
marginBottom: "1em"
|
marginBottom: "1em"
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
padding: "0 0.75rem"
|
||||||
|
},
|
||||||
|
clearfix: {
|
||||||
|
clear: "both"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,6 +46,18 @@ class RepositoryGroupEntry extends React.Component<Props, State> {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isLastEntry = (array: Repository[], index: number) => {
|
||||||
|
return index === array.length - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
isLengthOdd = (array: Repository[]) => {
|
||||||
|
return array.length % 2 !== 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
isFullSize = (array: Repository[], index: number) => {
|
||||||
|
return this.isLastEntry(array, index) && this.isLengthOdd(array);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { group, classes } = this.props;
|
const { group, classes } = this.props;
|
||||||
const { collapsed } = this.state;
|
const { collapsed } = this.state;
|
||||||
@@ -47,7 +66,10 @@ class RepositoryGroupEntry extends React.Component<Props, State> {
|
|||||||
let content = null;
|
let content = null;
|
||||||
if (!collapsed) {
|
if (!collapsed) {
|
||||||
content = group.repositories.map((repository, index) => {
|
content = group.repositories.map((repository, index) => {
|
||||||
return <RepositoryEntry repository={repository} key={index} />;
|
const fullColumnWidth = this.isFullSize(group.repositories, index);
|
||||||
|
return (
|
||||||
|
<RepositoryEntry repository={repository} fullColumnWidth={fullColumnWidth} key={index} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -58,8 +80,11 @@ class RepositoryGroupEntry extends React.Component<Props, State> {
|
|||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<hr />
|
<hr />
|
||||||
|
<div className={classNames("columns", "is-multiline", classes.wrapper)}>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={classes.clearfix} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Branch, Repository } from "@scm-manager/ui-types";
|
import type { Branch, Repository } from "@scm-manager/ui-types";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
import { Route, withRouter } from "react-router-dom";
|
import { Route, withRouter } from "react-router-dom";
|
||||||
import Changesets from "./Changesets";
|
import Changesets from "./Changesets";
|
||||||
import BranchSelector from "./BranchSelector";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
import {
|
||||||
|
BranchSelector,
|
||||||
|
ErrorNotification,
|
||||||
|
Loading
|
||||||
|
} from "@scm-manager/ui-components";
|
||||||
import {
|
import {
|
||||||
fetchBranches,
|
fetchBranches,
|
||||||
getBranches,
|
getBranches,
|
||||||
@@ -32,7 +36,8 @@ type Props = {
|
|||||||
|
|
||||||
// Context props
|
// Context props
|
||||||
history: any, // TODO flow type
|
history: any, // TODO flow type
|
||||||
match: any
|
match: any,
|
||||||
|
t: string => string
|
||||||
};
|
};
|
||||||
|
|
||||||
class BranchRoot extends React.Component<Props> {
|
class BranchRoot extends React.Component<Props> {
|
||||||
@@ -92,10 +97,11 @@ class BranchRoot extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderBranchSelector = () => {
|
renderBranchSelector = () => {
|
||||||
const { repository, branches, selected } = this.props;
|
const { repository, branches, selected, t } = this.props;
|
||||||
if (repository._links.branches) {
|
if (repository._links.branches) {
|
||||||
return (
|
return (
|
||||||
<BranchSelector
|
<BranchSelector
|
||||||
|
label={t("branch-selector.label")}
|
||||||
branches={branches}
|
branches={branches}
|
||||||
selectedBranch={selected}
|
selectedBranch={selected}
|
||||||
selected={(b: Branch) => {
|
selected={(b: Branch) => {
|
||||||
@@ -133,6 +139,7 @@ const mapStateToProps = (state: any, ownProps: Props) => {
|
|||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
withRouter,
|
withRouter,
|
||||||
|
translate("repos"),
|
||||||
connect(
|
connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {deleteRepo, fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending} from "../modules/repos";
|
||||||
deleteRepo,
|
|
||||||
fetchRepoByName,
|
|
||||||
getFetchRepoFailure,
|
|
||||||
getRepository,
|
|
||||||
isFetchRepoPending
|
|
||||||
} from "../modules/repos";
|
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import { Route, Switch } from "react-router-dom";
|
import {Route, Switch} from "react-router-dom";
|
||||||
import type { Repository } from "@scm-manager/ui-types";
|
import type {Repository} from "@scm-manager/ui-types";
|
||||||
|
|
||||||
import {
|
import {ErrorPage, Loading, Navigation, NavLink, Page, Section} from "@scm-manager/ui-components";
|
||||||
ErrorPage,
|
import {translate} from "react-i18next";
|
||||||
Loading,
|
|
||||||
Navigation,
|
|
||||||
NavLink,
|
|
||||||
Page,
|
|
||||||
Section
|
|
||||||
} from "@scm-manager/ui-components";
|
|
||||||
import { translate } from "react-i18next";
|
|
||||||
import RepositoryDetails from "../components/RepositoryDetails";
|
import RepositoryDetails from "../components/RepositoryDetails";
|
||||||
import DeleteNavAction from "../components/DeleteNavAction";
|
import DeleteNavAction from "../components/DeleteNavAction";
|
||||||
import Edit from "../containers/Edit";
|
import Edit from "../containers/Edit";
|
||||||
import Permissions from "../permissions/containers/Permissions";
|
import Permissions from "../permissions/containers/Permissions";
|
||||||
|
|
||||||
import type { History } from "history";
|
import type {History} from "history";
|
||||||
import EditNavLink from "../components/EditNavLink";
|
import EditNavLink from "../components/EditNavLink";
|
||||||
|
|
||||||
import BranchRoot from "./ChangesetsRoot";
|
import BranchRoot from "./ChangesetsRoot";
|
||||||
@@ -34,8 +21,8 @@ import ChangesetView from "./ChangesetView";
|
|||||||
import PermissionsNavLink from "../components/PermissionsNavLink";
|
import PermissionsNavLink from "../components/PermissionsNavLink";
|
||||||
import Sources from "../sources/containers/Sources";
|
import Sources from "../sources/containers/Sources";
|
||||||
import RepositoryNavLink from "../components/RepositoryNavLink";
|
import RepositoryNavLink from "../components/RepositoryNavLink";
|
||||||
import { getRepositoriesLink } from "../../modules/indexResource";
|
import {getRepositoriesLink} from "../../modules/indexResource";
|
||||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
import {ExtensionPoint} from "@scm-manager/ui-extensions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
namespace: string,
|
namespace: string,
|
||||||
@@ -114,7 +101,7 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<Page title={repository.namespace + "/" + repository.name}>
|
<Page title={repository.namespace + "/" + repository.name}>
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="column is-three-quarters">
|
<div className="column is-three-quarters is-clipped">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
path={url}
|
path={url}
|
||||||
@@ -198,16 +185,16 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
label={t("repository-root.sources")}
|
label={t("repository-root.sources")}
|
||||||
activeOnlyWhenExact={false}
|
activeOnlyWhenExact={false}
|
||||||
/>
|
/>
|
||||||
<ExtensionPoint
|
|
||||||
name="repository.navigation"
|
|
||||||
props={extensionProps}
|
|
||||||
renderAll={true}
|
|
||||||
/>
|
|
||||||
<PermissionsNavLink
|
<PermissionsNavLink
|
||||||
permissionUrl={`${url}/permissions`}
|
permissionUrl={`${url}/permissions`}
|
||||||
repository={repository}
|
repository={repository}
|
||||||
/>
|
/>
|
||||||
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||||
|
<ExtensionPoint
|
||||||
|
name="repository.navigation"
|
||||||
|
props={extensionProps}
|
||||||
|
renderAll={true}
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
<Section label={t("repository-root.actions-label")}>
|
<Section label={t("repository-root.actions-label")}>
|
||||||
<DeleteNavAction repository={repository} delete={this.delete} />
|
<DeleteNavAction repository={repository} delete={this.delete} />
|
||||||
|
|||||||
@@ -224,9 +224,8 @@ export function modifyRepo(repository: Repository, callback?: () => void) {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(fetchRepoByLink(repository));
|
dispatch(fetchRepoByLink(repository));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`failed to modify repo: ${cause.message}`);
|
dispatch(modifyRepoFailure(repository, err));
|
||||||
dispatch(modifyRepoFailure(repository, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<hr />
|
||||||
<h2 className="subtitle">
|
<h2 className="subtitle">
|
||||||
{t("permission.add-permission.add-permission-heading")}
|
{t("permission.add-permission.add-permission-heading")}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -153,19 +154,29 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
|||||||
{t("permission.group-permission")}
|
{t("permission.group-permission")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{this.renderAutocompletionField()}
|
|
||||||
|
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-three-quarters">
|
||||||
|
{this.renderAutocompletionField()}
|
||||||
|
</div>
|
||||||
|
<div className="column is-one-quarter">
|
||||||
<TypeSelector
|
<TypeSelector
|
||||||
label={t("permission.type")}
|
label={t("permission.type")}
|
||||||
helpText={t("permission.help.typeHelpText")}
|
helpText={t("permission.help.typeHelpText")}
|
||||||
handleTypeChange={this.handleTypeChange}
|
handleTypeChange={this.handleTypeChange}
|
||||||
type={type ? type : "READ"}
|
type={type ? type : "READ"}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
label={t("permission.add-permission.submit-button")}
|
label={t("permission.add-permission.submit-button")}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!this.state.valid || this.state.name === ""}
|
disabled={!this.state.valid || this.state.name === ""}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class Permissions extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<table className="table is-hoverable is-fullwidth">
|
<table className="has-background-light table is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("permission.name")}</th>
|
<th>{t("permission.name")}</th>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import type {Action} from "@scm-manager/ui-components";
|
import type { Action } from "@scm-manager/ui-components";
|
||||||
import {apiClient} from "@scm-manager/ui-components";
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
import * as types from "../../../modules/types";
|
import * as types from "../../../modules/types";
|
||||||
import type {Permission, PermissionCollection, PermissionCreateEntry} from "@scm-manager/ui-types";
|
import type {
|
||||||
import {isPending} from "../../../modules/pending";
|
Permission,
|
||||||
import {getFailure} from "../../../modules/failure";
|
PermissionCollection,
|
||||||
import {Dispatch} from "redux";
|
PermissionCreateEntry
|
||||||
|
} from "@scm-manager/ui-types";
|
||||||
|
import { isPending } from "../../../modules/pending";
|
||||||
|
import { getFailure } from "../../../modules/failure";
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
|
||||||
export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS";
|
export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS";
|
||||||
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
|
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
|
||||||
@@ -141,13 +145,8 @@ export function modifyPermission(
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(
|
dispatch(modifyPermissionFailure(permission, err, namespace, repoName));
|
||||||
`failed to modify permission: ${cause.message}`
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
modifyPermissionFailure(permission, error, namespace, repoName)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -241,15 +240,7 @@ export function createPermission(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err =>
|
.catch(err =>
|
||||||
dispatch(
|
dispatch(createPermissionFailure(err, namespace, repoName))
|
||||||
createPermissionFailure(
|
|
||||||
new Error(
|
|
||||||
`failed to add permission ${permission.name}: ${err.message}`
|
|
||||||
),
|
|
||||||
namespace,
|
|
||||||
repoName
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -318,13 +309,8 @@ export function deletePermission(
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(
|
dispatch(deletePermissionFailure(permission, namespace, repoName, err));
|
||||||
`could not delete permission ${permission.name}: ${cause.message}`
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
deletePermissionFailure(permission, namespace, repoName, error)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ class FileTree extends React.Component<Props> {
|
|||||||
<th className="is-hidden-mobile">
|
<th className="is-hidden-mobile">
|
||||||
{t("sources.file-tree.lastModified")}
|
{t("sources.file-tree.lastModified")}
|
||||||
</th>
|
</th>
|
||||||
<th>{t("sources.file-tree.description")}</th>
|
<th className="is-hidden-mobile">
|
||||||
|
{t("sources.file-tree.description")}
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import FileSize from "./FileSize";
|
|||||||
import FileIcon from "./FileIcon";
|
import FileIcon from "./FileIcon";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import type { File } from "@scm-manager/ui-types";
|
import type { File } from "@scm-manager/ui-types";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
iconColumn: {
|
iconColumn: {
|
||||||
width: "16px"
|
width: "16px"
|
||||||
|
},
|
||||||
|
wordBreakMinWidth: {
|
||||||
|
minWidth: "10em"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,12 +75,14 @@ class FileTreeLeaf extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td className={classes.iconColumn}>{this.createFileIcon(file)}</td>
|
<td className={classes.iconColumn}>{this.createFileIcon(file)}</td>
|
||||||
<td>{this.createFileName(file)}</td>
|
<td className={classNames(classes.wordBreakMinWidth, "is-word-break")}>{this.createFileName(file)}</td>
|
||||||
<td className="is-hidden-mobile">{fileSize}</td>
|
<td className="is-hidden-mobile">{fileSize}</td>
|
||||||
<td className="is-hidden-mobile">
|
<td className="is-hidden-mobile">
|
||||||
<DateFromNow date={file.lastModified} />
|
<DateFromNow date={file.lastModified} />
|
||||||
</td>
|
</td>
|
||||||
<td>{file.description}</td>
|
<td className={classNames(classes.wordBreakMinWidth, "is-word-break", "is-hidden-mobile")}>
|
||||||
|
{file.description}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ const styles = {
|
|||||||
isVerticalCenter: {
|
isVerticalCenter: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center"
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
hasBackground: {
|
||||||
|
backgroundColor: "#FBFBFB"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,7 +96,7 @@ class Content extends React.Component<Props, State> {
|
|||||||
classes.marginInHeader
|
classes.marginInHeader
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span>{file.name}</span>
|
<span className="is-word-break">{file.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-right">{selector}</div>
|
<div className="media-right">{selector}</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -120,16 +123,22 @@ class Content extends React.Component<Props, State> {
|
|||||||
const fileSize = file.directory ? "" : <FileSize bytes={file.length} />;
|
const fileSize = file.directory ? "" : <FileSize bytes={file.length} />;
|
||||||
if (!collapsed) {
|
if (!collapsed) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames("panel-block", classes.toCenterContent)}>
|
<div
|
||||||
<table className="table">
|
className={classNames(
|
||||||
|
"panel-block",
|
||||||
|
classes.toCenterContent,
|
||||||
|
classes.hasBackground
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<table className={classNames("table", classes.hasBackground)}>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("sources.content.path")}</td>
|
<td>{t("sources.content.path")}</td>
|
||||||
<td>{file.path}</td>
|
<td className="is-word-break">{file.path}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("sources.content.branch")}</td>
|
<td>{t("sources.content.branch")}</td>
|
||||||
<td>{revision}</td>
|
<td className="is-word-break">{revision}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("sources.content.size")}</td>
|
<td>{t("sources.content.size")}</td>
|
||||||
@@ -141,7 +150,7 @@ class Content extends React.Component<Props, State> {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("sources.content.description")}</td>
|
<td>{t("sources.content.description")}</td>
|
||||||
<td>{description}</td>
|
<td className="is-word-break">{description}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { withRouter } from "react-router-dom";
|
|||||||
import type { Branch, Repository } from "@scm-manager/ui-types";
|
import type { Branch, Repository } from "@scm-manager/ui-types";
|
||||||
import FileTree from "../components/FileTree";
|
import FileTree from "../components/FileTree";
|
||||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||||
import BranchSelector from "../../containers/BranchSelector";
|
import BranchSelector from "../../../../../scm-ui-components/packages/ui-components/src/BranchSelector";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
fetchBranches,
|
fetchBranches,
|
||||||
getBranches,
|
getBranches,
|
||||||
@@ -32,7 +33,8 @@ type Props = {
|
|||||||
|
|
||||||
// Context props
|
// Context props
|
||||||
history: any,
|
history: any,
|
||||||
match: any
|
match: any,
|
||||||
|
t: string => string
|
||||||
};
|
};
|
||||||
|
|
||||||
class Sources extends React.Component<Props> {
|
class Sources extends React.Component<Props> {
|
||||||
@@ -91,7 +93,7 @@ class Sources extends React.Component<Props> {
|
|||||||
|
|
||||||
if (currentFileIsDirectory) {
|
if (currentFileIsDirectory) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={"has-border-around"}>
|
||||||
{this.renderBranchSelector()}
|
{this.renderBranchSelector()}
|
||||||
<FileTree
|
<FileTree
|
||||||
repository={repository}
|
repository={repository}
|
||||||
@@ -99,7 +101,7 @@ class Sources extends React.Component<Props> {
|
|||||||
path={path}
|
path={path}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
@@ -109,13 +111,14 @@ class Sources extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderBranchSelector = () => {
|
renderBranchSelector = () => {
|
||||||
const { branches, revision } = this.props;
|
const { branches, revision, t } = this.props;
|
||||||
|
|
||||||
if (branches) {
|
if (branches) {
|
||||||
return (
|
return (
|
||||||
<BranchSelector
|
<BranchSelector
|
||||||
branches={branches}
|
branches={branches}
|
||||||
selectedBranch={revision}
|
selectedBranch={revision}
|
||||||
|
label={t("branch-selector.label")}
|
||||||
selected={(b: Branch) => {
|
selected={(b: Branch) => {
|
||||||
this.branchSelected(b);
|
this.branchSelected(b);
|
||||||
}}
|
}}
|
||||||
@@ -160,6 +163,7 @@ const mapDispatchToProps = dispatch => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
|
translate("repos"),
|
||||||
withRouter,
|
withRouter,
|
||||||
connect(
|
connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ export function fetchSources(
|
|||||||
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
|
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
const error = new Error(`failed to fetch sources: ${err.message}`);
|
dispatch(fetchSourcesFailure(repository, revision, path, err));
|
||||||
dispatch(fetchSourcesFailure(repository, revision, path, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -93,8 +92,8 @@ export default function reducer(
|
|||||||
): any {
|
): any {
|
||||||
if (action.itemId && action.type === FETCH_SOURCES_SUCCESS) {
|
if (action.itemId && action.type === FETCH_SOURCES_SUCCESS) {
|
||||||
return {
|
return {
|
||||||
[action.itemId]: action.payload,
|
...state,
|
||||||
...state
|
[action.itemId]: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.submit}>
|
<form onSubmit={this.submit}>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
{nameField}
|
{nameField}
|
||||||
<InputField
|
<InputField
|
||||||
label={t("user.displayName")}
|
label={t("user.displayName")}
|
||||||
@@ -114,6 +116,8 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
errorMessage={t("validation.displayname-invalid")}
|
errorMessage={t("validation.displayname-invalid")}
|
||||||
helpText={t("help.displayNameHelpText")}
|
helpText={t("help.displayNameHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("user.mail")}
|
label={t("user.mail")}
|
||||||
onChange={this.handleEmailChange}
|
onChange={this.handleEmailChange}
|
||||||
@@ -122,6 +126,10 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
errorMessage={t("validation.mail-invalid")}
|
errorMessage={t("validation.mail-invalid")}
|
||||||
helpText={t("help.mailHelpText")}
|
helpText={t("help.mailHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
{passwordChangeField}
|
{passwordChangeField}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={t("user.admin")}
|
label={t("user.admin")}
|
||||||
@@ -135,11 +143,17 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
checked={user ? user.active : false}
|
checked={user ? user.active : false}
|
||||||
helpText={t("help.activeHelpText")}
|
helpText={t("help.activeHelpText")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
disabled={!this.isValid()}
|
disabled={!this.isValid()}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
label={t("user-form.submit")}
|
label={t("user-form.submit")}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,43 +16,43 @@ class Details extends React.Component<Props> {
|
|||||||
<table className="table">
|
<table className="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("user.name")}</td>
|
<td className="has-text-weight-semibold">{t("user.name")}</td>
|
||||||
<td>{user.name}</td>
|
<td>{user.name}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("user.displayName")}</td>
|
<td className="has-text-weight-semibold">{t("user.displayName")}</td>
|
||||||
<td>{user.displayName}</td>
|
<td>{user.displayName}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("user.mail")}</td>
|
<td className="has-text-weight-semibold">{t("user.mail")}</td>
|
||||||
<td>
|
<td>
|
||||||
<MailLink address={user.mail} />
|
<MailLink address={user.mail} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("user.admin")}</td>
|
<td className="has-text-weight-semibold">{t("user.admin")}</td>
|
||||||
<td>
|
<td>
|
||||||
<Checkbox checked={user.admin} />
|
<Checkbox checked={user.admin} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("user.active")}</td>
|
<td className="has-text-weight-semibold">{t("user.active")}</td>
|
||||||
<td>
|
<td>
|
||||||
<Checkbox checked={user.active} />
|
<Checkbox checked={user.active} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("user.type")}</td>
|
<td className="has-text-weight-semibold">{t("user.type")}</td>
|
||||||
<td>{user.type}</td>
|
<td>{user.type}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("user.creationDate")}</td>
|
<td className="has-text-weight-semibold">{t("user.creationDate")}</td>
|
||||||
<td>
|
<td>
|
||||||
<DateFromNow date={user.creationDate} />
|
<DateFromNow date={user.creationDate} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("user.lastModified")}</td>
|
<td className="has-text-weight-semibold">{t("user.lastModified")}</td>
|
||||||
<td>
|
<td>
|
||||||
<DateFromNow date={user.lastModified} />
|
<DateFromNow date={user.lastModified} />
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ type Props = {
|
|||||||
users: User[]
|
users: User[]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
class UserTable extends React.Component<Props> {
|
class UserTable extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { users, t } = this.props;
|
const { users, t } = this.props;
|
||||||
return (
|
return (
|
||||||
<table className="table is-hoverable is-fullwidth">
|
<table className="card-table table is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="is-hidden-mobile">{t("user.name")}</th>
|
<th className="is-hidden-mobile">{t("user.name")}</th>
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`;
|
|||||||
|
|
||||||
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
|
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
|
||||||
|
|
||||||
// TODO i18n for error messages
|
|
||||||
|
|
||||||
// fetch users
|
// fetch users
|
||||||
|
|
||||||
export function fetchUsers(link: string) {
|
export function fetchUsers(link: string) {
|
||||||
@@ -57,9 +55,8 @@ export function fetchUsersByLink(link: string) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(fetchUsersSuccess(data));
|
dispatch(fetchUsersSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`could not fetch users: ${cause.message}`);
|
dispatch(fetchUsersFailure(link, err));
|
||||||
dispatch(fetchUsersFailure(link, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -108,9 +105,8 @@ function fetchUser(link: string, name: string) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(fetchUserSuccess(data));
|
dispatch(fetchUserSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`could not fetch user: ${cause.message}`);
|
dispatch(fetchUserFailure(name, err));
|
||||||
dispatch(fetchUserFailure(name, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -155,13 +151,7 @@ export function createUser(link: string, user: User, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err =>
|
.catch(err => dispatch(createUserFailure(err)));
|
||||||
dispatch(
|
|
||||||
createUserFailure(
|
|
||||||
new Error(`failed to add user ${user.name}: ${err.message}`)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,11 +250,8 @@ export function deleteUser(user: User, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(
|
dispatch(deleteUserFailure(user, err));
|
||||||
`could not delete user ${user.name}: ${cause.message}`
|
|
||||||
);
|
|
||||||
dispatch(deleteUserFailure(user, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user