merge with develop

This commit is contained in:
Sebastian Sdorra
2020-07-02 10:24:04 +02:00
55 changed files with 1382 additions and 971 deletions

View File

@@ -7,8 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Rename repository name (and namespace if permitted) ([#1218](https://github.com/scm-manager/scm-manager/pull/1218))
- enrich commit mentions in markdown viewer by internal links ([#1210](https://github.com/scm-manager/scm-manager/pull/1210))
- New extension point `changeset.description.tokens` to "enrich" commit messages ([#1231](https://github.com/scm-manager/scm-manager/pull/1231))
- restart service after rpm or deb package upgrade
### Changed
- Checkboxes can now be 'indeterminate' ([#1215](https://github.com/scm-manager/scm-manager/pull/1215))
@@ -16,6 +18,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed installation of debian packages on distros without preinstalled `at` ([#1216](https://github.com/scm-manager/scm-manager/issues/1216) and [#1217](https://github.com/scm-manager/scm-manager/pull/1217))
- Fixed restart with deb or rpm installation ([#1222](https://github.com/scm-manager/scm-manager/issues/1222) and [#1227](https://github.com/scm-manager/scm-manager/pull/1227))
- Fixed broken migration with empty security.xml ([#1219](https://github.com/scm-manager/scm-manager/issues/1219) and [#1221](https://github.com/scm-manager/scm-manager/pull/1221))
- Added missing architecture to debian installation documentation ([#1230](https://github.com/scm-manager/scm-manager/pull/1230))
- Fixed wrong package information for deb and rpm packages ([#1229](https://github.com/scm-manager/scm-manager/pull/1229))
## [2.1.1] - 2020-06-23
### Fixed

View File

@@ -5,12 +5,12 @@
</p>
The easiest way to share and manage your Git, Mercurial and Subversion
repositories over http.
repositories.
- Very easy installation
- No need to hack configuration files, SCM-Manager is completely
configureable from its Web-Interface
- No Apache and no database installation is required
configurable from its Web-Interface
- No Apache and no database installation required
- Central user, group and permission management
- Out of the box support for Git, Mercurial and Subversion
- Full RESTFul Web Service API (JSON and XML)
@@ -19,34 +19,32 @@ repositories over http.
- Useful plugins available
- Licensed under the MIT-License
This branch (default) is for the development of SCM-Manager 2.x. If you are interested in the development of version 1.x, please checkout the 1.x branch.
This branch (`develop`) is for the development of SCM-Manager 2.x. If you are interested in the development of version
1.x, please checkout the branch `support/1.x`.
## News
- **2018-09-25** - [SCM-Manager 2 gets a boost by Cloudogu
GmbH](https://www.scm-manager.org/scm-manager-2/scm-manager-2-gets-a-boost-by-cloudogu-gmbh/)
- **2018-05-04** - SCM-Manager 1.60 released
([download](http://www.scm-manager.org/download/) \|
[release notes](release-notes.md))
- **2018-04-11** - SCM-Manager 1.59 released
[All news](http://www.scm-manager.org/news/)
All news regarding SCM-Manager will be published in our [blog](https://www.scm-manager.org/blog/).
## Mailing List
- <scmmanager@googlegroups.com> -
[archive](http://groups.google.com/group/scmmanager) \|
[subscribe](mailto:scmmanager+subscribe@googlegroups.com)
\|
[subscribe](mailto:scmmanager+subscribe@googlegroups.com) \|
[unsubscribe](mailto:scmmanager+unsubscribe@googlegroups.com)
## Documentation
You can find the complete documentation in the [docs/](docs/Home.md) directory.
You can find the complete documentation on our [homepage](https://www.scm-manager.org/docs/).
## Need help?
Looking for more guidance? Full documentation lives [in the SCM-Manager repository](https://github.com/scm-manager/scm-manager/blob/develop/docs/Home.md). Do you have further ideas or need support?
Looking for more guidance? Full documentation lives on our [homepage](https://www.scm-manager.org/docs/) or the
dedicated pages for our [plugins](https://www.scm-manager.org/plugins/). Do you have further ideas or need support?
- **Community Support** - Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels. [Find more about this here](https://www.scm-manager.org/support/).
- **Community Support** - Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to
request features through the official channels. [Find more about this here](https://www.scm-manager.org/support/).
- **Enterprise Support** - Do you require support with the integration of SCM-Manager into your processes, with the customization of the tool or simply a service level agreement (SLA)? **Contact our development partner Cloudogu! Their team is looking forward to discussing your individual requirements with you and will be more than happy to give you a quote.** [Request Enterprise Support](https://cloudogu.com/en/scm-manager-enterprise/).
- **Enterprise Support** - Do you require support with the integration of SCM-Manager into your processes, with the
customization of the tool or simply a service level agreement (SLA)? **Contact our development partner Cloudogu!
Their team is looking forward to discussing your individual requirements with you and will be more than happy to
give you a quote.** [Request Enterprise Support](https://cloudogu.com/en/scm-manager-enterprise/).

View File

@@ -9,7 +9,7 @@ displayToc: true
The following code block will configure an apt repository for scm-manager and install it.
```bash
echo 'deb https://packages.scm-manager.org/repository/apt-v2-releases/ stable main' | sudo tee /etc/apt/sources.list.d/scm-manager.list
echo 'deb [arch=all] https://packages.scm-manager.org/repository/apt-v2-releases/ stable main' | sudo tee /etc/apt/sources.list.d/scm-manager.list
sudo apt-key adv --recv-keys --keyserver hkps://keys.openpgp.org 0x975922F193B07D6E
sudo apt-get update
sudo apt-get install scm-server
@@ -24,7 +24,7 @@ To install SCM-Manager as a debian package (.deb), we have to configure an apt r
Create a file at `/etc/apt/sources.list.d/scm-manager.list` with the following content:
```text
deb https://packages.scm-manager.org/repository/apt-v2-releases/ stable main
deb [arch=all] https://packages.scm-manager.org/repository/apt-v2-releases/ stable main
```
This will add the apt repository of the scm-manager stable releases to the list of your apt repositories.

35
pom.xml
View File

@@ -35,12 +35,17 @@
<version>2.2.0-SNAPSHOT</version>
<description>
The easiest way to share your Git, Mercurial
and Subversion repositories over http.
and Subversion repositories.
</description>
<name>scm</name>
<url>https://github.com/scm-manager/scm-manager</url>
<organization>
<name>Cloudogu GmbH</name>
<url>https://cloudogu.com</url>
</organization>
<licenses>
<license>
<name>MIT License</name>
@@ -50,9 +55,33 @@
<developers>
<developer>
<id>sdorra</id>
<id>sebastian.sdorra</id>
<name>Sebastian Sdorra</name>
<email>s.sdorra@gmail.com</email>
<email>sebastian.sdorra@cloudogu.com</email>
<timezone>Europe/Berlin</timezone>
</developer>
<developer>
<id>rene.pfeuffer</id>
<name>Rene Pfeufer</name>
<email>rene.pfeuffer@cloudogu.com</email>
<timezone>Europe/Berlin</timezone>
</developer>
<developer>
<id>eduard.heimbuch</id>
<name>Eduard Heimbuch</name>
<email>eduard.heimbuch@cloudogu.com</email>
<timezone>Europe/Berlin</timezone>
</developer>
<developer>
<id>florian.scholdei</id>
<name>Florian Scholdei</name>
<email>florian.scholdei@cloudogu.com</email>
<timezone>Europe/Berlin</timezone>
</developer>
<developer>
<id>Konstantin Schaper</id>
<name>Konstantin Schaper</name>
<email>konstantin.schaper@cloudogu.com</email>
<timezone>Europe/Berlin</timezone>
</developer>
</developers>

View File

@@ -0,0 +1,43 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import sonia.scm.BadRequestException;
import sonia.scm.ContextEntry;
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class ChangeNamespaceNotAllowedException extends BadRequestException {
public ChangeNamespaceNotAllowedException(Repository repository) {
super(ContextEntry.ContextBuilder.entity(repository).build(), "change of namespace is not allowed in current namespace strategy");
}
private static final String CODE = "ERS2vYb7U1";
@Override
public String getCode() {
return CODE;
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import sonia.scm.plugin.ExtensionPoint;
@@ -36,8 +36,16 @@ public interface NamespaceStrategy {
* Create new namespace for the given repository.
*
* @param repository repository
*
* @return namespace
*/
String createNamespace(Repository repository);
/**
* Checks if the namespace can be changed when using this namespace strategy
*
* @return namespace can be changed
*/
default boolean canBeChanged() {
return false;
}
}

View File

@@ -53,7 +53,7 @@ import java.util.Set;
*/
@StaticPermissions(
value = "repository",
permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"},
permissions = {"read", "modify", "delete", "rename", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"},
custom = true, customGlobal = true
)
@XmlAccessorType(XmlAccessType.FIELD)

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
@@ -38,18 +38,15 @@ import java.util.Collection;
* This class is a singleton and is available via injection.
*
* @author Sebastian Sdorra
*
* @apiviz.uses sonia.scm.repository.RepositoryHandler
*/
public interface RepositoryManager
extends TypeManager<Repository, RepositoryHandler>
{
extends TypeManager<Repository, RepositoryHandler> {
/**
* Fire {@link RepositoryHookEvent} to the event bus.
*
* @param event hook event
*
* @since 2.0.0
*/
public void fireHookEvent(RepositoryHookEvent event);
@@ -58,9 +55,7 @@ public interface RepositoryManager
* Imports an existing {@link Repository}.
* Note: This method should only be called from a {@link RepositoryHandler}.
*
*
* @param repository {@link Repository} to import
*
* @throws IOException
*/
public void importRepository(Repository repository) throws IOException;
@@ -71,10 +66,7 @@ public interface RepositoryManager
* Returns a {@link Repository} by its namespace and name or
* null if the {@link Repository} could not be found.
*
*
* @param namespaceAndName namespace and name of the {@link Repository}
*
*
* @return {@link Repository} by its namespace and name or null
* if the {@link Repository} could not be found
*/
@@ -83,7 +75,6 @@ public interface RepositoryManager
/**
* Returns all configured repository types.
*
*
* @return all configured repository types
*/
public Collection<RepositoryType> getConfiguredTypes();
@@ -91,11 +82,17 @@ public interface RepositoryManager
/**
* Returns a {@link RepositoryHandler} by the given type (hg, git, svn ...).
*
*
* @param type the type of the {@link RepositoryHandler}
*
* @return {@link RepositoryHandler} by the given type
*/
@Override
public RepositoryHandler getHandler(String type);
/**
* @param repository the repository {@link Repository}
* @param newNameSpace the new repository namespace
* @param newName the new repository name
* @return {@link Repository} the renamed repository
*/
public Repository rename(Repository repository, String newNameSpace, String newName);
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
@@ -42,17 +42,14 @@ import java.util.Collection;
*/
public class RepositoryManagerDecorator
extends ManagerDecorator<Repository>
implements RepositoryManager
{
implements RepositoryManager {
/**
* Constructs ...
*
*
* @param decorated
*/
public RepositoryManagerDecorator(RepositoryManager decorated)
{
public RepositoryManagerDecorator(RepositoryManager decorated) {
super(decorated);
this.decorated = decorated;
}
@@ -63,8 +60,7 @@ public class RepositoryManagerDecorator
* {@inheritDoc}
*/
@Override
public void fireHookEvent(RepositoryHookEvent event)
{
public void fireHookEvent(RepositoryHookEvent event) {
decorated.fireHookEvent(event);
}
@@ -79,65 +75,66 @@ public class RepositoryManagerDecorator
//~--- get methods ----------------------------------------------------------
@Override
public Repository get(NamespaceAndName namespaceAndName)
{
public Repository get(NamespaceAndName namespaceAndName) {
return decorated.get(namespaceAndName);
}
/**
* {@inheritDoc}
*
*
* @return
*/
@Override
public Collection<RepositoryType> getConfiguredTypes()
{
public Collection<RepositoryType> getConfiguredTypes() {
return decorated.getConfiguredTypes();
}
/**
* Returns the decorated {@link RepositoryManager}.
*
*
* @return decorated {@link RepositoryManager}
*
* @since 1.34
*/
public RepositoryManager getDecorated()
{
public RepositoryManager getDecorated() {
return decorated;
}
/**
* {@inheritDoc}
*
*
* @param type
*
* @return
*/
@Override
@SuppressWarnings("unchecked")
public RepositoryHandler getHandler(String type)
{
public RepositoryHandler getHandler(String type) {
return decorated.getHandler(type);
}
/**
* {@inheritDoc}
*
* @return
*/
@Override
public Collection<Type> getTypes() {
return decorated.getTypes();
}
/**
* {@inheritDoc}
*
* @return
*/
@Override
public Collection<Type> getTypes()
{
return decorated.getTypes();
public Repository rename(Repository repository, String newNamespace, String newName) {
return decorated.rename(repository, newNamespace, newName);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
/**
* Field description
*/
private final RepositoryManager decorated;
}

View File

@@ -49,7 +49,7 @@ public final class RepositoryModificationEvent extends RepositoryEvent implement
*/
public RepositoryModificationEvent(HandlerEventType eventType, Repository item, Repository itemBeforeModification)
{
super(eventType, item);
super(eventType, item, itemBeforeModification);
this.itemBeforeModification = itemBeforeModification;
}

View File

@@ -73,30 +73,6 @@
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
<execution>
<id>copy-jsvc</id>
<phase>prepare-package</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>commons-daemon</groupId>
<artifactId>commons-daemon-native</artifactId>
<version>1.1.0</version>
<type>tar.gz</type>
<fileMappers>
<org.codehaus.plexus.components.io.filemappers.FlattenFileMapper/>
</fileMappers>
</artifactItem>
</artifactItems>
<includes>**/jsvc-linux-*</includes>
<outputDirectory>${project.build.directory}/deb/libexec</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
</configuration>
</execution>
<execution>
<id>copy-webapp</id>
<phase>prepare-package</phase>
@@ -157,7 +133,6 @@
</mapper>
</data>
<data>
<type>file</type>
<src>src/main/fs/etc/scm/logging.xml</src>
@@ -208,18 +183,6 @@
</mapper>
</data>
<data>
<type>directory</type>
<src>${project.build.directory}/deb/libexec</src>
<mapper>
<type>perm</type>
<prefix>/opt/scm-server/libexec</prefix>
<user>root</user>
<group>scm</group>
<filemode>0755</filemode>
</mapper>
</data>
<data>
<type>file</type>
<src>src/main/fs/opt/scm-server/var/webapp/docroot/index.html</src>

View File

@@ -18,58 +18,14 @@
# Copyright (c) 2001-2002 The Apache Software Foundation. All rights
# reserved.
# user used to run the daemon (defaults to current user)
USER="scm"
# extra jvm arguments
EXTRA_JVM_ARGUMENTS="-Djava.awt.headless=true -Dlogback.configurationFile=logging.xml"
BASEDIR="/opt/scm-server"
# set pid path for jsvc
PIDFILE="/var/run/scm-server.pid"
# set log dir for jsvc
LOGDIR="/var/log/scm"
# load settings from defaults directory
[ -r /etc/default/scm-server ] && . /etc/default/scm-server
OS=`uname | tr '[:upper:]' '[:lower:]'`
ARCH=`uname -m`
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
case "$OS" in
sunos*) OS="solaris"
ARCH=`uname -p`
;;
cygwin*) cygwin=true ;;
darwin*) darwin=true
if [ -z "$JAVA_VERSION" ] ; then
JAVA_VERSION="CurrentJDK"
else
echo "Using Java version: $JAVA_VERSION"
fi
if [ -z "$JAVA_HOME" ] ; then
JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/${JAVA_VERSION}/Home
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# If a specific java binary isn't specified search for the standard 'java' binary
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
@@ -97,96 +53,12 @@ fi
CLASSPATH=$CLASSPATH_PREFIX:"$BASEDIR"/conf:"$REPO"/*
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$HOME" ] && HOME=`cygpath --path --windows "$HOME"`
[ -n "$BASEDIR" ] && BASEDIR=`cygpath --path --windows "$BASEDIR"`
[ -n "$REPO" ] && REPO=`cygpath --path --windows "$REPO"`
fi
jsvc=false;
stop="";
if [ "$1" == "start" ]
then
jsvc=true;
else
if [ "$1" == "stop" ]
then
jsvc=true;
stop='-stop';
fi
fi
USER_ARGUMENT=""
if [ "x$USER" != "x" ]
then
USER_ARGUMENT="-user $USER"
fi
DARWIN_USE_ARCH="false"
if $jsvc; then
JSVCCMD=""
if [ "$OS" == "darwin" ]; then
if [ "$DARWIN_USE_ARCH" == "true" ]; then
JSVCCMD="/usr/bin/arch -arch $ARCH $BASEDIR/libexec/jsvc-$OS"
else
JSVCCMD="exec $BASEDIR/libexec/jsvc-$OS"
fi
else
JSVCCMD="exec $BASEDIR/libexec/jsvc-$OS-$ARCH"
fi
# try to extract JAVA_HOME from JAVACMD
if [ -z "$JAVA_HOME" ] ; then
PRG="$JAVACMD"
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
DIR="$(dirname "$PRG")"
DIR="$(dirname "$DIR")"
if [ -d "$DIR" ] ; then
JAVA_HOME="$DIR"
fi
fi
# TODO JVM Arguments
$JSVCCMD -cp "$CLASSPATH" $JAVA_OPTS \
$EXTRA_JVM_ARGUMENTS $USER_ARGUMENT \
-outfile "$LOGDIR/scm-server.out" \
-errfile "$LOGDIR/scm-server.err" \
-pidfile "$PIDFILE" \
-jvm server \
-home "$JAVA_HOME" \
-Dapp.name="scm-server" \
-Dapp.pid="$$" \
-Dapp.repo="$REPO" \
-Dbasedir="$BASEDIR" \
$stop sonia.scm.server.ScmServerDaemon \
"$@"
else
exec "$JAVACMD" $JAVA_OPTS \
$EXTRA_JVM_ARGUMENTS \
-classpath "$CLASSPATH" \
-Dapp.name="scm-server" \
-Dapp.pid="$$" \
-Dapp.repo="$REPO" \
-Dbasedir="$BASEDIR" \
sonia.scm.server.ScmServerDaemon \
"$@"
fi
exec "$JAVACMD" $JAVA_OPTS \
$EXTRA_JVM_ARGUMENTS \
-classpath "$CLASSPATH" \
-Dapp.name="scm-server" \
-Dapp.pid="$$" \
-Dapp.repo="$REPO" \
-Dbasedir="$BASEDIR" \
sonia.scm.server.ScmServerDaemon \
"$@"

View File

@@ -3,7 +3,8 @@ Version: [[version]]
Section: devel
Priority: extra
Architecture: all
Description: The easiest way to share and manage your Git, Mercurial and Subversion repositories over http
Maintainer: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
Description: The easiest way to share and manage your Git, Mercurial and Subversion repositories
Maintainer: SCM-Team <scm-team@cloudogu.com>
Homepage: https://scm-manager.org
Depends: adduser, procps, psmisc, net-tools
Recommends: openjdk-11-jre-headless, mercurial

View File

@@ -37,7 +37,22 @@ systemctl daemon-reload
# enable and start the service
sudo systemctl enable scm-server
# we start scm-manager after 5 seconds
# this is required, because if we install scm-manager with recommend java
# java is not fully setup if we ran our postint script
nohup sh -c "sleep 5; systemctl start scm-server" >/dev/null 2>&1 &
# reload systemd and make service available
systemctl --system daemon-reload || true
# enable service
if ! systemctl is-enabled scm-server >/dev/null
then
systemctl enable scm-server
fi
# start or restart service
if systemctl is-active scm-server >/dev/null
then
systemctl restart scm-server
else
# we start scm-manager after 5 seconds
# this is required, because if we install scm-manager with recommend java
# java is not fully setup if we ran our postint script
nohup sh -c "sleep 5; systemctl start scm-server" >/dev/null 2>&1 &
fi

View File

@@ -29,21 +29,12 @@ HOST=0.0.0.0
# scm-server port
PORT=8080
# change user
USER=scm
# home of scm-manager
export SCM_HOME=/var/lib/scm
# force jvm path
# JAVA_HOME="/usr/lib/jvm/java-11-openjdk-amd64"
# path to pid
PIDFILE=/var/run/scm/scm.pid
# path to log directory
LOGDIR=/var/log/scm
# increase memory
# EXTRA_JVM_ARGUMENTS="$EXTRA_JVM_ARGUMENTS -Xms1g -Xmx1g"

View File

@@ -100,7 +100,8 @@
<root level="WARN">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
<!-- uncomment to enable logging to stdout -->
<!-- appender-ref ref="STDOUT" /-->
</root>
</configuration>

View File

@@ -3,14 +3,19 @@ Description=SCM-Manager Server
After=syslog.target network.target
[Service]
Type=forking
Type=simple
User=scm
Group=scm
WorkingDirectory=/opt/scm-server
ExecStart=/opt/scm-server/bin/scm-server start
ExecStop=/opt/scm-server/bin/scm-server stop
ExecStart=/opt/scm-server/bin/scm-server
Restart=on-failure
# Exit code 143 means that the program received a SIGTERM signal to instruct it to exit,
# but it did not handle the signal properly.
# we suppress that warning for now
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target

View File

@@ -41,6 +41,12 @@
<description>Packaging for RedHat/Centos/Fedora</description>
<name>rpm</name>
<!--
de.dentrassi.maven:rpm
has no other way to set the url of the package
-->
<url>https://scm-manager.org</url>
<dependencies>
<dependency>
@@ -73,30 +79,6 @@
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
<execution>
<id>copy-jsvc</id>
<phase>prepare-package</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>commons-daemon</groupId>
<artifactId>commons-daemon-native</artifactId>
<version>1.1.0</version>
<type>tar.gz</type>
<fileMappers>
<org.codehaus.plexus.components.io.filemappers.FlattenFileMapper/>
</fileMappers>
</artifactItem>
</artifactItems>
<includes>**/jsvc-linux-*</includes>
<outputDirectory>${project.build.directory}/rpm/libexec</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
</configuration>
</execution>
<execution>
<id>copy-webapp</id>
<phase>prepare-package</phase>
@@ -127,6 +109,8 @@
<configuration>
<attach>true</attach>
<packageName>scm-server</packageName>
<summary>SCM-Manager Server</summary>
<description>The easiest way to share and manage your Git, Mercurial and Subversion repositories</description>
<group>Development/Tools</group>
<license>MIT</license>
<skipSigning>true</skipSigning>
@@ -163,15 +147,6 @@
<group>scm</group>
<mode>0644</mode>
</rule>
<rule>
<when>
<type>file</type>
<prefix>/opt/scm-server/libexec</prefix>
</when>
<user>root</user>
<group>scm</group>
<mode>0755</mode>
</rule>
</rules>
</ruleset>
</rulesets>
@@ -229,14 +204,6 @@
<ruleset>default</ruleset>
</entry>
<entry>
<name>/opt/scm-server/libexec</name>
<collect>
<from>${project.build.directory}/rpm/libexec</from>
</collect>
<ruleset>default</ruleset>
</entry>
<entry>
<name>/opt/scm-server/var/webapp/docroot/index.html</name>
<file>src/main/fs/opt/scm-server/var/webapp/docroot/index.html</file>
@@ -319,7 +286,6 @@
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>

View File

@@ -18,58 +18,14 @@
# Copyright (c) 2001-2002 The Apache Software Foundation. All rights
# reserved.
# user used to run the daemon (defaults to current user)
USER="scm"
# extra jvm arguments
EXTRA_JVM_ARGUMENTS="-Djava.awt.headless=true -Dlogback.configurationFile=logging.xml"
BASEDIR="/opt/scm-server"
# set pid path for jsvc
PIDFILE="/var/run/scm-server.pid"
# set log dir for jsvc
LOGDIR="/var/log/scm"
# load settings from defaults directory
[ -r /etc/default/scm-server ] && . /etc/default/scm-server
OS=`uname | tr '[:upper:]' '[:lower:]'`
ARCH=`uname -m`
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
case "$OS" in
sunos*) OS="solaris"
ARCH=`uname -p`
;;
cygwin*) cygwin=true ;;
darwin*) darwin=true
if [ -z "$JAVA_VERSION" ] ; then
JAVA_VERSION="CurrentJDK"
else
echo "Using Java version: $JAVA_VERSION"
fi
if [ -z "$JAVA_HOME" ] ; then
JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/${JAVA_VERSION}/Home
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# If a specific java binary isn't specified search for the standard 'java' binary
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
@@ -97,96 +53,12 @@ fi
CLASSPATH=$CLASSPATH_PREFIX:"$BASEDIR"/conf:"$REPO"/*
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$HOME" ] && HOME=`cygpath --path --windows "$HOME"`
[ -n "$BASEDIR" ] && BASEDIR=`cygpath --path --windows "$BASEDIR"`
[ -n "$REPO" ] && REPO=`cygpath --path --windows "$REPO"`
fi
jsvc=false;
stop="";
if [ "$1" == "start" ]
then
jsvc=true;
else
if [ "$1" == "stop" ]
then
jsvc=true;
stop='-stop';
fi
fi
USER_ARGUMENT=""
if [ "x$USER" != "x" ]
then
USER_ARGUMENT="-user $USER"
fi
DARWIN_USE_ARCH="false"
if $jsvc; then
JSVCCMD=""
if [ "$OS" == "darwin" ]; then
if [ "$DARWIN_USE_ARCH" == "true" ]; then
JSVCCMD="/usr/bin/arch -arch $ARCH $BASEDIR/libexec/jsvc-$OS"
else
JSVCCMD="exec $BASEDIR/libexec/jsvc-$OS"
fi
else
JSVCCMD="exec $BASEDIR/libexec/jsvc-$OS-$ARCH"
fi
# try to extract JAVA_HOME from JAVACMD
if [ -z "$JAVA_HOME" ] ; then
PRG="$JAVACMD"
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
DIR="$(dirname "$PRG")"
DIR="$(dirname "$DIR")"
if [ -d "$DIR" ] ; then
JAVA_HOME="$DIR"
fi
fi
# TODO JVM Arguments
$JSVCCMD -cp "$CLASSPATH" $JAVA_OPTS \
$EXTRA_JVM_ARGUMENTS $USER_ARGUMENT \
-outfile "$LOGDIR/scm-server.out" \
-errfile "$LOGDIR/scm-server.err" \
-pidfile "$PIDFILE" \
-jvm server \
-home "$JAVA_HOME" \
-Dapp.name="scm-server" \
-Dapp.pid="$$" \
-Dapp.repo="$REPO" \
-Dbasedir="$BASEDIR" \
$stop sonia.scm.server.ScmServerDaemon \
"$@"
else
exec "$JAVACMD" $JAVA_OPTS \
$EXTRA_JVM_ARGUMENTS \
-classpath "$CLASSPATH" \
-Dapp.name="scm-server" \
-Dapp.pid="$$" \
-Dapp.repo="$REPO" \
-Dbasedir="$BASEDIR" \
sonia.scm.server.ScmServerDaemon \
"$@"
fi
exec "$JAVACMD" $JAVA_OPTS \
$EXTRA_JVM_ARGUMENTS \
-classpath "$CLASSPATH" \
-Dapp.name="scm-server" \
-Dapp.pid="$$" \
-Dapp.repo="$REPO" \
-Dbasedir="$BASEDIR" \
sonia.scm.server.ScmServerDaemon \
"$@"

View File

@@ -29,21 +29,12 @@ HOST=0.0.0.0
# scm-server port
PORT=8080
# change user
USER=scm
# home of scm-manager
export SCM_HOME=/var/lib/scm
# force jvm path
# JAVA_HOME="/usr/lib/jvm/jre-11"
# path to pid
PIDFILE=/var/run/scm/scm.pid
# path to log directory
LOGDIR=/var/log/scm
# increase memory
# EXTRA_JVM_ARGUMENTS="$EXTRA_JVM_ARGUMENTS -Xms1g -Xmx1g"

View File

@@ -100,7 +100,8 @@
<root level="WARN">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
<!-- uncomment to enable logging to stdout -->
<!-- appender-ref ref="STDOUT" /-->
</root>
</configuration>

View File

@@ -3,14 +3,19 @@ Description=SCM-Manager Server
After=syslog.target network.target
[Service]
Type=forking
Type=simple
User=scm
Group=scm
WorkingDirectory=/opt/scm-server
ExecStart=/opt/scm-server/bin/scm-server start
ExecStop=/opt/scm-server/bin/scm-server stop
ExecStart=/opt/scm-server/bin/scm-server
Restart=on-failure
# Exit code 143 means that the program received a SIGTERM signal to instruct it to exit,
# but it did not handle the signal properly.
# we suppress that warning for now
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target

View File

@@ -32,8 +32,18 @@ if [ -d "${WORKDIR}" ]; then
fi
# reload systemd and make service available
systemctl daemon-reload
systemctl --system daemon-reload || true
# enable and start the service
sudo systemctl enable scm-server
sudo systemctl start scm-server
# enable service
if ! systemctl is-enabled scm-server >/dev/null
then
systemctl enable scm-server
fi
# start or restart service
if systemctl is-active scm-server >/dev/null
then
systemctl restart scm-server
else
systemctl start scm-server
fi

View File

@@ -113,7 +113,8 @@
"repositoryForm": {
"subtitle": "Repository bearbeiten",
"submit": "Speichern",
"initializeRepository": "Repository initiieren"
"initializeRepository": "Repository initiieren",
"dangerZone": "Gefahrenzone"
},
"sources": {
"file-tree": {
@@ -196,6 +197,8 @@
},
"deleteRepo": {
"button": "Repository löschen",
"subtitle": "Löscht dieses Repository",
"description": "Diese Aktion kann nicht rückgangig gemacht werden.",
"confirmAlert": {
"title": "Repository löschen",
"message": "Soll das Repository wirklich gelöscht werden?",
@@ -203,6 +206,22 @@
"cancel": "Nein"
}
},
"renameRepo": {
"button": "Repository umbenennen",
"subtitle": "Benennt dieses Repository um",
"description": "Es werden keine Weiterleitung auf den neuen Namen eingerichtet.",
"modal": {
"title": "Repository umbenennen",
"label": {
"repoName": "Repository Name",
"repoNamespace": "Repository Namespace"
},
"button": {
"rename": "Umbenennen",
"cancel": "Abbrechen"
}
}
},
"diff": {
"sideBySide": "Zur zweispaltigen Ansicht wechseln",
"combined": "Zur kombinierten Ansicht wechseln",

View File

@@ -113,7 +113,8 @@
"repositoryForm": {
"subtitle": "Edit Repository",
"submit": "Save",
"initializeRepository": "Initialize repository"
"initializeRepository": "Initialize repository",
"dangerZone": "Danger Zone"
},
"sources": {
"file-tree": {
@@ -196,6 +197,8 @@
},
"deleteRepo": {
"button": "Delete Repository",
"subtitle": "Deletes this repository",
"description": "Once a repository was deleted, this cannot be undone. Please be careful with this action.",
"confirmAlert": {
"title": "Delete repository",
"message": "Do you really want to delete the repository?",
@@ -203,6 +206,22 @@
"cancel": "No"
}
},
"renameRepo": {
"button": "Rename Repository",
"subtitle": "Renames this repository",
"description": "There will be no redirects to the renamed repository.",
"modal": {
"title": "Rename repository",
"label": {
"repoName": "Repository name",
"repoNamespace": "Repository namespace"
},
"button": {
"rename": "Rename",
"cancel": "Cancel"
}
}
},
"diff": {
"changes": {
"add": "added",

View File

@@ -44,7 +44,7 @@ import GlobalConfig from "./GlobalConfig";
import RepositoryRoles from "../roles/containers/RepositoryRoles";
import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole";
import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole";
import { StateMenuContextProvider } from "@scm-manager/ui-components/src/navigation/MenuContext";
import { StateMenuContextProvider } from "@scm-manager/ui-components";
type Props = RouteComponentProps &
WithTranslation & {

View File

@@ -54,7 +54,7 @@ import PluginBottomActions from "../components/PluginBottomActions";
import ExecutePendingActionModal from "../components/ExecutePendingActionModal";
import CancelPendingActionModal from "../components/CancelPendingActionModal";
import UpdateAllActionModal from "../components/UpdateAllActionModal";
import { Plugin } from "@scm-manager/ui-types/src";
import { Plugin } from "@scm-manager/ui-types";
import ShowPendingModal from "../components/ShowPendingModal";
type Props = WithTranslation & {

View File

@@ -31,7 +31,7 @@ import { Page } from "@scm-manager/ui-components";
import { getGroupsLink, getUserAutoCompleteLink } from "../../modules/indexResource";
import { createGroup, createGroupReset, getCreateGroupFailure, isCreateGroupPending } from "../modules/groups";
import GroupForm from "../components/GroupForm";
import { apiClient } from "@scm-manager/ui-components/src";
import { apiClient } from "@scm-manager/ui-components";
type Props = WithTranslation & {
createGroup: (link: string, group: Group, callback?: () => void) => void;

View File

@@ -31,7 +31,7 @@ import { DisplayedUser, Group } from "@scm-manager/ui-types";
import { ErrorNotification } from "@scm-manager/ui-components";
import { getUserAutoCompleteLink } from "../../modules/indexResource";
import DeleteGroup from "./DeleteGroup";
import { apiClient } from "@scm-manager/ui-components/src";
import { apiClient } from "@scm-manager/ui-components";
import { compose } from "redux";
type Props = {

View File

@@ -26,8 +26,9 @@ import styled from "styled-components";
import { WithTranslation, withTranslation } from "react-i18next";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { Repository, RepositoryType } from "@scm-manager/ui-types";
import { Checkbox, Level, InputField, Select, SubmitButton, Subtitle, Textarea } from "@scm-manager/ui-components";
import { Checkbox, InputField, Level, Select, SubmitButton, Subtitle, Textarea } from "@scm-manager/ui-components";
import * as validator from "./repositoryValidation";
import { CUSTOM_NAMESPACE_STRATEGY } from "../../modules/repos";
const CheckboxWrapper = styled.div`
margin-top: 2em;
@@ -59,8 +60,6 @@ type State = {
contactValidationError: boolean;
};
const CUSTOM_NAMESPACE_STRATEGY = "CustomNamespaceStrategy";
class RepositoryForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
@@ -108,7 +107,7 @@ class RepositoryForm extends React.Component<Props, State> {
);
};
submit = (event: Event) => {
submit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (this.isValid()) {
this.props.submitForm(this.state.repository, this.state.initRepository);

View File

@@ -0,0 +1,73 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { Repository, Links } from "@scm-manager/ui-types";
import RenameRepository from "./RenameRepository";
import DeleteRepo from "./DeleteRepo";
import styled from "styled-components";
import { Subtitle } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
type Props = {
repository: Repository;
indexLinks: Links;
};
const DangerZoneContainer = styled.div`
padding: 1rem;
border: 1px solid #ff6a88;
border-radius: 5px;
> *:not(:last-child) {
padding-bottom: 1.5rem;
border-bottom: solid 2px whitesmoke;
}
`;
const DangerZone: FC<Props> = ({ repository, indexLinks }) => {
const [t] = useTranslation("repos");
const dangerZone = [];
if (repository?._links?.rename || repository?._links?.renameWithNamespace) {
dangerZone.push(<RenameRepository repository={repository} indexLinks={indexLinks} />);
}
if (repository?._links?.delete) {
// @ts-ignore
dangerZone.push(<DeleteRepo repository={repository} />);
}
if (dangerZone.length === 0) {
return null;
}
return (
<>
<hr />
<Subtitle subtitle={t("repositoryForm.dangerZone")} />
<DangerZoneContainer>{dangerZone.map(entry => entry)}</DangerZoneContainer>
</>
);
};
export default DangerZone;

View File

@@ -24,23 +24,21 @@
import React from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { withRouter } from "react-router-dom";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { Repository } from "@scm-manager/ui-types";
import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { confirmAlert, DeleteButton, ErrorNotification, Level, ButtonGroup } from "@scm-manager/ui-components";
import { deleteRepo, getDeleteRepoFailure, isDeleteRepoPending } from "../modules/repos";
type Props = WithTranslation & {
loading: boolean;
error: Error;
repository: Repository;
confirmDialog?: boolean;
deleteRepo: (p1: Repository, p2: () => void) => void;
// context props
history: History;
};
type Props = RouteComponentProps &
WithTranslation & {
loading: boolean;
error: Error;
repository: Repository;
confirmDialog?: boolean;
deleteRepo: (p1: Repository, p2: () => void) => void;
};
class DeleteRepo extends React.Component<Props> {
static defaultProps = {
@@ -88,9 +86,16 @@ class DeleteRepo extends React.Component<Props> {
return (
<>
<hr />
<ErrorNotification error={error} />
<Level right={<DeleteButton label={t("deleteRepo.button")} action={action} loading={loading} />} />
<Level
left={
<div>
<strong>{t("deleteRepo.subtitle")}</strong>
<p>{t("deleteRepo.description")}</p>
</div>
}
right={<DeleteButton label={t("deleteRepo.button")} action={action} loading={loading} />}
/>
</>
);
}

View File

@@ -25,17 +25,19 @@ import React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import RepositoryForm from "../components/form";
import DeleteRepo from "./DeleteRepo";
import { Repository } from "@scm-manager/ui-types";
import { Repository, Links } from "@scm-manager/ui-types";
import { getModifyRepoFailure, isModifyRepoPending, modifyRepo, modifyRepoReset } from "../modules/repos";
import { History } from "history";
import { ErrorNotification } from "@scm-manager/ui-components";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { compose } from "redux";
import DangerZone from "./DangerZone";
import { getLinks } from "../../modules/indexResource";
type Props = {
loading: boolean;
error: Error;
indexLinks: Links;
modifyRepo: (p1: Repository, p2: () => void) => void;
modifyRepoReset: (p: Repository) => void;
@@ -69,7 +71,7 @@ class EditRepo extends React.Component<Props> {
};
render() {
const { loading, error, repository } = this.props;
const { loading, error, repository, indexLinks } = this.props;
const url = this.matchedUrl();
@@ -79,7 +81,7 @@ class EditRepo extends React.Component<Props> {
};
return (
<div>
<>
<ErrorNotification error={error} />
<RepositoryForm
repository={this.props.repository}
@@ -89,8 +91,8 @@ class EditRepo extends React.Component<Props> {
}}
/>
<ExtensionPoint name="repo-config.route" props={extensionProps} renderAll={true} />
<DeleteRepo repository={repository} />
</div>
<DangerZone repository={repository} indexLinks={indexLinks} />
</>
);
}
}
@@ -99,9 +101,12 @@ const mapStateToProps = (state: any, ownProps: Props) => {
const { namespace, name } = ownProps.repository;
const loading = isModifyRepoPending(state, namespace, name);
const error = getModifyRepoFailure(state, namespace, name);
const indexLinks = getLinks(state);
return {
loading,
error
error,
indexLinks
};
};

View File

@@ -0,0 +1,178 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useEffect, useState } from "react";
import { Link, Links, Repository } from "@scm-manager/ui-types";
import { CONTENT_TYPE, CUSTOM_NAMESPACE_STRATEGY } from "../modules/repos";
import { Button, ButtonGroup, ErrorNotification, InputField, Level, Loading, Modal } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { apiClient } from "@scm-manager/ui-components";
import { useHistory } from "react-router-dom";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import * as validator from "../components/form/repositoryValidation";
type Props = {
repository: Repository;
indexLinks: Links;
};
const RenameRepository: FC<Props> = ({ repository, indexLinks }) => {
let history = useHistory();
const [t] = useTranslation("repos");
const [error, setError] = useState<Error | undefined>(undefined);
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [name, setName] = useState(repository.name);
const [namespace, setNamespace] = useState(repository.namespace);
const [nameValidationError, setNameValidationError] = useState(false);
const [namespaceValidationError, setNamespaceValidationError] = useState(false);
const [currentNamespaceStrategie, setCurrentNamespaceStrategy] = useState("");
useEffect(() => {
apiClient
.get((indexLinks?.namespaceStrategies as Link).href)
.then(result => result.json())
.then(result => setCurrentNamespaceStrategy(result.current))
.catch(setError);
}, [repository]);
if (error) {
return <ErrorNotification error={error} />;
}
if (loading) {
return <Loading />;
}
const isValid =
!nameValidationError &&
!namespaceValidationError &&
(repository.name !== name || repository.namespace !== namespace);
const handleNamespaceChange = (namespace: string) => {
setNamespaceValidationError(!validator.isNameValid(namespace));
setNamespace(namespace);
};
const handleNameChange = (name: string) => {
setNameValidationError(!validator.isNameValid(name));
setName(name);
};
const renderNamespaceField = () => {
const props = {
label: t("repository.namespace"),
helpText: t("help.namespaceHelpText"),
value: namespace,
onChange: handleNamespaceChange,
errorMessage: t("validation.namespace-invalid"),
validationError: namespaceValidationError
};
if (currentNamespaceStrategie === CUSTOM_NAMESPACE_STRATEGY) {
return <InputField {...props} />;
}
return <ExtensionPoint name="repos.create.namespace" props={props} renderAll={false} />;
};
const rename = () => {
setLoading(true);
const url = repository?._links?.renameWithNamespace
? (repository?._links?.renameWithNamespace as Link).href
: (repository?._links?.rename as Link).href;
apiClient
.post(url, { name, namespace }, CONTENT_TYPE)
.then(() => setLoading(false))
.then(() => history.push(`/repo/${namespace}/${name}`))
.catch(setError);
};
const modalBody = (
<div>
<InputField
label={t("renameRepo.modal.label.repoName")}
name={t("renameRepo.modal.label.repoName")}
errorMessage={t("validation.name-invalid")}
helpText={t("help.nameHelpText")}
validationError={nameValidationError}
value={name}
onChange={handleNameChange}
/>
{renderNamespaceField()}
</div>
);
const footer = (
<>
<ButtonGroup>
<Button
color="warning"
icon="exclamation-triangle"
label={t("renameRepo.modal.button.rename")}
disabled={!isValid}
title={t("renameRepo.modal.button.rename")}
action={rename}
/>
<Button
label={t("renameRepo.modal.button.cancel")}
title={t("renameRepo.modal.button.cancel")}
action={() => setShowModal(false)}
/>
</ButtonGroup>
</>
);
return (
<>
<Modal
active={showModal}
title={t("renameRepo.modal.title")}
footer={footer}
body={modalBody}
closeFunction={() => setShowModal(false)}
/>
<Level
left={
<div>
<strong>{t("renameRepo.subtitle")}</strong>
<p>{t("renameRepo.description")}</p>
</div>
}
right={
<Button
label={t("renameRepo.button")}
action={() => setShowModal(true)}
loading={loading}
color="warning"
icon="edit"
/>
}
/>
</>
);
};
export default RenameRepository;

View File

@@ -74,6 +74,13 @@ class RepositoryRoot extends React.Component<Props> {
fetchRepoByName(repoLink, namespace, name);
}
componentDidUpdate(prevProps: Props) {
const { fetchRepoByName, namespace, name, repoLink } = this.props;
if (namespace !== prevProps.namespace || name !== prevProps.name) {
fetchRepoByName(repoLink, namespace, name);
}
}
stripEndingSlash = (url: string) => {
if (url.endsWith("/")) {
return url.substring(0, url.length - 1);

View File

@@ -27,7 +27,6 @@ import * as types from "../../modules/types";
import { Action, Repository, RepositoryCollection } from "@scm-manager/ui-types";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
import React from "react";
export const FETCH_REPOS = "scm/repos/FETCH_REPOS";
export const FETCH_REPOS_PENDING = `${FETCH_REPOS}_${types.PENDING_SUFFIX}`;
@@ -56,7 +55,9 @@ export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`;
export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_SUFFIX}`;
export const DELETE_REPO_FAILURE = `${DELETE_REPO}_${types.FAILURE_SUFFIX}`;
const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2";
export const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2";
export const CUSTOM_NAMESPACE_STRATEGY = "CustomNamespaceStrategy";
// fetch repos

View File

@@ -29,7 +29,7 @@ import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { File } from "@scm-manager/ui-types";
import { DateFromNow, FileSize, Tooltip } from "@scm-manager/ui-components";
import FileIcon from "./FileIcon";
import { Icon } from "@scm-manager/ui-components/src";
import { Icon } from "@scm-manager/ui-components";
import { WithTranslation, withTranslation } from "react-i18next";
type Props = WithTranslation & {

View File

@@ -0,0 +1,39 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.NoArgsConstructor;
import sonia.scm.util.ValidationUtil;
import javax.validation.constraints.Pattern;
@Getter
@NoArgsConstructor
public class RepositoryRenameDto {
@Pattern(regexp = ValidationUtil.REGEX_REPOSITORYNAME)
private String name;
private String namespace;
}

View File

@@ -34,11 +34,11 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@@ -64,8 +64,7 @@ public class RepositoryResource {
public RepositoryResource(
RepositoryToRepositoryDtoMapper repositoryToDtoMapper,
RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, RepositoryManager manager,
RepositoryBasedResourceProvider resourceProvider
) {
RepositoryBasedResourceProvider resourceProvider) {
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
this.manager = manager;
this.repositoryToDtoMapper = repositoryToDtoMapper;
@@ -79,8 +78,7 @@ public class RepositoryResource {
* <strong>Note:</strong> This method requires "repository" privilege.
*
* @param namespace the namespace of the repository
* @param name the name of the repository
*
* @param name the name of the repository
*/
@GET
@Path("")
@@ -118,7 +116,7 @@ public class RepositoryResource {
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name){
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) {
return adapter.get(loadBy(namespace, name), repositoryToDtoMapper::map);
}
@@ -128,8 +126,7 @@ public class RepositoryResource {
* <strong>Note:</strong> This method requires "repository" privilege.
*
* @param namespace the namespace of the repository to delete
* @param name the name of the repository to delete
*
* @param name the name of the repository to delete
*/
@DELETE
@Path("")
@@ -147,8 +144,8 @@ public class RepositoryResource {
*
* <strong>Note:</strong> This method requires "repository" privilege.
*
* @param namespace the namespace of the repository to be modified
* @param name the name of the repository to be modified
* @param namespace the namespace of the repository to be modified
* @param name the name of the repository to be modified
* @param repository repository object to modify
*/
@PUT
@@ -176,6 +173,37 @@ public class RepositoryResource {
);
}
/**
* Renames the given repository.
*
* <strong>Note:</strong> This method requires "repository" privilege.
*
* @param namespace the namespace of the repository to be modified
* @param name the name of the repository to be modified
* @param renameDto renameDto object to modify
*/
@POST
@Path("rename")
@Consumes(VndMediaType.REPOSITORY)
@Operation(summary = "Rename repository", description = "Renames the repository for the given namespace and name.", tags = "Repository")
@ApiResponse(responseCode = "204", description = "update success")
@ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of namespace or name")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository:renameDto\" privilege")
@ApiResponse(
responseCode = "404",
description = "not found, no repository with the specified namespace and name available",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
@ApiResponse(responseCode = "500", description = "internal server error")
public Response rename(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryRenameDto renameDto) {
Repository repository = loadBy(namespace, name).get();
manager.rename(repository, renameDto.getNamespace(), renameDto.getName());
return Response.status(204).build();
}
private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) {
Repository changedRepository = dtoToRepositoryMapper.map(repositoryDto, existing.getId());
changedRepository.setPermissions(existing.getPermissions());

View File

@@ -24,14 +24,15 @@
package sonia.scm.api.v2.resources;
import com.google.inject.Inject;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Link;
import de.otto.edison.hal.Links;
import org.mapstruct.Mapper;
import org.mapstruct.ObjectFactory;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.Feature;
import sonia.scm.repository.HealthCheckFailure;
import sonia.scm.repository.NamespaceStrategy;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.Command;
@@ -41,7 +42,9 @@ import sonia.scm.repository.api.ScmProtocol;
import sonia.scm.web.EdisonHalAppender;
import sonia.scm.web.api.RepositoryToHalMapper;
import javax.inject.Inject;
import java.util.List;
import java.util.Set;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Link.link;
@@ -56,7 +59,11 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
@Inject
private ResourceLinks resourceLinks;
@Inject
private ScmConfiguration scmConfiguration;
@Inject
private RepositoryServiceFactory serviceFactory;
@Inject
private Set<NamespaceStrategy> strategies;
abstract HealthCheckFailureDto toDto(HealthCheckFailure failure);
@@ -72,6 +79,13 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
if (RepositoryPermissions.modify(repository).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.repository().update(repository.getNamespace(), repository.getName())));
}
if (RepositoryPermissions.rename(repository).isPermitted()) {
if (isRenameNamespacePossible()) {
linksBuilder.single(link("renameWithNamespace", resourceLinks.repository().rename(repository.getNamespace(), repository.getName())));
} else {
linksBuilder.single(link("rename", resourceLinks.repository().rename(repository.getNamespace(), repository.getName())));
}
}
if (RepositoryPermissions.permissionRead(repository).isPermitted()) {
linksBuilder.single(link("permissions", resourceLinks.repositoryPermission().all(repository.getNamespace(), repository.getName())));
}
@@ -105,6 +119,15 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
return new RepositoryDto(linksBuilder.build(), embeddedBuilder.build());
}
private boolean isRenameNamespacePossible() {
for (NamespaceStrategy strategy : strategies) {
if (strategy.getClass().getSimpleName().equals(scmConfiguration.getNamespaceStrategy())) {
return strategy.canBeChanged();
}
}
return false;
}
private Link createProtocolLink(ScmProtocol protocol) {
return Link.linkBuilder("protocol", protocol.getUrl()).withName(protocol.getType()).build();
}

View File

@@ -279,6 +279,10 @@ class ResourceLinks {
String update(String namespace, String name) {
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("update").parameters().href();
}
String rename(String namespace, String name) {
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("rename").parameters().href();
}
}
RepositoryCollectionLinks repositoryCollection() {

View File

@@ -41,4 +41,9 @@ public class CustomNamespaceStrategy implements NamespaceStrategy {
return namespace;
}
@Override
public boolean canBeChanged() {
return true;
}
}

View File

@@ -21,10 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.github.sdorra.ssp.PermissionActionCheck;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.Inject;
@@ -35,6 +36,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.ConfigurationException;
import sonia.scm.HandlerEventType;
import sonia.scm.ManagerDaoAdapter;
import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException;
import sonia.scm.SCMContextProvider;
import sonia.scm.Type;
@@ -83,7 +85,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
private final Provider<NamespaceStrategy> namespaceStrategyProvider;
private final ManagerDaoAdapter<Repository> managerDaoAdapter;
@Inject
public DefaultRepositoryManager(ScmConfiguration configuration,
SCMContextProvider contextProvider, KeyGenerator keyGenerator,
@@ -154,7 +155,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
}
@Override
public void delete(Repository repository){
public void delete(Repository repository) {
logger.info("delete repository {}/{} of type {}", repository.getNamespace(), repository.getName(), repository.getType());
managerDaoAdapter.delete(
repository,
@@ -179,7 +180,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
}
@Override
public void modify(Repository repository){
public void modify(Repository repository) {
logger.info("modify repository {}/{} of type {}", repository.getNamespace(), repository.getName(), repository.getType());
managerDaoAdapter.modify(
@@ -243,6 +244,40 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
return repository;
}
public Repository rename(Repository repository, String newNamespace, String newName) {
if (hasNamespaceOrNameNotChanged(repository, newNamespace, newName)) {
throw new NoChangesMadeException(repository);
}
Repository changedRepository = repository.clone();
if (!Strings.isNullOrEmpty(newName)) {
changedRepository.setName(newName);
}
if (!Strings.isNullOrEmpty(newNamespace) && !repository.getNamespace().equals(newNamespace)) {
NamespaceStrategy strategy = namespaceStrategyProvider.get();
if (!strategy.canBeChanged()) {
throw new ChangeNamespaceNotAllowedException(repository);
}
changedRepository.setNamespace(strategy.createNamespace(changedRepository));
}
managerDaoAdapter.modify(
changedRepository,
RepositoryPermissions::rename,
notModified -> {
},
notModified -> fireEvent(HandlerEventType.MODIFY, changedRepository, repository));
return changedRepository;
}
private boolean hasNamespaceOrNameNotChanged(Repository repository, String newNamespace, String newName) {
return repository.getName().equals(newName)
&& repository.getNamespace().equals(newNamespace);
}
@Override
public Collection<Repository> getAll(Predicate<Repository> filter, Comparator<Repository> comparator) {
List<Repository> repositories = Lists.newArrayList();
@@ -345,8 +380,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
types.add(type);
}
private RepositoryHandler getHandler(Repository repository)
{
private RepositoryHandler getHandler(Repository repository) {
String type = repository.getType();
RepositoryHandler handler = handlerMap.get(type);

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.update.security;
import org.slf4j.Logger;
@@ -44,6 +44,7 @@ import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
@@ -179,7 +180,7 @@ public class XmlSecurityV1UpdateStep implements UpdateStep {
@XmlRootElement(name = "configuration")
private static class V1Security {
@XmlElement(name = "entry")
private List<Entry> entries;
private List<Entry> entries = new ArrayList<>();
}
@XmlAccessorType(XmlAccessType.FIELD)

View File

@@ -35,6 +35,9 @@
<permission>
<value>repository:read,pull,push:*</value>
</permission>
<permission>
<value>repository:read,rename:*</value>
</permission>
<permission>
<value>repository:*</value>
</permission>

View File

@@ -28,6 +28,7 @@
<verb>read</verb>
<verb>modify</verb>
<verb>delete</verb>
<verb>rename</verb>
<verb>pull</verb>
<verb>push</verb>
<verb>permissionRead</verb>

View File

@@ -28,6 +28,12 @@
"description": "Darf alle Repositories lesen, klonen und schreiben."
}
},
"read,rename": {
"*": {
"displayName": "Alle Repositories umbenennen",
"description": "Darf alle Repositories lesen und umbenennen."
}
},
"*": {
"displayName": "Alle Repositories besitzen (Owner)",
"description": "Darf alle Repositories lesen, klonen, schreiben, konfigurieren und löschen."
@@ -103,6 +109,10 @@
"displayName": "Repository Löschen",
"description": "Darf das Repository löschen."
},
"rename": {
"displayName": "Repository umbenennen",
"description": "Darf das Repository umbenennen."
},
"pull": {
"displayName": "Pull/Checkout",
"description": "Darf pull/checkout auf das Repository ausführen."
@@ -187,6 +197,10 @@
"displayName": "Es wurden keine Änderungen durchgeführt",
"description": "Das Repository wurde nicht verändert. Daher konnte kein neuer Commit erzeugt werden. Womöglich werden Änderungen aufgrund einer .ignore-Datei Definition nicht berücksichtigt."
},
"ERS2vYb7U1": {
"displayName": "Änderung des Namespace nicht möglich",
"description": "Namespaces dürfen nur mit der Namespace Strategie \"Benutzerdefiniert\" verändert werden."
},
"4iRct4avG1": {
"displayName": "Die Revisionen haben keinen gemeinsamen Ursprung",
"description": "Die Historie der Revisionen hat keinen gemeinsamen Urspung und kann somit auch nicht gegen einen solchen verglichen werden."

View File

@@ -28,6 +28,12 @@
"description": "May see, clone and push to all repositories"
}
},
"read,rename": {
"*": {
"displayName": "Rename all repositories",
"description": "May see and rename all repositories"
}
},
"*": {
"displayName": "Own all repositories",
"description": "May see, clone, push to, configure and delete all repositories"
@@ -103,6 +109,10 @@
"displayName": "delete repository",
"description": "May delete the repository"
},
"rename": {
"displayName": "rename repository",
"description": "May rename the repository."
},
"pull": {
"displayName": "pull/checkout repository",
"description": "May pull/checkout the repository"
@@ -187,6 +197,10 @@
"displayName": "No changes were made",
"description": "No changes were made to the files of the repository. Therefor no new commit could be created. Possibly changes cannot be applied due to an .ignore-File definition."
},
"ERS2vYb7U1": {
"displayName": "Illegal change of namespace",
"description": "Namespaces can only be changed if namespace strategy is \"custom\"."
},
"4iRct4avG1": {
"displayName": "The revisions have unrelated histories",
"description": "The revisions have unrelated histories. Therefor there is no common commit to compare with."

View File

@@ -26,8 +26,8 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Resources;
import com.google.inject.util.Providers;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.jboss.resteasy.mock.MockHttpRequest;
@@ -40,7 +40,10 @@ import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.PageResult;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.CustomNamespaceStrategy;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.NamespaceStrategy;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryInitializer;
import sonia.scm.repository.RepositoryManager;
@@ -56,6 +59,7 @@ import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Set;
import java.util.function.Predicate;
import static java.util.Collections.singletonList;
@@ -72,6 +76,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyObject;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -103,6 +108,10 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
private ScmPathInfo uriInfo;
@Mock
private RepositoryInitializer repositoryInitializer;
@Mock
private ScmConfiguration configuration;
@Mock
private Set<NamespaceStrategy> strategies;
@Captor
private ArgumentCaptor<Predicate<Repository>> filterCaptor;
@@ -127,6 +136,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(scmPathInfoStore.get()).thenReturn(uriInfo);
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y"));
doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator();
SimplePrincipalCollection trillian = new SimplePrincipalCollection("trillian", REALM);
trillian.add(new User("trillian"), REALM);
shiro.setSubject(
@@ -152,6 +162,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
@Test
public void shouldFindExistingRepository() throws URISyntaxException, UnsupportedEncodingException {
mockRepository("space", "repo");
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo");
MockHttpResponse response = new MockHttpResponse();
@@ -166,6 +177,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
public void shouldGetAll() throws URISyntaxException, UnsupportedEncodingException {
PageResult<Repository> singletonPageResult = createSingletonPageResult(mockRepository("space", "repo"));
when(repositoryManager.getPage(any(), any(), eq(0), eq(10))).thenReturn(singletonPageResult);
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2);
MockHttpResponse response = new MockHttpResponse();
@@ -180,6 +192,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
public void shouldCreateFilterForSearch() throws URISyntaxException {
PageResult<Repository> singletonPageResult = createSingletonPageResult(mockRepository("space", "repo"));
when(repositoryManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult);
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?q=Rep");
MockHttpResponse response = new MockHttpResponse();
@@ -362,6 +375,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
public void shouldCreateArrayOfProtocolUrls() throws Exception {
mockRepository("space", "repo");
when(service.getSupportedProtocols()).thenReturn(of(new MockScmProtocol("http", "http://"), new MockScmProtocol("ssh", "ssh://")));
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo");
MockHttpResponse response = new MockHttpResponse();
@@ -372,6 +386,28 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
assertTrue(response.getContentAsString().contains("\"protocol\":[{\"href\":\"http://\",\"name\":\"http\"},{\"href\":\"ssh://\",\"name\":\"ssh\"}]"));
}
@Test
public void shouldRenameRepository() throws Exception {
String namespace = "space";
String name = "repo";
Repository repository1 = mockRepository(namespace, name);
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository1);
URL url = Resources.getResource("sonia/scm/api/v2/rename-repo.json");
byte[] repository = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/rename")
.contentType(VndMediaType.REPOSITORY)
.content(repository);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(SC_NO_CONTENT, response.getStatus());
verify(repositoryManager).rename(repository1, "space", "x");
}
private PageResult<Repository> createSingletonPageResult(Repository repository) {
return new PageResult<>(singletonList(repository), 0);
}

View File

@@ -27,7 +27,6 @@ package sonia.scm.api.v2.resources;
import sonia.scm.repository.RepositoryManager;
import static com.google.inject.util.Providers.of;
import static org.mockito.Mockito.mock;
abstract class RepositoryTestBase {
@@ -47,7 +46,6 @@ abstract class RepositoryTestBase {
RepositoryCollectionResource repositoryCollectionResource;
AnnotateResource annotateResource;
RepositoryRootResource getRepositoryRootResource() {
RepositoryBasedResourceProvider repositoryBasedResourceProvider = new RepositoryBasedResourceProvider(
of(tagRootResource),
@@ -66,8 +64,7 @@ abstract class RepositoryTestBase {
repositoryToDtoMapper,
dtoToRepositoryMapper,
manager,
repositoryBasedResourceProvider
)),
repositoryBasedResourceProvider)),
of(repositoryCollectionResource));
}
}

View File

@@ -21,11 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet;
import org.apache.shiro.util.ThreadContext;
import org.junit.After;
import org.junit.Before;
@@ -33,7 +34,10 @@ import org.junit.Rule;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.CustomNamespaceStrategy;
import sonia.scm.repository.HealthCheckFailure;
import sonia.scm.repository.NamespaceStrategy;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.RepositoryService;
@@ -41,13 +45,16 @@ import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.api.ScmProtocol;
import java.net.URI;
import java.util.Set;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static java.util.stream.Stream.of;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@@ -72,6 +79,10 @@ public class RepositoryToRepositoryDtoMapperTest {
private ScmPathInfoStore scmPathInfoStore;
@Mock
private ScmPathInfo uriInfo;
@Mock
private ScmConfiguration configuration;
@Mock
private Set<NamespaceStrategy> strategies;
@InjectMocks
private RepositoryToRepositoryDtoMapperImpl mapper;
@@ -83,7 +94,9 @@ public class RepositoryToRepositoryDtoMapperTest {
when(repositoryService.isSupported(any(Command.class))).thenReturn(true);
when(repositoryService.getSupportedProtocols()).thenReturn(of());
when(scmPathInfoStore.get()).thenReturn(uriInfo);
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y"));
doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator();
}
@After
@@ -129,6 +142,23 @@ public class RepositoryToRepositoryDtoMapperTest {
dto.getLinks().getLinkBy("update").get().getHref());
}
@Test
public void shouldCreateRenameLink() {
when(configuration.getNamespaceStrategy()).thenReturn("test");
RepositoryDto dto = mapper.map(createTestRepository());
assertEquals(
"http://example.com/base/v2/repositories/testspace/test/rename",
dto.getLinks().getLinkBy("rename").get().getHref());
}
@Test
public void shouldCreateRenameWithNamespaceLink() {
RepositoryDto dto = mapper.map(createTestRepository());
assertEquals(
"http://example.com/base/v2/repositories/testspace/test/rename",
dto.getLinks().getLinkBy("renameWithNamespace").get().getHref());
}
@Test
public void shouldMapHealthCheck() {
RepositoryDto dto = mapper.map(createTestRepository());

View File

@@ -43,6 +43,7 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
@@ -107,15 +108,17 @@ class XmlSecurityV1UpdateStepTest {
@Nested
class WithExistingSecurityXml {
private Path configDir;
@BeforeEach
void createSecurityV1XML(@TempDir Path tempDir) throws IOException {
Path configDir = tempDir.resolve("config");
configDir = tempDir.resolve("config");
Files.createDirectories(configDir);
copyTestDatabaseFile(configDir, "securityV1.xml");
}
@Test
void shouldMapV1PermissionsFromSecurityV1XML() throws JAXBException {
void shouldMapV1PermissionsFromSecurityV1XML() throws IOException, JAXBException {
copyTestDatabaseFile(configDir, "securityV1.xml");
updateStep.doUpdate();
List<String> assignedPermission =
assignedPermissionStore.getAll().values()
@@ -127,15 +130,27 @@ class XmlSecurityV1UpdateStepTest {
assertThat(assignedPermission).contains("test");
}
@Test
void shouldNotFailOnEmptyV1SecurityXml() throws IOException, JAXBException {
copyTestDatabaseFile(configDir, "emptySecurityV1.xml", "securityV1.xml");
updateStep.doUpdate();
assertThat(assignedPermissionStore.getAll()).isEmpty();
}
}
private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException {
URL url = Resources.getResource("sonia/scm/update/security/" + fileName);
Files.copy(url.openStream(), configDir.resolve(fileName));
copyTestDatabaseFile(configDir, fileName, fileName);
}
private void copyTestDatabaseFile(Path configDir, String sourceFileName, String targetFileName) throws IOException {
URL url = Resources.getResource("sonia/scm/update/security/" + sourceFileName);
Files.copy(url.openStream(), configDir.resolve(targetFileName));
}
@Test
void shouldNotFailForMissingConfigDir() throws JAXBException {
updateStep.doUpdate();
assertThat(assignedPermissionStore.getAll()).isEmpty();
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "x",
"namespace": "space"
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" ?>
<!--
MIT License
Copyright (c) 2020-present Cloudogu GmbH and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<configuration>
</configuration>