Merge pull request #1460 from scm-manager/feature/import_git_from_url
Feature/import git from url
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Healthcheck for docker image ([#1428](https://github.com/scm-manager/scm-manager/issues/1428) and [#1454](https://github.com/scm-manager/scm-manager/issues/1454))
|
||||
- Tags can now be added and deleted through the ui ([#1456](https://github.com/scm-manager/scm-manager/pull/1456))
|
||||
- The ui now displays tag signatures ([#1456](https://github.com/scm-manager/scm-manager/pull/1456))
|
||||
- Repository import via URL for git ([#1460](https://github.com/scm-manager/scm-manager/pull/1460))
|
||||
|
||||
### Changed
|
||||
- Send mercurial hook callbacks over separate tcp socket instead of http ([#1416](https://github.com/scm-manager/scm-manager/pull/1416))
|
||||
|
||||
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 34 KiB |
BIN
docs/de/user/repo/assets/import-repository.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 38 KiB |
@@ -35,10 +35,19 @@ Im SCM-Manager können neue Git, Mercurial & Subersion (SVN) Repositories über
|
||||
Optional kann man das Repository beim Erstellen direkt initialisieren. Damit werden für Git und Mercurial jeweils der Standard-Branch (master bzw. default) angelegt. Außerdem wird ein initialer Commit ausgeführt, der eine README.md erzeugt.
|
||||
Für Subversion Repositories wird die README.md in einen Ordner `trunk` abgelegt.
|
||||
|
||||
Ist die Namespace-Strategie auf "Benutzerdefiniert" eingestellt, muss noch ein Namespace eingetragen werden. Für den Namespace gelten dieselben Regeln wie für den Namen des Repositories. Darüber hinaus darf ein Namespace nicht nur aus bis zu drei Ziffern (z. B. "123") oder dem Wort "create" bestehen.
|
||||
Ist die Namespace-Strategie auf "Benutzerdefiniert" eingestellt, muss noch ein Namespace eingetragen werden. Für den Namespace gelten dieselben Regeln wie für den Namen des Repositories. Darüber hinaus darf ein Namespace nicht nur aus bis zu drei Ziffern (z. B. "123") oder den Wörter "create" und "import" bestehen.
|
||||
|
||||

|
||||
|
||||
### Repository importieren
|
||||
Neben dem Erstellen von neuen Repository können auch bestehende Repository in SCM-Manager importiert werden.
|
||||
Wechseln Sie über den Schalter oben rechts auf die Importseite und füllen Sie die benötigten Informationen aus.
|
||||
|
||||
Das gewählte Repository wird zum SCM-Manager hinzugefügt und sämtliche Repository Daten inklusive aller Branches und Tags werden importiert.
|
||||
|
||||

|
||||
|
||||
|
||||
### Repository Informationen
|
||||
Die Informationsseite eines Repository zeigt die Metadaten zum Repository an. Darunter befinden sich Beschreibungen zu den unterschiedlichen Möglichkeiten wie man mit diesem Repository arbeiten kann. In der Überschrift kann der Namespace angeklickt werden, um alle Repositories aus diesem Namespace anzuzeigen.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 32 KiB |
BIN
docs/en/user/repo/assets/import-repository.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 36 KiB |
@@ -28,15 +28,23 @@ Icon | Description
|
||||
Clicking the icon on the right-hand side of each namespace caption, you can change additional settings for this namespace.
|
||||
|
||||
### Create a Repository
|
||||
In SCM-Manager new Git, Mercurial & Subversion (SVN) repositories can be created via a form that can be accessed via the "Create Repository" button. A valid name and the repository type are mandatory.
|
||||
In SCM-Manager new Git, Mercurial & Subversion (SVN) repositories can be created via a form that can be accessed via the "Create Repository" button. A valid name and the repository type are mandatory.
|
||||
|
||||
Optionally, repositories can be initialized during the creation. That creates a standard branch (master or default) for Git and Mercurial repositories.
|
||||
Additionally, it performs a commit that creates a README.md. For Subversion repositories the README.md will be created in a directory named `trunk`.
|
||||
|
||||
If the namespace strategy is set to custom, the namespace field is also mandatory. The namespace must heed the same restrictions as the name. Additionally, namespaces that only consist of three digits, or the word "create" are not valid.
|
||||
If the namespace strategy is set to custom, the namespace field is also mandatory. The namespace must heed the same restrictions as the name. Additionally, namespaces that only consist of three digits, or the words "create" and "import" are not valid.
|
||||
|
||||

|
||||
|
||||
### Import a Repository
|
||||
Beneath creating new repositories you also may import existing repositories to SCM-Manager.
|
||||
Just use the Switcher on top right to navigate to the import page and fill the import wizard with the required information.
|
||||
|
||||
Your repository will be added to SCM-Manager and all repository data including all branches and tags will be imported.
|
||||
|
||||

|
||||
|
||||
### Repository Information
|
||||
The information screen of repositories shows meta data about the repository. Amongst that are descriptions for the different options on how the repository can be used. In the heading you can click the namespace to get the list of all repositories for this namespace.
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ import java.util.List;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.12
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class AbstactImportHandler implements AdvancedImportHandler
|
||||
{
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ package sonia.scm.repository;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.43
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public interface AdvancedImportHandler extends ImportHandler
|
||||
{
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
@@ -34,19 +34,18 @@ import java.util.List;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.12
|
||||
* @deprecated
|
||||
*/
|
||||
public interface ImportHandler
|
||||
{
|
||||
@Deprecated
|
||||
public interface ImportHandler {
|
||||
|
||||
/**
|
||||
* Import existing and non managed repositories.
|
||||
*
|
||||
*
|
||||
* @param manager The global {@link RepositoryManager}
|
||||
*
|
||||
*
|
||||
* @return a {@link List} names of imported repositories
|
||||
* @throws IOException
|
||||
* @deprecated
|
||||
*/
|
||||
public List<String> importRepositories(RepositoryManager manager) throws IOException;
|
||||
}
|
||||
|
||||
@@ -39,10 +39,12 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.43
|
||||
* @deprecated
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
@Deprecated
|
||||
public final class ImportResult {
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,9 +49,11 @@ public interface RepositoryHandler
|
||||
*
|
||||
* @return {@link ImportHandler} for the repository type of this handler
|
||||
* @since 1.12
|
||||
* @deprecated
|
||||
*
|
||||
* @throws FeatureNotSupportedException
|
||||
*/
|
||||
@Deprecated
|
||||
public ImportHandler getImportHandler() throws FeatureNotSupportedException;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.event.Event;
|
||||
|
||||
/**
|
||||
* Event which is fired whenever repository import is successful or failed.
|
||||
*
|
||||
* @since 2.11.0
|
||||
*/
|
||||
@Event
|
||||
@Getter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class RepositoryImportEvent extends RepositoryEvent {
|
||||
|
||||
private final boolean failed;
|
||||
|
||||
public RepositoryImportEvent(HandlerEventType eventType, Repository repository, boolean failed) {
|
||||
super(eventType, repository);
|
||||
this.failed = failed;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import sonia.scm.TypeManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* The central class for managing {@link Repository} objects.
|
||||
@@ -96,4 +97,20 @@ public interface RepositoryManager
|
||||
* @return all namespaces
|
||||
*/
|
||||
Collection<String> getAllNamespaces();
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new repository and afterwards executes the logic from the {@param afterCreation}.
|
||||
*
|
||||
* @param repository the repository to create
|
||||
* @param afterCreation consumer which is executed after the repository was created
|
||||
* @return created repository
|
||||
*
|
||||
* @since 2.11.0
|
||||
*/
|
||||
default Repository create(Repository repository, Consumer<Repository> afterCreation) {
|
||||
Repository newRepository = create(repository);
|
||||
afterCreation.accept(newRepository);
|
||||
return newRepository;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.ExceptionWithContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This exception is thrown if the repository import fails.
|
||||
*
|
||||
* @since 2.11.0
|
||||
*/
|
||||
public class ImportFailedException extends ExceptionWithContext {
|
||||
|
||||
private static final String CODE = "D6SHRfqQw1";
|
||||
|
||||
public ImportFailedException(List<ContextEntry> context, String message, Exception cause) {
|
||||
super(context, message, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return CODE;
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,8 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
@@ -38,50 +36,62 @@ import sonia.scm.repository.spi.PullCommandRequest;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The pull command pull changes from a other repository.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.31
|
||||
*/
|
||||
public final class PullCommandBuilder
|
||||
{
|
||||
public final class PullCommandBuilder {
|
||||
|
||||
/**
|
||||
* the logger for PullCommandBuilder
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(PullCommandBuilder.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(PullCommandBuilder.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
private final PullCommand command;
|
||||
private final Repository localRepository;
|
||||
private final PullCommandRequest request = new PullCommandRequest();
|
||||
|
||||
/**
|
||||
* Constructs a new PullCommandBuilder.
|
||||
*
|
||||
*
|
||||
* @param command pull command implementation
|
||||
* @param command pull command implementation
|
||||
* @param localRepository local repository
|
||||
*/
|
||||
PullCommandBuilder(PullCommand command, Repository localRepository)
|
||||
{
|
||||
PullCommandBuilder(PullCommand command, Repository localRepository) {
|
||||
this.command = command;
|
||||
this.localRepository = localRepository;
|
||||
request.reset();
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
/**
|
||||
* Set username for authentication
|
||||
*
|
||||
* @param username username
|
||||
* @return this builder instance.
|
||||
* @since 2.11.0
|
||||
*/
|
||||
public PullCommandBuilder withUsername(String username) {
|
||||
request.setUsername(username);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password for authentication
|
||||
*
|
||||
* @param password password
|
||||
* @return this builder instance.
|
||||
* @since 2.11.0
|
||||
*/
|
||||
public PullCommandBuilder withPassword(String password) {
|
||||
request.setPassword(password);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull all changes from the given remote url.
|
||||
*
|
||||
*
|
||||
* @param url remote url
|
||||
*
|
||||
* @return informations over the executed pull command
|
||||
*
|
||||
* @throws IOException
|
||||
*
|
||||
* @since 1.43
|
||||
*/
|
||||
public PullResponse pull(String url) throws IOException {
|
||||
@@ -89,24 +99,20 @@ public final class PullCommandBuilder
|
||||
//J-
|
||||
subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
|
||||
//J+
|
||||
|
||||
|
||||
URL remoteUrl = new URL(url);
|
||||
request.reset();
|
||||
request.setRemoteUrl(remoteUrl);
|
||||
|
||||
|
||||
logger.info("pull changes from url {}", url);
|
||||
|
||||
|
||||
return command.pull(request);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pull all changes from the given remote repository.
|
||||
*
|
||||
*
|
||||
* @param remoteRepository remote repository
|
||||
*
|
||||
* @return informations over the executed pull command
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public PullResponse pull(Repository remoteRepository) throws IOException {
|
||||
@@ -124,15 +130,4 @@ public final class PullCommandBuilder
|
||||
|
||||
return command.pull(request);
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** pull command implementation */
|
||||
private PullCommand command;
|
||||
|
||||
/** local repository */
|
||||
private Repository localRepository;
|
||||
|
||||
/** pull command request */
|
||||
private PullCommandRequest request = new PullCommandRequest();
|
||||
}
|
||||
|
||||
@@ -21,57 +21,31 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.31
|
||||
*/
|
||||
public abstract class RemoteCommandRequest implements Resetable
|
||||
{
|
||||
@Getter
|
||||
@Setter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
public abstract class RemoteCommandRequest implements Resetable {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getClass() != obj.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
final RemoteCommandRequest other = (RemoteCommandRequest) obj;
|
||||
|
||||
return Objects.equal(remoteRepository, other.remoteRepository)
|
||||
&& Objects.equal(remoteUrl, other.remoteUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(remoteRepository, remoteUrl);
|
||||
}
|
||||
protected Repository remoteRepository;
|
||||
protected URL remoteUrl;
|
||||
protected String username;
|
||||
protected String password;
|
||||
|
||||
/**
|
||||
* Resets the request object.
|
||||
@@ -79,82 +53,10 @@ public abstract class RemoteCommandRequest implements Resetable
|
||||
* @since 1.43
|
||||
*/
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
public void reset() {
|
||||
remoteRepository = null;
|
||||
remoteUrl = null;
|
||||
username = null;
|
||||
password = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
//J-
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("remoteRepository", remoteRepository)
|
||||
.add("remoteUrl", remoteUrl)
|
||||
.toString();
|
||||
//J+
|
||||
}
|
||||
|
||||
//~--- set methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
* @param remoteRepository
|
||||
*/
|
||||
public void setRemoteRepository(Repository remoteRepository)
|
||||
{
|
||||
this.remoteRepository = remoteRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param remoteUrl
|
||||
*
|
||||
* @since 1.43
|
||||
*/
|
||||
public void setRemoteUrl(URL remoteUrl)
|
||||
{
|
||||
this.remoteUrl = remoteUrl;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Repository getRemoteRepository()
|
||||
{
|
||||
return remoteRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @since 1.43
|
||||
*/
|
||||
URL getRemoteUrl()
|
||||
{
|
||||
return remoteUrl;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
protected Repository remoteRepository;
|
||||
|
||||
/** remote url */
|
||||
protected URL remoteUrl;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public final class AuthorizationChangedEvent {
|
||||
* @return {@code true} if every user is affected
|
||||
*/
|
||||
public boolean isEveryUserAffected(){
|
||||
return nameOfAffectedUser != null;
|
||||
return nameOfAffectedUser == null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 --------------------------------------------------------
|
||||
@@ -30,16 +30,20 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @deprecated
|
||||
*/
|
||||
public class GitImportHandler extends AbstactImportHandler
|
||||
{
|
||||
@Deprecated
|
||||
public class GitImportHandler extends AbstactImportHandler {
|
||||
|
||||
/** Field description */
|
||||
/**
|
||||
* Field description
|
||||
*/
|
||||
public static final String GIT_DIR = ".git";
|
||||
|
||||
/** Field description */
|
||||
/**
|
||||
* Field description
|
||||
*/
|
||||
public static final String GIT_DIR_REFS = "refs";
|
||||
|
||||
/**
|
||||
@@ -53,11 +57,9 @@ public class GitImportHandler extends AbstactImportHandler
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param handler
|
||||
*/
|
||||
public GitImportHandler(GitRepositoryHandler handler)
|
||||
{
|
||||
public GitImportHandler(GitRepositoryHandler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
@@ -66,29 +68,27 @@ public class GitImportHandler extends AbstactImportHandler
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected String[] getDirectoryNames()
|
||||
{
|
||||
return new String[] { GIT_DIR, GIT_DIR_REFS };
|
||||
protected String[] getDirectoryNames() {
|
||||
return new String[]{GIT_DIR, GIT_DIR_REFS};
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected AbstractRepositoryHandler<?> getRepositoryHandler()
|
||||
{
|
||||
protected AbstractRepositoryHandler<?> getRepositoryHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
/**
|
||||
* Field description
|
||||
*/
|
||||
private GitRepositoryHandler handler;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ package sonia.scm.repository.spi;
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Iterables;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
@@ -36,33 +37,34 @@ import org.eclipse.jgit.transport.FetchResult;
|
||||
import org.eclipse.jgit.transport.RefSpec;
|
||||
import org.eclipse.jgit.transport.TagOpt;
|
||||
import org.eclipse.jgit.transport.TrackingRefUpdate;
|
||||
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.ImportFailedException;
|
||||
import sonia.scm.repository.api.PullResponse;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class GitPullCommand extends AbstractGitPushOrPullCommand
|
||||
implements PullCommand
|
||||
{
|
||||
implements PullCommand {
|
||||
|
||||
/** Field description */
|
||||
/**
|
||||
* Field description
|
||||
*/
|
||||
private static final String REF_SPEC = "refs/heads/*:refs/heads/*";
|
||||
|
||||
/** Field description */
|
||||
/**
|
||||
* Field description
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(GitPullCommand.class);
|
||||
|
||||
@@ -71,12 +73,11 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
* @param handler
|
||||
* @param handler
|
||||
* @param context
|
||||
*/
|
||||
@Inject
|
||||
public GitPullCommand(GitRepositoryHandler handler, GitContext context)
|
||||
{
|
||||
public GitPullCommand(GitRepositoryHandler handler, GitContext context) {
|
||||
super(handler, context);
|
||||
}
|
||||
|
||||
@@ -85,30 +86,21 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public PullResponse pull(PullCommandRequest request)
|
||||
throws IOException
|
||||
{
|
||||
throws IOException {
|
||||
PullResponse response;
|
||||
Repository sourceRepository = request.getRemoteRepository();
|
||||
|
||||
if (sourceRepository != null)
|
||||
{
|
||||
if (sourceRepository != null) {
|
||||
response = pullFromScmRepository(sourceRepository);
|
||||
}
|
||||
else if (request.getRemoteUrl() != null)
|
||||
{
|
||||
response = pullFromUrl(request.getRemoteUrl());
|
||||
}
|
||||
else
|
||||
{
|
||||
} else if (request.getRemoteUrl() != null) {
|
||||
response = pullFromUrl(request);
|
||||
} else {
|
||||
throw new IllegalArgumentException("repository or url is required");
|
||||
}
|
||||
|
||||
@@ -118,8 +110,7 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
|
||||
private PullResponse convert(Git git, FetchResult fetch) {
|
||||
long counter = 0l;
|
||||
|
||||
for (TrackingRefUpdate tru : fetch.getTrackingRefUpdates())
|
||||
{
|
||||
for (TrackingRefUpdate tru : fetch.getTrackingRefUpdates()) {
|
||||
counter += count(git, tru);
|
||||
}
|
||||
|
||||
@@ -131,52 +122,40 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param git
|
||||
* @param tru
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private long count(Git git, TrackingRefUpdate tru)
|
||||
{
|
||||
private long count(Git git, TrackingRefUpdate tru) {
|
||||
long counter = 0;
|
||||
|
||||
if (GitUtil.isHead(tru.getLocalName()))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (GitUtil.isHead(tru.getLocalName())) {
|
||||
try {
|
||||
org.eclipse.jgit.api.LogCommand log = git.log();
|
||||
|
||||
ObjectId oldId = tru.getOldObjectId();
|
||||
|
||||
if (GitUtil.isValidObjectId(oldId))
|
||||
{
|
||||
if (GitUtil.isValidObjectId(oldId)) {
|
||||
log.not(oldId);
|
||||
}
|
||||
|
||||
ObjectId newId = tru.getNewObjectId();
|
||||
|
||||
if (GitUtil.isValidObjectId(newId))
|
||||
{
|
||||
if (GitUtil.isValidObjectId(newId)) {
|
||||
log.add(newId);
|
||||
}
|
||||
|
||||
Iterable<RevCommit> commits = log.call();
|
||||
|
||||
if (commits != null)
|
||||
{
|
||||
if (commits != null) {
|
||||
counter += Iterables.size(commits);
|
||||
}
|
||||
|
||||
logger.trace("counting {} commits for ref update {}", counter, tru);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
} catch (Exception ex) {
|
||||
logger.error("could not count pushed/pulled changesets", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
logger.debug("do not count non branch ref update {}", tru);
|
||||
}
|
||||
|
||||
@@ -184,8 +163,7 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
|
||||
}
|
||||
|
||||
private PullResponse pullFromScmRepository(Repository sourceRepository)
|
||||
throws IOException
|
||||
{
|
||||
throws IOException {
|
||||
File sourceDirectory = handler.getDirectory(sourceRepository.getId());
|
||||
|
||||
Preconditions.checkArgument(sourceDirectory.exists(),
|
||||
@@ -203,42 +181,46 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
|
||||
|
||||
org.eclipse.jgit.lib.Repository source = null;
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
source = Git.open(sourceDirectory).getRepository();
|
||||
response = new PullResponse(push(source, getRemoteUrl(targetDirectory)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
} finally {
|
||||
GitUtil.close(source);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private PullResponse pullFromUrl(URL url)
|
||||
throws IOException
|
||||
{
|
||||
logger.debug("pull changes from {} to {}", url, repository.getId());
|
||||
private PullResponse pullFromUrl(PullCommandRequest request)
|
||||
throws IOException {
|
||||
logger.debug("pull changes from {} to {}", request.getRemoteUrl(), repository.getId());
|
||||
|
||||
PullResponse response;
|
||||
Git git = Git.wrap(open());
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
//J-
|
||||
FetchResult result = git.fetch()
|
||||
.setCredentialsProvider(
|
||||
new UsernamePasswordCredentialsProvider(
|
||||
Strings.nullToEmpty(request.getUsername()),
|
||||
Strings.nullToEmpty(request.getPassword())
|
||||
)
|
||||
)
|
||||
.setRefSpecs(new RefSpec(REF_SPEC))
|
||||
.setRemote(url.toExternalForm())
|
||||
.setRemote(request.getRemoteUrl().toExternalForm())
|
||||
.setTagOpt(TagOpt.FETCH_TAGS)
|
||||
.call();
|
||||
//J+
|
||||
|
||||
response = convert(git, result);
|
||||
}
|
||||
catch (GitAPIException ex)
|
||||
{
|
||||
throw new InternalRepositoryException(repository, "error during pull", ex);
|
||||
} catch
|
||||
(GitAPIException ex) {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Repository import failed. The credentials are wrong or missing.",
|
||||
ex
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -41,7 +41,9 @@ import java.io.IOException;
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public class HgImportHandler extends AbstactImportHandler
|
||||
{
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ package sonia.scm.repository;
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public class SvnImportHandler extends AbstactImportHandler
|
||||
{
|
||||
|
||||
|
||||
@@ -46248,6 +46248,7 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Default 1`] = `
|
||||
>
|
||||
<input
|
||||
className="input"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
placeholder=""
|
||||
@@ -46271,6 +46272,7 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Default 1`] = `
|
||||
>
|
||||
<input
|
||||
className="input"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
placeholder=""
|
||||
@@ -46324,6 +46326,7 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Disabled 1`] = `
|
||||
<input
|
||||
className="input"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
placeholder=""
|
||||
@@ -46348,6 +46351,7 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Disabled 1`] = `
|
||||
<input
|
||||
className="input"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
placeholder=""
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { ChangeEvent, KeyboardEvent } from "react";
|
||||
import React, { ChangeEvent, KeyboardEvent, FocusEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
||||
import { createAttributesForTesting } from "../devBuild";
|
||||
@@ -41,6 +41,7 @@ type Props = {
|
||||
helpText?: string;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
onBlur?: (value: string, name?: string) => void;
|
||||
};
|
||||
|
||||
class InputField extends React.Component<Props> {
|
||||
@@ -61,6 +62,12 @@ class InputField extends React.Component<Props> {
|
||||
this.props.onChange(event.target.value, this.props.name);
|
||||
};
|
||||
|
||||
handleBlur = (event: FocusEvent<HTMLInputElement>) => {
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(event.target.value, this.props.name);
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
const onReturnPressed = this.props.onReturnPressed;
|
||||
if (!onReturnPressed) {
|
||||
@@ -102,6 +109,7 @@ class InputField extends React.Component<Props> {
|
||||
onChange={this.handleInput}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
disabled={disabled}
|
||||
onBlur={this.handleBlur}
|
||||
{...createAttributesForTesting(testId)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,7 @@ const MarginLeft = styled.div`
|
||||
const FlexContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 2.25rem;
|
||||
`;
|
||||
|
||||
export default class Page extends React.Component<Props> {
|
||||
|
||||
@@ -138,3 +138,87 @@ describe("test path validation", () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("test url validation", () => {
|
||||
const invalid = [
|
||||
"http://",
|
||||
"http://.",
|
||||
"http://..",
|
||||
"http://../",
|
||||
"http://?",
|
||||
"http://??",
|
||||
"http://??/",
|
||||
"http://#",
|
||||
"http://##",
|
||||
"http://##/",
|
||||
"http://foo.bar?q=Spaces should be encoded",
|
||||
"//",
|
||||
"//a",
|
||||
"///a",
|
||||
"///",
|
||||
"foo.com",
|
||||
"http:// shouldfail.com",
|
||||
":// should fail",
|
||||
"http://foo.bar/foo(bar)baz quux",
|
||||
"http://.www.foo.bar/",
|
||||
"http://.www.foo.bar./"
|
||||
];
|
||||
for (const url of invalid) {
|
||||
it(`should return false for '${url}'`, () => {
|
||||
expect(validator.isUrlValid(url)).toBe(false);
|
||||
});
|
||||
}
|
||||
const valid = [
|
||||
"ftps://foo.bar/",
|
||||
"h://test",
|
||||
"rdar://1234",
|
||||
"file:///blah/index.html",
|
||||
"https://foo.com/blah_blah",
|
||||
"ssh://foo.com/blah_blah",
|
||||
"https://foo.com/blah_blah/",
|
||||
"https://foo.com/blah_blah_(wikipedia)",
|
||||
"https://foo.com/blah_blah_(wikipedia)_(again)",
|
||||
"http://www.example.com/wpstyle/?p=364",
|
||||
"http://foo.com/blah_blah",
|
||||
"http://foo.com/blah_blah/",
|
||||
"http://foo.com/blah_blah_(wikipedia)",
|
||||
"http://foo.com/blah_blah_(wikipedia)_(again)",
|
||||
"http://www.example.com/wpstyle/?p=364",
|
||||
"https://www.example.com/foo/?bar=baz&inga=42&quux",
|
||||
"http://✪df.ws/123",
|
||||
"http://userid:password@example.com:8080",
|
||||
"http://userid:password@example.com:8080/",
|
||||
"http://userid@example.com",
|
||||
"http://userid@example.com/",
|
||||
"http://userid@example.com:8080",
|
||||
"http://userid@example.com:8080/",
|
||||
"http://userid:password@example.com",
|
||||
"http://userid:password@example.com/",
|
||||
"http://142.42.1.1/",
|
||||
"http://142.42.1.1:8080/",
|
||||
"http://➡.ws/䨹",
|
||||
"http://⌘.ws",
|
||||
"http://⌘.ws/",
|
||||
"http://foo.com/blah_(wikipedia)#cite-1",
|
||||
"http://foo.com/blah_(wikipedia)_blah#cite-1",
|
||||
"http://foo.com/unicode_(✪)_in_parens",
|
||||
"http://foo.com/(something)?after=parens",
|
||||
"http://☺.damowmow.com/",
|
||||
"http://code.google.com/events/#&product=browser",
|
||||
"http://j.mp",
|
||||
"http://foo.bar/?q=Test%20URL-encoded%20stuff",
|
||||
"http://مثال.إختبار",
|
||||
"http://例子.测试",
|
||||
"http://उदाहरण.परीक्षा",
|
||||
"http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com",
|
||||
"http://1337.net",
|
||||
"http://a.b-c.de",
|
||||
"http://223.255.255.254",
|
||||
"http://0.0.0.0"
|
||||
];
|
||||
for (const url of valid) {
|
||||
it(`should return true for '${url}'`, () => {
|
||||
expect(validator.isUrlValid(url)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,3 +49,9 @@ const pathRegex = /^((?!\/{2,}).)*$/;
|
||||
export const isPathValid = (path: string) => {
|
||||
return pathRegex.test(path);
|
||||
};
|
||||
|
||||
const urlRegex = /^[A-Za-z0-9]+:\/\/[^\s$.?#].[^\s]*$/;
|
||||
|
||||
export const isUrlValid = (url: string) => {
|
||||
return urlRegex.test(url);
|
||||
};
|
||||
|
||||
@@ -39,6 +39,12 @@ export type RepositoryCreation = Repository & {
|
||||
contextEntries: { [key: string]: any };
|
||||
};
|
||||
|
||||
export type RepositoryUrlImport = Repository & {
|
||||
importUrl: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export type Namespace = {
|
||||
namespace: string;
|
||||
_links: Links;
|
||||
|
||||
@@ -22,11 +22,12 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Collection } from "./hal";
|
||||
import { Collection, Links } from "./hal";
|
||||
|
||||
export type RepositoryType = {
|
||||
name: string;
|
||||
displayName: string;
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
export type RepositoryTypeCollection = Collection & {
|
||||
|
||||
@@ -35,7 +35,8 @@ export {
|
||||
RepositoryGroup,
|
||||
RepositoryCreation,
|
||||
Namespace,
|
||||
NamespaceCollection
|
||||
NamespaceCollection,
|
||||
RepositoryUrlImport
|
||||
} from "./Repositories";
|
||||
export { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"namespace-invalid": "Der Namespace des Repository ist ungültig",
|
||||
"name-invalid": "Der Name des Repository ist ungültig",
|
||||
"contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein",
|
||||
"url-invalid": "Die URL ist ungültig",
|
||||
"branch": {
|
||||
"nameInvalid": "Der Name des Branches ist ungültig"
|
||||
}
|
||||
@@ -22,7 +23,10 @@
|
||||
"typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).",
|
||||
"contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.",
|
||||
"descriptionHelpText": "Eine kurze Beschreibung des Repository.",
|
||||
"initializeRepository": "Erstellt einen ersten Branch und committet eine README.md."
|
||||
"initializeRepository": "Erstellt einen ersten Branch und committet eine README.md.",
|
||||
"importUrlHelpText": "Importiert das gesamte Repository inkl. aller Branches und Tags über die Remote URL.",
|
||||
"usernameHelpText": "Benutzername könnte für den Import benötigt werden. Wird ignoriert, falls nicht gesetzt.",
|
||||
"passwordHelpText": "Password könnte für den Import benötigt werden. Wird ignoriert, falls nicht gesetzt."
|
||||
},
|
||||
"repositoryRoot": {
|
||||
"errorTitle": "Fehler",
|
||||
@@ -49,13 +53,30 @@
|
||||
"title": "Repositories",
|
||||
"subtitle": "Übersicht aller verfügbaren Repositories",
|
||||
"noRepositories": "Keine Repositories gefunden.",
|
||||
"createButton": "Repository erstellen",
|
||||
"createButton": "Repository hinzufügen",
|
||||
"searchRepository": "Repository suchen",
|
||||
"allNamespaces": "Alle Namespaces"
|
||||
},
|
||||
"create": {
|
||||
"title": "Repository erstellen",
|
||||
"subtitle": "Erstellen eines neuen Repository"
|
||||
"title": "Repository hinzufügen",
|
||||
"subtitle": "Neues Repository erstellen"
|
||||
},
|
||||
"import": {
|
||||
"subtitle": "Bestehendes Repository importieren",
|
||||
"importUrl": "Remote Repository URL",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"pending": {
|
||||
"subtitle": "Repository wird importiert...",
|
||||
"infoText": "Ihr Repository wird gerade importiert. Dies kann einen Moment dauern. Sie werden weitergeleitet, sobald der Import abgeschlossen ist. Wenn Sie diese Seite verlassen, können Sie nicht zurückkehren, um den Import-Status zu erfahren."
|
||||
},
|
||||
"importTypes": {
|
||||
"label": "Import Modus",
|
||||
"url": {
|
||||
"label": "Import via URL",
|
||||
"helpText": "Das Repository wird über eine URL importiert."
|
||||
}
|
||||
}
|
||||
},
|
||||
"branches": {
|
||||
"overview": {
|
||||
@@ -200,9 +221,12 @@
|
||||
},
|
||||
"repositoryForm": {
|
||||
"subtitle": "Repository bearbeiten",
|
||||
"submit": "Speichern",
|
||||
"submitCreate": "Speichern",
|
||||
"submitImport": "Importieren",
|
||||
"initializeRepository": "Repository initiieren",
|
||||
"dangerZone": "Umbenennen und Löschen"
|
||||
"dangerZone": "Umbenennen und Löschen",
|
||||
"createButton": "Neues Repository erstellen",
|
||||
"importButton": "Repository importieren"
|
||||
},
|
||||
"sources": {
|
||||
"fileTree": {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"namespace-invalid": "The repository namespace is invalid",
|
||||
"name-invalid": "The repository name is invalid",
|
||||
"contact-invalid": "Contact must be a valid mail address",
|
||||
"url-invalid": "The URL is invalid",
|
||||
"branch": {
|
||||
"nameInvalid": "The branch name is invalid"
|
||||
}
|
||||
@@ -22,7 +23,10 @@
|
||||
"typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).",
|
||||
"contactHelpText": "Email address of the person who is responsible for this repository.",
|
||||
"descriptionHelpText": "A short description of the repository.",
|
||||
"initializeRepository": "Creates an initial branch and commits a basic README.md."
|
||||
"initializeRepository": "Creates an initial branch and commits a basic README.md.",
|
||||
"importUrlHelpText": "Import the whole repository including all branches and tags via remote url",
|
||||
"usernameHelpText": "Username may be required to import the remote repository. Will be ignored if not provided.",
|
||||
"passwordHelpText": "Password may be required to import the remote repository. Will be ignored if not provided."
|
||||
},
|
||||
"repositoryRoot": {
|
||||
"errorTitle": "Error",
|
||||
@@ -49,13 +53,31 @@
|
||||
"title": "Repositories",
|
||||
"subtitle": "Overview of available repositories",
|
||||
"noRepositories": "No repositories found.",
|
||||
"createButton": "Create Repository",
|
||||
"createButton": "Add Repository",
|
||||
"searchRepository": "Search repository",
|
||||
"allNamespaces": "All namespaces"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Repository",
|
||||
"subtitle": "Create a new repository"
|
||||
"title": "Add Repository",
|
||||
"subtitle": "Create a new repository",
|
||||
"createButton": "Create new repository"
|
||||
},
|
||||
"import": {
|
||||
"subtitle": "Import existing repository",
|
||||
"importUrl": "Remote repository url",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"pending": {
|
||||
"subtitle": "Importing Repository...",
|
||||
"infoText": "Your repository is currently being imported. This may take a moment. You will be forwarded as soon as the import is finished. If you leave this page you cannot return to find out the import status."
|
||||
},
|
||||
"importTypes": {
|
||||
"label": "Import Mode",
|
||||
"url": {
|
||||
"label": "Import via URL",
|
||||
"helpText": "The Repository will be imported via the provided URL."
|
||||
}
|
||||
}
|
||||
},
|
||||
"branches": {
|
||||
"overview": {
|
||||
@@ -200,9 +222,12 @@
|
||||
},
|
||||
"repositoryForm": {
|
||||
"subtitle": "Edit Repository",
|
||||
"submit": "Save",
|
||||
"submitCreate": "Save",
|
||||
"submitImport": "Import",
|
||||
"initializeRepository": "Initialize repository",
|
||||
"dangerZone": "Rename and delete"
|
||||
"dangerZone": "Rename and delete",
|
||||
"createButton": "Create Repository",
|
||||
"importButton": "Import repository"
|
||||
},
|
||||
"sources": {
|
||||
"fileTree": {
|
||||
|
||||
@@ -31,13 +31,13 @@ import Users from "../users/containers/Users";
|
||||
import Login from "../containers/Login";
|
||||
import Logout from "../containers/Logout";
|
||||
|
||||
import { ProtectedRoute, ErrorBoundary } from "@scm-manager/ui-components";
|
||||
import { ErrorBoundary, ProtectedRoute } from "@scm-manager/ui-components";
|
||||
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
|
||||
import CreateUser from "../users/containers/CreateUser";
|
||||
import SingleUser from "../users/containers/SingleUser";
|
||||
import RepositoryRoot from "../repos/containers/RepositoryRoot";
|
||||
import Create from "../repos/containers/Create";
|
||||
import CreateRepository from "../repos/containers/CreateRepository";
|
||||
|
||||
import Groups from "../groups/containers/Groups";
|
||||
import SingleGroup from "../groups/containers/SingleGroup";
|
||||
@@ -47,6 +47,7 @@ import Admin from "../admin/containers/Admin";
|
||||
|
||||
import Profile from "./Profile";
|
||||
import NamespaceRoot from "../repos/namespaces/containers/NamespaceRoot";
|
||||
import ImportRepository from "../repos/containers/ImportRepository";
|
||||
|
||||
type Props = {
|
||||
me: Me;
|
||||
@@ -77,7 +78,8 @@ class Main extends React.Component<Props> {
|
||||
<Route path="/logout" component={Logout} />
|
||||
<Redirect exact strict from="/repos" to="/repos/" />
|
||||
<ProtectedRoute exact path="/repos/" component={Overview} authenticated={authenticated} />
|
||||
<ProtectedRoute exact path="/repos/create" component={Create} authenticated={authenticated} />
|
||||
<ProtectedRoute exact path="/repos/create" component={CreateRepository} authenticated={authenticated} />
|
||||
<ProtectedRoute exact path="/repos/import" component={ImportRepository} authenticated={authenticated} />
|
||||
<ProtectedRoute exact path="/repos/:namespace" component={Overview} authenticated={authenticated} />
|
||||
<ProtectedRoute exact path="/repos/:namespace/:page" component={Overview} authenticated={authenticated} />
|
||||
<ProtectedRoute path="/repo/:namespace/:name" component={RepositoryRoot} authenticated={authenticated} />
|
||||
|
||||
104
scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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, useState } from "react";
|
||||
import { RepositoryUrlImport } from "@scm-manager/ui-types";
|
||||
import { InputField, validation } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
repository: RepositoryUrlImport;
|
||||
onChange: (repository: RepositoryUrlImport) => void;
|
||||
setValid: (valid: boolean) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const Column = styled.div`
|
||||
padding: 0 0.75rem;
|
||||
`;
|
||||
|
||||
const Columns = styled.div`
|
||||
padding: 0.75rem 0 0;
|
||||
`;
|
||||
|
||||
const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid, disabled }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [urlValidationError, setUrlValidationError] = useState(false);
|
||||
|
||||
const handleImportUrlChange = (importUrl: string) => {
|
||||
onChange({ ...repository, importUrl });
|
||||
const valid = validation.isUrlValid(importUrl);
|
||||
setUrlValidationError(!valid);
|
||||
setValid(valid);
|
||||
};
|
||||
|
||||
const handleImportUrlBlur = (importUrl: string) => {
|
||||
if (!repository.name) {
|
||||
// If the repository name is not fill we set a name suggestion
|
||||
const match = importUrl.match(/([^\/]+?)(?:.git)?$/);
|
||||
if (match && match[1]) {
|
||||
onChange({ ...repository, name: match[1] });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Columns className="columns is-multiline">
|
||||
<Column className="column is-full">
|
||||
<InputField
|
||||
label={t("import.importUrl")}
|
||||
onChange={handleImportUrlChange}
|
||||
value={repository.importUrl}
|
||||
helpText={t("help.importUrlHelpText")}
|
||||
validationError={urlValidationError}
|
||||
errorMessage={t("validation.url-invalid")}
|
||||
disabled={disabled}
|
||||
onBlur={handleImportUrlBlur}
|
||||
/>
|
||||
</Column>
|
||||
<Column className="column is-half">
|
||||
<InputField
|
||||
label={t("import.username")}
|
||||
onChange={username => onChange({ ...repository, username })}
|
||||
value={repository.username}
|
||||
helpText={t("help.usernameHelpText")}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Column>
|
||||
<Column className="column is-half">
|
||||
<InputField
|
||||
label={t("import.password")}
|
||||
onChange={password => onChange({ ...repository, password })}
|
||||
value={repository.password}
|
||||
type="password"
|
||||
helpText={t("help.passwordHelpText")}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Column>
|
||||
</Columns>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportFromUrlForm;
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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, FormEvent, useState } from "react";
|
||||
import NamespaceAndNameFields from "./NamespaceAndNameFields";
|
||||
import { Repository, RepositoryUrlImport } from "@scm-manager/ui-types";
|
||||
import ImportFromUrlForm from "./ImportFromUrlForm";
|
||||
import RepositoryInformationForm from "./RepositoryInformationForm";
|
||||
import { apiClient, ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
repositoryType: string;
|
||||
setImportPending: (pending: boolean) => void;
|
||||
};
|
||||
|
||||
const ImportRepositoryFromUrl: FC<Props> = ({ url, repositoryType, setImportPending }) => {
|
||||
const [repo, setRepo] = useState<RepositoryUrlImport>({
|
||||
name: "",
|
||||
namespace: "",
|
||||
type: repositoryType,
|
||||
contact: "",
|
||||
description: "",
|
||||
importUrl: "",
|
||||
username: "",
|
||||
password: "",
|
||||
_links: {}
|
||||
});
|
||||
|
||||
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, importUrl: false });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>();
|
||||
const history = useHistory();
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const isValid = () => Object.values(valid).every(v => v);
|
||||
|
||||
const handleImportLoading = (loading: boolean) => {
|
||||
setImportPending(loading);
|
||||
setLoading(loading);
|
||||
};
|
||||
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError(undefined);
|
||||
const currentPath = history.location.pathname;
|
||||
handleImportLoading(true);
|
||||
apiClient
|
||||
.post(url, repo, "application/vnd.scmm-repository+json;v=2")
|
||||
.then(response => {
|
||||
const location = response.headers.get("Location");
|
||||
return apiClient.get(location!);
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(repo => {
|
||||
if (history.location.pathname === currentPath) {
|
||||
history.push(`/repo/${repo.namespace}/${repo.name}/code/sources`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error);
|
||||
handleImportLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
<ErrorNotification error={error} />
|
||||
<ImportFromUrlForm
|
||||
repository={repo}
|
||||
onChange={setRepo}
|
||||
setValid={(importUrl: boolean) => setValid({ ...valid, importUrl })}
|
||||
disabled={loading}
|
||||
/>
|
||||
<hr />
|
||||
<NamespaceAndNameFields
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>}
|
||||
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
|
||||
disabled={loading}
|
||||
/>
|
||||
<RepositoryInformationForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>}
|
||||
disabled={loading}
|
||||
setValid={(contact: boolean) => setValid({ ...valid, contact })}
|
||||
/>
|
||||
<Level
|
||||
right={<SubmitButton disabled={!isValid()} loading={loading} label={t("repositoryForm.submitImport")} />}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportRepositoryFromUrl;
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 { RepositoryType } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Select } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
repositoryTypes: RepositoryType[];
|
||||
repositoryType?: RepositoryType;
|
||||
setRepositoryType: (repositoryType: RepositoryType) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const ImportRepositoryTypeSelect: FC<Props> = ({ repositoryTypes, repositoryType, setRepositoryType, disabled }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const createSelectOptions = () => {
|
||||
const options = repositoryTypes
|
||||
.filter(repoType => !!repoType._links.import)
|
||||
.map(repositoryType => {
|
||||
return {
|
||||
label: repositoryType.displayName,
|
||||
value: repositoryType.name
|
||||
};
|
||||
});
|
||||
options.unshift({ label: "", value: "" });
|
||||
return options;
|
||||
};
|
||||
|
||||
const onChangeType = (type: string) => {
|
||||
const repositoryType = repositoryTypes.filter(t => t.name === type)[0];
|
||||
setRepositoryType(repositoryType);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={t("repository.type")}
|
||||
onChange={onChangeType}
|
||||
value={repositoryType ? repositoryType.name : ""}
|
||||
options={createSelectOptions()}
|
||||
helpText={t("help.typeHelpText")}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportRepositoryTypeSelect;
|
||||
64
scm-ui/ui-webapp/src/repos/components/ImportTypeSelect.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { Link, RepositoryType } from "@scm-manager/ui-types";
|
||||
import { LabelWithHelpIcon, Radio } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
repositoryType: RepositoryType;
|
||||
importType: string;
|
||||
setImportType: (type: string) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const ImportTypeSelect: FC<Props> = ({ repositoryType, importType, setImportType, disabled }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const changeImportType = (checked: boolean, name?: string) => {
|
||||
if (name && checked) {
|
||||
setImportType(name);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LabelWithHelpIcon label={t("import.importTypes.label")} key="import.importTypes.label" />
|
||||
{(repositoryType._links.import as Link[]).map((type, index) => (
|
||||
<Radio
|
||||
name={type.name}
|
||||
checked={importType === type.name}
|
||||
value={type.name}
|
||||
label={t(`import.importTypes.${type.name}.label`)}
|
||||
helpText={t(`import.importTypes.${type.name}.helpText`)}
|
||||
onChange={changeImportType}
|
||||
key={index}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportTypeSelect;
|
||||
119
scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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 { Repository } from "@scm-manager/ui-types";
|
||||
import { CUSTOM_NAMESPACE_STRATEGY } from "../modules/repos";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { InputField } from "@scm-manager/ui-components";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import * as validator from "./form/repositoryValidation";
|
||||
import { getNamespaceStrategies } from "../../admin/modules/namespaceStrategies";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
onChange: (repository: Repository) => void;
|
||||
namespaceStrategy: string;
|
||||
setValid: (valid: boolean) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, namespaceStrategy, setValid, disabled }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [namespaceValidationError, setNamespaceValidationError] = useState(false);
|
||||
const [nameValidationError, setNameValidationError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (repository.name) {
|
||||
const nameValid = validator.isNameValid(repository.name);
|
||||
setNameValidationError(!nameValid);
|
||||
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
|
||||
if (repository.namespace) {
|
||||
const namespaceValid = validator.isNamespaceValid(repository.namespace);
|
||||
setValid(!(!namespaceValid || !nameValid));
|
||||
} else {
|
||||
setValid(false);
|
||||
}
|
||||
} else {
|
||||
setValid(nameValid);
|
||||
}
|
||||
} else {
|
||||
setValid(false);
|
||||
}
|
||||
}, [repository.name, repository.namespace]);
|
||||
|
||||
const handleNamespaceChange = (namespace: string) => {
|
||||
const valid = validator.isNamespaceValid(namespace);
|
||||
setNamespaceValidationError(!valid);
|
||||
onChange({ ...repository, namespace });
|
||||
};
|
||||
|
||||
const handleNameChange = (name: string) => {
|
||||
const valid = validator.isNameValid(name);
|
||||
setNameValidationError(!valid);
|
||||
onChange({ ...repository, name });
|
||||
};
|
||||
|
||||
const renderNamespaceField = () => {
|
||||
const props = {
|
||||
label: t("repository.namespace"),
|
||||
helpText: t("help.namespaceHelpText"),
|
||||
value: repository ? repository.namespace : "",
|
||||
onChange: handleNamespaceChange,
|
||||
errorMessage: t("validation.namespace-invalid"),
|
||||
validationError: namespaceValidationError,
|
||||
disabled: disabled
|
||||
};
|
||||
|
||||
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
|
||||
return <InputField {...props} />;
|
||||
}
|
||||
|
||||
return <ExtensionPoint name="repos.create.namespace" props={props} renderAll={false} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderNamespaceField()}
|
||||
<InputField
|
||||
label={t("repository.name")}
|
||||
onChange={handleNameChange}
|
||||
value={repository ? repository.name : ""}
|
||||
validationError={nameValidationError}
|
||||
errorMessage={t("validation.name-invalid")}
|
||||
helpText={t("help.nameHelpText")}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any) => {
|
||||
const namespaceStrategy = getNamespaceStrategies(state).current;
|
||||
return {
|
||||
namespaceStrategy
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(NamespaceAndNameFields);
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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, useState } from "react";
|
||||
import { InputField, Textarea } from "@scm-manager/ui-components";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as validator from "./form/repositoryValidation";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
onChange: (repository: Repository) => void;
|
||||
disabled: boolean;
|
||||
setValid: (valid: boolean) => void;
|
||||
};
|
||||
|
||||
const RepositoryInformationForm: FC<Props> = ({ repository, onChange, disabled, setValid }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [contactValidationError, setContactValidationError] = useState(false);
|
||||
|
||||
const handleContactChange = (contact: string) => {
|
||||
const valid = validator.isContactValid(contact);
|
||||
setContactValidationError(!valid);
|
||||
setValid(valid);
|
||||
onChange({ ...repository, contact });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputField
|
||||
label={t("repository.contact")}
|
||||
onChange={handleContactChange}
|
||||
value={repository ? repository.contact : ""}
|
||||
validationError={contactValidationError}
|
||||
errorMessage={t("validation.contact-invalid")}
|
||||
helpText={t("help.contactHelpText")}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Textarea
|
||||
label={t("repository.description")}
|
||||
onChange={description => onChange({ ...repository, description })}
|
||||
value={repository ? repository.description : ""}
|
||||
helpText={t("help.descriptionHelpText")}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryInformationForm;
|
||||
@@ -21,14 +21,15 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { Repository, RepositoryCreation, RepositoryType } from "@scm-manager/ui-types";
|
||||
import { Checkbox, InputField, Level, Select, SubmitButton, Subtitle, Textarea } from "@scm-manager/ui-components";
|
||||
import * as validator from "./repositoryValidation";
|
||||
import { Repository, RepositoryType } from "@scm-manager/ui-types";
|
||||
import { Checkbox, Level, Select, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { CUSTOM_NAMESPACE_STRATEGY } from "../../modules/repos";
|
||||
import NamespaceAndNameFields from "../NamespaceAndNameFields";
|
||||
import RepositoryInformationForm from "../RepositoryInformationForm";
|
||||
|
||||
const CheckboxWrapper = styled.div`
|
||||
margin-top: 2em;
|
||||
@@ -44,150 +45,71 @@ const SpaceBetween = styled.div`
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
type Props = WithTranslation & {
|
||||
submitForm: (repo: RepositoryCreation, shouldInit: boolean) => void;
|
||||
type Props = {
|
||||
createRepository?: (repo: RepositoryCreation, shouldInit: boolean) => void;
|
||||
modifyRepository?: (repo: Repository) => void;
|
||||
repository?: Repository;
|
||||
repositoryTypes?: RepositoryType[];
|
||||
namespaceStrategy?: string;
|
||||
loading?: boolean;
|
||||
indexResources: any;
|
||||
loading: boolean;
|
||||
indexResources?: any;
|
||||
};
|
||||
|
||||
type State = {
|
||||
repository: RepositoryCreation;
|
||||
initRepository: boolean;
|
||||
namespaceValidationError: boolean;
|
||||
nameValidationError: boolean;
|
||||
contactValidationError: boolean;
|
||||
type RepositoryCreation = Repository & {
|
||||
contextEntries: object;
|
||||
};
|
||||
|
||||
class RepositoryForm extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const RepositoryForm: FC<Props> = ({
|
||||
createRepository,
|
||||
modifyRepository,
|
||||
repository,
|
||||
repositoryTypes,
|
||||
namespaceStrategy,
|
||||
loading,
|
||||
indexResources
|
||||
}) => {
|
||||
const [repo, setRepo] = useState<Repository>({
|
||||
name: "",
|
||||
namespace: "",
|
||||
type: "",
|
||||
contact: "",
|
||||
description: "",
|
||||
_links: {}
|
||||
});
|
||||
const [initRepository, setInitRepository] = useState(false);
|
||||
const [contextEntries, setContextEntries] = useState({});
|
||||
const [valid, setValid] = useState({ namespaceAndName: false, contact: true });
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
this.state = {
|
||||
repository: {
|
||||
name: "",
|
||||
namespace: "",
|
||||
type: "",
|
||||
contact: "",
|
||||
description: "",
|
||||
contextEntries: {},
|
||||
_links: {}
|
||||
},
|
||||
initRepository: false,
|
||||
namespaceValidationError: false,
|
||||
nameValidationError: false,
|
||||
contactValidationError: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { repository } = this.props;
|
||||
useEffect(() => {
|
||||
if (repository) {
|
||||
this.setState({
|
||||
repository: {
|
||||
...repository,
|
||||
contextEntries: {}
|
||||
}
|
||||
});
|
||||
setRepo({ ...repository });
|
||||
}
|
||||
}
|
||||
}, [repository]);
|
||||
|
||||
isFalsy(value: string) {
|
||||
return !value;
|
||||
}
|
||||
|
||||
isValid = () => {
|
||||
const { namespaceStrategy } = this.props;
|
||||
const { repository } = this.state;
|
||||
return !(
|
||||
this.state.namespaceValidationError ||
|
||||
this.state.nameValidationError ||
|
||||
this.state.contactValidationError ||
|
||||
this.isFalsy(repository.name) ||
|
||||
(namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY && this.isFalsy(repository.namespace))
|
||||
);
|
||||
};
|
||||
|
||||
submit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (this.isValid()) {
|
||||
this.props.submitForm(this.state.repository, this.state.initRepository);
|
||||
}
|
||||
};
|
||||
|
||||
isCreateMode = () => {
|
||||
return !this.props.repository;
|
||||
};
|
||||
|
||||
isModifiable = () => {
|
||||
return !!this.props.repository && !!this.props.repository._links.update;
|
||||
};
|
||||
|
||||
toggleInitCheckbox = () => {
|
||||
this.setState({
|
||||
initRepository: !this.state.initRepository
|
||||
});
|
||||
};
|
||||
|
||||
setCreationContextEntry = (key: string, value: any) => {
|
||||
this.setState({
|
||||
repository: {
|
||||
...this.state.repository,
|
||||
contextEntries: {
|
||||
...this.state.repository.contextEntries,
|
||||
[key]: value
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, t } = this.props;
|
||||
const repository = this.state.repository;
|
||||
|
||||
const disabled = !this.isModifiable() && !this.isCreateMode();
|
||||
|
||||
const submitButton = disabled ? null : (
|
||||
<Level right={<SubmitButton disabled={!this.isValid()} loading={loading} label={t("repositoryForm.submit")} />} />
|
||||
);
|
||||
|
||||
let subtitle = null;
|
||||
if (this.props.repository) {
|
||||
// edit existing repo
|
||||
subtitle = <Subtitle subtitle={t("repositoryForm.subtitle")} />;
|
||||
}
|
||||
const isEditMode = () => !!repository;
|
||||
const isModifiable = () => !!repository && !!repository._links.update;
|
||||
const disabled = (!isModifiable() && isEditMode()) || loading;
|
||||
|
||||
const isValid = () => {
|
||||
return (
|
||||
<>
|
||||
{subtitle}
|
||||
<form onSubmit={this.submit}>
|
||||
{this.renderCreateOnlyFields()}
|
||||
<InputField
|
||||
label={t("repository.contact")}
|
||||
onChange={this.handleContactChange}
|
||||
value={repository ? repository.contact : ""}
|
||||
validationError={this.state.contactValidationError}
|
||||
errorMessage={t("validation.contact-invalid")}
|
||||
helpText={t("help.contactHelpText")}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label={t("repository.description")}
|
||||
onChange={this.handleDescriptionChange}
|
||||
value={repository ? repository.description : ""}
|
||||
helpText={t("help.descriptionHelpText")}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{submitButton}
|
||||
</form>
|
||||
</>
|
||||
!(!repo.name || (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY && !repo.namespace)) &&
|
||||
Object.values(valid).every(v => v)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
createSelectOptions(repositoryTypes?: RepositoryType[]) {
|
||||
const submit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (isValid()) {
|
||||
if (createRepository) {
|
||||
createRepository({ ...repo, contextEntries }, initRepository);
|
||||
} else if (modifyRepository) {
|
||||
modifyRepository(repo);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createSelectOptions = (repositoryTypes?: RepositoryType[]) => {
|
||||
if (repositoryTypes) {
|
||||
return repositoryTypes.map(repositoryType => {
|
||||
return {
|
||||
@@ -197,122 +119,87 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
renderNamespaceField = () => {
|
||||
const { namespaceStrategy, t } = this.props;
|
||||
const repository = this.state.repository;
|
||||
const props = {
|
||||
label: t("repository.namespace"),
|
||||
helpText: t("help.namespaceHelpText"),
|
||||
value: repository ? repository.namespace : "",
|
||||
onChange: this.handleNamespaceChange,
|
||||
errorMessage: t("validation.namespace-invalid"),
|
||||
validationError: this.state.namespaceValidationError
|
||||
};
|
||||
|
||||
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
|
||||
return <InputField {...props} />;
|
||||
}
|
||||
|
||||
return <ExtensionPoint name="repos.create.namespace" props={props} renderAll={false} />;
|
||||
};
|
||||
|
||||
renderCreateOnlyFields() {
|
||||
if (!this.isCreateMode()) {
|
||||
const renderCreateOnlyFields = () => {
|
||||
if (isEditMode()) {
|
||||
return null;
|
||||
}
|
||||
const { repositoryTypes, indexResources, t } = this.props;
|
||||
const repository = this.state.repository;
|
||||
const extensionProps = {
|
||||
repository,
|
||||
setCreationContextEntry: this.setCreationContextEntry,
|
||||
repository: repo,
|
||||
setCreationContextEntry: setCreationContextEntry,
|
||||
indexResources
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{this.renderNamespaceField()}
|
||||
<InputField
|
||||
label={t("repository.name")}
|
||||
onChange={this.handleNameChange}
|
||||
value={repository ? repository.name : ""}
|
||||
validationError={this.state.nameValidationError}
|
||||
errorMessage={t("validation.name-invalid")}
|
||||
helpText={t("help.nameHelpText")}
|
||||
<NamespaceAndNameFields
|
||||
repository={repo}
|
||||
onChange={setRepo}
|
||||
setValid={namespaceAndName => setValid({ ...valid, namespaceAndName })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<SpaceBetween>
|
||||
<SelectWrapper>
|
||||
<Select
|
||||
label={t("repository.type")}
|
||||
onChange={this.handleTypeChange}
|
||||
value={repository ? repository.type : ""}
|
||||
options={this.createSelectOptions(repositoryTypes)}
|
||||
onChange={type => setRepo({ ...repo, type })}
|
||||
value={repo ? repo.type : ""}
|
||||
options={createSelectOptions(repositoryTypes)}
|
||||
helpText={t("help.typeHelpText")}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</SelectWrapper>
|
||||
<CheckboxWrapper>
|
||||
<Checkbox
|
||||
label={t("repositoryForm.initializeRepository")}
|
||||
checked={this.state.initRepository}
|
||||
onChange={this.toggleInitCheckbox}
|
||||
checked={initRepository}
|
||||
onChange={toggleInitCheckbox}
|
||||
helpText={t("help.initializeRepository")}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{this.state.initRepository && (
|
||||
{initRepository && (
|
||||
<ExtensionPoint name="repos.create.initialize" props={extensionProps} renderAll={true} />
|
||||
)}
|
||||
</CheckboxWrapper>
|
||||
</SpaceBetween>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleNamespaceChange = (namespace: string) => {
|
||||
this.setState({
|
||||
namespaceValidationError: !validator.isNamespaceValid(namespace),
|
||||
repository: {
|
||||
...this.state.repository,
|
||||
namespace
|
||||
}
|
||||
const toggleInitCheckbox = () => {
|
||||
setInitRepository(!initRepository);
|
||||
};
|
||||
|
||||
const setCreationContextEntry = (key: string, value: any) => {
|
||||
setContextEntries({
|
||||
...contextEntries,
|
||||
[key]: value
|
||||
});
|
||||
};
|
||||
|
||||
handleNameChange = (name: string) => {
|
||||
this.setState({
|
||||
nameValidationError: !validator.isNameValid(name),
|
||||
repository: {
|
||||
...this.state.repository,
|
||||
name
|
||||
}
|
||||
});
|
||||
const submitButton = () => {
|
||||
if (disabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Level
|
||||
right={<SubmitButton disabled={!isValid()} loading={loading} label={t("repositoryForm.submitCreate")} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
handleTypeChange = (type: string) => {
|
||||
this.setState({
|
||||
repository: {
|
||||
...this.state.repository,
|
||||
type
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
{renderCreateOnlyFields()}
|
||||
<RepositoryInformationForm
|
||||
repository={repo}
|
||||
onChange={setRepo}
|
||||
disabled={disabled}
|
||||
setValid={contact => setValid({ ...valid, contact })}
|
||||
/>
|
||||
{submitButton()}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
handleContactChange = (contact: string) => {
|
||||
this.setState({
|
||||
contactValidationError: !validator.isContactValid(contact),
|
||||
repository: {
|
||||
...this.state.repository,
|
||||
contact
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleDescriptionChange = (description: string) => {
|
||||
this.setState({
|
||||
repository: {
|
||||
...this.state.repository,
|
||||
description
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default withTranslation("repos")(RepositoryForm);
|
||||
export default RepositoryForm;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 styled from "styled-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, ButtonAddons, Icon, Level } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
creationMode: "CREATE" | "IMPORT";
|
||||
};
|
||||
|
||||
const MarginIcon = styled(Icon)`
|
||||
padding-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const SmallButton = styled(Button)`
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const TopLevel = styled(Level)`
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: -1.5rem;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@media (max-width: 785px) {
|
||||
margin-top: 4.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const RepositoryFormSwitcher: FC<Props> = ({ creationMode }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const isImportMode = () => {
|
||||
return creationMode === "IMPORT";
|
||||
};
|
||||
|
||||
const isCreateMode = () => {
|
||||
return creationMode === "CREATE";
|
||||
};
|
||||
|
||||
return (
|
||||
<TopLevel
|
||||
right={
|
||||
<ButtonAddons>
|
||||
<SmallButton
|
||||
color={isCreateMode() ? "link is-selected" : undefined}
|
||||
link={isImportMode() ? "/repos/create" : undefined}
|
||||
>
|
||||
<MarginIcon name="fa fa-plus" color={isCreateMode() ? "white" : "default"} />
|
||||
<p className="is-hidden-mobile is-hidden-tablet-only">{t("repositoryForm.createButton")}</p>
|
||||
</SmallButton>
|
||||
<SmallButton
|
||||
color={isImportMode() ? "link is-selected" : undefined}
|
||||
link={isCreateMode() ? "/repos/import" : undefined}
|
||||
className="has-text-left-desktop"
|
||||
>
|
||||
<MarginIcon name="fa fa-file-upload" color={isImportMode() ? "white" : "default"} />
|
||||
<p className="is-hidden-mobile is-hidden-tablet-only">{t("repositoryForm.importButton")}</p>
|
||||
</SmallButton>
|
||||
</ButtonAddons>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryFormSwitcher;
|
||||
@@ -25,7 +25,7 @@
|
||||
import { validation } from "@scm-manager/ui-components";
|
||||
|
||||
const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[.]git$)(?!.*[\\\[\]])^[A-Za-z0-9\.][A-Za-z0-9\.\-_]*$/;
|
||||
const namespaceExceptionsRegex = /^(([0-9]{1,3})|(create))$/;
|
||||
const namespaceExceptionsRegex = /^(([0-9]{1,3})|(create)|(import))$/;
|
||||
|
||||
export const isNamespaceValid = (name: string) => {
|
||||
return nameRegex.test(name) && !namespaceExceptionsRegex.test(name);
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
isFetchRepositoryTypesPending
|
||||
} from "../modules/repositoryTypes";
|
||||
import RepositoryForm from "../components/form";
|
||||
import RepositoryFormSwitcher from "../components/form/RepositoryFormSwitcher";
|
||||
import { createRepo, createRepoReset, getCreateRepoFailure, isCreateRepoPending } from "../modules/repos";
|
||||
import { getRepositoriesLink } from "../../modules/indexResource";
|
||||
import {
|
||||
@@ -42,32 +43,35 @@ import {
|
||||
getNamespaceStrategies,
|
||||
isFetchNamespaceStrategiesPending
|
||||
} from "../../admin/modules/namespaceStrategies";
|
||||
import { RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
repositoryTypes: RepositoryType[];
|
||||
namespaceStrategies: NamespaceStrategies;
|
||||
pageLoading: boolean;
|
||||
createLoading: boolean;
|
||||
error: Error;
|
||||
repoLink: string;
|
||||
indexResources: any;
|
||||
type Props = WithTranslation &
|
||||
RouteComponentProps & {
|
||||
repositoryTypes: RepositoryType[];
|
||||
namespaceStrategies: NamespaceStrategies;
|
||||
pageLoading: boolean;
|
||||
createLoading: boolean;
|
||||
error: Error;
|
||||
repoLink: string;
|
||||
indexResources: any;
|
||||
|
||||
// dispatch functions
|
||||
fetchNamespaceStrategiesIfNeeded: () => void;
|
||||
fetchRepositoryTypesIfNeeded: () => void;
|
||||
createRepo: (
|
||||
link: string,
|
||||
repository: RepositoryCreation,
|
||||
initRepository: boolean,
|
||||
callback: (repo: Repository) => void
|
||||
) => void;
|
||||
resetForm: () => void;
|
||||
// dispatch functions
|
||||
fetchNamespaceStrategiesIfNeeded: () => void;
|
||||
fetchRepositoryTypesIfNeeded: () => void;
|
||||
createRepo: (
|
||||
link: string,
|
||||
repository: RepositoryCreation,
|
||||
initRepository: boolean,
|
||||
callback: (repo: Repository) => void
|
||||
) => void;
|
||||
resetForm: () => void;
|
||||
|
||||
// context props
|
||||
history: History;
|
||||
};
|
||||
// context props
|
||||
history: History;
|
||||
};
|
||||
|
||||
class Create extends React.Component<Props> {
|
||||
class CreateRepository extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.resetForm();
|
||||
this.props.fetchRepositoryTypesIfNeeded();
|
||||
@@ -75,19 +79,27 @@ class Create extends React.Component<Props> {
|
||||
}
|
||||
|
||||
repoCreated = (repo: Repository) => {
|
||||
const { history } = this.props;
|
||||
|
||||
history.push("/repo/" + repo.namespace + "/" + repo.name);
|
||||
this.props.history.push("/repo/" + repo.namespace + "/" + repo.name);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { pageLoading, createLoading, repositoryTypes, namespaceStrategies, createRepo, error, indexResources } = this.props;
|
||||
const {
|
||||
pageLoading,
|
||||
createLoading,
|
||||
repositoryTypes,
|
||||
namespaceStrategies,
|
||||
createRepo,
|
||||
error,
|
||||
indexResources,
|
||||
repoLink,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const { t, repoLink } = this.props;
|
||||
return (
|
||||
<Page
|
||||
title={t("create.title")}
|
||||
subtitle={t("create.subtitle")}
|
||||
afterTitle={<RepositoryFormSwitcher creationMode={"CREATE"} />}
|
||||
loading={pageLoading}
|
||||
error={error}
|
||||
showContentOnError={true}
|
||||
@@ -96,7 +108,7 @@ class Create extends React.Component<Props> {
|
||||
repositoryTypes={repositoryTypes}
|
||||
loading={createLoading}
|
||||
namespaceStrategy={namespaceStrategies.current}
|
||||
submitForm={(repo, initRepository) => {
|
||||
createRepository={(repo, initRepository) => {
|
||||
createRepo(repoLink, repo, initRepository, (repo: Repository) => this.repoCreated(repo));
|
||||
}}
|
||||
indexResources={indexResources}
|
||||
@@ -144,4 +156,8 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation("repos")(Create));
|
||||
export default compose(
|
||||
withRouter,
|
||||
withTranslation("repos"),
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(CreateRepository);
|
||||
@@ -28,25 +28,27 @@ import RepositoryForm from "../components/form";
|
||||
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 { ErrorNotification, Subtitle } from "@scm-manager/ui-components";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { compose } from "redux";
|
||||
import RepositoryDangerZone from "./RepositoryDangerZone";
|
||||
import { getLinks } from "../../modules/indexResource";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
import { TranslationProps, withTranslation } from "react-i18next";
|
||||
|
||||
type Props = RouteComponentProps & {
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
indexLinks: Links;
|
||||
type Props = TranslationProps &
|
||||
RouteComponentProps & {
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
indexLinks: Links;
|
||||
|
||||
modifyRepo: (p1: Repository, p2: () => void) => void;
|
||||
modifyRepoReset: (p: Repository) => void;
|
||||
modifyRepo: (p1: Repository, p2: () => void) => void;
|
||||
modifyRepoReset: (p: Repository) => void;
|
||||
|
||||
// context props
|
||||
repository: Repository;
|
||||
history: History;
|
||||
};
|
||||
// context props
|
||||
repository: Repository;
|
||||
history: History;
|
||||
};
|
||||
|
||||
class EditRepo extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
@@ -60,7 +62,7 @@ class EditRepo extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error, repository, indexLinks } = this.props;
|
||||
const { loading, error, repository, indexLinks, t } = this.props;
|
||||
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
@@ -71,11 +73,12 @@ class EditRepo extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("repositoryForm.subtitle")} />
|
||||
<ErrorNotification error={error} />
|
||||
<RepositoryForm
|
||||
repository={this.props.repository}
|
||||
loading={loading}
|
||||
submitForm={repo => {
|
||||
modifyRepository={repo => {
|
||||
this.props.modifyRepo(repo, this.repoModified);
|
||||
}}
|
||||
/>
|
||||
@@ -110,4 +113,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(connect(mapStateToProps, mapDispatchToProps), withRouter)(EditRepo);
|
||||
export default compose(connect(mapStateToProps, mapDispatchToProps), withRouter)(withTranslation("repos")(EditRepo));
|
||||
|
||||
157
scm-ui/ui-webapp/src/repos/containers/ImportRepository.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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, RepositoryType } from "@scm-manager/ui-types";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ImportRepositoryTypeSelect from "../components/ImportRepositoryTypeSelect";
|
||||
import ImportTypeSelect from "../components/ImportTypeSelect";
|
||||
import ImportRepositoryFromUrl from "../components/ImportRepositoryFromUrl";
|
||||
import { Loading, Notification, Page } from "@scm-manager/ui-components";
|
||||
import RepositoryFormSwitcher from "../components/form/RepositoryFormSwitcher";
|
||||
import {
|
||||
fetchRepositoryTypesIfNeeded,
|
||||
getFetchRepositoryTypesFailure,
|
||||
getRepositoryTypes,
|
||||
isFetchRepositoryTypesPending
|
||||
} from "../modules/repositoryTypes";
|
||||
import { connect } from "react-redux";
|
||||
import { fetchNamespaceStrategiesIfNeeded } from "../../admin/modules/namespaceStrategies";
|
||||
|
||||
type Props = {
|
||||
repositoryTypes: RepositoryType[];
|
||||
pageLoading: boolean;
|
||||
error?: Error;
|
||||
fetchRepositoryTypesIfNeeded: () => void;
|
||||
fetchNamespaceStrategiesIfNeeded: () => void;
|
||||
};
|
||||
|
||||
const ImportPendingLoading = ({ importPending }: { importPending: boolean }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
if (!importPending) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Notification type="info">{t("import.pending.infoText")}</Notification>
|
||||
<Loading />
|
||||
<hr />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportRepository: FC<Props> = ({
|
||||
repositoryTypes,
|
||||
pageLoading,
|
||||
error,
|
||||
fetchRepositoryTypesIfNeeded,
|
||||
fetchNamespaceStrategiesIfNeeded
|
||||
}) => {
|
||||
const [importPending, setImportPending] = useState(false);
|
||||
const [repositoryType, setRepositoryType] = useState<RepositoryType | undefined>();
|
||||
const [importType, setImportType] = useState("");
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
useEffect(() => {
|
||||
fetchRepositoryTypesIfNeeded();
|
||||
fetchNamespaceStrategiesIfNeeded();
|
||||
}, [repositoryTypes]);
|
||||
|
||||
const changeRepositoryType = (repositoryType: RepositoryType) => {
|
||||
setRepositoryType(repositoryType);
|
||||
setImportType(repositoryType?._links ? ((repositoryType!._links?.import as Link[])[0] as Link).name! : "");
|
||||
};
|
||||
|
||||
const renderImportComponent = () => {
|
||||
if (importType === "url") {
|
||||
return (
|
||||
<ImportRepositoryFromUrl
|
||||
url={((repositoryType!._links.import as Link[])!.find((link: Link) => link.name === "url") as Link).href}
|
||||
repositoryType={repositoryType!.name}
|
||||
setImportPending={setImportPending}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error("Unknown import type");
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={t("create.title")}
|
||||
subtitle={t("import.subtitle")}
|
||||
afterTitle={<RepositoryFormSwitcher creationMode={"IMPORT"} />}
|
||||
loading={pageLoading}
|
||||
error={error}
|
||||
showContentOnError={true}
|
||||
>
|
||||
<ImportPendingLoading importPending={importPending} />
|
||||
<ImportRepositoryTypeSelect
|
||||
repositoryTypes={repositoryTypes}
|
||||
repositoryType={repositoryType}
|
||||
setRepositoryType={changeRepositoryType}
|
||||
disabled={importPending}
|
||||
/>
|
||||
{repositoryType && (
|
||||
<>
|
||||
<hr />
|
||||
<ImportTypeSelect
|
||||
repositoryType={repositoryType}
|
||||
importType={importType}
|
||||
setImportType={setImportType}
|
||||
disabled={importPending}
|
||||
/>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
{importType && renderImportComponent()}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any) => {
|
||||
const repositoryTypes = getRepositoryTypes(state);
|
||||
const pageLoading = isFetchRepositoryTypesPending(state);
|
||||
const error = getFetchRepositoryTypesFailure(state);
|
||||
|
||||
return {
|
||||
repositoryTypes,
|
||||
pageLoading,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchRepositoryTypesIfNeeded: () => {
|
||||
dispatch(fetchRepositoryTypesIfNeeded());
|
||||
},
|
||||
fetchNamespaceStrategiesIfNeeded: () => {
|
||||
dispatch(fetchNamespaceStrategiesIfNeeded());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ImportRepository);
|
||||
@@ -25,7 +25,7 @@ import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { NamespaceCollection, RepositoryCollection } from "@scm-manager/ui-types";
|
||||
import { NamespaceCollection, RepositoryCollection, Link } from "@scm-manager/ui-types";
|
||||
import {
|
||||
CreateButton,
|
||||
LinkPaginator,
|
||||
@@ -95,7 +95,8 @@ class Overview extends React.Component<Props> {
|
||||
getReposLink = () => {
|
||||
const { namespace, namespaces, reposLink } = this.props;
|
||||
if (namespace) {
|
||||
return namespaces?._embedded.namespaces.find(n => n.namespace === namespace)?._links?.repositories?.href;
|
||||
return (namespaces?._embedded.namespaces.find(n => n.namespace === namespace)?._links?.repositories as Link)
|
||||
?.href;
|
||||
} else {
|
||||
return reposLink;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ import reducer, {
|
||||
MODIFY_REPO_SUCCESS,
|
||||
modifyRepo
|
||||
} from "./repos";
|
||||
import { Repository, RepositoryCollection } from "@scm-manager/ui-types";
|
||||
import { Link, Repository, RepositoryCollection } from "@scm-manager/ui-types";
|
||||
|
||||
const hitchhikerPuzzle42: Repository = {
|
||||
contact: "fourtytwo@hitchhiker.com",
|
||||
@@ -431,7 +431,7 @@ describe("repos fetch", () => {
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createRepo(URL, slartiFjords)).then(() => {
|
||||
return store.dispatch(createRepo(URL, { ...slartiFjords, contextEntries: {} }, false)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -454,7 +454,7 @@ describe("repos fetch", () => {
|
||||
};
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createRepo(URL, slartiFjords, false, callback)).then(() => {
|
||||
return store.dispatch(createRepo(URL, { ...slartiFjords, contextEntries: {} }, false, callback)).then(() => {
|
||||
expect(callMe).toBe("yeah");
|
||||
});
|
||||
});
|
||||
@@ -465,7 +465,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createRepo(URL, slartiFjords, false)).then(() => {
|
||||
return store.dispatch(createRepo(URL, { ...slartiFjords, contextEntries: {} }, false)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_REPO_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_REPO_FAILURE);
|
||||
@@ -530,7 +530,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
|
||||
it("should successfully modify slarti/fjords repo", () => {
|
||||
fetchMock.putOnce(slartiFjords._links.update.href, {
|
||||
fetchMock.putOnce((slartiFjords._links.update as Link).href, {
|
||||
status: 204
|
||||
});
|
||||
fetchMock.getOnce("http://localhost:8081/api/v2/repositories/slarti/fjords", {
|
||||
@@ -553,7 +553,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
|
||||
it("should successfully modify slarti/fjords repo and call the callback", () => {
|
||||
fetchMock.putOnce(slartiFjords._links.update.href, {
|
||||
fetchMock.putOnce((slartiFjords._links.update as Link).href, {
|
||||
status: 204
|
||||
});
|
||||
fetchMock.getOnce("http://localhost:8081/api/v2/repositories/slarti/fjords", {
|
||||
@@ -582,7 +582,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
|
||||
it("should fail modifying on HTTP 500", () => {
|
||||
fetchMock.putOnce(slartiFjords._links.update.href, {
|
||||
fetchMock.putOnce((slartiFjords._links.update as Link).href, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import * as types from "../../modules/types";
|
||||
import {
|
||||
Action, Namespace,
|
||||
Action,
|
||||
Link,
|
||||
Namespace,
|
||||
NamespaceCollection,
|
||||
Repository,
|
||||
RepositoryCollection,
|
||||
@@ -178,7 +180,7 @@ export function fetchNamespacesFailure(err: Error): Action {
|
||||
|
||||
// fetch repo
|
||||
export function fetchRepoByLink(repo: Repository) {
|
||||
return fetchRepo(repo._links.self.href, repo.namespace, repo.name);
|
||||
return fetchRepo((repo._links.self as Link).href, repo.namespace, repo.name);
|
||||
}
|
||||
|
||||
export function fetchRepoByName(link: string, namespace: string, name: string) {
|
||||
@@ -248,6 +250,7 @@ export function createRepo(
|
||||
.then(response => {
|
||||
const location = response.headers.get("Location");
|
||||
dispatch(createRepoSuccess());
|
||||
// @ts-ignore Location is always set if the repository creation was successful
|
||||
return apiClient.get(location);
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -294,7 +297,7 @@ export function modifyRepo(repository: Repository, callback?: () => void) {
|
||||
dispatch(modifyRepoPending(repository));
|
||||
|
||||
return apiClient
|
||||
.put(repository._links.update.href, repository, CONTENT_TYPE)
|
||||
.put((repository._links.update as Link).href, repository, CONTENT_TYPE)
|
||||
.then(() => {
|
||||
dispatch(modifyRepoSuccess(repository));
|
||||
if (callback) {
|
||||
@@ -353,7 +356,7 @@ export function deleteRepo(repository: Repository, callback?: () => void) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(deleteRepoPending(repository));
|
||||
return apiClient
|
||||
.delete(repository._links.delete.href)
|
||||
.delete((repository._links.delete as Link).href)
|
||||
.then(() => {
|
||||
dispatch(deleteRepoSuccess(repository));
|
||||
if (callback) {
|
||||
|
||||
@@ -1,684 +0,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.
|
||||
*/
|
||||
|
||||
package sonia.scm.api.rest.resources;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.inject.Inject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.FeatureNotSupportedException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.Type;
|
||||
import sonia.scm.api.rest.RestActionUploadResult;
|
||||
import sonia.scm.api.v2.resources.RepositoryResource;
|
||||
import sonia.scm.repository.AdvancedImportHandler;
|
||||
import sonia.scm.repository.ImportHandler;
|
||||
import sonia.scm.repository.ImportResult;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryHandler;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.api.UnbundleCommandBuilder;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.FormParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.GenericEntity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* Rest resource for importing repositories.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
// @Path("import/repositories")
|
||||
public class RepositoryImportResource {
|
||||
|
||||
/**
|
||||
* the logger for RepositoryImportResource
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(RepositoryImportResource.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs a new repository import resource.
|
||||
*
|
||||
* @param manager repository manager
|
||||
* @param serviceFactory
|
||||
*/
|
||||
@Inject
|
||||
public RepositoryImportResource(RepositoryManager manager,
|
||||
RepositoryServiceFactory serviceFactory) {
|
||||
this.manager = manager;
|
||||
this.serviceFactory = serviceFactory;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Imports a repository type specific bundle. The bundle file is uploaded to
|
||||
* the server which is running scm-manager. After the upload has finished, the
|
||||
* bundle file is passed to the {@link UnbundleCommandBuilder}. <strong>Note:</strong> This method
|
||||
* requires admin privileges.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
* @param inputStream input bundle
|
||||
* @param compressed true if the bundle is gzip compressed
|
||||
* @return empty response with location header which points to the imported repository
|
||||
* @since 1.43
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/bundle")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response importFromBundle(@Context UriInfo uriInfo,
|
||||
@PathParam("type") String type, @FormParam("name") String name,
|
||||
@FormParam("bundle") InputStream inputStream, @QueryParam("compressed")
|
||||
@DefaultValue("false") boolean compressed) {
|
||||
Repository repository = doImportFromBundle(type, name, inputStream,
|
||||
compressed);
|
||||
|
||||
return buildResponse(uriInfo, repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method works exactly like
|
||||
* {@link #importFromBundle(UriInfo, String, String, InputStream)}, but this
|
||||
* method returns an html content-type. The method exists only for a
|
||||
* workaround of the javascript ui extjs. <strong>Note:</strong> This method requires admin
|
||||
* privileges.
|
||||
*
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
* @param inputStream input bundle
|
||||
* @param compressed true if the bundle is gzip compressed
|
||||
* @return empty response with location header which points to the imported
|
||||
* repository
|
||||
* @since 1.43
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/bundle.html")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public Response importFromBundleUI(@PathParam("type") String type,
|
||||
@FormParam("name") String name,
|
||||
@FormParam("bundle") InputStream inputStream, @QueryParam("compressed")
|
||||
@DefaultValue("false") boolean compressed) {
|
||||
Response response;
|
||||
|
||||
try {
|
||||
doImportFromBundle(type, name, inputStream, compressed);
|
||||
response = Response.ok(new RestActionUploadResult(true)).build();
|
||||
} catch (WebApplicationException ex) {
|
||||
logger.warn("error durring bundle import", ex);
|
||||
response = Response.fromResponse(ex.getResponse()).entity(
|
||||
new RestActionUploadResult(false)).build();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a external repository which is accessible via url. The method can
|
||||
* only be used, if the repository type supports the {@link Command#PULL}. The
|
||||
* method will return a location header with the url to the imported
|
||||
* repository. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param type repository type
|
||||
* @param request request object
|
||||
* @return empty response with location header which points to the imported
|
||||
* repository
|
||||
* @since 1.43
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/url")
|
||||
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||
public Response importFromUrl(@Context UriInfo uriInfo,
|
||||
@PathParam("type") String type, UrlImportRequest request) {
|
||||
RepositoryPermissions.create().check();
|
||||
checkNotNull(request, "request is required");
|
||||
checkArgument(!Strings.isNullOrEmpty(request.getName()),
|
||||
"request does not contain name of the repository");
|
||||
checkArgument(!Strings.isNullOrEmpty(request.getUrl()),
|
||||
"request does not contain url of the remote repository");
|
||||
|
||||
Type t = type(type);
|
||||
|
||||
checkSupport(t, Command.PULL, request);
|
||||
|
||||
logger.info("start {} import for external url {}", type, request.getUrl());
|
||||
|
||||
Repository repository = create(type, request.getName());
|
||||
RepositoryService service = null;
|
||||
|
||||
try {
|
||||
service = serviceFactory.create(repository);
|
||||
service.getPullCommand().pull(request.getUrl());
|
||||
} catch (IOException ex) {
|
||||
handleImportFailure(ex, repository);
|
||||
} finally {
|
||||
IOUtil.close(service);
|
||||
}
|
||||
|
||||
return buildResponse(uriInfo, repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports repositories of the given type from the configured repository
|
||||
* directory. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param type repository type
|
||||
* @return imported repositories
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}")
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||
public Response importRepositories(@PathParam("type") String type) {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
List<Repository> repositories = new ArrayList<Repository>();
|
||||
|
||||
importFromDirectory(repositories, type);
|
||||
|
||||
//J-
|
||||
return Response.ok(
|
||||
new GenericEntity<List<Repository>>(repositories) {
|
||||
}
|
||||
).build();
|
||||
//J+
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports repositories of all supported types from the configured repository
|
||||
* directories. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @return imported repositories
|
||||
*/
|
||||
@POST
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||
public Response importRepositories() {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
logger.info("start directory import for all supported repository types");
|
||||
|
||||
List<Repository> repositories = new ArrayList<Repository>();
|
||||
|
||||
for (Type t : findImportableTypes()) {
|
||||
importFromDirectory(repositories, t.getName());
|
||||
}
|
||||
|
||||
//J-
|
||||
return Response.ok(
|
||||
new GenericEntity<List<Repository>>(repositories) {
|
||||
}
|
||||
).build();
|
||||
//J+
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports repositories of the given type from the configured repository
|
||||
* directory. Returns a list of successfully imported directories and a list
|
||||
* of failed directories. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param type repository type
|
||||
* @return imported repositories
|
||||
* @since 1.43
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/directory")
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||
public Response importRepositoriesFromDirectory(
|
||||
@PathParam("type") String type) {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
Response response;
|
||||
|
||||
RepositoryHandler handler = manager.getHandler(type);
|
||||
|
||||
if (handler != null) {
|
||||
logger.info("start directory import for repository type {}", type);
|
||||
|
||||
try {
|
||||
ImportResult result;
|
||||
ImportHandler importHandler = handler.getImportHandler();
|
||||
|
||||
if (importHandler instanceof AdvancedImportHandler) {
|
||||
logger.debug("start directory import, using advanced import handler");
|
||||
result =
|
||||
((AdvancedImportHandler) importHandler)
|
||||
.importRepositoriesFromDirectory(manager);
|
||||
} else {
|
||||
logger.debug("start directory import, using normal import handler");
|
||||
result = new ImportResult(importHandler.importRepositories(manager),
|
||||
ImmutableList.<String>of());
|
||||
}
|
||||
|
||||
response = Response.ok(result).build();
|
||||
} catch (FeatureNotSupportedException ex) {
|
||||
logger
|
||||
.warn(
|
||||
"import feature is not supported by repository handler for type "
|
||||
.concat(type), ex);
|
||||
response = Response.status(Response.Status.BAD_REQUEST).build();
|
||||
} catch (IOException ex) {
|
||||
logger.warn("exception occured durring directory import", ex);
|
||||
response = Response.serverError().build();
|
||||
}
|
||||
} else {
|
||||
logger.warn("could not find reposiotry handler for type {}", type);
|
||||
response = Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns a list of repository types, which support the directory import
|
||||
* feature. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @return list of repository types
|
||||
*/
|
||||
@GET
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||
public Response getImportableTypes() {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
List<Type> types = findImportableTypes();
|
||||
|
||||
//J-
|
||||
return Response.ok(
|
||||
new GenericEntity<List<Type>>(types) {
|
||||
}
|
||||
).build();
|
||||
//J+
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build rest response for repository.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param repository imported repository
|
||||
* @return rest response
|
||||
*/
|
||||
private Response buildResponse(UriInfo uriInfo, Repository repository) {
|
||||
URI location = uriInfo.getBaseUriBuilder().path(
|
||||
RepositoryResource.class).path(repository.getId()).build();
|
||||
|
||||
return Response.created(location).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check repository type for support for the given command.
|
||||
*
|
||||
* @param type repository type
|
||||
* @param cmd command
|
||||
* @param request request object
|
||||
*/
|
||||
private void checkSupport(Type type, Command cmd, Object request) {
|
||||
if (!(type instanceof RepositoryType)) {
|
||||
logger.warn("type {} is not a repository type", type.getName());
|
||||
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
Set<Command> cmds = ((RepositoryType) type).getSupportedCommands();
|
||||
|
||||
if (!cmds.contains(cmd)) {
|
||||
logger.warn("type {} does not support this type of import: {}",
|
||||
type.getName(), request);
|
||||
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new repository with the given name and type.
|
||||
*
|
||||
* @param type repository type
|
||||
* @param name repository name
|
||||
* @return newly created repository
|
||||
*/
|
||||
private Repository create(String type, String name) {
|
||||
Repository repository = null;
|
||||
|
||||
try {
|
||||
// TODO #8783
|
||||
// repository = new Repository(null, type, name);
|
||||
manager.create(repository);
|
||||
} catch (InternalRepositoryException ex) {
|
||||
handleGenericCreationFailure(ex, type, name);
|
||||
}
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start bundle import.
|
||||
*
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
* @param inputStream bundle stream
|
||||
* @param compressed true if the bundle is gzip compressed
|
||||
* @return imported repository
|
||||
*/
|
||||
private Repository doImportFromBundle(String type, String name,
|
||||
InputStream inputStream, boolean compressed) {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
checkArgument(!Strings.isNullOrEmpty(name),
|
||||
"request does not contain name of the repository");
|
||||
checkNotNull(inputStream, "bundle inputStream is required");
|
||||
|
||||
Repository repository;
|
||||
|
||||
try {
|
||||
Type t = type(type);
|
||||
|
||||
checkSupport(t, Command.UNBUNDLE, "bundle");
|
||||
|
||||
repository = create(type, name);
|
||||
|
||||
RepositoryService service = null;
|
||||
|
||||
File file = File.createTempFile("scm-import-", ".bundle");
|
||||
|
||||
try {
|
||||
long length = Files.asByteSink(file).writeFrom(inputStream);
|
||||
|
||||
logger.info("copied {} bytes to temp, start bundle import", length);
|
||||
service = serviceFactory.create(repository);
|
||||
service.getUnbundleCommand().setCompressed(compressed).unbundle(file);
|
||||
} catch (InternalRepositoryException ex) {
|
||||
handleImportFailure(ex, repository);
|
||||
} finally {
|
||||
IOUtil.close(service);
|
||||
IOUtil.delete(file);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
logger.warn("could not create temporary file", ex);
|
||||
|
||||
throw new WebApplicationException(ex);
|
||||
}
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private List<Type> findImportableTypes() {
|
||||
List<Type> types = new ArrayList<Type>();
|
||||
Collection<Type> handlerTypes = manager.getTypes();
|
||||
|
||||
for (Type t : handlerTypes) {
|
||||
RepositoryHandler handler = manager.getHandler(t.getName());
|
||||
|
||||
if (handler != null) {
|
||||
try {
|
||||
if (handler.getImportHandler() != null) {
|
||||
types.add(t);
|
||||
}
|
||||
} catch (FeatureNotSupportedException ex) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("import handler is not supported", ex);
|
||||
} else if (logger.isInfoEnabled()) {
|
||||
logger.info("{} handler does not support import of repositories",
|
||||
t.getName());
|
||||
}
|
||||
}
|
||||
} else if (logger.isWarnEnabled()) {
|
||||
logger.warn("could not find handler for type {}", t.getName());
|
||||
}
|
||||
}
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle creation failures.
|
||||
*
|
||||
* @param ex exception
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
*/
|
||||
private void handleGenericCreationFailure(Exception ex, String type,
|
||||
String name) {
|
||||
logger.error(String.format("could not create repository %s with type %s",
|
||||
type, name), ex);
|
||||
|
||||
throw new WebApplicationException(ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle import failures.
|
||||
*
|
||||
* @param ex exception
|
||||
* @param repository repository
|
||||
*/
|
||||
private void handleImportFailure(Exception ex, Repository repository) {
|
||||
logger.error("import for repository failed, delete repository", ex);
|
||||
|
||||
try {
|
||||
manager.delete(repository);
|
||||
} catch (InternalRepositoryException | NotFoundException e) {
|
||||
logger.error("can not delete repository after import failure", e);
|
||||
}
|
||||
|
||||
throw new WebApplicationException(ex,
|
||||
Response.Status.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import repositories from a specific type.
|
||||
*
|
||||
* @param repositories repository list
|
||||
* @param type type of repository
|
||||
*/
|
||||
private void importFromDirectory(List<Repository> repositories, String type) {
|
||||
RepositoryHandler handler = manager.getHandler(type);
|
||||
|
||||
if (handler != null) {
|
||||
logger.info("start directory import for repository type {}", type);
|
||||
|
||||
try {
|
||||
List<String> repositoryNames =
|
||||
handler.getImportHandler().importRepositories(manager);
|
||||
|
||||
if (repositoryNames != null) {
|
||||
for (String repositoryName : repositoryNames) {
|
||||
// TODO #8783
|
||||
/*Repository repository = null; //manager.get(type, repositoryName);
|
||||
|
||||
if (repository != null)
|
||||
{
|
||||
repositories.add(repository);
|
||||
}
|
||||
else if (logger.isWarnEnabled())
|
||||
{
|
||||
logger.warn("could not find imported repository {}",
|
||||
repositoryName);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
} catch (FeatureNotSupportedException ex) {
|
||||
throw new WebApplicationException(ex, Response.Status.BAD_REQUEST);
|
||||
} catch (IOException ex) {
|
||||
throw new WebApplicationException(ex);
|
||||
} catch (InternalRepositoryException ex) {
|
||||
throw new WebApplicationException(ex);
|
||||
}
|
||||
} else {
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
* @param type
|
||||
* @return
|
||||
*/
|
||||
private Type type(String type) {
|
||||
RepositoryHandler handler = manager.getHandler(type);
|
||||
|
||||
if (handler == null) {
|
||||
logger.warn("no handler for type {} found", type);
|
||||
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
}
|
||||
|
||||
return handler.getType();
|
||||
}
|
||||
|
||||
//~--- inner classes --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Request for importing external repositories which are accessible via url.
|
||||
*/
|
||||
@XmlRootElement(name = "import")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class UrlImportRequest {
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*/
|
||||
public UrlImportRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link UrlImportRequest}
|
||||
*
|
||||
* @param name name of the repository
|
||||
* @param url external url of the repository
|
||||
*/
|
||||
public UrlImportRequest(String name, String url) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
//~--- methods ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
//J-
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("name", name)
|
||||
.add("url", url)
|
||||
.toString();
|
||||
//J+
|
||||
}
|
||||
|
||||
//~--- get methods --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns name of the repository.
|
||||
*
|
||||
* @return name of the repository
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns external url of the repository.
|
||||
*
|
||||
* @return external url of the repository
|
||||
*/
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
//~--- fields -------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* name of the repository
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* external url of the repository
|
||||
*/
|
||||
private String url;
|
||||
}
|
||||
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* repository manager
|
||||
*/
|
||||
private final RepositoryManager manager;
|
||||
|
||||
/**
|
||||
* repository service factory
|
||||
*/
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.mapstruct.*;
|
||||
@@ -30,6 +30,10 @@ import sonia.scm.repository.Repository;
|
||||
@Mapper
|
||||
public abstract class RepositoryDtoToRepositoryMapper extends BaseDtoMapper {
|
||||
|
||||
public Repository map(RepositoryDto repositoryDto) {
|
||||
return map(repositoryDto, null);
|
||||
}
|
||||
|
||||
@Mapping(target = "creationDate", ignore = true)
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "healthCheckFailures", ignore = true)
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* 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 com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.inject.Inject;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.Links;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.Type;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryHandler;
|
||||
import sonia.scm.repository.RepositoryImportEvent;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.PullCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.util.ValidationUtil;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
public class RepositoryImportResource {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RepositoryImportResource.class);
|
||||
|
||||
private final RepositoryManager manager;
|
||||
private final RepositoryDtoToRepositoryMapper mapper;
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
private final ResourceLinks resourceLinks;
|
||||
private final ScmEventBus eventBus;
|
||||
|
||||
@Inject
|
||||
public RepositoryImportResource(RepositoryManager manager,
|
||||
RepositoryDtoToRepositoryMapper mapper,
|
||||
RepositoryServiceFactory serviceFactory,
|
||||
ResourceLinks resourceLinks,
|
||||
ScmEventBus eventBus) {
|
||||
this.manager = manager;
|
||||
this.mapper = mapper;
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.resourceLinks = resourceLinks;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a external repository which is accessible via url. The method can
|
||||
* only be used, if the repository type supports the {@link Command#PULL}. The
|
||||
* method will return a location header with the url to the imported
|
||||
* repository.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param type repository type
|
||||
* @param request request object
|
||||
* @return empty response with location header which points to the imported
|
||||
* repository
|
||||
* @since 2.11.0
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/url")
|
||||
@Consumes(VndMediaType.REPOSITORY)
|
||||
@Operation(summary = "Import repository from url", description = "Imports the repository for the given url.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "Repository import was successful",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY,
|
||||
schema = @Schema(implementation = ImportRepositoryDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "401",
|
||||
description = "not authenticated / invalid credentials"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "not authorized, the current user has no privileges to read the repository"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response importFromUrl(@Context UriInfo uriInfo,
|
||||
@PathParam("type") String type, @Valid RepositoryImportDto request) {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
Type t = type(type);
|
||||
if (!t.getName().equals(request.getType())) {
|
||||
throw new WebApplicationException("type of import url and repository does not match", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
checkSupport(t, Command.PULL, request);
|
||||
|
||||
logger.info("start {} import for external url {}", type, request.getImportUrl());
|
||||
|
||||
Repository repository = mapper.map(request);
|
||||
repository.setPermissions(singletonList(new RepositoryPermission(SecurityUtils.getSubject().getPrincipal().toString(), "OWNER", false)));
|
||||
|
||||
try {
|
||||
repository = manager.create(
|
||||
repository,
|
||||
pullChangesFromRemoteUrl(request)
|
||||
);
|
||||
eventBus.post(new RepositoryImportEvent(HandlerEventType.MODIFY, repository, false));
|
||||
|
||||
return Response.created(URI.create(resourceLinks.repository().self(repository.getNamespace(), repository.getName()))).build();
|
||||
} catch (Exception e) {
|
||||
eventBus.post(new RepositoryImportEvent(HandlerEventType.MODIFY, repository, true));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Consumer<Repository> pullChangesFromRemoteUrl(RepositoryImportDto request) {
|
||||
return repository -> {
|
||||
try (RepositoryService service = serviceFactory.create(repository)) {
|
||||
PullCommandBuilder pullCommand = service.getPullCommand();
|
||||
if (!Strings.isNullOrEmpty(request.getUsername()) && !Strings.isNullOrEmpty(request.getPassword())) {
|
||||
pullCommand
|
||||
.withUsername(request.getUsername())
|
||||
.withPassword(request.getPassword());
|
||||
}
|
||||
|
||||
pullCommand.pull(request.getImportUrl());
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(repository, "Failed to import from remote url", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check repository type for support for the given command.
|
||||
*
|
||||
* @param type repository type
|
||||
* @param cmd command
|
||||
* @param request request object
|
||||
*/
|
||||
private void checkSupport(Type type, Command cmd, Object request) {
|
||||
if (!(type instanceof RepositoryType)) {
|
||||
logger.warn("type {} is not a repository type", type.getName());
|
||||
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
Set<Command> cmds = ((RepositoryType) type).getSupportedCommands();
|
||||
|
||||
if (!cmds.contains(cmd)) {
|
||||
logger.warn("type {} does not support this type of import: {}",
|
||||
type.getName(), request);
|
||||
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private Type type(String type) {
|
||||
RepositoryHandler handler = manager.getHandler(type);
|
||||
|
||||
if (handler == null) {
|
||||
logger.warn("no handler for type {} found", type);
|
||||
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
}
|
||||
|
||||
return handler.getType();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("java:S2160")
|
||||
public static class RepositoryImportDto extends RepositoryDto implements ImportRepositoryDto {
|
||||
@NotEmpty
|
||||
private String importUrl;
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
RepositoryImportDto(Links links, Embedded embedded) {
|
||||
super(links, embedded);
|
||||
}
|
||||
}
|
||||
|
||||
interface ImportRepositoryDto {
|
||||
String getNamespace();
|
||||
@Pattern(regexp = ValidationUtil.REGEX_REPOSITORYNAME)
|
||||
String getName();
|
||||
@NotEmpty
|
||||
String getType();
|
||||
@Email
|
||||
String getContact();
|
||||
String getDescription();
|
||||
@NotEmpty
|
||||
String getImportUrl();
|
||||
String getUsername();
|
||||
String getPassword();
|
||||
}
|
||||
}
|
||||
@@ -34,22 +34,22 @@ import javax.ws.rs.Path;
|
||||
/**
|
||||
* RESTful Web Service Resource to manage repositories.
|
||||
*/
|
||||
@OpenAPIDefinition(
|
||||
tags = {
|
||||
@Tag(name = "Repository", description = "Repository related endpoints")
|
||||
}
|
||||
)
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Repository", description = "Repository related endpoints")
|
||||
})
|
||||
@Path(RepositoryRootResource.REPOSITORIES_PATH_V2)
|
||||
public class RepositoryRootResource {
|
||||
static final String REPOSITORIES_PATH_V2 = "v2/repositories/";
|
||||
|
||||
private final Provider<RepositoryResource> repositoryResource;
|
||||
private final Provider<RepositoryCollectionResource> repositoryCollectionResource;
|
||||
private final Provider<RepositoryImportResource> repositoryImportResource;
|
||||
|
||||
@Inject
|
||||
public RepositoryRootResource(Provider<RepositoryResource> repositoryResource, Provider<RepositoryCollectionResource> repositoryCollectionResource) {
|
||||
public RepositoryRootResource(Provider<RepositoryResource> repositoryResource, Provider<RepositoryCollectionResource> repositoryCollectionResource, Provider<RepositoryImportResource> repositoryImportResource) {
|
||||
this.repositoryResource = repositoryResource;
|
||||
this.repositoryCollectionResource = repositoryCollectionResource;
|
||||
this.repositoryImportResource = repositoryImportResource;
|
||||
}
|
||||
|
||||
@Path("{namespace}/{name}")
|
||||
@@ -61,4 +61,9 @@ public class RepositoryRootResource {
|
||||
public RepositoryCollectionResource getRepositoryCollectionResource() {
|
||||
return repositoryCollectionResource.get();
|
||||
}
|
||||
|
||||
@Path("import")
|
||||
public RepositoryImportResource getRepositoryImportResource() {
|
||||
return repositoryImportResource.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,17 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.repository.api.Command;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -43,6 +46,11 @@ public abstract class RepositoryTypeToRepositoryTypeDtoMapper extends BaseMapper
|
||||
@AfterMapping
|
||||
void appendLinks(RepositoryType repositoryType, @MappingTarget RepositoryTypeDto target) {
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryType().self(repositoryType.getName()));
|
||||
|
||||
if (RepositoryPermissions.create().isPermitted() && repositoryType.getSupportedCommands().contains(Command.PULL)) {
|
||||
linksBuilder.array(Link.linkBuilder("import", resourceLinks.repository().importFromUrl(repositoryType.getName())).withName("url").build());
|
||||
}
|
||||
|
||||
target.add(linksBuilder.build());
|
||||
}
|
||||
|
||||
|
||||
@@ -334,9 +334,11 @@ class ResourceLinks {
|
||||
|
||||
static class RepositoryLinks {
|
||||
private final LinkBuilder repositoryLinkBuilder;
|
||||
private final LinkBuilder repositoryImportLinkBuilder;
|
||||
|
||||
RepositoryLinks(ScmPathInfo pathInfo) {
|
||||
repositoryLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class);
|
||||
repositoryImportLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryImportResource.class);
|
||||
}
|
||||
|
||||
String self(String namespace, String name) {
|
||||
@@ -354,6 +356,10 @@ class ResourceLinks {
|
||||
String rename(String namespace, String name) {
|
||||
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("rename").parameters().href();
|
||||
}
|
||||
|
||||
String importFromUrl(String type) {
|
||||
return repositoryImportLinkBuilder.method("getRepositoryImportResource").parameters().method("importFromUrl").parameters(type).href();
|
||||
}
|
||||
}
|
||||
|
||||
RepositoryCollectionLinks repositoryCollection() {
|
||||
|
||||
@@ -41,6 +41,8 @@ import sonia.scm.NotFoundException;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.Type;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.security.AuthorizationChangedEvent;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.util.AssertUtil;
|
||||
import sonia.scm.util.CollectionAppender;
|
||||
@@ -59,6 +61,7 @@ import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
@@ -123,10 +126,16 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
|
||||
@Override
|
||||
public Repository create(Repository repository) {
|
||||
return create(repository, true);
|
||||
return create(repository, r -> {
|
||||
}, true);
|
||||
}
|
||||
|
||||
public Repository create(Repository repository, boolean initRepository) {
|
||||
@Override
|
||||
public Repository create(Repository repository, Consumer<Repository> afterCreation) {
|
||||
return create(repository, afterCreation, true);
|
||||
}
|
||||
|
||||
public Repository create(Repository repository, Consumer<Repository> afterCreation, boolean initRepository) {
|
||||
repository.setId(keyGenerator.createKey());
|
||||
repository.setNamespace(namespaceStrategyProvider.get().createNamespace(repository));
|
||||
|
||||
@@ -137,15 +146,20 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
RepositoryPermissions::create,
|
||||
newRepository -> fireEvent(HandlerEventType.BEFORE_CREATE, newRepository),
|
||||
newRepository -> {
|
||||
fireEvent(HandlerEventType.CREATE, newRepository);
|
||||
if (initRepository) {
|
||||
try {
|
||||
getHandler(newRepository).create(newRepository);
|
||||
} catch (InternalRepositoryException e) {
|
||||
invalidateRepositoryPermissions();
|
||||
afterCreation.accept(newRepository);
|
||||
} catch (Exception e) {
|
||||
delete(repository);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
invalidateRepositoryPermissions();
|
||||
afterCreation.accept(newRepository);
|
||||
}
|
||||
fireEvent(HandlerEventType.CREATE, newRepository);
|
||||
},
|
||||
newRepository -> {
|
||||
if (repositoryDAO.contains(newRepository.getNamespaceAndName())) {
|
||||
@@ -155,6 +169,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
);
|
||||
}
|
||||
|
||||
private void invalidateRepositoryPermissions() {
|
||||
ScmEventBus.getInstance().post(AuthorizationChangedEvent.createForEveryUser());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Repository repository) {
|
||||
logger.info("delete repository {}/{} of type {}", repository.getNamespace(), repository.getName(), repository.getType());
|
||||
@@ -173,7 +191,8 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
|
||||
@Override
|
||||
public void importRepository(Repository repository) {
|
||||
create(repository, false);
|
||||
create(repository, r -> {
|
||||
}, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -193,7 +193,8 @@ public class AuthorizationChangedEventProducer {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAuthorizationDataModified(Collection<RepositoryPermission> newPermissions, Collection<RepositoryPermission> permissionsBeforeModification) {
|
||||
private boolean isAuthorizationDataModified
|
||||
(Collection<RepositoryPermission> newPermissions, Collection<RepositoryPermission> permissionsBeforeModification) {
|
||||
return !(newPermissions.containsAll(permissionsBeforeModification) && permissionsBeforeModification.containsAll(newPermissions));
|
||||
}
|
||||
|
||||
@@ -201,7 +202,7 @@ public class AuthorizationChangedEventProducer {
|
||||
sendEvent(AuthorizationChangedEvent.createForEveryUser());
|
||||
}
|
||||
|
||||
private void handleRepositoryEvent(RepositoryEvent event){
|
||||
private void handleRepositoryEvent(RepositoryEvent event) {
|
||||
logger.debug(
|
||||
"fire authorization changed event, because of received {} event for repository {}",
|
||||
event.getEventType(), event.getItem().getName()
|
||||
@@ -237,8 +238,8 @@ public class AuthorizationChangedEventProducer {
|
||||
|
||||
private void handleUserPermissionChange(AssignedPermission permission) {
|
||||
logger.debug(
|
||||
"fire authorization changed event for user {}, because permission {} has changed",
|
||||
permission.getName(), permission.getPermission()
|
||||
"fire authorization changed event for user {}, because permission {} has changed",
|
||||
permission.getName(), permission.getPermission()
|
||||
);
|
||||
fireEventForUser(permission.getName());
|
||||
}
|
||||
@@ -281,7 +282,7 @@ public class AuthorizationChangedEventProducer {
|
||||
return !group.getMembers().equals(beforeModification.getMembers());
|
||||
}
|
||||
|
||||
private void handleGroupEvent(GroupEvent event){
|
||||
private void handleGroupEvent(GroupEvent event) {
|
||||
logger.debug(
|
||||
"fire authorization changed event, because of received group event {} for group {}",
|
||||
event.getEventType(),
|
||||
@@ -294,5 +295,4 @@ public class AuthorizationChangedEventProducer {
|
||||
protected void sendEvent(AuthorizationChangedEvent event) {
|
||||
ScmEventBus.getInstance().post(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -298,9 +298,9 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
@Subscribe(async = false)
|
||||
public void invalidateCache(AuthorizationChangedEvent event) {
|
||||
if (event.isEveryUserAffected()) {
|
||||
invalidateUserCache(event.getNameOfAffectedUser());
|
||||
} else {
|
||||
invalidateCache();
|
||||
} else {
|
||||
invalidateUserCache(event.getNameOfAffectedUser());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -286,12 +286,16 @@
|
||||
"FVS9JY1T21": {
|
||||
"displayName": "Fehler bei der Anfrage",
|
||||
"description": "Bei der Anfrage trat ein Fehler auf. Prüfen Sie bitte den Status der HTTP Antwort und die konkrete Meldung."
|
||||
},
|
||||
"D6SHRfqQw1": {
|
||||
"displayName": "Repository Import fehlgeschlagen",
|
||||
"description": "Das Repository konnte nicht importiert werden. Möglicherweise wurden die Zugangsdaten (Benutzername/Passwort) nicht gesetzt oder sind fehlerhaft. Bitte prüfen Sie Ihre Eingaben."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
"UsernameNamespaceStrategy": "Benutzername",
|
||||
"CustomNamespaceStrategy": "Benutzerdefiniert",
|
||||
"CurrentYearNamespaceStrategy": "Aktuelles Jahr",
|
||||
"UsernameNamespaceStrategy": "Benutzername",
|
||||
"CustomNamespaceStrategy": "Benutzerdefiniert",
|
||||
"CurrentYearNamespaceStrategy": "Aktuelles Jahr",
|
||||
"RepositoryTypeNamespaceStrategy": "Repository Typ"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +286,10 @@
|
||||
"FVS9JY1T21": {
|
||||
"displayName": "Error in the request",
|
||||
"description": "While processing the request there was an error. Please check the http return status and the concrete error message."
|
||||
},
|
||||
"D6SHRfqQw1": {
|
||||
"displayName": "Repository import failed",
|
||||
"description": "The repository could not be imported. It's likely that either the credentials (username/password) are wrong or missing. Please check your inputs."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -39,14 +39,23 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.repository.CustomNamespaceStrategy;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryHandler;
|
||||
import sonia.scm.repository.RepositoryImportEvent;
|
||||
import sonia.scm.repository.RepositoryInitializer;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.ImportFailedException;
|
||||
import sonia.scm.repository.api.PullCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.user.User;
|
||||
@@ -60,24 +69,31 @@ import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.stream.Stream.of;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyObject;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.RETURNS_SELF;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -92,7 +108,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
private static final String REALM = "AdminRealm";
|
||||
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
private final RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
@@ -104,6 +120,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
@Mock
|
||||
private RepositoryService service;
|
||||
@Mock
|
||||
private RepositoryHandler repositoryHandler;
|
||||
@Mock
|
||||
private ScmPathInfoStore scmPathInfoStore;
|
||||
@Mock
|
||||
private ScmPathInfo uriInfo;
|
||||
@@ -113,6 +131,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
private ScmConfiguration configuration;
|
||||
@Mock
|
||||
private Set<NamespaceStrategy> strategies;
|
||||
@Mock
|
||||
private ScmEventBus eventBus;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Predicate<Repository>> filterCaptor;
|
||||
@@ -133,6 +153,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
super.manager = repositoryManager;
|
||||
RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
|
||||
super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer);
|
||||
super.repositoryImportResource = new RepositoryImportResource(repositoryManager, dtoToRepositoryMapper, serviceFactory, resourceLinks, eventBus);
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
|
||||
when(scmPathInfoStore.get()).thenReturn(uriInfo);
|
||||
@@ -443,6 +464,107 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
verify(repositoryManager).rename(repository1, "space", "x");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldImportRepositoryFromUrl() throws URISyntaxException, IOException {
|
||||
ArgumentCaptor<RepositoryImportEvent> captor = ArgumentCaptor.forClass(RepositoryImportEvent.class);
|
||||
when(manager.getHandler("git")).thenReturn(repositoryHandler);
|
||||
when(repositoryHandler.getType()).thenReturn(new RepositoryType("git", "git", ImmutableSet.of(Command.PULL)));
|
||||
when(manager.create(any(Repository.class), any())).thenReturn(RepositoryTestData.create42Puzzle());
|
||||
|
||||
URL url = Resources.getResource("sonia/scm/api/v2/import-repo.json");
|
||||
byte[] importRequest = Resources.toByteArray(url);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/git/url")
|
||||
.contentType(VndMediaType.REPOSITORY)
|
||||
.content(importRequest);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_CREATED, response.getStatus());
|
||||
verify(eventBus).post(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().isFailed()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailOnImportRepositoryFromUrl() throws URISyntaxException, IOException {
|
||||
ArgumentCaptor<RepositoryImportEvent> captor = ArgumentCaptor.forClass(RepositoryImportEvent.class);
|
||||
when(manager.getHandler("git")).thenReturn(repositoryHandler);
|
||||
when(repositoryHandler.getType()).thenReturn(new RepositoryType("git", "git", ImmutableSet.of(Command.PULL)));
|
||||
doThrow(ImportFailedException.class).when(manager).create(any(Repository.class), any());
|
||||
|
||||
URL url = Resources.getResource("sonia/scm/api/v2/import-repo.json");
|
||||
byte[] importRequest = Resources.toByteArray(url);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/git/url")
|
||||
.contentType(VndMediaType.REPOSITORY)
|
||||
.content(importRequest);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(500, response.getStatus());
|
||||
verify(eventBus).post(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().isFailed()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPullChangesFromRemoteUrl() throws IOException {
|
||||
PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF);
|
||||
when(service.getPullCommand()).thenReturn(pullCommandBuilder);
|
||||
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
RepositoryImportResource.RepositoryImportDto repositoryImportDto = new RepositoryImportResource.RepositoryImportDto();
|
||||
repositoryImportDto.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
repositoryImportDto.setNamespace("scmadmin");
|
||||
repositoryImportDto.setName("scm-manager");
|
||||
|
||||
Consumer<Repository> repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportDto);
|
||||
repositoryConsumer.accept(repository);
|
||||
|
||||
verify(pullCommandBuilder).pull("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPullChangesFromRemoteUrlWithCredentials() {
|
||||
PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF);
|
||||
when(service.getPullCommand()).thenReturn(pullCommandBuilder);
|
||||
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
RepositoryImportResource.RepositoryImportDto repositoryImportDto = new RepositoryImportResource.RepositoryImportDto();
|
||||
repositoryImportDto.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
repositoryImportDto.setNamespace("scmadmin");
|
||||
repositoryImportDto.setName("scm-manager");
|
||||
repositoryImportDto.setUsername("trillian");
|
||||
repositoryImportDto.setPassword("secret");
|
||||
|
||||
Consumer<Repository> repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportDto);
|
||||
repositoryConsumer.accept(repository);
|
||||
|
||||
verify(pullCommandBuilder).withUsername("trillian");
|
||||
verify(pullCommandBuilder).withPassword("secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldThrowImportFailedEvent() throws IOException {
|
||||
PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF);
|
||||
when(service.getPullCommand()).thenReturn(pullCommandBuilder);
|
||||
doThrow(ImportFailedException.class).when(pullCommandBuilder).pull(anyString());
|
||||
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
RepositoryImportResource.RepositoryImportDto repositoryImportDto = new RepositoryImportResource.RepositoryImportDto();
|
||||
repositoryImportDto.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
repositoryImportDto.setNamespace("scmadmin");
|
||||
repositoryImportDto.setName("scm-manager");
|
||||
|
||||
Consumer<Repository> repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportDto);
|
||||
assertThrows(ImportFailedException.class, () -> repositoryConsumer.accept(repository));
|
||||
}
|
||||
|
||||
private PageResult<Repository> createSingletonPageResult(Repository repository) {
|
||||
return new PageResult<>(singletonList(repository), 0);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ abstract class RepositoryTestBase {
|
||||
IncomingRootResource incomingRootResource;
|
||||
RepositoryCollectionResource repositoryCollectionResource;
|
||||
AnnotateResource annotateResource;
|
||||
RepositoryImportResource repositoryImportResource;
|
||||
|
||||
RepositoryRootResource getRepositoryRootResource() {
|
||||
RepositoryBasedResourceProvider repositoryBasedResourceProvider = new RepositoryBasedResourceProvider(
|
||||
@@ -65,6 +66,6 @@ abstract class RepositoryTestBase {
|
||||
dtoToRepositoryMapper,
|
||||
manager,
|
||||
repositoryBasedResourceProvider)),
|
||||
of(repositoryCollectionResource));
|
||||
of(repositoryCollectionResource), of(repositoryImportResource));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,14 @@ import com.google.common.collect.Sets;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Link;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
|
||||
@@ -50,10 +54,13 @@ public class RepositoryTypeCollectionToDtoMapperTest {
|
||||
@SuppressWarnings("unused") // Is injected
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
@InjectMocks
|
||||
private RepositoryTypeToRepositoryTypeDtoMapperImpl mapper;
|
||||
|
||||
private List<RepositoryType> types = Lists.newArrayList(
|
||||
private final List<RepositoryType> types = Lists.newArrayList(
|
||||
new RepositoryType("hk", "Hitchhiker", Sets.newHashSet()),
|
||||
new RepositoryType("hog", "Heart of Gold", Sets.newHashSet())
|
||||
);
|
||||
@@ -63,6 +70,12 @@ public class RepositoryTypeCollectionToDtoMapperTest {
|
||||
@Before
|
||||
public void setUpEnvironment() {
|
||||
collectionMapper = new RepositoryTypeCollectionToDtoMapper(mapper, resourceLinks);
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -27,8 +27,11 @@ package sonia.scm.api.v2.resources;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -55,10 +58,12 @@ import static org.mockito.Mockito.when;
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
public class RepositoryTypeRootResourceTest {
|
||||
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
private final RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Mock
|
||||
private RepositoryManager repositoryManager;
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
@@ -66,13 +71,15 @@ public class RepositoryTypeRootResourceTest {
|
||||
@InjectMocks
|
||||
private RepositoryTypeToRepositoryTypeDtoMapperImpl mapper;
|
||||
|
||||
private List<RepositoryType> types = Lists.newArrayList(
|
||||
private final List<RepositoryType> types = Lists.newArrayList(
|
||||
new RepositoryType("hk", "Hitchhiker", Sets.newHashSet()),
|
||||
new RepositoryType("hog", "Heart of Gold", Sets.newHashSet())
|
||||
);
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
ThreadContext.bind(subject);
|
||||
|
||||
when(repositoryManager.getConfiguredTypes()).thenReturn(types);
|
||||
|
||||
RepositoryTypeCollectionToDtoMapper collectionMapper = new RepositoryTypeCollectionToDtoMapper(mapper, resourceLinks);
|
||||
@@ -82,6 +89,11 @@ public class RepositoryTypeRootResourceTest {
|
||||
dispatcher.addSingletonResource(rootResource);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHaveCollectionVndMediaType() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryTypeRootResource.PATH);
|
||||
|
||||
@@ -21,19 +21,28 @@
|
||||
* 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.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.repository.api.Command;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
public class RepositoryTypeToRepositoryTypeDtoMapperTest {
|
||||
@@ -43,10 +52,23 @@ public class RepositoryTypeToRepositoryTypeDtoMapperTest {
|
||||
@SuppressWarnings("unused") // Is injected
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
@InjectMocks
|
||||
private RepositoryTypeToRepositoryTypeDtoMapperImpl mapper;
|
||||
|
||||
private RepositoryType type = new RepositoryType("hk", "Hitchhiker", Sets.newHashSet());
|
||||
private final RepositoryType type = new RepositoryType("hk", "Hitchhiker", Sets.newHashSet());
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMapSimpleProperties() {
|
||||
@@ -63,4 +85,31 @@ public class RepositoryTypeToRepositoryTypeDtoMapperTest {
|
||||
dto.getLinks().getLinkBy("self").get().getHref()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAppendImportFromUrlLink() {
|
||||
RepositoryType type = new RepositoryType("hk", "Hitchhiker", ImmutableSet.of(Command.PULL));
|
||||
when(subject.isPermitted("repository:create")).thenReturn(true);
|
||||
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertEquals(
|
||||
"https://scm-manager.org/scm/v2/repositories/import/hk/url",
|
||||
dto.getLinks().getLinkBy("import").get().getHref()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotAppendImportFromUrlLinkIfCommandNotSupported() {
|
||||
when(subject.isPermitted("repository:create")).thenReturn(true);
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertFalse(dto.getLinks().getLinkBy("import").isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotAppendImportFromUrlLinkIfNotPermitted() {
|
||||
RepositoryType type = new RepositoryType("hk", "Hitchhiker", ImmutableSet.of(Command.PULL));
|
||||
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertFalse(dto.getLinks().getLinkBy("import").isPresent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"namespace": "hitchhiker",
|
||||
"name": "HeartOfGold",
|
||||
"importUrl": "https://scm-manager-org/scm/repo/secret/puzzle42",
|
||||
"username": "trillian",
|
||||
"password": "secret"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"namespace": "hitchhiker",
|
||||
"name": "HeartOfGold",
|
||||
"type": "git",
|
||||
"importUrl": "https://scm-manager-org/scm/repo/secret/puzzle42"
|
||||
}
|
||||