diff --git a/deployments/helm/templates/configmap.yaml b/deployments/helm/templates/configmap.yaml index dd52b6fa8c..1ccb773355 100644 --- a/deployments/helm/templates/configmap.yaml +++ b/deployments/helm/templates/configmap.yaml @@ -108,7 +108,7 @@ data: - <-- + diff --git a/deployments/helm/templates/deployment.yaml b/deployments/helm/templates/deployment.yaml index 928daa5f06..7e19f61e57 100644 --- a/deployments/helm/templates/deployment.yaml +++ b/deployments/helm/templates/deployment.yaml @@ -29,6 +29,17 @@ spec: volumeMounts: - name: data mountPath: /data + {{- if .Values.plugins }} + - name: install-plugins + image: alpine:3.8 + imagePullPolicy: IfNotPresent + command: ['sh', '/scripts/install-plugins.sh'] + volumeMounts: + - name: data + mountPath: /data + - name: scripts + mountPath: /scripts + {{- end }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" @@ -63,6 +74,11 @@ spec: - name: config configMap: name: {{ include "scm-manager.fullname" . }} + {{- if .Values.plugins }} + - name: scripts + configMap: + name: {{ include "scm-manager.fullname" . }}-scripts + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{ toYaml . | indent 8 }} diff --git a/deployments/helm/templates/scripts.yaml b/deployments/helm/templates/scripts.yaml new file mode 100644 index 0000000000..43a442a8e2 --- /dev/null +++ b/deployments/helm/templates/scripts.yaml @@ -0,0 +1,21 @@ +{{- if .Values.plugins }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "scm-manager.fullname" . }}-scripts + labels: + app: {{ include "scm-manager.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +data: + install-plugins.sh: | + #!/bin/sh + mkdir -p /data/plugins + chown 1000:1000 /data/plugins + {{ range $i, $plugin := .Values.plugins }} + # install plugin {{ $plugin.name }} + wget -O /data/plugins/{{ $plugin.name }}.smp {{ $plugin.url }} + chown 1000:1000 /data/plugins/{{ $plugin.name }}.smp + {{ end }} +{{- end }} diff --git a/deployments/helm/values.yaml b/deployments/helm/values.yaml index d54088aa8b..0b107a8168 100644 --- a/deployments/helm/values.yaml +++ b/deployments/helm/values.yaml @@ -10,6 +10,10 @@ image: tag: latest pullPolicy: Always +# plugins: +# - name: scm-review-plugin +# url: https://oss.cloudogu.com/jenkins/job/scm-manager/job/scm-manager-bitbucket/job/scm-review-plugin/job/develop/lastSuccessfulBuild/artifact/target/scm-review-plugin-2.0.0-SNAPSHOT.smp + nameOverride: "" fullnameOverride: "" diff --git a/pom.xml b/pom.xml index e480d1527b..f8bb7c5727 100644 --- a/pom.xml +++ b/pom.xml @@ -142,6 +142,11 @@ junit-vintage-engine + + org.junit-pioneer + junit-pioneer + + org.hamcrest hamcrest-core @@ -159,6 +164,11 @@ mockito-core + + org.mockito + mockito-junit-jupiter + + org.assertj assertj-core @@ -325,6 +335,13 @@ test + + org.junit-pioneer + junit-pioneer + 0.3.0 + test + + org.hamcrest hamcrest-core @@ -346,6 +363,13 @@ test + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + org.assertj assertj-core diff --git a/scm-core/src/main/java/sonia/scm/BasicContextProvider.java b/scm-core/src/main/java/sonia/scm/BasicContextProvider.java index f6507fc453..6954c03832 100644 --- a/scm-core/src/main/java/sonia/scm/BasicContextProvider.java +++ b/scm-core/src/main/java/sonia/scm/BasicContextProvider.java @@ -35,6 +35,7 @@ package sonia.scm; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; import sonia.scm.util.Util; //~--- JDK imports ------------------------------------------------------------ @@ -43,6 +44,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.util.Locale; import java.util.Properties; @@ -105,8 +107,26 @@ public class BasicContextProvider implements SCMContextProvider } } + @VisibleForTesting + BasicContextProvider(File baseDirectory, String version, Stage stage) { + this.baseDirectory = baseDirectory; + this.version = version; + this.stage = stage; + } + //~--- methods -------------------------------------------------------------- + + @Override + public Path resolve(Path path) { + if (path.isAbsolute()) { + return path; + } + + return baseDirectory.toPath().resolve(path); + } + + /** * {@inheritDoc} */ diff --git a/scm-core/src/main/java/sonia/scm/SCMContextProvider.java b/scm-core/src/main/java/sonia/scm/SCMContextProvider.java index 18328403fe..93918770c8 100644 --- a/scm-core/src/main/java/sonia/scm/SCMContextProvider.java +++ b/scm-core/src/main/java/sonia/scm/SCMContextProvider.java @@ -37,6 +37,7 @@ package sonia.scm; import java.io.Closeable; import java.io.File; +import java.nio.file.Path; /** * The main class for retrieving the home and the version of the SCM-Manager. @@ -65,6 +66,17 @@ public interface SCMContextProvider extends Closeable */ public File getBaseDirectory(); + /** + * Resolves the given path against the base directory. + * + * @param path path to resolve + * + * @return absolute resolved path + * + * @since 2.0.0 + */ + Path resolve(Path path); + /** * Returns the current stage of SCM-Manager. * diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java index d494858494..567d2a6e84 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java @@ -172,7 +172,7 @@ public abstract class AbstractRepositoryHandler * @throws NotSupportedFeatureException */ @Override - public ImportHandler getImportHandler() throws NotSupportedFeatureException + public ImportHandler getImportHandler() { throw new NotSupportedFeatureException("import"); } diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index b14c7dd3de..9941a4253b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -76,7 +76,7 @@ public abstract class AbstractSimpleRepositoryHandler) invocationOnMock -> invocationOnMock.getArgument(0)); + } + + private RepositoryLocationResolver createResolver(RepositoryDAO pathBasedRepositoryDAO) { + return new RepositoryLocationResolver(contextProvider, pathBasedRepositoryDAO, initialRepositoryLocationResolver); + } + + @Test + void shouldReturnPathFromDao() { + Path repositoryPath = Paths.get("repos", "42"); + when(pathBasedRepositoryDAO.getPath("42")).thenReturn(repositoryPath); + + RepositoryLocationResolver resolver = createResolver(pathBasedRepositoryDAO); + Path path = resolver.getPath("42"); + + assertThat(path).isSameAs(repositoryPath); + } + + @Test + void shouldReturnInitialPathIfDaoIsNotPathBased() { + Path repositoryPath = Paths.get("r", "42"); + when(initialRepositoryLocationResolver.getPath("42")).thenReturn(repositoryPath); + + RepositoryLocationResolver resolver = createResolver(repositoryDAO); + Path path = resolver.getPath("42"); + + assertThat(path).isSameAs(repositoryPath); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java b/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java index ecfdd06a0f..16c4278793 100644 --- a/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java +++ b/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java @@ -89,7 +89,7 @@ public class IndentXMLStreamWriterTest StringBuilder buffer = new StringBuilder(""); buffer.append(IndentXMLStreamWriter.LINE_SEPARATOR); buffer.append("").append(IndentXMLStreamWriter.LINE_SEPARATOR); - buffer.append(" Hello"); + buffer.append(" Hello"); buffer.append(IndentXMLStreamWriter.LINE_SEPARATOR); buffer.append("").append(IndentXMLStreamWriter.LINE_SEPARATOR); diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java new file mode 100644 index 0000000000..1f5f0e81b6 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java @@ -0,0 +1,50 @@ +package sonia.scm.repository.xml; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ContextEntry; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import java.nio.file.Path; + +class MetadataStore { + + private static final Logger LOG = LoggerFactory.getLogger(MetadataStore.class); + + private final JAXBContext jaxbContext; + + MetadataStore() { + try { + jaxbContext = JAXBContext.newInstance(Repository.class); + } catch (JAXBException ex) { + throw new IllegalStateException("failed to create jaxb context for repository", ex); + } + } + + Repository read(Path path) { + LOG.trace("read repository metadata from {}", path); + try { + return (Repository) jaxbContext.createUnmarshaller().unmarshal(path.toFile()); + } catch (JAXBException ex) { + throw new InternalRepositoryException( + ContextEntry.ContextBuilder.entity(Path.class, path.toString()).build(), "failed read repository metadata", ex + ); + } + } + + void write(Path path, Repository repository) { + LOG.trace("write repository metadata of {} to {}", repository.getNamespaceAndName(), path); + try { + Marshaller marshaller = jaxbContext.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + marshaller.marshal(repository, path.toFile()); + } catch (JAXBException ex) { + throw new InternalRepositoryException(repository, "failed write repository metadata", ex); + } + } + +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java new file mode 100644 index 0000000000..bddcdec570 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java @@ -0,0 +1,145 @@ +package sonia.scm.repository.xml; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ContextEntry; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.xml.IndentXMLStreamWriter; +import sonia.scm.xml.XmlStreams; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +class PathDatabase { + + private static final Logger LOG = LoggerFactory.getLogger(PathDatabase.class); + + private static final String ENCODING = "UTF-8"; + private static final String VERSION = "1.0"; + + private static final String ELEMENT_REPOSITORIES = "repositories"; + private static final String ATTRIBUTE_CREATION_TIME = "creation-time"; + private static final String ATTRIBUTE_LAST_MODIFIED = "last-modified"; + + private static final String ELEMENT_REPOSITORY = "repository"; + private static final String ATTRIBUTE_ID = "id"; + + private final Path storePath; + + PathDatabase(Path storePath){ + this.storePath = storePath; + } + + void write(Long creationTime, Long lastModified, Map pathDatabase) { + ensureParentDirectoryExists(); + LOG.trace("write repository path database to {}", storePath); + + try (IndentXMLStreamWriter writer = XmlStreams.createWriter(storePath)) { + writer.writeStartDocument(ENCODING, VERSION); + + writeRepositoriesStart(writer, creationTime, lastModified); + for (Map.Entry e : pathDatabase.entrySet()) { + writeRepository(writer, e.getKey(), e.getValue()); + } + writer.writeEndElement(); + + writer.writeEndDocument(); + } catch (XMLStreamException | IOException ex) { + throw new InternalRepositoryException( + ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(), + "failed to write repository path database", + ex + ); + } + } + + private void ensureParentDirectoryExists() { + Path parent = storePath.getParent(); + if (!Files.exists(parent)) { + try { + Files.createDirectories(parent); + } catch (IOException ex) { + throw new InternalRepositoryException( + ContextEntry.ContextBuilder.entity(Path.class, parent.toString()).build(), + "failed to create parent directory", + ex + ); + } + } + } + + private void writeRepositoriesStart(XMLStreamWriter writer, Long creationTime, Long lastModified) throws XMLStreamException { + writer.writeStartElement(ELEMENT_REPOSITORIES); + writer.writeAttribute(ATTRIBUTE_CREATION_TIME, String.valueOf(creationTime)); + writer.writeAttribute(ATTRIBUTE_LAST_MODIFIED, String.valueOf(lastModified)); + } + + private void writeRepository(XMLStreamWriter writer, String id, Path value) throws XMLStreamException { + writer.writeStartElement(ELEMENT_REPOSITORY); + writer.writeAttribute(ATTRIBUTE_ID, id); + writer.writeCharacters(value.toString()); + writer.writeEndElement(); + } + + void read(OnRepositories onRepositories, OnRepository onRepository) { + LOG.trace("read repository path database from {}", storePath); + XMLStreamReader reader = null; + try { + reader = XmlStreams.createReader(storePath); + + while (reader.hasNext()) { + int eventType = reader.next(); + + if (eventType == XMLStreamReader.START_ELEMENT) { + String element = reader.getLocalName(); + if (ELEMENT_REPOSITORIES.equals(element)) { + readRepositories(reader, onRepositories); + } else if (ELEMENT_REPOSITORY.equals(element)) { + readRepository(reader, onRepository); + } + } + } + } catch (XMLStreamException | IOException ex) { + throw new InternalRepositoryException( + ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(), + "failed to read repository path database", + ex + ); + } finally { + XmlStreams.close(reader); + } + } + + private void readRepository(XMLStreamReader reader, OnRepository onRepository) throws XMLStreamException { + String id = reader.getAttributeValue(null, ATTRIBUTE_ID); + Path path = Paths.get(reader.getElementText()); + onRepository.handle(id, path); + } + + private void readRepositories(XMLStreamReader reader, OnRepositories onRepositories) { + String creationTime = reader.getAttributeValue(null, ATTRIBUTE_CREATION_TIME); + String lastModified = reader.getAttributeValue(null, ATTRIBUTE_LAST_MODIFIED); + onRepositories.handle(Long.parseLong(creationTime), Long.parseLong(lastModified)); + } + + @FunctionalInterface + interface OnRepositories { + + void handle(Long creationTime, Long lastModified); + + } + + @FunctionalInterface + interface OnRepository { + + void handle(String id, Path path); + + } + +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/RepositoryPath.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/RepositoryPath.java deleted file mode 100644 index db57b228f9..0000000000 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/RepositoryPath.java +++ /dev/null @@ -1,101 +0,0 @@ -package sonia.scm.repository.xml; - -import org.apache.commons.lang.StringUtils; -import sonia.scm.ModelObject; -import sonia.scm.repository.Repository; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlTransient; - -@XmlRootElement(name = "repository-link") -@XmlAccessorType(XmlAccessType.FIELD) -public class RepositoryPath implements ModelObject { - - private String path; - private String id; - private Long lastModified; - private Long creationDate; - - @XmlTransient - private Repository repository; - - @XmlTransient - private boolean toBeSynchronized; - - /** - * Needed from JAXB - */ - public RepositoryPath() { - } - - public RepositoryPath(String path, String id, Repository repository) { - this.path = path; - this.id = id; - this.repository = repository; - } - - public Repository getRepository() { - return repository; - } - - public void setRepository(Repository repository) { - this.repository = repository; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - @Override - public String getId() { - return id; - } - - @Override - public void setLastModified(Long lastModified) { - this.lastModified = lastModified; - } - - @Override - public Long getCreationDate() { - return creationDate; - } - - @Override - public void setCreationDate(Long creationDate) { - this.creationDate = creationDate; - } - - public void setId(String id) { - this.id = id; - } - - @Override - public Long getLastModified() { - return lastModified; - } - - @Override - public String getType() { - return getRepository()!= null? getRepository().getType():""; - } - - @Override - public boolean isValid() { - return StringUtils.isNotEmpty(path); - } - - public boolean toBeSynchronized() { - return toBeSynchronized; - } - - public void setToBeSynchronized(boolean toBeSynchronized) { - this.toBeSynchronized = toBeSynchronized; - } -} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java index 7c4290c836..de51ebdef7 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java @@ -1,19 +1,19 @@ /** * 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. + * 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. + * 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. - * + * 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 @@ -24,9 +24,8 @@ * 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 - * */ @@ -34,153 +33,229 @@ package sonia.scm.repository.xml; //~--- non-JDK imports -------------------------------------------------------- -import com.google.inject.Inject; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import com.google.inject.Singleton; import sonia.scm.SCMContextProvider; import sonia.scm.io.FileSystem; import sonia.scm.repository.InitialRepositoryLocationResolver; -import sonia.scm.repository.InitialRepositoryLocationResolver.InitialRepositoryLocation; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.PathBasedRepositoryDAO; import sonia.scm.repository.Repository; -import sonia.scm.store.JAXBConfigurationStore; -import sonia.scm.store.Store; import sonia.scm.store.StoreConstants; -import sonia.scm.xml.AbstractXmlDAO; -import java.io.File; +import javax.inject.Inject; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.time.Clock; import java.util.Collection; -import java.util.Optional; +import java.util.LinkedHashMap; +import java.util.Map; /** * @author Sebastian Sdorra */ @Singleton -public class XmlRepositoryDAO - extends AbstractXmlDAO - implements PathBasedRepositoryDAO { +public class XmlRepositoryDAO implements PathBasedRepositoryDAO { - public static final String STORE_NAME = "repositories"; + private static final String STORE_NAME = "repositories"; + + private final PathDatabase pathDatabase; + private final MetadataStore metadataStore = new MetadataStore(); - private InitialRepositoryLocationResolver initialRepositoryLocationResolver; - private final FileSystem fileSystem; private final SCMContextProvider context; + private final InitialRepositoryLocationResolver locationResolver; + private final FileSystem fileSystem; - //~--- constructors --------------------------------------------------------- + @VisibleForTesting + Clock clock = Clock.systemUTC(); + + private Long creationTime; + private Long lastModified; + + private Map pathById; + private Map byId; + private Map byNamespaceAndName; @Inject - public XmlRepositoryDAO(InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem, SCMContextProvider context) { - super(new JAXBConfigurationStore<>(XmlRepositoryDatabase.class, - new File(context.getBaseDirectory(), Store.CONFIG.getGlobalStoreDirectory()+File.separator+ STORE_NAME + StoreConstants.FILE_EXTENSION))); - this.initialRepositoryLocationResolver = initialRepositoryLocationResolver; - this.fileSystem = fileSystem; + public XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem) { this.context = context; + this.locationResolver = locationResolver; + this.fileSystem = fileSystem; + + this.creationTime = clock.millis(); + + this.pathById = new LinkedHashMap<>(); + this.byId = new LinkedHashMap<>(); + this.byNamespaceAndName = new LinkedHashMap<>(); + + pathDatabase = new PathDatabase(createStorePath()); + read(); } - //~--- methods -------------------------------------------------------------- + private void read() { + Path storePath = createStorePath(); - @Override - public boolean contains(NamespaceAndName namespaceAndName) { - return db.contains(namespaceAndName); + if (!Files.exists(storePath)) { + return; + } + + pathDatabase.read(this::loadDates, this::loadRepository); } - //~--- get methods ---------------------------------------------------------- - - @Override - public Repository get(NamespaceAndName namespaceAndName) { - return db.get(namespaceAndName); + private void loadDates(Long creationTime, Long lastModified) { + this.creationTime = creationTime; + this.lastModified = lastModified; } - //~--- methods -------------------------------------------------------------- + private void loadRepository(String id, Path repositoryPath) { + Path metadataPath = createMetadataPath(context.resolve(repositoryPath)); + Repository repository = metadataStore.read(metadataPath); + + byId.put(id, repository); + byNamespaceAndName.put(repository.getNamespaceAndName(), repository); + pathById.put(id, repositoryPath); + } + + @VisibleForTesting + Path createStorePath() { + return context.getBaseDirectory() + .toPath() + .resolve(StoreConstants.CONFIG_DIRECTORY_NAME) + .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION)); + } + + + @VisibleForTesting + Path createMetadataPath(Path repositoryPath) { + return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION)); + } @Override - public void modify(Repository repository) { - RepositoryPath repositoryPath = findExistingRepositoryPath(repository).orElseThrow(() -> new InternalRepositoryException(repository, "path object for repository not found")); - repositoryPath.setRepository(repository); - repositoryPath.setToBeSynchronized(true); - storeDB(); + public String getType() { + return "xml"; + } + + @Override + public Long getCreationTime() { + return creationTime; + } + + @Override + public Long getLastModified() { + return lastModified; } @Override public void add(Repository repository) { - InitialRepositoryLocation initialLocation = initialRepositoryLocationResolver.getRelativeRepositoryPath(repository); + Repository clone = repository.clone(); + + Path repositoryPath = locationResolver.getPath(repository.getId()); + Path resolvedPath = context.resolve(repositoryPath); + try { - fileSystem.create(initialLocation.getAbsolutePath()); + fileSystem.create(resolvedPath.toFile()); + + Path metadataPath = createMetadataPath(resolvedPath); + metadataStore.write(metadataPath, repository); + + synchronized (this) { + pathById.put(repository.getId(), repositoryPath); + + byId.put(repository.getId(), clone); + byNamespaceAndName.put(repository.getNamespaceAndName(), clone); + + writePathDatabase(); + } + } catch (IOException e) { - throw new InternalRepositoryException(repository, "could not create directory for repository data: " + initialLocation.getAbsolutePath(), e); - } - RepositoryPath repositoryPath = new RepositoryPath(initialLocation.getRelativePath(), repository.getId(), repository.clone()); - repositoryPath.setToBeSynchronized(true); - synchronized (store) { - db.add(repositoryPath); - storeDB(); + throw new InternalRepositoryException(repository, "failed to create filesystem", e); } } + private void writePathDatabase() { + lastModified = clock.millis(); + pathDatabase.write(creationTime, lastModified, pathById); + } + + @Override + public boolean contains(Repository repository) { + return byId.containsKey(repository.getId()); + } + + @Override + public boolean contains(NamespaceAndName namespaceAndName) { + return byNamespaceAndName.containsKey(namespaceAndName); + } + + @Override + public boolean contains(String id) { + return byId.containsKey(id); + } + + @Override + public Repository get(NamespaceAndName namespaceAndName) { + return byNamespaceAndName.get(namespaceAndName); + } + @Override public Repository get(String id) { - RepositoryPath repositoryPath = db.get(id); - if (repositoryPath != null) { - return repositoryPath.getRepository(); - } - return null; + return byId.get(id); } @Override public Collection getAll() { - return db.getRepositories(); + return ImmutableList.copyOf(byNamespaceAndName.values()); } - /** - * Method description - * - * @param repository - * @return - */ @Override - protected Repository clone(Repository repository) { - return repository.clone(); + public void modify(Repository repository) { + Repository clone = repository.clone(); + + synchronized (this) { + // remove old namespaceAndName from map, in case of rename + Repository prev = byId.put(clone.getId(), clone); + if (prev != null) { + byNamespaceAndName.remove(prev.getNamespaceAndName()); + } + byNamespaceAndName.put(clone.getNamespaceAndName(), clone); + + writePathDatabase(); + } + + Path repositoryPath = context.resolve(getPath(repository.getId())); + Path metadataPath = createMetadataPath(repositoryPath); + metadataStore.write(metadataPath, clone); } @Override public void delete(Repository repository) { - Path directory = getPath(repository); - super.delete(repository); + Path path; + synchronized (this) { + Repository prev = byId.remove(repository.getId()); + if (prev != null) { + byNamespaceAndName.remove(prev.getNamespaceAndName()); + } + + path = pathById.remove(repository.getId()); + + writePathDatabase(); + } + + path = context.resolve(path); + try { - fileSystem.destroy(directory.toFile()); + fileSystem.destroy(path.toFile()); } catch (IOException e) { - throw new InternalRepositoryException(repository, "could not delete repository directory", e); + throw new InternalRepositoryException(repository, "failed to destroy filesystem", e); } } - /** - * Method description - * - * @return - */ @Override - protected XmlRepositoryDatabase createNewDatabase() { - return new XmlRepositoryDatabase(); - } - - @Override - public Path getPath(Repository repository) { - return context - .getBaseDirectory() - .toPath() - .resolve( - findExistingRepositoryPath(repository) - .map(RepositoryPath::getPath) - .orElseThrow(() -> new InternalRepositoryException(repository, "could not find base directory for repository"))); - } - - private Optional findExistingRepositoryPath(Repository repository) { - return db.values().stream() - .filter(repoPath -> repoPath.getId().equals(repository.getId())) - .findAny(); + public Path getPath(String repositoryId) { + return pathById.get(repositoryId); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDatabase.java deleted file mode 100644 index c7b2af656f..0000000000 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDatabase.java +++ /dev/null @@ -1,188 +0,0 @@ -/** - * 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.xml; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.Repository; -import sonia.scm.xml.XmlDatabase; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -//~--- JDK imports ------------------------------------------------------------ - -@XmlRootElement(name = "repository-db") -@XmlAccessorType(XmlAccessType.FIELD) -public class XmlRepositoryDatabase implements XmlDatabase { - - private Long creationTime; - - private Long lastModified; - - @XmlJavaTypeAdapter(XmlRepositoryMapAdapter.class) - @XmlElement(name = "repositories") - private Map repositoryPathMap = new LinkedHashMap<>(); - - - public XmlRepositoryDatabase() { - long c = System.currentTimeMillis(); - creationTime = c; - lastModified = c; - } - - static String createKey(NamespaceAndName namespaceAndName) - { - return namespaceAndName.getNamespace() + ":" + namespaceAndName.getName(); - } - - static String createKey(Repository repository) - { - return createKey(repository.getNamespaceAndName()); - } - - @Override - public void add(RepositoryPath repositoryPath) - { - repositoryPathMap.put(createKey(repositoryPath.getRepository()), repositoryPath); - } - - public boolean contains(NamespaceAndName namespaceAndName) - { - return repositoryPathMap.containsKey(createKey(namespaceAndName)); - } - - @Override - public boolean contains(String id) - { - return get(id) != null; - } - - @Override - public RepositoryPath remove(String id) - { - return repositoryPathMap.remove(createKey(get(id).getRepository())); - } - - public Collection getRepositories() { - List repositories = new ArrayList<>(); - for (RepositoryPath repositoryPath : repositoryPathMap.values()) { - Repository repository = repositoryPath.getRepository(); - repositories.add(repository); - } - return repositories; - } - - @Override - public Collection values() - { - return repositoryPathMap.values(); - } - - public Repository get(NamespaceAndName namespaceAndName) { - RepositoryPath repositoryPath = repositoryPathMap.get(createKey(namespaceAndName)); - if (repositoryPath != null) { - return repositoryPath.getRepository(); - } - return null; - } - - - @Override - public RepositoryPath get(String id) { - return values().stream() - .filter(repoPath -> repoPath.getId().equals(id)) - .findFirst() - .orElse(null); - } - - /** - * Method description - * - * - * @return - */ - @Override - public long getCreationTime() - { - return creationTime; - } - - /** - * Method description - * - * - * @return - */ - @Override - public long getLastModified() - { - return lastModified; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param creationTime - */ - @Override - public void setCreationTime(long creationTime) - { - this.creationTime = creationTime; - } - - /** - * Method description - * - * - * @param lastModified - */ - @Override - public void setLastModified(long lastModified) - { - this.lastModified = lastModified; - } -} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryList.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryList.java deleted file mode 100644 index b31f870a8e..0000000000 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryList.java +++ /dev/null @@ -1,123 +0,0 @@ -/** - * 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.xml; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Map; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "repositories") -@XmlAccessorType(XmlAccessType.FIELD) -public class XmlRepositoryList implements Iterable -{ - - /** - * Constructs ... - * - */ - public XmlRepositoryList() {} - - /** - * Constructs ... - * - * - * - * @param repositoryMap - */ - public XmlRepositoryList(Map repositoryMap) - { - this.repositories = new LinkedList<>(repositoryMap.values()); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public Iterator iterator() - { - return repositories.iterator(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public LinkedList getRepositoryPaths() - { - return repositories; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param repositories - */ - public void setRepositories(LinkedList repositories) - { - this.repositories = repositories; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @XmlElement(name = "repository-path") - private LinkedList repositories; -} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryMapAdapter.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryMapAdapter.java deleted file mode 100644 index 633c9a27b3..0000000000 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryMapAdapter.java +++ /dev/null @@ -1,112 +0,0 @@ -/** - * 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.xml; - -import sonia.scm.SCMContext; -import sonia.scm.SCMContextProvider; -import sonia.scm.repository.InternalRepositoryException; -import sonia.scm.repository.Repository; -import sonia.scm.store.StoreConstants; -import sonia.scm.store.StoreException; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Marshaller; -import javax.xml.bind.Unmarshaller; -import javax.xml.bind.annotation.adapters.XmlAdapter; -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.LinkedHashMap; -import java.util.Map; - - -/** - * @author Sebastian Sdorra - */ -public class XmlRepositoryMapAdapter extends XmlAdapter> { - - @Override - public XmlRepositoryList marshal(Map repositoryMap) { - XmlRepositoryList repositoryPaths = new XmlRepositoryList(repositoryMap); - try { - JAXBContext context = JAXBContext.newInstance(Repository.class); - Marshaller marshaller = context.createMarshaller(); - - marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - - // marshall the repo_path/metadata.xml files - for (RepositoryPath repositoryPath : repositoryPaths.getRepositoryPaths()) { - if (repositoryPath.toBeSynchronized()) { - - File baseDirectory = SCMContext.getContext().getBaseDirectory(); - Path dir = baseDirectory.toPath().resolve(repositoryPath.getPath()); - - if (!Files.isDirectory(dir)) { - throw new InternalRepositoryException(repositoryPath.getRepository(), "repository path not found"); - } - marshaller.marshal(repositoryPath.getRepository(), getRepositoryMetadataFile(dir.toFile())); - repositoryPath.setToBeSynchronized(false); - } - } - } catch (JAXBException ex) { - throw new StoreException("failed to marshal repository database", ex); - } - - return repositoryPaths; - - } - - private File getRepositoryMetadataFile(File dir) { - return new File(dir, StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION)); - } - - @Override - public Map unmarshal(XmlRepositoryList repositoryPaths) { - Map repositoryPathMap = new LinkedHashMap<>(); - try { - JAXBContext context = JAXBContext.newInstance(Repository.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - for (RepositoryPath repositoryPath : repositoryPaths) { - SCMContextProvider contextProvider = SCMContext.getContext(); - File baseDirectory = contextProvider.getBaseDirectory(); - Repository repository = (Repository) unmarshaller.unmarshal(getRepositoryMetadataFile(baseDirectory.toPath().resolve(repositoryPath.getPath()).toFile())); - - repositoryPath.setRepository(repository); - repositoryPathMap.put(XmlRepositoryDatabase.createKey(repository), repositoryPath); - } - } catch (JAXBException ex) { - throw new StoreException("failed to unmarshal object", ex); - } - return repositoryPathMap; - } -} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java index 6a9098b545..40cf03c8a8 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java @@ -35,32 +35,14 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.base.Charsets; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Maps; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.security.KeyGenerator; import sonia.scm.xml.IndentXMLStreamWriter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Map.Entry; +import sonia.scm.xml.XmlStreams; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; @@ -68,11 +50,14 @@ import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.namespace.QName; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; -import javax.xml.stream.XMLStreamWriter; +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -255,74 +240,6 @@ public class JAXBConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore store; - @Mock - private XmlRepositoryDatabase db; @Mock private SCMContextProvider context; - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Mock + private InitialRepositoryLocationResolver locationResolver; - private final FileSystem fileSystem = new DefaultFileSystem(); + private FileSystem fileSystem = new DefaultFileSystem(); - @Before - public void init() throws IOException { - StoreParameters storeParameters = new StoreParameters().withType(XmlRepositoryDatabase.class).withName(STORE_NAME).build(); - when(storeFactory.getStore(storeParameters)).thenReturn(store); - when(store.get()).thenReturn(db); - when(context.getBaseDirectory()).thenReturn(temporaryFolder.newFolder()); + private XmlRepositoryDAO dao; + + private Path baseDirectory; + + private AtomicLong atomicClock; + + @BeforeEach + void createDAO(@TempDirectory.TempDir Path baseDirectory) { + this.baseDirectory = baseDirectory; + this.atomicClock = new AtomicLong(); + + when(locationResolver.getPath("42")).thenReturn(Paths.get("repos", "42")); + when(locationResolver.getPath("42+1")).thenReturn(Paths.get("repos", "puzzle")); + + when(context.getBaseDirectory()).thenReturn(baseDirectory.toFile()); + when(context.resolve(any(Path.class))).then(ic -> { + Path path = ic.getArgument(0); + return baseDirectory.resolve(path); + }); + + dao = createDAO(); + } + + private XmlRepositoryDAO createDAO() { + XmlRepositoryDAO dao = new XmlRepositoryDAO(context, locationResolver, fileSystem); + + Clock clock = mock(Clock.class); + when(clock.millis()).then(ic -> atomicClock.incrementAndGet()); + dao.clock = clock; + + return dao; } @Test - public void addShouldCreateNewRepositoryPathWithRelativePath() { - InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(context); - XmlRepositoryDAO dao = new XmlRepositoryDAO(initialRepositoryLocationResolver, fileSystem, context); - - dao.add(new Repository("id", "git", "namespace", "name")); - - verify(db).add(argThat(repositoryPath -> { - assertThat(repositoryPath.getId()).isEqualTo("id"); - assertThat(repositoryPath.getPath()).isEqualTo(InitialRepositoryLocationResolver.DEFAULT_REPOSITORY_PATH + "/id"); - return true; - })); - verify(store).set(db); + void shouldReturnXmlType() { + assertThat(dao.getType()).isEqualTo("xml"); } @Test - public void modifyShouldStoreChangedRepository() { - Repository oldRepository = new Repository("id", "old", null, null); - RepositoryPath repositoryPath = new RepositoryPath("/path", "id", oldRepository); - when(db.values()).thenReturn(asList(repositoryPath)); - - XmlRepositoryDAO dao = new XmlRepositoryDAO(new InitialRepositoryLocationResolver(context), fileSystem, context); - - Repository newRepository = new Repository("id", "new", null, null); - dao.modify(newRepository); - - assertThat(repositoryPath.getRepository()).isSameAs(newRepository); - verify(store).set(db); + void shouldReturnCreationTimeAfterCreation() { + long now = System.currentTimeMillis(); + assertThat(dao.getCreationTime()).isBetween(now - 200, now + 200); } @Test - public void shouldGetPathInBaseDirectoryForRelativePath() { - Repository existingRepository = new Repository("id", "old", null, null); - RepositoryPath repositoryPath = new RepositoryPath("path", "id", existingRepository); - when(db.values()).thenReturn(asList(repositoryPath)); - - XmlRepositoryDAO dao = new XmlRepositoryDAO(new InitialRepositoryLocationResolver(context), fileSystem, context); - - Path path = dao.getPath(existingRepository); - - assertThat(path.toString()).isEqualTo(context.getBaseDirectory().getPath() + "/path"); + void shouldNotReturnLastModifiedAfterCreation() { + assertThat(dao.getLastModified()).isNull(); } @Test - public void shouldGetPathInBaseDirectoryForAbsolutePath() { - Repository existingRepository = new Repository("id", "old", null, null); - RepositoryPath repositoryPath = new RepositoryPath("/tmp/path", "id", existingRepository); - when(db.values()).thenReturn(asList(repositoryPath)); + void shouldReturnTrueForEachContainsMethod() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); - XmlRepositoryDAO dao = new XmlRepositoryDAO(new InitialRepositoryLocationResolver(context), fileSystem, context); + assertThat(dao.contains(heartOfGold)).isTrue(); + assertThat(dao.contains(heartOfGold.getId())).isTrue(); + assertThat(dao.contains(heartOfGold.getNamespaceAndName())).isTrue(); + } - Path path = dao.getPath(existingRepository); + private Repository createHeartOfGold() { + Repository heartOfGold = RepositoryTestData.createHeartOfGold(); + heartOfGold.setId("42"); + return heartOfGold; + } - assertThat(path.toString()).isEqualTo("/tmp/path"); + @Test + void shouldReturnFalseForEachContainsMethod() { + Repository heartOfGold = createHeartOfGold(); + + assertThat(dao.contains(heartOfGold)).isFalse(); + assertThat(dao.contains(heartOfGold.getId())).isFalse(); + assertThat(dao.contains(heartOfGold.getNamespaceAndName())).isFalse(); + } + + @Test + void shouldReturnNullForEachGetMethod() { + assertThat(dao.get("42")).isNull(); + assertThat(dao.get(new NamespaceAndName("hitchhiker","HeartOfGold"))).isNull(); + } + + @Test + void shouldReturnRepository() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + assertThat(dao.get("42")).isEqualTo(heartOfGold); + assertThat(dao.get(new NamespaceAndName("hitchhiker","HeartOfGold"))).isEqualTo(heartOfGold); + } + + @Test + void shouldNotReturnTheSameInstance() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + Repository repository = dao.get("42"); + assertThat(repository).isNotSameAs(heartOfGold); + } + + @Test + void shouldReturnAllRepositories() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + Repository puzzle = createPuzzle(); + dao.add(puzzle); + + Collection repositories = dao.getAll(); + assertThat(repositories).containsExactlyInAnyOrder(heartOfGold, puzzle); + } + + private Repository createPuzzle() { + Repository puzzle = RepositoryTestData.create42Puzzle(); + puzzle.setId("42+1"); + return puzzle; + } + + @Test + void shouldModifyRepository() { + Repository heartOfGold = createHeartOfGold(); + heartOfGold.setDescription("HeartOfGold"); + dao.add(heartOfGold); + assertThat(dao.get("42").getDescription()).isEqualTo("HeartOfGold"); + + heartOfGold = createHeartOfGold(); + heartOfGold.setDescription("Heart of Gold"); + dao.modify(heartOfGold); + + assertThat(dao.get("42").getDescription()).isEqualTo("Heart of Gold"); + } + + @Test + void shouldRemoveRepository() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + assertThat(dao.contains("42")).isTrue(); + + dao.delete(heartOfGold); + assertThat(dao.contains("42")).isFalse(); + assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); + } + + @Test + void shouldUpdateLastModifiedAfterEachWriteOperation() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + Long firstLastModified = dao.getLastModified(); + assertThat(firstLastModified).isNotNull(); + + Repository puzzle = createPuzzle(); + dao.add(puzzle); + + Long lastModifiedAdded = dao.getLastModified(); + assertThat(lastModifiedAdded).isGreaterThan(firstLastModified); + + heartOfGold.setDescription("Heart of Gold"); + dao.modify(heartOfGold); + + Long lastModifiedModified = dao.getLastModified(); + assertThat(lastModifiedModified).isGreaterThan(lastModifiedAdded); + + dao.delete(puzzle); + + Long lastModifiedRemoved = dao.getLastModified(); + assertThat(lastModifiedRemoved).isGreaterThan(lastModifiedModified); + } + + @Test + void shouldRenameTheRepository() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + heartOfGold.setNamespace("hg2tg"); + heartOfGold.setName("hog"); + + dao.modify(heartOfGold); + + Repository repository = dao.get("42"); + assertThat(repository.getNamespace()).isEqualTo("hg2tg"); + assertThat(repository.getName()).isEqualTo("hog"); + + assertThat(dao.contains(new NamespaceAndName("hg2tg", "hog"))).isTrue(); + assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); + } + + @Test + void shouldDeleteRepositoryEvenWithChangedNamespace() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + heartOfGold.setNamespace("hg2tg"); + heartOfGold.setName("hog"); + + dao.delete(heartOfGold); + + assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); + } + + @Test + void shouldReturnThePathForTheRepository() { + Path repositoryPath = Paths.get("r", "42"); + when(locationResolver.getPath("42")).thenReturn(repositoryPath); + + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + Path path = dao.getPath("42"); + assertThat(path).isEqualTo(repositoryPath); + } + + @Test + void shouldCreateTheDirectoryForTheRepository() { + Path repositoryPath = Paths.get("r", "42"); + when(locationResolver.getPath("42")).thenReturn(repositoryPath); + + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + Path path = getAbsolutePathFromDao("42"); + assertThat(path).isDirectory(); + } + + @Test + void shouldRemoveRepositoryDirectoryAfterDeletion() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + Path path = getAbsolutePathFromDao(heartOfGold.getId()); + assertThat(path).isDirectory(); + + dao.delete(heartOfGold); + assertThat(path).doesNotExist(); + } + + private Path getAbsolutePathFromDao(String id) { + return context.resolve(dao.getPath(id)); + } + + @Test + void shouldCreateRepositoryPathDatabase() throws IOException { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + Path storePath = dao.createStorePath(); + assertThat(storePath).isRegularFile(); + + String content = content(storePath); + + assertThat(content).contains(heartOfGold.getId()); + assertThat(content).contains(dao.getPath(heartOfGold.getId()).toString()); + } + + private String content(Path storePath) throws IOException { + return new String(Files.readAllBytes(storePath), Charsets.UTF_8); + } + + @Test + void shouldStoreRepositoryMetadataAfterAdd() throws IOException { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId()); + Path metadataPath = dao.createMetadataPath(repositoryDirectory); + + assertThat(metadataPath).isRegularFile(); + + String content = content(metadataPath); + assertThat(content).contains(heartOfGold.getName()); + assertThat(content).contains(heartOfGold.getNamespace()); + assertThat(content).contains(heartOfGold.getDescription()); + } + + @Test + void shouldUpdateRepositoryMetadataAfterModify() throws IOException { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + heartOfGold.setDescription("Awesome Spaceship"); + dao.modify(heartOfGold); + + Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId()); + Path metadataPath = dao.createMetadataPath(repositoryDirectory); + + String content = content(metadataPath); + assertThat(content).contains("Awesome Spaceship"); + } + + @Test + void shouldReadPathDatabaseAndMetadataOfRepositories() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + // reload data + dao = createDAO(); + + heartOfGold = dao.get("42"); + assertThat(heartOfGold.getName()).isEqualTo("HeartOfGold"); + + Path path = getAbsolutePathFromDao(heartOfGold.getId()); + assertThat(path).isDirectory(); + } + + @Test + void shouldReadCreationTimeAndLastModifedDateFromDatabase() { + Repository heartOfGold = createHeartOfGold(); + dao.add(heartOfGold); + + Long creationTime = dao.getCreationTime(); + Long lastModified = dao.getLastModified(); + + // reload data + dao = createDAO(); + + assertThat(dao.getCreationTime()).isEqualTo(creationTime); + assertThat(dao.getLastModified()).isEqualTo(lastModified); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java index 248eae92d1..9a30cc9f3e 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java @@ -121,7 +121,7 @@ public class GitGcTask implements Runnable { } private void gc(Repository repository){ - File file = repositoryHandler.getDirectory(repository); + File file = repositoryHandler.getDirectory(repository.getId()); Git git = null; try { git = open(file); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java index 3cf72166ea..348203af92 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java @@ -119,7 +119,7 @@ public abstract class AbstractGitIncomingOutgoingCommand Git git = Git.wrap(open()); - GitUtil.fetch(git, handler.getDirectory(remoteRepository), remoteRepository); + GitUtil.fetch(git, handler.getDirectory(remoteRepository.getId()), remoteRepository); ObjectId localId = getDefaultBranch(git.getRepository()); ObjectId remoteId = null; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java index e4e37d6fed..a9b9e25aca 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java @@ -196,7 +196,7 @@ public abstract class AbstractGitPushOrPullCommand extends AbstractGitCommand */ protected String getRemoteUrl(sonia.scm.repository.Repository repository) { - return getRemoteUrl(handler.getDirectory(repository)); + return getRemoteUrl(handler.getDirectory(repository.getId())); } //~--- methods -------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java index 7a829355bf..8810c15c58 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java @@ -196,12 +196,12 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand private PullResponse pullFromScmRepository(Repository sourceRepository) throws IOException { - File sourceDirectory = handler.getDirectory(sourceRepository); + File sourceDirectory = handler.getDirectory(sourceRepository.getId()); Preconditions.checkArgument(sourceDirectory.exists(), "source repository directory does not exists"); - File targetDirectory = handler.getDirectory(repository); + File targetDirectory = handler.getDirectory(repository.getId()); Preconditions.checkArgument(sourceDirectory.exists(), "target repository directory does not exists"); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index bda0d87b21..ae1af333bc 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -73,7 +73,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository) { this.handler = handler; this.repository = repository; - this.context = new GitContext(handler.getDirectory(repository), repository); + this.context = new GitContext(handler.getDirectory(repository.getId()), repository); } //~--- methods -------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java index eb57ac8ff8..a2114a1b6a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java @@ -106,7 +106,7 @@ public class GitRepositoryResolver implements RepositoryResolver provider; - private RepositoryLocationResolver repositoryLocationResolver; - @Override protected void checkDirectory(File directory) { File hgDirectory = new File(directory, ".hg"); @@ -70,11 +68,8 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { } @Override - protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, - File directory) { - repositoryLocationResolver = new RepositoryLocationResolver(repoDao, new InitialRepositoryLocationResolver(contextProvider)); - HgRepositoryHandler handler = new HgRepositoryHandler(factory, - new HgContextProvider(), repositoryLocationResolver); + protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) { + HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver); handler.init(contextProvider); HgTestUtil.checkForSkip(handler); @@ -84,8 +79,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Test public void getDirectory() { - HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, - provider, repositoryLocationResolver); + HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver); HgConfig hgConfig = new HgConfig(); hgConfig.setHgBinary("hg"); @@ -93,7 +87,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { repositoryHandler.setConfig(hgConfig); initRepository(); - File path = repositoryHandler.getDirectory(repository); + File path = repositoryHandler.getDirectory(repository.getId()); assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath()); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java index d2344816ef..68f7e18a76 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java @@ -103,7 +103,7 @@ public final class HgTestUtil PathBasedRepositoryDAO repoDao = mock(PathBasedRepositoryDAO.class); - RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(repoDao, new InitialRepositoryLocationResolver(context)); + RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(context, repoDao, new InitialRepositoryLocationResolver()); HgRepositoryHandler handler = new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver); Path repoDir = directory.toPath(); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/TempSCMContextProvider.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/TempSCMContextProvider.java index bc6794e5a5..0a0064ad44 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/TempSCMContextProvider.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/TempSCMContextProvider.java @@ -41,6 +41,7 @@ import sonia.scm.Stage; import java.io.File; import java.io.IOException; +import java.nio.file.Path; /** * @@ -136,6 +137,11 @@ public class TempSCMContextProvider implements SCMContextProvider this.baseDirectory = baseDirectory; } + @Override + public Path resolve(Path path) { + return baseDirectory.toPath().resolve(path); + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java index 163a236d77..6c654b0e54 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java @@ -94,9 +94,9 @@ public abstract class IncomingOutgoingTestBase extends AbstractTestBase outgoing = Repository.create(createConfig(temp), outgoingDirectory); handler = mock(HgRepositoryHandler.class); - when(handler.getDirectory(incomingRepository)).thenReturn( + when(handler.getDirectory(incomingRepository.getId())).thenReturn( incomingDirectory); - when(handler.getDirectory(outgoingRepository)).thenReturn( + when(handler.getDirectory(outgoingRepository.getId())).thenReturn( outgoingDirectory); when(handler.getConfig()).thenReturn(temp.getConfig()); when(handler.getHgContext()).thenReturn(new HgContext()); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java index 5c1a076ddc..ff277947bd 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java @@ -63,7 +63,7 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider Repository repository) { this.repository = repository; - this.context = new SvnContext(handler.getDirectory(repository)); + this.context = new SvnContext(handler.getDirectory(repository.getId())); } //~--- methods -------------------------------------------------------------- diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVConfig.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVConfig.java index b220737ecb..a544e8051b 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVConfig.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVConfig.java @@ -292,7 +292,7 @@ public class SvnDAVConfig extends DAVConfig if (repository != null) { - directory = handler.getDirectory(repository); + directory = handler.getDirectory(repository.getId()); } return directory; diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java index 432bc486d8..04e8cf5c06 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java @@ -67,15 +67,10 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Mock private com.google.inject.Provider repositoryManagerProvider; - @Mock - private RepositoryDAO repositoryDAO; - private HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); private HookEventFacade facade = new HookEventFacade(repositoryManagerProvider, hookContextFactory); - private RepositoryLocationResolver repositoryLocationResolver; - @Override protected void checkDirectory(File directory) { File format = new File(directory, "format"); @@ -91,9 +86,9 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Override protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, + RepositoryLocationResolver locationResolver, File directory) { - repositoryLocationResolver = new RepositoryLocationResolver(repoDao, new InitialRepositoryLocationResolver(contextProvider)); - SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, null, repositoryLocationResolver); + SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, null, locationResolver); handler.init(contextProvider); @@ -109,13 +104,13 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { public void getDirectory() { when(factory.getStore(any())).thenReturn(store); SvnRepositoryHandler repositoryHandler = new SvnRepositoryHandler(factory, - facade, repositoryLocationResolver); + facade, locationResolver); SvnConfig svnConfig = new SvnConfig(); repositoryHandler.setConfig(svnConfig); initRepository(); - File path = repositoryHandler.getDirectory(repository); + File path = repositoryHandler.getDirectory(repository.getId()); assertEquals(repoPath.toString()+File.separator+ AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath()); } } diff --git a/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java b/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java index b81c39ca00..5dbd672b98 100644 --- a/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java +++ b/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java @@ -45,6 +45,7 @@ public final class RepositoryTestData { .type(type) .contact("douglas.adams@hitchhiker.com") .name("42Puzzle") + .namespace("hitchhiker") .description("The 42 Puzzle") .build(); } @@ -59,6 +60,7 @@ public final class RepositoryTestData { .type(type) .contact("zaphod.beeblebrox@hitchhiker.com") .name("happyVerticalPeopleTransporter") + .namespace("hitchhiker") .description("Happy Vertical People Transporter") .build(); } @@ -72,6 +74,7 @@ public final class RepositoryTestData { .type(type) .contact("zaphod.beeblebrox@hitchhiker.com") .name("HeartOfGold") + .namespace("hitchhiker") .description( "Heart of Gold is the first prototype ship to successfully utilise the revolutionary Infinite Improbability Drive") .build(); @@ -87,6 +90,7 @@ public final class RepositoryTestData { .type(type) .contact("douglas.adams@hitchhiker.com") .name("RestaurantAtTheEndOfTheUniverse") + .namespace("hitchhiker") .description("The Restaurant at the End of the Universe") .build(); } diff --git a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java index 97e562130e..f48744d460 100644 --- a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java @@ -44,6 +44,7 @@ import java.io.IOException; import java.nio.file.Path; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -62,7 +63,7 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { protected abstract void checkDirectory(File directory); protected abstract RepositoryHandler createRepositoryHandler( - ConfigurationStoreFactory factory, File directory) throws IOException, RepositoryPathNotFoundException; + ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) throws IOException, RepositoryPathNotFoundException; @Test public void testCreate() { @@ -74,7 +75,15 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { InMemoryConfigurationStoreFactory storeFactory = new InMemoryConfigurationStoreFactory(); baseDirectory = new File(contextProvider.getBaseDirectory(), "repositories"); IOUtil.mkdirs(baseDirectory); - handler = createRepositoryHandler(storeFactory, baseDirectory); + + locationResolver = mock(RepositoryLocationResolver.class); + + when(locationResolver.getPath(anyString())).then(ic -> { + String id = ic.getArgument(0); + return baseDirectory.toPath().resolve(id); + }); + + handler = createRepositoryHandler(storeFactory, locationResolver, baseDirectory); } @Override @@ -98,11 +107,12 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { repository = RepositoryTestData.createHeartOfGold(); File repoDirectory = new File(baseDirectory, repository.getId()); repoPath = repoDirectory.toPath(); - when(repoDao.getPath(repository)).thenReturn(repoPath); + when(repoDao.getPath(repository.getId())).thenReturn(repoPath); return new File(repoDirectory, AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY); } protected File baseDirectory; + protected RepositoryLocationResolver locationResolver; private RepositoryHandler handler; } diff --git a/scm-test/src/main/java/sonia/scm/util/MockUtil.java b/scm-test/src/main/java/sonia/scm/util/MockUtil.java index 756b2632be..76bf4ae24d 100644 --- a/scm-test/src/main/java/sonia/scm/util/MockUtil.java +++ b/scm-test/src/main/java/sonia/scm/util/MockUtil.java @@ -55,6 +55,7 @@ import static org.mockito.Mockito.*; import java.io.File; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; @@ -213,6 +214,10 @@ public final class MockUtil SCMContextProvider provider = mock(SCMContextProvider.class); when(provider.getBaseDirectory()).thenReturn(directory); + when(provider.resolve(any(Path.class))).then(ic -> { + Path p = ic.getArgument(0); + return directory.toPath().resolve(p); + }); return provider; } diff --git a/scm-ui/package.json b/scm-ui/package.json index c4b7cb3983..d80ee6571e 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -5,6 +5,7 @@ "private": true, "main": "src/index.js", "dependencies": { + "@babel/polyfill": "^7.0.0", "@fortawesome/fontawesome-free": "^5.3.1", "@scm-manager/ui-extensions": "^0.1.1", "bulma": "^0.7.1", @@ -31,17 +32,19 @@ "redux": "^4.0.0", "redux-devtools-extension": "^2.13.5", "redux-logger": "^3.0.6", - "redux-thunk": "^2.3.0" + "redux-thunk": "^2.3.0", + "whatwg-fetch": "^3.0.0" }, "scripts": { + "polyfills": "concat node_modules/@babel/polyfill/dist/polyfill.min.js node_modules/whatwg-fetch/dist/fetch.umd.js -o target/scm-ui/polyfills.bundle.js", "webfonts": "copyfiles -f node_modules/@fortawesome/fontawesome-free/webfonts/* target/scm-ui/styles/webfonts", "build-css": "node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/scm-ui/styles", "watch-css": "npm run build-css && node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/scm-ui/styles --watch --recursive", "start-js": "ui-bundler serve --target target/scm-ui --vendor vendor.bundle.js", - "start": "npm-run-all -p webfonts watch-css start-js", + "start": "npm-run-all -p webfonts watch-css polyfills start-js", "build-js": "ui-bundler bundle --mode=production target/scm-ui/scm-ui.bundle.js", "build-vendor": "ui-bundler vendor --mode=production target/scm-ui/vendor.bundle.js", - "build": "npm-run-all -s webfonts build-css build-vendor build-js", + "build": "npm-run-all -s webfonts build-css polyfills build-vendor build-js", "test": "ui-bundler test", "test-ci": "ui-bundler test --ci", "flow": "flow", @@ -49,6 +52,7 @@ }, "devDependencies": { "@scm-manager/ui-bundler": "^0.0.21", + "concat": "^1.0.3", "copyfiles": "^2.0.0", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", diff --git a/scm-ui/public/index.mustache b/scm-ui/public/index.mustache index 62a40d8e93..590b5e3cdb 100644 --- a/scm-ui/public/index.mustache +++ b/scm-ui/public/index.mustache @@ -34,6 +34,7 @@ + diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index 1a9f1d62e7..890ab595d0 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -2,7 +2,7 @@ import React from "react"; import { connect } from "react-redux"; import { withRouter } from "react-router-dom"; -import type { Repository, Branch } from "@scm-manager/ui-types"; +import type { Branch, Repository } from "@scm-manager/ui-types"; import FileTree from "../components/FileTree"; import { ErrorNotification, Loading } from "@scm-manager/ui-components"; import BranchSelector from "../../containers/BranchSelector"; @@ -109,9 +109,9 @@ class Sources extends React.Component { } renderBranchSelector = () => { - const { repository, branches, revision } = this.props; + const { branches, revision } = this.props; - if (repository._links.branches) { + if (branches) { return ( headers; + + private CacheControlResponseFilter filter = new CacheControlResponseFilter(); + + @Before + public void setUpMocks() { + when(responseContext.getHeaders()).thenReturn(headers); + } + + @Test + public void filterShouldAddCacheControlHeader() { + filter.filter(requestContext, responseContext); + + verify(headers).add("Cache-Control", "no-cache"); + } + + @Test + public void filterShouldNotSetHeaderIfLastModifiedIsNotNull() { + when(responseContext.getLastModified()).thenReturn(new Date()); + + filter.filter(requestContext, responseContext); + + verify(headers, never()).add("Cache-Control", "no-cache"); + } + + @Test + public void filterShouldNotSetHeaderIfEtagIsNotNull() { + when(responseContext.getEntityTag()).thenReturn(new EntityTag("42")); + + filter.filter(requestContext, responseContext); + + verify(headers, never()).add("Cache-Control", "no-cache"); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index 9c0aa00fe7..141a7f8527 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -65,7 +65,6 @@ import sonia.scm.security.KeyGenerator; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; -import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -434,10 +433,10 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { private DefaultRepositoryManager createRepositoryManager(boolean archiveEnabled, KeyGenerator keyGenerator) { DefaultFileSystem fileSystem = new DefaultFileSystem(); Set handlerSet = new HashSet<>(); - InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(contextProvider); - XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(initialRepositoryLocationResolver, fileSystem, contextProvider); - RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(repositoryDAO, initialRepositoryLocationResolver); - ConfigurationStoreFactory factory = new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver); + ConfigurationStoreFactory factory = new JAXBConfigurationStoreFactory(contextProvider); + InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(); + XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(contextProvider, initialRepositoryLocationResolver, fileSystem); + RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(contextProvider, repositoryDAO, initialRepositoryLocationResolver); handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver)); handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver) { @Override