Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2018-12-21 14:08:46 +01:00
118 changed files with 4004 additions and 12137 deletions

View File

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

View File

@@ -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);
}

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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));
}
} }

View File

@@ -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);
}
}

View File

@@ -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())

View 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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/"
}
}
}

View File

@@ -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/"
}
}
}
]
}
}

View File

@@ -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);
} }
/** /**

View File

@@ -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();
}
} }

View File

@@ -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()));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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() {
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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)
);
}
} }

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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));

View File

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

View File

@@ -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;

View File

@@ -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);
} }

View File

@@ -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() {
} }

View 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);

View File

@@ -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
);

View File

@@ -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!"
} }
} }
} }

View File

@@ -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;
} }
} }

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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());

View File

@@ -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);

View File

@@ -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));
} }

View File

@@ -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);
} }
} }

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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);

View File

@@ -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);
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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:*

View File

@@ -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/"
}
}
}

View File

@@ -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/"
}
}
}
]
}
}

View File

@@ -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<>();
} }
} }

View File

@@ -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);
}
}

View File

@@ -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<>();
}
}

View File

@@ -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);

View File

@@ -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;
} }
} }

View File

@@ -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);

View File

@@ -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>
); );
} }

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

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

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

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

View File

@@ -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}`)
)
);
}); });
}; };
} }

View File

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

View File

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

View File

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

View File

@@ -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));
}); });
}; };
} }

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,8 @@ import classNames from "classnames";
const styles = { const styles = {
link: { link: {
pointerEvents: "all" pointerEvents: "all",
marginRight: "1.25rem !important"
} }
}; };

View File

@@ -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>
); );
} }
} }

View File

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

View File

@@ -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} />

View File

@@ -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));
}); });
}; };
} }

View File

@@ -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>
); );

View File

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

View File

@@ -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)
);
}); });
}; };
} }

View File

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

View File

@@ -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>
); );
} }

View File

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

View File

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

View File

@@ -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;

View File

@@ -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>
); );
} }

View File

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

View File

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

View File

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